"use client"; import { useState } from "react"; import { trpc } from "~/lib/trpc/client.js"; type EditingRule = { id?: string; chapter: string; location: string; level: string; costMultiplier: number; billMultiplier: number; shoringRatio: string; additionalEffortRatio: string; description: string; sortOrder: number; }; type EditingSet = { id?: string; name: string; description: string; isDefault: boolean; rules: EditingRule[]; }; const CHAPTER_PRESETS = [ "Animation", "Compositing", "3D Modeling", "3D Lighting", "3D Rigging", "3D Environment", "Motion Graphics", "Art Direction", "Project Management", ]; const LOCATION_PRESETS = [ "Germany", "India", "Poland", "Romania", "Spain", "UK", "USA", "Canada", ]; const LEVEL_PRESETS = [ "Junior", "Mid", "Senior", "Lead", "Principal", ]; const emptyRule: EditingRule = { chapter: "", location: "", level: "", costMultiplier: 1.0, billMultiplier: 1.0, shoringRatio: "", additionalEffortRatio: "", description: "", sortOrder: 0, }; const emptySet: EditingSet = { name: "", description: "", isDefault: false, rules: [], }; export function ExperienceMultipliersClient() { const utils = trpc.useUtils(); const { data: sets, isLoading } = trpc.experienceMultiplier.list.useQuery(); const createMutation = trpc.experienceMultiplier.create.useMutation({ onSuccess: () => { utils.experienceMultiplier.list.invalidate(); setEditing(null); }, }); const updateMutation = trpc.experienceMultiplier.update.useMutation({ onSuccess: () => { utils.experienceMultiplier.list.invalidate(); setEditing(null); }, }); const deleteMutation = trpc.experienceMultiplier.delete.useMutation({ onSuccess: () => utils.experienceMultiplier.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) => ({ ...(r.chapter ? { chapter: r.chapter } : {}), ...(r.location ? { location: r.location } : {}), ...(r.level ? { level: r.level } : {}), costMultiplier: r.costMultiplier, billMultiplier: r.billMultiplier, ...(r.shoringRatio !== "" ? { shoringRatio: parseFloat(r.shoringRatio) } : {}), ...(r.additionalEffortRatio !== "" ? { additionalEffortRatio: parseFloat(r.additionalEffortRatio) } : {}), ...(r.description ? { description: r.description } : {}), sortOrder: i, })), }; if (editing.id) { updateMutation.mutate({ id: editing.id, ...payload }); } else { createMutation.mutate(payload); } } function handleEdit(set: NonNullable[number]) { setEditing({ id: set.id, name: set.name, description: set.description ?? "", isDefault: set.isDefault, rules: set.rules.map((r) => ({ id: r.id, chapter: r.chapter ?? "", location: r.location ?? "", level: r.level ?? "", costMultiplier: r.costMultiplier, billMultiplier: r.billMultiplier, shoringRatio: r.shoringRatio != null ? String(r.shoringRatio) : "", additionalEffortRatio: r.additionalEffortRatio != null ? String(r.additionalEffortRatio) : "", 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 (

Experience Multipliers

Define rate and effort adjustments by chapter, location, and experience level.

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

{editing.id ? "Edit multiplier set" : "New multiplier 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 Multipliers" />
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 rate and effort adjustments.

) : (
{editing.rules.map((rule, i) => ( ))}
Chapter Location Level Cost mult. Bill mult. Shoring % Add. effort %
updateRule(i, { chapter: e.target.value })} list="chapter-presets" className="w-full rounded-lg border border-gray-200 px-2 py-1 text-sm" placeholder="Any" /> updateRule(i, { location: e.target.value })} list="location-presets" className="w-full rounded-lg border border-gray-200 px-2 py-1 text-sm" placeholder="Any" /> updateRule(i, { level: e.target.value })} list="level-presets" className="w-full rounded-lg border border-gray-200 px-2 py-1 text-sm" placeholder="Any" /> updateRule(i, { costMultiplier: parseFloat(e.target.value) || 0 })} className="w-20 rounded-lg border border-gray-200 px-2 py-1 text-right text-sm tabular-nums" /> updateRule(i, { billMultiplier: parseFloat(e.target.value) || 0 })} className="w-20 rounded-lg border border-gray-200 px-2 py-1 text-right text-sm tabular-nums" /> updateRule(i, { shoringRatio: e.target.value })} className="w-20 rounded-lg border border-gray-200 px-2 py-1 text-right text-sm tabular-nums" placeholder="-" /> updateRule(i, { additionalEffortRatio: e.target.value })} className="w-20 rounded-lg border border-gray-200 px-2 py-1 text-right text-sm tabular-nums" placeholder="-" />
)}
{CHAPTER_PRESETS.map((d) => ( {LOCATION_PRESETS.map((d) => ( {LEVEL_PRESETS.map((d) => (
{(createMutation.error || updateMutation.error) && (

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

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

Loading...

} {sets && sets.length === 0 && !editing && (
No experience multiplier sets yet. Create one to define rate and effort adjustments.
)} {sets?.map((s) => (

{s.name}

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

{s.description}

} {expandedId === s.id && s.rules.length > 0 && (
{s.rules.map((r) => ( ))}
Chapter Location Level Cost mult. Bill mult. Shoring Add. effort
{r.chapter || "\u2014"} {r.location || "\u2014"} {r.level || "\u2014"} {r.costMultiplier.toFixed(2)}x {r.billMultiplier.toFixed(2)}x {r.shoringRatio != null ? `${(r.shoringRatio * 100).toFixed(0)}%` : "\u2014"} {r.additionalEffortRatio != null ? `${(r.additionalEffortRatio * 100).toFixed(0)}%` : "\u2014"}
)}
))}
); }