Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/app/(auth)/forgot-password/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ export default function ForgotPasswordPage() {
alt="SiteSpace"
width={160}
height={48}
className="h-10 w-auto brightness-0 invert"
className="h-10 brightness-0 invert"
style={{ width: "auto" }}
priority
/>
</Link>
Expand Down
3 changes: 2 additions & 1 deletion src/app/(auth)/login/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ export default async function LoginPage({
alt="SiteSpace"
width={160}
height={48}
className="h-10 w-auto brightness-0 invert"
className="h-10 brightness-0 invert"
style={{ width: "auto" }}
priority
/>
</Link>
Expand Down
3 changes: 2 additions & 1 deletion src/app/(auth)/register/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ export default function RegisterPage() {
alt="SiteSpace"
width={160}
height={48}
className="h-10 w-auto brightness-0 invert"
className="h-10 brightness-0 invert"
style={{ width: "auto" }}
priority
/>
</Link>
Expand Down
3 changes: 2 additions & 1 deletion src/app/(auth)/reset-password/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ export default async function ResetPasswordPage({
alt="SiteSpace"
width={160}
height={48}
className="h-10 w-auto brightness-0 invert"
className="h-10 brightness-0 invert"
style={{ width: "auto" }}
priority
/>
</Link>
Expand Down
3 changes: 2 additions & 1 deletion src/app/(auth)/set-password/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ export default async function SetPasswordPage({
alt="SiteSpace"
width={160}
height={48}
className="h-10 w-auto brightness-0 invert"
className="h-10 brightness-0 invert"
style={{ width: "auto" }}
priority
/>
</Link>
Expand Down
69 changes: 35 additions & 34 deletions src/app/(dashboard)/assets/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,11 +83,13 @@ const transformBackendAsset = (backendAsset: ApiAsset): Asset => {
usageInstructions: backendAsset.usage_instructions || "",
assetCode: backendAsset.asset_code,
pendingBookingCapacity: backendAsset.pending_booking_capacity,
maxHoursPerDay: backendAsset.max_hours_per_day ?? null,
canonicalType: backendAsset.canonical_type ?? null,
typeResolutionStatus: backendAsset.type_resolution_status ?? null,
typeInferenceSource: backendAsset.type_inference_source ?? null,
typeInferenceConfidence: backendAsset.type_inference_confidence ?? null,
planningReady: backendAsset.planning_ready,
capacityReady: backendAsset.capacity_ready,
_originalData: backendAsset,
};
};
Expand Down Expand Up @@ -272,11 +274,11 @@ export default function AssetsTable() {
}

if (readinessFilter === "ready") {
result = result.filter((asset) => asset.planningReady === true);
result = result.filter((asset) => Boolean(asset.capacityReady));
}

if (readinessFilter === "review") {
result = result.filter((asset) => asset.planningReady !== true);
result = result.filter((asset) => !asset.capacityReady);
}

// Sort
Expand Down Expand Up @@ -357,7 +359,7 @@ export default function AssetsTable() {
(a) => a.assetStatus === "Maintenance",
).length;
const planningReadyCount = (allAssets ?? []).filter(
(asset) => asset.planningReady === true,
(asset) => Boolean(asset.capacityReady),
).length;
const emptyStateMessage = searchTerm.trim()
? `No assets found matching "${searchTerm.trim()}".`
Expand Down Expand Up @@ -621,27 +623,21 @@ export default function AssetsTable() {
>
{asset.assetDescription}
</span>
{(asset.planningReady !== undefined ||
asset.typeResolutionStatus) && (
{(asset.typeResolutionStatus || asset.maxHoursPerDay != null) && (
<div className="mt-1 flex flex-wrap gap-1">
{asset.planningReady !== undefined && (
<span
className={`inline-flex rounded-full px-2 py-0.5 text-[10px] font-semibold ${
asset.planningReady
? "bg-emerald-50 text-emerald-700"
: "bg-amber-50 text-amber-700"
}`}
>
{asset.planningReady
? "Planning ready"
: "Needs type review"}
</span>
)}
{asset.typeResolutionStatus && (
<span className="inline-flex rounded-full bg-slate-100 px-2 py-0.5 text-[10px] font-semibold text-slate-600">
{asset.typeResolutionStatus}
</span>
)}
<span
className={`inline-flex rounded-full px-2 py-0.5 text-[10px] font-semibold ${
asset.capacityReady
? "bg-emerald-50 text-emerald-700"
: "bg-amber-50 text-amber-700"
}`}
>
{asset.capacityReady
? "Planning ready"
: asset.maxHoursPerDay == null
? "Capacity not set"
: "Needs review"}
</span>
</div>
)}
</div>
Expand Down Expand Up @@ -801,29 +797,34 @@ export default function AssetsTable() {
</span>
</div>
{(() => {
if (selectedAsset.planningReady === true) {
if (selectedAsset.capacityReady) {
return (
<span className="text-sm font-semibold text-emerald-700">
Yes
</span>
);
}

if (selectedAsset.planningReady === false) {
return (
<span className="text-sm font-semibold text-amber-700">
No
</span>
);
}

return (
<span className="text-sm font-semibold text-slate-500">
Unknown
<span className="text-sm font-semibold text-amber-700">
No
</span>
);
})()}
</div>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<Clock className="h-4 w-4 text-slate-400" />
<span className="text-sm font-medium text-slate-500">
Max Hours / Day
</span>
</div>
<span className="text-sm font-semibold text-slate-900">
{selectedAsset.maxHoursPerDay != null
? `${selectedAsset.maxHoursPerDay}h`
: "Using type default / not set"}
</span>
</div>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<Info className="h-4 w-4 text-slate-400" />
Expand Down
5 changes: 5 additions & 0 deletions src/app/(dashboard)/capacity-planning/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { CapacityDashboard } from "@/components/lookahead/CapacityDashboard";

export default function CapacityPlanningPage() {
return <CapacityDashboard />;
}
22 changes: 19 additions & 3 deletions src/app/(dashboard)/subcontractors/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,16 @@ type SortField =
| "isActive";
type SortDirection = "asc" | "desc";

const formatResolutionStatus = (status?: string | null): string => {
if (!status) {
return "Unspecified";
}

return status
.replace(/[_-]+/g, " ")
.replace(/\b\w/g, (char) => char.toUpperCase());
};

const transformBackendSubcontractor = (
backendSub: NormalizedSubcontractor,
): Contractor => {
Expand Down Expand Up @@ -406,7 +416,7 @@ export default function SubcontractorsPage() {
: "bg-amber-50 text-amber-700"
}`}
>
{sub.planningReady ? "Planning ready" : "Needs trade review"}
{sub.planningReady ? "Planning ready" : "Needs review"}
</span>
)}
</div>
Expand Down Expand Up @@ -601,7 +611,9 @@ export default function SubcontractorsPage() {
</span>
</div>
<span className="text-sm font-semibold text-slate-900">
{selectedContractor.tradeResolutionStatus || "Unspecified"}
{formatResolutionStatus(
selectedContractor.tradeResolutionStatus,
)}
</span>
</div>
<div className="flex items-center justify-between">
Expand Down Expand Up @@ -633,7 +645,11 @@ export default function SubcontractorsPage() {
</span>
</div>
<span className="text-sm font-semibold text-slate-900">
{selectedContractor.tradeInferenceSource || "Manual / unknown"}
{selectedContractor.tradeInferenceSource
? formatResolutionStatus(
selectedContractor.tradeInferenceSource,
)
: "Manual / unknown"}
</span>
</div>
</div>
Expand Down
7 changes: 7 additions & 0 deletions src/components/SideNav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
Menu,
X,
BarChart3,
Layers3,
} from "lucide-react";
import { useState, useEffect } from "react";
import { useAuth } from "@/app/context/AuthContext";
Expand Down Expand Up @@ -67,6 +68,12 @@ const SideNav = () => {
href: "/lookahead",
visible: ["admin", "manager"],
},
{
icon: Layers3,
label: "Capacity Planning",
href: "/capacity-planning",
visible: ["admin", "manager"],
},
{ icon: CalendarRange, label: "Bookings", href: "/bookings", visible: [] },
// { icon: Megaphone, label: "Announcements", href: "/announcements", visible: [] },
];
Expand Down
37 changes: 35 additions & 2 deletions src/components/forms/CreateAssetForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,12 @@ import { useAuth } from "@/app/context/AuthContext";
import { getApiErrorMessage } from "@/types";
import { reportError } from "@/lib/monitoring";
import { useProjectSelectionStore } from "@/stores/projectSelectionStore";
import { ASSET_TYPE_OPTIONS } from "@/lib/formOptions";
import {
ASSET_TYPE_OPTIONS,
MAX_ASSET_MAX_HOURS_PER_DAY,
MIN_ASSET_MAX_HOURS_PER_DAY,
clampAssetMaxHoursPerDay,
} from "@/lib/formOptions";

// ===== TYPE DEFINITIONS =====
interface AssetModalProps {
Expand All @@ -37,6 +42,7 @@ interface Asset {
assetTitle: string;
assetCode: string;
assetType: string;
maxHoursPerDay: string;
assetLocation: string;
maintenanceStartdt: string;
maintenanceEnddt: string;
Expand All @@ -58,6 +64,7 @@ interface AssetCreateRequest {
usage_instructions?: string;
maintenance_start_date?: string;
maintenance_end_date?: string;
max_hours_per_day?: number;
}

// ===== HELPER FUNCTIONS =====
Expand Down Expand Up @@ -99,6 +106,7 @@ const AssetModal: React.FC<AssetModalProps> = ({ isOpen, onClose, onSave }) => {
assetTitle: "",
assetCode: "",
assetType: "",
maxHoursPerDay: "",
assetLocation: "",
maintenanceStartdt: "",
maintenanceEnddt: "",
Expand Down Expand Up @@ -237,9 +245,15 @@ const AssetModal: React.FC<AssetModalProps> = ({ isOpen, onClose, onSave }) => {
if (asset.maintenanceEnddt) {
createRequest.maintenance_end_date = asset.maintenanceEnddt;
}
if (asset.maxHoursPerDay.trim()) {
const parsedMaxHours = Number(asset.maxHoursPerDay);
if (Number.isFinite(parsedMaxHours)) {
createRequest.max_hours_per_day = clampAssetMaxHoursPerDay(parsedMaxHours);
}
}

// Use new backend endpoint
const response = await api.post("/assets/", createRequest);
await api.post("/assets/", createRequest);

// Call onSave callback
onSave();
Expand All @@ -250,6 +264,7 @@ const AssetModal: React.FC<AssetModalProps> = ({ isOpen, onClose, onSave }) => {
assetTitle: "",
assetCode: "",
assetType: "",
maxHoursPerDay: "",
assetLocation: "",
maintenanceStartdt: "",
maintenanceEnddt: "",
Expand Down Expand Up @@ -371,6 +386,24 @@ const AssetModal: React.FC<AssetModalProps> = ({ isOpen, onClose, onSave }) => {
/>
</div>

<div className="space-y-2">
<Label htmlFor="maxHoursPerDay">Max Hours Per Day</Label>
<Input
id="maxHoursPerDay"
name="maxHoursPerDay"
type="number"
min={MIN_ASSET_MAX_HOURS_PER_DAY}
max={MAX_ASSET_MAX_HOURS_PER_DAY}
step="0.5"
value={asset.maxHoursPerDay}
onChange={handleChange}
placeholder="Leave blank to use asset-type default"
/>
<span className="text-xs text-gray-500">
Per-asset capacity override for the capacity dashboard in 30-minute increments. Leave blank to fall back to the asset-type default.
</span>
</div>

{/* Maintenance Dates */}
<div className="space-y-2">
<Label>Maintenance Dates (Optional)</Label>
Expand Down
Loading