"use client"; import { useState } from "react"; import { InfoTooltip } from "~/components/ui/InfoTooltip.js"; import { trpc } from "~/lib/trpc/client.js"; type CountryRow = { id: string; code: string; name: string; dailyWorkingHours: number; scheduleRules: unknown; isActive: boolean; metroCities: { id: string; name: string }[]; }; type EditingCountry = { id?: string; code: string; name: string; dailyWorkingHours: number; hasSpainRules: boolean; fridayHours: number; summerFrom: string; summerTo: string; summerHours: number; regularHours: number; }; const emptyCountry: EditingCountry = { code: "", name: "", dailyWorkingHours: 8, hasSpainRules: false, fridayHours: 6.5, summerFrom: "07-01", summerTo: "09-15", summerHours: 6.5, regularHours: 9, }; function parseSpainRules(rules: unknown): Partial { if (!rules || typeof rules !== "object") return { hasSpainRules: false }; const r = rules as Record; if (r.type !== "spain") return { hasSpainRules: false }; const sp = r as { fridayHours?: number; summerPeriod?: { from?: string; to?: string }; summerHours?: number; regularHours?: number }; return { hasSpainRules: true, fridayHours: sp.fridayHours ?? 6.5, summerFrom: sp.summerPeriod?.from ?? "07-01", summerTo: sp.summerPeriod?.to ?? "09-15", summerHours: sp.summerHours ?? 6.5, regularHours: sp.regularHours ?? 9, }; } export function CountriesClient() { const [editing, setEditing] = useState(null); const [cityName, setCityName] = useState(""); const [expandedId, setExpandedId] = useState(null); const [error, setError] = useState(null); const utils = trpc.useUtils(); const { data: countries, isLoading } = trpc.country.list.useQuery(); // @ts-ignore TS2589: tRPC infers union type too deeply for nullable JSONB scheduleRules schema const createMut = trpc.country.create.useMutation({ onSuccess: () => { void utils.country.list.invalidate(); setEditing(null); }, onError: (e) => setError(e.message), }); const updateMut = trpc.country.update.useMutation({ onSuccess: () => { void utils.country.list.invalidate(); setEditing(null); }, onError: (e) => setError(e.message), }); const createCityMut = trpc.country.createCity.useMutation({ onSuccess: () => { void utils.country.list.invalidate(); setCityName(""); }, onError: (e) => setError(e.message), }); const deleteCityMut = trpc.country.deleteCity.useMutation({ onSuccess: () => void utils.country.list.invalidate(), onError: (e) => setError(e.message), }); function openCreate() { setEditing({ ...emptyCountry }); setError(null); } function openEdit(c: CountryRow) { const spainParts = parseSpainRules(c.scheduleRules); setEditing({ id: c.id, code: c.code, name: c.name, dailyWorkingHours: c.dailyWorkingHours, hasSpainRules: false, fridayHours: 6.5, summerFrom: "07-01", summerTo: "09-15", summerHours: 6.5, regularHours: 9, ...spainParts, }); setError(null); } function handleSave() { if (!editing) return; setError(null); const scheduleRules = editing.hasSpainRules ? { type: "spain" as const, fridayHours: editing.fridayHours, summerPeriod: { from: editing.summerFrom, to: editing.summerTo }, summerHours: editing.summerHours, regularHours: editing.regularHours, } : null; if (editing.id) { updateMut.mutate({ id: editing.id, data: { code: editing.code, name: editing.name, dailyWorkingHours: editing.dailyWorkingHours, scheduleRules, }, }); } else { createMut.mutate({ code: editing.code, name: editing.name, dailyWorkingHours: editing.dailyWorkingHours, scheduleRules, }); } } function handleAddCity(countryId: string) { if (!cityName.trim()) return; createCityMut.mutate({ countryId, name: cityName.trim() }); } const isPending = createMut.isPending || updateMut.isPending; const rows = (countries ?? []) as unknown as CountryRow[]; return (

Countries

Manage countries, daily working hours, and metro cities

{error && (
{error}
)} {/* Country List */}
{isLoading && ( )} {!isLoading && rows.length === 0 && ( )} {rows.map((c) => ( ))}
Code Name Daily Hours Schedule Cities Actions
Loading...
No countries yet.
{c.code} {c.name} {c.dailyWorkingHours}h {c.scheduleRules && typeof c.scheduleRules === "object" && (c.scheduleRules as Record).type === "spain" ? ( Spain ) : ( Standard )}
{/* Expanded Metro Cities */} {expandedId && (() => { const country = rows.find((c) => c.id === expandedId); if (!country) return null; return (

Metro Cities for {country.name}

{country.metroCities.map((city) => ( {city.name} ))} {country.metroCities.length === 0 && ( No metro cities yet )}
setCityName(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") handleAddCity(country.id); }} placeholder="New city name..." className="border border-gray-300 dark:border-gray-600 rounded-lg px-3 py-1.5 text-sm bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-brand-400 flex-1" />
); })()} {/* Create/Edit Modal */} {editing && (

{editing.id ? "Edit Country" : "Add Country"}

setEditing({ ...editing, code: e.target.value.toUpperCase() })} maxLength={3} placeholder="DE" className="border border-gray-300 dark:border-gray-600 rounded-lg px-3 py-2 text-sm w-full bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-brand-400 font-mono" />
setEditing({ ...editing, dailyWorkingHours: parseFloat(e.target.value) || 8 })} min={1} max={24} step={0.5} className="border border-gray-300 dark:border-gray-600 rounded-lg px-3 py-2 text-sm w-full bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-brand-400" />
setEditing({ ...editing, name: e.target.value })} placeholder="Germany" className="border border-gray-300 dark:border-gray-600 rounded-lg px-3 py-2 text-sm w-full bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-brand-400" />
{/* Spain Schedule Rules */}
{editing.hasSpainRules && (
setEditing({ ...editing, fridayHours: parseFloat(e.target.value) || 6.5 })} step={0.5} className="border border-gray-300 dark:border-gray-600 rounded-lg px-3 py-2 text-sm w-full bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-brand-400" />
setEditing({ ...editing, regularHours: parseFloat(e.target.value) || 9 })} step={0.5} className="border border-gray-300 dark:border-gray-600 rounded-lg px-3 py-2 text-sm w-full bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-brand-400" />
setEditing({ ...editing, summerFrom: e.target.value })} placeholder="07-01" className="border border-gray-300 dark:border-gray-600 rounded-lg px-3 py-2 text-sm w-full bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-brand-400" />
setEditing({ ...editing, summerTo: e.target.value })} placeholder="09-15" className="border border-gray-300 dark:border-gray-600 rounded-lg px-3 py-2 text-sm w-full bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-brand-400" />
setEditing({ ...editing, summerHours: parseFloat(e.target.value) || 6.5 })} step={0.5} className="border border-gray-300 dark:border-gray-600 rounded-lg px-3 py-2 text-sm w-full bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-brand-400" />
)}
)}
); }