Files
app/src/admin/pages/AttendanceHistory.tsx
BOHA 54f3c414f5 fix(ui): unify icon badges — solid tile + white glyph via shared helper
The labelled icon-badge tiles were inconsistent: some used a same-hue
light tile + colored glyph (e.g. the Settings 2FA row: success.light tile
+ success.main glyph — low contrast, the reported "badly visible" icon),
and the solid-tile ones used the bright .main fill in dark mode without
darkening, so a white glyph on bright green/amber was low-contrast too
(DashProfile's success badge was ~1.9:1).

Add a single `iconBadgeSx(color)` helper (theme.ts): solid `<color>.main`
fill + white glyph, darkened in dark mode (FILLED_DARK_BG) so white stays
legible; `default` = neutral grey. Applied to every badge so they can't
drift: StatCard, DashActivityFeed, DashProfile (2FA badge), Dashboard
(2FA banner icon), AttendanceHistory (month tile), and the Settings 2FA
row tile (now solid green when required / grey when optional + white lock,
instead of the low-contrast light-green tile). Also switched DashProfile's
backup-codes warning callout from warning.light to a subtle warning wash
so its amber text stays readable in dark mode.

tsc -b --noEmit, npm run build, vitest 152/152 all clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 21:34:06 +02:00

884 lines
30 KiB
TypeScript

import { useState, useMemo, useRef } from "react";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";
import { useAuth } from "../context/AuthContext";
import Forbidden from "../components/Forbidden";
import { companySettingsOptions } from "../lib/queries/settings";
import { iconBadgeSx } from "../theme";
import {
attendanceHistoryOptions,
type AttendanceRecord,
type ProjectLogEntry,
} from "../lib/queries/attendance";
import {
formatDate,
formatDatetime,
formatTime,
calculateWorkMinutes,
formatMinutes,
getLeaveTypeName,
getLeaveTypeBadgeClass,
calculateWorkMinutesPrint,
formatTimeOrDatetimePrint,
calculateFreeHolidayHours,
holidaysInMonth,
normalizeDateStr,
formatHoursDecimal,
} from "../utils/attendanceHelpers";
import {
Button,
Card,
DataTable,
FilterBar,
MonthField,
ProgressBar,
StatusChip,
PageHeader,
PageEnter,
EmptyState,
LoadingState,
type DataColumn,
} from "../ui";
const MONTH_NAMES = [
"Leden",
"Únor",
"Březen",
"Duben",
"Květen",
"Červen",
"Červenec",
"Srpen",
"Září",
"Říjen",
"Listopad",
"Prosinec",
];
const PrintIcon = (
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<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" />
</svg>
);
const formatBreakRange = (record: AttendanceRecord): string => {
if (record.break_start && record.break_end) {
return `${formatTime(record.break_start)} - ${formatTime(record.break_end)}`;
}
if (record.break_start) {
return `${formatTime(record.break_start)} - ?`;
}
return "—";
};
function getEasterSunday(year: number): string {
const a = year % 19;
const b = Math.floor(year / 100);
const c = year % 100;
const d = Math.floor(b / 4);
const e = b % 4;
const f = Math.floor((b + 8) / 25);
const g = Math.floor((b - f + 1) / 3);
const h = (19 * a + b - d - g + 15) % 30;
const i = Math.floor(c / 4);
const k = c % 4;
const l = (32 + 2 * e + 2 * i - h - k) % 7;
const m = Math.floor((a + 11 * h + 22 * l) / 451);
const month = Math.floor((h + l - 7 * m + 114) / 31);
const day = ((h + l - 7 * m + 114) % 31) + 1;
return `${year}-${String(month).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
}
function getCzechHolidays(year: number): string[] {
const y = String(year);
const holidays = [
`${y}-01-01`,
`${y}-05-01`,
`${y}-05-08`,
`${y}-07-05`,
`${y}-07-06`,
`${y}-09-28`,
`${y}-10-28`,
`${y}-11-17`,
`${y}-12-24`,
`${y}-12-25`,
`${y}-12-26`,
];
const easterSunday = getEasterSunday(year);
const easterDate = new Date(easterSunday);
const goodFriday = new Date(easterDate);
goodFriday.setDate(goodFriday.getDate() - 2);
const easterMonday = new Date(easterDate);
easterMonday.setDate(easterMonday.getDate() + 1);
const pad = (n: number) => String(n).padStart(2, "0");
holidays.push(
`${goodFriday.getFullYear()}-${pad(goodFriday.getMonth() + 1)}-${pad(goodFriday.getDate())}`,
);
holidays.push(
`${easterMonday.getFullYear()}-${pad(easterMonday.getMonth() + 1)}-${pad(easterMonday.getDate())}`,
);
holidays.sort();
return holidays;
}
function getBusinessDaysInMonth(year: number, month: number): number {
const holidays = getCzechHolidays(year);
let count = 0;
const daysInMonth = new Date(year, month + 1, 0).getDate();
for (let day = 1; day <= daysInMonth; day++) {
const date = new Date(year, month, day);
const dow = date.getDay();
if (dow !== 0 && dow !== 6) {
const dateStr = `${year}-${String(month + 1).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
if (!holidays.includes(dateStr)) {
count++;
}
}
}
return count;
}
const renderProjectCell = (record: AttendanceRecord) => {
if (record.project_logs && record.project_logs.length > 0) {
return (
<Box sx={{ display: "flex", flexDirection: "column", gap: 0.25 }}>
{record.project_logs.map((log, i) => {
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;
} else {
isActive = !log.ended_at;
const end = log.ended_at ? new Date(log.ended_at) : new Date();
const mins = Math.max(
0,
Math.floor(
(end.getTime() - new Date(log.started_at!).getTime()) / 60000,
),
);
h = Math.floor(mins / 60);
m = mins % 60;
}
return (
<StatusChip
key={log.id || i}
color={isActive ? "info" : "default"}
label={`${log.project_name || `#${log.project_id}`} (${h}:${String(m).padStart(2, "0")}h${isActive ? " ▸" : ""})`}
sx={{ fontSize: "0.7rem" }}
/>
);
})}
</Box>
);
}
if (record.project_name) {
return (
<StatusChip
color="default"
label={record.project_name}
sx={{
fontSize: "0.75rem",
height: "auto",
"& .MuiChip-label": { whiteSpace: "normal", py: 0.25 },
}}
/>
);
}
return "—";
};
export default function AttendanceHistory() {
const { user, hasPermission } = useAuth();
const queryClient = useQueryClient();
const { data: companySettings } = useQuery(companySettingsOptions());
const companyName = companySettings?.company_name || "";
const printRef = useRef<HTMLDivElement>(null);
const [month, setMonth] = useState(() => {
const now = new Date();
return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}`;
});
const { data, isPending } = useQuery(
attendanceHistoryOptions({ month, userId: user?.id }),
);
const records = data ?? [];
const computed = useMemo(() => {
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 unpaidHours = 0;
for (const record of records) {
const leaveType = record.leave_type || "work";
if (leaveType === "work") {
totalMinutes += calculateWorkMinutes(record);
} else {
const hours =
record.leave_hours != null ? Number(record.leave_hours) : 8;
if (leaveType === "vacation") vacationHours += hours;
else if (leaveType === "sick") sickHours += hours;
else if (leaveType === "unpaid") unpaidHours += hours;
// "holiday" is no longer a user-selectable leave_type — it is
// auto-computed from Czech public holidays by the print/KPI code.
// Old records may still exist; skip so they don't double-count.
}
}
const yr = parseInt(yearStr, 10);
const mo = parseInt(monthStr, 10) - 1;
const businessDays = getBusinessDaysInMonth(yr, mo);
// Fund counts Mon-Fri + holidays (holidays are paid via the fond).
const holidayDates = holidaysInMonth(yr, mo + 1);
const holidayCount = holidayDates.length;
const fund = (businessDays + holidayCount) * 8;
const worked = Math.round((totalMinutes / 60) * 100) / 100;
// Covered = worked + vacation + sick (NOT unpaid — unpaid is voluntary).
const leaveHours = vacationHours + sickHours;
const covered = Math.round((worked + leaveHours) * 100) / 100;
// fondUsed = real_worked + vacation + worked_holiday + free_holiday.
// (the all-in number: everything the user got paid for this month).
const realWorked = totalMinutes / 60;
const freeHolidayHours = calculateFreeHolidayHours(
records as unknown as Parameters<typeof calculateFreeHolidayHours>[0],
holidayDates,
);
const workedHolidayHours = records
.filter(
(r) =>
(r.leave_type || "work") === "work" &&
holidayDates.includes(normalizeDateStr(r.shift_date)),
)
.reduce((sum, r) => sum + calculateWorkMinutesPrint(r) / 60, 0);
const fondUsed =
realWorked + vacationHours + workedHolidayHours + freeHolidayHours;
const delta = fondUsed - fund;
const missing = Math.max(0, Math.round(-delta * 100) / 100);
const overtime = Math.max(0, Math.round(delta * 100) / 100);
const remaining = missing;
const monthlyFund = {
fund,
business_days: businessDays,
holiday_count: holidayCount,
worked: Math.round(fondUsed * 100) / 100,
covered,
remaining,
overtime,
};
return {
monthName,
totalMinutes,
vacationHours,
sickHours,
unpaidHours,
monthlyFund,
fondUsed,
freeHolidayHours,
workedHolidayHours,
delta,
};
}, [records, month]);
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;
printWindow.document.write(`
<!DOCTYPE html>
<html lang="cs">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Docházka - ${computed.monthName}</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
font-size: 11px;
line-height: 1.4;
color: #000;
background: #fff;
padding: 15mm;
}
.print-wrapper-table { width: 100%; border-collapse: collapse; border: none; }
.print-wrapper-table > thead > tr > td,
.print-wrapper-table > tbody > tr > td { padding: 0; border: none; background: none; }
.print-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 20px;
padding-bottom: 15px;
border-bottom: 2px solid #333;
}
.print-header-left { display: flex; align-items: center; gap: 12px; }
.print-logo { height: 40px; width: auto; }
.print-header-text { text-align: left; }
.print-header-right { text-align: right; }
.print-header h1 { font-size: 18px; font-weight: 700; margin-bottom: 3px; }
.print-header .company { font-size: 11px; color: #666; }
.print-header .period { font-size: 13px; font-weight: 600; color: #333; margin-bottom: 2px; }
.print-header .filters { font-size: 10px; color: #666; }
.print-header .generated { font-size: 9px; color: #888; margin-top: 5px; }
.user-section { margin-bottom: 25px; page-break-inside: avoid; }
.user-header {
background: #f5f5f5;
border: 1px solid #ddd;
padding: 10px 15px;
margin-bottom: 10px;
display: flex;
justify-content: space-between;
align-items: center;
}
.user-header h3 { font-size: 13px; font-weight: 600; }
.user-header .total { font-size: 12px; font-weight: 600; }
.leave-summary {
margin-top: 10px;
padding: 8px 15px;
background: #f9f9f9;
border: 1px solid #ddd;
font-size: 10px;
}
.user-section table { width: 100%; border-collapse: collapse; margin-bottom: 15px; }
.user-section th, .user-section td { border: 1px solid #333; padding: 6px 8px; text-align: left; }
.user-section th { background: #333; color: #fff; font-weight: 600; font-size: 10px; text-transform: uppercase; }
.user-section td { font-size: 10px; }
.user-section tr:nth-child(even) { background: #f9f9f9; }
.text-center { text-align: center; }
.text-right { text-align: right; }
.user-section tfoot td { background: #eee; font-weight: 600; }
.leave-badge {
display: inline-block;
padding: 2px 6px;
border-radius: 3px;
font-size: 9px;
font-weight: 500;
}
.badge-vacation { background: #dbeafe; color: #1d4ed8; }
.badge-sick { background: #fee2e2; color: #dc2626; }
.badge-unpaid { background: #f3f4f6; color: #6b7280; }
.badge-overtime { background: #fef3c7; color: #d97706; }
@media print {
body { padding: 0; margin: 0; }
@page { size: A4 portrait; margin: 10mm; }
.user-section { page-break-inside: avoid; }
}
</style>
</head>
<body>
${content}
</body>
</html>
`);
printWindow.document.close();
printWindow.onload = () => {
printWindow.print();
};
};
const fund = computed.monthlyFund;
const progressValue =
fund.fund > 0 ? Math.min(100, (fund.worked / fund.fund) * 100) : 0;
const progressColor =
computed.delta > 0
? "warning"
: computed.delta >= 0
? "success"
: "primary";
const deltaColor =
computed.delta > 0
? "warning.main"
: computed.delta < 0
? "error.main"
: "success.main";
const columns: DataColumn<AttendanceRecord>[] = [
{
key: "date",
header: "Datum",
width: "10%",
mono: true,
render: (record) => formatDate(record.shift_date),
},
{
key: "type",
header: "Typ",
width: "11%",
render: (record) => {
const leaveType = record.leave_type || "work";
return (
<StatusChip
color={leaveTypeChipColor(leaveType)}
label={getLeaveTypeName(leaveType)}
/>
);
},
},
{
key: "arrival",
header: "Příchod",
width: "10%",
mono: true,
render: (record) => {
const isLeave = (record.leave_type || "work") !== "work";
return isLeave ? "—" : formatDatetime(record.arrival_time);
},
},
{
key: "break",
header: "Pauza",
width: "11%",
mono: true,
render: (record) => {
const isLeave = (record.leave_type || "work") !== "work";
return isLeave ? "—" : formatBreakRange(record);
},
},
{
key: "departure",
header: "Odchod",
width: "10%",
mono: true,
render: (record) => {
const isLeave = (record.leave_type || "work") !== "work";
return isLeave ? "—" : formatDatetime(record.departure_time);
},
},
{
key: "hours",
header: "Hodiny",
width: "9%",
mono: true,
render: (record) => {
const leaveType = record.leave_type || "work";
const isLeave = leaveType !== "work";
const workMinutes = isLeave
? (Number(record.leave_hours) || 8) * 60
: calculateWorkMinutes(record);
return workMinutes > 0 ? formatMinutes(workMinutes, true) : "—";
},
},
{
key: "projects",
header: "Projekty",
width: "18%",
render: (record) => renderProjectCell(record),
},
{
key: "notes",
header: "Poznámka",
width: "11%",
render: (record) => record.notes || "",
},
];
return (
<PageEnter>
<PageHeader
title="Historie docházky"
subtitle={computed.monthName}
actions={
records.length > 0 ? (
<Button
variant="outlined"
color="inherit"
startIcon={PrintIcon}
onClick={handlePrint}
title="Tisk docházky"
>
Tisk
</Button>
) : undefined
}
/>
{/* Filters */}
<FilterBar>
<Box sx={{ flex: "0 0 180px" }}>
<MonthField value={month} onChange={(val) => setMonth(val)} />
</Box>
</FilterBar>
{/* Monthly Fund Card */}
<Card sx={{ mt: 3 }}>
{isPending ? (
<LoadingState />
) : (
<Box
sx={{
display: "flex",
alignItems: "center",
gap: 2,
flexWrap: "wrap",
}}
>
<Box
sx={[
{
display: "flex",
alignItems: "center",
justifyContent: "center",
width: 44,
height: 44,
borderRadius: 2,
flexShrink: 0,
},
iconBadgeSx("info"),
]}
>
<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>
</Box>
<Box sx={{ flex: 1, minWidth: "200px" }}>
<Box
sx={{
display: "flex",
justifyContent: "space-between",
alignItems: "baseline",
mb: 0.75,
}}
>
<Typography sx={{ fontWeight: 600, fontSize: "1rem" }}>
Fond: {fund.worked}h / {fund.fund}h
</Typography>
<Typography
variant="body2"
color="text.secondary"
sx={{ fontSize: "0.8125rem" }}
>
{fund.business_days} prac. dnů
{fund.holiday_count > 0 && ` + ${fund.holiday_count} svátky`}
</Typography>
</Box>
<ProgressBar value={progressValue} color={progressColor} />
<Box
sx={{
display: "flex",
flexWrap: "wrap",
gap: 0.5,
mt: 1,
minHeight: "1.5rem",
alignItems: "center",
justifyContent: "space-between",
}}
>
<Box sx={{ display: "flex", flexWrap: "wrap", gap: 0.5 }}>
{computed.totalMinutes > 0 && (
<StatusChip
color="info"
label={`Práce: ${formatHoursDecimal(computed.totalMinutes)}h`}
title="Odpracováno (reálně)"
/>
)}
{computed.vacationHours > 0 && (
<StatusChip
color="success"
label={`Dov: ${computed.vacationHours}h`}
/>
)}
{computed.sickHours > 0 && (
<StatusChip
color="error"
label={`Nem: ${computed.sickHours}h`}
/>
)}
{computed.freeHolidayHours > 0 && (
<StatusChip
color="warning"
label={`Sv: ${Math.round(computed.freeHolidayHours)}h`}
/>
)}
{computed.unpaidHours > 0 && (
<StatusChip
color="default"
label={`Nep: ${computed.unpaidHours}h`}
/>
)}
</Box>
<Typography
variant="caption"
sx={{ fontWeight: 600, color: deltaColor }}
>
{computed.delta > 0
? `Přesčas: +${formatHoursDecimal(computed.delta * 60)}h`
: computed.delta < 0
? `Zbývá: ${formatHoursDecimal(-computed.delta * 60)}h`
: "Fond splněn"}
</Typography>
</Box>
</Box>
</Box>
)}
</Card>
{/* Records Table */}
<Card sx={{ mt: 3 }}>
{isPending ? (
<LoadingState />
) : (
<DataTable<AttendanceRecord>
columns={columns}
rows={records}
rowKey={(record) => record.id}
empty={<EmptyState title="Za tento měsíc nejsou žádné záznamy." />}
/>
)}
</Card>
{/* Hidden Print Content */}
{records.length > 0 && (
<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="/api/admin/company-settings/logo?variant=light"
alt=""
className="print-logo"
/>
<div className="print-header-text">
<h1>EVIDENCE DOCHÁZKY</h1>
<div className="company">{companyName}</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>
</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) && (
<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>{" "}
</>
)}
</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;
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>
)}
</PageEnter>
);
}
/** Map an attendance leave_type to a StatusChip semantic color (mirrors the
* legacy getLeaveTypeBadgeClass colors): work→info, vacation→info(blue),
* sick→error(red), unpaid→default(gray). */
function leaveTypeChipColor(
type: string,
): "default" | "success" | "error" | "warning" | "info" {
switch (type) {
case "vacation":
return "info";
case "sick":
return "error";
case "unpaid":
return "default";
default:
return "info";
}
}