"use client"; import { useState } from "react"; import { InfoTooltip } from "~/components/ui/InfoTooltip.js"; import { trpc } from "~/lib/trpc/client.js"; type EffortUnitMode = "per_frame" | "per_item" | "flat"; type EditingRule = { id?: string; scopeType: string; discipline: string; chapter: string; unitMode: EffortUnitMode; hoursPerUnit: number; description: string; sortOrder: number; }; type EditingRuleSet = { id?: string; name: string; description: string; isDefault: boolean; rules: EditingRule[]; }; const UNIT_MODE_LABELS: Record = { per_frame: "Per frame", per_item: "Per item", flat: "Flat", }; const SCOPE_TYPE_PRESETS = ["SHOT", "ASSET", "ENVIRONMENT", "SEQUENCE", "OTHER"]; const DISCIPLINE_PRESETS = [ "3D Animation", "3D Lighting", "3D Modeling", "3D Rigging", "3D Environment", "Compositing", "Motion Graphics", "Art Direction", "Conception / R&D", "Project Management", "Production Supervisor", "DataPrep", "Audio Production", ]; const emptyRule: EditingRule = { scopeType: "SHOT", discipline: "", chapter: "", unitMode: "per_frame", hoursPerUnit: 0, description: "", sortOrder: 0, }; const emptyRuleSet: EditingRuleSet = { name: "", description: "", isDefault: false, rules: [], }; export function EffortRulesClient() { const utils = trpc.useUtils(); const { data: ruleSets, isLoading } = trpc.effortRule.list.useQuery(); const createMutation = trpc.effortRule.create.useMutation({ onSuccess: () => { utils.effortRule.list.invalidate(); setEditing(null); }, }); const updateMutation = trpc.effortRule.update.useMutation({ onSuccess: () => { utils.effortRule.list.invalidate(); setEditing(null); }, }); const deleteMutation = trpc.effortRule.delete.useMutation({ onSuccess: () => utils.effortRule.list.invalidate(), }); const [editing, setEditing] = useState(null); const [expandedId, setExpandedId] = useState(null); function handleSave() { if (!editing) return; const payload = { name: editing.name, description: editing.description || undefined, isDefault: editing.isDefault, rules: editing.rules.map((r, i) => ({ scopeType: r.scopeType, discipline: r.discipline, ...(r.chapter ? { chapter: r.chapter } : {}), unitMode: r.unitMode, hoursPerUnit: r.hoursPerUnit, ...(r.description ? { description: r.description } : {}), sortOrder: i, })), }; if (editing.id) { updateMutation.mutate({ id: editing.id, ...payload }); } else { createMutation.mutate(payload); } } function handleEdit(ruleSet: NonNullable[number]) { setEditing({ id: ruleSet.id, name: ruleSet.name, description: ruleSet.description ?? "", isDefault: ruleSet.isDefault, rules: ruleSet.rules.map((r) => ({ id: r.id, scopeType: r.scopeType, discipline: r.discipline, chapter: r.chapter ?? "", unitMode: r.unitMode as EffortUnitMode, hoursPerUnit: r.hoursPerUnit, description: r.description ?? "", sortOrder: r.sortOrder, })), }); } function addRule() { if (!editing) return; setEditing({ ...editing, rules: [...editing.rules, { ...emptyRule, sortOrder: editing.rules.length }], }); } function removeRule(index: number) { if (!editing) return; setEditing({ ...editing, rules: editing.rules.filter((_, i) => i !== index), }); } function updateRule(index: number, updates: Partial) { if (!editing) return; setEditing({ ...editing, rules: editing.rules.map((r, i) => (i === index ? { ...r, ...updates } : r)), }); } const isSaving = createMutation.isPending || updateMutation.isPending; return (

Effort Rules

Define rules for auto-generating demand lines from scope items.

{!editing && ( )}
{/* Editor */} {editing && (

{editing.id ? "Edit rule set" : "New rule set"}

setEditing({ ...editing, name: e.target.value })} className="w-full rounded-xl border border-gray-300 px-3 py-2 text-sm" placeholder="e.g. CGI Standard Rules" />
setEditing({ ...editing, description: e.target.value })} className="w-full rounded-xl border border-gray-300 px-3 py-2 text-sm" placeholder="Optional description" />
{/* Rules table */}

Rules ({editing.rules.length})

{editing.rules.length === 0 ? (

No rules yet. Add rules to define how scope items expand into demand lines.

) : (
{editing.rules.map((rule, i) => ( ))}
Scope type Discipline Chapter Unit mode Hours/unit
updateRule(i, { discipline: e.target.value })} list="discipline-presets" className="w-full rounded-lg border border-gray-200 px-2 py-1 text-sm" placeholder="Discipline" /> updateRule(i, { chapter: e.target.value })} className="w-full rounded-lg border border-gray-200 px-2 py-1 text-sm" placeholder="Chapter" /> updateRule(i, { hoursPerUnit: parseFloat(e.target.value) || 0 })} className="w-20 rounded-lg border border-gray-200 px-2 py-1 text-right text-sm tabular-nums" />
)}
{DISCIPLINE_PRESETS.map((d) => (
{(createMutation.error || updateMutation.error) && (

{createMutation.error?.message || updateMutation.error?.message}

)}
)} {/* List */} {isLoading &&

Loading...

} {ruleSets && ruleSets.length === 0 && !editing && (
No effort rule sets yet. Create one to define how scope items expand into demand lines.
)} {ruleSets?.map((rs) => (

{rs.name}

{rs.isDefault && ( Default )} {rs.rules.length} rules
{rs.description &&

{rs.description}

} {expandedId === rs.id && rs.rules.length > 0 && (
{rs.rules.map((r) => ( ))}
Scope type Discipline Chapter Unit mode Hours/unit
{r.scopeType} {r.discipline} {r.chapter || "\u2014"} {UNIT_MODE_LABELS[r.unitMode as EffortUnitMode] ?? r.unitMode} {r.hoursPerUnit}
)}
))}
); }