import { useState, useEffect, useCallback } from "react"; 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 ConfirmModal from "../components/ConfirmModal"; import Forbidden from "../components/Forbidden"; import useModalLock from "../hooks/useModalLock"; import FormField from "../components/FormField"; 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 [loading, setLoading] = useState(true); const [activeTab, setActiveTab] = useState<"pending" | "processed">( "pending", ); const [pendingRequests, setPendingRequests] = useState([]); const [pendingCount, setPendingCount] = useState(0); const [processedRequests, setProcessedRequests] = useState( [], ); 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); const fetchPending = useCallback(async () => { try { const response = await apiFetch( `${API_BASE}/leave-requests?status=pending`, ); if (response.status === 401) return; const result = await response.json(); if (result.success) { const mapped = (result.data as RawLeaveRequest[]).map(mapLeaveRequest); setPendingRequests(mapped); setPendingCount(result.pagination?.total ?? mapped.length); } } catch { alert.error("Nepodařilo se načíst žádosti"); } }, [alert]); const fetchProcessed = useCallback(async () => { try { const response = await apiFetch( `${API_BASE}/leave-requests?status=approved`, ); if (response.status === 401) return; const resultApproved = await response.json(); const response2 = await apiFetch( `${API_BASE}/leave-requests?status=rejected`, ); if (response2.status === 401) return; const resultRejected = await response2.json(); const all = [ ...(resultApproved.success ? (resultApproved.data as RawLeaveRequest[]).map(mapLeaveRequest) : []), ...(resultRejected.success ? (resultRejected.data as RawLeaveRequest[]).map(mapLeaveRequest) : []), ].sort( (a: LeaveRequest, b: LeaveRequest) => new Date(b.reviewed_at!).getTime() - new Date(a.reviewed_at!).getTime(), ); setProcessedRequests(all); } catch { alert.error("Nepodařilo se načíst vyřízené žádosti"); } }, [alert]); useEffect(() => { setLoading(true); fetchPending().finally(() => setLoading(false)); }, [fetchPending]); useEffect(() => { if (activeTab === "processed" && processedRequests.length === 0) { fetchProcessed(); } }, [activeTab, processedRequests.length, fetchProcessed]); 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 fetchPending(); setProcessedRequests([]); 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 fetchPending(); setProcessedRequests([]); alert.success("Žádost byla zamítnuta"); } else { alert.error(result.error); } } catch { alert.error("Chyba připojení"); } finally { setProcessing(false); } }; if (loading) { return (
{[0, 1, 2, 3, 4].map((i) => (
))}
); } 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ů)

)}