feat: dashboard overhaul, chargeability reports, dispo import enhancements, UI polish

Dashboard: expanded chargeability widget, resource/project table widgets
with sorting and filters, stat cards with formatMoney integration.

Chargeability: new report client with filtering, chargeability-bookings
use case, updated dashboard overview logic.

Dispo import: TBD project handling, parse-dispo-matrix improvements,
stage-dispo-projects resource value scores, new tests.

Estimates: CommercialTermsEditor component, commercial-terms engine
module, expanded estimate schemas and types.

UI: AppShell navigation updates, timeline filter/toolbar enhancements,
role management improvements, signin page redesign, Tailwind/globals
polish, SystemSettings SMTP section, anonymization support.

Tests: new router tests (anonymization, chargeability, effort-rule,
entitlement, estimate, experience-multiplier, notification, resource,
staffing, vacation).

Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
2026-03-14 23:29:07 +01:00
parent ad0855902b
commit 625a842d89
74 changed files with 11680 additions and 1583 deletions
@@ -11,6 +11,9 @@ interface RoleAssignment {
isPrimary: boolean;
}
type CountryWithCities = { id: string; metroCities: { id: string; name: string }[] };
type ManagementGroupWithLevels = { id: string; levels: { id: string; name: string }[] };
interface SkillRow {
skill: string;
proficiency: 1 | 2 | 3 | 4 | 5;
@@ -47,6 +50,8 @@ interface FormState {
managementLevelId: string;
resourceType: string;
chgResponsibility: boolean;
rolledOff: boolean;
departed: boolean;
enterpriseId: string;
clientUnitId: string;
fte: string;
@@ -99,6 +104,8 @@ function resourceToFormState(resource: Resource): FormState {
managementLevelId: (resource as unknown as { managementLevelId?: string | null }).managementLevelId ?? "",
resourceType: (resource as unknown as { resourceType?: string }).resourceType ?? "EMPLOYEE",
chgResponsibility: (resource as unknown as { chgResponsibility?: boolean }).chgResponsibility ?? true,
rolledOff: (resource as unknown as { rolledOff?: boolean }).rolledOff ?? false,
departed: (resource as unknown as { departed?: boolean }).departed ?? false,
enterpriseId: (resource as unknown as { enterpriseId?: string | null }).enterpriseId ?? "",
clientUnitId: (resource as unknown as { clientUnitId?: string | null }).clientUnitId ?? "",
fte: String((resource as unknown as { fte?: number }).fte ?? 1),
@@ -133,6 +140,8 @@ function defaultFormState(): FormState {
managementLevelId: "",
resourceType: "EMPLOYEE",
chgResponsibility: true,
rolledOff: false,
departed: false,
enterpriseId: "",
clientUnitId: "",
fte: "1",
@@ -197,11 +206,13 @@ export function ResourceModal({ mode, resource, onClose }: ResourceModalProps) {
const { data: clients } = trpc.clientEntity.list.useQuery(undefined, { staleTime: 60_000 });
// Derive metro cities from selected country
const selectedCountry = (countries ?? []).find((c) => c.id === form.countryId) as unknown as { id: string; metroCities: { id: string; name: string }[] } | undefined;
const countryRows = (countries ?? []) as unknown as CountryWithCities[];
const selectedCountry = countryRows.find((c) => c.id === form.countryId);
const metroCities = selectedCountry?.metroCities ?? [];
// Derive levels from selected group
const selectedGroup = (mgmtGroups ?? []).find((g) => g.id === form.managementLevelGroupId) as unknown as { id: string; levels: { id: string; name: string }[] } | undefined;
const managementGroups = (mgmtGroups ?? []) as unknown as ManagementGroupWithLevels[];
const selectedGroup = managementGroups.find((g) => g.id === form.managementLevelGroupId);
const mgmtLevels = selectedGroup?.levels ?? [];
const createMutation = trpc.resource.create.useMutation({
@@ -294,6 +305,8 @@ export function ResourceModal({ mode, resource, onClose }: ResourceModalProps) {
...(form.managementLevelId ? { managementLevelId: form.managementLevelId } : {}),
resourceType: form.resourceType as ResourceType,
chgResponsibility: form.chgResponsibility,
rolledOff: form.rolledOff,
departed: form.departed,
...(form.enterpriseId.trim() !== "" ? { enterpriseId: form.enterpriseId.trim() } : {}),
...(form.clientUnitId ? { clientUnitId: form.clientUnitId } : {}),
fte: parseFloat(form.fte) || 1,
@@ -628,7 +641,7 @@ export function ResourceModal({ mode, resource, onClose }: ResourceModalProps) {
</div>
</div>
<div className="grid grid-cols-3 gap-4 mt-4">
<div className="grid grid-cols-4 gap-4 mt-4">
<div>
<label className={LABEL_CLASS} htmlFor="rm-resourceType">Resource Type</label>
<select
@@ -653,6 +666,28 @@ export function ResourceModal({ mode, resource, onClose }: ResourceModalProps) {
Chg Responsibility
</label>
</div>
<div className="flex items-end pb-2">
<label className="flex items-center gap-2 text-sm text-gray-700 dark:text-gray-300 cursor-pointer">
<input
type="checkbox"
checked={form.rolledOff}
onChange={(e) => setField("rolledOff", e.target.checked)}
className="rounded border-gray-300 text-brand-600 focus:ring-brand-500"
/>
Rolled Off
</label>
</div>
<div className="flex items-end pb-2">
<label className="flex items-center gap-2 text-sm text-gray-700 dark:text-gray-300 cursor-pointer">
<input
type="checkbox"
checked={form.departed}
onChange={(e) => setField("departed", e.target.checked)}
className="rounded border-gray-300 text-brand-600 focus:ring-brand-500"
/>
Departed
</label>
</div>
</div>
{/* Section 2: Cost & Chargeability */}