'use client'; import { useState, useEffect, useRef } from 'react'; import { CheckCircle2, Clock, Loader2, Wrench, BarChart2 } from 'lucide-react'; import Link from 'next/link'; import { useTranslations } from 'next-intl'; import { trpc } from '@/lib/trpc/client'; import type { RouterOutputs } from '@/lib/trpc/server'; import { LanguageSwitcher } from '../language-switcher'; type Status = 'OPEN' | 'CLAIMED' | 'RESOLVED'; type QueueItem = RouterOutputs['maintenanceRequest']['queue']['items'][number]; // ── Helpers ──────────────────────────────────────────────────────────────── type TFn = (key: string, values?: Record) => string; function timeAgo(date: Date | string, t: TFn): string { const diffMs = Date.now() - new Date(date).getTime(); const mins = Math.floor(diffMs / 60_000); if (mins < 1) return t('timeAgo.now'); if (mins < 60) return t('timeAgo.minutesAgo', { mins }); const hours = Math.floor(mins / 60); if (hours < 24) return t('timeAgo.hoursAgo', { hours }); return t('timeAgo.daysAgo', { days: Math.floor(hours / 24) }); } function playBeep() { try { const ctx = new AudioContext(); const osc = ctx.createOscillator(); const gain = ctx.createGain(); osc.connect(gain); gain.connect(ctx.destination); osc.type = 'sine'; osc.frequency.value = 880; gain.gain.setValueAtTime(0.2, ctx.currentTime); gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.4); osc.start(ctx.currentTime); osc.stop(ctx.currentTime + 0.4); } catch { // AudioContext may be blocked before user interaction — ignore. } } const STATUS_CLASS: Record = { OPEN: 'bg-orange-100 text-orange-700', CLAIMED: 'bg-blue-100 text-blue-700', RESOLVED: 'bg-green-100 text-green-700', }; // ── Thumbnail ─────────────────────────────────────────────────────────────── function Thumbnail({ photoKey, alt }: { photoKey: string | null; alt: string }) { const { data } = trpc.storage.signPhotoDownload.useQuery( { photoKey: photoKey! }, { enabled: !!photoKey, staleTime: 50_000 }, ); if (!photoKey) { return
; } if (!data?.url) { return
; } return ( // eslint-disable-next-line @next/next/no-img-element {alt} ); } // ── Request card ──────────────────────────────────────────────────────────── function RequestCard({ item, onClaim, onResolve, claiming, t, tc, }: { item: QueueItem; onClaim: () => void; onResolve: () => void; claiming: boolean; t: ReturnType>; tc: ReturnType>; }) { return (
{/* Top row: thumbnail + main info */}

{item.workstation.code} — {item.workstation.name}{' '} · {item.workstation.area}

{item.description}

{t('reportedBy', { email: item.reportedBy.email, time: timeAgo(item.createdAt, tc) })}

{/* Footer: badge + actions */}
{tc(`status.${item.status.toLowerCase() as 'open' | 'claimed' | 'resolved'}`)} {item.status === 'OPEN' && ( )} {item.status === 'CLAIMED' && (

{t('claimedBy', { email: item.claimedBy?.email ?? '?', time: timeAgo(item.claimedAt!, tc) })}

)} {item.status === 'RESOLVED' && (

{t('resolvedBy', { email: item.resolvedBy?.email ?? '?', time: timeAgo(item.resolvedAt!, tc) })}

)}
); } // ── Resolve dialog ────────────────────────────────────────────────────────── function ResolveDialog({ onConfirm, onCancel, note, onNoteChange, resolving, t, tc, }: { onConfirm: () => void; onCancel: () => void; note: string; onNoteChange: (v: string) => void; resolving: boolean; t: ReturnType>; tc: ReturnType>; }) { return (

{t('resolveDialogTitle')}