initial commit

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
BOHA
2026-03-23 08:46:51 +01:00
commit 4608494a3f
130 changed files with 40361 additions and 0 deletions

102
src/admin/utils/api.ts Normal file
View File

@@ -0,0 +1,102 @@
let showSessionExpiredAlert = false
let showLogoutAlert = false
let getTokenFn: (() => string | null) | null = null
let refreshFn: (() => Promise<boolean>) | null = null
let refreshPromise: Promise<boolean> | null = null
export const shouldShowSessionExpiredAlert = (): boolean => {
if (showSessionExpiredAlert) {
showSessionExpiredAlert = false
return true
}
return false
}
export const setSessionExpired = (): void => {
showSessionExpiredAlert = true
}
export const shouldShowLogoutAlert = (): boolean => {
if (showLogoutAlert) {
showLogoutAlert = false
return true
}
return false
}
export const setLogoutAlert = (): void => {
showLogoutAlert = true
}
export const setTokenGetter = (fn: () => string | null): void => {
getTokenFn = fn
}
export const setRefreshFn = (fn: () => Promise<boolean>): void => {
refreshFn = fn
}
export const apiFetch = async (url: string, options: RequestInit = {}): Promise<Response> => {
let token: string | null = null
try {
token = getTokenFn ? getTokenFn() : null
} catch {
// token retrieval failed
}
const headers: Record<string, string> = {
...(options.headers as Record<string, string>),
}
if (!headers['Content-Type'] && options.body && !(options.body instanceof FormData)) {
headers['Content-Type'] = 'application/json'
}
if (token) {
headers['Authorization'] = `Bearer ${token}`
}
let response = await fetch(url, {
...options,
headers,
credentials: 'include',
})
if (response.status === 401 && refreshFn) {
try {
if (!refreshPromise) {
refreshPromise = refreshFn().finally(() => {
refreshPromise = null
})
}
const refreshed = await refreshPromise
if (refreshed) {
token = getTokenFn ? getTokenFn() : null
if (token) {
headers['Authorization'] = `Bearer ${token}`
}
response = await fetch(url, {
...options,
headers,
credentials: 'include',
})
} else {
setSessionExpired()
}
} catch {
setSessionExpired()
}
}
return response
}
export const getAccessToken = (): string | null => {
try {
return getTokenFn ? getTokenFn() : null
} catch {
return null
}
}
export default apiFetch

View File

@@ -0,0 +1,151 @@
interface AttendanceRecord {
arrival_time?: string | null
departure_time?: string | null
break_start?: string | null
break_end?: string | null
leave_type?: string
leave_hours?: number
shift_date?: string
notes?: string
project_logs?: Array<{
id?: number
project_id?: number
project_name?: string
started_at?: string
ended_at?: string | null
hours?: string | number | null
minutes?: string | number | null
}>
}
export const formatDate = (dateStr: string | null | undefined): string => {
if (!dateStr) return '—'
const d = new Date(dateStr)
return d.toLocaleDateString('cs-CZ')
}
export const formatDatetime = (datetime: string | null | undefined): string => {
if (!datetime) return '—'
const d = new Date(datetime)
return `${d.getDate()}.${d.getMonth() + 1}. ${d.toLocaleTimeString('cs-CZ', { hour: '2-digit', minute: '2-digit' })}`
}
export const formatTime = (datetime: string | null | undefined): string => {
if (!datetime) return '—'
return new Date(datetime).toLocaleTimeString('cs-CZ', { hour: '2-digit', minute: '2-digit' })
}
export const calculateWorkMinutes = (record: AttendanceRecord): number => {
if (!record.arrival_time || !record.departure_time) return 0
const arrival = new Date(record.arrival_time).getTime()
const departure = new Date(record.departure_time).getTime()
let minutes = (departure - arrival) / 60000
if (record.break_start && record.break_end) {
const breakStart = new Date(record.break_start).getTime()
const breakEnd = new Date(record.break_end).getTime()
minutes -= (breakEnd - breakStart) / 60000
}
return Math.max(0, Math.floor(minutes))
}
export const formatMinutes = (minutes: number, withUnit = false): string => {
const h = Math.floor(minutes / 60)
const m = minutes % 60
return `${h}:${String(m).padStart(2, '0')}${withUnit ? ' h' : ''}`
}
export const getLeaveTypeName = (type: string): string => {
const types: Record<string, string> = {
work: 'Práce',
vacation: 'Dovolená',
sick: 'Nemoc',
holiday: 'Svátek',
unpaid: 'Neplacené volno',
}
return types[type] || 'Práce'
}
export const getLeaveTypeBadgeClass = (type: string): string => {
const classes: Record<string, string> = {
vacation: 'badge-vacation',
sick: 'badge-sick',
holiday: 'badge-holiday',
unpaid: 'badge-unpaid',
}
return classes[type] || ''
}
export const getDatePart = (datetime: string | null | undefined): string => {
if (!datetime) return ''
if (datetime.includes('T')) {
return datetime.split('T')[0]
}
return datetime.split(' ')[0]
}
export const getTimePart = (datetime: string | null | undefined): string => {
if (!datetime) return ''
const d = new Date(datetime)
return `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`
}
export const calcProjectMinutesTotal = (logs: Array<{ project_id?: number; hours?: string | number; minutes?: string | number }>): number => {
return logs.filter(l => l.project_id).reduce((sum, l) => {
return sum + (parseInt(String(l.hours)) || 0) * 60 + (parseInt(String(l.minutes)) || 0)
}, 0)
}
interface ShiftForm {
arrival_time?: string
departure_time?: string
arrival_date?: string
departure_date?: string
break_start_time?: string
break_end_time?: string
break_start_date?: string
break_end_date?: string
}
export const calcFormWorkMinutes = (form: ShiftForm): number => {
if (!form.arrival_time || !form.departure_time) return 0
const arrivalStr = `${form.arrival_date}T${form.arrival_time}`
const departureStr = `${form.departure_date}T${form.departure_time}`
let mins = (new Date(departureStr).getTime() - new Date(arrivalStr).getTime()) / 60000
if (form.break_start_time && form.break_end_time) {
const bsStr = `${form.break_start_date}T${form.break_start_time}`
const beStr = `${form.break_end_date}T${form.break_end_time}`
mins -= (new Date(beStr).getTime() - new Date(bsStr).getTime()) / 60000
}
return Math.max(0, Math.floor(mins))
}
export const formatTimeOrDatetimePrint = (datetime: string | null | undefined, shiftDate: string): string => {
if (!datetime) return '—'
const timeDate = new Date(datetime).toISOString().split('T')[0]
if (timeDate !== shiftDate) {
const d = new Date(datetime)
return `${d.getDate()}.${d.getMonth() + 1}. ${d.toLocaleTimeString('cs-CZ', { hour: '2-digit', minute: '2-digit' })}`
}
return new Date(datetime).toLocaleTimeString('cs-CZ', { hour: '2-digit', minute: '2-digit' })
}
export const calculateWorkMinutesPrint = (record: AttendanceRecord): number => {
const leaveType = record.leave_type || 'work'
if (leaveType !== 'work') {
return (Number(record.leave_hours) || 8) * 60
}
if (!record.arrival_time || !record.departure_time) return 0
const arrival = new Date(record.arrival_time).getTime()
const departure = new Date(record.departure_time).getTime()
let minutes = (departure - arrival) / 60000
if (record.break_start && record.break_end) {
const breakStart = new Date(record.break_start).getTime()
const breakEnd = new Date(record.break_end).getTime()
minutes -= (breakEnd - breakStart) / 60000
}
return Math.max(0, Math.floor(minutes))
}

View File

@@ -0,0 +1,79 @@
export const LEAVE_TYPE_LABELS: Record<string, string> = {
vacation: 'Dovolená',
sick: 'Nemoc',
holiday: 'Svátek',
unpaid: 'Neplacené volno',
}
export const STATUS_DOT_CLASS: Record<string, string> = {
in: 'dash-status-in',
away: 'dash-status-away',
out: 'dash-status-out',
leave: 'dash-status-leave',
}
export const STATUS_LABELS: Record<string, string> = {
in: 'Přítomen',
away: 'Přestávka',
out: 'Nepřihlášen',
leave: 'Nepřítomen',
}
export const ENTITY_TYPE_LABELS: Record<string, string> = {
user: 'Uživatel',
attendance: 'Docházka',
leave_request: 'Žádost o nepřítomnost',
offers_quotation: 'Nabídka',
offers_customer: 'Zákazník',
offers_item_template: 'Šablona položky',
offers_scope_template: 'Šablona rozsahu',
offers_settings: 'Nastavení nabídek',
orders_order: 'Objednávka',
invoices_invoice: 'Faktura',
projects_project: 'Projekt',
role: 'Role',
trips: 'Jízda',
vehicles: 'Vozidlo',
bank_account: 'Bankovní účet',
}
export const ACTION_LABELS: Record<string, string> = {
create: 'Vytvořil',
update: 'Upravil',
delete: 'Smazal',
login: 'Přihlášení',
}
export function getCzechDate(): string {
const now = new Date()
const days = ['Neděle', 'Pondělí', 'Úterý', 'Středa', 'Čtvrtek', 'Pátek', 'Sobota']
const months = ['ledna', 'února', 'března', 'dubna', 'května', 'června', 'července', 'srpna', 'září', 'října', 'listopadu', 'prosince']
const day = days[now.getDay()]
const oneJan = new Date(now.getFullYear(), 0, 1)
const week = Math.ceil(((now.getTime() - oneJan.getTime()) / 86400000 + oneJan.getDay() + 1) / 7)
return `${day}, ${now.getDate()}. ${months[now.getMonth()]} ${now.getFullYear()} · Týden ${week}`
}
export function getActivityIconClass(action: string): string {
const map: Record<string, string> = { create: 'success', update: 'info', delete: 'danger', login: 'accent' }
return map[action] || 'muted'
}
export function formatActivityTime(dateString: string): string {
const date = new Date(dateString)
const now = new Date()
const diff = now.getTime() - date.getTime()
if (diff < 60000) return 'Právě teď'
if (diff < 3600000) return `${Math.floor(diff / 60000)} min`
if (date.toDateString() === now.toDateString()) {
return date.toLocaleTimeString('cs-CZ', { hour: '2-digit', minute: '2-digit' })
}
return date.toLocaleDateString('cs-CZ', { day: '2-digit', month: '2-digit' })
}
export function formatSessionDate(dateString: string): string {
const date = new Date(dateString)
return date.toLocaleDateString('cs-CZ', {
day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit',
})
}

View File

@@ -0,0 +1,26 @@
export function formatCurrency(amount: number | string, currency: string): string {
const num = Number(amount) || 0
switch (currency) {
case 'EUR': return `${num.toLocaleString('cs-CZ', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} €`
case 'USD': return `$${num.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`
case 'CZK': return `${num.toLocaleString('cs-CZ', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} Kč`
case 'GBP': return `£${num.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`
default: return `${num.toFixed(2)} ${currency}`
}
}
export function formatDate(dateStr: string | null | undefined): string {
if (!dateStr) return '—'
const d = new Date(dateStr)
return d.toLocaleDateString('cs-CZ')
}
export function formatKm(km: number | string): string {
return new Intl.NumberFormat('cs-CZ').format(Number(km) || 0)
}
export function czechPlural(n: number, one: string, few: string, many: string): string {
if (n === 1) return one
if (n >= 2 && n <= 4) return few
return many
}