Files
XBid/src/components/line-items/line-items-table.tsx
T
2026-08-18 12:11:20 +02:00

227 lines
8.8 KiB
TypeScript

"use client";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { Trash2, Pencil, ChevronUp, ChevronDown } from "lucide-react";
import { calcLineItem, formatCurrency } from "@/lib/calculations";
import { LineItemDialog } from "./line-item-dialog";
import { updateLineItem } from "@/actions/shots-assets";
import { toast } from "sonner";
interface LineItem {
id: string;
taskName: string;
artistRole: string | null;
quantityHours: any;
dailyRate: any;
hoursPerDay: any;
numArtists: any;
numDays: any;
manualHourlyRate: any;
manualSubtotal: any;
markupPercent: any;
discountPercent: any;
taxPercent: any;
notes: string | null;
rateCard: any;
}
interface Props {
lineItems: LineItem[];
currency: string;
rateCards: any[];
complexityMultiplier: string;
parentId: string;
parentType: "shot" | "asset" | "supervision" | "previs";
jobId: string;
onDelete: (id: string) => void;
onUpdate: (item: any) => void;
}
export function LineItemsTable({
lineItems,
currency,
rateCards,
complexityMultiplier,
parentId,
parentType,
jobId,
onDelete,
onUpdate,
}: Props) {
const [editItem, setEditItem] = useState<LineItem | null>(null);
const [adjustingHours, setAdjustingHours] = useState<Set<string>>(new Set());
const handleAdjustHours = async (li: LineItem, delta: number) => {
const newHours = Math.max(0, Number(li.quantityHours) + delta);
setAdjustingHours((prev) => new Set(prev).add(li.id));
try {
const result = await updateLineItem(
li.id,
jobId,
{
taskName: li.taskName,
artistRole: li.artistRole ?? undefined,
rateCardId: li.rateCard?.id,
quantityHours: newHours,
dailyRate: Number(li.dailyRate),
hoursPerDay: Number(li.hoursPerDay),
numArtists: Number(li.numArtists),
numDays: Number(li.numDays),
manualHourlyRate: li.manualHourlyRate != null ? Number(li.manualHourlyRate) : null,
manualSubtotal: li.manualSubtotal != null ? Number(li.manualSubtotal) : null,
markupPercent: Number(li.markupPercent),
discountPercent: Number(li.discountPercent),
taxPercent: Number(li.taxPercent),
notes: li.notes ?? undefined,
},
complexityMultiplier
);
onUpdate(result.lineItem);
} catch {
toast.error("Failed to update hours");
} finally {
setAdjustingHours((prev) => {
const next = new Set(prev);
next.delete(li.id);
return next;
});
}
};
const handleUpdate = async (data: any) => {
if (!editItem) return;
try {
const result = await updateLineItem(editItem.id, jobId, data, complexityMultiplier);
onUpdate(result.lineItem);
setEditItem(null);
toast.success("Line item updated");
} catch {
toast.error("Failed to update line item");
}
};
if (lineItems.length === 0) {
return (
<div className="px-4 py-6 text-center text-sm text-zinc-500">
No tasks yet. Add a task below.
</div>
);
}
return (
<>
<div className="overflow-x-auto">
<table className="w-full text-xs">
<thead>
<tr className="border-b border-zinc-800 text-zinc-500 uppercase tracking-wider">
<th className="px-4 py-2 text-left">Task</th>
<th className="px-4 py-2 text-left">Role</th>
<th className="px-4 py-2 text-right">Hours</th>
<th className="px-4 py-2 text-right">Day Rate</th>
<th className="px-4 py-2 text-right">Artists</th>
<th className="px-4 py-2 text-right">Days</th>
<th className="px-4 py-2 text-right">Subtotal</th>
<th className="px-4 py-2 text-right">Total</th>
<th className="px-4 py-2" />
</tr>
</thead>
<tbody>
{lineItems.map((li) => {
const calc = calcLineItem(
{
quantityHours: li.quantityHours?.toString() ?? "0",
dailyRate: li.dailyRate?.toString() ?? "0",
hoursPerDay: li.hoursPerDay?.toString() ?? "8",
numArtists: li.numArtists?.toString() ?? "1",
numDays: li.numDays?.toString() ?? "0",
manualHourlyRate: li.manualHourlyRate?.toString(),
manualSubtotal: li.manualSubtotal?.toString(),
markupPercent: li.markupPercent?.toString() ?? "0",
discountPercent: li.discountPercent?.toString() ?? "0",
taxPercent: li.taxPercent?.toString() ?? "0",
},
complexityMultiplier
);
return (
<tr key={li.id} className="border-b border-zinc-800/30 hover:bg-zinc-800/20 transition-colors">
<td className="px-4 py-2 font-medium text-white">{li.taskName}</td>
<td className="px-4 py-2 text-zinc-400">{li.artistRole ?? "—"}</td>
<td className="px-4 py-2 text-right">
<div className="flex items-center justify-end gap-1">
<div className="flex flex-col">
<button
onClick={() => handleAdjustHours(li, 0.5)}
disabled={adjustingHours.has(li.id)}
className="text-zinc-500 hover:text-amber-400 disabled:opacity-30 leading-none"
>
<ChevronUp className="h-2.5 w-2.5" />
</button>
<button
onClick={() => handleAdjustHours(li, -0.5)}
disabled={adjustingHours.has(li.id) || Number(li.quantityHours) <= 0}
className="text-zinc-500 hover:text-amber-400 disabled:opacity-30 leading-none"
>
<ChevronDown className="h-2.5 w-2.5" />
</button>
</div>
<span className="font-mono text-zinc-300 min-w-10 text-right">
{Number(li.quantityHours) > 0 ? Number(li.quantityHours).toFixed(1) : "—"}
</span>
</div>
</td>
<td className="px-4 py-2 text-right text-zinc-300 font-mono">
{Number(li.dailyRate) > 0 ? formatCurrency(Number(li.dailyRate), currency) : "—"}
</td>
<td className="px-4 py-2 text-right text-zinc-300 font-mono">{Number(li.numArtists)}</td>
<td className="px-4 py-2 text-right text-zinc-300 font-mono">
{Number(li.numDays) > 0 ? Number(li.numDays).toFixed(1) : "—"}
</td>
<td className="px-4 py-2 text-right text-zinc-300 font-mono">{formatCurrency(calc.subtotal, currency)}</td>
<td className="px-4 py-2 text-right font-mono font-semibold text-white">{formatCurrency(calc.total, currency)}</td>
<td className="px-4 py-2">
<div className="flex gap-1 justify-end">
<Button variant="ghost" size="icon" className="h-6 w-6" onClick={() => setEditItem(li)}>
<Pencil className="h-3 w-3" />
</Button>
<Button variant="ghost" size="icon" className="h-6 w-6 text-red-400 hover:text-red-300" onClick={() => onDelete(li.id)}>
<Trash2 className="h-3 w-3" />
</Button>
</div>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
{editItem && (
<LineItemDialog
open={!!editItem}
onOpenChange={(v) => !v && setEditItem(null)}
onSubmit={handleUpdate}
rateCards={rateCards}
initialData={{
taskName: editItem.taskName,
artistRole: editItem.artistRole ?? undefined,
rateCardId: editItem.rateCard?.id,
quantityHours: Number(editItem.quantityHours),
dailyRate: Number(editItem.dailyRate),
hoursPerDay: Number(editItem.hoursPerDay),
numArtists: Number(editItem.numArtists),
numDays: Number(editItem.numDays),
manualHourlyRate: editItem.manualHourlyRate != null ? Number(editItem.manualHourlyRate) : null,
manualSubtotal: editItem.manualSubtotal != null ? Number(editItem.manualSubtotal) : null,
markupPercent: Number(editItem.markupPercent),
discountPercent: Number(editItem.discountPercent),
taxPercent: Number(editItem.taxPercent),
notes: editItem.notes ?? undefined,
}}
defaultTasks={[]}
/>
)}
</>
);
}