diff --git a/frontend/components/messages/appointment-detail-dialog.tsx b/frontend/components/messages/appointment-detail-dialog.tsx
new file mode 100644
index 0000000..682746d
--- /dev/null
+++ b/frontend/components/messages/appointment-detail-dialog.tsx
@@ -0,0 +1,86 @@
+"use client";
+
+import { CalendarClock } from "lucide-react";
+import type { ReactNode } from "react";
+import { useTranslation } from "react-i18next";
+
+import { Badge } from "@/components/ui/badge";
+import {
+ Dialog,
+ DialogDescription,
+ DialogHeader,
+ DialogPanel,
+ DialogPopup,
+ DialogTitle,
+} from "@/components/ui/dialog";
+import type { MessageAttachment } from "@/lib/messages";
+
+// The appointment data carried inside a shared-appointment message. It's a
+// point-in-time snapshot, so it survives the appointment later being edited or
+// deleted.
+type AppointmentSnapshot = Extract<
+ MessageAttachment,
+ { kind: "appointment" }
+>["appointment"];
+
+function Row({ label, value }: { label: string; value: ReactNode }) {
+ if (!value) return null;
+ return (
+
+
{label}
+ {value}
+
+ );
+}
+
+// A read-only view of a shared appointment, opened by clicking the card in the
+// thread. Works the same for the sender and the recipient.
+export function AppointmentDetailDialog({
+ appointment,
+ open,
+ onOpenChange,
+}: {
+ appointment: AppointmentSnapshot;
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+}) {
+ const { t } = useTranslation();
+ const a = appointment;
+ const statusLabel = a.status
+ ? t(`appointments.status.${a.status}`, { defaultValue: a.status })
+ : null;
+
+ return (
+
+ );
+}
diff --git a/frontend/components/messages/messages-view.tsx b/frontend/components/messages/messages-view.tsx
index ac194eb..7c3cb7a 100644
--- a/frontend/components/messages/messages-view.tsx
+++ b/frontend/components/messages/messages-view.tsx
@@ -5,6 +5,7 @@ import {
Download,
FileText,
Mail,
+ Paperclip,
Plus,
Search,
SendHorizonal,
@@ -21,6 +22,7 @@ import {
} from "react";
import { useTranslation } from "react-i18next";
+import { AppointmentDetailDialog } from "@/components/messages/appointment-detail-dialog";
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import { Button } from "@/components/ui/button";
import {
@@ -40,7 +42,6 @@ import {
EmptyTitle,
} from "@/components/ui/empty";
import { Input } from "@/components/ui/input";
-import { Menu, MenuItem, MenuPopup, MenuTrigger } from "@/components/ui/menu";
import { type Appointment, listAppointments } from "@/lib/appointments";
import { authClient } from "@/lib/auth-client";
import {
@@ -90,6 +91,7 @@ const GROUP_WINDOW_MS = 5 * 60 * 1000;
// shared-appointment card. Alignment (left/right) comes from the parent column.
function SentAttachment({ att }: { att: MessageAttachment }) {
const { t } = useTranslation();
+ const [apptOpen, setApptOpen] = useState(false);
if (att.kind === "file") {
return (