From 4accee95a45510cd471f817d1a57b0ea17e5ec47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hartmut=20N=C3=B6renberg?= Date: Tue, 7 Apr 2026 16:06:58 +0200 Subject: [PATCH] refactor(dispo): clean up validate UI in NewImportModal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Extract EMPTY_VALIDATE_INPUT as module constant (prevents new object on every render) - Extract IssueList component + ISSUE_STYLES map (eliminates blocker/warning copy-paste) - Extract ReadinessIssue type from ReadinessReport - Reuse buildValidateInput in handleSubmit (single source for path mapping) - Guard setValidateInput(null) in onChange — only resets when not already null - Remove unnecessary `as ReadinessReport` cast (tRPC infers the type) Co-Authored-By: Claude Sonnet 4.6 --- .../components/admin/DispoImportClient.tsx | 127 ++++++++---------- 1 file changed, 56 insertions(+), 71 deletions(-) diff --git a/apps/web/src/components/admin/DispoImportClient.tsx b/apps/web/src/components/admin/DispoImportClient.tsx index 1e78c74..25da6c3 100644 --- a/apps/web/src/components/admin/DispoImportClient.tsx +++ b/apps/web/src/components/admin/DispoImportClient.tsx @@ -65,32 +65,72 @@ const WORKBOOK_LABELS: { key: string; label: string; placeholder: string }[] = [ { key: "roles", label: "Roles Workbook", placeholder: "/data/dispo/roles.xlsx" }, ]; +const EMPTY_VALIDATE_INPUT = { + referenceWorkbookPath: "", + planningWorkbookPath: "", + chargeabilityWorkbookPath: "", +} as const; + +type ReadinessIssue = { + code: string; + count: number; + message: string; + resolution: string; + severity: "blocker" | "warning"; +}; + type ReadinessReport = { assignmentCount: number; availabilityRuleCount: number; canCommitWithFallbacks: boolean; canCommitWithStrictSourceData: boolean; fallbackAssumptions: string[]; - issues: { - code: string; - count: number; - message: string; - resolution: string; - severity: "blocker" | "warning"; - }[]; + issues: ReadinessIssue[]; projectCount: number; resourceCount: number; unresolvedCount: number; vacationCount: number; }; +const ISSUE_STYLES = { + blocker: { + container: "rounded bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 px-3 py-2", + title: "text-xs font-medium text-red-700 dark:text-red-400", + resolution: "text-xs text-red-600/80 dark:text-red-300/70 mt-0.5", + }, + warning: { + container: "rounded bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 px-3 py-2", + title: "text-xs font-medium text-amber-700 dark:text-amber-400", + resolution: "text-xs text-amber-600/80 dark:text-amber-300/70 mt-0.5", + }, +} as const; + +function IssueList({ issues }: { issues: ReadinessIssue[] }) { + if (issues.length === 0) return null; + return ( +
+ {issues.map((issue) => { + const s = ISSUE_STYLES[issue.severity]; + return ( +
+

+ {issue.message} + {issue.count > 1 && ({issue.count})} +

+

{issue.resolution}

+
+ ); + })} +
+ ); +} + function ReadinessReportPanel({ report }: { report: ReadinessReport }) { const blockers = report.issues.filter((i) => i.severity === "blocker"); const warnings = report.issues.filter((i) => i.severity === "warning"); return (
- {/* Status line */} {report.canCommitWithStrictSourceData ? (

Ready to stage — no blockers found. @@ -105,7 +145,6 @@ function ReadinessReportPanel({ report }: { report: ReadinessReport }) {

)} - {/* Counts */}
{report.resourceCount} resources {report.projectCount} projects @@ -118,51 +157,9 @@ function ReadinessReportPanel({ report }: { report: ReadinessReport }) { )}
- {/* Blockers */} - {blockers.length > 0 && ( -
- {blockers.map((issue) => ( -
-

- {issue.message} - {issue.count > 1 && ( - ({issue.count}) - )} -

-

- {issue.resolution} -

-
- ))} -
- )} + + - {/* Warnings */} - {warnings.length > 0 && ( -
- {warnings.map((issue) => ( -
-

- {issue.message} - {issue.count > 1 && ( - ({issue.count}) - )} -

-

- {issue.resolution} -

-
- ))} -
- )} - - {/* Fallback assumptions */} {report.fallbackAssumptions.length > 0 && (

@@ -206,7 +203,7 @@ function NewImportModal({ const [validateInput, setValidateInput] = useState | null>(null); const validateQuery = trpc.dispo.validateImportBatch.useQuery( - validateInput ?? { referenceWorkbookPath: "", planningWorkbookPath: "", chargeabilityWorkbookPath: "" }, + validateInput ?? EMPTY_VALIDATE_INPUT, { enabled: validateInput !== null, retry: false }, ); @@ -233,24 +230,12 @@ function NewImportModal({ function handleSubmit() { setError(null); - const get = (key: string) => (filePaths[key] ?? "").trim(); - const referenceWorkbookPath = get("resources"); - const planningWorkbookPath = get("projects"); - const chargeabilityWorkbookPath = get("assignments"); - const rosterWorkbookPath = get("vacations"); - const costWorkbookPath = get("roles"); - - if (!referenceWorkbookPath && !planningWorkbookPath && !chargeabilityWorkbookPath) { + const input = buildValidateInput(filePaths); + if (!input.referenceWorkbookPath && !input.planningWorkbookPath && !input.chargeabilityWorkbookPath) { setError("Provide at least one workbook path."); return; } - stageMutation.mutate({ - referenceWorkbookPath, - planningWorkbookPath, - chargeabilityWorkbookPath, - ...(rosterWorkbookPath ? { rosterWorkbookPath } : {}), - ...(costWorkbookPath ? { costWorkbookPath } : {}), - } as any); + stageMutation.mutate(input as any); } function handleClose() { @@ -279,7 +264,7 @@ function NewImportModal({ value={filePaths[key] ?? ""} onChange={(e) => { setFilePaths((prev) => ({ ...prev, [key]: e.target.value })); - setValidateInput(null); + if (validateInput !== null) setValidateInput(null); }} />

@@ -291,7 +276,7 @@ function NewImportModal({

Validating…

)} {!validateQuery.isFetching && validateQuery.data && ( - + )} {validateQuery.error && (