Task #725: red "external" badge + structured expanded booking details table

- protocol.tsx bookings table: external meeting-type badge changed from
  violet to rose (bg-rose-100 text-rose-700); internal unchanged.
- Expanded row rebuilt as a professional card-table: header row with
  meeting title (label above value, full width), grid of labeled cells
  (requester, department, phone, entity for external only, meeting type,
  attendee count, booking reference, purpose) with light dividers,
  keyAttendees AttendeesTable section below, rejection reason as a red
  banner row.
- Added i18n key protocol.bookings.table.title (ar: "اسم الاجتماع",
  en: "Meeting name").
- UI-only; tsc --noEmit clean; HMR applied; architect review PASS.
This commit is contained in:
Replit Agent
2026-07-09 08:19:42 +00:00
parent 16b42a3125
commit a4bf222dc4
3 changed files with 42 additions and 27 deletions
+1
View File
@@ -1811,6 +1811,7 @@
"linkCopied": "تم نسخ الرابط", "linkCopied": "تم نسخ الرابط",
"linkCopyFailed": "تعذّر نسخ الرابط", "linkCopyFailed": "تعذّر نسخ الرابط",
"table": { "table": {
"title": "اسم الاجتماع",
"number": "رقم الحجز", "number": "رقم الحجز",
"requester": "مقدم الطلب", "requester": "مقدم الطلب",
"date": "تاريخ الحجز", "date": "تاريخ الحجز",
+1
View File
@@ -1664,6 +1664,7 @@
"linkCopied": "Link copied", "linkCopied": "Link copied",
"linkCopyFailed": "Could not copy the link", "linkCopyFailed": "Could not copy the link",
"table": { "table": {
"title": "Meeting name",
"number": "Booking No.", "number": "Booking No.",
"requester": "Requester", "requester": "Requester",
"date": "Booking date", "date": "Booking date",
+40 -27
View File
@@ -1104,7 +1104,7 @@ export default function ProtocolPage() {
className={cn( className={cn(
"text-xs px-2 py-0.5 rounded-full", "text-xs px-2 py-0.5 rounded-full",
b.meetingType === "external" b.meetingType === "external"
? "bg-violet-100 text-violet-700" ? "bg-rose-100 text-rose-700"
: "bg-slate-100 text-slate-600", : "bg-slate-100 text-slate-600",
)} )}
> >
@@ -1219,19 +1219,26 @@ export default function ProtocolPage() {
</TableRow> </TableRow>
{expanded && ( {expanded && (
<TableRow key={`${b.id}-details`} className="bg-slate-50/80"> <TableRow key={`${b.id}-details`} className="bg-slate-50/80">
<TableCell colSpan={detailColSpan} className="py-3"> <TableCell colSpan={detailColSpan} className="p-3">
<div className="space-y-1"> <div className="overflow-hidden rounded-lg border border-slate-200 bg-white">
<div className="text-sm text-slate-700 font-semibold"> <div className="border-b border-slate-200 bg-slate-50 px-4 py-2.5">
{b.title} <span className="text-[11px] text-slate-400">
{t("protocol.bookings.table.title")}
</span>
<div className="text-sm font-bold text-slate-800">
{b.title}
</div>
</div> </div>
<div className="grid gap-x-6 gap-y-1 sm:grid-cols-2 lg:grid-cols-3 text-xs"> <div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 divide-x divide-x-reverse divide-slate-100 border-b border-slate-200">
{( {(
[ [
[t("protocol.bookings.requester"), b.requesterName], [t("protocol.bookings.requester"), b.requesterName],
[t("protocol.bookings.department"), b.department], [t("protocol.bookings.department"), b.department],
[ [
t("protocol.bookings.meetingType"), t("protocol.bookings.phone"),
t(`protocol.bookings.meetingTypes.${b.meetingType}`), b.requesterPhone ? (
<span dir="ltr">{b.requesterPhone}</span>
) : null,
], ],
...(b.meetingType === "external" ...(b.meetingType === "external"
? [ ? [
@@ -1242,49 +1249,55 @@ export default function ProtocolPage() {
] ]
: []), : []),
[ [
t("protocol.bookings.phone"), t("protocol.bookings.meetingType"),
b.requesterPhone ? ( t(`protocol.bookings.meetingTypes.${b.meetingType}`),
<span dir="ltr">{b.requesterPhone}</span>
) : null,
], ],
[ [
t("protocol.bookings.attendeeCount"), t("protocol.bookings.attendeeCount"),
b.attendeeCount != null ? b.attendeeCount : null, b.attendeeCount != null ? b.attendeeCount : null,
], ],
[t("protocol.bookings.purpose"), b.purpose],
[ [
t("protocol.bookings.reference"), t("protocol.bookings.reference"),
b.bookingReference ? ( b.bookingReference ? (
<span dir="ltr">{b.bookingReference}</span> <span dir="ltr">{b.bookingReference}</span>
) : null, ) : null,
], ],
[t("protocol.bookings.purpose"), b.purpose],
] as Array<[string, React.ReactNode]> ] as Array<[string, React.ReactNode]>
).map(([label, value], i) => ( ).map(([label, value], i) => (
<div key={i} className="flex gap-1"> <div
<span className="text-slate-400 shrink-0"> key={i}
{label}: className="border-b border-slate-100 px-4 py-2"
</span> >
<span className="text-slate-600 min-w-0 break-words"> <div className="text-[11px] text-slate-400">
{label}
</div>
<div className="text-xs font-medium text-slate-700 break-words">
{value != null && value !== "" ? value : "—"} {value != null && value !== "" ? value : "—"}
</span> </div>
</div> </div>
))} ))}
</div> </div>
<div className="text-xs"> <div className="px-4 py-2.5">
<span className="text-slate-400 font-medium"> <div className="mb-1.5 text-[11px] font-semibold text-slate-400">
{t("protocol.bookings.keyAttendees")}: {t("protocol.bookings.keyAttendees")}
</span> </div>
{b.keyAttendees && b.keyAttendees.length > 0 ? ( {b.keyAttendees && b.keyAttendees.length > 0 ? (
<div className="mt-1.5 max-w-2xl"> <div className="max-w-2xl">
<AttendeesTable attendees={b.keyAttendees} t={t} /> <AttendeesTable attendees={b.keyAttendees} t={t} />
</div> </div>
) : ( ) : (
<span className="ms-1 text-slate-600"></span> <span className="text-xs text-slate-600"></span>
)} )}
</div> </div>
{b.status === "rejected" && b.rejectionReason && ( {b.status === "rejected" && b.rejectionReason && (
<div className="text-xs text-rose-500"> <div className="border-t border-rose-200 bg-rose-50 px-4 py-2">
{b.rejectionReason} <div className="text-[11px] font-semibold text-rose-600">
{t("protocol.bookings.details.rejectionReason")}
</div>
<div className="text-xs text-rose-700 break-words">
{b.rejectionReason}
</div>
</div> </div>
)} )}
</div> </div>