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:
@@ -20,7 +20,11 @@ export function StaffingPanel() {
|
||||
const [hoursPerDay, setHoursPerDay] = useState(8);
|
||||
const [submitted, setSubmitted] = useState(false);
|
||||
const [assignedIds, setAssignedIds] = useState<Set<string>>(new Set());
|
||||
const [toast, setToast] = useState<{ show: boolean; message: string; variant: "success" | "warning" }>({
|
||||
const [toast, setToast] = useState<{
|
||||
show: boolean;
|
||||
message: string;
|
||||
variant: "success" | "warning";
|
||||
}>({
|
||||
show: false,
|
||||
message: "",
|
||||
variant: "success",
|
||||
@@ -47,7 +51,12 @@ export function StaffingPanel() {
|
||||
|
||||
return (
|
||||
<div className="app-page space-y-6">
|
||||
<SuccessToast show={toast.show} message={toast.message} variant={toast.variant} onDone={clearToast} />
|
||||
<SuccessToast
|
||||
show={toast.show}
|
||||
message={toast.message}
|
||||
variant={toast.variant}
|
||||
onDone={clearToast}
|
||||
/>
|
||||
|
||||
<div className="app-page-header gap-4">
|
||||
<div className="space-y-3">
|
||||
@@ -57,14 +66,16 @@ export function StaffingPanel() {
|
||||
<div>
|
||||
<h1 className="app-page-title">Staffing Suggestions</h1>
|
||||
<p className="app-page-subtitle max-w-2xl">
|
||||
Match open work with the strongest available people based on skills, availability, utilization, and cost.
|
||||
Match open work with the strongest available people based on skills, availability,
|
||||
utilization, and cost.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="app-surface max-w-xl p-4">
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-gray-100">How scoring works</p>
|
||||
<p className="mt-1 text-sm text-gray-500">
|
||||
CapaKraken blends skill fit, free capacity, cost, and current utilization. Add the must-have skills first, then narrow the date window to get cleaner results.
|
||||
Nexus blends skill fit, free capacity, cost, and current utilization. Add the must-have
|
||||
skills first, then narrow the date window to get cleaner results.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { useState } from "react";
|
||||
import { trpc } from "~/lib/trpc/client.js";
|
||||
import { useInvalidatePlanningViews } from "~/hooks/useInvalidatePlanningViews.js";
|
||||
import { AllocationStatus } from "@capakraken/shared";
|
||||
import { AllocationStatus } from "@nexus/shared";
|
||||
import { DateInput } from "~/components/ui/DateInput.js";
|
||||
import { InfoTooltip } from "~/components/ui/InfoTooltip.js";
|
||||
import { Button } from "~/components/ui/Button.js";
|
||||
@@ -92,12 +92,22 @@ interface StaffingResultCardProps {
|
||||
onError: (message: string) => void;
|
||||
}
|
||||
|
||||
export function StaffingResultCard({ suggestion, rank, searchCriteria, onAssigned, onError }: StaffingResultCardProps) {
|
||||
export function StaffingResultCard({
|
||||
suggestion,
|
||||
rank,
|
||||
searchCriteria,
|
||||
onAssigned,
|
||||
onError,
|
||||
}: StaffingResultCardProps) {
|
||||
const [showDetails, setShowDetails] = useState(false);
|
||||
const [showAssignForm, setShowAssignForm] = useState(false);
|
||||
const locationLabel =
|
||||
suggestion.location?.label ||
|
||||
[suggestion.location?.countryCode, suggestion.location?.federalState, suggestion.location?.metroCityName]
|
||||
[
|
||||
suggestion.location?.countryCode,
|
||||
suggestion.location?.federalState,
|
||||
suggestion.location?.metroCityName,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" / ") ||
|
||||
"No location";
|
||||
@@ -105,10 +115,13 @@ export function StaffingResultCard({ suggestion, rank, searchCriteria, onAssigne
|
||||
const conflicts = suggestion.conflicts;
|
||||
const conflictCount = conflicts?.count ?? suggestion.availabilityConflicts.length;
|
||||
const remainingHours = capacity?.remainingHours ?? suggestion.remainingHours ?? 0;
|
||||
const remainingHoursPerDay = capacity?.remainingHoursPerDay ?? suggestion.remainingHoursPerDay ?? 0;
|
||||
const remainingHoursPerDay =
|
||||
capacity?.remainingHoursPerDay ?? suggestion.remainingHoursPerDay ?? 0;
|
||||
const baseAvailableHours = capacity?.baseAvailableHours ?? suggestion.baseAvailableHours ?? 0;
|
||||
const effectiveAvailableHours = capacity?.effectiveAvailableHours ?? suggestion.effectiveAvailableHours ?? 0;
|
||||
const holidayHoursDeduction = capacity?.holidayHoursDeduction ?? suggestion.holidayHoursDeduction ?? 0;
|
||||
const effectiveAvailableHours =
|
||||
capacity?.effectiveAvailableHours ?? suggestion.effectiveAvailableHours ?? 0;
|
||||
const holidayHoursDeduction =
|
||||
capacity?.holidayHoursDeduction ?? suggestion.holidayHoursDeduction ?? 0;
|
||||
|
||||
return (
|
||||
<div data-suggestion className="app-surface p-5">
|
||||
@@ -118,7 +131,9 @@ export function StaffingResultCard({ suggestion, rank, searchCriteria, onAssigne
|
||||
{rank}
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-base font-semibold text-gray-900 dark:text-gray-100">{suggestion.resourceName}</div>
|
||||
<div className="text-base font-semibold text-gray-900 dark:text-gray-100">
|
||||
{suggestion.resourceName}
|
||||
</div>
|
||||
<div className="text-sm text-gray-500">{suggestion.eid}</div>
|
||||
<div className="mt-1 text-xs text-gray-500">{locationLabel}</div>
|
||||
</div>
|
||||
@@ -135,19 +150,27 @@ export function StaffingResultCard({ suggestion, rank, searchCriteria, onAssigne
|
||||
Match Score
|
||||
<InfoTooltip content="Composite score (0-100) blending skill fit, free capacity, cost efficiency, and current utilization." />
|
||||
</div>
|
||||
<div className="mt-1 text-3xl font-semibold text-brand-700 dark:text-brand-100">{suggestion.score}</div>
|
||||
<div className="mt-1 text-3xl font-semibold text-brand-700 dark:text-brand-100">
|
||||
{suggestion.score}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 flex flex-wrap gap-2">
|
||||
{suggestion.matchedSkills.map((skill) => (
|
||||
<span key={skill} className="rounded-full bg-green-50 px-2.5 py-1 text-xs font-medium text-green-700 dark:bg-green-950/30 dark:text-green-300">
|
||||
<span
|
||||
key={skill}
|
||||
className="rounded-full bg-green-50 px-2.5 py-1 text-xs font-medium text-green-700 dark:bg-green-950/30 dark:text-green-300"
|
||||
>
|
||||
{skill}
|
||||
</span>
|
||||
))}
|
||||
{suggestion.missingSkills.map((skill) => (
|
||||
<span key={skill} className="rounded-full bg-red-50 px-2.5 py-1 text-xs font-medium text-red-600 dark:bg-red-950/30 dark:text-red-300">
|
||||
<span
|
||||
key={skill}
|
||||
className="rounded-full bg-red-50 px-2.5 py-1 text-xs font-medium text-red-600 dark:bg-red-950/30 dark:text-red-300"
|
||||
>
|
||||
{skill} missing
|
||||
</span>
|
||||
))}
|
||||
@@ -169,13 +192,21 @@ export function StaffingResultCard({ suggestion, rank, searchCriteria, onAssigne
|
||||
label="Holiday Deduction"
|
||||
value={holidayHoursDeduction > 0 ? formatHours(holidayHoursDeduction) : "0h"}
|
||||
tone={holidayHoursDeduction > 0 ? "warn" : "neutral"}
|
||||
helper={capacity ? `${capacity.holidayWorkdayCount} affected workdays` : "No local holiday impact"}
|
||||
helper={
|
||||
capacity
|
||||
? `${capacity.holidayWorkdayCount} affected workdays`
|
||||
: "No local holiday impact"
|
||||
}
|
||||
/>
|
||||
<StatCard
|
||||
label="Conflicts"
|
||||
value={String(conflictCount)}
|
||||
tone={conflictCount > 0 ? "warn" : "good"}
|
||||
helper={conflictCount > 0 ? `${conflictCount} overloaded day${conflictCount === 1 ? "" : "s"}` : "No day-level overloads"}
|
||||
helper={
|
||||
conflictCount > 0
|
||||
? `${conflictCount} overloaded day${conflictCount === 1 ? "" : "s"}`
|
||||
: "No day-level overloads"
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -193,38 +224,81 @@ export function StaffingResultCard({ suggestion, rank, searchCriteria, onAssigne
|
||||
{showDetails && (
|
||||
<div className="mt-5 space-y-4 rounded-2xl border border-gray-200 bg-gray-50/80 p-4 dark:border-gray-800 dark:bg-gray-950/40">
|
||||
<div className="grid gap-3 sm:grid-cols-2 xl:grid-cols-4">
|
||||
<ScoreBar label="Skills" value={suggestion.scoreBreakdown.skillScore} tooltip="Quality of skill overlap with the requested stack, weighted by proficiency level." />
|
||||
<ScoreBar label="Availability" value={suggestion.scoreBreakdown.availabilityScore} tooltip="Free capacity during the selected period, accounting for existing bookings and vacations." />
|
||||
<ScoreBar label="Cost" value={suggestion.scoreBreakdown.costScore} tooltip="Cost efficiency based on the resource's LCR relative to the team average." />
|
||||
<ScoreBar label="Utilization" value={suggestion.scoreBreakdown.utilizationScore} tooltip="Current workload. Higher score means more capacity available (lower utilization)." />
|
||||
<ScoreBar
|
||||
label="Skills"
|
||||
value={suggestion.scoreBreakdown.skillScore}
|
||||
tooltip="Quality of skill overlap with the requested stack, weighted by proficiency level."
|
||||
/>
|
||||
<ScoreBar
|
||||
label="Availability"
|
||||
value={suggestion.scoreBreakdown.availabilityScore}
|
||||
tooltip="Free capacity during the selected period, accounting for existing bookings and vacations."
|
||||
/>
|
||||
<ScoreBar
|
||||
label="Cost"
|
||||
value={suggestion.scoreBreakdown.costScore}
|
||||
tooltip="Cost efficiency based on the resource's LCR relative to the team average."
|
||||
/>
|
||||
<ScoreBar
|
||||
label="Utilization"
|
||||
value={suggestion.scoreBreakdown.utilizationScore}
|
||||
tooltip="Current workload. Higher score means more capacity available (lower utilization)."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 xl:grid-cols-[1.15fr_1fr]">
|
||||
<div className="rounded-2xl border border-gray-200 bg-white p-4 dark:border-gray-800 dark:bg-gray-950/60">
|
||||
<div className="text-[11px] font-semibold uppercase tracking-[0.18em] text-gray-500">Capacity Basis</div>
|
||||
<div className="text-[11px] font-semibold uppercase tracking-[0.18em] text-gray-500">
|
||||
Capacity Basis
|
||||
</div>
|
||||
<div className="mt-3 grid gap-3 sm:grid-cols-2">
|
||||
<MetricLine label="Requested load" value={`${formatHours(capacity?.requestedHoursPerDay ?? searchCriteria.hoursPerDay)} / day`} />
|
||||
<MetricLine label="Requested total" value={formatHours(capacity?.requestedHoursTotal ?? 0)} />
|
||||
<MetricLine label="Base working days" value={String(capacity?.baseWorkingDays ?? 0)} />
|
||||
<MetricLine label="Effective working days" value={String(capacity?.effectiveWorkingDays ?? 0)} />
|
||||
<MetricLine
|
||||
label="Requested load"
|
||||
value={`${formatHours(capacity?.requestedHoursPerDay ?? searchCriteria.hoursPerDay)} / day`}
|
||||
/>
|
||||
<MetricLine
|
||||
label="Requested total"
|
||||
value={formatHours(capacity?.requestedHoursTotal ?? 0)}
|
||||
/>
|
||||
<MetricLine
|
||||
label="Base working days"
|
||||
value={String(capacity?.baseWorkingDays ?? 0)}
|
||||
/>
|
||||
<MetricLine
|
||||
label="Effective working days"
|
||||
value={String(capacity?.effectiveWorkingDays ?? 0)}
|
||||
/>
|
||||
<MetricLine label="Base available hours" value={formatHours(baseAvailableHours)} />
|
||||
<MetricLine label="Effective available hours" value={formatHours(effectiveAvailableHours)} />
|
||||
<MetricLine
|
||||
label="Effective available hours"
|
||||
value={formatHours(effectiveAvailableHours)}
|
||||
/>
|
||||
<MetricLine label="Booked hours" value={formatHours(capacity?.bookedHours ?? 0)} />
|
||||
<MetricLine label="Remaining hours" value={formatHours(remainingHours)} />
|
||||
<MetricLine label="Holiday deduction" value={formatHours(holidayHoursDeduction)} />
|
||||
<MetricLine label="Absence deduction" value={formatHours(capacity?.absenceHoursDeduction ?? 0)} />
|
||||
<MetricLine
|
||||
label="Absence deduction"
|
||||
value={formatHours(capacity?.absenceHoursDeduction ?? 0)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="rounded-2xl border border-gray-200 bg-white p-4 dark:border-gray-800 dark:bg-gray-950/60">
|
||||
<div className="text-[11px] font-semibold uppercase tracking-[0.18em] text-gray-500">Ranking Basis</div>
|
||||
<div className="text-[11px] font-semibold uppercase tracking-[0.18em] text-gray-500">
|
||||
Ranking Basis
|
||||
</div>
|
||||
<p className="mt-2 text-sm text-gray-600 dark:text-gray-300">
|
||||
{suggestion.ranking?.model ?? "Composite ranking across skill fit, availability, cost, and utilization."}
|
||||
{suggestion.ranking?.model ??
|
||||
"Composite ranking across skill fit, availability, cost, and utilization."}
|
||||
</p>
|
||||
<div className="mt-3 space-y-2 text-sm text-gray-600 dark:text-gray-300">
|
||||
{(suggestion.ranking?.components ?? []).map((component) => (
|
||||
<MetricLine key={component.key} label={component.label} value={`${component.score}`} />
|
||||
<MetricLine
|
||||
key={component.key}
|
||||
label={component.label}
|
||||
value={`${component.score}`}
|
||||
/>
|
||||
))}
|
||||
{suggestion.ranking?.tieBreakerReason && (
|
||||
<div className="rounded-xl border border-amber-200 bg-amber-50 px-3 py-2 text-xs text-amber-800 dark:border-amber-900/50 dark:bg-amber-950/30 dark:text-amber-200">
|
||||
@@ -235,12 +309,20 @@ export function StaffingResultCard({ suggestion, rank, searchCriteria, onAssigne
|
||||
</div>
|
||||
|
||||
<div className="rounded-2xl border border-gray-200 bg-white p-4 dark:border-gray-800 dark:bg-gray-950/60">
|
||||
<div className="text-[11px] font-semibold uppercase tracking-[0.18em] text-gray-500">Location + Calendar</div>
|
||||
<div className="text-[11px] font-semibold uppercase tracking-[0.18em] text-gray-500">
|
||||
Location + Calendar
|
||||
</div>
|
||||
<div className="mt-3 space-y-2 text-sm text-gray-600 dark:text-gray-300">
|
||||
<MetricLine label="Location" value={locationLabel} />
|
||||
<MetricLine label="Holiday dates" value={String(capacity?.holidayCount ?? 0)} />
|
||||
<MetricLine label="Holiday workdays" value={String(capacity?.holidayWorkdayCount ?? 0)} />
|
||||
<MetricLine label="Absence days" value={String(capacity?.absenceDayEquivalent ?? 0)} />
|
||||
<MetricLine
|
||||
label="Holiday workdays"
|
||||
value={String(capacity?.holidayWorkdayCount ?? 0)}
|
||||
/>
|
||||
<MetricLine
|
||||
label="Absence days"
|
||||
value={String(capacity?.absenceDayEquivalent ?? 0)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -248,9 +330,12 @@ export function StaffingResultCard({ suggestion, rank, searchCriteria, onAssigne
|
||||
|
||||
<div className="rounded-2xl border border-gray-200 bg-white p-4 dark:border-gray-800 dark:bg-gray-950/60">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="text-[11px] font-semibold uppercase tracking-[0.18em] text-gray-500">Conflict Check</div>
|
||||
<div className="text-[11px] font-semibold uppercase tracking-[0.18em] text-gray-500">
|
||||
Conflict Check
|
||||
</div>
|
||||
<div className="text-xs text-gray-500">
|
||||
Requested {formatHours(searchCriteria.hoursPerDay)} / day between {searchCriteria.startDate} and {searchCriteria.endDate}
|
||||
Requested {formatHours(searchCriteria.hoursPerDay)} / day between{" "}
|
||||
{searchCriteria.startDate} and {searchCriteria.endDate}
|
||||
</div>
|
||||
</div>
|
||||
{conflictCount === 0 ? (
|
||||
@@ -260,13 +345,19 @@ export function StaffingResultCard({ suggestion, rank, searchCriteria, onAssigne
|
||||
) : (
|
||||
<div className="mt-3 space-y-2">
|
||||
{(conflicts?.details ?? []).slice(0, 6).map((item) => (
|
||||
<div key={item.date} className="rounded-xl border border-amber-200 bg-amber-50/80 px-3 py-2 text-sm text-amber-900 dark:border-amber-900/50 dark:bg-amber-950/30 dark:text-amber-100">
|
||||
<div
|
||||
key={item.date}
|
||||
className="rounded-xl border border-amber-200 bg-amber-50/80 px-3 py-2 text-sm text-amber-900 dark:border-amber-900/50 dark:bg-amber-950/30 dark:text-amber-100"
|
||||
>
|
||||
<div className="flex flex-wrap items-center justify-between gap-2">
|
||||
<span className="font-medium">{item.date}</span>
|
||||
<span>Short by {formatHours(item.shortageHours)}</span>
|
||||
</div>
|
||||
<div className="mt-1 text-xs">
|
||||
Base {formatHours(item.baseHours)} | Effective {formatHours(item.effectiveHours)} | Already booked {formatHours(item.allocatedHours)} | Remaining {formatHours(item.remainingHours)}
|
||||
Base {formatHours(item.baseHours)} | Effective{" "}
|
||||
{formatHours(item.effectiveHours)} | Already booked{" "}
|
||||
{formatHours(item.allocatedHours)} | Remaining{" "}
|
||||
{formatHours(item.remainingHours)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
@@ -308,7 +399,14 @@ interface AssignFormProps {
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
function AssignForm({ resourceId, resourceName, searchCriteria, onAssigned, onError, onCancel }: AssignFormProps) {
|
||||
function AssignForm({
|
||||
resourceId,
|
||||
resourceName,
|
||||
searchCriteria,
|
||||
onAssigned,
|
||||
onError,
|
||||
onCancel,
|
||||
}: AssignFormProps) {
|
||||
const [projectId, setProjectId] = useState("");
|
||||
const [assignStartDate, setAssignStartDate] = useState(searchCriteria.startDate);
|
||||
const [assignEndDate, setAssignEndDate] = useState(searchCriteria.endDate);
|
||||
@@ -350,24 +448,36 @@ function AssignForm({ resourceId, resourceName, searchCriteria, onAssigned, onEr
|
||||
});
|
||||
};
|
||||
|
||||
const projectList = (projects as { projects?: Array<{ id: string; name: string; shortCode?: string | null }> } | undefined)?.projects ?? [];
|
||||
const projectList =
|
||||
(
|
||||
projects as
|
||||
| { projects?: Array<{ id: string; name: string; shortCode?: string | null }> }
|
||||
| undefined
|
||||
)?.projects ?? [];
|
||||
const rolesList = (roles ?? []) as Array<{ id: string; name: string }>;
|
||||
const selectedProject = projectList.find((p) => p.id === projectId);
|
||||
|
||||
return (
|
||||
<div className="mt-4 rounded-xl border border-brand-200 bg-brand-50/50 p-4 dark:border-brand-900/40 dark:bg-brand-950/30">
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<h4 className="text-sm font-semibold text-gray-900 dark:text-gray-100">Assign {resourceName}</h4>
|
||||
<h4 className="text-sm font-semibold text-gray-900 dark:text-gray-100">
|
||||
Assign {resourceName}
|
||||
</h4>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-3 sm:grid-cols-2">
|
||||
<div className="sm:col-span-2">
|
||||
<label className="app-label">Project *</label>
|
||||
<select value={projectId} onChange={(e) => setProjectId(e.target.value)} className="app-input">
|
||||
<select
|
||||
value={projectId}
|
||||
onChange={(e) => setProjectId(e.target.value)}
|
||||
className="app-input"
|
||||
>
|
||||
<option value="">Select project...</option>
|
||||
{projectList.map((p) => (
|
||||
<option key={p.id} value={p.id}>
|
||||
{p.shortCode ? `[${p.shortCode}] ` : ""}{p.name}
|
||||
{p.shortCode ? `[${p.shortCode}] ` : ""}
|
||||
{p.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
@@ -380,7 +490,12 @@ function AssignForm({ resourceId, resourceName, searchCriteria, onAssigned, onEr
|
||||
|
||||
<div>
|
||||
<label className="app-label">End Date</label>
|
||||
<DateInput value={assignEndDate} onChange={setAssignEndDate} min={assignStartDate} className="app-input" />
|
||||
<DateInput
|
||||
value={assignEndDate}
|
||||
onChange={setAssignEndDate}
|
||||
min={assignStartDate}
|
||||
className="app-input"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
@@ -399,7 +514,11 @@ function AssignForm({ resourceId, resourceName, searchCriteria, onAssigned, onEr
|
||||
<div>
|
||||
<label className="app-label">Role</label>
|
||||
{rolesList.length > 0 ? (
|
||||
<select value={roleId} onChange={(e) => setRoleId(e.target.value)} className="app-input">
|
||||
<select
|
||||
value={roleId}
|
||||
onChange={(e) => setRoleId(e.target.value)}
|
||||
className="app-input"
|
||||
>
|
||||
<option value="">No role</option>
|
||||
{rolesList.map((r) => (
|
||||
<option key={r.id} value={r.id}>
|
||||
@@ -422,13 +541,20 @@ function AssignForm({ resourceId, resourceName, searchCriteria, onAssigned, onEr
|
||||
{selectedProject && (
|
||||
<div className="mt-3 text-xs text-gray-500">
|
||||
Assigning to{" "}
|
||||
<span className="font-medium text-gray-700 dark:text-gray-300">{selectedProject.name}</span>
|
||||
{" "}from {assignStartDate} to {assignEndDate} at {assignHours}h/day
|
||||
<span className="font-medium text-gray-700 dark:text-gray-300">
|
||||
{selectedProject.name}
|
||||
</span>{" "}
|
||||
from {assignStartDate} to {assignEndDate} at {assignHours}h/day
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-4 flex items-center gap-2">
|
||||
<Button variant="primary" size="sm" disabled={!canSubmit || createAssignment.isPending} onClick={handleSubmit}>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
disabled={!canSubmit || createAssignment.isPending}
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
{createAssignment.isPending ? "Assigning..." : "Confirm Assignment"}
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" onClick={onCancel} disabled={createAssignment.isPending}>
|
||||
@@ -487,7 +613,9 @@ function StatCard({
|
||||
|
||||
return (
|
||||
<div className={`rounded-2xl border p-3 ${toneClass}`}>
|
||||
<div className="text-[11px] font-semibold uppercase tracking-[0.16em] text-gray-500">{label}</div>
|
||||
<div className="text-[11px] font-semibold uppercase tracking-[0.16em] text-gray-500">
|
||||
{label}
|
||||
</div>
|
||||
<div className="mt-2 text-lg font-semibold text-gray-900 dark:text-gray-100">{value}</div>
|
||||
{helper && <div className="mt-1 text-xs text-gray-500 dark:text-gray-400">{helper}</div>}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user