import { useState } from "react"; import { useQuery } from "@tanstack/react-query"; import { useAuth } from "../context/AuthContext"; import { useAlert } from "../context/AlertContext"; import Box from "@mui/material/Box"; import Typography from "@mui/material/Typography"; import { formatDate, formatDatetime } from "../utils/attendanceHelpers"; import { czechPlural } from "../utils/formatters"; import { leavePendingOptions, leaveProcessedOptions, } from "../lib/queries/leave"; import Forbidden from "../components/Forbidden"; import { useApiMutation } from "../lib/queries/mutations"; import { Button, Card, DataTable, ConfirmDialog, Modal, StatusChip, Field, TextField, PageHeader, PageEnter, EmptyState, LoadingState, Tabs, TabPanel, type DataColumn, type TabDef, } from "../ui"; const API_BASE = "/api/admin"; const leaveTypeLabels: Record = { vacation: "Dovolená", sick: "Nemoc", unpaid: "Neplacené volno", }; const statusLabels: Record = { pending: "Čeká na schválení", approved: "Schváleno", rejected: "Zamítnuto", cancelled: "Zrušeno", }; // Leave type → StatusChip color const leaveTypeColor: Record< string, "info" | "error" | "default" | "success" | "warning" > = { vacation: "info", sick: "error", unpaid: "default", }; // Status → StatusChip color const statusColor: Record = { pending: "warning", approved: "success", rejected: "error", cancelled: "default", }; interface RawLeaveRequest { id: number; leave_type: string; date_from: string; date_to: string; total_days: number; total_hours: number; status: string; notes?: string; reviewer_note?: string; created_at: string; reviewed_at?: string; users_leave_requests_user_idTousers?: { first_name: string; last_name: string; }; users_leave_requests_reviewer_idTousers?: { first_name: string; last_name: string; } | null; } interface LeaveRequest { id: number; employee_name: string; leave_type: string; date_from: string; date_to: string; total_days: number; total_hours: number; status: string; notes?: string; reviewer_name?: string; reviewer_note?: string; created_at: string; reviewed_at?: string; } function mapLeaveRequest(raw: RawLeaveRequest): LeaveRequest { const user = raw.users_leave_requests_user_idTousers; const reviewer = raw.users_leave_requests_reviewer_idTousers; return { id: raw.id, employee_name: user ? `${user.first_name} ${user.last_name}` : "Neznámý", leave_type: raw.leave_type, date_from: raw.date_from, date_to: raw.date_to, total_days: raw.total_days, total_hours: raw.total_hours, status: raw.status, notes: raw.notes, reviewer_name: reviewer ? `${reviewer.first_name} ${reviewer.last_name}` : undefined, reviewer_note: raw.reviewer_note, created_at: raw.created_at, reviewed_at: raw.reviewed_at, }; } export default function LeaveApproval() { const { hasPermission } = useAuth(); const alert = useAlert(); const [activeTab, setActiveTab] = useState<"pending" | "processed">( "pending", ); const { data: pendingData, isPending: loading } = useQuery( leavePendingOptions(), ); const { data: processedData } = useQuery({ ...leaveProcessedOptions(), enabled: activeTab === "processed", }); const pendingRequests = (pendingData as RawLeaveRequest[] | undefined)?.map(mapLeaveRequest) ?? []; const pendingCount = pendingRequests.length; const processedRequests = (processedData as RawLeaveRequest[] | undefined)?.map(mapLeaveRequest) ?? []; const [approveModal, setApproveModal] = useState<{ open: boolean; request: LeaveRequest | null; }>({ open: false, request: null }); const [rejectModal, setRejectModal] = useState<{ open: boolean; request: LeaveRequest | null; }>({ open: false, request: null }); const [rejectNote, setRejectNote] = useState(""); const approveMutation = useApiMutation< { id: number; status: "approved" }, unknown >({ url: ({ id }) => `${API_BASE}/leave-requests/${id}`, method: () => "PUT", invalidate: ["leave-requests", "leave", "attendance", "users", "dashboard"], onSuccess: () => { setApproveModal({ open: false, request: null }); alert.success("Žádost byla schválena"); }, }); const rejectMutation = useApiMutation< { id: number; status: "rejected"; reviewer_note: string }, unknown >({ url: ({ id }) => `${API_BASE}/leave-requests/${id}`, method: () => "PUT", invalidate: ["leave-requests", "leave", "attendance", "users", "dashboard"], onSuccess: () => { setRejectModal({ open: false, request: null }); setRejectNote(""); alert.success("Žádost byla zamítnuta"); }, }); if (!hasPermission("attendance.approve")) return ; const handleApprove = async () => { if (!approveModal.request) return; try { await approveMutation.mutateAsync({ id: approveModal.request.id, status: "approved", }); } catch (e) { alert.error(e instanceof Error ? e.message : "Chyba připojení"); } }; const handleReject = async () => { if (!rejectNote.trim()) { alert.error("Důvod zamítnutí je povinný"); return; } if (!rejectModal.request) return; try { await rejectMutation.mutateAsync({ id: rejectModal.request.id, status: "rejected", reviewer_note: rejectNote, }); } catch (e) { alert.error(e instanceof Error ? e.message : "Chyba připojení"); } }; if (loading) { return ; } const tabs: TabDef[] = [ { value: "pending", label: `Ke schválení (${pendingCount})` }, { value: "processed", label: "Vyřízené" }, ]; const processedColumns: DataColumn[] = [ { key: "employee_name", header: "Zaměstnanec", width: "16%", bold: true, render: (req) => req.employee_name, }, { key: "leave_type", header: "Typ", width: "13%", render: (req) => ( ), }, { key: "date_from", header: "Od", width: "10%", mono: true, render: (req) => formatDate(req.date_from), }, { key: "date_to", header: "Do", width: "10%", mono: true, render: (req) => formatDate(req.date_to), }, { key: "total_days", header: "Dny", width: "6%", mono: true, render: (req) => String(req.total_days), }, { key: "status", header: "Stav", width: "13%", render: (req) => ( ), }, { key: "reviewer_name", header: "Schválil", width: "12%", render: (req) => req.reviewer_name || "—", }, { key: "reviewer_note", header: "Poznámka", width: "12%", render: (req) => req.reviewer_note ? ( {req.reviewer_note.length > 40 ? `${req.reviewer_note.substring(0, 40)}...` : req.reviewer_note} ) : ( "—" ), }, { key: "reviewed_at", header: "Vyřízeno", width: "8%", mono: true, render: (req) => formatDatetime(req.reviewed_at), }, ]; return ( 0 ? `${pendingCount} ${czechPlural(pendingCount, "žádost čeká", "žádosti čekají", "žádostí čeká")} na schválení` : "Žádné čekající žádosti" } /> setActiveTab(v as "pending" | "processed")} tabs={tabs} /> {/* Pending Tab */} {pendingRequests.length === 0 ? ( ) : ( {pendingRequests.map((req) => ( {req.employee_name} {formatDate(req.date_from)} —{" "} {formatDate(req.date_to)} {req.total_days}{" "} {czechPlural(req.total_days, "den", "dny", "dnů")} ( {req.total_hours}h) Podáno: {formatDatetime(req.created_at)} {req.notes && ( {req.notes} )} ))} )} {/* Processed Tab */} columns={processedColumns} rows={processedRequests} rowKey={(req) => req.id} empty={} /> {/* Approve Confirmation */} setApproveModal({ open: false, request: null })} onConfirm={handleApprove} title="Schválit žádost" message={ approveModal.request ? `Schválit ${approveModal.request.total_days} ${czechPlural(approveModal.request.total_days, "den", "dny", "dnů")} ${leaveTypeLabels[approveModal.request.leave_type]?.toLowerCase() || ""} pro ${approveModal.request.employee_name}?` : "" } confirmText="Schválit" loading={approveMutation.isPending} /> {/* Reject Modal */} { setRejectModal({ open: false, request: null }); setRejectNote(""); }} onSubmit={handleReject} title="Zamítnout žádost" submitText="Zamítnout" loading={rejectMutation.isPending} > {rejectModal.request && ( <> {rejectModal.request.employee_name} —{" "} {leaveTypeLabels[rejectModal.request.leave_type]},{" "} {formatDate(rejectModal.request.date_from)} —{" "} {formatDate(rejectModal.request.date_to)} ( {rejectModal.request.total_days} dnů) setRejectNote(e.target.value)} placeholder="Uveďte důvod zamítnutí..." multiline rows={3} autoFocus /> )} ); }