diff --git a/backend/modules/fleet/models.py b/backend/modules/fleet/models.py index e46d8d7..d3eca2d 100644 --- a/backend/modules/fleet/models.py +++ b/backend/modules/fleet/models.py @@ -748,25 +748,47 @@ class MonthlyServicerDayEntry(BaseModel): def __str__(self): return f"{self.user_id} @ {self.entry_date} ({self.entry_type})" + def _calculate_regular_hours(self, start_time, end_time): + if start_time is None or end_time is None: + return Decimal('8.00') + + start_minutes = (start_time.hour * 60) + start_time.minute + (start_time.second / 60) + end_minutes = (end_time.hour * 60) + end_time.minute + (end_time.second / 60) + if end_minutes <= start_minutes: + raise ValidationError({ + 'end_time': _('Kraj rada mora biti nakon početka rada.'), + }) + + hours = Decimal(str((end_minutes - start_minutes) / 60)).quantize(Decimal('0.01')) + return hours + def apply_defaults(self): + start_time = self.start_time + end_time = self.end_time + if self.entry_type == 'office': - self.description = 'Rad u uredu' - self.location = 'Zagreb (ured)' - self.start_time = time(8, 0) - self.end_time = time(16, 0) - self.regular_hours = Decimal('8.00') - self.overtime_hours = Decimal('0.00') - return - - if self.entry_type == 'vacation': - self.description = 'Godišnji odmor' + if not self.description: + self.description = 'Rad u uredu' + if not self.location: + self.location = 'Zagreb (ured)' + if start_time is None: + start_time = time(8, 0) + if end_time is None: + end_time = time(16, 0) + elif self.entry_type == 'vacation': + if not self.description: + self.description = 'Godišnji odmor' + self.location = self.location or '' elif self.entry_type == 'sick_leave': - self.description = 'Bolovanje' + if not self.description: + self.description = 'Bolovanje' + self.location = self.location or '' + else: + self.location = self.location or '' - self.location = '' - self.start_time = None - self.end_time = None - self.regular_hours = Decimal('8.00') + self.start_time = start_time + self.end_time = end_time + self.regular_hours = self._calculate_regular_hours(start_time, end_time) self.overtime_hours = Decimal('0.00') def clean(self): diff --git a/backend/modules/fleet/serializers.py b/backend/modules/fleet/serializers.py index dfd8f4b..1a8f2c4 100644 --- a/backend/modules/fleet/serializers.py +++ b/backend/modules/fleet/serializers.py @@ -250,8 +250,6 @@ class MonthlyServicerDayEntrySerializer(serializers.ModelSerializer): 'id', 'description', 'location', - 'start_time', - 'end_time', 'regular_hours', 'overtime_hours', 'created_at', diff --git a/frontend/src/components/dashboard/TaskCalendarWidget.jsx b/frontend/src/components/dashboard/TaskCalendarWidget.jsx index cb2e5c6..58d7a1c 100644 --- a/frontend/src/components/dashboard/TaskCalendarWidget.jsx +++ b/frontend/src/components/dashboard/TaskCalendarWidget.jsx @@ -47,6 +47,22 @@ function formatTime(value) { return parsed.toLocaleTimeString('hr-HR', { hour: '2-digit', minute: '2-digit' }); } +function toTimeInputValue(value) { + const formatted = formatTime(value); + return formatted === '-' ? '' : formatted; +} + +function calculateHours(startTime, endTime) { + if (!startTime || !endTime) return null; + const [startHour, startMinute] = String(startTime).split(':').map((part) => Number(part)); + const [endHour, endMinute] = String(endTime).split(':').map((part) => Number(part)); + if (![startHour, startMinute, endHour, endMinute].every((value) => Number.isFinite(value))) return null; + const startTotal = (startHour * 60) + startMinute; + const endTotal = (endHour * 60) + endMinute; + if (endTotal <= startTotal) return null; + return ((endTotal - startTotal) / 60).toFixed(2).replace(/\.?0+$/, ''); +} + function formatHourValue(value, fallback = '-') { if (value == null || value === '') return fallback; const numeric = Number(value); @@ -70,6 +86,8 @@ export default function TaskCalendarWidget({ tasks = [], notes = [], workOrders const [loadingManualEntries, setLoadingManualEntries] = useState(false); const [loadingCostInvoices, setLoadingCostInvoices] = useState(false); const [manualEntryTarget, setManualEntryTarget] = useState(null); + const [manualEntryDraft, setManualEntryDraft] = useState(null); + const [manualEntryError, setManualEntryError] = useState(''); const [savingManualEntry, setSavingManualEntry] = useState(false); const reportOpen = reportType !== null; @@ -342,15 +360,36 @@ export default function TaskCalendarWidget({ tasks = [], notes = [], workOrders function handleServicerRowClick(row) { if (!row?.clickable) return; setManualEntryTarget(row); + setManualEntryDraft(null); + setManualEntryError(''); } - async function handleSaveManualEntry(entryType) { - if (!manualEntryTarget || savingManualEntry) return; + function startManualEntry(entryType) { + if (!manualEntryTarget) return; + const isOffice = entryType === 'office'; + setManualEntryError(''); + setManualEntryDraft({ + entryType, + startTime: isOffice ? '08:00' : '', + endTime: isOffice ? '16:00' : '', + }); + } + + async function handleSaveManualEntry() { + if (!manualEntryTarget || !manualEntryDraft || savingManualEntry) return; + const { entryType, startTime, endTime } = manualEntryDraft; + if (startTime && endTime && !calculateHours(startTime, endTime)) { + setManualEntryError('Kraj rada mora biti nakon početka rada.'); + return; + } + setManualEntryError(''); setSavingManualEntry(true); try { const saved = await upsertMonthlyServicerEntry({ entry_date: manualEntryTarget.key, entry_type: entryType, + start_time: startTime || undefined, + end_time: endTime || undefined, }); setManualEntries((prev) => { const next = Array.isArray(prev) ? [...prev] : []; @@ -363,6 +402,8 @@ export default function TaskCalendarWidget({ tasks = [], notes = [], workOrders return next.sort((a, b) => String(a.entry_date || '').localeCompare(String(b.entry_date || ''))); }); setManualEntryTarget(null); + setManualEntryDraft(null); + setManualEntryError(''); } finally { setSavingManualEntry(false); } @@ -679,14 +720,20 @@ export default function TaskCalendarWidget({ tasks = [], notes = [], workOrders
{formatDateFromKey(manualEntryTarget.key)} nema putni nalog. Odaberi status dana.
{manualEntryError}
+ )} +