refactor(web): decompose TimelineView, ReportBuilder, and ResourceModal into focused components
Extract overlay/popover JSX from TimelineView (1268→1037 lines) into TimelineDragOverlays and TimelinePopovers. Extract ResourceMonthConfigSection from ReportBuilder (1132→1018 lines). Extract ResourceSkillsEditor and ResourceOrgClassification from ResourceModal (1035→714 lines). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -10,6 +10,7 @@ import {
|
||||
type ReportExplainability,
|
||||
} from "./reportBuilderExplainability.js";
|
||||
import { ReportResultsPanel } from "./ReportResultsPanel.js";
|
||||
import { ResourceMonthConfigSection } from "./ResourceMonthConfigSection.js";
|
||||
|
||||
// ─── Types ──────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -753,135 +754,20 @@ export function ReportBuilder() {
|
||||
))}
|
||||
</div>
|
||||
{entity === "resource_month" && (
|
||||
<div className="mt-4 space-y-4 rounded-2xl border border-emerald-200 bg-emerald-50/70 p-4 dark:border-emerald-900/60 dark:bg-emerald-950/20">
|
||||
<div className="flex flex-wrap items-end gap-4">
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-emerald-900 dark:text-emerald-200">
|
||||
Period month
|
||||
</label>
|
||||
<input
|
||||
type="month"
|
||||
value={periodMonth}
|
||||
onChange={(e) => setPeriodMonth(e.target.value)}
|
||||
className="rounded-xl border border-emerald-300 bg-white px-3 py-2 text-sm text-gray-700 focus:border-emerald-500 focus:ring-emerald-500 dark:border-emerald-900 dark:bg-slate-950 dark:text-gray-300"
|
||||
/>
|
||||
</div>
|
||||
<p className="max-w-2xl text-sm text-emerald-900/80 dark:text-emerald-200/80">
|
||||
Resource Months uses the CapaKraken holiday and absence logic directly. SAH,
|
||||
booked hours and chargeability are calculated per resource and month with country,
|
||||
state and city context.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-3 lg:grid-cols-3">
|
||||
{resourceMonthBlueprints.map((blueprint) => (
|
||||
<button
|
||||
key={blueprint.id}
|
||||
type="button"
|
||||
onClick={() => applyBlueprint(blueprint)}
|
||||
className="rounded-2xl border border-emerald-200 bg-white/80 p-4 text-left transition hover:border-emerald-400 hover:bg-white dark:border-emerald-900/70 dark:bg-slate-950/60 dark:hover:border-emerald-700"
|
||||
>
|
||||
<div className="text-sm font-semibold text-emerald-950 dark:text-emerald-100">
|
||||
{blueprint.label}
|
||||
</div>
|
||||
<p className="mt-1 text-xs leading-5 text-emerald-900/75 dark:text-emerald-200/75">
|
||||
{blueprint.description}
|
||||
</p>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="rounded-2xl border border-emerald-200/80 bg-white/60 p-4 dark:border-emerald-900/60 dark:bg-slate-950/40">
|
||||
{displayedResourceMonthCompleteness ? (
|
||||
<div className="mb-4 rounded-2xl border border-emerald-200/80 bg-emerald-50/80 p-4 dark:border-emerald-900/60 dark:bg-emerald-950/20">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span
|
||||
className={clsx(
|
||||
"rounded-full px-2.5 py-1 text-[11px] font-semibold uppercase tracking-[0.14em]",
|
||||
displayedResourceMonthCompleteness.isAuditReady
|
||||
? "bg-emerald-500 text-white"
|
||||
: "bg-amber-100 text-amber-800 dark:bg-amber-950/60 dark:text-amber-200",
|
||||
)}
|
||||
>
|
||||
{displayedResourceMonthCompleteness.isAuditReady
|
||||
? "Audit ready"
|
||||
: "Audit gap"}
|
||||
</span>
|
||||
<span className="rounded-full bg-white px-2.5 py-1 text-[11px] font-medium text-emerald-900 dark:bg-slate-950 dark:text-emerald-100">
|
||||
{displayedResourceMonthCompleteness.selectedMinimumAuditColumnCount}/
|
||||
{displayedResourceMonthCompleteness.minimumAuditColumnCount} minimum audit
|
||||
columns
|
||||
</span>
|
||||
<span className="rounded-full bg-white px-2.5 py-1 text-[11px] font-medium text-emerald-900 dark:bg-slate-950 dark:text-emerald-100">
|
||||
{displayedResourceMonthCompleteness.selectedRecommendedColumnCount}/
|
||||
{displayedResourceMonthCompleteness.recommendedColumnCount} recommended
|
||||
columns
|
||||
</span>
|
||||
<span className="rounded-full bg-white px-2.5 py-1 text-[11px] text-emerald-900/80 dark:bg-slate-950 dark:text-emerald-200/80">
|
||||
{selectedTemplate && !hasTemplateDraftChanges
|
||||
? "Saved template status"
|
||||
: "Current builder status"}
|
||||
</span>
|
||||
</div>
|
||||
{displayedResourceMonthCompleteness.missingMinimumAuditColumns.length > 0 ? (
|
||||
<p className="mt-3 text-xs text-amber-800 dark:text-amber-200">
|
||||
Missing audit/export basis columns:{" "}
|
||||
{summarizeMissingColumns(
|
||||
displayedResourceMonthCompleteness.missingMinimumAuditColumns,
|
||||
columnLabelMap,
|
||||
)}
|
||||
</p>
|
||||
) : displayedResourceMonthCompleteness.missingRecommendedColumns.length > 0 ? (
|
||||
<p className="mt-3 text-xs text-emerald-900/80 dark:text-emerald-200/80">
|
||||
Audit-ready, but still missing recommended basis columns:{" "}
|
||||
{summarizeMissingColumns(
|
||||
displayedResourceMonthCompleteness.missingRecommendedColumns,
|
||||
columnLabelMap,
|
||||
)}
|
||||
</p>
|
||||
) : (
|
||||
<p className="mt-3 text-xs text-emerald-900/80 dark:text-emerald-200/80">
|
||||
This view includes the full recommended audit/export basis set for monthly
|
||||
SAH and chargeability checks.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
<div className="text-sm font-medium text-emerald-950 dark:text-emerald-100">
|
||||
Recommended transparency columns
|
||||
</div>
|
||||
<div className="mt-2 flex flex-wrap gap-2">
|
||||
{RESOURCE_MONTH_RECOMMENDED_COLUMNS.map((column) => (
|
||||
<button
|
||||
key={column}
|
||||
type="button"
|
||||
onClick={() => toggleColumn(column)}
|
||||
className={clsx(
|
||||
"rounded-full border px-3 py-1 text-xs font-medium transition",
|
||||
selectedColumns.has(column)
|
||||
? "border-emerald-500 bg-emerald-500 text-white"
|
||||
: "border-emerald-200 bg-white text-emerald-900 hover:border-emerald-400 dark:border-emerald-900 dark:bg-slate-950 dark:text-emerald-200 dark:hover:border-emerald-700",
|
||||
)}
|
||||
>
|
||||
{columnLabelMap.get(column) ?? column}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<p className="mt-3 text-xs text-emerald-900/75 dark:text-emerald-200/75">
|
||||
Formula reference: base available hours - holiday deduction - absence deduction =
|
||||
monthly SAH. Chargeability uses booked hours divided by monthly SAH.
|
||||
</p>
|
||||
<p className="mt-2 text-xs text-emerald-900/75 dark:text-emerald-200/75">
|
||||
Export recommendation: include both basis columns and computed metrics in the CSV.
|
||||
That keeps Excel as a review layer instead of rebuilding CapaKraken logic outside
|
||||
the product.
|
||||
</p>
|
||||
<p className="mt-2 text-xs text-emerald-900/75 dark:text-emerald-200/75">
|
||||
Minimum audit set: month, location context, SAH, holiday deductions, absence
|
||||
deductions, target hours, booked hours and unassigned hours.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<ResourceMonthConfigSection
|
||||
periodMonth={periodMonth}
|
||||
onPeriodMonthChange={setPeriodMonth}
|
||||
blueprints={resourceMonthBlueprints}
|
||||
onApplyBlueprint={applyBlueprint}
|
||||
completeness={displayedResourceMonthCompleteness}
|
||||
selectedTemplate={selectedTemplate}
|
||||
hasTemplateDraftChanges={hasTemplateDraftChanges}
|
||||
selectedColumns={selectedColumns}
|
||||
onToggleColumn={toggleColumn}
|
||||
columnLabelMap={columnLabelMap}
|
||||
recommendedColumns={RESOURCE_MONTH_RECOMMENDED_COLUMNS}
|
||||
summarizeMissing={summarizeMissingColumns}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user