Revízny systém - kompletná implementácia

- Backend: CRUD revízií, schedule endpoint (agregovaný plán), skip revízia, stats
- Shared utility revisionSchedule.ts - centralizovaná logika výpočtu cyklov
- Equipment detail s revíznym plánom, históriou a prílohami
- Frontend: RevisionsList s tabmi (nadchádzajúce/po termíne/vykonané/preskočené)
- Pozičné labeling cyklov (eliminuje drift 4×90≠365)
- EquipmentRevisionSchedule model (many-to-many typy revízií)
- Aktualizovaná dokumentácia HELPDESK_INIT_V2.md

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-23 21:59:23 +01:00
parent 2ca0c4f4d8
commit da265ff097
28 changed files with 4587 additions and 149 deletions

View File

@@ -0,0 +1,102 @@
import { useQuery } from '@tanstack/react-query';
import { settingsApi } from '@/services/settings.api';
import { AlertTriangle, Clock, CheckCircle, SkipForward, type LucideIcon } from 'lucide-react';
export interface RevisionThreshold {
days: number;
label: string;
color: string;
}
const DEFAULT_THRESHOLDS: RevisionThreshold[] = [
{ days: 30, label: 'Blíži sa', color: '#EAB308' },
{ days: 14, label: 'Blíži sa!', color: '#F97316' },
{ days: 7, label: 'Urgentné!', color: '#EF4444' },
];
const OVERDUE_COLOR = '#DC2626';
const OK_COLOR = '#22C55E';
const SKIP_COLOR = '#9CA3AF';
export type RevisionContext = 'schedule' | 'performed' | 'skipped';
export interface RevisionStatusResult {
label: string;
color: string | null;
icon: LucideIcon | null;
variant: 'default' | 'secondary' | 'destructive' | 'outline';
}
export function useRevisionStatus() {
const { data: settingData } = useQuery({
queryKey: ['system-setting', 'REVISION_STATUS_THRESHOLDS'],
queryFn: () => settingsApi.getSystemSetting('REVISION_STATUS_THRESHOLDS'),
staleTime: 5 * 60 * 1000,
});
const thresholds: RevisionThreshold[] = Array.isArray(settingData?.data?.value)
? (settingData.data.value as RevisionThreshold[])
: DEFAULT_THRESHOLDS;
// Zoradiť vzostupne podľa dní
const sorted = [...thresholds].sort((a, b) => a.days - b.days);
function getStatus(nextDueDate?: string | null, context?: RevisionContext | boolean): RevisionStatusResult {
// Spätná kompatibilita: boolean → context
let ctx: RevisionContext;
if (typeof context === 'boolean') {
ctx = context ? 'schedule' : 'performed';
} else {
ctx = context || 'schedule';
}
if (ctx === 'performed') {
return { label: 'Vykonaná', color: OK_COLOR, icon: CheckCircle, variant: 'secondary' };
}
if (ctx === 'skipped') {
return { label: 'Vynechaná', color: SKIP_COLOR, icon: SkipForward, variant: 'secondary' };
}
// context === 'schedule' → countdown logika
if (!nextDueDate) {
return { label: '-', color: null, icon: null, variant: 'secondary' };
}
const now = new Date();
const due = new Date(nextDueDate);
const diffDays = Math.ceil((due.getTime() - now.getTime()) / (1000 * 60 * 60 * 24));
// Po termíne
if (diffDays < 0) {
return {
label: `Po termíne (${Math.abs(diffDays)} dní)`,
color: OVERDUE_COLOR,
icon: AlertTriangle,
variant: 'destructive',
};
}
// Nájsť prvý prah kde diffDays <= threshold.days
for (const threshold of sorted) {
if (diffDays <= threshold.days) {
return {
label: `${threshold.label} (${diffDays} dní)`,
color: threshold.color,
icon: Clock,
variant: 'default',
};
}
}
// V poriadku - ďaleko od termínu
return {
label: `Za ${diffDays} dní`,
color: OK_COLOR,
icon: CheckCircle,
variant: 'default',
};
}
return { getStatus, thresholds };
}