style: run prettier on entire codebase
This commit is contained in:
@@ -1,164 +1,210 @@
|
||||
import { useState, useEffect, useCallback, useMemo, useRef } from 'react'
|
||||
import { useAlert } from '../context/AlertContext'
|
||||
import { useAuth } from '../context/AuthContext'
|
||||
import Forbidden from '../components/Forbidden'
|
||||
import { motion } from 'framer-motion'
|
||||
import AdminDatePicker from '../components/AdminDatePicker'
|
||||
import { formatDate, formatDatetime, formatTime, calculateWorkMinutes, formatMinutes, getLeaveTypeName, getLeaveTypeBadgeClass, calculateWorkMinutesPrint, formatTimeOrDatetimePrint } from '../utils/attendanceHelpers'
|
||||
import FormField from '../components/FormField'
|
||||
import apiFetch from '../utils/api'
|
||||
import { useState, useEffect, useCallback, useMemo, useRef } from "react";
|
||||
import { useAlert } from "../context/AlertContext";
|
||||
import { useAuth } from "../context/AuthContext";
|
||||
import Forbidden from "../components/Forbidden";
|
||||
import { motion } from "framer-motion";
|
||||
import AdminDatePicker from "../components/AdminDatePicker";
|
||||
import {
|
||||
formatDate,
|
||||
formatDatetime,
|
||||
formatTime,
|
||||
calculateWorkMinutes,
|
||||
formatMinutes,
|
||||
getLeaveTypeName,
|
||||
getLeaveTypeBadgeClass,
|
||||
calculateWorkMinutesPrint,
|
||||
formatTimeOrDatetimePrint,
|
||||
} from "../utils/attendanceHelpers";
|
||||
import FormField from "../components/FormField";
|
||||
import apiFetch from "../utils/api";
|
||||
|
||||
const API_BASE = '/api/admin'
|
||||
const API_BASE = "/api/admin";
|
||||
|
||||
interface ProjectLog {
|
||||
id?: number
|
||||
project_id?: number
|
||||
project_name?: string
|
||||
started_at?: string
|
||||
ended_at?: string | null
|
||||
hours?: string | number | null
|
||||
minutes?: string | number | null
|
||||
id?: number;
|
||||
project_id?: number;
|
||||
project_name?: string;
|
||||
started_at?: string;
|
||||
ended_at?: string | null;
|
||||
hours?: string | number | null;
|
||||
minutes?: string | number | null;
|
||||
}
|
||||
|
||||
interface AttendanceRecord {
|
||||
id: number
|
||||
shift_date: string
|
||||
leave_type?: string
|
||||
leave_hours?: number
|
||||
arrival_time?: string | null
|
||||
departure_time?: string | null
|
||||
break_start?: string | null
|
||||
break_end?: string | null
|
||||
notes?: string
|
||||
project_name?: string
|
||||
project_logs?: ProjectLog[]
|
||||
id: number;
|
||||
shift_date: string;
|
||||
leave_type?: string;
|
||||
leave_hours?: number;
|
||||
arrival_time?: string | null;
|
||||
departure_time?: string | null;
|
||||
break_start?: string | null;
|
||||
break_end?: string | null;
|
||||
notes?: string;
|
||||
project_name?: string;
|
||||
project_logs?: ProjectLog[];
|
||||
}
|
||||
|
||||
const MONTH_NAMES = [
|
||||
'Leden', 'Únor', 'Březen', 'Duben', 'Květen', 'Červen',
|
||||
'Červenec', 'Srpen', 'Září', 'Říjen', 'Listopad', 'Prosinec'
|
||||
]
|
||||
"Leden",
|
||||
"Únor",
|
||||
"Březen",
|
||||
"Duben",
|
||||
"Květen",
|
||||
"Červen",
|
||||
"Červenec",
|
||||
"Srpen",
|
||||
"Září",
|
||||
"Říjen",
|
||||
"Listopad",
|
||||
"Prosinec",
|
||||
];
|
||||
|
||||
const formatBreakRange = (record: AttendanceRecord): string => {
|
||||
if (record.break_start && record.break_end) {
|
||||
return `${formatTime(record.break_start)} - ${formatTime(record.break_end)}`
|
||||
return `${formatTime(record.break_start)} - ${formatTime(record.break_end)}`;
|
||||
}
|
||||
if (record.break_start) {
|
||||
return `${formatTime(record.break_start)} - ?`
|
||||
return `${formatTime(record.break_start)} - ?`;
|
||||
}
|
||||
return '—'
|
||||
}
|
||||
return "—";
|
||||
};
|
||||
|
||||
const renderProjectCell = (record: AttendanceRecord) => {
|
||||
if (record.project_logs && record.project_logs.length > 0) {
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.125rem' }}>
|
||||
<div
|
||||
style={{ display: "flex", flexDirection: "column", gap: "0.125rem" }}
|
||||
>
|
||||
{record.project_logs.map((log, i) => {
|
||||
let h: number, m: number, isActive = false
|
||||
let h: number,
|
||||
m: number,
|
||||
isActive = false;
|
||||
if (log.hours !== null && log.hours !== undefined) {
|
||||
h = parseInt(String(log.hours)) || 0
|
||||
m = parseInt(String(log.minutes)) || 0
|
||||
h = parseInt(String(log.hours)) || 0;
|
||||
m = parseInt(String(log.minutes)) || 0;
|
||||
} else {
|
||||
isActive = !log.ended_at
|
||||
const end = log.ended_at ? new Date(log.ended_at) : new Date()
|
||||
const mins = Math.floor((end.getTime() - new Date(log.started_at!).getTime()) / 60000)
|
||||
h = Math.floor(mins / 60)
|
||||
m = mins % 60
|
||||
isActive = !log.ended_at;
|
||||
const end = log.ended_at ? new Date(log.ended_at) : new Date();
|
||||
const mins = Math.floor(
|
||||
(end.getTime() - new Date(log.started_at!).getTime()) / 60000,
|
||||
);
|
||||
h = Math.floor(mins / 60);
|
||||
m = mins % 60;
|
||||
}
|
||||
return (
|
||||
<span key={log.id || i} className="admin-badge" style={{ fontSize: '0.7rem', display: 'inline-block', background: isActive ? 'var(--accent-light)' : undefined }}>
|
||||
{log.project_name || `#${log.project_id}`} ({h}:{String(m).padStart(2, '0')}h{isActive ? ' ▸' : ''})
|
||||
<span
|
||||
key={log.id || i}
|
||||
className="admin-badge"
|
||||
style={{
|
||||
fontSize: "0.7rem",
|
||||
display: "inline-block",
|
||||
background: isActive ? "var(--accent-light)" : undefined,
|
||||
}}
|
||||
>
|
||||
{log.project_name || `#${log.project_id}`} ({h}:
|
||||
{String(m).padStart(2, "0")}h{isActive ? " ▸" : ""})
|
||||
</span>
|
||||
)
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
if (record.project_name) {
|
||||
return <span className="admin-badge admin-badge-wrap" style={{ fontSize: '0.75rem' }}>{record.project_name}</span>
|
||||
return (
|
||||
<span
|
||||
className="admin-badge admin-badge-wrap"
|
||||
style={{ fontSize: "0.75rem" }}
|
||||
>
|
||||
{record.project_name}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return '—'
|
||||
}
|
||||
return "—";
|
||||
};
|
||||
|
||||
export default function AttendanceHistory() {
|
||||
const alert = useAlert()
|
||||
const { user, hasPermission } = useAuth()
|
||||
const [loading, setLoading] = useState(true)
|
||||
const printRef = useRef<HTMLDivElement>(null)
|
||||
const alert = useAlert();
|
||||
const { user, hasPermission } = useAuth();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const printRef = useRef<HTMLDivElement>(null);
|
||||
const [month, setMonth] = useState(() => {
|
||||
const now = new Date()
|
||||
return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`
|
||||
})
|
||||
const [records, setRecords] = useState<AttendanceRecord[]>([])
|
||||
const now = new Date();
|
||||
return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}`;
|
||||
});
|
||||
const [records, setRecords] = useState<AttendanceRecord[]>([]);
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
setLoading(true)
|
||||
setLoading(true);
|
||||
try {
|
||||
const [yearStr, monthStr] = month.split('-')
|
||||
const response = await apiFetch(`${API_BASE}/attendance?year=${yearStr}&month=${monthStr}&limit=1000&user_id=${user?.id || ''}`)
|
||||
if (response.status === 401) return
|
||||
const result = await response.json()
|
||||
const [yearStr, monthStr] = month.split("-");
|
||||
const response = await apiFetch(
|
||||
`${API_BASE}/attendance?year=${yearStr}&month=${monthStr}&limit=1000&user_id=${user?.id || ""}`,
|
||||
);
|
||||
if (response.status === 401) return;
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
setRecords(result.data)
|
||||
setRecords(result.data);
|
||||
}
|
||||
} catch {
|
||||
alert.error('Nepodařilo se načíst data')
|
||||
alert.error("Nepodařilo se načíst data");
|
||||
} finally {
|
||||
setLoading(false)
|
||||
setLoading(false);
|
||||
}
|
||||
}, [month, alert, user?.id])
|
||||
}, [month, alert, user?.id]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchData()
|
||||
}, [fetchData])
|
||||
fetchData();
|
||||
}, [fetchData]);
|
||||
|
||||
// Compute totals client-side from raw records
|
||||
const computed = useMemo(() => {
|
||||
const [yearStr, monthStr] = month.split('-')
|
||||
const monthIndex = parseInt(monthStr, 10) - 1
|
||||
const monthName = `${MONTH_NAMES[monthIndex]} ${yearStr}`
|
||||
const [yearStr, monthStr] = month.split("-");
|
||||
const monthIndex = parseInt(monthStr, 10) - 1;
|
||||
const monthName = `${MONTH_NAMES[monthIndex]} ${yearStr}`;
|
||||
|
||||
let totalMinutes = 0
|
||||
let vacationHours = 0
|
||||
let sickHours = 0
|
||||
let holidayHours = 0
|
||||
let unpaidHours = 0
|
||||
let totalMinutes = 0;
|
||||
let vacationHours = 0;
|
||||
let sickHours = 0;
|
||||
let holidayHours = 0;
|
||||
let unpaidHours = 0;
|
||||
|
||||
for (const record of records) {
|
||||
const leaveType = record.leave_type || 'work'
|
||||
if (leaveType === 'work') {
|
||||
totalMinutes += calculateWorkMinutes(record)
|
||||
const leaveType = record.leave_type || "work";
|
||||
if (leaveType === "work") {
|
||||
totalMinutes += calculateWorkMinutes(record);
|
||||
} else {
|
||||
const hours = Number(record.leave_hours) || 8
|
||||
if (leaveType === 'vacation') vacationHours += hours
|
||||
else if (leaveType === 'sick') sickHours += hours
|
||||
else if (leaveType === 'holiday') holidayHours += hours
|
||||
else if (leaveType === 'unpaid') unpaidHours += hours
|
||||
const hours = Number(record.leave_hours) || 8;
|
||||
if (leaveType === "vacation") vacationHours += hours;
|
||||
else if (leaveType === "sick") sickHours += hours;
|
||||
else if (leaveType === "holiday") holidayHours += hours;
|
||||
else if (leaveType === "unpaid") unpaidHours += hours;
|
||||
}
|
||||
}
|
||||
|
||||
// Compute monthly fund (working days * 8h)
|
||||
// Exclude holidays from business days (matching PHP CzechHolidays logic)
|
||||
const yr = parseInt(yearStr, 10)
|
||||
const mo = parseInt(monthStr, 10) - 1
|
||||
const yr = parseInt(yearStr, 10);
|
||||
const mo = parseInt(monthStr, 10) - 1;
|
||||
// Count holiday records to subtract from business days
|
||||
const holidayDays = records.filter(r => (r.leave_type || 'work') === 'holiday').length
|
||||
let businessDays = 0
|
||||
const cur = new Date(yr, mo, 1)
|
||||
const holidayDays = records.filter(
|
||||
(r) => (r.leave_type || "work") === "holiday",
|
||||
).length;
|
||||
let businessDays = 0;
|
||||
const cur = new Date(yr, mo, 1);
|
||||
while (cur.getMonth() === mo) {
|
||||
const dow = cur.getDay()
|
||||
if (dow !== 0 && dow !== 6) businessDays++
|
||||
cur.setDate(cur.getDate() + 1)
|
||||
const dow = cur.getDay();
|
||||
if (dow !== 0 && dow !== 6) businessDays++;
|
||||
cur.setDate(cur.getDate() + 1);
|
||||
}
|
||||
// Subtract holidays from business days (holidays are non-working days, not part of the fund)
|
||||
businessDays = Math.max(0, businessDays - holidayDays)
|
||||
const fund = businessDays * 8
|
||||
const worked = Math.round((totalMinutes / 60) * 100) / 100
|
||||
businessDays = Math.max(0, businessDays - holidayDays);
|
||||
const fund = businessDays * 8;
|
||||
const worked = Math.round((totalMinutes / 60) * 100) / 100;
|
||||
// Covered = worked + vacation + sick (NOT holiday/unpaid — holiday is excluded from fund, unpaid is voluntary)
|
||||
const leaveHours = vacationHours + sickHours
|
||||
const covered = Math.round((worked + leaveHours) * 100) / 100
|
||||
const remaining = Math.max(0, Math.round((fund - covered) * 100) / 100)
|
||||
const overtime = Math.max(0, Math.round((covered - fund) * 100) / 100)
|
||||
const leaveHours = vacationHours + sickHours;
|
||||
const covered = Math.round((worked + leaveHours) * 100) / 100;
|
||||
const remaining = Math.max(0, Math.round((fund - covered) * 100) / 100);
|
||||
const overtime = Math.max(0, Math.round((covered - fund) * 100) / 100);
|
||||
|
||||
const monthlyFund = {
|
||||
fund,
|
||||
@@ -167,18 +213,26 @@ export default function AttendanceHistory() {
|
||||
covered,
|
||||
remaining,
|
||||
overtime,
|
||||
}
|
||||
};
|
||||
|
||||
return { monthName, totalMinutes, vacationHours, sickHours, holidayHours, unpaidHours, monthlyFund }
|
||||
}, [records, month])
|
||||
return {
|
||||
monthName,
|
||||
totalMinutes,
|
||||
vacationHours,
|
||||
sickHours,
|
||||
holidayHours,
|
||||
unpaidHours,
|
||||
monthlyFund,
|
||||
};
|
||||
}, [records, month]);
|
||||
|
||||
if (!hasPermission('attendance.history')) return <Forbidden />
|
||||
if (!hasPermission("attendance.history")) return <Forbidden />;
|
||||
|
||||
const handlePrint = () => {
|
||||
if (!printRef.current) return
|
||||
const content = printRef.current.innerHTML
|
||||
const printWindow = window.open('', '_blank')
|
||||
if (!printWindow) return
|
||||
if (!printRef.current) return;
|
||||
const content = printRef.current.innerHTML;
|
||||
const printWindow = window.open("", "_blank");
|
||||
if (!printWindow) return;
|
||||
printWindow.document.write(`
|
||||
<!DOCTYPE html>
|
||||
<html lang="cs">
|
||||
@@ -266,12 +320,12 @@ export default function AttendanceHistory() {
|
||||
${content}
|
||||
</body>
|
||||
</html>
|
||||
`)
|
||||
printWindow.document.close()
|
||||
`);
|
||||
printWindow.document.close();
|
||||
printWindow.onload = () => {
|
||||
printWindow.print()
|
||||
}
|
||||
}
|
||||
printWindow.print();
|
||||
};
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -292,7 +346,15 @@ export default function AttendanceHistory() {
|
||||
className="admin-btn admin-btn-secondary"
|
||||
title="Tisk docházky"
|
||||
>
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" style={{ marginRight: '0.5rem' }}>
|
||||
<svg
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
style={{ marginRight: "0.5rem" }}
|
||||
>
|
||||
<polyline points="6 9 6 2 18 2 18 9" />
|
||||
<path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2" />
|
||||
<rect x="6" y="14" width="12" height="8" />
|
||||
@@ -332,33 +394,81 @@ export default function AttendanceHistory() {
|
||||
>
|
||||
<div className="admin-card-body">
|
||||
{loading && (
|
||||
<div className="admin-skeleton" style={{ gap: '0.5rem' }}>
|
||||
<div className="admin-skeleton-row" style={{ gap: '1rem' }}>
|
||||
<div className="admin-skeleton-line" style={{ width: '48px', height: '48px', borderRadius: '12px', flexShrink: 0 }} />
|
||||
<div className="admin-skeleton" style={{ gap: "0.5rem" }}>
|
||||
<div className="admin-skeleton-row" style={{ gap: "1rem" }}>
|
||||
<div
|
||||
className="admin-skeleton-line"
|
||||
style={{
|
||||
width: "48px",
|
||||
height: "48px",
|
||||
borderRadius: "12px",
|
||||
flexShrink: 0,
|
||||
}}
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<div className="admin-skeleton-line w-1/2" style={{ marginBottom: '0.5rem' }} />
|
||||
<div className="admin-skeleton-line w-full" style={{ height: '6px', borderRadius: '3px' }} />
|
||||
<div className="admin-skeleton-line w-1/3" style={{ height: '10px', marginTop: '0.5rem' }} />
|
||||
<div
|
||||
className="admin-skeleton-line w-1/2"
|
||||
style={{ marginBottom: "0.5rem" }}
|
||||
/>
|
||||
<div
|
||||
className="admin-skeleton-line w-full"
|
||||
style={{ height: "6px", borderRadius: "3px" }}
|
||||
/>
|
||||
<div
|
||||
className="admin-skeleton-line w-1/3"
|
||||
style={{ height: "10px", marginTop: "0.5rem" }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{!loading && computed.monthlyFund && (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem', flexWrap: 'wrap' }}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "1rem",
|
||||
flexWrap: "wrap",
|
||||
}}
|
||||
>
|
||||
<div className="admin-stat-icon info">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
>
|
||||
<rect x="3" y="4" width="18" height="18" rx="2" ry="2" />
|
||||
<line x1="16" y1="2" x2="16" y2="6" />
|
||||
<line x1="8" y1="2" x2="8" y2="6" />
|
||||
<line x1="3" y1="10" x2="21" y2="10" />
|
||||
</svg>
|
||||
</div>
|
||||
<div style={{ flex: 1, minWidth: '200px' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', marginBottom: '0.375rem' }}>
|
||||
<span style={{ fontWeight: 600, fontSize: '1rem', color: 'var(--text-primary)' }}>
|
||||
Fond: {computed.monthlyFund.worked}h / {computed.monthlyFund.fund}h
|
||||
<div style={{ flex: 1, minWidth: "200px" }}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "baseline",
|
||||
marginBottom: "0.375rem",
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
fontWeight: 600,
|
||||
fontSize: "1rem",
|
||||
color: "var(--text-primary)",
|
||||
}}
|
||||
>
|
||||
Fond: {computed.monthlyFund.worked}h /{" "}
|
||||
{computed.monthlyFund.fund}h
|
||||
</span>
|
||||
<span className="text-secondary" style={{ fontSize: '0.8125rem' }}>
|
||||
<span
|
||||
className="text-secondary"
|
||||
style={{ fontSize: "0.8125rem" }}
|
||||
>
|
||||
{computed.monthlyFund.business_days} prac. dnů
|
||||
</span>
|
||||
</div>
|
||||
@@ -367,23 +477,41 @@ export default function AttendanceHistory() {
|
||||
className="attendance-balance-progress"
|
||||
style={{
|
||||
width: `${Math.min(100, computed.monthlyFund.fund > 0 ? (computed.monthlyFund.covered / computed.monthlyFund.fund) * 100 : 0)}%`,
|
||||
background: computed.monthlyFund.covered >= computed.monthlyFund.fund
|
||||
? 'linear-gradient(135deg, var(--success), #059669)'
|
||||
: 'var(--gradient)'
|
||||
background:
|
||||
computed.monthlyFund.covered >=
|
||||
computed.monthlyFund.fund
|
||||
? "linear-gradient(135deg, var(--success), #059669)"
|
||||
: "var(--gradient)",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="text-muted" style={{ display: 'flex', justifyContent: 'space-between', fontSize: '0.75rem', marginTop: '0.375rem' }}>
|
||||
<div
|
||||
className="text-muted"
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
fontSize: "0.75rem",
|
||||
marginTop: "0.375rem",
|
||||
}}
|
||||
>
|
||||
<span>
|
||||
{'Pokryto: '}{computed.monthlyFund.covered}h (práce {computed.monthlyFund.worked}h
|
||||
{computed.vacationHours > 0 && ` + dovolená ${computed.vacationHours}h`}
|
||||
{computed.sickHours > 0 && ` + nemoc ${computed.sickHours}h`}
|
||||
{computed.holidayHours > 0 && ` + svátek ${computed.holidayHours}h`}
|
||||
{computed.unpaidHours > 0 && ` + neplacené ${computed.unpaidHours}h`}
|
||||
{"Pokryto: "}
|
||||
{computed.monthlyFund.covered}h (práce{" "}
|
||||
{computed.monthlyFund.worked}h
|
||||
{computed.vacationHours > 0 &&
|
||||
` + dovolená ${computed.vacationHours}h`}
|
||||
{computed.sickHours > 0 &&
|
||||
` + nemoc ${computed.sickHours}h`}
|
||||
{computed.holidayHours > 0 &&
|
||||
` + svátek ${computed.holidayHours}h`}
|
||||
{computed.unpaidHours > 0 &&
|
||||
` + neplacené ${computed.unpaidHours}h`}
|
||||
)
|
||||
</span>
|
||||
{computed.monthlyFund.overtime > 0 ? (
|
||||
<span className="text-warning fw-600">Přesčas: +{computed.monthlyFund.overtime}h</span>
|
||||
<span className="text-warning fw-600">
|
||||
Přesčas: +{computed.monthlyFund.overtime}h
|
||||
</span>
|
||||
) : (
|
||||
<span>Zbývá: {computed.monthlyFund.remaining}h</span>
|
||||
)}
|
||||
@@ -392,7 +520,14 @@ export default function AttendanceHistory() {
|
||||
</div>
|
||||
)}
|
||||
{!loading && !computed.monthlyFund && (
|
||||
<div className="text-muted" style={{ fontSize: '0.875rem', textAlign: 'center', padding: '0.5rem 0' }}>
|
||||
<div
|
||||
className="text-muted"
|
||||
style={{
|
||||
fontSize: "0.875rem",
|
||||
textAlign: "center",
|
||||
padding: "0.5rem 0",
|
||||
}}
|
||||
>
|
||||
Fond měsíce není k dispozici
|
||||
</div>
|
||||
)}
|
||||
@@ -408,8 +543,8 @@ export default function AttendanceHistory() {
|
||||
>
|
||||
<div className="admin-card-body">
|
||||
{loading && (
|
||||
<div className="admin-skeleton" style={{ gap: '1.25rem' }}>
|
||||
{[0, 1, 2, 3, 4].map(i => (
|
||||
<div className="admin-skeleton" style={{ gap: "1.25rem" }}>
|
||||
{[0, 1, 2, 3, 4].map((i) => (
|
||||
<div key={i} className="admin-skeleton-row">
|
||||
<div className="admin-skeleton-line w-1/4" />
|
||||
<div className="admin-skeleton-line w-1/3" />
|
||||
@@ -440,34 +575,53 @@ export default function AttendanceHistory() {
|
||||
</thead>
|
||||
<tbody>
|
||||
{records.map((record) => {
|
||||
const leaveType = record.leave_type || 'work'
|
||||
const isLeave = leaveType !== 'work'
|
||||
const leaveType = record.leave_type || "work";
|
||||
const isLeave = leaveType !== "work";
|
||||
const workMinutes = isLeave
|
||||
? (Number(record.leave_hours) || 8) * 60
|
||||
: calculateWorkMinutes(record)
|
||||
: calculateWorkMinutes(record);
|
||||
|
||||
return (
|
||||
<tr key={record.id}>
|
||||
<td className="admin-mono">{formatDate(record.shift_date)}</td>
|
||||
<td className="admin-mono">
|
||||
{formatDate(record.shift_date)}
|
||||
</td>
|
||||
<td>
|
||||
<span className={`attendance-leave-badge ${getLeaveTypeBadgeClass(leaveType)}`}>
|
||||
<span
|
||||
className={`attendance-leave-badge ${getLeaveTypeBadgeClass(leaveType)}`}
|
||||
>
|
||||
{getLeaveTypeName(leaveType)}
|
||||
</span>
|
||||
</td>
|
||||
<td className="admin-mono">{isLeave ? '—' : formatDatetime(record.arrival_time)}</td>
|
||||
<td className="admin-mono">
|
||||
{isLeave ? '—' : formatBreakRange(record)}
|
||||
{isLeave ? "—" : formatDatetime(record.arrival_time)}
|
||||
</td>
|
||||
<td className="admin-mono">{isLeave ? '—' : formatDatetime(record.departure_time)}</td>
|
||||
<td className="admin-mono">{workMinutes > 0 ? formatMinutes(workMinutes, true) : '—'}</td>
|
||||
<td>
|
||||
{renderProjectCell(record)}
|
||||
<td className="admin-mono">
|
||||
{isLeave ? "—" : formatBreakRange(record)}
|
||||
</td>
|
||||
<td style={{ maxWidth: '150px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
||||
{record.notes || ''}
|
||||
<td className="admin-mono">
|
||||
{isLeave
|
||||
? "—"
|
||||
: formatDatetime(record.departure_time)}
|
||||
</td>
|
||||
<td className="admin-mono">
|
||||
{workMinutes > 0
|
||||
? formatMinutes(workMinutes, true)
|
||||
: "—"}
|
||||
</td>
|
||||
<td>{renderProjectCell(record)}</td>
|
||||
<td
|
||||
style={{
|
||||
maxWidth: "150px",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
}}
|
||||
>
|
||||
{record.notes || ""}
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -478,109 +632,216 @@ export default function AttendanceHistory() {
|
||||
|
||||
{/* Hidden Print Content */}
|
||||
{records.length > 0 && (
|
||||
<div ref={printRef} style={{ display: 'none' }}>
|
||||
<div ref={printRef} style={{ display: "none" }}>
|
||||
<table className="print-wrapper-table">
|
||||
<thead>
|
||||
<tr><td>
|
||||
<div className="print-header">
|
||||
<div className="print-header-left">
|
||||
<img src="/images/logo-light.png" alt="BOHA" className="print-logo" />
|
||||
<div className="print-header-text">
|
||||
<h1>EVIDENCE DOCHÁZKY</h1>
|
||||
<div className="company">BOHA Automation s.r.o.</div>
|
||||
<tr>
|
||||
<td>
|
||||
<div className="print-header">
|
||||
<div className="print-header-left">
|
||||
<img
|
||||
src="/images/logo-light.png"
|
||||
alt="BOHA"
|
||||
className="print-logo"
|
||||
/>
|
||||
<div className="print-header-text">
|
||||
<h1>EVIDENCE DOCHÁZKY</h1>
|
||||
<div className="company">BOHA Automation s.r.o.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="print-header-right">
|
||||
<div className="period">{computed.monthName}</div>
|
||||
<div className="filters">
|
||||
Zaměstnanec: {user?.fullName || ""}
|
||||
</div>
|
||||
<div className="generated">
|
||||
Vygenerováno: {new Date().toLocaleString("cs-CZ")}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="print-header-right">
|
||||
<div className="period">{computed.monthName}</div>
|
||||
<div className="filters">Zaměstnanec: {user?.fullName || ''}</div>
|
||||
<div className="generated">Vygenerováno: {new Date().toLocaleString('cs-CZ')}</div>
|
||||
</div>
|
||||
</div>
|
||||
</td></tr>
|
||||
</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td>
|
||||
<div className="user-section">
|
||||
<div className="user-header">
|
||||
<h3>{user?.fullName || ''}</h3>
|
||||
<span className="total">Odpracováno: {formatMinutes(computed.totalMinutes, true)}</span>
|
||||
</div>
|
||||
|
||||
{(computed.vacationHours > 0 || computed.sickHours > 0 || computed.holidayHours > 0) && (
|
||||
<div className="leave-summary">
|
||||
{computed.vacationHours > 0 && <><span className="leave-badge badge-vacation">Dovolená: {computed.vacationHours}h</span> </>}
|
||||
{computed.sickHours > 0 && <><span className="leave-badge badge-sick">Nemoc: {computed.sickHours}h</span> </>}
|
||||
{computed.holidayHours > 0 && <><span className="leave-badge badge-holiday">Svátek: {computed.holidayHours}h</span> </>}
|
||||
<tr>
|
||||
<td>
|
||||
<div className="user-section">
|
||||
<div className="user-header">
|
||||
<h3>{user?.fullName || ""}</h3>
|
||||
<span className="total">
|
||||
Odpracováno:{" "}
|
||||
{formatMinutes(computed.totalMinutes, true)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={{ width: '70px' }}>Datum</th>
|
||||
<th style={{ width: '70px' }}>Typ</th>
|
||||
<th className="text-center" style={{ width: '70px' }}>Příchod</th>
|
||||
<th className="text-center" style={{ width: '90px' }}>Pauza</th>
|
||||
<th className="text-center" style={{ width: '70px' }}>Odchod</th>
|
||||
<th className="text-center" style={{ width: '80px' }}>Hodiny</th>
|
||||
<th>Projekty</th>
|
||||
<th>Poznámka</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{[...records].sort((a, b) => a.shift_date.localeCompare(b.shift_date)).map((record) => {
|
||||
const leaveType = record.leave_type || 'work'
|
||||
const isLeave = leaveType !== 'work'
|
||||
const workMinutes = calculateWorkMinutesPrint(record)
|
||||
const hours = Math.floor(workMinutes / 60)
|
||||
const mins = workMinutes % 60
|
||||
{(computed.vacationHours > 0 ||
|
||||
computed.sickHours > 0 ||
|
||||
computed.holidayHours > 0) && (
|
||||
<div className="leave-summary">
|
||||
{computed.vacationHours > 0 && (
|
||||
<>
|
||||
<span className="leave-badge badge-vacation">
|
||||
Dovolená: {computed.vacationHours}h
|
||||
</span>{" "}
|
||||
</>
|
||||
)}
|
||||
{computed.sickHours > 0 && (
|
||||
<>
|
||||
<span className="leave-badge badge-sick">
|
||||
Nemoc: {computed.sickHours}h
|
||||
</span>{" "}
|
||||
</>
|
||||
)}
|
||||
{computed.holidayHours > 0 && (
|
||||
<>
|
||||
<span className="leave-badge badge-holiday">
|
||||
Svátek: {computed.holidayHours}h
|
||||
</span>{" "}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
return (
|
||||
<tr key={record.id}>
|
||||
<td>{formatDate(record.shift_date)}</td>
|
||||
<td><span className={`leave-badge ${getLeaveTypeBadgeClass(leaveType)}`}>{getLeaveTypeName(leaveType)}</span></td>
|
||||
<td className="text-center">{isLeave ? '—' : formatTimeOrDatetimePrint(record.arrival_time, record.shift_date)}</td>
|
||||
<td className="text-center">
|
||||
{isLeave || !record.break_start || !record.break_end
|
||||
? '—'
|
||||
: `${formatTimeOrDatetimePrint(record.break_start, record.shift_date)} - ${formatTimeOrDatetimePrint(record.break_end, record.shift_date)}`
|
||||
}
|
||||
</td>
|
||||
<td className="text-center">{isLeave ? '—' : formatTimeOrDatetimePrint(record.departure_time, record.shift_date)}</td>
|
||||
<td className="text-center">{workMinutes > 0 ? `${hours}:${String(mins).padStart(2, '0')}` : '—'}</td>
|
||||
<td style={{ fontSize: '8px' }}>
|
||||
{(record.project_logs && record.project_logs.length > 0)
|
||||
? record.project_logs.map((log, i) => {
|
||||
let h: number, m: number
|
||||
if (log.hours !== null && log.hours !== undefined) {
|
||||
h = parseInt(String(log.hours)) || 0; m = parseInt(String(log.minutes)) || 0
|
||||
} else if (log.started_at && log.ended_at) {
|
||||
const mins2 = Math.max(0, Math.floor((new Date(log.ended_at).getTime() - new Date(log.started_at).getTime()) / 60000))
|
||||
h = Math.floor(mins2 / 60); m = mins2 % 60
|
||||
} else { h = 0; m = 0 }
|
||||
return <div key={log.id || i}>{log.project_name || `#${log.project_id}`} ({h}:{String(m).padStart(2, '0')}h)</div>
|
||||
})
|
||||
: record.project_name || '—'}
|
||||
</td>
|
||||
<td>{record.notes || ''}</td>
|
||||
</tr>
|
||||
)
|
||||
})}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td colSpan={6} className="text-right">Odpracováno:</td>
|
||||
<td className="text-center">{formatMinutes(computed.totalMinutes, true)}</td>
|
||||
<td colSpan={2}></td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</td></tr>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={{ width: "70px" }}>Datum</th>
|
||||
<th style={{ width: "70px" }}>Typ</th>
|
||||
<th className="text-center" style={{ width: "70px" }}>
|
||||
Příchod
|
||||
</th>
|
||||
<th className="text-center" style={{ width: "90px" }}>
|
||||
Pauza
|
||||
</th>
|
||||
<th className="text-center" style={{ width: "70px" }}>
|
||||
Odchod
|
||||
</th>
|
||||
<th className="text-center" style={{ width: "80px" }}>
|
||||
Hodiny
|
||||
</th>
|
||||
<th>Projekty</th>
|
||||
<th>Poznámka</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{[...records]
|
||||
.sort((a, b) =>
|
||||
a.shift_date.localeCompare(b.shift_date),
|
||||
)
|
||||
.map((record) => {
|
||||
const leaveType = record.leave_type || "work";
|
||||
const isLeave = leaveType !== "work";
|
||||
const workMinutes =
|
||||
calculateWorkMinutesPrint(record);
|
||||
const hours = Math.floor(workMinutes / 60);
|
||||
const mins = workMinutes % 60;
|
||||
|
||||
return (
|
||||
<tr key={record.id}>
|
||||
<td>{formatDate(record.shift_date)}</td>
|
||||
<td>
|
||||
<span
|
||||
className={`leave-badge ${getLeaveTypeBadgeClass(leaveType)}`}
|
||||
>
|
||||
{getLeaveTypeName(leaveType)}
|
||||
</span>
|
||||
</td>
|
||||
<td className="text-center">
|
||||
{isLeave
|
||||
? "—"
|
||||
: formatTimeOrDatetimePrint(
|
||||
record.arrival_time,
|
||||
record.shift_date,
|
||||
)}
|
||||
</td>
|
||||
<td className="text-center">
|
||||
{isLeave ||
|
||||
!record.break_start ||
|
||||
!record.break_end
|
||||
? "—"
|
||||
: `${formatTimeOrDatetimePrint(record.break_start, record.shift_date)} - ${formatTimeOrDatetimePrint(record.break_end, record.shift_date)}`}
|
||||
</td>
|
||||
<td className="text-center">
|
||||
{isLeave
|
||||
? "—"
|
||||
: formatTimeOrDatetimePrint(
|
||||
record.departure_time,
|
||||
record.shift_date,
|
||||
)}
|
||||
</td>
|
||||
<td className="text-center">
|
||||
{workMinutes > 0
|
||||
? `${hours}:${String(mins).padStart(2, "0")}`
|
||||
: "—"}
|
||||
</td>
|
||||
<td style={{ fontSize: "8px" }}>
|
||||
{record.project_logs &&
|
||||
record.project_logs.length > 0
|
||||
? record.project_logs.map((log, i) => {
|
||||
let h: number, m: number;
|
||||
if (
|
||||
log.hours !== null &&
|
||||
log.hours !== undefined
|
||||
) {
|
||||
h = parseInt(String(log.hours)) || 0;
|
||||
m =
|
||||
parseInt(String(log.minutes)) || 0;
|
||||
} else if (
|
||||
log.started_at &&
|
||||
log.ended_at
|
||||
) {
|
||||
const mins2 = Math.max(
|
||||
0,
|
||||
Math.floor(
|
||||
(new Date(
|
||||
log.ended_at,
|
||||
).getTime() -
|
||||
new Date(
|
||||
log.started_at,
|
||||
).getTime()) /
|
||||
60000,
|
||||
),
|
||||
);
|
||||
h = Math.floor(mins2 / 60);
|
||||
m = mins2 % 60;
|
||||
} else {
|
||||
h = 0;
|
||||
m = 0;
|
||||
}
|
||||
return (
|
||||
<div key={log.id || i}>
|
||||
{log.project_name ||
|
||||
`#${log.project_id}`}{" "}
|
||||
({h}:{String(m).padStart(2, "0")}h)
|
||||
</div>
|
||||
);
|
||||
})
|
||||
: record.project_name || "—"}
|
||||
</td>
|
||||
<td>{record.notes || ""}</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td colSpan={6} className="text-right">
|
||||
Odpracováno:
|
||||
</td>
|
||||
<td className="text-center">
|
||||
{formatMinutes(computed.totalMinutes, true)}
|
||||
</td>
|
||||
<td colSpan={2}></td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user