Second slice of Phase 4 (stacked on 4a crew records). Leave (site-applied, Manager-decided) with clash auto-backfill, and the daily attendance calendar, per Crewing-Implementation-Spec §5.3/§8.9–8.10. Behind NEXT_PUBLIC_CREWING_ENABLED. What's in - Schema (crewing_leave_attendance migration): LeaveRequest (LeaveType, LeaveStatus) + Attendance (AttendanceStatus, unique per assignment+date) on CrewAssignment; CrewActionType += LEAVE_APPLIED/LEAVE_DECIDED/ATTENDANCE_RECORDED. - Leave (R1): site staff apply on behalf (apply_leave); Manager decides (decide_leave) → assignment ON_LEAVE; MPO has no leave role. Leave approvals also surface in the central /approvals queue (§8.13 Leave kind). Notification LEAVE_FOR_APPROVAL. - Clash auto-backfill (R6): lib/leave-clash.ts, required strength = 1 — approving a leave that leaves the vessel with zero active same-rank cover auto-raises a LEAVE requisition via the Phase-2 autoRaiseRequisition. - Attendance (R5): daily month calendar; site staff record (record_attendance), Manager views (view_attendance) but cannot edit, MPO neither. saveAttendance bulk-upserts dirty cells. - Screens: /crewing/leave (apply-on-behalf + Manager Approve/Decline) and /crewing/attendance (tap-to-cycle calendar + Save). Leave + Attendance added to the flag-gated nav (Manager + Site staff). Tests & docs - Integration: leave-attendance.test.ts (7) — apply/decide, clash auto-raise (and no-raise when cover remains), MPO/Manager attendance lockout, permission gating. type-check clean; full unit (240) + integration (182) green. - CLAUDE.md updated with the Phase 4b surface. Deferred: the 6-month leave-planner timeline (lightweight list for now); hours/ overtime attendance (A7). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
52 lines
2 KiB
TypeScript
52 lines
2 KiB
TypeScript
import { auth } from "@/auth";
|
|
import { db } from "@/lib/db";
|
|
import { hasPermission } from "@/lib/permissions";
|
|
import { CREWING_ENABLED } from "@/lib/feature-flags";
|
|
import { redirect, notFound } from "next/navigation";
|
|
import { LeaveManager } from "./leave-manager";
|
|
import type { Metadata } from "next";
|
|
|
|
export const metadata: Metadata = { title: "Leave" };
|
|
|
|
export default async function LeavePage() {
|
|
if (!CREWING_ENABLED) notFound();
|
|
|
|
const session = await auth();
|
|
if (!session?.user) redirect("/login");
|
|
const role = session.user.role;
|
|
const canApply = hasPermission(role, "apply_leave");
|
|
const canDecide = hasPermission(role, "decide_leave");
|
|
if (!canApply && !canDecide) redirect("/dashboard"); // MPO has no leave screen (R1)
|
|
|
|
const [assignments, requests] = await Promise.all([
|
|
db.crewAssignment.findMany({
|
|
where: { status: { not: "SIGNED_OFF" } },
|
|
orderBy: { crewMember: { name: "asc" } },
|
|
include: { crewMember: { select: { name: true } }, rank: { select: { name: true } }, vessel: { select: { name: true } }, site: { select: { name: true } } },
|
|
}),
|
|
db.leaveRequest.findMany({
|
|
orderBy: { createdAt: "desc" },
|
|
take: 100,
|
|
include: { assignment: { include: { crewMember: { select: { name: true } }, rank: { select: { name: true } }, vessel: { select: { name: true } }, site: { select: { name: true } } } } },
|
|
}),
|
|
]);
|
|
|
|
return (
|
|
<LeaveManager
|
|
assignments={assignments.map((a) => ({ id: a.id, crewName: a.crewMember.name, rank: a.rank.name, location: a.vessel?.name ?? a.site?.name ?? "—" }))}
|
|
requests={requests.map((r) => ({
|
|
id: r.id,
|
|
crewName: r.assignment.crewMember.name,
|
|
rank: r.assignment.rank.name,
|
|
location: r.assignment.vessel?.name ?? r.assignment.site?.name ?? "—",
|
|
type: r.type,
|
|
status: r.status,
|
|
fromDate: r.fromDate.toISOString(),
|
|
toDate: r.toDate.toISOString(),
|
|
reason: r.reason,
|
|
}))}
|
|
canApply={canApply}
|
|
canDecide={canDecide}
|
|
/>
|
|
);
|
|
}
|