rename(phase 1): CapaKraken → Nexus across code, UI, docs, CI (#61)
CI / Architecture Guardrails (push) Successful in 2m38s
CI / Assistant Split Regression (push) Successful in 3m33s
CI / Typecheck (push) Successful in 3m51s
CI / Lint (push) Successful in 5m2s
CI / E2E Tests (push) Has been cancelled
CI / Fresh-Linux Docker Deploy (push) Has been cancelled
CI / Release Images (push) Has been cancelled
CI / Build (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Architecture Guardrails (push) Successful in 2m38s
CI / Assistant Split Regression (push) Successful in 3m33s
CI / Typecheck (push) Successful in 3m51s
CI / Lint (push) Successful in 5m2s
CI / E2E Tests (push) Has been cancelled
CI / Fresh-Linux Docker Deploy (push) Has been cancelled
CI / Release Images (push) Has been cancelled
CI / Build (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
rename(phase 1): CapaKraken → Nexus across code, UI, docs, CI (#61) Co-authored-by: Hartmut Nörenberg <hn@hartmut-noerenberg.com> Co-committed-by: Hartmut Nörenberg <hn@hartmut-noerenberg.com>
This commit was merged in pull request #61.
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useRef, useState, useMemo } from "react";
|
||||
import { AllocationStatus } from "@capakraken/shared";
|
||||
import { AllocationStatus } from "@nexus/shared";
|
||||
import { useFocusTrap } from "~/hooks/useFocusTrap.js";
|
||||
import { formatCents, formatDateMedium } from "~/lib/format.js";
|
||||
import { getPlanningEntryMutationId } from "~/lib/planningEntryIds.js";
|
||||
@@ -75,7 +75,11 @@ export function FillOpenDemandModal({ allocation, onClose, onSuccess }: FillOpen
|
||||
const { data: resources } = trpc.resource.listStaff.useQuery(
|
||||
{ isActive: true, search: debouncedSearch || undefined, limit: 50 },
|
||||
{ staleTime: 15_000 },
|
||||
) as { data: { resources: Array<{ id: string; displayName: string; eid: string; lcrCents: number }> } | undefined };
|
||||
) as {
|
||||
data:
|
||||
| { resources: Array<{ id: string; displayName: string; eid: string; lcrCents: number }> }
|
||||
| undefined;
|
||||
};
|
||||
|
||||
const availabilityQuery = trpc.allocation.checkResourceAvailability.useQuery(
|
||||
{
|
||||
@@ -118,17 +122,20 @@ export function FillOpenDemandModal({ allocation, onClose, onSuccess }: FillOpen
|
||||
const lcrCents = selectedResource.lcrCents ?? 0;
|
||||
const estimatedCostCents = Math.round(lcrCents * avail.totalAvailableHours);
|
||||
|
||||
setPlanned((prev) => [...prev, {
|
||||
resourceId: selectedResource.id,
|
||||
resourceName: selectedResource.displayName,
|
||||
eid: selectedResource.eid,
|
||||
hoursPerDay,
|
||||
availableHours: avail.totalAvailableHours,
|
||||
availableDays: avail.availableDays,
|
||||
conflictDays: avail.conflictDays,
|
||||
coveragePercent: avail.coveragePercent,
|
||||
estimatedCostCents,
|
||||
}]);
|
||||
setPlanned((prev) => [
|
||||
...prev,
|
||||
{
|
||||
resourceId: selectedResource.id,
|
||||
resourceName: selectedResource.displayName,
|
||||
eid: selectedResource.eid,
|
||||
hoursPerDay,
|
||||
availableHours: avail.totalAvailableHours,
|
||||
availableDays: avail.availableDays,
|
||||
conflictDays: avail.conflictDays,
|
||||
coveragePercent: avail.coveragePercent,
|
||||
estimatedCostCents,
|
||||
},
|
||||
]);
|
||||
|
||||
// Reset for next resource
|
||||
setResourceId("");
|
||||
@@ -160,7 +167,9 @@ export function FillOpenDemandModal({ allocation, onClose, onSuccess }: FillOpen
|
||||
status: AllocationStatus.PROPOSED,
|
||||
});
|
||||
} catch (err) {
|
||||
setServerError(`Failed to assign ${p.resourceName}: ${err instanceof Error ? err.message : String(err)}`);
|
||||
setServerError(
|
||||
`Failed to assign ${p.resourceName}: ${err instanceof Error ? err.message : String(err)}`,
|
||||
);
|
||||
setSubmitting(false);
|
||||
return;
|
||||
}
|
||||
@@ -177,12 +186,16 @@ export function FillOpenDemandModal({ allocation, onClose, onSuccess }: FillOpen
|
||||
return (
|
||||
<div
|
||||
className="fixed inset-0 bg-black/50 z-50 flex items-start justify-center overflow-y-auto py-8"
|
||||
onClick={(e) => { if (e.target === e.currentTarget && !submitting) onClose(); }}
|
||||
onClick={(e) => {
|
||||
if (e.target === e.currentTarget && !submitting) onClose();
|
||||
}}
|
||||
>
|
||||
<div
|
||||
ref={panelRef}
|
||||
className="bg-white dark:bg-gray-800 rounded-xl shadow-2xl w-full max-w-lg mx-4"
|
||||
onKeyDown={(e) => { if (e.key === "Escape" && !submitting) onClose(); }}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Escape" && !submitting) onClose();
|
||||
}}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between px-6 py-4 border-b border-gray-200 dark:border-gray-700">
|
||||
@@ -190,21 +203,34 @@ export function FillOpenDemandModal({ allocation, onClose, onSuccess }: FillOpen
|
||||
{phase === "plan" ? "Plan Demand Assignment" : "Confirm Assignments"}
|
||||
<InfoTooltip content="Fill an open demand by assigning one or more real resources to a placeholder staffing requirement. Each assignment creates a new allocation." />
|
||||
</h2>
|
||||
<button type="button" onClick={onClose} disabled={submitting} className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 text-xl leading-none disabled:opacity-30">×</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
disabled={submitting}
|
||||
className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 text-xl leading-none disabled:opacity-30"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="px-6 pt-4 pb-2 space-y-3">
|
||||
{/* Demand summary */}
|
||||
<div className="bg-gray-50 dark:bg-gray-900 rounded-lg p-3 flex items-start gap-3">
|
||||
<div className="w-3 h-3 rounded-full mt-1 flex-shrink-0" style={{ backgroundColor: roleColor }} />
|
||||
<div
|
||||
className="w-3 h-3 rounded-full mt-1 flex-shrink-0"
|
||||
style={{ backgroundColor: roleColor }}
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<div className="font-medium text-gray-900 dark:text-gray-100">{roleName}</div>
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
|
||||
{allocation.project?.name} · {formatDateMedium(allocation.startDate)} – {formatDateMedium(allocation.endDate)}
|
||||
{allocation.project?.name} · {formatDateMedium(allocation.startDate)} –{" "}
|
||||
{formatDateMedium(allocation.endDate)}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400">
|
||||
{allocation.hoursPerDay}h/day · {totalDemandHours.toLocaleString()}h total
|
||||
{allocation.budgetCents && allocation.budgetCents > 0 ? ` · Budget: ${formatCents(allocation.budgetCents)} EUR` : ""}
|
||||
{allocation.budgetCents && allocation.budgetCents > 0
|
||||
? ` · Budget: ${formatCents(allocation.budgetCents)} EUR`
|
||||
: ""}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -213,7 +239,10 @@ export function FillOpenDemandModal({ allocation, onClose, onSuccess }: FillOpen
|
||||
<div className="bg-gray-50 dark:bg-gray-900 rounded-lg p-3">
|
||||
<div className="flex items-center justify-between text-xs text-gray-500 dark:text-gray-400 mb-1.5">
|
||||
<span>Demand coverage</span>
|
||||
<span>{Math.round(consumedHours)}h / {totalDemandHours}h ({totalDemandHours > 0 ? Math.round((consumedHours / totalDemandHours) * 100) : 0}%)</span>
|
||||
<span>
|
||||
{Math.round(consumedHours)}h / {totalDemandHours}h (
|
||||
{totalDemandHours > 0 ? Math.round((consumedHours / totalDemandHours) * 100) : 0}%)
|
||||
</span>
|
||||
</div>
|
||||
<div className="w-full h-2.5 bg-gray-200 dark:bg-gray-700 rounded-full overflow-hidden flex">
|
||||
{planned.map((r, i) => (
|
||||
@@ -234,11 +263,18 @@ export function FillOpenDemandModal({ allocation, onClose, onSuccess }: FillOpen
|
||||
<div className="mt-2 space-y-1">
|
||||
{planned.map((r, i) => (
|
||||
<div key={r.resourceId} className="flex items-center gap-2 text-xs group">
|
||||
<div className="w-2 h-2 rounded-full flex-shrink-0" style={{ backgroundColor: `hsl(${(i * 60 + 200) % 360}, 60%, 55%)` }} />
|
||||
<span className="text-gray-700 dark:text-gray-300 font-medium">{r.resourceName}</span>
|
||||
<div
|
||||
className="w-2 h-2 rounded-full flex-shrink-0"
|
||||
style={{ backgroundColor: `hsl(${(i * 60 + 200) % 360}, 60%, 55%)` }}
|
||||
/>
|
||||
<span className="text-gray-700 dark:text-gray-300 font-medium">
|
||||
{r.resourceName}
|
||||
</span>
|
||||
<span className="text-gray-400">({r.eid})</span>
|
||||
<span className="text-gray-500">{r.hoursPerDay}h/day</span>
|
||||
<span className="ml-auto text-gray-500">{Math.round(r.availableHours)}h · {r.coveragePercent}%</span>
|
||||
<span className="ml-auto text-gray-500">
|
||||
{Math.round(r.availableHours)}h · {r.coveragePercent}%
|
||||
</span>
|
||||
{phase === "plan" && (
|
||||
<button
|
||||
type="button"
|
||||
@@ -254,7 +290,9 @@ export function FillOpenDemandModal({ allocation, onClose, onSuccess }: FillOpen
|
||||
{remainingHours > 0 && (
|
||||
<div className="flex items-center gap-2 text-xs">
|
||||
<div className="w-2 h-2 rounded-full bg-gray-300 dark:bg-gray-600 flex-shrink-0" />
|
||||
<span className="text-amber-600 dark:text-amber-400 font-medium">Remaining: {Math.round(remainingHours)}h</span>
|
||||
<span className="text-amber-600 dark:text-amber-400 font-medium">
|
||||
Remaining: {Math.round(remainingHours)}h
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -266,7 +304,9 @@ export function FillOpenDemandModal({ allocation, onClose, onSuccess }: FillOpen
|
||||
{phase === "plan" && (
|
||||
<div className="px-6 pb-5 space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Search Resource</label>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
Search Resource
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search by name or EID..."
|
||||
@@ -277,7 +317,9 @@ export function FillOpenDemandModal({ allocation, onClose, onSuccess }: FillOpen
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Select Resource</label>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
Select Resource
|
||||
</label>
|
||||
<select
|
||||
value={resourceId}
|
||||
onChange={(e) => setResourceId(e.target.value)}
|
||||
@@ -297,7 +339,9 @@ export function FillOpenDemandModal({ allocation, onClose, onSuccess }: FillOpen
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Hours / Day</label>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
Hours / Day
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={hoursPerDay}
|
||||
@@ -311,41 +355,53 @@ export function FillOpenDemandModal({ allocation, onClose, onSuccess }: FillOpen
|
||||
|
||||
{/* Availability preview */}
|
||||
{resourceId && avail && (
|
||||
<div className={`rounded-lg p-3 border text-sm ${
|
||||
avail.coveragePercent >= 100
|
||||
? "bg-green-50 border-green-200 dark:bg-green-900/20 dark:border-green-800"
|
||||
: avail.coveragePercent >= 50
|
||||
? "bg-amber-50 border-amber-200 dark:bg-amber-900/20 dark:border-amber-800"
|
||||
: "bg-red-50 border-red-200 dark:bg-red-900/20 dark:border-red-800"
|
||||
}`}>
|
||||
<div
|
||||
className={`rounded-lg p-3 border text-sm ${
|
||||
avail.coveragePercent >= 100
|
||||
? "bg-green-50 border-green-200 dark:bg-green-900/20 dark:border-green-800"
|
||||
: avail.coveragePercent >= 50
|
||||
? "bg-amber-50 border-amber-200 dark:bg-amber-900/20 dark:border-amber-800"
|
||||
: "bg-red-50 border-red-200 dark:bg-red-900/20 dark:border-red-800"
|
||||
}`}
|
||||
>
|
||||
<div className="font-medium text-gray-900 dark:text-gray-100 mb-1.5">
|
||||
Availability: {avail.resource.name}
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-2 text-xs">
|
||||
<div>
|
||||
<span className="text-gray-500 dark:text-gray-400">Available</span>
|
||||
<div className="font-semibold text-green-700 dark:text-green-400">{avail.availableDays} days</div>
|
||||
<div className="font-semibold text-green-700 dark:text-green-400">
|
||||
{avail.availableDays} days
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-gray-500 dark:text-gray-400">Conflicts</span>
|
||||
<div className="font-semibold text-red-700 dark:text-red-400">{avail.conflictDays} days</div>
|
||||
<div className="font-semibold text-red-700 dark:text-red-400">
|
||||
{avail.conflictDays} days
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-gray-500 dark:text-gray-400">Hours</span>
|
||||
<div className="font-semibold text-gray-900 dark:text-gray-100">{avail.totalAvailableHours}h / {avail.totalRequestedHours}h</div>
|
||||
<div className="font-semibold text-gray-900 dark:text-gray-100">
|
||||
{avail.totalAvailableHours}h / {avail.totalRequestedHours}h
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{avail.existingAssignments.length > 0 && (
|
||||
<div className="mt-2 pt-2 border-t border-gray-200 dark:border-gray-700">
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400 mb-1">Existing bookings:</div>
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400 mb-1">
|
||||
Existing bookings:
|
||||
</div>
|
||||
{avail.existingAssignments.slice(0, 4).map((a, i) => (
|
||||
<div key={i} className="text-xs text-gray-600 dark:text-gray-300">
|
||||
{a.code} · {a.hoursPerDay}h/day · {a.start} – {a.end}
|
||||
</div>
|
||||
))}
|
||||
{avail.existingAssignments.length > 4 && (
|
||||
<div className="text-xs text-gray-400">+{avail.existingAssignments.length - 4} more</div>
|
||||
<div className="text-xs text-gray-400">
|
||||
+{avail.existingAssignments.length - 4} more
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
@@ -353,12 +409,18 @@ export function FillOpenDemandModal({ allocation, onClose, onSuccess }: FillOpen
|
||||
)}
|
||||
|
||||
{resourceId && availabilityQuery.isLoading && (
|
||||
<div className="text-xs text-gray-400 dark:text-gray-500 animate-pulse">Checking availability...</div>
|
||||
<div className="text-xs text-gray-400 dark:text-gray-500 animate-pulse">
|
||||
Checking availability...
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Action buttons */}
|
||||
<div className="flex items-center justify-between gap-3 pt-2">
|
||||
<button type="button" onClick={onClose} className="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -391,11 +453,27 @@ export function FillOpenDemandModal({ allocation, onClose, onSuccess }: FillOpen
|
||||
<table className="w-full text-sm">
|
||||
<thead className="bg-gray-50 dark:bg-gray-900">
|
||||
<tr>
|
||||
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 dark:text-gray-400">Resource</th>
|
||||
<th className="px-3 py-2 text-right text-xs font-medium text-gray-500 dark:text-gray-400">h/day</th>
|
||||
<th className="px-3 py-2 text-right text-xs font-medium text-gray-500 dark:text-gray-400">Hours</th>
|
||||
<th className="px-3 py-2 text-right text-xs font-medium text-gray-500 dark:text-gray-400"><span className="inline-flex items-center justify-end gap-0.5">Est. Cost<InfoTooltip content="Estimated cost = resource LCR x available hours in the demand period." /></span></th>
|
||||
<th className="px-3 py-2 text-right text-xs font-medium text-gray-500 dark:text-gray-400"><span className="inline-flex items-center justify-end gap-0.5">Coverage<InfoTooltip content="Percentage of the demand period this resource can cover, accounting for existing bookings." /></span></th>
|
||||
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 dark:text-gray-400">
|
||||
Resource
|
||||
</th>
|
||||
<th className="px-3 py-2 text-right text-xs font-medium text-gray-500 dark:text-gray-400">
|
||||
h/day
|
||||
</th>
|
||||
<th className="px-3 py-2 text-right text-xs font-medium text-gray-500 dark:text-gray-400">
|
||||
Hours
|
||||
</th>
|
||||
<th className="px-3 py-2 text-right text-xs font-medium text-gray-500 dark:text-gray-400">
|
||||
<span className="inline-flex items-center justify-end gap-0.5">
|
||||
Est. Cost
|
||||
<InfoTooltip content="Estimated cost = resource LCR x available hours in the demand period." />
|
||||
</span>
|
||||
</th>
|
||||
<th className="px-3 py-2 text-right text-xs font-medium text-gray-500 dark:text-gray-400">
|
||||
<span className="inline-flex items-center justify-end gap-0.5">
|
||||
Coverage
|
||||
<InfoTooltip content="Percentage of the demand period this resource can cover, accounting for existing bookings." />
|
||||
</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-100 dark:divide-gray-800">
|
||||
@@ -405,11 +483,19 @@ export function FillOpenDemandModal({ allocation, onClose, onSuccess }: FillOpen
|
||||
{r.resourceName}
|
||||
<span className="ml-1 text-xs text-gray-400 font-mono">{r.eid}</span>
|
||||
</td>
|
||||
<td className="px-3 py-2 text-right text-gray-600 dark:text-gray-300">{r.hoursPerDay}h</td>
|
||||
<td className="px-3 py-2 text-right text-gray-600 dark:text-gray-300">{Math.round(r.availableHours)}h</td>
|
||||
<td className="px-3 py-2 text-right text-gray-600 dark:text-gray-300">{formatCents(r.estimatedCostCents)} EUR</td>
|
||||
<td className="px-3 py-2 text-right text-gray-600 dark:text-gray-300">
|
||||
{r.hoursPerDay}h
|
||||
</td>
|
||||
<td className="px-3 py-2 text-right text-gray-600 dark:text-gray-300">
|
||||
{Math.round(r.availableHours)}h
|
||||
</td>
|
||||
<td className="px-3 py-2 text-right text-gray-600 dark:text-gray-300">
|
||||
{formatCents(r.estimatedCostCents)} EUR
|
||||
</td>
|
||||
<td className="px-3 py-2 text-right">
|
||||
<span className={`font-medium ${r.coveragePercent >= 100 ? "text-green-600" : r.coveragePercent >= 50 ? "text-amber-600" : "text-red-600"}`}>
|
||||
<span
|
||||
className={`font-medium ${r.coveragePercent >= 100 ? "text-green-600" : r.coveragePercent >= 50 ? "text-amber-600" : "text-red-600"}`}
|
||||
>
|
||||
{r.coveragePercent}%
|
||||
</span>
|
||||
</td>
|
||||
@@ -418,7 +504,9 @@ export function FillOpenDemandModal({ allocation, onClose, onSuccess }: FillOpen
|
||||
</tbody>
|
||||
<tfoot className="bg-gray-50 dark:bg-gray-900">
|
||||
<tr>
|
||||
<td className="px-3 py-2 text-xs font-semibold text-gray-700 dark:text-gray-300">Total</td>
|
||||
<td className="px-3 py-2 text-xs font-semibold text-gray-700 dark:text-gray-300">
|
||||
Total
|
||||
</td>
|
||||
<td className="px-3 py-2" />
|
||||
<td className="px-3 py-2 text-right text-xs font-semibold text-gray-700 dark:text-gray-300">
|
||||
{Math.round(consumedHours)}h / {totalDemandHours}h
|
||||
@@ -427,12 +515,20 @@ export function FillOpenDemandModal({ allocation, onClose, onSuccess }: FillOpen
|
||||
{formatCents(planned.reduce((s, r) => s + r.estimatedCostCents, 0))} EUR
|
||||
</td>
|
||||
<td className="px-3 py-2 text-right text-xs font-semibold text-gray-700 dark:text-gray-300">
|
||||
{totalDemandHours > 0 ? Math.round((consumedHours / totalDemandHours) * 100) : 0}%
|
||||
{totalDemandHours > 0
|
||||
? Math.round((consumedHours / totalDemandHours) * 100)
|
||||
: 0}
|
||||
%
|
||||
</td>
|
||||
</tr>
|
||||
{allocation.budgetCents && allocation.budgetCents > 0 && (
|
||||
<tr>
|
||||
<td colSpan={3} className="px-3 py-1.5 text-right text-xs text-gray-500 dark:text-gray-400">Role Budget:</td>
|
||||
<td
|
||||
colSpan={3}
|
||||
className="px-3 py-1.5 text-right text-xs text-gray-500 dark:text-gray-400"
|
||||
>
|
||||
Role Budget:
|
||||
</td>
|
||||
<td className="px-3 py-1.5 text-right text-xs font-semibold text-gray-700 dark:text-gray-300">
|
||||
{formatCents(allocation.budgetCents)} EUR
|
||||
</td>
|
||||
@@ -441,8 +537,12 @@ export function FillOpenDemandModal({ allocation, onClose, onSuccess }: FillOpen
|
||||
const totalCost = planned.reduce((s, r) => s + r.estimatedCostCents, 0);
|
||||
const remain = allocation.budgetCents! - totalCost;
|
||||
return (
|
||||
<span className={remain < 0 ? "text-red-600 font-medium" : "text-green-600"}>
|
||||
{remain < 0 ? `${formatCents(Math.abs(remain))} over` : `${formatCents(remain)} left`}
|
||||
<span
|
||||
className={remain < 0 ? "text-red-600 font-medium" : "text-green-600"}
|
||||
>
|
||||
{remain < 0
|
||||
? `${formatCents(Math.abs(remain))} over`
|
||||
: `${formatCents(remain)} left`}
|
||||
</span>
|
||||
);
|
||||
})()}
|
||||
@@ -455,7 +555,8 @@ export function FillOpenDemandModal({ allocation, onClose, onSuccess }: FillOpen
|
||||
|
||||
{remainingHours > 0 && (
|
||||
<div className="text-xs text-amber-600 dark:text-amber-400 bg-amber-50 dark:bg-amber-900/20 rounded-lg px-3 py-2 border border-amber-200 dark:border-amber-800">
|
||||
{Math.round(remainingHours)}h remain uncovered. You can add more resources or assign partially.
|
||||
{Math.round(remainingHours)}h remain uncovered. You can add more resources or assign
|
||||
partially.
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -486,7 +587,9 @@ export function FillOpenDemandModal({ allocation, onClose, onSuccess }: FillOpen
|
||||
disabled={submitting || planned.length === 0}
|
||||
className="px-5 py-2 bg-brand-600 text-white rounded-lg hover:bg-brand-700 text-sm font-semibold disabled:opacity-50"
|
||||
>
|
||||
{submitting ? `Assigning ${submitProgress}/${planned.length}...` : `Confirm & Assign ${planned.length} Resource${planned.length !== 1 ? "s" : ""}`}
|
||||
{submitting
|
||||
? `Assigning ${submitProgress}/${planned.length}...`
|
||||
: `Confirm & Assign ${planned.length} Resource${planned.length !== 1 ? "s" : ""}`}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user