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
@@ -2,7 +2,6 @@
import { useState } from "react";
import Link from "next/link";
import { useSession } from "next-auth/react";
import type { AllocationLike, AllocationReadModel, AllocationWithDetails, Resource, SkillEntry } from "@planarchy/shared";
import { trpc } from "~/lib/trpc/client.js";
import { formatDate } from "~/lib/format.js";
@@ -59,10 +58,10 @@ function StatCard({ label, value, sub }: { label: string; value: string | number
export function ResourceDetail({ resourceId }: ResourceDetailProps) {
const [editOpen, setEditOpen] = useState(false);
const [includeProposedChargeability, setIncludeProposedChargeability] = useState(false);
const [uploadOpen, setUploadOpen] = useState(false);
const utils = trpc.useUtils();
const { canViewCosts, canEdit, canViewScores } = usePermissions();
const { data: session } = useSession();
const _resourceQuery = trpc.resource.getById.useQuery({ id: resourceId });
const resource = _resourceQuery.data as unknown as Resource | undefined;
@@ -98,7 +97,7 @@ export function ResourceDetail({ resourceId }: ResourceDetailProps) {
);
const chargeabilityStatsResult = trpc.resource.getChargeabilityStats.useQuery(
{ resourceId },
{ includeProposed: includeProposedChargeability, resourceId },
{ enabled: canViewCosts, staleTime: 60_000 },
);
const chargeStats = (chargeabilityStatsResult.data as unknown as Array<{
@@ -156,10 +155,9 @@ export function ResourceDetail({ resourceId }: ResourceDetailProps) {
total: number;
} | null;
valueScoreUpdatedAt?: Date | null;
isOwnedByCurrentUser?: boolean;
};
const currentUserEmail = session?.user?.email;
const isOwner = !!(resourceWithMeta.userId && currentUserEmail &&
(resource as unknown as { user?: { email?: string } }).user?.email === currentUserEmail);
const isOwner = resourceWithMeta.isOwnedByCurrentUser === true;
const canUpload = isOwner || canEdit;
// Compute stats
@@ -260,6 +258,19 @@ export function ResourceDetail({ resourceId }: ResourceDetailProps) {
</div>
{/* Stats cards */}
{canViewCosts && (
<div className="flex justify-end">
<label className="flex items-center gap-2 text-sm text-gray-600">
<input
type="checkbox"
checked={includeProposedChargeability}
onChange={(event) => setIncludeProposedChargeability(event.target.checked)}
className="rounded border-gray-300"
/>
Include proposed in chargeability
</label>
</div>
)}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
{canViewCosts && (
<StatCard
@@ -278,17 +289,21 @@ export function ResourceDetail({ resourceId }: ResourceDetailProps) {
value={`${resource.chargeabilityTarget}%`}
/>
{canViewCosts && (
<StatCard
label="Actual (this month)"
value={chargeStats != null ? `${chargeStats.actualChargeability}%` : "—"}
sub="Excl. draft projects"
/>
<StatCard
label="Actual (this month)"
value={chargeStats != null ? `${chargeStats.actualChargeability}%` : "—"}
sub={
includeProposedChargeability
? "Incl. proposed + imported TBD planning"
: "Confirmed + active only"
}
/>
)}
{canViewCosts && (
<StatCard
label="Expected (this month)"
value={chargeStats != null ? `${chargeStats.expectedChargeability}%` : "—"}
sub="Incl. draft projects"
sub="All non-cancelled bookings"
/>
)}
<StatCard