ultisuite-client/components/gmail/contacts/contact-detail-view.tsx
R3D347HR4Y ae54fa29e4 Hehe
2026-05-18 17:47:32 +02:00

324 lines
12 KiB
TypeScript

"use client"
import { useMemo } from "react"
import {
ArrowLeft,
Pencil,
Star,
X,
Mail,
Phone,
Building2,
MapPin,
Cake,
FileText,
MessageSquare,
Video,
} from "lucide-react"
import { Button } from "@/components/ui/button"
import { ScrollArea } from "@/components/ui/scroll-area"
import { useContactsStore } from "@/lib/contacts/contacts-store"
import { fullContactDisplayName } from "@/lib/contacts/types"
import { avatarColor, senderInitial } from "@/lib/sender-display"
import { emails as allEmails } from "@/lib/email-data"
import { useComposeActions } from "@/lib/compose-context"
import { useNavStore } from "@/lib/stores/nav-store"
interface ContactDetailViewProps {
contactId: string | null
}
const FRENCH_MONTHS = [
"janvier", "février", "mars", "avril", "mai", "juin",
"juillet", "août", "septembre", "octobre", "novembre", "décembre",
]
function formatBirthday(b: { day?: number; month?: number; year?: number }): string {
const parts: string[] = []
if (b.day) parts.push(String(b.day))
if (b.month) parts.push(FRENCH_MONTHS[b.month - 1] ?? "")
if (b.year) parts.push(String(b.year))
return parts.join(" ")
}
function formatEmailDate(iso: string): string {
const d = new Date(iso)
const now = new Date()
const diffDays = Math.floor((now.getTime() - d.getTime()) / 86_400_000)
if (diffDays === 0) return "Aujourd'hui"
if (diffDays === 1) return "Hier"
if (diffDays < 7) return `Il y a ${diffDays} jours`
return d.toLocaleDateString("fr-FR", { day: "numeric", month: "short", year: "numeric" })
}
export function ContactDetailView({ contactId }: ContactDetailViewProps) {
const { contacts, setView, closePanel } = useContactsStore()
const { openComposeWithInitial } = useComposeActions()
const labelRows = useNavStore((s) => s.labelRows)
const contact = contacts.find((c) => c.id === contactId)
const recentInteractions = useMemo(() => {
if (!contact) return []
const contactEmails = new Set(
contact.emails.map((e) => e.value.toLowerCase()).filter(Boolean)
)
if (contactEmails.size === 0) return []
return allEmails
.filter((email) => {
const se = email.senderEmail?.toLowerCase()
if (se && contactEmails.has(se)) return true
const senderLower = email.sender.toLowerCase()
return [...contactEmails].some((ce) => senderLower.includes(ce.split("@")[0] ?? ""))
})
.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())
.slice(0, 10)
}, [contact])
if (!contact) {
return (
<div className="flex h-full items-center justify-center text-sm text-gray-500">
Contact introuvable
</div>
)
}
const displayName = fullContactDisplayName(contact)
const name = displayName || contact.emails[0]?.value || contact.phones[0]?.value || "?"
const color = avatarColor(name)
const initial = senderInitial(name)
const primaryEmail = contact.emails[0]?.value
return (
<div className="flex h-full min-w-0 flex-col overflow-hidden">
{/* Header */}
<div className="flex h-12 shrink-0 items-center justify-between border-b border-gray-200 px-2">
<Button
type="button"
variant="ghost"
size="icon"
className="h-8 w-8 rounded-full text-gray-600"
onClick={() => setView("list")}
>
<ArrowLeft className="h-4 w-4" />
</Button>
<div className="flex items-center gap-1">
<Button
type="button"
variant="ghost"
size="icon"
className="h-8 w-8 rounded-full text-gray-600"
onClick={() => setView("edit", contactId)}
aria-label="Modifier"
>
<Pencil className="h-4 w-4" />
</Button>
<Button
type="button"
variant="ghost"
size="icon"
className="h-8 w-8 rounded-full text-gray-400"
>
<Star className="h-4 w-4" />
</Button>
<Button
type="button"
variant="ghost"
size="icon"
className="h-8 w-8 rounded-full text-gray-600"
onClick={closePanel}
>
<X className="h-4 w-4" />
</Button>
</div>
</div>
<ScrollArea className="min-h-0 min-w-0 flex-1 overflow-hidden [&_[data-slot=scroll-area-viewport]>div]:!block [&_[data-slot=scroll-area-viewport]>div]:min-w-0 [&_[data-slot=scroll-area-viewport]>div]:max-w-full">
<div className="w-full min-w-0 max-w-full overflow-x-hidden">
{/* Avatar + Name */}
<div className="flex flex-col items-center px-4 pt-6 pb-4">
{contact.avatarUrl ? (
<img
src={contact.avatarUrl}
alt={name}
className="h-20 w-20 rounded-full object-cover"
/>
) : (
<div
className="flex h-20 w-20 items-center justify-center rounded-full text-2xl font-medium text-white"
style={{ backgroundColor: color }}
>
{initial}
</div>
)}
<h2 className="mt-3 max-w-full truncate px-2 text-center text-lg font-medium text-gray-900">
{name}
</h2>
{contact.company && (
<p className="max-w-full truncate px-2 text-center text-sm text-gray-500">
{contact.jobTitle ? `${contact.jobTitle}` : ""}
{contact.company}
</p>
)}
{contact.labels && contact.labels.length > 0 && (
<div className="mt-2 flex flex-wrap gap-1">
{contact.labels.map((labelId) => {
const row = labelRows.find((r) => r.id === labelId)
return (
<span
key={labelId}
className="inline-flex items-center gap-1 rounded-full border border-gray-200 bg-gray-50 px-2.5 py-0.5 text-xs font-medium text-gray-700"
>
{row && (
<span className={`inline-block h-2 w-2 rounded-full ${row.color}`} />
)}
{row?.label ?? labelId}
</span>
)
})}
</div>
)}
</div>
{/* Quick actions */}
{primaryEmail && (
<div className="flex min-w-0 flex-wrap items-center justify-center gap-2 px-4 pb-4">
<button
type="button"
className="inline-flex h-9 items-center gap-2 rounded-full bg-[#d3e3fd] px-5 text-sm font-medium text-[#001d35] transition-colors hover:bg-[#c4d9fc]"
onClick={() =>
openComposeWithInitial({
to: [{ name: displayName, email: primaryEmail }],
})
}
>
<Mail className="h-4 w-4" />
Envoyer un e-mail
</button>
<button
type="button"
className="flex h-9 w-9 items-center justify-center rounded-full border border-[#dadce0] text-gray-500 hover:bg-gray-50"
>
<MessageSquare className="h-4 w-4" />
</button>
<button
type="button"
className="flex h-9 w-9 items-center justify-center rounded-full border border-[#dadce0] text-gray-500 hover:bg-gray-50"
>
<Video className="h-4 w-4" />
</button>
</div>
)}
{/* Contact details */}
<div className="min-w-0 border-t border-gray-100">
{contact.emails.length > 0 && (
<DetailSection icon={<Mail className="h-4.5 w-4.5 text-gray-400" />}>
{contact.emails.map((e, i) => (
<div key={i}>
<p className="truncate text-sm text-[#1a73e8]">{e.value}</p>
<p className="text-xs text-gray-500">{e.label}</p>
</div>
))}
</DetailSection>
)}
{contact.phones.length > 0 && (
<DetailSection icon={<Phone className="h-4.5 w-4.5 text-gray-400" />}>
{contact.phones.map((p, i) => (
<div key={i}>
<p className="text-sm text-[#1a73e8]">{p.value}</p>
<p className="text-xs text-gray-500">{p.label}</p>
</div>
))}
</DetailSection>
)}
{contact.company && (
<DetailSection icon={<Building2 className="h-4.5 w-4.5 text-gray-400" />}>
<div>
<p className="text-sm text-gray-900">{contact.company}</p>
{contact.department && (
<p className="text-xs text-gray-500">{contact.department}</p>
)}
{contact.jobTitle && (
<p className="text-xs text-gray-500">{contact.jobTitle}</p>
)}
</div>
</DetailSection>
)}
{contact.addresses && contact.addresses.length > 0 && (
<DetailSection icon={<MapPin className="h-4.5 w-4.5 text-gray-400" />}>
{contact.addresses.map((addr, i) => (
<div key={i}>
<p className="break-words text-sm text-gray-900 [overflow-wrap:anywhere]">
{[addr.street, [addr.postalCode, addr.city].filter(Boolean).join(" "), addr.region, addr.country]
.filter(Boolean)
.join(", ")}
</p>
<p className="text-xs text-gray-500">{addr.label}</p>
</div>
))}
</DetailSection>
)}
{contact.birthday && (contact.birthday.day || contact.birthday.month) && (
<DetailSection icon={<Cake className="h-4.5 w-4.5 text-gray-400" />}>
<p className="text-sm text-gray-900">{formatBirthday(contact.birthday)}</p>
</DetailSection>
)}
{contact.notes && (
<DetailSection icon={<FileText className="h-4.5 w-4.5 text-gray-400" />}>
<p className="text-sm text-gray-700 whitespace-pre-wrap">{contact.notes}</p>
</DetailSection>
)}
</div>
{/* Recent interactions */}
{recentInteractions.length > 0 && (
<div className="min-w-0 overflow-hidden border-t border-gray-100 pt-3 pb-4">
<h3 className="px-4 pb-2 text-xs font-medium uppercase text-gray-500">
Interactions récentes
</h3>
{recentInteractions.map((email) => (
<div
key={email.id}
className="flex min-w-0 gap-3 overflow-hidden px-4 py-2 hover:bg-gray-50"
>
<Mail className="mt-0.5 h-4 w-4 shrink-0 text-gray-400" />
<div className="min-w-0 flex-1 overflow-hidden">
<p className="truncate text-sm text-gray-900">{email.subject}</p>
<p className="line-clamp-2 break-words [overflow-wrap:anywhere] text-xs text-gray-500">
{email.preview}
</p>
<p className="mt-0.5 text-xs text-gray-400">{formatEmailDate(email.date)}</p>
</div>
</div>
))}
</div>
)}
</div>
</ScrollArea>
</div>
)
}
function DetailSection({
icon,
children,
}: {
icon: React.ReactNode
children: React.ReactNode
}) {
return (
<div className="flex min-w-0 gap-3 px-4 py-3">
<div className="flex w-5 shrink-0 pt-0.5">{icon}</div>
<div className="min-w-0 flex-1 space-y-2 overflow-hidden">{children}</div>
</div>
)
}