"use client"; import { useState } from "react"; import { AnimatedModal } from "~/components/ui/AnimatedModal.js"; import { ConfirmDialog } from "~/components/ui/ConfirmDialog.js"; import { InfoTooltip } from "~/components/ui/InfoTooltip.js"; import { trpc } from "~/lib/trpc/client.js"; const TRIGGER_TYPES = ["SICK", "VACATION", "PUBLIC_HOLIDAY", "CUSTOM"] as const; const COST_EFFECTS = ["CHARGE", "ZERO", "REDUCE"] as const; const CHARGEABILITY_EFFECTS = ["COUNT", "SKIP"] as const; const ORDER_TYPES = ["BD", "CHARGEABLE", "INTERNAL", "OVERHEAD"] as const; const TRIGGER_LABELS: Record = { SICK: "Sick Leave", VACATION: "Vacation", PUBLIC_HOLIDAY: "Public Holiday", CUSTOM: "Custom", }; const COST_LABELS: Record = { CHARGE: "Charge to Project", ZERO: "No Project Cost", REDUCE: "Reduced Cost", }; const CHG_LABELS: Record = { COUNT: "Person Chargeable", SKIP: "Not Chargeable", }; type RuleRow = { id: string; name: string; description: string | null; triggerType: string; projectId: string | null; orderType: string | null; costEffect: string; costReductionPercent: number | null; chargeabilityEffect: string; priority: number; isActive: boolean; project?: { id: string; name: string; shortCode: string } | null; }; type EditingRule = { id?: string; name: string; description: string; triggerType: string; projectId: string; orderType: string; costEffect: string; costReductionPercent: number; chargeabilityEffect: string; priority: number; isActive: boolean; }; const emptyRule: EditingRule = { name: "", description: "", triggerType: "SICK", projectId: "", orderType: "", costEffect: "ZERO", costReductionPercent: 0, chargeabilityEffect: "COUNT", priority: 0, isActive: true, }; export function CalculationRulesClient() { const [editing, setEditing] = useState(null); const [confirmDeleteRule, setConfirmDeleteRule] = useState(null); const [error, setError] = useState(null); const utils = trpc.useUtils(); const { data: rules, isLoading } = trpc.calculationRule.list.useQuery(); const createMut = trpc.calculationRule.create.useMutation({ onSuccess: () => { void utils.calculationRule.list.invalidate(); setEditing(null); }, onError: (e) => setError(e.message), }); const updateMut = trpc.calculationRule.update.useMutation({ onSuccess: () => { void utils.calculationRule.list.invalidate(); setEditing(null); }, onError: (e) => setError(e.message), }); const deleteMut = trpc.calculationRule.delete.useMutation({ onSuccess: () => { void utils.calculationRule.list.invalidate(); }, onError: (e) => setError(e.message), }); function openCreate() { setEditing({ ...emptyRule }); setError(null); } function openEdit(r: RuleRow) { setEditing({ id: r.id, name: r.name, description: r.description ?? "", triggerType: r.triggerType, projectId: r.projectId ?? "", orderType: r.orderType ?? "", costEffect: r.costEffect, costReductionPercent: r.costReductionPercent ?? 0, chargeabilityEffect: r.chargeabilityEffect, priority: r.priority, isActive: r.isActive, }); setError(null); } function handleSave() { if (!editing) return; setError(null); const payload = { name: editing.name, triggerType: editing.triggerType as (typeof TRIGGER_TYPES)[number], costEffect: editing.costEffect as (typeof COST_EFFECTS)[number], chargeabilityEffect: editing.chargeabilityEffect as (typeof CHARGEABILITY_EFFECTS)[number], priority: editing.priority, isActive: editing.isActive, ...(editing.description ? { description: editing.description } : {}), ...(editing.projectId ? { projectId: editing.projectId } : {}), ...(editing.orderType ? { orderType: editing.orderType } : {}), ...(editing.costEffect === "REDUCE" ? { costReductionPercent: editing.costReductionPercent } : {}), }; if (editing.id) { updateMut.mutate({ id: editing.id, ...payload }); } else { createMut.mutate(payload); } } if (isLoading) return
Loading...
; return (

Calculation Rules

Configure how absences affect project costs and chargeability reporting.

{error && (
{error}
)} {/* ── Rules Table ── */}
{(rules ?? []).map((r) => { const rule = r as unknown as RuleRow; return ( ); })} {(rules ?? []).length === 0 && ( )}
Name Trigger Cost Effect Chargeability Scope Priority Active Actions
{rule.name} {TRIGGER_LABELS[rule.triggerType] ?? rule.triggerType} {COST_LABELS[rule.costEffect] ?? rule.costEffect} {rule.costEffect === "REDUCE" && rule.costReductionPercent != null && ( ({rule.costReductionPercent}%) )} {CHG_LABELS[rule.chargeabilityEffect] ?? rule.chargeabilityEffect} {rule.project ? rule.project.shortCode : rule.orderType ? rule.orderType : "Global"} {rule.priority}
No calculation rules configured. Add a rule to control how absences affect costs and chargeability.
{/* ── Edit/Create Modal ── */} setEditing(null)} maxWidth="max-w-lg"> {editing && (<>

{editing.id ? "Edit Rule" : "New Rule"}

setEditing({ ...editing, name: e.target.value })} className="w-full rounded-md border px-3 py-2 text-sm dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100" />