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>
169 lines
7.7 KiB
TypeScript
169 lines
7.7 KiB
TypeScript
import { clsx } from "clsx";
|
|
|
|
interface ResourceMonthTemplateCompleteness {
|
|
scope: "resource_month";
|
|
isAuditReady: boolean;
|
|
isRecommendedComplete: boolean;
|
|
recommendedColumnCount: number;
|
|
selectedRecommendedColumnCount: number;
|
|
minimumAuditColumnCount: number;
|
|
selectedMinimumAuditColumnCount: number;
|
|
missingRecommendedColumns: string[];
|
|
missingMinimumAuditColumns: string[];
|
|
}
|
|
|
|
interface ResourceMonthConfigSectionProps<
|
|
TBlueprint extends { id: string; label: string; description: string },
|
|
> {
|
|
periodMonth: string;
|
|
onPeriodMonthChange: (value: string) => void;
|
|
blueprints: TBlueprint[];
|
|
onApplyBlueprint: (blueprint: TBlueprint) => void;
|
|
completeness: ResourceMonthTemplateCompleteness | null;
|
|
selectedTemplate: { isShared?: boolean; isOwner?: boolean } | null;
|
|
hasTemplateDraftChanges: boolean;
|
|
selectedColumns: Set<string>;
|
|
onToggleColumn: (column: string) => void;
|
|
columnLabelMap: Map<string, string>;
|
|
recommendedColumns: readonly string[];
|
|
summarizeMissing: (columns: string[], labelMap: Map<string, string>) => string;
|
|
}
|
|
|
|
export function ResourceMonthConfigSection<
|
|
TBlueprint extends { id: string; label: string; description: string },
|
|
>({
|
|
periodMonth,
|
|
onPeriodMonthChange,
|
|
blueprints,
|
|
onApplyBlueprint,
|
|
completeness,
|
|
selectedTemplate,
|
|
hasTemplateDraftChanges,
|
|
selectedColumns,
|
|
onToggleColumn,
|
|
columnLabelMap,
|
|
recommendedColumns,
|
|
summarizeMissing,
|
|
}: ResourceMonthConfigSectionProps<TBlueprint>) {
|
|
return (
|
|
<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) => onPeriodMonthChange(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">
|
|
{blueprints.map((blueprint) => (
|
|
<button
|
|
key={blueprint.id}
|
|
type="button"
|
|
onClick={() => onApplyBlueprint(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">
|
|
{completeness ? (
|
|
<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]",
|
|
completeness.isAuditReady
|
|
? "bg-emerald-500 text-white"
|
|
: "bg-amber-100 text-amber-800 dark:bg-amber-950/60 dark:text-amber-200",
|
|
)}
|
|
>
|
|
{completeness.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">
|
|
{completeness.selectedMinimumAuditColumnCount}/
|
|
{completeness.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">
|
|
{completeness.selectedRecommendedColumnCount}/{completeness.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>
|
|
{completeness.missingMinimumAuditColumns.length > 0 ? (
|
|
<p className="mt-3 text-xs text-amber-800 dark:text-amber-200">
|
|
Missing audit/export basis columns:{" "}
|
|
{summarizeMissing(completeness.missingMinimumAuditColumns, columnLabelMap)}
|
|
</p>
|
|
) : completeness.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:{" "}
|
|
{summarizeMissing(completeness.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">
|
|
{recommendedColumns.map((column) => (
|
|
<button
|
|
key={column}
|
|
type="button"
|
|
onClick={() => onToggleColumn(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>
|
|
);
|
|
}
|