Files
Nexus/apps/web/src/components/vacations/MyVacationsClient.tsx
T
Hartmut cd78f72f33 chore: full technical rename planarchy → capakraken
Complete rename of all technical identifiers across the codebase:

Package names (11 packages):
- @planarchy/* → @capakraken/* in all package.json, tsconfig, imports

Import statements: 277 files, 548 occurrences replaced

Database & Docker:
- PostgreSQL user/db: planarchy → capakraken
- Docker volumes: planarchy_pgdata → capakraken_pgdata
- Connection strings updated in docker-compose, .env, CI

CI/CD:
- GitHub Actions workflow: all filter commands updated
- Test database credentials updated

Infrastructure:
- Redis channel: planarchy:sse → capakraken:sse
- Logger service name: planarchy-api → capakraken-api
- Anonymization seed updated
- Start/stop/restart scripts updated

Test data:
- Seed emails: @planarchy.dev → @capakraken.dev
- E2E test credentials: all 11 spec files updated
- Email defaults: @planarchy.app → @capakraken.app
- localStorage keys: planarchy_* → capakraken_*

Documentation: 30+ .md files updated

Verification:
- pnpm install: workspace resolution works
- TypeScript: only pre-existing TS2589 (no new errors)
- Engine: 310/310 tests pass
- Staffing: 37/37 tests pass

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-03-27 13:18:09 +01:00

164 lines
7.5 KiB
TypeScript

"use client";
import { useState } from "react";
import { VacationStatus, VacationType } from "@capakraken/shared";
import { trpc } from "~/lib/trpc/client.js";
import { VacationModal } from "./VacationModal.js";
import { InfoTooltip } from "~/components/ui/InfoTooltip.js";
import { BalanceCard } from "./BalanceCard.js";
import { VacationCalendar } from "./VacationCalendar.js";
import { VACATION_STATUS_BADGE as STATUS_BADGE, VACATION_TYPE_LABELS as TYPE_LABELS, VACATION_TYPE_BADGE } from "~/lib/status-styles.js";
export function MyVacationsClient() {
const [showModal, setShowModal] = useState(false);
const utils = trpc.useUtils();
// Find resource linked to current user
const { data: myResource } = trpc.resource.getMyResource.useQuery(undefined, {
staleTime: 60_000,
});
const resourceId = myResource?.id;
const { data: vacations, isLoading, refetch } = trpc.vacation.list.useQuery(
{ resourceId, limit: 200 },
{ enabled: !!resourceId, staleTime: 15_000 },
);
const cancelMutation = trpc.vacation.cancel.useMutation({
onSuccess: async () => {
await utils.vacation.list.invalidate();
await utils.entitlement.getBalance.invalidate();
},
});
const vacationList = vacations ?? [];
return (
<div className="p-6 max-w-4xl mx-auto space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-gray-900 dark:text-gray-100">My Vacations</h1>
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">Manage your personal vacation requests</p>
</div>
<button
type="button"
onClick={() => setShowModal(true)}
disabled={!resourceId}
className="px-4 py-2 bg-brand-600 text-white rounded-lg hover:bg-brand-700 text-sm font-medium disabled:opacity-50"
>
+ Request Vacation
</button>
</div>
{!resourceId && (
<div className="rounded-xl bg-amber-50 border border-amber-200 p-4 text-sm text-amber-700">
Your account is not linked to a resource. Please contact an administrator.
</div>
)}
{/* Balance card */}
{resourceId && (
<BalanceCard resourceId={resourceId} />
)}
{/* Calendar */}
{resourceId && vacationList.length > 0 && (
<VacationCalendar vacations={vacationList} />
)}
{/* Vacation list */}
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 overflow-hidden">
{isLoading ? (
<div className="p-8 text-center text-sm text-gray-400">Loading</div>
) : vacationList.length === 0 ? (
<div className="p-8 text-center text-sm text-gray-400">No vacation requests yet.</div>
) : (
<table className="w-full text-sm">
<thead>
<tr className="border-b border-gray-100 dark:border-gray-700 bg-gray-50 dark:bg-gray-900">
<th className="text-left px-4 py-3 font-medium text-gray-500 dark:text-gray-400 text-xs uppercase tracking-wider">
Type <InfoTooltip content="ANNUAL = paid annual leave · SICK = sick leave · PUBLIC_HOLIDAY = public holiday · OTHER = other leave types." />
</th>
<th className="text-left px-4 py-3 font-medium text-gray-500 dark:text-gray-400 text-xs uppercase tracking-wider">Start</th>
<th className="text-left px-4 py-3 font-medium text-gray-500 dark:text-gray-400 text-xs uppercase tracking-wider">End</th>
<th className="text-right px-4 py-3 font-medium text-gray-500 dark:text-gray-400 text-xs uppercase tracking-wider">
<span className="inline-flex items-center justify-end gap-0.5">
Days <InfoTooltip content="Calendar days from start to end date (inclusive). Shows 0.5 for half-day requests (½ indicator on start date)." />
</span>
</th>
<th className="text-left px-4 py-3 font-medium text-gray-500 dark:text-gray-400 text-xs uppercase tracking-wider">
Status <InfoTooltip content="PENDING = awaiting manager approval · APPROVED = confirmed leave · REJECTED = declined by manager · CANCELLED = withdrawn by employee." />
</th>
<th className="text-left px-4 py-3 font-medium text-gray-500 dark:text-gray-400 text-xs uppercase tracking-wider">
Note <InfoTooltip content="Your note on the request, or the manager's rejection reason if declined." />
</th>
<th className="px-4 py-3" />
</tr>
</thead>
<tbody className="divide-y divide-gray-50 dark:divide-gray-700">
{vacationList.map((v) => {
const start = new Date(v.startDate);
const end = new Date(v.endDate);
const days = Math.round((end.getTime() - start.getTime()) / 86_400_000) + 1;
const status = v.status as string;
const type = v.type as string;
const vWithExtra = v as unknown as { rejectionReason?: string | null; isHalfDay?: boolean };
return (
<tr key={v.id} className="hover:bg-gray-50 dark:hover:bg-gray-700/50">
<td className="px-4 py-3">
<span className={`inline-flex px-2 py-0.5 rounded-full text-xs font-medium ${VACATION_TYPE_BADGE[type] ?? "bg-gray-100 text-gray-600"}`}>
{TYPE_LABELS[type] ?? type}
</span>
</td>
<td className="px-4 py-3 text-gray-600 dark:text-gray-400">{start.toLocaleDateString("en-GB")}</td>
<td className="px-4 py-3 text-gray-600 dark:text-gray-400">{end.toLocaleDateString("en-GB")}</td>
<td className="px-4 py-3 text-gray-600 dark:text-gray-400">{vWithExtra.isHalfDay ? "0.5" : days}</td>
<td className="px-4 py-3">
<span className={`inline-flex px-2 py-0.5 rounded-full text-xs font-medium ${STATUS_BADGE[status] ?? ""}`}>
{status}
</span>
</td>
<td className="px-4 py-3 text-gray-400 dark:text-gray-500 text-xs max-w-[200px]">
{vWithExtra.rejectionReason ? (
<span className="text-red-500">{vWithExtra.rejectionReason}</span>
) : (v.note ?? "—")}
</td>
<td className="px-4 py-3 text-right">
{(status === VacationStatus.PENDING || status === VacationStatus.APPROVED) && (
<button
type="button"
onClick={() => cancelMutation.mutate({ id: v.id })}
disabled={cancelMutation.isPending}
className="text-xs text-gray-400 hover:text-red-600 underline disabled:opacity-50"
>
Cancel
</button>
)}
</td>
</tr>
);
})}
</tbody>
</table>
)}
</div>
{showModal && resourceId && (
<VacationModal
resourceId={resourceId}
onClose={() => setShowModal(false)}
onSuccess={() => {
setShowModal(false);
void refetch();
void utils.entitlement.getBalance.invalidate();
}}
/>
)}
</div>
);
}