Files
Nexus/apps/web/src/components/vacations/EntitlementManager.tsx
T

133 lines
6.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { useState } from "react";
import { trpc } from "~/lib/trpc/client.js";
import { InfoTooltip } from "~/components/ui/InfoTooltip.js";
export function EntitlementManager() {
const [year, setYear] = useState(new Date().getFullYear());
const [bulkDays, setBulkDays] = useState(28);
const [bulkResult, setBulkResult] = useState<number | null>(null);
const utils = trpc.useUtils();
const bulkSetMutation = trpc.entitlement.bulkSet.useMutation({
onSuccess: async (data) => {
setBulkResult(data.updated);
await utils.entitlement.getBalance.invalidate();
},
});
const { data: summary, isLoading } = trpc.entitlement.getYearSummary.useQuery(
{ year },
{ staleTime: 30_000 },
);
return (
<div className="rounded-xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 p-5 space-y-5">
<h3 className="text-sm font-semibold text-gray-800 dark:text-gray-100">Vacation Entitlement Manager</h3>
{/* Controls */}
<div className="flex flex-wrap gap-4 items-end">
<div>
<label className="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">Year</label>
<input
type="number"
value={year}
onChange={(e) => setYear(parseInt(e.target.value, 10))}
min={2020}
max={2030}
className="w-24 px-3 py-2 border border-gray-200 dark:border-gray-600 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-brand-500 dark:bg-gray-900 dark:text-gray-100"
/>
</div>
<div>
<label className="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">Default Days (bulk set)</label>
<input
type="number"
value={bulkDays}
onChange={(e) => setBulkDays(parseInt(e.target.value, 10))}
min={0}
max={365}
className="w-24 px-3 py-2 border border-gray-200 dark:border-gray-600 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-brand-500 dark:bg-gray-900 dark:text-gray-100"
/>
</div>
<button
type="button"
onClick={() => {
setBulkResult(null);
bulkSetMutation.mutate({ year, entitledDays: bulkDays });
}}
disabled={bulkSetMutation.isPending}
className="px-4 py-2 bg-brand-600 text-white rounded-lg hover:bg-brand-700 text-sm font-medium disabled:opacity-50"
>
{bulkSetMutation.isPending ? "Setting…" : "Bulk Set All Resources"}
</button>
{bulkResult !== null && (
<span className="text-sm text-emerald-600 dark:text-emerald-400">Updated {bulkResult} resources</span>
)}
</div>
{/* Year summary table */}
<div className="overflow-hidden rounded-lg border border-gray-100 dark:border-gray-700">
{isLoading ? (
<div className="p-6 text-center text-sm text-gray-400">Loading</div>
) : !summary?.length ? (
<div className="p-6 text-center text-sm text-gray-400">No resources found.</div>
) : (
<table className="w-full text-sm">
<thead>
<tr className="bg-gray-50 dark:bg-gray-900 border-b border-gray-100 dark:border-gray-700">
<th className="text-left px-4 py-2.5 text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Resource</th>
<th className="text-left px-4 py-2.5 text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Chapter</th>
<th className="text-right px-4 py-2.5 text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
<span className="inline-flex items-center justify-end gap-0.5">
Entitled <InfoTooltip content="Total vacation days granted to this resource for the selected year." />
</span>
</th>
<th className="text-right px-4 py-2.5 text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
<span className="inline-flex items-center justify-end gap-0.5">
Carryover <InfoTooltip content="Unused days carried over from the previous year." />
</span>
</th>
<th className="text-right px-4 py-2.5 text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
<span className="inline-flex items-center justify-end gap-0.5">
Used <InfoTooltip content="Days already consumed by APPROVED vacations that have passed." />
</span>
</th>
<th className="text-right px-4 py-2.5 text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
<span className="inline-flex items-center justify-end gap-0.5">
Pending <InfoTooltip content="Days reserved by APPROVED future vacations not yet started." />
</span>
</th>
<th className="text-right px-4 py-2.5 text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
<span className="inline-flex items-center justify-end gap-0.5">
Remaining <InfoTooltip content="Entitled + Carryover Used Pending. Shown in red if fewer than 5 days remain." />
</span>
</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-50 dark:divide-gray-700">
{summary.map((row) => (
<tr key={row.resourceId} className="hover:bg-gray-50 dark:hover:bg-gray-700/50">
<td className="px-4 py-2.5">
<span className="font-medium text-gray-900 dark:text-gray-100">{row.displayName}</span>
<span className="text-xs text-gray-400 dark:text-gray-500 ml-1">({row.eid})</span>
</td>
<td className="px-4 py-2.5 text-gray-500 dark:text-gray-400">{row.chapter ?? "—"}</td>
<td className="px-4 py-2.5 text-right text-gray-700 dark:text-gray-300">{row.entitledDays}</td>
<td className="px-4 py-2.5 text-right text-gray-400 dark:text-gray-500">{row.carryoverDays}</td>
<td className="px-4 py-2.5 text-right text-gray-700 dark:text-gray-300">{row.usedDays}</td>
<td className="px-4 py-2.5 text-right text-amber-600">{row.pendingDays}</td>
<td className={`px-4 py-2.5 text-right font-semibold ${row.remainingDays < 5 ? "text-red-600" : "text-emerald-600"}`}>
{row.remainingDays}
</td>
</tr>
))}
</tbody>
</table>
)}
</div>
</div>
);
}