fix: omoguci rucni unos vremena u mjesecnom izvjestaju
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:
@@ -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>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user