chore(repo): initialize planarchy workspace

This commit is contained in:
2026-03-14 14:31:09 +01:00
commit dd55d0e78b
769 changed files with 166461 additions and 0 deletions
@@ -0,0 +1,788 @@
"use client";
import { useState } from "react";
import { trpc } from "~/lib/trpc/client.js";
// ─── Local types ────────────────────────────────────────────────────────────
type RateCardRow = {
id: string;
name: string;
currency: string;
effectiveFrom: string | null;
effectiveTo: string | null;
source: string | null;
isActive: boolean;
clientId: string | null;
client: { id: string; name: string; code: string | null } | null;
_count: { lines: number };
};
type RateCardLine = {
id: string;
rateCardId: string;
roleId: string | null;
chapter: string | null;
location: string | null;
seniority: string | null;
workType: string | null;
serviceGroup: string | null;
costRateCents: number;
billRateCents: number | null;
machineRateCents: number | null;
attributes: unknown;
role: { id: string; name: string; color: string | null } | null;
createdAt: string;
updatedAt: string;
};
type ClientOption = {
id: string;
name: string;
code: string | null;
};
type EditingCard = {
id?: string;
name: string;
currency: string;
effectiveFrom: string;
effectiveTo: string;
source: string;
clientId: string;
};
type EditingLine = {
id?: string;
roleId: string;
chapter: string;
location: string;
seniority: string;
workType: string;
serviceGroup: string;
costRateCents: number;
billRateCents: number;
machineRateCents: number;
};
const emptyCard: EditingCard = {
name: "",
currency: "EUR",
effectiveFrom: "",
effectiveTo: "",
source: "",
clientId: "",
};
const emptyLine: EditingLine = {
roleId: "",
chapter: "",
location: "",
seniority: "",
workType: "",
serviceGroup: "",
costRateCents: 0,
billRateCents: 0,
machineRateCents: 0,
};
function formatCents(cents: number | null | undefined): string {
if (cents == null) return "-";
return (cents / 100).toFixed(2);
}
function formatDate(d: string | null | undefined): string {
if (!d) return "-";
return new Date(d).toLocaleDateString("de-DE");
}
// ─── Component ──────────────────────────────────────────────────────────────
export function RateCardsClient() {
const [search, setSearch] = useState("");
const [filterClientId, setFilterClientId] = useState<string>("");
const [selectedId, setSelectedId] = useState<string | null>(null);
const [editingCard, setEditingCard] = useState<EditingCard | null>(null);
const [editingLine, setEditingLine] = useState<EditingLine | null>(null);
const [error, setError] = useState<string | null>(null);
const utils = trpc.useUtils();
// ─── Queries ────────────────────────────────────────────────────────────
const { data: cards, isLoading } = trpc.rateCard.list.useQuery(
{
search: search || undefined,
...(filterClientId ? { clientId: filterClientId } : {}),
},
);
const { data: detail } = trpc.rateCard.getById.useQuery(
{ id: selectedId! },
{ enabled: !!selectedId },
);
const { data: roles } = trpc.role.list.useQuery({});
const { data: clientsData } = trpc.clientEntity.list.useQuery({});
// ─── Mutations ──────────────────────────────────────────────────────────
const invalidateAll = () => {
void utils.rateCard.list.invalidate();
if (selectedId) void utils.rateCard.getById.invalidate({ id: selectedId });
};
// Use bare useMutation() to avoid TS2589 deep inference (see LEARNINGS.md)
const createMut = trpc.rateCard.create.useMutation();
const updateMut = trpc.rateCard.update.useMutation();
const deactivateMut = trpc.rateCard.deactivate.useMutation();
const addLineMut = trpc.rateCard.addLine.useMutation();
const updateLineMut = trpc.rateCard.updateLine.useMutation();
const deleteLineMut = trpc.rateCard.deleteLine.useMutation();
// ─── Handlers ───────────────────────────────────────────────────────────
function openCreateCard() {
setEditingCard({ ...emptyCard });
setError(null);
}
function openEditCard() {
if (!detail) return;
setEditingCard({
id: detail.id,
name: detail.name,
currency: detail.currency,
effectiveFrom: detail.effectiveFrom
? new Date(detail.effectiveFrom).toISOString().slice(0, 10)
: "",
effectiveTo: detail.effectiveTo
? new Date(detail.effectiveTo).toISOString().slice(0, 10)
: "",
source: detail.source ?? "",
clientId: detail.clientId ?? "",
});
setError(null);
}
async function handleSaveCard() {
if (!editingCard) return;
setError(null);
try {
if (editingCard.id) {
await updateMut.mutateAsync({
id: editingCard.id,
data: {
name: editingCard.name,
currency: editingCard.currency,
...(editingCard.effectiveFrom
? { effectiveFrom: new Date(editingCard.effectiveFrom) }
: { effectiveFrom: null }),
...(editingCard.effectiveTo
? { effectiveTo: new Date(editingCard.effectiveTo) }
: { effectiveTo: null }),
source: editingCard.source || null,
clientId: editingCard.clientId || null,
},
});
invalidateAll();
setEditingCard(null);
} else {
const created = await createMut.mutateAsync({
name: editingCard.name,
currency: editingCard.currency,
...(editingCard.effectiveFrom
? { effectiveFrom: new Date(editingCard.effectiveFrom) }
: {}),
...(editingCard.effectiveTo
? { effectiveTo: new Date(editingCard.effectiveTo) }
: {}),
...(editingCard.source ? { source: editingCard.source } : {}),
...(editingCard.clientId ? { clientId: editingCard.clientId } : {}),
lines: [],
});
invalidateAll();
setEditingCard(null);
setSelectedId(created.id);
}
} catch (e) {
setError(e instanceof Error ? e.message : "Failed to save rate card");
}
}
function openAddLine() {
setEditingLine({ ...emptyLine });
setError(null);
}
function openEditLine(line: RateCardLine) {
setEditingLine({
id: line.id,
roleId: line.roleId ?? "",
chapter: line.chapter ?? "",
location: line.location ?? "",
seniority: line.seniority ?? "",
workType: line.workType ?? "",
serviceGroup: line.serviceGroup ?? "",
costRateCents: line.costRateCents,
billRateCents: line.billRateCents ?? 0,
machineRateCents: line.machineRateCents ?? 0,
});
setError(null);
}
async function handleSaveLine() {
if (!editingLine || !selectedId) return;
setError(null);
const lineData = {
...(editingLine.roleId ? { roleId: editingLine.roleId } : {}),
...(editingLine.chapter ? { chapter: editingLine.chapter } : {}),
...(editingLine.location ? { location: editingLine.location } : {}),
...(editingLine.seniority ? { seniority: editingLine.seniority } : {}),
...(editingLine.workType ? { workType: editingLine.workType } : {}),
...(editingLine.serviceGroup ? { serviceGroup: editingLine.serviceGroup } : {}),
costRateCents: editingLine.costRateCents,
...(editingLine.billRateCents ? { billRateCents: editingLine.billRateCents } : {}),
...(editingLine.machineRateCents ? { machineRateCents: editingLine.machineRateCents } : {}),
attributes: {},
};
try {
if (editingLine.id) {
await updateLineMut.mutateAsync({ lineId: editingLine.id, data: lineData });
} else {
await addLineMut.mutateAsync({ rateCardId: selectedId, line: lineData });
}
invalidateAll();
setEditingLine(null);
} catch (e) {
setError(e instanceof Error ? e.message : "Failed to save rate line");
}
}
async function handleDeleteLine(lineId: string) {
if (!confirm("Delete this rate line?")) return;
try {
await deleteLineMut.mutateAsync({ lineId });
invalidateAll();
} catch (e) {
setError(e instanceof Error ? e.message : "Failed to delete rate line");
}
}
async function handleDeactivate(id: string) {
if (!confirm("Deactivate this rate card?")) return;
try {
await deactivateMut.mutateAsync({ id });
invalidateAll();
} catch (e) {
setError(e instanceof Error ? e.message : "Failed to deactivate rate card");
}
}
async function handleReactivate(id: string) {
try {
await updateMut.mutateAsync({ id, data: { isActive: true } });
invalidateAll();
} catch (e) {
setError(e instanceof Error ? e.message : "Failed to reactivate rate card");
}
}
const isPending =
createMut.isPending ||
updateMut.isPending ||
addLineMut.isPending ||
updateLineMut.isPending ||
deleteLineMut.isPending;
const cardList = (cards ?? []) as unknown as RateCardRow[];
const lines = ((detail?.lines ?? []) as unknown as RateCardLine[]);
const roleList = (roles ?? []) as unknown as { id: string; name: string; color: string | null }[];
const clientList = (clientsData ?? []) as unknown as ClientOption[];
// ─── Render ─────────────────────────────────────────────────────────────
return (
<div className="p-6 max-w-7xl mx-auto">
{/* Header */}
<div className="flex items-center justify-between mb-6">
<div>
<h1 className="text-2xl font-bold text-gray-900 dark:text-gray-50">Rate Cards</h1>
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
Manage cost and billing rates per role, chapter, and seniority
</p>
</div>
<button
type="button"
onClick={openCreateCard}
className="px-4 py-2 bg-brand-600 text-white rounded-lg hover:bg-brand-700 text-sm font-medium"
>
+ New Rate Card
</button>
</div>
{/* Error banner */}
{error && (
<div className="mb-4 rounded-lg bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-700 px-4 py-3 text-sm text-red-700 dark:text-red-400 flex items-center justify-between">
{error}
<button type="button" onClick={() => setError(null)} className="text-red-400 hover:text-red-600 text-lg leading-none">&times;</button>
</div>
)}
<div className="flex gap-6">
{/* ─── Left: Card list ─────────────────────────────────────────── */}
<div className="w-80 shrink-0">
<div className="mb-3 space-y-2">
<input
type="search"
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Search rate cards..."
className="border border-gray-300 dark:border-gray-600 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-brand-400 w-full bg-white dark:bg-gray-900 dark:text-gray-100"
/>
<select
value={filterClientId}
onChange={(e) => setFilterClientId(e.target.value)}
className="border border-gray-300 dark:border-gray-600 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-brand-400 w-full bg-white dark:bg-gray-900 dark:text-gray-100"
>
<option value="">All clients</option>
{clientList.map((c) => (
<option key={c.id} value={c.id}>
{c.code ? `${c.code}${c.name}` : c.name}
</option>
))}
</select>
</div>
<div className="bg-white dark:bg-gray-900 rounded-xl border border-gray-200 dark:border-gray-700 divide-y divide-gray-100 dark:divide-gray-800">
{isLoading && <div className="text-center py-8 text-gray-400">Loading...</div>}
{!isLoading && cardList.length === 0 && (
<div className="text-center py-8 text-gray-400">
{search ? "No rate cards match your search." : "No rate cards yet."}
</div>
)}
{cardList.map((card) => (
<button
key={card.id}
type="button"
onClick={() => setSelectedId(card.id)}
className={`w-full text-left px-4 py-3 hover:bg-gray-50 dark:hover:bg-gray-800/30 transition-colors ${
selectedId === card.id
? "bg-brand-50 dark:bg-brand-900/20 border-l-2 border-brand-600"
: ""
}`}
>
<div className="flex items-center justify-between">
<span className="text-sm font-medium text-gray-900 dark:text-gray-100 truncate">
{card.name}
</span>
{!card.isActive && (
<span className="text-xs text-gray-400 italic ml-2 shrink-0">inactive</span>
)}
</div>
<div className="flex items-center gap-3 mt-1 text-xs text-gray-500 dark:text-gray-400">
<span>{card.currency}</span>
<span>{card._count.lines} lines</span>
{card.effectiveFrom && (
<span>from {formatDate(card.effectiveFrom)}</span>
)}
</div>
{card.client && (
<div className="mt-1 text-xs text-brand-600 dark:text-brand-400">
{card.client.code ? `${card.client.code}${card.client.name}` : card.client.name}
</div>
)}
</button>
))}
</div>
</div>
{/* ─── Right: Detail ───────────────────────────────────────────── */}
<div className="flex-1 min-w-0">
{!selectedId && (
<div className="bg-white dark:bg-gray-900 rounded-xl border border-gray-200 dark:border-gray-700 p-12 text-center text-gray-400">
Select a rate card to view details, or create a new one.
</div>
)}
{selectedId && detail && (
<div className="bg-white dark:bg-gray-900 rounded-xl border border-gray-200 dark:border-gray-700">
{/* Card header */}
<div className="px-6 py-4 border-b border-gray-200 dark:border-gray-700">
<div className="flex items-center justify-between">
<div>
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100">
{detail.name}
</h2>
<div className="flex items-center gap-4 mt-1 text-sm text-gray-500 dark:text-gray-400">
<span>{detail.currency}</span>
{detail.clientId && (detail as unknown as RateCardRow).client && (
<span>Client: {(detail as unknown as RateCardRow).client!.name}</span>
)}
{detail.source && <span>Source: {detail.source}</span>}
<span>
{detail.effectiveFrom
? formatDate(detail.effectiveFrom as unknown as string)
: "Open start"}{" "}
&mdash;{" "}
{detail.effectiveTo
? formatDate(detail.effectiveTo as unknown as string)
: "Open end"}
</span>
{!detail.isActive && (
<span className="text-amber-600 dark:text-amber-400 font-medium">Inactive</span>
)}
</div>
</div>
<div className="flex items-center gap-2">
<button
type="button"
onClick={openEditCard}
className="px-3 py-1.5 text-sm text-brand-600 hover:text-brand-800 dark:text-brand-400 dark:hover:text-brand-300 font-medium"
>
Edit
</button>
{detail.isActive ? (
<button
type="button"
onClick={() => handleDeactivate(detail.id)}
className="px-3 py-1.5 text-sm text-gray-500 hover:text-red-600 font-medium"
>
Deactivate
</button>
) : (
<button
type="button"
onClick={() => handleReactivate(detail.id)}
className="px-3 py-1.5 text-sm text-green-600 hover:text-green-800 font-medium"
>
Reactivate
</button>
)}
</div>
</div>
</div>
{/* Lines header */}
<div className="px-6 py-3 border-b border-gray-200 dark:border-gray-700 flex items-center justify-between">
<h3 className="text-sm font-semibold text-gray-700 dark:text-gray-300">
Rate Lines ({lines.length})
</h3>
<button
type="button"
onClick={openAddLine}
className="px-3 py-1.5 bg-brand-600 text-white rounded-lg hover:bg-brand-700 text-xs font-medium"
>
+ Add Line
</button>
</div>
{/* Lines table */}
<div className="overflow-x-auto">
{lines.length === 0 ? (
<div className="text-center py-8 text-gray-400 text-sm">
No rate lines yet. Add one to get started.
</div>
) : (
<table className="w-full text-sm">
<thead>
<tr className="text-left text-xs text-gray-500 dark:text-gray-400 border-b border-gray-100 dark:border-gray-800">
<th className="px-4 py-2 font-medium">Role</th>
<th className="px-4 py-2 font-medium">Chapter</th>
<th className="px-4 py-2 font-medium">Location</th>
<th className="px-4 py-2 font-medium">Seniority</th>
<th className="px-4 py-2 font-medium">Work Type</th>
<th className="px-4 py-2 font-medium text-right">Cost Rate</th>
<th className="px-4 py-2 font-medium text-right">Bill Rate</th>
<th className="px-4 py-2 font-medium text-right">Machine Rate</th>
<th className="px-4 py-2 font-medium w-20"></th>
</tr>
</thead>
<tbody className="divide-y divide-gray-100 dark:divide-gray-800">
{lines.map((line) => (
<tr key={line.id} className="hover:bg-gray-50 dark:hover:bg-gray-800/30 group">
<td className="px-4 py-2 text-gray-900 dark:text-gray-100">
{line.role?.name ?? <span className="text-gray-400">-</span>}
</td>
<td className="px-4 py-2 text-gray-700 dark:text-gray-300">{line.chapter || "-"}</td>
<td className="px-4 py-2 text-gray-700 dark:text-gray-300">{line.location || "-"}</td>
<td className="px-4 py-2 text-gray-700 dark:text-gray-300">{line.seniority || "-"}</td>
<td className="px-4 py-2 text-gray-700 dark:text-gray-300">{line.workType || "-"}</td>
<td className="px-4 py-2 text-right font-mono text-gray-900 dark:text-gray-100">
{formatCents(line.costRateCents)}
</td>
<td className="px-4 py-2 text-right font-mono text-gray-700 dark:text-gray-300">
{formatCents(line.billRateCents)}
</td>
<td className="px-4 py-2 text-right font-mono text-gray-700 dark:text-gray-300">
{formatCents(line.machineRateCents)}
</td>
<td className="px-4 py-2">
<div className="flex gap-2 opacity-0 group-hover:opacity-100 transition-opacity justify-end">
<button
type="button"
onClick={() => openEditLine(line)}
className="text-xs text-brand-600 hover:text-brand-800 font-medium"
>
Edit
</button>
<button
type="button"
onClick={() => handleDeleteLine(line.id)}
className="text-xs text-red-500 hover:text-red-700 font-medium"
>
Delete
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
)}
</div>
</div>
)}
</div>
</div>
{/* ─── Rate Card Modal ─────────────────────────────────────────────── */}
{editingCard && (
<div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4">
<div className="bg-white dark:bg-gray-900 rounded-xl shadow-2xl w-full max-w-md mx-4">
<div className="flex items-center justify-between px-6 py-4 border-b border-gray-200 dark:border-gray-700">
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100">
{editingCard.id ? "Edit Rate Card" : "New Rate Card"}
</h2>
<button type="button" onClick={() => setEditingCard(null)} className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 text-2xl leading-none">&times;</button>
</div>
<div className="px-6 py-5 space-y-4">
<div>
<label className="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">Name</label>
<input
type="text"
value={editingCard.name}
onChange={(e) => setEditingCard({ ...editingCard, name: e.target.value })}
placeholder="e.g. Standard 2026"
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"
/>
</div>
<div>
<label className="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">Client</label>
<select
value={editingCard.clientId}
onChange={(e) => setEditingCard({ ...editingCard, clientId: e.target.value })}
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"
>
<option value="">-- No client --</option>
{clientList.map((c) => (
<option key={c.id} value={c.id}>
{c.code ? `${c.code}${c.name}` : c.name}
</option>
))}
</select>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">Currency</label>
<input
type="text"
value={editingCard.currency}
onChange={(e) => setEditingCard({ ...editingCard, currency: e.target.value.toUpperCase() })}
placeholder="EUR"
maxLength={3}
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"
/>
</div>
<div>
<label className="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">Source</label>
<input
type="text"
value={editingCard.source}
onChange={(e) => setEditingCard({ ...editingCard, source: e.target.value })}
placeholder="e.g. Finance dept"
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"
/>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">Effective From</label>
<input
type="date"
value={editingCard.effectiveFrom}
onChange={(e) => setEditingCard({ ...editingCard, effectiveFrom: e.target.value })}
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"
/>
</div>
<div>
<label className="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">Effective To</label>
<input
type="date"
value={editingCard.effectiveTo}
onChange={(e) => setEditingCard({ ...editingCard, effectiveTo: e.target.value })}
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"
/>
</div>
</div>
</div>
<div className="flex items-center justify-end px-6 py-4 border-t border-gray-200 dark:border-gray-700 gap-3">
<button type="button" onClick={() => setEditingCard(null)} className="px-4 py-2 text-sm text-gray-600 hover:text-gray-800 dark:text-gray-400 dark:hover:text-gray-200">Cancel</button>
<button
type="button"
onClick={handleSaveCard}
disabled={isPending || !editingCard.name || editingCard.currency.length !== 3}
className="px-4 py-2 bg-brand-600 text-white rounded-lg hover:bg-brand-700 text-sm font-medium disabled:opacity-50"
>
{isPending ? "Saving..." : editingCard.id ? "Update" : "Create"}
</button>
</div>
</div>
</div>
)}
{/* ─── Rate Line Modal ─────────────────────────────────────────────── */}
{editingLine && (
<div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4">
<div className="bg-white dark:bg-gray-900 rounded-xl shadow-2xl w-full max-w-lg mx-4">
<div className="flex items-center justify-between px-6 py-4 border-b border-gray-200 dark:border-gray-700">
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100">
{editingLine.id ? "Edit Rate Line" : "Add Rate Line"}
</h2>
<button type="button" onClick={() => setEditingLine(null)} className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 text-2xl leading-none">&times;</button>
</div>
<div className="px-6 py-5 space-y-4">
<div>
<label className="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">Role</label>
<select
value={editingLine.roleId}
onChange={(e) => setEditingLine({ ...editingLine, roleId: e.target.value })}
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"
>
<option value="">-- No specific role --</option>
{roleList.map((r) => (
<option key={r.id} value={r.id}>{r.name}</option>
))}
</select>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">Chapter</label>
<input
type="text"
value={editingLine.chapter}
onChange={(e) => setEditingLine({ ...editingLine, chapter: e.target.value })}
placeholder="e.g. Animation"
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"
/>
</div>
<div>
<label className="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">Location</label>
<input
type="text"
value={editingLine.location}
onChange={(e) => setEditingLine({ ...editingLine, location: e.target.value })}
placeholder="e.g. Munich"
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"
/>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">Seniority</label>
<input
type="text"
value={editingLine.seniority}
onChange={(e) => setEditingLine({ ...editingLine, seniority: e.target.value })}
placeholder="e.g. Senior"
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"
/>
</div>
<div>
<label className="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">Work Type</label>
<input
type="text"
value={editingLine.workType}
onChange={(e) => setEditingLine({ ...editingLine, workType: e.target.value })}
placeholder="e.g. Onsite"
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"
/>
</div>
</div>
<div>
<label className="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">Service Group</label>
<input
type="text"
value={editingLine.serviceGroup}
onChange={(e) => setEditingLine({ ...editingLine, serviceGroup: e.target.value })}
placeholder="e.g. Post Production"
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"
/>
</div>
<div className="grid grid-cols-3 gap-4">
<div>
<label className="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">Cost Rate (cents)</label>
<input
type="number"
value={editingLine.costRateCents}
onChange={(e) => setEditingLine({ ...editingLine, costRateCents: parseInt(e.target.value) || 0 })}
min={0}
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"
/>
<span className="text-xs text-gray-400 mt-0.5 block">= {formatCents(editingLine.costRateCents)}</span>
</div>
<div>
<label className="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">Bill Rate (cents)</label>
<input
type="number"
value={editingLine.billRateCents}
onChange={(e) => setEditingLine({ ...editingLine, billRateCents: parseInt(e.target.value) || 0 })}
min={0}
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"
/>
<span className="text-xs text-gray-400 mt-0.5 block">= {formatCents(editingLine.billRateCents)}</span>
</div>
<div>
<label className="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">Machine Rate (cents)</label>
<input
type="number"
value={editingLine.machineRateCents}
onChange={(e) => setEditingLine({ ...editingLine, machineRateCents: parseInt(e.target.value) || 0 })}
min={0}
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"
/>
<span className="text-xs text-gray-400 mt-0.5 block">= {formatCents(editingLine.machineRateCents)}</span>
</div>
</div>
</div>
<div className="flex items-center justify-end px-6 py-4 border-t border-gray-200 dark:border-gray-700 gap-3">
<button type="button" onClick={() => setEditingLine(null)} className="px-4 py-2 text-sm text-gray-600 hover:text-gray-800 dark:text-gray-400 dark:hover:text-gray-200">Cancel</button>
<button
type="button"
onClick={handleSaveLine}
disabled={isPending || editingLine.costRateCents <= 0}
className="px-4 py-2 bg-brand-600 text-white rounded-lg hover:bg-brand-700 text-sm font-medium disabled:opacity-50"
>
{isPending ? "Saving..." : editingLine.id ? "Update Line" : "Add Line"}
</button>
</div>
</div>
</div>
)}
</div>
);
}