302 lines
11 KiB
TypeScript
302 lines
11 KiB
TypeScript
"use client";
|
||
|
||
import { useAuth } from "@/lib/auth-context";
|
||
import { DEPARTMENTS } from "@/lib/constants";
|
||
import { useShift } from "@/lib/shift-context";
|
||
import { useEffect, useState } from "react";
|
||
|
||
import type { ShiftDetailModalProps } from "./ShiftSchedule.types";
|
||
|
||
function parseShiftDate(date: Date | string | undefined): Date | null {
|
||
if (!date) return null;
|
||
// Date object → dùng thẳng
|
||
if (date instanceof Date) return isNaN(date.getTime()) ? null : date;
|
||
// ISO string hoặc "YYYY-MM-DD" → chỉ lấy 10 ký tự đầu tránh lệch timezone
|
||
const [y, m, d] = (date as string).slice(0, 10).split("-").map(Number);
|
||
if (!y || !m || !d) return null;
|
||
return new Date(y, m - 1, d);
|
||
}
|
||
|
||
export default function ShiftDetailModal({
|
||
shift,
|
||
isOpen,
|
||
onClose,
|
||
}: ShiftDetailModalProps) {
|
||
const { user } = useAuth();
|
||
const { registerShift, unregisterShift, deleteShift } = useShift();
|
||
const [error, setError] = useState<string | null>(null);
|
||
const [success, setSuccess] = useState<string | null>(null);
|
||
|
||
if (!isOpen || !shift) return null;
|
||
|
||
// const dept = DEPARTMENTS.find((d) => d.id === shift.department);
|
||
const isManager = user?.role === "manager";
|
||
const registeredStaff = shift.registeredStaff ?? [];
|
||
const isRegistered = user
|
||
? registeredStaff.some((s) => s.id === user.id)
|
||
: false;
|
||
const isFull = registeredStaff.length >= shift.maxStaff;
|
||
|
||
const handleRegister = async () => {
|
||
if (!user) return;
|
||
setError(null);
|
||
setSuccess(null);
|
||
const result = await registerShift(shift.id, user.id, user.name);
|
||
if (result.success) {
|
||
setSuccess("Shift registered successfully!");
|
||
setTimeout(onClose, 1200);
|
||
} else {
|
||
setError(result.error ?? "An error occurred.");
|
||
}
|
||
};
|
||
|
||
const handleUnregister = () => {
|
||
if (!user) return;
|
||
setError(null);
|
||
unregisterShift(shift.id, user.id);
|
||
setSuccess("Shift registration cancelled.");
|
||
setTimeout(onClose, 1200);
|
||
};
|
||
|
||
const handleManagerUnregister = (staffId: string) => {
|
||
unregisterShift(shift.id, staffId);
|
||
setSuccess("Staff removed from shift.");
|
||
};
|
||
|
||
const handleDelete = async () => {
|
||
await deleteShift(shift.id);
|
||
onClose();
|
||
};
|
||
|
||
const statusLabel = {
|
||
available: "Available",
|
||
registered: "Registered",
|
||
approved_leave: "On leave",
|
||
absent: "Absent",
|
||
};
|
||
|
||
const statusColor = {
|
||
available: "bg-blue-100 text-blue-700",
|
||
registered: "bg-blue-200 text-blue-900",
|
||
approved_leave: "bg-purple-100 text-purple-700",
|
||
absent: "bg-red-100 text-red-700",
|
||
};
|
||
|
||
const StaffName = ({ staffId }: { staffId: string }) => {
|
||
const [name, setName] = useState("");
|
||
const [loading, setLoading] = useState(true);
|
||
|
||
useEffect(() => {
|
||
// Hàm gọi API
|
||
const getName = async () => {
|
||
try {
|
||
setLoading(true);
|
||
|
||
const response = await fetch(`/api/staff/${staffId}`);
|
||
|
||
if (!response.ok) {
|
||
throw new Error("Mạng lỗi hoặc không tìm thấy nhân viên");
|
||
}
|
||
|
||
const data = await response.json();
|
||
|
||
setName(data["name"]);
|
||
} catch (error) {
|
||
console.error("Lỗi khi lấy tên:", error);
|
||
setName("Lỗi tải tên");
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
if (staffId) {
|
||
getName();
|
||
}
|
||
}, [staffId]); // Chạy lại nếu staffId thay đổi
|
||
|
||
if (loading)
|
||
return <span className="animate-pulse text-gray-400">...</span>;
|
||
|
||
return <span>{name || "N/A"}</span>;
|
||
};
|
||
|
||
return (
|
||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
||
{/* Backdrop */}
|
||
<div className="absolute inset-0 bg-black/40" onClick={onClose} />
|
||
|
||
{/* Modal */}
|
||
<div className="relative w-full max-w-md rounded-2xl bg-white shadow-xl">
|
||
{/* Header */}
|
||
<div className="flex items-center justify-between border-b border-(--color-border-light) px-5 py-4">
|
||
<div className="flex items-center gap-3">
|
||
{/* {dept && <i className={`${dept.icon} text-(--color-primary)`}></i>} */}
|
||
<div>
|
||
<h2 className="text-foreground text-base font-bold">
|
||
Shift Details
|
||
</h2>
|
||
{/* <p className="text-xs text-(--color-text-muted)">{dept?.name}</p> */}
|
||
</div>
|
||
</div>
|
||
<button
|
||
title="Close"
|
||
type="button"
|
||
onClick={onClose}
|
||
className="flex h-8 w-8 cursor-pointer items-center justify-center rounded-full border-none bg-transparent text-(--color-text-muted) transition hover:bg-gray-100"
|
||
>
|
||
<i className="fa-solid fa-xmark"></i>
|
||
</button>
|
||
</div>
|
||
|
||
{/* Body */}
|
||
<div className="space-y-4 px-5 py-4">
|
||
{/* Status badge */}
|
||
{/* <div className="flex items-center gap-2">
|
||
<span
|
||
className={`rounded-full px-3 py-1 text-xs font-semibold ${statusColor[shift.status]}`}
|
||
>
|
||
{statusLabel[shift.status]}
|
||
</span>
|
||
</div> */}
|
||
|
||
{/* Shift info grid */}
|
||
<div className="grid grid-cols-2 gap-3">
|
||
<div className="rounded-xl bg-gray-50 p-3">
|
||
<p className="text-[10px] font-semibold text-(--color-text-muted) uppercase">
|
||
Date
|
||
</p>
|
||
<p className="text-foreground mt-1 text-sm font-bold">
|
||
{parseShiftDate(
|
||
shift.date as Date | string | undefined,
|
||
)?.toLocaleDateString("vi-VN", {
|
||
weekday: "long",
|
||
day: "2-digit",
|
||
month: "2-digit",
|
||
year: "numeric",
|
||
}) ?? "—"}
|
||
</p>
|
||
</div>
|
||
<div className="rounded-xl bg-gray-50 p-3">
|
||
<p className="text-[10px] font-semibold text-(--color-text-muted) uppercase">
|
||
Working hours
|
||
</p>
|
||
<p className="text-foreground mt-1 text-sm font-bold">
|
||
{shift.startTime} – {shift.endTime}
|
||
</p>
|
||
</div>
|
||
<div className="rounded-xl bg-gray-50 p-3">
|
||
<p className="text-[10px] font-semibold text-(--color-text-muted) uppercase">
|
||
Duration
|
||
</p>
|
||
<p className="text-foreground mt-1 text-sm font-bold">{""} giờ</p>
|
||
</div>
|
||
<div className="rounded-xl bg-gray-50 p-3">
|
||
<p className="text-[10px] font-semibold text-(--color-text-muted) uppercase">
|
||
Shift wage
|
||
</p>
|
||
<p className="mt-1 text-sm font-bold text-(--color-primary)">
|
||
{(shift.wage ?? 0).toLocaleString("vi-VN")} VND
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Registered staff */}
|
||
<div>
|
||
<p className="mb-2 text-xs font-semibold text-(--color-text-secondary)">
|
||
Registered staff ({registeredStaff.length}/{shift.maxStaff})
|
||
</p>
|
||
{registeredStaff.length === 0 ? (
|
||
<p className="text-xs text-(--color-text-muted) italic">
|
||
No staff registered yet
|
||
</p>
|
||
) : (
|
||
<div className="space-y-2">
|
||
{registeredStaff.map((staff) => (
|
||
<div
|
||
key={staff.id}
|
||
className="flex items-center justify-between rounded-xl bg-gray-50 px-3 py-2"
|
||
>
|
||
<div className="flex items-center gap-2">
|
||
<div className="flex h-7 w-7 items-center justify-center rounded-full bg-(--color-primary)/10">
|
||
<i className="fa-solid fa-user text-[10px] text-(--color-primary)"></i>
|
||
</div>
|
||
<span className="text-foreground text-sm font-medium">
|
||
<StaffName staffId={staff.staffId} />
|
||
</span>
|
||
</div>
|
||
{isManager && (
|
||
<button
|
||
type="button"
|
||
onClick={() => handleManagerUnregister(staff.id)}
|
||
className="cursor-pointer rounded-lg border-none bg-transparent px-2 py-1 text-xs text-red-500 transition hover:bg-red-50"
|
||
>
|
||
<i className="fa-solid fa-user-minus mr-1"></i>
|
||
Remove
|
||
</button>
|
||
)}
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* Feedback messages */}
|
||
{error && (
|
||
<div className="rounded-xl bg-red-50 px-4 py-3 text-sm text-red-700">
|
||
<i className="fa-solid fa-circle-exclamation mr-2"></i>
|
||
{error}
|
||
</div>
|
||
)}
|
||
{success && (
|
||
<div className="rounded-xl bg-green-50 px-4 py-3 text-sm text-green-700">
|
||
<i className="fa-solid fa-circle-check mr-2"></i>
|
||
{success}
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* Footer actions */}
|
||
<div className="flex gap-2 border-t border-(--color-border-light) px-5 py-4">
|
||
{!isManager && !isRegistered && !isFull && (
|
||
<button
|
||
type="button"
|
||
onClick={handleRegister}
|
||
className="flex-1 cursor-pointer rounded-xl border-none bg-(--color-primary) px-4 py-2.5 text-sm font-semibold text-white transition hover:opacity-90"
|
||
>
|
||
<i className="fa-solid fa-calendar-plus mr-2"></i>
|
||
Register
|
||
</button>
|
||
)}
|
||
{isRegistered && (
|
||
<button
|
||
type="button"
|
||
onClick={handleUnregister}
|
||
className="flex-1 cursor-pointer rounded-xl border border-red-200 bg-transparent px-4 py-2.5 text-sm font-semibold text-red-600 transition hover:bg-red-50"
|
||
>
|
||
<i className="fa-solid fa-calendar-minus mr-2"></i>
|
||
Unregister
|
||
</button>
|
||
)}
|
||
{isManager && (
|
||
<button
|
||
type="button"
|
||
onClick={handleDelete}
|
||
className="cursor-pointer rounded-xl border border-red-200 bg-transparent px-4 py-2.5 text-sm font-semibold text-red-600 transition hover:bg-red-50"
|
||
>
|
||
<i className="fa-solid fa-trash mr-2"></i>
|
||
Delete shift
|
||
</button>
|
||
)}
|
||
<button
|
||
type="button"
|
||
onClick={onClose}
|
||
className="cursor-pointer rounded-xl border border-(--color-border-light) bg-transparent px-4 py-2.5 text-sm font-medium text-(--color-text-secondary) transition hover:bg-gray-50"
|
||
>
|
||
Close
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|