feat(platform): checkpoint current implementation state

This commit is contained in:
2026-04-01 07:42:03 +02:00
parent 3e53471f05
commit 8c5be51251
125 changed files with 10269 additions and 17808 deletions
@@ -26,6 +26,7 @@ const SkillMatrixUpload = dynamic(
import { InfoTooltip } from "~/components/ui/InfoTooltip.js";
import { ProgressRing } from "~/components/ui/ProgressRing.js";
import { FadeIn } from "~/components/ui/FadeIn.js";
import { CommentThread } from "~/components/comments/CommentThread.js";
interface ResourceDetailProps {
resourceId: string;
@@ -91,6 +92,7 @@ export function ResourceDetail({ resourceId }: ResourceDetailProps) {
const resource = _resourceQuery.data as unknown as Resource | undefined;
const loadingResource = _resourceQuery.isLoading;
const error = _resourceQuery.error;
const errorCode = (error as any)?.data?.code as string | undefined;
// Fetch allocations for this resource (all non-cancelled)
const now = new Date();
@@ -119,6 +121,14 @@ export function ResourceDetail({ resourceId }: ResourceDetailProps) {
},
{ enabled: !!resourceId },
);
const vacationList = (vacations ?? []) as Array<{
endDate: Date | string;
id: string;
note?: string | null;
startDate: Date | string;
status: string;
type: string;
}>;
const chargeabilityStatsResult = trpc.resource.getChargeabilityStats.useQuery(
{ includeProposed: includeProposedChargeability, resourceId },
@@ -143,7 +153,7 @@ export function ResourceDetail({ resourceId }: ResourceDetailProps) {
);
}
if (error || !resource) {
if (errorCode === "NOT_FOUND") {
return (
<div className="p-6">
<div className="bg-red-50 border border-red-200 rounded-xl p-6 text-red-700 text-sm">
@@ -154,6 +164,17 @@ export function ResourceDetail({ resourceId }: ResourceDetailProps) {
);
}
if (error || !resource) {
return (
<div className="p-6">
<div className="bg-amber-50 border border-amber-200 rounded-xl p-6 text-amber-800 text-sm">
This resource could not be loaded right now.{" "}
<Link href="/resources" className="underline">Back to resources</Link>
</div>
</div>
);
}
const skills = resource.skills as unknown as SkillEntry[];
const resourceRoles = (resource as unknown as {
resourceRoles?: { isPrimary: boolean; role: { id: string; name: string; color: string | null } }[];
@@ -433,6 +454,24 @@ export function ResourceDetail({ resourceId }: ResourceDetailProps) {
onGenerated={async () => { await utils.resource.getById.invalidate({ id: resourceId }); }}
/>
<section
id="comments"
className="bg-white dark:bg-gray-900 rounded-xl border border-gray-200 dark:border-gray-700 p-5 scroll-mt-24"
>
<div className="mb-4">
<h2 className="text-sm font-semibold text-gray-800 dark:text-gray-200">Comments</h2>
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
Discussion for this resource follows the same visibility as the resource detail itself.
</p>
</div>
<CommentThread
commentTarget={{
entityType: "resource",
entityId: resourceId,
}}
/>
</section>
{/* Main Skills Badges */}
{mainSkills.length > 0 && (
<div className="bg-white dark:bg-gray-900 rounded-xl border border-gray-200 dark:border-gray-700 p-5">
@@ -594,11 +633,11 @@ export function ResourceDetail({ resourceId }: ResourceDetailProps) {
</div>
{loadingVacations ? (
<div className="p-6 text-center text-gray-400 text-sm animate-pulse">Loading</div>
) : (vacations ?? []).length === 0 ? (
) : vacationList.length === 0 ? (
<div className="text-center py-8 text-gray-400 text-sm">No vacations recorded.</div>
) : (
<div className="divide-y divide-gray-100">
{(vacations ?? []).map((v) => {
{vacationList.map((v) => {
const days =
Math.round(
(new Date(v.endDate).getTime() - new Date(v.startDate).getTime()) / (1000 * 60 * 60 * 24),