"use client"; import { ReactNode, useState } from "react"; import Link from "next/link"; import { AlertTriangle, CheckCircle2, ChevronDown, Clock, Loader2, type LucideIcon, } from "lucide-react"; import { VOICE_TOO_MANY_OPEN_HINT, voiceRejectionText } from "@/lib/offline/queue-texts"; import type { VoiceCaptureListItem } from "@/lib/voice/api"; import type { StoredVoiceNote } from "@/lib/voice/pending-voice"; import { VOICE_WAITING_FOR_NETWORK, describeVoiceCapture, type VoiceStatusIcon, type VoiceStatusText, } from "@/lib/voice/status-texts"; import { Button } from "@/components/ui/button"; import { Card, CardContent } from "@/components/ui/card"; import { VoiceConfirmDialog } from "./voice-confirm-dialog"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; const STATUS_ICONS: Record = { wait: Clock, work: Loader2, ready: CheckCircle2, failed: AlertTriangle, }; const TIME_FORMAT = new Intl.DateTimeFormat("de-DE", { day: "2-digit", month: "2-digit", hour: "2-digit", minute: "2-digit", timeZone: "Europe/Berlin", }); function timeText(iso: string): string { return `${TIME_FORMAT.format(new Date(iso))} Uhr`.replace(", ", " · "); } function StatusLine({ status }: { status: VoiceStatusText }) { const Icon = STATUS_ICONS[status.icon]; return (

{status.label}

); } interface DiscardMenuProps { /** Names the menu for assistive tech, e.g. "B3, 14.10. · 09:12 Uhr". */ subject: string; onDiscard(): void; disabled?: boolean; } /** „Mehr“ with „Verwerfen“, which asks first: a discarded note cannot come back. */ function DiscardMenu({ subject, onDiscard, disabled }: DiscardMenuProps) { const [confirmOpen, setConfirmOpen] = useState(false); return ( <> } > Mehr setConfirmOpen(true)} > Verwerfen setConfirmOpen(false)} onConfirm={() => { setConfirmOpen(false); onDiscard(); }} /> ); } interface CardShellProps { title: string; time: string; status: VoiceStatusText; detail?: string | null; children?: ReactNode; } function CardShell({ title, time, status, detail, children }: CardShellProps) { return (

{title}

{time}

{detail ?

{detail}

: null} {children ?
{children}
: null}
); } interface LocalVoiceCardProps { note: StoredVoiceNote; colonyName: string; /** Whether the device is online: a waiting note is then about to be sent. */ online: boolean; onDiscard(): void; } /** What a recording on this device is doing, in words. */ function localStatus(note: StoredVoiceNote, online: boolean): { status: VoiceStatusText; detail: string | null } { if (note.lastError) { return { status: { label: "Fehlgeschlagen", icon: "failed" }, detail: voiceRejectionText(note.lastError), }; } if (note.blockedBy === "TOO_MANY_OPEN") { return { status: { label: "Wartet auf freien Platz", icon: "wait" }, detail: VOICE_TOO_MANY_OPEN_HINT, }; } if (note.state === "uploading" || online) { return { status: { label: "Wird gesendet", icon: "work" }, detail: null }; } return { status: VOICE_WAITING_FOR_NETWORK, detail: null }; } /** A recording that still waits on this device. */ export function LocalVoiceCard({ note, colonyName, online, onDiscard }: LocalVoiceCardProps) { const time = timeText(note.createdAt); const { status, detail } = localStatus(note, online); const again = new URLSearchParams({ colonyId: note.colonyId, date: note.inspectionDate }); return ( {note.lastError ? ( ) : null} ); } interface VoiceCaptureCardProps { item: VoiceCaptureListItem; /** False when the operator switched the evaluation off. */ extractionEnabled?: boolean; onRetry(): void; onDiscard(): void; busy?: boolean; } /** A note the server holds. */ export function VoiceCaptureCard({ item, extractionEnabled = true, onRetry, onDiscard, busy, }: VoiceCaptureCardProps) { const state = describeVoiceCapture(item, extractionEnabled); const time = timeText(item.createdAt); const reviewHref = `/inspections/new?colonyId=${encodeURIComponent(item.colonyId)}&voice=${encodeURIComponent(item.id)}`; return ( {state.action !== "none" ? ( ) : null} {state.canRetry ? ( ) : null} ); }