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>
46 lines
1.6 KiB
TypeScript
46 lines
1.6 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 { AttendanceCalendar } from "./attendance-calendar";
|
|
import type { Metadata } from "next";
|
|
|
|
export const metadata: Metadata = { title: "Attendance" };
|
|
|
|
export default async function AttendancePage() {
|
|
if (!CREWING_ENABLED) notFound();
|
|
|
|
const session = await auth();
|
|
if (!session?.user) redirect("/login");
|
|
const role = session.user.role;
|
|
if (!hasPermission(role, "view_attendance")) redirect("/dashboard"); // MPO has no attendance (R5)
|
|
|
|
const cutoff = new Date();
|
|
cutoff.setMonth(cutoff.getMonth() - 4);
|
|
|
|
const assignments = await 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 } },
|
|
attendance: { where: { date: { gte: cutoff } }, select: { date: true, status: true } },
|
|
},
|
|
});
|
|
|
|
return (
|
|
<AttendanceCalendar
|
|
assignments={assignments.map((a) => ({
|
|
id: a.id,
|
|
crewName: a.crewMember.name,
|
|
rank: a.rank.name,
|
|
location: a.vessel?.name ?? a.site?.name ?? "—",
|
|
marks: Object.fromEntries(a.attendance.map((m) => [m.date.toISOString().slice(0, 10), m.status])),
|
|
}))}
|
|
canEdit={hasPermission(role, "record_attendance")}
|
|
/>
|
|
);
|
|
}
|