fix: omoguci rucni unos vremena u mjesecnom izvjestaju
Some checks failed
ERP CI/CD Pipeline / test (push) Has been cancelled
ERP CI/CD Pipeline / Deploy (server git pull + compose) (push) Has been cancelled

Dodaje uredski unos pocetka i kraja rada u sidebaru mjesecnog izvjestaja servisera i automatski racuna redovan rad iz unesenog raspona. Zadrzava postojece predpopunjavanje za rad u uredu i validira neispravan vremenski raspon.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
mariomitte
2026-07-31 17:37:44 +02:00
parent 882e255523
commit 51a65f9b80
3 changed files with 169 additions and 50 deletions

View File

@@ -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
<div className="w-full max-w-sm rounded-xl border border-border-hairline bg-canvas-elevated p-4 shadow-2xl">
<div className="flex items-start justify-between gap-3">
<div>
<h3 className="text-sm font-semibold text-text-main">Odaberi vrstu unosa</h3>
<h3 className="text-sm font-semibold text-text-main">
{manualEntryDraft ? 'Unos radnog dana' : 'Odaberi vrstu unosa'}
</h3>
<p className="mt-1 text-xs text-text-muted">
{formatDateFromKey(manualEntryTarget.key)} nema putni nalog. Odaberi status dana.
</p>
</div>
<button
type="button"
onClick={() => setManualEntryTarget(null)}
onClick={() => {
setManualEntryTarget(null);
setManualEntryDraft(null);
setManualEntryError('');
}}
className="rounded p-1 text-text-muted hover:bg-canvas-deep"
aria-label="Zatvori"
>
@@ -694,35 +741,87 @@ export default function TaskCalendarWidget({ tasks = [], notes = [], workOrders
</button>
</div>
<div className="mt-4 grid gap-2">
<button
type="button"
disabled={savingManualEntry}
onClick={() => handleSaveManualEntry('office')}
className="rounded-lg border border-border-hairline px-3 py-2 text-left text-sm hover:bg-indigo-50"
>
<span className="block font-medium text-text-main">Rad u uredu</span>
<span className="block text-xs text-text-muted">08:00 - 16:00, 8h, Zagreb (ured)</span>
</button>
<button
type="button"
disabled={savingManualEntry}
onClick={() => handleSaveManualEntry('sick_leave')}
className="rounded-lg border border-border-hairline px-3 py-2 text-left text-sm hover:bg-indigo-50"
>
<span className="block font-medium text-text-main">Bolovanje</span>
<span className="block text-xs text-text-muted">Ukupno 8h</span>
</button>
<button
type="button"
disabled={savingManualEntry}
onClick={() => handleSaveManualEntry('vacation')}
className="rounded-lg border border-border-hairline px-3 py-2 text-left text-sm hover:bg-indigo-50"
>
<span className="block font-medium text-text-main">Godisnji</span>
<span className="block text-xs text-text-muted">Ukupno 8h</span>
</button>
</div>
{!manualEntryDraft ? (
<div className="mt-4 grid gap-2">
<button
type="button"
disabled={savingManualEntry}
onClick={() => startManualEntry('office')}
className="rounded-lg border border-border-hairline px-3 py-2 text-left text-sm hover:bg-indigo-50"
>
<span className="block font-medium text-text-main">Rad u uredu</span>
<span className="block text-xs text-text-muted">08:00 - 16:00, 8h, Zagreb (ured)</span>
</button>
<button
type="button"
disabled={savingManualEntry}
onClick={() => startManualEntry('sick_leave')}
className="rounded-lg border border-border-hairline px-3 py-2 text-left text-sm hover:bg-indigo-50"
>
<span className="block font-medium text-text-main">Bolovanje</span>
<span className="block text-xs text-text-muted">Unesi vrijeme po potrebi</span>
</button>
<button
type="button"
disabled={savingManualEntry}
onClick={() => startManualEntry('vacation')}
className="rounded-lg border border-border-hairline px-3 py-2 text-left text-sm hover:bg-indigo-50"
>
<span className="block font-medium text-text-main">Godisnji</span>
<span className="block text-xs text-text-muted">Unesi vrijeme po potrebi</span>
</button>
</div>
) : (
<div className="mt-4 space-y-3">
<div className="grid grid-cols-2 gap-3">
<label className="block">
<span className="mb-1 block text-[11px] font-semibold uppercase tracking-wide text-text-muted">Početak rada</span>
<input
type="time"
value={manualEntryDraft.startTime}
onChange={(event) => setManualEntryDraft((prev) => ({ ...prev, startTime: event.target.value }))}
className="w-full rounded-lg border border-border-hairline bg-canvas-base px-3 py-2 text-sm text-text-main"
/>
</label>
<label className="block">
<span className="mb-1 block text-[11px] font-semibold uppercase tracking-wide text-text-muted">Kraj rada</span>
<input
type="time"
value={manualEntryDraft.endTime}
onChange={(event) => setManualEntryDraft((prev) => ({ ...prev, endTime: event.target.value }))}
className="w-full rounded-lg border border-border-hairline bg-canvas-base px-3 py-2 text-sm text-text-main"
/>
</label>
</div>
<div className="rounded-lg bg-canvas-deep px-3 py-2 text-xs text-text-muted">
Redovan rad (h): {calculateHours(manualEntryDraft.startTime, manualEntryDraft.endTime) || '0'}
</div>
{manualEntryError && (
<p className="text-xs text-red-600">{manualEntryError}</p>
)}
<div className="flex gap-2">
<button
type="button"
disabled={savingManualEntry}
onClick={() => {
setManualEntryDraft(null);
setManualEntryError('');
}}
className="flex-1 rounded-lg border border-border-hairline px-3 py-2 text-sm hover:bg-canvas-deep"
>
Nazad
</button>
<button
type="button"
disabled={savingManualEntry}
onClick={handleSaveManualEntry}
className="flex-1 rounded-lg bg-indigo-600 px-3 py-2 text-sm text-white hover:bg-indigo-700 disabled:opacity-60"
>
{savingManualEntry ? 'Spremam...' : 'Spremi'}
</button>
</div>
</div>
)}
</div>
</div>
)}