import { useState } from "react"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import { useAuth } from "../context/AuthContext"; import { useAlert } from "../context/AlertContext"; import { motion, AnimatePresence } from "framer-motion"; import { formatDate, formatDatetime } from "../utils/attendanceHelpers"; import apiFetch from "../utils/api"; import { czechPlural } from "../utils/formatters"; import { leavePendingOptions, leaveProcessedOptions, } from "../lib/queries/leave"; import ConfirmModal from "../components/ConfirmModal"; import Forbidden from "../components/Forbidden"; import useModalLock from "../hooks/useModalLock"; import FormField from "../components/FormField"; import { Skeleton } from "boneyard-js/react"; import LeaveApprovalFixture from "../fixtures/LeaveApprovalFixture"; const API_BASE = "/api/admin"; const leaveTypeLabels: Record = { vacation: "Dovolená", sick: "Nemoc", unpaid: "Neplacené volno", }; const leaveTypeClasses: Record = { vacation: "badge-vacation", sick: "badge-sick", unpaid: "badge-unpaid", }; const statusLabels: Record = { pending: "Čeká na schválení", approved: "Schváleno", rejected: "Zamítnuto", cancelled: "Zrušeno", }; const statusClasses: Record = { pending: "badge-pending", approved: "badge-approved", rejected: "badge-rejected", cancelled: "badge-cancelled", }; 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 queryClient = useQueryClient(); 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 [processing, setProcessing] = useState(false); useModalLock(rejectModal.open); if (!hasPermission("attendance.approve")) return ; const handleApprove = async () => { setProcessing(true); try { const response = await apiFetch( `${API_BASE}/leave-requests/${approveModal.request!.id}`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: "approved" }), }, ); if (response.status === 401) return; const result = await response.json(); if (result.success) { setApproveModal({ open: false, request: null }); await queryClient.invalidateQueries({ queryKey: ["leave"] }); alert.success("Žádost byla schválena"); } else { alert.error(result.error); } } catch { alert.error("Chyba připojení"); } finally { setProcessing(false); } }; const handleReject = async () => { if (!rejectNote.trim()) { alert.error("Důvod zamítnutí je povinný"); return; } setProcessing(true); try { const response = await apiFetch( `${API_BASE}/leave-requests/${rejectModal.request!.id}`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: "rejected", reviewer_note: rejectNote, }), }, ); if (response.status === 401) return; const result = await response.json(); if (result.success) { setRejectModal({ open: false, request: null }); setRejectNote(""); await queryClient.invalidateQueries({ queryKey: ["leave"] }); alert.success("Žádost byla zamítnuta"); } else { alert.error(result.error); } } catch { alert.error("Chyba připojení"); } finally { setProcessing(false); } }; return ( } >

Schvalování nepřítomnosti

{pendingCount > 0 ? `${pendingCount} ${czechPlural(pendingCount, "žádost čeká", "žádosti čekají", "žádostí čeká")} na schválení` : "Žádné čekající žádosti"}

{/* Tabs */}
{/* Pending Tab */} {activeTab === "pending" && ( {pendingRequests.length === 0 ? (

Žádné čekající žádosti

) : (
{pendingRequests.map((req) => (
{req.employee_name} {leaveTypeLabels[req.leave_type] || req.leave_type}
{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 */} {activeTab === "processed" && (
{processedRequests.length === 0 ? (

Zatím žádné vyřízené žádosti

) : (
{processedRequests.map((req) => ( ))}
Zaměstnanec Typ Od Do Dny Stav Schválil Poznámka Vyřízeno
{req.employee_name} {leaveTypeLabels[req.leave_type] || req.leave_type} {formatDate(req.date_from)} {formatDate(req.date_to)} {req.total_days} {statusLabels[req.status] || req.status} {req.reviewer_name || "—"} {req.reviewer_note ? ( {req.reviewer_note.length > 40 ? `${req.reviewer_note.substring(0, 40)}...` : req.reviewer_note} ) : ( "—" )} {formatDatetime(req.reviewed_at)}
)}
)} {/* 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" type="info" loading={processing} /> {/* Reject Modal */} {rejectModal.open && (
{ setRejectModal({ open: false, request: null }); setRejectNote(""); }} />

Zamítnout žádost

{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ů)

)}