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

1146 lines
52 KiB
HTML

{% extends "layout.html" %}
{% block content %}
{% if all_done %}
<div class="page-wide" style="display:flex;justify-content:center;align-items:flex-start;padding-top:40px;">
<div class="panel" style="width:720px; max-width:90%; padding:20px 24px;">
<div class="panel-title">Time Period Complete</div>
<div class="alert success" style="margin-bottom:16px; font-size:16px;">{{ flash }}</div>
<div style="display:flex;gap:12px;flex-wrap:wrap;">
<a class="btn" href="/review?timesheet_id={{ active_ts }}">Review Timesheets</a>
{% if can_edit %}
<a class="btn" href="/upload">Import New Time Period</a>
{% endif %}
</div>
</div>
</div>
{% else %}
<div class="page-wide">
<div class="editor-grid">
<!-- Sidebar sized to names -->
<aside class="panel sidebar">
<div class="panel-title">Employees</div>
<ul class="emp-list">
{% for e in employees %}
<!-- Add data-employee-id to enable right-click removal -->
<li data-employee-id="{{ e.id }}" class="{% if selected_employee and e.id == selected_employee.id %}active{% endif %}">
<a href="/viewer?timesheet_id={{ active_ts }}&employee_id={{ e.id }}" title="{{ e.name }}">{{ e.name }}</a>
</li>
{% endfor %}
{% if not employees or employees|length == 0 %}
<li class="empty">No employees yet for this timesheet.</li>
{% endif %}
</ul>
<div class="divider"></div>
<div class="panel-title">Actions</div>
<div class="actions">
{% if active_ts and can_edit %}
<a class="btn w-full btn-md btn-text-left" id="reassign-weeks-btn"
href="/assign-weeks?timesheet_id={{ active_ts }}">
Reassign weeks
</a>
<!-- Import another department -->
<a class="btn w-full" id="import-dept-btn" href="/import/department?timesheet_id={{ active_ts }}">
Import Department
</a>
<!-- Undo last department import -->
<form method="post" action="/import/department/undo-last" class="inline w-full"
onsubmit="return confirm('Undo last department import for this time period?');">
<input type="hidden" name="timesheet_id" value="{{ active_ts }}">
<button class="btn w-full btn-md btn-text-left" id="undo-dept-btn" type="submit">
Undo Department Import
</button>
</form>
{% endif %}
</div>
</aside>
<section class="panel main">
{% if flash %}<div class="alert success">{{ flash }}</div>{% endif %}
<!-- Toolbar -->
<div class="panel toolbar" style="gap:16px;flex-wrap:wrap;align-items:center;">
<form method="get" action="/viewer" class="inline" style="display:flex;align-items:center;gap:8px;">
{% if selected_employee %}
<input type="hidden" name="employee_id" value="{{ selected_employee.id }}">
{% endif %}
<label class="label mr-8">Time Period</label>
<select name="timesheet_id" class="select">
{% for p in period_options %}
<option value="{{ p.timesheet_id }}" {% if p.timesheet_id == active_ts %}selected{% endif %}>
{{ p.display }}
</option>
{% endfor %}
</select>
<button type="submit" class="btn primary btn-md btn-text-center">Load</button>
</form>
{% if active_ts and can_edit %}
<form method="post" action="/viewer/delete-period"
onsubmit="return confirm('Delete this entire time period instance (including department imports)? This cannot be undone.');"
class="inline" style="display:flex;align-items:center;">
<input type="hidden" name="timesheet_id" value="{{ active_ts }}">
<button class="btn danger btn-md btn-text-center" type="submit">Delete Time Period</button>
</form>
{% endif %}
{% if active_ts and selected_employee and can_edit %}
<form method="post" action="/viewer/submit" class="inline">
<input type="hidden" name="employee_id" value="{{ selected_employee.id }}">
<input type="hidden" name="timesheet_id" value="{{ active_ts }}">
<button type="submit" class="btn primary btn-md btn-text-center">Submit Timesheets</button>
</form>
{% endif %}
{% if can_edit %}
<a class="btn btn-md btn-text-center" href="/upload">Import New Time Period</a>
{% endif %}
</div>
{% if active_ts and selected_employee %}
<!-- Employee settings: Carry over hours -->
<div class="panel" style="margin-top:12px;">
<div class="panel-title">Employee Settings</div>
<form id="carry-over-form-viewer" method="post" action="/viewer/update-employee-period"
class="inline" style="display:flex;gap:8px;align-items:center;flex-wrap:wrap;"
onsubmit="return saveCarryOverViewer(event)">
<input type="hidden" name="employee_id" value="{{ selected_employee.id }}">
<input type="hidden" name="timesheet_id" value="{{ active_ts }}">
<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="{{ employee_setting.carry_over_hours|fmt2 }}" {% if not can_edit %}disabled{% endif %}>
<button class="btn btn-md btn-text-center" 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>
</div>
{% endif %}
{% if employees and selected_employee and grouped %}
{% if duplicates and duplicates|length > 0 %}
<div class="alert warn">
<div class="mb-8"><strong>Duplicate dates detected{% if selected_employee %} for {{ selected_employee.name }}{% endif %}:</strong></div>
<ul class="dup-list">
{% for d in duplicates %}
<li>{{ d.date.strftime("%A, %B %d, %Y") }} - {{ d.count }} rows</li>
{% endfor %}
</ul>
{% if selected_employee and active_ts and can_edit %}
<div class="mt-8 inline" style="gap:8px;">
<form method="post" action="/viewer/keep-duplicates">
<input type="hidden" name="employee_id" value="{{ selected_employee.id }}">
<input type="hidden" name="timesheet_id" value="{{ active_ts }}">
<button type="submit" class="btn btn-md btn-text-center">Reviewed</button>
</form>
</div>
{% endif %}
<div class="mt-8">Duplicate rows are highlighted in the grid until you click Reviewed.</div>
</div>
{% endif %}
{% if pto_needs and pto_needs|length > 0 %}
<div class="alert warn" style="margin-top:12px;">
<div class="mb-8"><strong>PTO review required{% if selected_employee %} for {{ selected_employee.name }}{% endif %}:</strong> Missing PTO type on the dates below.</div>
<ul class="dup-list">
{% for r in pto_needs %}
<li>{{ r.work_date.strftime("%A, %B %d, %Y") }} &ndash; PTO {{ r.pto_hours|fmt2 }} hr(s)</li>
{% endfor %}
</ul>
{% if selected_employee and active_ts and can_edit %}
<div class="mt-8 inline" style="gap:8px;">
<form method="post" action="/viewer/review-pto-needs">
<input type="hidden" name="employee_id" value="{{ selected_employee.id }}">
<input type="hidden" name="timesheet_id" value="{{ active_ts }}">
<button class="btn btn-md btn-text-center" type="submit">Reviewed</button>
</form>
</div>
{% endif %}
<div class="mt-8">Rows needing PTO type stay highlighted until you click Reviewed. Saving rows does not clear highlights.</div>
</div>
{% endif %}
{% if holiday_needs is defined and holiday_needs|length > 0 %}
<div class="alert warn" style="margin-top:12px;">
<div class="mb-8"><strong>Holiday review required{% if selected_employee %} for {{ selected_employee.name }}{% endif %}:</strong> Dates below include Holiday hours.</div>
<ul class="dup-list">
{% for r in holiday_needs %}
<li>{{ r.work_date.strftime("%A, %B %d, %Y") }} &ndash; Holiday {{ r.holiday_hours|fmt2 }} hr(s)</li>
{% endfor %}
</ul>
{% if selected_employee and active_ts and can_edit %}
<div class="mt-8 inline" style="gap:8px;">
<form method="post" action="/viewer/review-holiday-needs">
<input type="hidden" name="employee_id" value="{{ selected_employee.id }}">
<input type="hidden" name="timesheet_id" value="{{ active_ts }}">
<button class="btn btn-md btn-text-center" type="submit">Reviewed</button>
</form>
</div>
{% endif %}
<div class="mt-8">Holiday rows are highlighted until you click Reviewed. Saving rows does not clear highlights.</div>
</div>
{% endif %}
{% if long_shift_needs and long_shift_needs|length > 0 %}
<div class="alert warn" style="margin-top:12px;">
<div class="mb-8"><strong>Long shifts detected{% if selected_employee %} for {{ selected_employee.name }}{% endif %} (over 10 hours):</strong></div>
<ul class="dup-list">
{% for r in long_shift_needs %}
<li>{{ r.work_date.strftime("%A, %B %d, %Y") }} &ndash; {{ r.total_hours|fmt2 }} hours</li>
{% endfor %}
</ul>
{% if selected_employee and active_ts and can_edit %}
<div class="mt-8 inline" style="gap:8px;">
<form method="post" action="/viewer/review-long-shifts">
<input type="hidden" name="employee_id" value="{{ selected_employee.id }}">
<input type="hidden" name="timesheet_id" value="{{ active_ts }}">
<button class="btn btn-md btn-text-center" type="submit">Reviewed</button>
</form>
</div>
{% endif %}
<div class="mt-8">Long-shift rows are highlighted in the grid until you click Reviewed.</div>
</div>
{% endif %}
<!-- Summary tiles -->
<div class="panel totals" id="summary-tiles">
<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">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">Overtime</div><div class="t-val">{{ grouped.totals.overtime|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>
<!-- Grid -->
<div class="panel">
<div class="table-wrap">
<table class="table" style="width:100%;">
<!-- Column widths -->
<colgroup>
<col style="width:160px;">
<col style="width:240px;">
<col style="width:240px;">
<col style="width:90px;">
<col style="width:90px;">
<col style="width:120px;">
<col style="width:90px;">
<col style="width:90px;">
<col style="width:90px;">
<col style="width:92px;">
<col style="width:120px;">
</colgroup>
<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_long = (r.work_date in flagged_long_dates and r.work_date not in reviewed_long_dates) %}
{% set highlight_pto = (r.work_date in flagged_pto_dates and r.work_date not in reviewed_pto_dates) %}
{% 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 r.work_date in dup_dates %}dup-row{% endif %} {% if highlight_long %}row-long{% endif %} {% if highlight_pto %}needs-pto{% endif %} {% if highlight_holiday %}row-holiday{% endif %}" data-entry-id="{{ r.entry_id }}" tabindex="0">
<td>
{{ r.work_date.strftime("%A, %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>
<!-- CLOCK IN with calendar -->
<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>
<!-- CLOCK OUT with calendar -->
<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>
<!-- BREAK -->
<td class="num col-break">
<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="{{ active_ts }}">
<input form="row-{{ r.entry_id }}" type="hidden" name="employee_id" value="{{ selected_employee.id if selected_employee else '' }}">
<input class="input compact" 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>
<!-- TOTAL -->
<td class="num col-total">
<input class="input compact 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>
<!-- PTO (numeric) -->
<td class="num col-pto">
<input class="input compact pto-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>
<!-- PTO TYPE (select) -->
<td class="col-pto-type">
<select class="select compact" 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>
<!-- HOLIDAY -->
<td class="num col-holiday">
<input class="input compact" 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>
<!-- OTHER -->
<td class="num col-other">
<input class="input compact" 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>
<div class="row-actions">
<input form="row-{{ r.entry_id }}" type="hidden" name="redirect_to" value="/viewer?timesheet_id={{ active_ts }}&employee_id={{ selected_employee.id if selected_employee else '' }}">
{% if can_edit %}
<button type="button" class="btn primary sm" onclick="saveEntry(event, {{ r.entry_id }})">Save</button>
<!-- Delete button removed: use right-click context menu -->
{% else %}
<button type="button" class="btn primary sm" disabled>Save</button>
{% endif %}
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<!-- Weekly summary (separate box under the table) -->
<div class="panel" id="weekly-summary">
<div class="table-wrap">
<table class="table compact" style="width:100%;">
<thead>
<tr>
<th></th>
<th class="num">Reg</th>
<th class="num">OT</th>
<th class="num">Week Total</th>
</tr>
</thead>
<tbody>
{% for w in grouped.weekly_summary %}
<tr>
<td>{{ w.label }}</td>
<td class="num mono">{{ w.reg|fmt2 }}</td>
<td class="num mono">{{ w.ot|fmt2 }}</td>
<td class="num mono">{{ w.all|fmt2 }}</td>
</tr>
{% endfor %}
<tr class="total">
<td>Total</td>
<td class="num mono">{{ (grouped.weekly_summary|sum(attribute='reg'))|fmt2 }}</td>
<td class="num mono">{{ (grouped.weekly_summary|sum(attribute='ot'))|fmt2 }}</td>
<td class="num mono">{{ (grouped.weekly_summary|sum(attribute='all'))|fmt2 }}</td>
</tr>
</tbody>
</table>
</div>
</div>
{% else %}
<div class="panel">
<div class="empty">No timesheet instances found.</div>
{% if can_edit %}
<div class="mt-8">
<a class="btn primary" href="/upload">Import New Time Period</a>
</div>
{% endif %}
</div>
{% endif %}
</section>
</div>
</div>
<!-- Date+Time picker popover (shared) -->
<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>
<!-- Row context menu (right-click to delete entries) -->
<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(s)</button>
{% else %}
<button type="button" class="ctx-item danger" role="menuitem" disabled>Delete row</button>
{% endif %}
</div>
<!-- NEW: Employee sidebar context menu (right-click to remove employee from period) -->
<div id="emp-menu" class="ctx-menu" role="menu" hidden>
{% if can_edit %}
<button type="button" id="ctx-emp-remove" class="ctx-item danger" role="menuitem">Remove employee from this time period</button>
{% else %}
<button type="button" class="ctx-item danger" role="menuitem" disabled>Remove employee</button>
{% endif %}
</div>
<!-- Toast -->
<div id="toast" class="toast" hidden>Time entry saved</div>
<style>
.table thead th { text-align: center; }
.table thead th.num { text-align: center; } /* default for main grid headers */
.table tbody td.num, .table tfoot td.num { text-align: right; }
.table { table-layout: fixed; }
.editor-grid { display: grid; grid-template-columns: minmax(220px, 340px) 1fr; gap: 12px; align-items: start; }
.panel.sidebar { padding: 12px; }
.emp-list { list-style: none; padding: 0; margin: 0; }
.emp-list li { margin: 0; }
.emp-list li a { display: block; padding: 6px 8px; font-size: 14px; line-height: 1.25; white-space: nowrap; }
.emp-list li.active a { font-weight: 600; }
.emp-list li.empty { padding: 6px 8px; }
.actions { display: flex; flex-direction: column; gap: 10px; }
.actions .btn.w-full { width: 100%; white-space: nowrap; }
.actions form { margin: 0; }
.panel.toolbar { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 12px; align-items: center; }
.panel.toolbar .inline { display: flex; gap: 8px; align-items: center; }
.panel.toolbar .inline .label { white-space: nowrap; }
.panel.toolbar .inline .select { flex: 1; min-width: 160px; }
.panel.toolbar .btn, .panel.toolbar a.btn { width: 100%; }
.btn { min-height: 36px; line-height: 1; padding: 8px 12px; }
.btn.sm { min-height: 28px; padding: 6px 10px; }
.btn-md { min-height: 36px; padding: 8px 12px; font-size: 14px; }
.btn-text-left { display:inline-flex; justify-content:flex-start; text-align:left; }
.btn-text-center{ display:inline-flex; justify-content:center; text-align:center; }
.btn-text-right { display:inline-flex; justify-content:flex-end; text-align:right; }
#import-dept-btn { background:#2563eb; border:1px solid #2563eb; color:#fff; }
#import-dept-btn:hover { filter:brightness(0.98); }
#undo-dept-btn { font-weight:600; border-radius:8px; letter-spacing:.2px; }
#reassign-weeks-btn { font-weight:600; border-radius:8px; letter-spacing:.2px; }
.badge { display:inline-block; padding:2px 6px; font-size:11px; border-radius:10px; border:1px solid #d08; color:#a0063a; background:#ffe5ef; }
.badge.warn { border-color:#d08; color:#a0063a; background:#ffe5ef; }
.row-holiday { background: #fff6e6; }
.badge.holiday { border: 1px solid #ffc470; color: #8a5200; background: #fff4e0; }
.needs-pto { background: #fff6d5; }
.row-long { background: #fff6d5; }
.dup-row { background: #fff6d5; }
.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; }
.input.compact, .select.compact { width: 100%; min-width: 0; padding: 2px 4px; }
.input.compact { text-align: right; }
.col-break .input.compact,
.col-total .input.compact,
.col-holiday .input.compact,
.col-other .input.compact { width: 10ch; }
.pto-input { width: 100%; }
.col-pto-type .select.compact { width: 100%; min-width: 0; }
.table th, .table td { overflow: hidden; }
.table td.clock-cell { overflow: visible; }
.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; }
.total-hours { background:#fff; }
.row-actions { display: grid; grid-template-columns: 1fr; gap: 6px; }
.ctx-menu { position: fixed; z-index: 2000; min-width: 160px; background:#fff; border:1px solid var(--line); border-radius:8px; box-shadow: 0 10px 24px rgba(2,6,23,0.18); padding:6px; }
.ctx-item { display:block; width:100%; text-align:left; padding:8px 10px; border:0; background:transparent; border-radius:6px; font-size:13px; cursor:pointer; }
.ctx-item:hover { background:#f3f4f6; }
.ctx-item.danger { color:#b91c1c; }
.ctx-item[disabled] { opacity:.5; cursor:not-allowed; }
.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 { display:flex; align-items:center; justify-content:space-between; gap:8px; margin-bottom:8px; }
.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; }
/* Align weekly summary headers with right-aligned numbers below */
#weekly-summary thead th.num { text-align: right; }
/* Multi-select row highlight */
.table tbody tr.is-selected {
outline: 2px solid #fdba74;
outline-offset: -2px;
background: #fff7ed;
}
@media (max-width: 1260px) { .editor-grid { grid-template-columns: minmax(220px, 300px) 1fr; } }
@media (max-width: 960px) { .editor-grid { grid-template-columns: minmax(180px, 260px) 1fr; } .clock-row { grid-template-columns: 1fr 28px; } .clock-text { width:auto; } .panel.toolbar { grid-template-columns: 1fr; } }
</style>
<script>
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');
// If Holiday hours are present, keep showing "Holiday" regardless of PTO type
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);}
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 = `/viewer?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;
const newWeekly = doc.querySelector('#weekly-summary');
const curWeekly = document.querySelector('#weekly-summary');
if (newWeekly && curWeekly) curWeekly.innerHTML = newWeekly.innerHTML;
} catch (e) {
console.warn('refreshSummary failed', e);
}
}
async function saveCarryOverViewer(ev) {
if (ev) ev.preventDefault();
if (!window.can_edit) { showToast('Read-only mode'); return false; }
const form = document.getElementById('carry-over-form-viewer');
if (!form) return false;
const fd = new FormData(form);
try {
const res = await fetch('/viewer/update-employee-period', {
method: 'POST',
credentials: 'same-origin',
body: fd
});
if (!res.ok) { showToast('Save failed (' + res.status + ')'); return false; }
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;
}
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 hiddenId = (field === 'clock_in' ? 'ci-input-' : 'co-input-') + entryId;
const cell = document.getElementById(cellId);
const hidden = document.getElementById(hiddenId);
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) => {
if (!pop.hidden && !pop.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';
} 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);
}
}
}
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;
}
const tsInput = document.querySelector('#row-'+entryId+' input[name="timesheet_id"]');
const empInput = document.querySelector('#row-'+entryId+' input[name="employee_id"]');
await refreshSummary(tsInput ? tsInput.value : '{{ active_ts }}',
empInput ? empInput.value : '{{ selected_employee.id if selected_employee else "" }}');
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(function(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';
} else if (ptoType) {
if (ciText) ciText.textContent = ptoType;
if (coText) coText.textContent = ptoType;
} else {
if (ciText) ciText.textContent = (ciCell ? (ciCell.getAttribute('data-ci') || '-') : '-');
if (coText) coText.textContent = (coCell ? (coCell.getAttribute('data-co') || '-') : '-');
}
}
const tsInput = document.querySelector('#row-'+entryId+' input[name="timesheet_id"]');
const empInput = document.querySelector('#row-'+entryId+' input[name="employee_id"]');
await refreshSummary(tsInput ? tsInput.value : '{{ active_ts }}',
empInput ? empInput.value : '{{ selected_employee.id if selected_employee else "" }}');
showToast('Time entry saved');
} catch (e) {
console.error('saveEntry error', e);
showToast('Save failed');
}
}
async function deleteEntry(ev, entryId) {
if (ev) ev.preventDefault();
if (!window.can_edit) return;
if (!confirm('Delete this time entry? This cannot be undone.')) return;
const row = document.getElementById('row-tr-' + entryId);
const tsInput = document.querySelector('#row-'+entryId+' input[name="timesheet_id"]');
const empInput = document.querySelector('#row-'+entryId+' input[name="employee_id"]');
const fd = new FormData();
fd.append('entry_id', entryId);
fd.append('timesheet_id', tsInput ? tsInput.value : '{{ active_ts }}');
try {
const res = await fetch('/timesheet/delete-entry', {
method: 'POST',
credentials: 'same-origin',
body: fd
});
if (!res.ok) {
alert('Delete failed (' + res.status + ').');
return;
}
const data = await res.json();
if (!data || !data.ok) {
alert('Delete failed.');
return;
}
if (row) row.remove();
await refreshSummary(tsInput ? tsInput.value : '{{ active_ts }}',
empInput ? empInput.value : '{{ selected_employee.id if selected_employee else "" }}');
showToast('Entry deleted');
} catch (e) {
console.error('delete entry error', e);
alert('Unable to delete entry. Please try again.');
}
}
/* Multi-select + bulk delete via right-click */
(function multiSelectAndBulkDelete() {
var gridBody = document.querySelector('.table tbody');
if (!gridBody) return;
function getRow(el) { return el && el.closest ? el.closest('tr[data-entry-id]') : null; }
function getId(row) { var id = row ? row.getAttribute('data-entry-id') : null; return id ? parseInt(id, 10) : null; }
function selectedRows() { return Array.from(gridBody.querySelectorAll('tr[data-entry-id].is-selected')); }
function clearSelection() { selectedRows().forEach(function(r){ r.classList.remove('is-selected'); }); }
// Click to toggle selection; Ctrl/Cmd supports additive multi-select
gridBody.addEventListener('click', function(ev) {
if (ev.target.closest('button, input, select, a, label')) return;
var row = getRow(ev.target);
if (!row) return;
var additive = ev.ctrlKey || ev.metaKey;
if (!additive) {
if (!row.classList.contains('is-selected')) clearSelection();
}
row.classList.toggle('is-selected');
});
// Clear when clicking outside the table
document.addEventListener('click', function(ev) {
if (!gridBody.contains(ev.target)) clearSelection();
});
})();
/* Context menu with bulk delete support */
(function setupRowContextMenu() {
var menu = document.getElementById('row-menu');
var deleteBtn = document.getElementById('ctx-delete');
var gridBody = document.querySelector('.table tbody');
var targetEntryId = null;
var touchTimer = null;
var touchStartXY = null;
if (!menu || !gridBody) return;
function selectedRows() { return Array.from(gridBody.querySelectorAll('tr[data-entry-id].is-selected')); }
function selectedIds() { return selectedRows().map(function(r){ return parseInt(r.getAttribute('data-entry-id'), 10); }).filter(Boolean); }
function closeMenu() { if (!menu.hidden) { menu.hidden = true; targetEntryId = null; } }
function placeMenu(x, y) {
var vw = window.innerWidth, vh = window.innerHeight;
var rect = menu.getBoundingClientRect(); var left = x, top = y;
if (left + rect.width > vw - 8) left = Math.max(8, vw - rect.width - 8);
if (top + rect.height > vh - 8) top = Math.max(8, vh - rect.height - 8);
menu.style.left = left + 'px'; menu.style.top = top + 'px';
}
function openMenuForRow(entryId, clientX, clientY) {
if (!window.can_edit) return;
targetEntryId = entryId; menu.hidden = false; placeMenu(clientX, clientY);
if (deleteBtn && !deleteBtn.disabled) deleteBtn.focus();
}
gridBody.addEventListener('contextmenu', function(e) {
var tr = e.target.closest('tr[data-entry-id]'); if (!tr) return;
e.preventDefault(); openMenuForRow(tr.getAttribute('data-entry-id'), e.clientX, e.clientY);
});
gridBody.addEventListener('keydown', function(e) {
if (!((e.shiftKey && e.key === 'F10') || e.key === 'ContextMenu')) return;
var tr = e.target.closest('tr[data-entry-id]'); if (!tr) return;
e.preventDefault(); var r = tr.getBoundingClientRect(); openMenuForRow(tr.getAttribute('data-entry-id'), r.right - 8, r.top + 16);
});
gridBody.addEventListener('touchstart', function(e) {
var t = e.target.closest('tr[data-entry-id]'); if (!t) return;
touchStartXY = (e.touches && e.touches[0]) ? { x: e.touches[0].clientX, y: e.touches[0].clientY } : null;
clearTimeout(touchTimer);
touchTimer = setTimeout(function(){
if (!touchStartXY) return;
openMenuForRow(t.getAttribute('data-entry-id'), touchStartXY.x, touchStartXY.y);
}, 600);
}, { passive: true });
gridBody.addEventListener('touchmove', function(e) {
if (!touchStartXY) return;
var pt = e.touches && e.touches[0]; if (!pt) return;
var dx = Math.abs(pt.clientX - touchStartXY.x), dy = Math.abs(pt.clientY - touchStartXY.y);
if (dx > 10 || dy > 10) { clearTimeout(touchTimer); touchTimer = null; }
}, { passive: true });
['touchend','touchcancel'].forEach(function(evt){
gridBody.addEventListener(evt, function(){ clearTimeout(touchTimer); touchTimer = null; touchStartXY = null; }, { passive: true });
});
async function bulkDelete(ids) {
if (!ids || !ids.length) return;
var fd = new FormData();
fd.append('timesheet_id', '{{ active_ts }}');
fd.append('entry_ids', ids.join(','));
try {
var res = await fetch('/timesheet/delete-entries', { method: 'POST', credentials: 'same-origin', body: fd });
var json = await res.json();
if (!json || !json.ok) { alert('Delete failed. Please try again.'); return; }
ids.forEach(function(id){
var r = document.querySelector('tr[data-entry-id="' + id + '"]');
if (r && r.parentNode) r.parentNode.removeChild(r);
});
await refreshSummary('{{ active_ts }}', '{{ selected_employee.id if selected_employee else "" }}');
showToast(ids.length === 1 ? 'Entry deleted' : ('Deleted ' + ids.length + ' rows'));
} catch (e) {
alert('Delete failed: ' + (e && e.message ? e.message : e));
}
}
if (deleteBtn) {
deleteBtn.addEventListener('click', async function() {
var ids = selectedIds();
if (ids.length > 0) {
if (!confirm('Delete ' + ids.length + ' selected row(s)? This cannot be undone.')) { closeMenu(); return; }
await bulkDelete(ids);
} else if (targetEntryId) {
if (!confirm('Delete this row? This cannot be undone.')) { closeMenu(); return; }
await bulkDelete([parseInt(targetEntryId, 10)]);
}
closeMenu();
});
}
window.addEventListener('mousedown', function(e){ if (!menu.hidden && !menu.contains(e.target)) closeMenu(); });
window.addEventListener('scroll', closeMenu, true);
window.addEventListener('resize', closeMenu);
window.addEventListener('keydown', function(e){ if (e.key === 'Escape') closeMenu(); });
})();
/* NEW: Employee right-click removal in sidebar */
(function setupEmployeeContextMenu() {
const list = document.querySelector('.emp-list');
const menu = document.getElementById('emp-menu');
const removeBtn = document.getElementById('ctx-emp-remove');
let targetEmpId = null;
if (!list || !menu) return;
function placeMenu(x, y) {
const vw = window.innerWidth, vh = window.innerHeight;
const rect = menu.getBoundingClientRect();
let left = x, top = y;
if (left + rect.width > vw - 8) left = Math.max(8, vw - rect.width - 8);
if (top + rect.height > vh - 8) top = Math.max(8, vh - rect.height - 8);
menu.style.left = left + 'px';
menu.style.top = top + 'px';
}
function closeMenu() { menu.hidden = true; targetEmpId = null; }
list.addEventListener('contextmenu', function(e) {
const li = e.target.closest('li[data-employee-id]');
if (!li) return;
e.preventDefault();
targetEmpId = li.getAttribute('data-employee-id');
if (!targetEmpId) return;
menu.hidden = false;
placeMenu(e.clientX, e.clientY);
if (removeBtn && !removeBtn.disabled) removeBtn.focus();
});
window.addEventListener('mousedown', function(e) {
if (!menu.hidden && !menu.contains(e.target)) closeMenu();
});
window.addEventListener('scroll', closeMenu, true);
window.addEventListener('resize', closeMenu);
window.addEventListener('keydown', function(e){ if (e.key === 'Escape') closeMenu(); });
if (removeBtn) {
removeBtn.addEventListener('click', async function() {
if (!targetEmpId) { closeMenu(); return; }
if (!window.can_edit) { closeMenu(); return; }
if (!confirm('Remove this employee and all of their time entries from this time period? This cannot be undone.')) {
closeMenu();
return;
}
const fd = new FormData();
fd.append('timesheet_id', '{{ active_ts }}');
fd.append('employee_id', targetEmpId);
try {
const res = await fetch('/viewer/remove-employee', { method: 'POST', credentials: 'same-origin', body: fd });
if (!res.ok) { alert('Remove failed (' + res.status + ')'); closeMenu(); return; }
const json = await res.json();
if (!json || !json.ok) { alert('Remove failed'); closeMenu(); return; }
// Remove the employee from the sidebar quickly
const li = document.querySelector('.emp-list li[data-employee-id="' + targetEmpId + '"]');
if (li && li.parentNode) li.parentNode.removeChild(li);
// If we just removed the selected employee, navigate to the viewer without employee_id
const currentSel = '{{ selected_employee.id if selected_employee else "" }}';
if (String(currentSel) === String(targetEmpId)) {
window.location.href = '/viewer?timesheet_id={{ active_ts }}';
} else {
window.location.reload();
}
} catch (e) {
alert('Unable to remove employee. Please try again.');
} finally {
closeMenu();
}
});
}
})();
</script>
{% endif %}
{% endblock %}