style: run prettier on entire codebase
This commit is contained in:
@@ -1,109 +1,169 @@
|
||||
import { useAlert } from '../context/AlertContext'
|
||||
import { useAuth } from '../context/AuthContext'
|
||||
import Forbidden from '../components/Forbidden'
|
||||
import { motion } from 'framer-motion'
|
||||
import ConfirmModal from '../components/ConfirmModal'
|
||||
import AdminDatePicker from '../components/AdminDatePicker'
|
||||
import BulkAttendanceModal from '../components/BulkAttendanceModal'
|
||||
import ShiftFormModal from '../components/ShiftFormModal'
|
||||
import AttendanceShiftTable from '../components/AttendanceShiftTable'
|
||||
import useModalLock from '../hooks/useModalLock'
|
||||
import useAttendanceAdmin from '../hooks/useAttendanceAdmin'
|
||||
import FormField from '../components/FormField'
|
||||
import { formatMinutes } from '../utils/attendanceHelpers'
|
||||
import { useAlert } from "../context/AlertContext";
|
||||
import { useAuth } from "../context/AuthContext";
|
||||
import Forbidden from "../components/Forbidden";
|
||||
import { motion } from "framer-motion";
|
||||
import ConfirmModal from "../components/ConfirmModal";
|
||||
import AdminDatePicker from "../components/AdminDatePicker";
|
||||
import BulkAttendanceModal from "../components/BulkAttendanceModal";
|
||||
import ShiftFormModal from "../components/ShiftFormModal";
|
||||
import AttendanceShiftTable from "../components/AttendanceShiftTable";
|
||||
import useModalLock from "../hooks/useModalLock";
|
||||
import useAttendanceAdmin from "../hooks/useAttendanceAdmin";
|
||||
import FormField from "../components/FormField";
|
||||
import { formatMinutes } from "../utils/attendanceHelpers";
|
||||
|
||||
interface UserTotalData {
|
||||
name: string
|
||||
minutes: number
|
||||
working: boolean
|
||||
vacation_hours: number
|
||||
sick_hours: number
|
||||
holiday_hours: number
|
||||
unpaid_hours: number
|
||||
fund: number | null
|
||||
worked_hours: number
|
||||
covered: number
|
||||
missing: number
|
||||
overtime: number
|
||||
name: string;
|
||||
minutes: number;
|
||||
working: boolean;
|
||||
vacation_hours: number;
|
||||
sick_hours: number;
|
||||
holiday_hours: number;
|
||||
unpaid_hours: number;
|
||||
fund: number | null;
|
||||
worked_hours: number;
|
||||
covered: number;
|
||||
missing: number;
|
||||
overtime: number;
|
||||
}
|
||||
|
||||
function getFundBarBackground(data: UserTotalData) {
|
||||
if (data.overtime > 0) return 'linear-gradient(135deg, var(--warning), #d97706)'
|
||||
if (data.covered >= (data.fund ?? 0)) return 'linear-gradient(135deg, var(--success), #059669)'
|
||||
return 'var(--gradient)'
|
||||
if (data.overtime > 0)
|
||||
return "linear-gradient(135deg, var(--warning), #d97706)";
|
||||
if (data.covered >= (data.fund ?? 0))
|
||||
return "linear-gradient(135deg, var(--success), #059669)";
|
||||
return "var(--gradient)";
|
||||
}
|
||||
|
||||
export default function AttendanceAdmin() {
|
||||
const alert = useAlert()
|
||||
const { hasPermission } = useAuth()
|
||||
const alert = useAlert();
|
||||
const { hasPermission } = useAuth();
|
||||
|
||||
const {
|
||||
loading, month, setMonth,
|
||||
filterUserId, setFilterUserId,
|
||||
data, hasData,
|
||||
showBulkModal, setShowBulkModal,
|
||||
bulkSubmitting, bulkForm, setBulkForm,
|
||||
showCreateModal, setShowCreateModal,
|
||||
createForm, setCreateForm,
|
||||
showEditModal, setShowEditModal,
|
||||
editingRecord, editForm, setEditForm,
|
||||
deleteConfirm, setDeleteConfirm,
|
||||
loading,
|
||||
month,
|
||||
setMonth,
|
||||
filterUserId,
|
||||
setFilterUserId,
|
||||
data,
|
||||
hasData,
|
||||
showBulkModal,
|
||||
setShowBulkModal,
|
||||
bulkSubmitting,
|
||||
bulkForm,
|
||||
setBulkForm,
|
||||
showCreateModal,
|
||||
setShowCreateModal,
|
||||
createForm,
|
||||
setCreateForm,
|
||||
showEditModal,
|
||||
setShowEditModal,
|
||||
editingRecord,
|
||||
editForm,
|
||||
setEditForm,
|
||||
deleteConfirm,
|
||||
setDeleteConfirm,
|
||||
projectList,
|
||||
createProjectLogs, setCreateProjectLogs,
|
||||
editProjectLogs, setEditProjectLogs,
|
||||
openCreateModal, handleCreateShiftDateChange, handleCreateSubmit,
|
||||
openBulkModal, toggleBulkUser, toggleAllBulkUsers, handleBulkSubmit,
|
||||
openEditModal, handleEditSubmit,
|
||||
handleDelete, handlePrint
|
||||
} = useAttendanceAdmin({ alert })
|
||||
createProjectLogs,
|
||||
setCreateProjectLogs,
|
||||
editProjectLogs,
|
||||
setEditProjectLogs,
|
||||
openCreateModal,
|
||||
handleCreateShiftDateChange,
|
||||
handleCreateSubmit,
|
||||
openBulkModal,
|
||||
toggleBulkUser,
|
||||
toggleAllBulkUsers,
|
||||
handleBulkSubmit,
|
||||
openEditModal,
|
||||
handleEditSubmit,
|
||||
handleDelete,
|
||||
handlePrint,
|
||||
} = useAttendanceAdmin({ alert });
|
||||
|
||||
useModalLock(showBulkModal)
|
||||
useModalLock(showEditModal)
|
||||
useModalLock(showCreateModal)
|
||||
useModalLock(showBulkModal);
|
||||
useModalLock(showEditModal);
|
||||
useModalLock(showCreateModal);
|
||||
|
||||
if (!hasPermission('attendance.admin')) return <Forbidden />
|
||||
if (!hasPermission("attendance.admin")) return <Forbidden />;
|
||||
|
||||
// Show skeleton only on initial load (no data yet), not on filter changes
|
||||
const isInitialLoad = loading && data.records.length === 0 && Object.keys(data.user_totals).length === 0
|
||||
const isInitialLoad =
|
||||
loading &&
|
||||
data.records.length === 0 &&
|
||||
Object.keys(data.user_totals).length === 0;
|
||||
|
||||
if (isInitialLoad) {
|
||||
return (
|
||||
<div className="admin-skeleton" style={{ padding: 0, gap: '1.5rem' }}>
|
||||
<div className="admin-skeleton-row" style={{ justifyContent: 'space-between' }}>
|
||||
<div className="admin-skeleton" style={{ padding: 0, gap: "1.5rem" }}>
|
||||
<div
|
||||
className="admin-skeleton-row"
|
||||
style={{ justifyContent: "space-between" }}
|
||||
>
|
||||
<div>
|
||||
<div className="admin-skeleton-line h-8" style={{ width: '200px', marginBottom: '0.5rem' }} />
|
||||
<div
|
||||
className="admin-skeleton-line h-8"
|
||||
style={{ width: "200px", marginBottom: "0.5rem" }}
|
||||
/>
|
||||
</div>
|
||||
<div className="admin-skeleton-row" style={{ gap: '0.5rem' }}>
|
||||
<div className="admin-skeleton-line h-10" style={{ width: '120px', borderRadius: '8px' }} />
|
||||
<div className="admin-skeleton-line h-10" style={{ width: '120px', borderRadius: '8px' }} />
|
||||
<div className="admin-skeleton-line h-10" style={{ width: '140px', borderRadius: '8px' }} />
|
||||
<div className="admin-skeleton-row" style={{ gap: "0.5rem" }}>
|
||||
<div
|
||||
className="admin-skeleton-line h-10"
|
||||
style={{ width: "120px", borderRadius: "8px" }}
|
||||
/>
|
||||
<div
|
||||
className="admin-skeleton-line h-10"
|
||||
style={{ width: "120px", borderRadius: "8px" }}
|
||||
/>
|
||||
<div
|
||||
className="admin-skeleton-line h-10"
|
||||
style={{ width: "140px", borderRadius: "8px" }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="admin-card">
|
||||
<div className="admin-skeleton" style={{ gap: '0.75rem', padding: '1rem' }}>
|
||||
<div
|
||||
className="admin-skeleton"
|
||||
style={{ gap: "0.75rem", padding: "1rem" }}
|
||||
>
|
||||
<div className="admin-skeleton-row">
|
||||
<div className="admin-skeleton-line h-10" style={{ flex: 1, borderRadius: '8px' }} />
|
||||
<div className="admin-skeleton-line h-10" style={{ flex: 1, borderRadius: '8px' }} />
|
||||
<div
|
||||
className="admin-skeleton-line h-10"
|
||||
style={{ flex: 1, borderRadius: "8px" }}
|
||||
/>
|
||||
<div
|
||||
className="admin-skeleton-line h-10"
|
||||
style={{ flex: 1, borderRadius: "8px" }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="admin-grid admin-grid-3">
|
||||
{[0, 1, 2].map(i => (
|
||||
{[0, 1, 2].map((i) => (
|
||||
<div key={i} className="admin-card">
|
||||
<div className="admin-card-body">
|
||||
<div className="admin-skeleton" style={{ gap: '0.75rem' }}>
|
||||
<div className="admin-skeleton" style={{ gap: "0.75rem" }}>
|
||||
<div className="admin-skeleton-line w-1/2" />
|
||||
<div className="admin-skeleton-line h-8" style={{ width: '80px' }} />
|
||||
<div className="admin-skeleton-line w-1/3" style={{ height: '10px' }} />
|
||||
<div className="admin-skeleton-line w-full" style={{ height: '4px' }} />
|
||||
<div
|
||||
className="admin-skeleton-line h-8"
|
||||
style={{ width: "80px" }}
|
||||
/>
|
||||
<div
|
||||
className="admin-skeleton-line w-1/3"
|
||||
style={{ height: "10px" }}
|
||||
/>
|
||||
<div
|
||||
className="admin-skeleton-line w-full"
|
||||
style={{ height: "4px" }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="admin-card">
|
||||
<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" />
|
||||
@@ -113,7 +173,7 @@ export default function AttendanceAdmin() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -134,7 +194,15 @@ export default function AttendanceAdmin() {
|
||||
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" />
|
||||
@@ -152,7 +220,14 @@ export default function AttendanceAdmin() {
|
||||
onClick={openCreateModal}
|
||||
className="admin-btn admin-btn-primary"
|
||||
>
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
>
|
||||
<line x1="12" y1="5" x2="12" y2="19" />
|
||||
<line x1="5" y1="12" x2="19" y2="12" />
|
||||
</svg>
|
||||
@@ -185,7 +260,9 @@ export default function AttendanceAdmin() {
|
||||
>
|
||||
<option value="">Všichni</option>
|
||||
{data.users.map((user) => (
|
||||
<option key={user.id} value={user.id}>{user.name}</option>
|
||||
<option key={user.id} value={user.id}>
|
||||
{user.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</FormField>
|
||||
@@ -202,68 +279,110 @@ export default function AttendanceAdmin() {
|
||||
transition={{ duration: 0.25, delay: 0.09 }}
|
||||
>
|
||||
{Object.entries(data.user_totals).map(([uid, userData]) => {
|
||||
const ut = userData as UserTotalData
|
||||
const ut = userData as UserTotalData;
|
||||
return (
|
||||
<div key={uid} className="admin-card">
|
||||
<div className="admin-card-body">
|
||||
<div className="flex-row gap-2 mb-2">
|
||||
<span style={{ fontWeight: 600 }}>{ut.name}</span>
|
||||
<span className={`attendance-working-badge ${ut.working ? 'working' : 'finished'}`}>
|
||||
{ut.working ? '\u2713' : '\u2717'}
|
||||
<span
|
||||
className={`attendance-working-badge ${ut.working ? "working" : "finished"}`}
|
||||
>
|
||||
{ut.working ? "\u2713" : "\u2717"}
|
||||
</span>
|
||||
</div>
|
||||
<div className="admin-stat-value">{formatMinutes(ut.minutes)}</div>
|
||||
<div className="admin-stat-value">
|
||||
{formatMinutes(ut.minutes)}
|
||||
</div>
|
||||
<div className="admin-stat-label">odpracováno</div>
|
||||
<div style={{ marginTop: '0.5rem', display: 'flex', flexWrap: 'wrap', gap: '0.25rem' }}>
|
||||
<div
|
||||
style={{
|
||||
marginTop: "0.5rem",
|
||||
display: "flex",
|
||||
flexWrap: "wrap",
|
||||
gap: "0.25rem",
|
||||
}}
|
||||
>
|
||||
{ut.vacation_hours > 0 && (
|
||||
<span className="attendance-leave-badge badge-vacation">Dov: {ut.vacation_hours}h</span>
|
||||
<span className="attendance-leave-badge badge-vacation">
|
||||
Dov: {ut.vacation_hours}h
|
||||
</span>
|
||||
)}
|
||||
{ut.sick_hours > 0 && (
|
||||
<span className="attendance-leave-badge badge-sick">Nem: {ut.sick_hours}h</span>
|
||||
<span className="attendance-leave-badge badge-sick">
|
||||
Nem: {ut.sick_hours}h
|
||||
</span>
|
||||
)}
|
||||
{ut.holiday_hours > 0 && (
|
||||
<span className="attendance-leave-badge badge-holiday">Sv: {ut.holiday_hours}h</span>
|
||||
<span className="attendance-leave-badge badge-holiday">
|
||||
Sv: {ut.holiday_hours}h
|
||||
</span>
|
||||
)}
|
||||
{ut.unpaid_hours > 0 && (
|
||||
<span className="attendance-leave-badge badge-unpaid">Nep: {ut.unpaid_hours}h</span>
|
||||
<span className="attendance-leave-badge badge-unpaid">
|
||||
Nep: {ut.unpaid_hours}h
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{ut.fund !== null && (
|
||||
<div className="mt-2">
|
||||
<div className="text-secondary" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', fontSize: '0.8rem' }}>
|
||||
<span>Fond: {ut.worked_hours}h / {ut.fund}h</span>
|
||||
<div
|
||||
className="text-secondary"
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
fontSize: "0.8rem",
|
||||
}}
|
||||
>
|
||||
<span>
|
||||
Fond: {ut.worked_hours}h / {ut.fund}h
|
||||
</span>
|
||||
{ut.overtime > 0 && (
|
||||
<span className="text-warning fw-600">+{ut.overtime}h</span>
|
||||
<span className="text-warning fw-600">
|
||||
+{ut.overtime}h
|
||||
</span>
|
||||
)}
|
||||
{ut.overtime <= 0 && ut.missing > 0 && (
|
||||
<span className="text-danger fw-600">-{ut.missing}h</span>
|
||||
<span className="text-danger fw-600">
|
||||
-{ut.missing}h
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div style={{
|
||||
marginTop: '0.375rem',
|
||||
height: '4px',
|
||||
background: 'var(--bg-tertiary)',
|
||||
borderRadius: '2px',
|
||||
overflow: 'hidden'
|
||||
}}>
|
||||
<div style={{
|
||||
height: '100%',
|
||||
width: `${Math.min(100, (ut.covered / (ut.fund || 1)) * 100)}%`,
|
||||
background: getFundBarBackground(ut),
|
||||
borderRadius: '2px',
|
||||
transition: 'width 0.3s ease'
|
||||
}} />
|
||||
<div
|
||||
style={{
|
||||
marginTop: "0.375rem",
|
||||
height: "4px",
|
||||
background: "var(--bg-tertiary)",
|
||||
borderRadius: "2px",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
height: "100%",
|
||||
width: `${Math.min(100, (ut.covered / (ut.fund || 1)) * 100)}%`,
|
||||
background: getFundBarBackground(ut),
|
||||
borderRadius: "2px",
|
||||
transition: "width 0.3s ease",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{data.leave_balances[uid] && (
|
||||
<div className="text-secondary" style={{ marginTop: '0.5rem', fontSize: '0.8rem' }}>
|
||||
Zbývá dovolené: {data.leave_balances[uid].vacation_remaining.toFixed(1)}h / {data.leave_balances[uid].vacation_total}h
|
||||
<div
|
||||
className="text-secondary"
|
||||
style={{ marginTop: "0.5rem", fontSize: "0.8rem" }}
|
||||
>
|
||||
Zbývá dovolené:{" "}
|
||||
{data.leave_balances[uid].vacation_remaining.toFixed(1)}h
|
||||
/ {data.leave_balances[uid].vacation_total}h
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
})}
|
||||
</motion.div>
|
||||
)}
|
||||
@@ -337,5 +456,5 @@ export default function AttendanceAdmin() {
|
||||
confirmVariant="danger"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user