timekeeper/app/templates/review_edit.html
2026-01-15 15:46:35 -05:00

659 lines
30 KiB
HTML

{% extends "layout.html" %}
{% block content %}
<div class="page-wide">
<div class="panel" style="width:100%;">
<div class="panel-title">Review & Edit</div>
{% if flash %}<div class="alert success">{{ flash }}</div>{% endif %}
<div style="display:flex;gap:8px;align-items:center;justify-content:space-between;margin-bottom:10px;flex-wrap:wrap;">
<div class="muted"><strong>{{ employee.name }}</strong> &ndash; {{ period_name }}</div>
<div style="display:flex;gap:8px;flex-wrap:wrap;">
<a class="btn" href="/review?timesheet_id={{ timesheet_id }}">Return to Review Timesheets</a>
<button class="btn" type="button" onclick="openPrint('/review/print?timesheet_id={{ timesheet_id }}&employee_id={{ employee.id }}')">Print</button>
</div>
</div>
<form id="carry-over-form-edit" method="post" action="/viewer/update-employee-period"
style="display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-bottom:12px;"
onsubmit="return saveCarryOverEdit(event)">
<input type="hidden" name="employee_id" value="{{ employee.id }}">
<input type="hidden" name="timesheet_id" value="{{ timesheet_id }}">
<input type="hidden" name="redirect_to" value="/review/edit?timesheet_id={{ timesheet_id }}&employee_id={{ employee.id }}">
<label class="label" for="carry_over_hours">Carry over hours</label>
<input class="input" id="carry_over_hours" name="carry_over_hours" type="number" step="0.01" value="{{ carry_over_hours|fmt2 }}" {% if not can_edit %}disabled{% endif %}>
<button class="btn" type="submit" {% if not can_edit %}disabled{% endif %}>Save</button>
<span class="muted">Applied to Week 1 regular cap (40 minus carry over).</span>
</form>
<!-- NEW: Payroll extras panel (included in Excel export) -->
<div class="panel" style="margin-bottom:12px;">
<div class="panel-title">Payroll Extras</div>
<form method="post" action="/review/update-payroll-note" class="inline" style="display:flex;gap:12px;align-items:flex-end;flex-wrap:wrap;">
<input type="hidden" name="timesheet_id" value="{{ timesheet_id }}">
<input type="hidden" name="employee_id" value="{{ employee.id }}">
<div style="display:grid;gap:6px;min-width:220px;">
<label class="label">Reimbursement (optional)</label>
<input class="input" name="reimbursement_amount" type="text" placeholder="$0.00"
value="{% if payroll_note and payroll_note.reimbursement_amount is not none %}{{ '%.2f'|format(payroll_note.reimbursement_amount) }}{% endif %}"
{% if not can_edit %}disabled{% endif %}>
</div>
<div style="display:grid;gap:6px;min-width:220px;">
<label class="label">Additional Payroll Changes (optional)</label>
<input class="input" name="additional_payroll_amount" type="text" placeholder="$0.00"
value="{% if payroll_note and payroll_note.additional_payroll_amount is not none %}{{ '%.2f'|format(payroll_note.additional_payroll_amount) }}{% endif %}"
{% if not can_edit %}disabled{% endif %}>
</div>
<div style="display:grid;gap:6px;min-width:320px;flex:1;">
<label class="label">Notes</label>
<textarea class="input" name="notes" rows="2" placeholder="Notes for payroll team..." {% if not can_edit %}disabled{% endif %}>{{ payroll_note.notes if payroll_note and payroll_note.notes else '' }}</textarea>
</div>
<div>
<button type="submit" class="btn primary" {% if not can_edit %}disabled{% endif %}>Save Extras</button>
</div>
</form>
<div class="muted" style="margin-top:8px;">Saved values appear in the Overview Excel export.</div>
</div>
<!-- /Payroll extras -->
{% if holiday_needs is defined and holiday_needs|length > 0 %}
<div class="alert warn" style="margin-bottom:12px;">
<div class="mb-8"><strong>Holiday review required{% if employee %} for {{ employee.name }}{% endif %}:</strong> The dates below include Holiday hours.</div>
<ul class="dup-list">
{% for h in holiday_needs %}
<li>{{ h.work_date.strftime("%A, %B %d, %Y") }} &ndash; Holiday {{ h.holiday_hours|fmt2 }} hr(s)</li>
{% endfor %}
</ul>
{% if timesheet_id and employee and can_edit %}
<div class="mt-8 inline" style="gap:8px;">
<form method="post" action="/review/review-holiday-needs">
<input type="hidden" name="employee_id" value="{{ employee.id }}">
<input type="hidden" name="timesheet_id" value="{{ timesheet_id }}">
<button class="btn" type="submit">Reviewed</button>
</form>
</div>
{% endif %}
<div class="mt-8">Holiday rows are highlighted in the grid until you click Reviewed. Saving rows does not clear highlights.</div>
</div>
{% endif %}
<div class="table-wrap">
<table class="table" style="width:100%;">
<thead>
<tr>
<th>DATE</th>
<th>CLOCK IN</th>
<th>CLOCK OUT</th>
<th>BREAK</th>
<th>TOTAL</th>
<th>PTO</th>
<th>PTO TYPE</th>
<th>HOLIDAY</th>
<th>OTHER</th>
<th>PAID</th>
<th></th>
</tr>
</thead>
<tbody>
{% set has_holiday_flags = flagged_holiday_dates is defined and reviewed_holiday_dates is defined %}
{% for r in grouped.rows %}
<form id="row-{{ r.entry_id }}" action="/timesheet/update-entry" method="post"></form>
{% set highlight_holiday = (has_holiday_flags and (r.work_date in flagged_holiday_dates and r.work_date not in reviewed_holiday_dates)) or (not has_holiday_flags and (r.holiday_hours and r.holiday_hours > 0)) %}
<tr id="row-tr-{{ r.entry_id }}" class="{% if highlight_holiday %}row-holiday{% endif %}" data-entry-id="{{ r.entry_id }}" tabindex="0">
<td class="mono">
{{ r.work_date.strftime("%b %d, %Y") }}
{% if highlight_holiday %}
<span class="badge holiday" title="This entry has Holiday hours and needs review.">Holiday &ndash; Needs review</span>
{% endif %}
</td>
<td id="ci-{{ r.entry_id }}" class="mono clock-cell"
title="{{ r.clock_in|fmt_dt }}"
data-ci="{{ r.clock_in|fmt_excel_dt }}"
data-date="{{ r.work_date.strftime('%Y-%m-%d') }}">
<div class="clock-row">
<span class="clock-text">
{% if r.holiday_hours and r.holiday_hours > 0 %}
Holiday
{% elif r.pto_type %}
{{ r.pto_type }}
{% else %}
{{ r.clock_in|fmt_excel_dt if r.clock_in else '-' }}
{% endif %}
</span>
<button type="button" class="icon-btn calendar" title="Pick date & time"
onclick="if(window.can_edit){openClockPicker(this, {{ r.entry_id }}, 'clock_in')}"
{% if not can_edit %}disabled{% endif %}>
<svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true">
<rect x="3" y="4" width="18" height="18" rx="3" ry="3" fill="none" stroke="currentColor" stroke-width="2"/>
<line x1="3" y1="10" x2="21" y2="10" stroke="currentColor" stroke-width="2"/>
<line x1="8" y1="2" x2="8" y2="6" stroke="currentColor" stroke-width="2"/>
<line x1="16" y1="2" x2="16" y2="6" stroke="currentColor" stroke-width="2"/>
</svg>
</button>
</div>
<input class="clock-input" id="ci-input-{{ r.entry_id }}" type="datetime-local"
value="{{ r.clock_in.strftime('%Y-%m-%dT%H:%M') if r.clock_in else '' }}"
onchange="onClockChanged({{ r.entry_id }}, 'clock_in', this.value)" />
</td>
<td id="co-{{ r.entry_id }}" class="mono clock-cell"
title="{{ r.clock_out|fmt_dt }}"
data-co="{{ r.clock_out|fmt_excel_dt }}"
data-date="{{ r.work_date.strftime('%Y-%m-%d') }}">
<div class="clock-row">
<span class="clock-text">
{% if r.holiday_hours and r.holiday_hours > 0 %}
Holiday
{% elif r.pto_type %}
{{ r.pto_type }}
{% else %}
{{ r.clock_out|fmt_excel_dt if r.clock_out else '-' }}
{% endif %}
</span>
<button type="button" class="icon-btn calendar" title="Pick date & time"
onclick="if(window.can_edit){openClockPicker(this, {{ r.entry_id }}, 'clock_out')}"
{% if not can_edit %}disabled{% endif %}>
<svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true">
<rect x="3" y="4" width="18" height="18" rx="3" ry="3" fill="none" stroke="currentColor" stroke-width="2"/>
<line x1="3" y1="10" x2="21" y2="10" stroke="currentColor" stroke-width="2"/>
<line x1="8" y1="2" x2="8" y2="6" stroke="currentColor" stroke-width="2"/>
<line x1="16" y1="2" x2="16" y2="6" stroke="currentColor" stroke-width="2"/>
</svg>
</button>
</div>
<input class="clock-input" id="co-input-{{ r.entry_id }}" type="datetime-local"
value="{{ r.clock_out.strftime('%Y-%m-%dT%H:%M') if r.clock_out else '' }}"
onchange="onClockChanged({{ r.entry_id }}, 'clock_out', this.value)" />
</td>
<td class="num">
<input form="row-{{ r.entry_id }}" type="hidden" name="entry_id" value="{{ r.entry_id }}">
<input form="row-{{ r.entry_id }}" type="hidden" name="timesheet_id" value="{{ timesheet_id }}">
<input form="row-{{ r.entry_id }}" type="hidden" name="employee_id" value="{{ employee.id }}">
<input class="input" form="row-{{ r.entry_id }}" type="number" step="0.01" name="break_hours" value="{{ r.break_hours|fmt2 }}" {% if not can_edit %}readonly disabled{% endif %}>
</td>
<td class="num">
<input class="input total-hours" form="row-{{ r.entry_id }}" type="number" step="0.01" name="total_hours" value="{{ r.total_hours|fmt2 }}" {% if not can_edit %}readonly disabled{% endif %}>
</td>
<td class="num">
<input class="input" form="row-{{ r.entry_id }}" type="number" step="0.01" name="pto_hours" value="{{ r.pto_hours|fmt2 }}" {% if not can_edit %}readonly disabled{% endif %}>
</td>
<td>
<select class="select" form="row-{{ r.entry_id }}" name="pto_type"
onchange="onPtoTypeChange(this, {{ r.entry_id }})" {% if not can_edit %}disabled{% endif %}>
<option value="" {% if not r.pto_type %}selected{% endif %}></option>
<option value="PTO" {% if r.pto_type == 'PTO' %}selected{% endif %}>PTO</option>
<option value="Sick" {% if r.pto_type == 'Sick' %}selected{% endif %}>Sick</option>
<option value="Off" {% if r.pto_type == 'Off' %}selected{% endif %}>Off</option>
</select>
</td>
<td class="num">
<input class="input" form="row-{{ r.entry_id }}" type="number" step="0.01" name="holiday_hours" value="{{ r.holiday_hours|fmt2 }}" {% if not can_edit %}readonly disabled{% endif %}>
</td>
<td class="num">
<input class="input" form="row-{{ r.entry_id }}" type="number" step="0.01" name="bereavement_hours" value="{{ r.bereavement_hours|fmt2 }}" {% if not can_edit %}readonly disabled{% endif %}>
</td>
<td class="num mono paid-cell">{{ r.hours_paid|fmt2 }}</td>
<td>
<input form="row-{{ r.entry_id }}" type="hidden" name="redirect_to" value="/review/edit?timesheet_id={{ timesheet_id }}&employee_id={{ employee.id }}">
{% if can_edit %}
<button type="button" class="btn primary sm" onclick="saveEntry(event, {{ r.entry_id }})">Save</button>
{% else %}
<button type="button" class="btn primary sm" disabled>Save</button>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="panel totals" id="summary-tiles" style="width:100%;">
<div class="total"><div class="t-label">Regular</div><div class="t-val">{{ grouped.totals.regular|fmt2 }}</div></div>
<div class="total"><div class="t-label">Overtime</div><div class="t-val">{{ grouped.totals.overtime|fmt2 }}</div></div>
<div class="total"><div class="t-label">PTO</div><div class="t-val">{{ grouped.totals.pto|fmt2 }}</div></div>
<div class="total"><div class="t-label">Holiday</div><div class="t-val">{{ grouped.totals.holiday|fmt2 }}</div></div>
<div class="total"><div class="t-label">Other</div><div class="t-val">{{ grouped.totals.bereavement|fmt2 }}</div></div>
<div class="total"><div class="t-label">Paid Total</div><div class="t-val">{{ grouped.totals.paid_total|fmt2 }}</div></div>
</div>
</div>
</div>
<iframe id="print-iframe" style="position:fixed; inset:0; width:0; height:0; border:0; visibility:hidden;"></iframe>
<div id="clock-picker" class="picker-popover" hidden>
<div class="picker-header">
<div class="picker-title">Set date & time</div>
<div class="picker-actions">
<button class="btn sm" type="button" onclick="closeClockPicker()">Cancel</button>
<button id="picker-apply" class="btn primary sm" type="button" onclick="applyClockPicker()">Apply</button>
</div>
</div>
<div class="picker-body">
<div class="picker-row">
<label>Date</label>
<input id="picker-date" type="date">
</div>
<div class="picker-row">
<label>Time</label>
<input id="picker-time" type="time" step="60">
</div>
</div>
</div>
<div id="row-menu" class="ctx-menu" role="menu" hidden>
{% if can_edit %}
<button type="button" id="ctx-delete" class="ctx-item danger" role="menuitem">Delete row</button>
{% else %}
<button type="button" class="ctx-item danger" role="menuitem" disabled>Delete row</button>
{% endif %}
</div>
<div id="toast" class="toast" hidden>Time entry saved</div>
<style>
.table thead th { text-align: center; }
.table thead th.num { text-align: center; }
.table tbody td.num, .table tfoot td.num { text-align: right; }
.row-holiday { background: #fff6e6; }
.badge.holiday { display:inline-block; padding:2px 6px; font-size:11px; border-radius:10px; border:1px solid #ffc470; color:#8a5200; background:#fff4e0; margin-left: 6px; }
.clock-cell { white-space: nowrap; overflow: visible; }
.clock-row { display: grid; grid-template-columns: 1fr 28px; align-items: center; column-gap: 6px; }
.clock-text { display: block; width: 100%; min-width: 0; text-align: center; overflow: hidden; text-overflow: ellipsis; }
.icon-btn.calendar { border:0; background:#f8fafc; color:#64748b; cursor:pointer; width:28px; height:28px; border-radius:8px; display:inline-flex; align-items:center; justify-content:center; border:1px solid var(--line); }
.icon-btn.calendar:hover { color:#0f172a; filter:brightness(1.02); }
.icon-btn.calendar[disabled] { opacity: 0.5; cursor: not-allowed; }
.clock-input { display:none; }
.picker-popover { position:fixed; z-index:1000; width:280px; background:#fff; border:1px solid var(--line); border-radius:8px; box-shadow:0 8px 24px rgba(2,6,23,0.15); overflow:hidden; }
.picker-header { display:flex; align-items:center; justify-content:space-between; gap:8px; position:sticky; top:0; background:#fff; border-bottom:1px solid var(--line); padding:8px; }
.picker-title { font-size:12px; color:#475569; font-weight:600; }
.picker-actions { display:flex; gap:8px; }
.picker-body { padding:10px; }
.picker-row input { flex:1; min-height:32px; }
.toast { position:fixed; right:16px; bottom:16px; background:#10b981; color:#fff; padding:10px 14px; border-radius:8px; box-shadow:0 8px 24px rgba(2,6,23,0.25); opacity:0; transform:translateY(6px); transition:opacity .2s ease, transform .2s ease; z-index:1500; }
.toast.show { opacity:1; transform:translateY(0); }
.table-wrap { overflow-x:auto; }
</style>
<script>
function openPrint(url) {
const iframe = document.getElementById('print-iframe');
iframe.src = url;
}
window.addEventListener('message', function (e) {
if (e && e.data && e.data.type === 'close-print') {
const iframe = document.getElementById('print-iframe');
if (iframe) iframe.removeAttribute('src');
}
});
function onPtoTypeChange(selectEl, entryId) {
const ciCell = document.getElementById('ci-' + entryId);
const coCell = document.getElementById('co-' + entryId);
if (!ciCell || !coCell) return;
const type = (selectEl.value || '').trim();
const ciTextEl = ciCell.querySelector('.clock-text');
const coTextEl = coCell.querySelector('.clock-text');
const row = document.getElementById('row-tr-' + entryId);
const holInput = row ? row.querySelector('input[name="holiday_hours"]') : null;
const holVal = holInput ? parseFloat(holInput.value || '0') : 0;
if (holVal > 0) {
if (ciTextEl) ciTextEl.textContent = 'Holiday';
if (coTextEl) coTextEl.textContent = 'Holiday';
return;
}
if (type) {
if (ciTextEl) ciTextEl.textContent = type;
if (coTextEl) coTextEl.textContent = type;
} else {
const ci = ciCell.getAttribute('data-ci') || '-';
const co = coCell.getAttribute('data-co') || '-';
if (ciTextEl) ciTextEl.textContent = ci;
if (coTextEl) coTextEl.textContent = co;
}
}
function num(v){const n=parseFloat(v);return isFinite(n)?n:0;}
function fmt2(v){return (Math.round(num(v)*100)/100).toFixed(2);}
// FIX: read PTO type from the table row (not the empty <form>)
function getPtoType(entryId){
const row = document.getElementById('row-tr-' + entryId);
const sel = row ? row.querySelector('select[name="pto_type"]') : null;
return (sel && sel.value ? sel.value.trim() : '');
}
let toastTimer = null;
function showToast(msg) {
const t = document.getElementById('toast');
if (!t) return;
t.textContent = msg || 'Saved';
t.hidden = false;
t.classList.add('show');
clearTimeout(toastTimer);
toastTimer = setTimeout(() => {
t.classList.remove('show');
setTimeout(() => { t.hidden = true; }, 200);
}, 1800);
}
async function refreshSummary(timesheetId, employeeId) {
try {
const url = `/review/edit?timesheet_id=${encodeURIComponent(timesheetId)}&employee_id=${encodeURIComponent(employeeId)}`;
const res = await fetch(url, { credentials: 'same-origin' });
if (!res.ok) return;
const html = await res.text();
const doc = new DOMParser().parseFromString(html, 'text/html');
const newTiles = doc.querySelector('#summary-tiles');
const curTiles = document.querySelector('#summary-tiles');
if (newTiles && curTiles) curTiles.innerHTML = newTiles.innerHTML;
} catch (e) {
console.warn('refreshSummary failed', e);
}
}
let pickerState = { anchor: null, entryId: null, field: null };
let pickerOutsideHandler = null;
function placePopover(anchorBtn) {
const pop = document.getElementById('clock-picker');
const rect = anchorBtn.getBoundingClientRect();
const vpW = window.innerWidth || document.documentElement.clientWidth;
const vpH = window.innerHeight || document.documentElement.clientHeight;
pop.style.visibility = 'hidden';
pop.hidden = false;
const measure = pop.getBoundingClientRect();
const popW = measure.width || 280;
const popH = measure.height || 220;
const belowTop = rect.bottom + 8;
const aboveTop = rect.top - popH - 8;
const top = (belowTop + popH + 16 <= vpH) ? belowTop : Math.max(8, aboveTop);
const left = Math.min(vpW - popW - 8, rect.right + 8);
pop.style.left = left + 'px';
pop.style.top = top + 'px';
pop.style.visibility = 'visible';
}
function openClockPicker(anchorBtn, entryId, field) {
const pop = document.getElementById('clock-picker');
const dateInput = document.getElementById('picker-date');
const timeInput = document.getElementById('picker-time');
pickerState.anchor = anchorBtn;
pickerState.entryId = entryId;
pickerState.field = field;
const cellId = (field === 'clock_in' ? 'ci-' : 'co-') + entryId;
const theHiddenId = (field === 'clock_in' ? 'ci-input-' : 'co-input-') + entryId;
const cell = document.getElementById(cellId);
const hidden = document.getElementById(theHiddenId);
const baseDate = cell ? (cell.getAttribute('data-date') || '') : '';
let dval = '', tval = '';
if (hidden && hidden.value) {
const parts = hidden.value.split('T');
dval = parts[0] || baseDate || '';
tval = parts[1] || '';
} else {
dval = baseDate || '';
tval = '08:00';
}
dateInput.value = dval;
timeInput.value = tval;
placePopover(anchorBtn);
setTimeout(() => {
pickerOutsideHandler = (e) => {
const popNow = document.getElementById('clock-picker');
if (!popNow.hidden && !popNow.contains(e.target) && e.target !== anchorBtn) {
closeClockPicker();
document.removeEventListener('mousedown', pickerOutsideHandler);
pickerOutsideHandler = null;
}
};
document.addEventListener('mousedown', pickerOutsideHandler);
}, 0);
}
function closeClockPicker() {
const pop = document.getElementById('clock-picker');
pop.hidden = true;
pickerState = { anchor: null, entryId: null, field: null };
if (pickerOutsideHandler) {
document.removeEventListener('mousedown', pickerOutsideHandler);
pickerOutsideHandler = null;
}
}
function applyClockPicker() {
const dateInput = document.getElementById('picker-date');
const timeInput = document.getElementById('picker-time');
const dval = (dateInput.value || '').trim();
const tval = (timeInput.value || '').trim();
if (!dval || !tval || !pickerState.entryId || !pickerState.field) {
closeClockPicker();
return;
}
const newIso = dval + 'T' + tval;
const otherHiddenId = (pickerState.field === 'clock_in' ? 'co-input-' : 'ci-input-') + pickerState.entryId;
const otherHidden = document.getElementById(otherHiddenId);
const otherVal = otherHidden ? (otherHidden.value || '').trim() : '';
if (otherVal) {
const newDt = new Date(newIso);
const otherDt = new Date(otherVal);
if (pickerState.field === 'clock_in' && newDt >= otherDt) { alert('Clock In must be before Clock Out for the same entry.'); return; }
if (pickerState.field === 'clock_out' && newDt <= otherDt) { alert('Clock Out must be after Clock In for the same entry.'); return; }
}
const hiddenId = (pickerState.field === 'clock_in' ? 'ci-input-' : 'co-input-') + pickerState.entryId;
const hidden = document.getElementById(hiddenId);
if (hidden) hidden.value = newIso;
onClockChanged(pickerState.entryId, pickerState.field, newIso);
closeClockPicker();
}
async function onClockChanged(entryId, field, value) {
if (!window.can_edit) { showToast('Read-only mode'); return; }
const row = document.getElementById('row-tr-' + entryId);
const breakVal = row ? ((row.querySelector('input[name="break_hours"]') || {}).value || '') : '';
const ptoVal = row ? ((row.querySelector('input[name="pto_hours"]') || {}).value || '') : '';
const holVal = row ? ((row.querySelector('input[name="holiday_hours"]') || {}).value || '') : '';
const othVal = row ? ((row.querySelector('input[name="bereavement_hours"]') || {}).value || '') : '';
const fd = new FormData();
fd.append('entry_id', entryId);
if (field === 'clock_in') fd.append('clock_in', value);
if (field === 'clock_out') fd.append('clock_out', value);
fd.append('break_hours', breakVal);
fd.append('pto_hours', ptoVal);
fd.append('holiday_hours', holVal);
fd.append('bereavement_hours', othVal);
try {
const res = await fetch('/timesheet/update-clocks', {
method: 'POST',
headers: { 'Accept': 'application/json' },
credentials: 'same-origin',
body: fd
});
if (!res.ok) {
let err = '';
try { const e = await res.json(); err = e && e.error ? e.error : JSON.stringify(e); } catch {}
alert('Unable to update clock time (' + res.status + '). ' + (err || 'Please try again.'));
return;
}
const data = await res.json();
const ciCell = document.getElementById('ci-' + entryId);
const coCell = document.getElementById('co-' + entryId);
if (parseFloat(holVal || '0') > 0) {
const ciText = ciCell ? ciCell.querySelector('.clock-text') : null;
const coText = coCell ? coCell.querySelector('.clock-text') : null;
if (ciText) ciText.textContent = 'Holiday';
if (coText) coText.textContent = 'Holiday';
row.classList.add('row-holiday');
const dateCell = row.querySelector('td:first-child');
if (dateCell && !dateCell.querySelector('.badge.holiday')) {
const b = document.createElement('span');
b.className = 'badge holiday';
b.title = 'This entry has Holiday hours and needs review.';
b.textContent = 'Holiday - Needs review';
dateCell.appendChild(document.createTextNode(' '));
dateCell.appendChild(b);
}
} else {
const ptoType = getPtoType(entryId);
if (ptoType) {
const ciText = ciCell ? ciCell.querySelector('.clock-text') : null;
const coText = coCell ? coCell.querySelector('.clock-text') : null;
if (ciText) ciText.textContent = ptoType;
if (coText) coText.textContent = ptoType;
} else {
if (data.clock_in_fmt && ciCell) {
const t = ciCell.querySelector('.clock-text'); if (t) t.textContent = data.clock_in_fmt;
ciCell.setAttribute('data-ci', data.clock_in_fmt);
}
if (data.clock_out_fmt && coCell) {
const t = coCell.querySelector('.clock-text'); if (t) t.textContent = data.clock_out_fmt;
coCell.setAttribute('data-co', data.clock_out_fmt);
}
}
row.classList.remove('row-holiday');
const dateCell = row.querySelector('td:first-child');
if (dateCell) {
const badge = dateCell.querySelector('.badge.holiday');
if (badge) badge.remove();
}
}
if (row) {
const totalInput = row.querySelector('input.total-hours');
if (totalInput && typeof data.total_hours_fmt === 'string') totalInput.value = data.total_hours_fmt;
const paidCell = row.querySelector('.paid-cell');
if (paidCell && typeof data.hours_paid_fmt === 'string') paidCell.textContent = data.hours_paid_fmt;
}
await refreshSummary('{{ timesheet_id }}', '{{ employee.id }}');
showToast('Time updated');
} catch (e) {
console.error('Clock update failed', e);
alert('Unable to update clock time. Please try again.');
}
}
async function saveEntry(ev, entryId) {
if (ev) ev.preventDefault();
if (!window.can_edit) { showToast('Read-only mode'); return; }
const form = document.getElementById('row-' + entryId);
if (!form) return;
const fd = new FormData(form);
try {
const res = await fetch('/timesheet/update-entry', {
method: 'POST',
credentials: 'same-origin',
body: fd
});
if (!res.ok) { showToast('Save failed (' + res.status + ')'); return; }
const row = document.getElementById('row-tr-' + entryId);
if (row) {
['break_hours','total_hours','pto_hours','holiday_hours','bereavement_hours'].forEach(name=>{
const inp = row.querySelector('input[name="'+name+'"]');
if (inp) inp.value = fmt2(inp.value);
});
const total = num((row.querySelector('input[name="total_hours"]')||{}).value);
const brk = num((row.querySelector('input[name="break_hours"]')||{}).value);
const pto = num((row.querySelector('input[name="pto_hours"]')||{}).value);
const hol = num((row.querySelector('input[name="holiday_hours"]')||{}).value);
const oth = num((row.querySelector('input[name="bereavement_hours"]')||{}).value);
const paid = Math.max(0, total - brk) + pto + hol + oth;
const paidCell = row.querySelector('.paid-cell');
if (paidCell) paidCell.textContent = fmt2(paid);
const ciCell = document.getElementById('ci-' + entryId);
const coCell = document.getElementById('co-' + entryId);
const ciText = ciCell ? ciCell.querySelector('.clock-text') : null;
const coText = coCell ? coCell.querySelector('.clock-text') : null;
const ptoType = getPtoType(entryId);
if (hol > 0) {
if (ciText) ciText.textContent = 'Holiday';
if (coText) coText.textContent = 'Holiday';
row.classList.add('row-holiday');
const dateCell = row.querySelector('td:first-child');
if (dateCell && !dateCell.querySelector('.badge.holiday')) {
const b = document.createElement('span');
b.className = 'badge holiday';
b.title = 'This entry has Holiday hours and needs review.';
b.textContent = 'Holiday - Needs review';
dateCell.appendChild(document.createTextNode(' '));
dateCell.appendChild(b);
}
} else if (ptoType) {
if (ciText) ciText.textContent = ptoType;
if (coText) coText.textContent = ptoType;
row.classList.remove('row-holiday');
const dateCell = row.querySelector('td:first-child');
if (dateCell) { const badge = dateCell.querySelector('.badge.holiday'); if (badge) badge.remove(); }
} else {
if (ciText) ciText.textContent = (ciCell ? (ciCell.getAttribute('data-ci') || '-') : '-');
if (coText) coText.textContent = (coCell ? (coCell.getAttribute('data-co') || '-') : '-');
row.classList.remove('row-holiday');
const dateCell = row.querySelector('td:first-child');
if (dateCell) { const badge = dateCell.querySelector('.badge.holiday'); if (badge) badge.remove(); }
}
}
await refreshSummary('{{ timesheet_id }}', '{{ employee.id }}');
showToast('Time entry saved');
} catch (e) {
console.error('saveEntry error', e);
showToast('Save failed');
}
}
function saveCarryOverEdit(ev) {
if (ev) ev.preventDefault();
if (!window.can_edit) { showToast('Read-only mode'); return false; }
const form = document.getElementById('carry-over-form-edit');
if (!form) return false;
const fd = new FormData(form);
fetch('/viewer/update-employee-period', {
method: 'POST',
credentials: 'same-origin',
body: fd
}).then(async (res) => {
if (!res.ok) { showToast('Save failed ('+res.status+')'); return; }
await refreshSummary(fd.get('timesheet_id'), fd.get('employee_id'));
showToast('Carry over saved');
}).catch((e) => {
console.error('carry-over save error', e);
showToast('Save failed');
});
return false;
}
</script>
{% endblock %}