synch correction and db update

This commit is contained in:
Stan
2026-04-26 16:00:43 +02:00
parent c10e259ae8
commit bfd812747e
18 changed files with 3934 additions and 767 deletions
+372 -266
View File
@@ -76,11 +76,14 @@
<button class="admin-nav-item btn btn-sm btn-link text-start w-100" type="button" data-panel="settings-policies">Image Policy</button>
<button class="admin-nav-item btn btn-sm btn-link text-start w-100" type="button" data-panel="settings-template">Template</button>
<button class="admin-nav-item btn btn-sm btn-link text-start w-100" type="button" data-panel="settings-task">Task</button>
<button class="admin-nav-item btn btn-sm btn-link text-start w-100" type="button" data-panel="settings-geo">Geo Location</button>
</div>
</div>
</nav>
<div class="p-3 border-top">
<button id="adminSyncBtn" class="btn btn-outline-secondary btn-sm w-100 mb-1" type="button"><i class="bi bi-arrow-repeat me-1"></i>Sync</button>
<button id="adminLogoutBtn" class="btn btn-outline-danger btn-sm w-100 mb-1" type="button"><i class="bi bi-box-arrow-right me-1"></i>Logout</button>
<a class="btn btn-outline-secondary btn-sm w-100 mb-1" href="/user">User area</a>
<a class="btn btn-secondary btn-sm w-100 mb-1" href="/admin">Admin area</a>
<a class="btn btn-outline-secondary btn-sm w-100" href="/">Back to portal</a>
@@ -183,55 +186,59 @@
<p class="text-muted">Define dropdown values for checklist record forms.</p>
</div>
<!-- Categories -->
<div class="card mb-3">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-2">
<h6 class="fw-semibold mb-0">Categories</h6>
<button type="button" class="btn btn-outline-primary btn-sm" data-ts-action="add-cat"><i class="bi bi-plus-lg me-1"></i>Add</button>
</div>
<div id="tsCatList"></div>
</div>
</div>
<!-- Sub Categories -->
<div class="card mb-3">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-1">
<h6 class="fw-semibold mb-0">Sub Categories</h6>
<button type="button" class="btn btn-outline-primary btn-sm" data-ts-action="add-subcat"><i class="bi bi-plus-lg me-1"></i>Add</button>
</div>
<p class="text-muted small mb-2">Parent category is mandatory.</p>
<div id="tsSubCatList"></div>
</div>
</div>
<!-- Severities -->
<div class="card mb-3">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-2">
<h6 class="fw-semibold mb-0">Severities</h6>
<button type="button" class="btn btn-outline-primary btn-sm" data-ts-action="add-sev"><i class="bi bi-plus-lg me-1"></i>Add</button>
</div>
<p class="text-muted small mb-2">Optionally assign a background color displayed on task records.</p>
<div id="tsSevList"></div>
</div>
</div>
<!-- Statuses -->
<div class="card mb-3">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-2">
<h6 class="fw-semibold mb-0">Statuses</h6>
<button type="button" class="btn btn-outline-primary btn-sm" data-ts-action="add-status"><i class="bi bi-plus-lg me-1"></i>Add</button>
</div>
<p class="text-muted small mb-2">Optionally assign a background color displayed on task records.</p>
<div id="tsStatusList"></div>
</div>
</div>
<!-- Handled By -->
<div class="card">
<div class="card-body">
<!-- Categories -->
<h6 class="fw-semibold">Categories</h6>
<div class="input-group input-group-sm mb-2">
<input id="tsCatInput" class="form-control" type="text" placeholder="Add category…" />
<button type="button" class="btn btn-outline-primary" data-ts-action="add-cat">Add</button>
</div>
<div id="tsCatList" class="mb-4"></div>
<!-- Sub Categories -->
<h6 class="fw-semibold">Sub Categories</h6>
<p class="text-muted small">Parent category is mandatory.</p>
<div class="input-group input-group-sm mb-2">
<select id="tsSubCatParent" class="form-select" style="max-width:180px"><option value="">Parent…</option></select>
<input id="tsSubCatInput" class="form-control" type="text" placeholder="Add sub category…" />
<button type="button" class="btn btn-outline-primary" data-ts-action="add-subcat">Add</button>
</div>
<div id="tsSubCatList" class="mb-4"></div>
<!-- Severities -->
<h6 class="fw-semibold">Severities</h6>
<div class="input-group input-group-sm mb-2">
<input id="tsSevInput" class="form-control" type="text" placeholder="Add severity…" />
<button type="button" class="btn btn-outline-primary" data-ts-action="add-sev">Add</button>
</div>
<div id="tsSevList" class="mb-4"></div>
<!-- Statuses -->
<h6 class="fw-semibold">Statuses</h6>
<div class="input-group input-group-sm mb-1">
<input id="tsStatusInput" class="form-control" type="text" placeholder="Add status…" />
<button type="button" class="btn btn-outline-primary" data-ts-action="add-status">Add</button>
</div>
<div class="form-check form-check-inline small mb-2">
<input class="form-check-input" type="checkbox" id="tsStatusReqHandled" />
<label class="form-check-label" for="tsStatusReqHandled">Handled By required</label>
</div>
<div class="form-check form-check-inline small mb-2">
<input class="form-check-input" type="checkbox" id="tsStatusReqComment" />
<label class="form-check-label" for="tsStatusReqComment">Comment required</label>
</div>
<div id="tsStatusList" class="mb-4"></div>
<!-- Handled By -->
<h6 class="fw-semibold">Handled By</h6>
<div class="input-group input-group-sm mb-2">
<input id="tsHandledInput" class="form-control" type="text" placeholder="Add handler…" />
<button type="button" class="btn btn-outline-primary" data-ts-action="add-handled">Add</button>
<div class="d-flex justify-content-between align-items-center mb-2">
<h6 class="fw-semibold mb-0">Handled By</h6>
<button type="button" class="btn btn-outline-primary btn-sm" data-ts-action="add-handled"><i class="bi bi-plus-lg me-1"></i>Add</button>
</div>
<div id="tsHandledList"></div>
</div>
@@ -246,29 +253,55 @@
<p class="text-muted">Define Project and Process values for task assignment.</p>
</div>
<!-- Projects -->
<div class="card mb-3">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-2">
<h6 class="fw-semibold mb-0">Projects</h6>
<button type="button" class="btn btn-outline-primary btn-sm" data-tk-action="add-proj"><i class="bi bi-plus-lg me-1"></i>Add</button>
</div>
<div id="tkProjList"></div>
</div>
</div>
<!-- Processes -->
<div class="card">
<div class="card-body">
<!-- Projects -->
<h6 class="fw-semibold">Projects</h6>
<div class="input-group input-group-sm mb-2">
<input id="tkProjInput" class="form-control" type="text" placeholder="Add project…" />
<button type="button" class="btn btn-outline-primary" data-tk-action="add-proj">Add</button>
</div>
<div id="tkProjList" class="mb-4"></div>
<!-- Processes -->
<h6 class="fw-semibold">Processes</h6>
<p class="text-muted small">Parent project is mandatory.</p>
<div class="input-group input-group-sm mb-2">
<select id="tkProcParent" class="form-select" style="max-width:180px"><option value="">Parent project…</option></select>
<input id="tkProcInput" class="form-control" type="text" placeholder="Add process…" />
<button type="button" class="btn btn-outline-primary" data-tk-action="add-proc">Add</button>
<div class="d-flex justify-content-between align-items-center mb-1">
<h6 class="fw-semibold mb-0">Processes</h6>
<button type="button" class="btn btn-outline-primary btn-sm" data-tk-action="add-proc"><i class="bi bi-plus-lg me-1"></i>Add</button>
</div>
<p class="text-muted small mb-2">Parent project is mandatory.</p>
<div id="tkProcList"></div>
</div>
</div>
</section>
<!-- SETTINGS GEO LOCATION -->
<section id="panel-settings-geo" class="admin-panel">
<div class="mb-3">
<p class="text-muted small mb-0">Settings Geo Location</p>
<h3 class="fw-bold">Geo Location Settings</h3>
<p class="text-muted">Configure the allowed radius for image geo-location checks on tasks.</p>
</div>
<div class="card">
<div class="card-body">
<form id="geoSettingsForm">
<div class="row g-3 align-items-end">
<div class="col-md-4">
<label for="geoFenceRadius" class="form-label fw-semibold">Geo Fence Radius (metres)</label>
<input id="geoFenceRadius" class="form-control" type="number" min="1" max="100000" step="1" placeholder="50" />
<div class="form-text">Images taken within this radius of the site location will be accepted. Images outside will trigger a warning.</div>
</div>
<div class="col-md-2">
<button type="submit" class="btn btn-primary btn-sm w-100">Save</button>
</div>
</div>
</form>
</div>
</div>
</section>
<!-- USERS -->
<section id="panel-users" class="admin-panel">
<div class="d-flex justify-content-between align-items-center mb-3">
@@ -285,51 +318,58 @@
</div>
</div>
<div id="userFormSection" class="card" style="display:none">
<div class="card-header d-flex justify-content-between align-items-center">
<h6 id="userFormHeading" class="mb-0 fw-semibold">Add User</h6>
</div>
<div class="card-body">
<form id="userForm">
<div class="row g-3">
<div class="col-md-6">
<label for="userEmail" class="form-label">Email</label>
<input id="userEmail" class="form-control" type="email" required />
</section>
<!-- USER MODAL -->
<div class="modal fade" id="userFormModal" tabindex="-1" aria-labelledby="userFormHeading" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="userFormHeading">Add User</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="userForm">
<div class="row g-3">
<div class="col-md-6">
<label for="userEmail" class="form-label">Email</label>
<input id="userEmail" class="form-control" type="email" required />
</div>
<div class="col-md-6">
<label for="userPassword" class="form-label">Password</label>
<input id="userPassword" class="form-control" type="password" required />
</div>
<div class="col-md-4">
<label for="userName" class="form-label">Name</label>
<input id="userName" class="form-control" type="text" required />
</div>
<div class="col-md-4">
<label for="userFamilyName" class="form-label">Family Name</label>
<input id="userFamilyName" class="form-control" type="text" required />
</div>
<div class="col-md-4">
<label for="userCompany" class="form-label">Company</label>
<input id="userCompany" class="form-control" type="text" />
</div>
<div class="col-md-4">
<label for="userRole" class="form-label">Role</label>
<select id="userRole" class="form-select" required>
<option value="">— Select —</option>
<option value="CW">CW</option>
<option value="ANT">ANT</option>
<option value="CW/ANT">CW/ANT</option>
</select>
</div>
</div>
<div class="col-md-6">
<label for="userPassword" class="form-label">Password</label>
<input id="userPassword" class="form-control" type="password" required />
</div>
<div class="col-md-4">
<label for="userName" class="form-label">Name</label>
<input id="userName" class="form-control" type="text" required />
</div>
<div class="col-md-4">
<label for="userFamilyName" class="form-label">Family Name</label>
<input id="userFamilyName" class="form-control" type="text" required />
</div>
<div class="col-md-4">
<label for="userCompany" class="form-label">Company</label>
<input id="userCompany" class="form-control" type="text" />
</div>
<div class="col-md-4">
<label for="userRole" class="form-label">Role</label>
<select id="userRole" class="form-select" required>
<option value="">— Select —</option>
<option value="CW">CW</option>
<option value="ANT">ANT</option>
<option value="CW/ANT">CW/ANT</option>
</select>
</div>
</div>
<div class="mt-3 d-flex gap-2">
<button class="btn btn-primary btn-sm" type="submit">Save User</button>
<button id="cancelUserBtn" class="btn btn-outline-secondary btn-sm" type="button">Cancel</button>
</div>
</form>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary btn-sm" data-bs-dismiss="modal">Cancel</button>
<button type="submit" form="userForm" class="btn btn-primary btn-sm">Save User</button>
</div>
</div>
</div>
</section>
</div>
<!-- SITES -->
<section id="panel-sites" class="admin-panel">
@@ -347,40 +387,62 @@
</div>
</div>
<div id="siteFormSection" class="card" style="display:none">
<div class="card-header"><h6 id="siteFormHeading" class="mb-0 fw-semibold">Add Site</h6></div>
<div class="card-body">
<form id="siteForm">
<div class="row g-3">
<div class="col-md-6">
<label for="siteSiteCode" class="form-label">Site Code</label>
<input id="siteSiteCode" class="form-control" type="text" required />
</section>
<!-- SITE MODAL -->
<div class="modal fade" id="siteFormModal" tabindex="-1" aria-labelledby="siteFormHeading" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="siteFormHeading">Add Site</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="siteForm">
<div class="row g-3">
<div class="col-md-6">
<label for="siteSiteCode" class="form-label">Site Code</label>
<input id="siteSiteCode" class="form-control" type="text" required />
</div>
<div class="col-md-6">
<label for="siteHost" class="form-label">Host</label>
<select id="siteHost" class="form-select">
<option value="">— Select —</option>
<option value="OBE">OBE</option>
<option value="PXS">PXS</option>
</select>
</div>
<div class="col-md-6">
<label for="siteObe" class="form-label">OBE Site Code</label>
<input id="siteObe" class="form-control" type="text" />
</div>
<div class="col-md-6">
<label for="sitePxs" class="form-label">PXS Site Code</label>
<input id="sitePxs" class="form-control" type="text" />
</div>
<!-- Geo Location -->
<div class="col-12">
<label class="form-label fw-semibold">Geo Location <span class="text-muted fw-normal small">(optional — click the map to set)</span></label>
<div id="siteMapPicker" style="height:260px;border-radius:6px;border:1px solid #dee2e6;"></div>
</div>
<div class="col-md-6">
<label for="siteLat" class="form-label">Latitude</label>
<input id="siteLat" class="form-control" type="number" step="any" placeholder="e.g. 51.5074" />
</div>
<div class="col-md-6">
<label for="siteLng" class="form-label">Longitude</label>
<input id="siteLng" class="form-control" type="number" step="any" placeholder="e.g. -0.1278" />
</div>
</div>
<div class="col-md-6">
<label for="siteHost" class="form-label">Host</label>
<select id="siteHost" class="form-select">
<option value="">— Select —</option>
<option value="OBE">OBE</option>
<option value="PXS">PXS</option>
</select>
</div>
<div class="col-md-6">
<label for="siteObe" class="form-label">OBE Site Code</label>
<input id="siteObe" class="form-control" type="text" />
</div>
<div class="col-md-6">
<label for="sitePxs" class="form-label">PXS Site Code</label>
<input id="sitePxs" class="form-control" type="text" />
</div>
</div>
<div class="mt-3 d-flex gap-2">
<button class="btn btn-primary btn-sm" type="submit">Save Site</button>
<button id="cancelSiteBtn" class="btn btn-outline-secondary btn-sm" type="button">Cancel</button>
</div>
</form>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary btn-sm" data-bs-dismiss="modal">Cancel</button>
<button type="submit" form="siteForm" class="btn btn-primary btn-sm">Save Site</button>
</div>
</div>
</div>
</section>
</div>
<!-- CHECK LISTS > TEMPLATES -->
<section id="panel-cl-templates" class="admin-panel">
@@ -402,49 +464,58 @@
</div>
</div>
<div id="clTplFormSection" class="card" style="display:none">
<div class="card-header"><h6 id="clTplFormHeading" class="mb-0 fw-semibold">Add Template</h6></div>
<div class="card-body">
<form id="clTemplateForm">
<div class="row g-3 mb-3">
<div class="col-12">
<label for="clTplName" class="form-label">Template Name</label>
<input id="clTplName" class="form-control" type="text" required />
</section>
<!-- CL TEMPLATE MODAL -->
<div class="modal fade" id="clTplFormModal" tabindex="-1" aria-labelledby="clTplFormHeading" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="clTplFormHeading">Add Template</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="clTemplateForm">
<div class="row g-3 mb-3">
<div class="col-12">
<label for="clTplName" class="form-label">Template Name</label>
<input id="clTplName" class="form-control" type="text" required />
</div>
<div class="col-md-4">
<label for="clTplScope" class="form-label">Scope</label>
<select id="clTplScope" class="form-select">
<option value="">— Select —</option>
<option value="CW">CW</option>
<option value="ANT">ANT</option>
<option value="ANT_CPsite">ANT_CPsite</option>
</select>
</div>
<div class="col-md-4">
<label for="clTplVersion" class="form-label">Version</label>
<input id="clTplVersion" class="form-control" type="text" />
</div>
<div class="col-md-4"></div>
<div class="col-md-6">
<label for="clTplValidFrom" class="form-label">Valid From</label>
<input id="clTplValidFrom" class="form-control" type="date" />
</div>
<div class="col-md-6">
<label for="clTplValidTill" class="form-label">Valid Till</label>
<input id="clTplValidTill" class="form-control" type="date" />
</div>
</div>
<div class="col-md-4">
<label for="clTplScope" class="form-label">Scope</label>
<select id="clTplScope" class="form-select">
<option value="">— Select —</option>
<option value="CW">CW</option>
<option value="ANT">ANT</option>
<option value="ANT_CPsite">ANT_CPsite</option>
</select>
</div>
<div class="col-md-4">
<label for="clTplVersion" class="form-label">Version</label>
<input id="clTplVersion" class="form-control" type="text" />
</div>
<div class="col-md-4"></div>
<div class="col-md-6">
<label for="clTplValidFrom" class="form-label">Valid From</label>
<input id="clTplValidFrom" class="form-control" type="date" />
</div>
<div class="col-md-6">
<label for="clTplValidTill" class="form-label">Valid Till</label>
<input id="clTplValidTill" class="form-control" type="date" />
</div>
</div>
<h6 class="fw-semibold">Include Records</h6>
<p class="text-muted small">Select records to include in this template.</p>
<div id="clTplRecordSelection" class="mb-3"></div>
<div class="d-flex gap-2">
<button class="btn btn-primary btn-sm" type="submit">Save Template</button>
<button id="cancelClTplBtn" class="btn btn-outline-secondary btn-sm" type="button">Cancel</button>
</div>
</form>
<h6 class="fw-semibold">Include Records</h6>
<p class="text-muted small">Select records to include in this template.</p>
<div id="clTplRecordSelection" class="mb-3"></div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary btn-sm" data-bs-dismiss="modal">Cancel</button>
<button type="submit" form="clTemplateForm" class="btn btn-primary btn-sm">Save Template</button>
</div>
</div>
</div>
</section>
</div>
<!-- CHECK LISTS > RECORDS -->
<section id="panel-cl-records" class="admin-panel">
@@ -466,70 +537,79 @@
</div>
</div>
<div id="clRecFormSection" class="card" style="display:none">
<div class="card-header"><h6 id="clRecFormHeading" class="mb-0 fw-semibold">Add Record</h6></div>
<div class="card-body">
<form id="clRecordForm">
<div class="row g-3 mb-3">
<div class="col-md-3">
<label for="clRecSort" class="form-label">Sort</label>
<input id="clRecSort" class="form-control" type="number" min="1" step="1" required />
<div class="form-text">Unique number.</div>
</div>
<div class="col-md-3">
<label for="clRecCategory" class="form-label">Category</label>
<select id="clRecCategory" class="form-select"></select>
</div>
<div class="col-md-3">
<label for="clRecSubCategory" class="form-label">Sub Category</label>
<select id="clRecSubCategory" class="form-select"></select>
</div>
<div class="col-md-3">
<label for="clRecSeverity" class="form-label">Severity</label>
<select id="clRecSeverity" class="form-select"></select>
</div>
<div class="col-12">
<div class="form-check">
<input id="clRecImageRequired" class="form-check-input" type="checkbox" />
<label class="form-check-label" for="clRecImageRequired">Image Required — user must attach image</label>
</section>
<!-- CL RECORD MODAL -->
<div class="modal fade" id="clRecFormModal" tabindex="-1" aria-labelledby="clRecFormHeading" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="clRecFormHeading">Add Record</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="clRecordForm">
<div class="row g-3 mb-3">
<div class="col-md-3">
<label for="clRecSort" class="form-label">Sort</label>
<input id="clRecSort" class="form-control" type="number" min="1" step="1" required />
<div class="form-text">Unique number.</div>
</div>
<div class="col-md-3">
<label for="clRecCategory" class="form-label">Category</label>
<select id="clRecCategory" class="form-select"></select>
</div>
<div class="col-md-3">
<label for="clRecSubCategory" class="form-label">Sub Category</label>
<select id="clRecSubCategory" class="form-select"></select>
</div>
<div class="col-md-3">
<label for="clRecSeverity" class="form-label">Severity</label>
<select id="clRecSeverity" class="form-select"></select>
</div>
<div class="col-12">
<div class="form-check">
<input id="clRecImageRequired" class="form-check-input" type="checkbox" />
<label class="form-check-label" for="clRecImageRequired">Image Required — user must attach image</label>
</div>
</div>
<div class="col-12">
<label for="clRecDescEN" class="form-label">Description EN</label>
<input id="clRecDescEN" class="form-control" type="text" />
</div>
<div class="col-md-6">
<label for="clRecDescFR" class="form-label">Description FR</label>
<input id="clRecDescFR" class="form-control" type="text" />
</div>
<div class="col-md-6">
<label for="clRecDescNL" class="form-label">Description NL</label>
<input id="clRecDescNL" class="form-control" type="text" />
</div>
<div class="col-md-4">
<label for="clRecStatus" class="form-label">Status <span class="badge bg-warning text-dark">PLACEHOLDER</span></label>
<select id="clRecStatus" class="form-select" disabled></select>
<div class="form-text">Set by user at runtime.</div>
</div>
<div class="col-md-4">
<label for="clRecHandledBy" class="form-label">Handled By <span class="badge bg-warning text-dark">PLACEHOLDER</span></label>
<select id="clRecHandledBy" class="form-select" disabled></select>
<div class="form-text">Set by user at runtime.</div>
</div>
<div class="col-md-4">
<label for="clRecComment" class="form-label">Comment <span class="badge bg-warning text-dark">PLACEHOLDER</span></label>
<textarea id="clRecComment" class="form-control" rows="2" disabled></textarea>
<div class="form-text">Filled by user at runtime.</div>
</div>
</div>
<div class="col-12">
<label for="clRecDescEN" class="form-label">Description EN</label>
<input id="clRecDescEN" class="form-control" type="text" />
</div>
<div class="col-md-6">
<label for="clRecDescFR" class="form-label">Description FR</label>
<input id="clRecDescFR" class="form-control" type="text" />
</div>
<div class="col-md-6">
<label for="clRecDescNL" class="form-label">Description NL</label>
<input id="clRecDescNL" class="form-control" type="text" />
</div>
<div class="col-md-4">
<label for="clRecStatus" class="form-label">Status <span class="badge bg-warning text-dark">PLACEHOLDER</span></label>
<select id="clRecStatus" class="form-select" disabled></select>
<div class="form-text">Set by user at runtime.</div>
</div>
<div class="col-md-4">
<label for="clRecHandledBy" class="form-label">Handled By <span class="badge bg-warning text-dark">PLACEHOLDER</span></label>
<select id="clRecHandledBy" class="form-select" disabled></select>
<div class="form-text">Set by user at runtime.</div>
</div>
<div class="col-md-4">
<label for="clRecComment" class="form-label">Comment <span class="badge bg-warning text-dark">PLACEHOLDER</span></label>
<textarea id="clRecComment" class="form-control" rows="2" disabled></textarea>
<div class="form-text">Filled by user at runtime.</div>
</div>
</div>
<div class="d-flex gap-2">
<button class="btn btn-primary btn-sm" type="submit">Save Record</button>
<button id="cancelClRecBtn" class="btn btn-outline-secondary btn-sm" type="button">Cancel</button>
</div>
</form>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary btn-sm" data-bs-dismiss="modal">Cancel</button>
<button type="submit" form="clRecordForm" class="btn btn-primary btn-sm">Save Record</button>
</div>
</div>
</div>
</section>
</div>
<!-- REPORTS -->
<section id="panel-reports" class="admin-panel admin-panel-active">
@@ -547,40 +627,66 @@
</div>
</div>
<div id="taskFormSection" class="card" style="display:none">
<div class="card-header"><h6 id="taskFormHeading" class="mb-0 fw-semibold">Create Task Assignment</h6></div>
<div class="card-body">
<form id="taskForm">
<div class="row g-3">
<div class="col-md-6">
<label for="taskUser" class="form-label">User</label>
<select id="taskUser" class="form-select" required></select>
</section>
<!-- TASK MODAL -->
<div class="modal fade" id="taskFormModal" tabindex="-1" aria-labelledby="taskFormHeading" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="taskFormHeading">Create Task Assignment</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="taskForm">
<div class="row g-3">
<div class="col-md-6">
<label for="taskUser" class="form-label">User</label>
<select id="taskUser" class="form-select" required></select>
</div>
<div class="col-md-6">
<label for="taskSite" class="form-label">Site</label>
<select id="taskSite" class="form-select" required></select>
</div>
<div class="col-md-4">
<label for="taskTemplate" class="form-label">Template</label>
<select id="taskTemplate" class="form-select" required></select>
</div>
<div class="col-md-4">
<label for="taskProject" class="form-label">Project</label>
<select id="taskProject" class="form-select"></select>
</div>
<div class="col-md-4">
<label for="taskProcess" class="form-label">Process</label>
<select id="taskProcess" class="form-select"></select>
</div>
</div>
<div class="col-md-6">
<label for="taskSite" class="form-label">Site</label>
<select id="taskSite" class="form-select" required></select>
</div>
<div class="col-md-4">
<label for="taskTemplate" class="form-label">Template</label>
<select id="taskTemplate" class="form-select" required></select>
</div>
<div class="col-md-4">
<label for="taskProject" class="form-label">Project</label>
<select id="taskProject" class="form-select"></select>
</div>
<div class="col-md-4">
<label for="taskProcess" class="form-label">Process</label>
<select id="taskProcess" class="form-select"></select>
</div>
</div>
<div class="mt-3 d-flex gap-2">
<button class="btn btn-primary btn-sm" type="submit">Assign Task</button>
<button id="cancelTaskBtn" class="btn btn-outline-secondary btn-sm" type="button">Cancel</button>
</div>
</form>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary btn-sm" data-bs-dismiss="modal">Cancel</button>
<button type="submit" form="taskForm" class="btn btn-primary btn-sm">Assign Task</button>
</div>
</div>
</div>
</section>
</div>
<!-- SETTINGS ITEM MODAL (shared for all Settings add/edit popups) -->
<div class="modal fade" id="settingsItemModal" tabindex="-1" aria-labelledby="settingsItemModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="settingsItemModalLabel">Add Item</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body" id="settingsItemModalBody"></div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary btn-sm" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary btn-sm" id="settingsItemModalSave">Save</button>
</div>
</div>
</div>
</div>
</main>
</div>
+922 -334
View File
File diff suppressed because it is too large Load Diff
+170 -12
View File
@@ -36,7 +36,10 @@ const userState = {
taskSettings: { projects: [], processes: [] },
taskData: {},
imageRules: null,
appConfig: {},
currentTaskId: null,
currentUserId: null,
currentUserName: '',
language: 'EN',
activeCategory: null,
searchQuery: '',
@@ -86,6 +89,7 @@ async function loadFromServer() {
userState.clRecords = data.clRecords || [];
userState.clTemplates = data.clTemplates || [];
userState.tasks = data.tasks || [];
userState.appConfig = data.appConfig || {};
} catch (err) {
/* Network failure (offline, DNS, etc.) — fall back to cache. */
console.warn('Failed to load data from server, using IndexedDB cache', err);
@@ -108,6 +112,7 @@ async function loadFromCache() {
userState.clRecords = data.clRecords || [];
userState.clTemplates = data.clTemplates || [];
userState.tasks = data.tasks || [];
userState.appConfig = data.appConfig || {};
} catch { /* ignore */ }
}
@@ -118,6 +123,23 @@ async function loadFromCache() {
export async function initUser() {
userState.language = localStorage.getItem('user_language') || 'EN';
await openUserDB();
/* Identify the logged-in user from the server session cookie */
try {
const checkResp = await fetch('/api/v1/auth/check');
if (checkResp.ok) {
const checkData = await checkResp.json();
if (checkData.authenticated && checkData.session?.type === 'user') {
userState.currentUserId = checkData.session.id;
userState.currentUserName = `${checkData.session.name || ''} ${checkData.session.familyName || ''}`.trim();
}
}
} catch (e) { /* non-blocking */ }
/* Display logged-in user name in sidebar */
const nameEl = document.getElementById('userDisplayName');
if (nameEl && userState.currentUserName) nameEl.textContent = userState.currentUserName;
/* Load admin entity data from server before reading IndexedDB cache. */
if (navigator.onLine) {
await loadFromServer();
@@ -165,6 +187,13 @@ async function forceSyncWithServer() {
filterTasksByUser();
renderTaskListView();
renderSidebarTasks();
/* If a task detail is open, also refresh its record data from the server so
* changes made on another device become visible immediately after a manual sync. */
if (userState.currentTaskId) {
await maybeHydrateFromServer(userState.currentTaskId);
await maybeDownloadImages(userState.currentTaskId);
renderTaskDetail();
}
} finally {
if (btn) {
btn.disabled = false;
@@ -174,10 +203,11 @@ async function forceSyncWithServer() {
}
function filterTasksByUser() {
/* Prefer session-based user id; fall back to URL param for compatibility */
const params = new URLSearchParams(window.location.search);
const userId = params.get('userId');
if (userId) {
const uid = Number(userId);
const urlUserId = params.get('userId');
const uid = userState.currentUserId || (urlUserId ? Number(urlUserId) : null);
if (uid) {
userState.tasks = userState.tasks.filter(t => t.userId === uid);
}
}
@@ -186,6 +216,9 @@ function bindEvents() {
document.getElementById('backToListBtn')?.addEventListener('click', showListView);
document.getElementById('saveDraftBtn')?.addEventListener('click', saveDraft);
document.getElementById('saveFinalBtn')?.addEventListener('click', saveFinal);
document.getElementById('mobileSaveDraftBtn')?.addEventListener('click', saveDraft);
document.getElementById('mobileSaveFinalBtn')?.addEventListener('click', saveFinal);
document.getElementById('logoutBtn')?.addEventListener('click', logoutUser);
document.getElementById('showSettingsBtn')?.addEventListener('click', showSettingsView);
document.getElementById('syncBtn')?.addEventListener('click', forceSyncWithServer);
document.getElementById('closeSettingsBtn')?.addEventListener('click', closeSettingsView);
@@ -227,12 +260,14 @@ function hideAllViews() {
function showSettingsView() {
hideAllViews();
document.getElementById('settingsView').classList.add('workspace-view-active');
document.getElementById('desktopSaveActions')?.classList.remove('is-visible');
}
function closeSettingsView() {
hideAllViews();
if (userState.currentTaskId) {
document.getElementById('taskDetailView').classList.add('workspace-view-active');
document.getElementById('desktopSaveActions')?.classList.add('is-visible');
} else {
document.getElementById('taskListView').classList.add('workspace-view-active');
}
@@ -242,6 +277,9 @@ async function showListView() {
userState.currentTaskId = null;
hideAllViews();
document.getElementById('taskListView').classList.add('workspace-view-active');
const msavHide = document.getElementById('mobileSaveActions');
if (msavHide) msavHide.style.display = 'none';
document.getElementById('desktopSaveActions')?.classList.remove('is-visible');
await loadAllData();
filterTasksByUser();
renderTaskListView();
@@ -260,6 +298,9 @@ function showDetailView(taskId) {
if (searchInput) searchInput.value = '';
hideAllViews();
document.getElementById('taskDetailView').classList.add('workspace-view-active');
const msav = document.getElementById('mobileSaveActions');
if (msav) msav.style.display = '';
document.getElementById('desktopSaveActions')?.classList.add('is-visible');
/* If task was reopened and images were stripped, try to re-download from server.
Chain: hydrate values first → then fetch image blobs → then render. */
maybeHydrateFromServer(id)
@@ -279,6 +320,8 @@ function renderTaskListView() {
container.innerHTML = '<div class="p-4 text-center text-muted"><h5>No tasks assigned</h5><p>Tasks will appear here once an administrator assigns them to you.</p></div>';
return;
}
/* Desktop: table layout */
const rows = userState.tasks.map((task) => {
const site = userState.sites.find((s) => s.id === task.siteId);
const tpl = userState.clTemplates.find((t) => t.id === task.templateId);
@@ -292,9 +335,30 @@ function renderTaskListView() {
<td><button class="btn btn-primary btn-sm" data-open-task="${task.id}">Open</button></td>
</tr>`;
}).join('');
container.innerHTML = `<table class="table table-hover table-sm mb-0"><thead><tr>
<th>Site</th><th>Template</th><th>Project</th><th>Process</th><th>Status</th><th></th>
</tr></thead><tbody>${rows}</tbody></table>`;
/* Mobile: card layout — fields expand horizontally with flex-wrap */
const cards = userState.tasks.map((task) => {
const site = userState.sites.find((s) => s.id === task.siteId);
const tpl = userState.clTemplates.find((t) => t.id === task.templateId);
const badgeCls = task.status === 'final' ? 'bg-success' : task.status === 'draft' ? 'bg-warning text-dark' : 'bg-secondary';
return `<div class="task-list-card">
<div class="task-list-card-fields">
<span class="task-list-field"><small>Site</small><strong>${esc(site?.siteCode || '-')}</strong></span>
<span class="task-list-field"><small>Template</small><strong>${esc(tpl?.name || '-')}</strong></span>
<span class="task-list-field"><small>Project</small><strong>${esc(task.project || '-')}</strong></span>
<span class="task-list-field"><small>Process</small><strong>${esc(task.process || '-')}</strong></span>
<span class="badge ${badgeCls} align-self-center">${esc(task.status || 'pending')}</span>
</div>
<button class="btn btn-primary btn-sm flex-shrink-0" data-open-task="${task.id}">Open</button>
</div>`;
}).join('');
container.innerHTML =
`<div class="d-none d-md-block"><table class="table table-hover table-sm mb-0"><thead><tr>
<th>Site</th><th>Template</th><th>Project</th><th>Process</th><th>Status</th><th></th>
</tr></thead><tbody>${rows}</tbody></table></div>` +
`<div class="d-md-none">${cards}</div>`;
container.querySelectorAll('[data-open-task]').forEach((b) => {
b.addEventListener('click', () => showDetailView(b.dataset.openTask));
});
@@ -432,6 +496,49 @@ function renderCategoryTabs(task, tpl) {
renderTaskRecords(taskObj, tplObj, getTaskData(userState.currentTaskId));
});
});
/* Scroll-arrow setup — runs after the DOM is painted so scrollWidth is correct */
requestAnimationFrame(() => initTabsScrollArrows(tabsContainer));
}
/**
* Attaches left/right scroll-arrow logic to the category tabs wrapper.
* Arrows are shown only when the tab list overflows its container,
* and each arrow scrolls by roughly the width of three tabs.
* Called via requestAnimationFrame so layout is complete before measuring.
*/
function initTabsScrollArrows(tabsEl) {
const wrapper = tabsEl?.closest('.tabs-scroll-wrapper');
const btnLeft = document.getElementById('tabsScrollLeft');
const btnRight = document.getElementById('tabsScrollRight');
if (!wrapper || !btnLeft || !btnRight) return;
function updateArrows() {
const overflows = tabsEl.scrollWidth > tabsEl.clientWidth;
wrapper.classList.toggle('has-overflow', overflows);
wrapper.classList.toggle('at-start', tabsEl.scrollLeft <= 2);
wrapper.classList.toggle('at-end',
tabsEl.scrollLeft >= tabsEl.scrollWidth - tabsEl.clientWidth - 2);
}
/* Scroll by ~3 average tab widths on each click */
const scrollStep = () => Math.max(tabsEl.clientWidth * 0.45, 120);
btnLeft.addEventListener('click', () => {
tabsEl.scrollBy({ left: -scrollStep(), behavior: 'smooth' });
});
btnRight.addEventListener('click', () => {
tabsEl.scrollBy({ left: scrollStep(), behavior: 'smooth' });
});
tabsEl.addEventListener('scroll', updateArrows, { passive: true });
/* Re-evaluate when the container is resized (e.g. sidebar open/close) */
if (window._tabsResizeObs) window._tabsResizeObs.disconnect();
window._tabsResizeObs = new ResizeObserver(updateArrows);
window._tabsResizeObs.observe(tabsEl);
updateArrows();
}
function onSearchInput(e) {
@@ -560,6 +667,10 @@ function renderTaskRecords(task, tpl, data) {
const category = rec.category || '-';
const subCategory = rec.subCategory || '-';
const severity = rec.severity || '-';
const sevColor = getSevColor(rec.severityId);
const sevBadge = sevColor
? `<span class="badge" style="background:${sevColor};color:${colorContrast(sevColor)}">${esc(severity)}</span>`
: esc(severity);
html += `<div class="task-record-card" data-record-id="${rec.id}" data-sub-cat="${esc(subCat)}">
<div class="task-record-header">
@@ -570,7 +681,7 @@ function renderTaskRecords(task, tpl, data) {
<div class="d-flex gap-3 mb-2 small text-muted">
<span><strong>Category:</strong> ${esc(category)}</span>
<span><strong>Sub:</strong> ${esc(subCategory)}</span>
<span><strong>Severity:</strong> ${esc(severity)}</span>
<span><strong>Severity:</strong> ${sevBadge}</span>
</div>
<div class="row g-2">
<div class="col-md-4">
@@ -611,6 +722,9 @@ function renderTaskRecords(task, tpl, data) {
container.innerHTML = html;
/* Apply colour coding to status selects based on current value */
applyStatusColors(container);
/* Bind change events */
container.querySelectorAll('.rec-status').forEach((el) => el.addEventListener('change', onRecordFieldChange));
container.querySelectorAll('.rec-handled').forEach((el) => el.addEventListener('change', onRecordFieldChange));
@@ -717,8 +831,11 @@ function onRecordFieldChange(e) {
const data = getTaskData(userState.currentTaskId);
if (!data.records[recId]) data.records[recId] = { status: '', handledBy: '', comment: '', images: [] };
if (e.target.classList.contains('rec-status')) data.records[recId].status = e.target.value;
else if (e.target.classList.contains('rec-handled')) data.records[recId].handledBy = e.target.value;
if (e.target.classList.contains('rec-status')) {
data.records[recId].status = e.target.value;
const color = getStatColor(e.target.value);
e.target.style.borderLeft = color ? `4px solid ${color}` : '';
} else if (e.target.classList.contains('rec-handled')) data.records[recId].handledBy = e.target.value;
else if (e.target.classList.contains('rec-comment')) data.records[recId].comment = e.target.value;
data.visitDate = document.getElementById('visitDate').value;
@@ -1059,6 +1176,13 @@ function showValidationErrors(errors) {
* Helpers
* ═══════════════════════════════════════════════════════════════════════════ */
async function logoutUser() {
try {
await fetch('/api/v1/auth/logout', { method: 'POST' });
} catch (e) { /* ignore */ }
window.location.href = '/';
}
function updateConnectionBadge() {
const badge = document.getElementById('connectionBadge');
if (!badge) return;
@@ -1097,6 +1221,34 @@ function esc(text) {
return d.innerHTML;
}
/* Returns white or black depending on background luminance */
function colorContrast(hex) {
if (!hex || hex.length < 7) return '#000000';
const r = parseInt(hex.slice(1, 3), 16);
const g = parseInt(hex.slice(3, 5), 16);
const b = parseInt(hex.slice(5, 7), 16);
const lum = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
return lum > 0.5 ? '#000000' : '#ffffff';
}
function getSevColor(severityId) {
const sev = userState.templateSettings.severities.find(s => s.id === severityId);
return sev?.color || '';
}
function getStatColor(statusValue) {
const stat = userState.templateSettings.statuses.find(s => s.value === statusValue);
return stat?.color || '';
}
/* Applies a colored left border to each status select based on the selected value */
function applyStatusColors(container) {
container.querySelectorAll('.rec-status').forEach(sel => {
const color = getStatColor(sel.value);
sel.style.borderLeft = color ? `4px solid ${color}` : '';
});
}
function formatFileSizeUser(bytes) {
if (!bytes) return '-';
if (bytes < 1024) return `${bytes} B`;
@@ -1280,6 +1432,10 @@ function applyTaskLock(task) {
if (visitDate) visitDate.disabled = locked;
if (saveDraftBtn) saveDraftBtn.disabled = locked;
if (saveFinalBtn) saveFinalBtn.disabled = locked;
const mobileSaveDraftBtn = document.getElementById('mobileSaveDraftBtn');
const mobileSaveFinalBtn = document.getElementById('mobileSaveFinalBtn');
if (mobileSaveDraftBtn) mobileSaveDraftBtn.disabled = locked;
if (mobileSaveFinalBtn) mobileSaveFinalBtn.disabled = locked;
if (container && locked) {
container.querySelectorAll('select, textarea, input').forEach(el => { el.disabled = true; });
@@ -1344,9 +1500,11 @@ function stripImageDataFromStorage(taskId) {
async function maybeHydrateFromServer(taskId) {
const id = Number(taskId);
/* Skip if IndexedDB already has record data for this task. */
const existing = userState.taskData[id];
if (existing && Object.keys(existing.records || {}).length > 0) return;
/* Skip if the user has unsaved local changes — don't overwrite their in-progress work.
* unsavedChanges is set by markUnsaved() on every field edit and cleared by markSaved()
* after an explicit Save Draft / Save Final action. It resets to false on page reload,
* so opening the task on a fresh device (or a new tab) always fetches from the server. */
if (userState.unsavedChanges) return;
if (!navigator.onLine) return;
+2 -6
View File
@@ -108,12 +108,8 @@
const data = await response.json();
if (data.success) {
/* Store token and user info in sessionStorage */
sessionStorage.setItem('auth_token', data.token);
sessionStorage.setItem('auth_type', 'user');
sessionStorage.setItem('auth_user', JSON.stringify(data.user));
/* Redirect to user page with userId */
window.location.href = '/user?userId=' + data.user.id;
/* Cookie is set by the server; redirect to user workspace */
window.location.href = '/user';
} else {
errorAlert.textContent = data.message || 'Login failed. Please check your credentials.';
errorAlert.classList.add('show');
+2 -63
View File
@@ -19,23 +19,13 @@
<div class="text-center mb-4">
<p class="text-uppercase text-muted small fw-semibold mb-1">Check List Access</p>
<h1 class="fw-bold">Choose workspace</h1>
<p class="text-muted">Select a user, then open the operator or administrator workspace.</p>
</div>
<!-- User selection -->
<div class="card mb-4">
<div class="card-body">
<label for="portalUserSelect" class="form-label fw-semibold">Select User</label>
<select id="portalUserSelect" class="form-select">
<option value="">— Choose a user to see their tasks —</option>
</select>
</div>
<p class="text-muted">Open the operator or administrator workspace. You will be prompted to log in if you are not already signed in.</p>
</div>
<div class="row g-3">
<!-- Operator workspace -->
<div class="col-md-6">
<a id="portalUserLink" class="card text-decoration-none h-100 portal-card" href="#">
<a class="card text-decoration-none h-100 portal-card" href="/user">
<div class="card-body text-center">
<i class="bi bi-clipboard-check fs-1 text-primary mb-2 d-block"></i>
<p class="text-muted small mb-1">User area</p>
@@ -84,61 +74,10 @@
</main>
<script>
// Populate user dropdown from server API
(function() {
const sel = document.getElementById('portalUserSelect');
const userLink = document.getElementById('portalUserLink');
/* Prevent opening user area without selecting a user */
userLink.addEventListener('click', (e) => {
if (!sel.value) {
e.preventDefault();
sel.focus();
sel.classList.add('is-invalid');
setTimeout(() => sel.classList.remove('is-invalid'), 2000);
}
});
async function populateUsers() {
try {
const resp = await fetch('/api/v1/admin/users', { headers: { Accept: 'application/json' } });
if (!resp.ok) throw new Error('Failed to load users');
const data = await resp.json();
const users = data.items || [];
sel.innerHTML = '<option value="">— Choose a user to see their tasks —</option>';
users.forEach(u => {
const opt = document.createElement('option');
opt.value = u.id;
opt.textContent = `${u.name} ${u.familyName} (${u.email}) — ${u.role}`;
sel.appendChild(opt);
});
const saved = localStorage.getItem('portal_selected_user');
if (saved) sel.value = saved;
updateUserLink();
} catch (err) {
console.warn('Could not load users from server:', err.message);
}
}
sel.addEventListener('change', () => {
localStorage.setItem('portal_selected_user', sel.value);
updateUserLink();
});
function updateUserLink() {
const uid = sel.value;
userLink.href = uid ? `/user?userId=${encodeURIComponent(uid)}` : '#';
}
populateUsers();
/* Re-populate when navigating back (fixes stale data after bfcache) */
window.addEventListener('pageshow', (e) => { if (e.persisted) populateUsers(); });
// Cleanup button
document.getElementById('cleanupBtn').addEventListener('click', () => {
if (!confirm('This will remove ALL application data (cached data, settings). Continue?')) return;
localStorage.removeItem('portal_selected_user');
localStorage.removeItem('user_language');
// Clear IndexedDB
indexedDB.databases().then(dbs => {
+117 -1
View File
@@ -55,6 +55,29 @@ body {
/* ── Toast notification ─────────────────────────────────────────────────── */
/* ── Desktop floating save bar (user task detail view, ≥md only) ─────────── */
.desktop-save-bar {
display: none; /* hidden until JS adds .is-visible */
position: fixed;
bottom: 1.5rem;
right: 1.5rem;
z-index: 200;
gap: 0.5rem;
align-items: center;
}
.desktop-save-bar.is-visible {
display: flex;
}
/* Never show the floating bar on mobile — mobile uses sidebar save buttons */
@media (max-width: 767.98px) {
.desktop-save-bar.is-visible {
display: none !important;
}
}
.admin-toast {
position: fixed;
top: 20px;
@@ -145,7 +168,7 @@ body {
.task-record-sort {
font-weight: 700;
font-size: 0.9rem;
color: var(--bs-primary);
color: #fff;
}
.task-record-desc {
@@ -320,6 +343,60 @@ body {
transform: rotate(90deg);
}
/* ── Category tabs (horizontal scroll / swipe on mobile) ───────────────── */
.tabs-scroll-wrapper {
position: relative;
display: flex;
align-items: stretch;
}
/* Arrow buttons — hidden by default, shown only when there is overflow */
.tabs-scroll-arrow {
display: none;
flex-shrink: 0;
align-items: center;
justify-content: center;
width: 28px;
background: #fff;
border: 1px solid #dee2e6;
border-bottom: none;
cursor: pointer;
z-index: 2;
color: #495057;
padding: 0;
transition: background 0.15s;
}
.tabs-scroll-arrow:hover {
background: #f8f9fa;
}
.tabs-scroll-arrow--left { border-radius: 4px 0 0 0; }
.tabs-scroll-arrow--right { border-radius: 0 4px 0 0; }
/* Show arrows only when the list overflows */
.tabs-scroll-wrapper.has-overflow .tabs-scroll-arrow {
display: flex;
}
/* Hide the arrow when already at that end */
.tabs-scroll-wrapper.at-start .tabs-scroll-arrow--left { opacity: 0.3; pointer-events: none; }
.tabs-scroll-wrapper.at-end .tabs-scroll-arrow--right { opacity: 0.3; pointer-events: none; }
#recordCategoryTabs {
flex: 1;
min-width: 0;
flex-wrap: nowrap;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
scrollbar-width: none;
}
#recordCategoryTabs::-webkit-scrollbar {
display: none;
}
/* ── Mobile hamburger button (hidden on desktop) ───────────────────────── */
.mobile-menu-btn {
@@ -386,4 +463,43 @@ body {
.mobile-menu-btn {
display: flex !important;
}
}
/* ── Mobile task list cards ─────────────────────────────────────────────── */
.task-list-card {
padding: 12px 16px;
border-bottom: 1px solid rgba(0,0,0,0.08);
display: flex;
align-items: center;
gap: 12px;
}
.task-list-card:last-child {
border-bottom: none;
}
.task-list-card-fields {
flex: 1;
display: flex;
flex-wrap: wrap;
gap: 6px 14px;
align-items: center;
}
.task-list-field {
display: flex;
flex-direction: column;
line-height: 1.2;
}
.task-list-field small {
font-size: 0.7rem;
color: #6c757d;
text-transform: uppercase;
letter-spacing: 0.03em;
}
.task-list-field strong {
font-size: 0.88rem;
}
+30 -17
View File
@@ -25,6 +25,7 @@
<p class="text-uppercase text-muted small fw-semibold mb-0">Hybrid Inspection Reporting</p>
<h5 class="fw-bold mb-0">Check List</h5>
<small class="text-muted">Task processing workspace</small>
<div class="mt-1"><small id="userDisplayName" class="text-muted fst-italic"></small></div>
</div>
<div class="p-3 border-bottom">
@@ -39,7 +40,15 @@
<div id="taskListSidebar"></div>
</div>
<!-- Mobile save actions (shown only when a task is open, hidden on desktop via d-md-none) -->
<div id="mobileSaveActions" class="p-3 border-top d-md-none" style="display:none">
<button id="mobileSaveDraftBtn" class="btn btn-outline-secondary btn-sm w-100 mb-1" type="button">Save as Draft</button>
<button id="mobileSaveFinalBtn" class="btn btn-primary btn-sm w-100" type="button">Save as Final</button>
</div>
<div class="p-3 border-top">
<button id="syncBtn" class="btn btn-outline-secondary btn-sm w-100 mb-1" type="button"><i class="bi bi-arrow-repeat me-1"></i>Sync</button>
<button id="logoutBtn" class="btn btn-outline-danger btn-sm w-100 mb-1" type="button"><i class="bi bi-box-arrow-right me-1"></i>Logout</button>
<button id="showSettingsBtn" class="btn btn-outline-primary btn-sm w-100 mb-1" type="button"><i class="bi bi-gear me-1"></i>Settings</button>
<a class="btn btn-secondary btn-sm w-100 mb-1" href="/user">User area</a>
<a class="btn btn-outline-secondary btn-sm w-100 mb-1" href="/admin">Admin area</a>
@@ -73,17 +82,10 @@
<!-- TASK LIST VIEW (shown by default) -->
<section id="taskListView" class="workspace-view workspace-view-active">
<div class="d-flex justify-content-between align-items-start mb-4">
<div>
<p class="text-muted small mb-0">User workspace</p>
<h3 class="fw-bold">Assigned Tasks</h3>
<p class="text-muted mb-0">Select a task to begin processing.</p>
</div>
<!-- Force a fresh fetch from the server (useful after a server restart
that cleared in-memory sessions — log in again first, then press Sync). -->
<button id="syncBtn" class="btn btn-outline-secondary btn-sm mt-1" type="button">
<i class="bi bi-arrow-repeat me-1"></i>Sync
</button>
<div class="mb-4">
<p class="text-muted small mb-0">User workspace</p>
<h3 class="fw-bold">Assigned Tasks</h3>
<p class="text-muted mb-0">Select a task to begin processing.</p>
</div>
<div class="card">
@@ -168,16 +170,21 @@
<input id="recordSearchInput" class="form-control form-control-sm" type="search" placeholder="Search records (full text)…" />
</div>
<!-- Category tabs -->
<ul id="recordCategoryTabs" class="nav nav-tabs mb-3"></ul>
<!-- Category tabs with scroll arrows for desktop -->
<div class="tabs-scroll-wrapper mb-3">
<button type="button" class="tabs-scroll-arrow tabs-scroll-arrow--left" id="tabsScrollLeft" aria-label="Scroll tabs left" tabindex="-1">
<i class="bi bi-chevron-left"></i>
</button>
<ul id="recordCategoryTabs" class="nav nav-tabs"></ul>
<button type="button" class="tabs-scroll-arrow tabs-scroll-arrow--right" id="tabsScrollRight" aria-label="Scroll tabs right" tabindex="-1">
<i class="bi bi-chevron-right"></i>
</button>
</div>
<!-- Records container (filtered by active tab + search) -->
<div id="taskRecordsContainer"></div>
<div class="d-flex gap-2 mt-4 pt-3 border-top">
<button id="saveDraftBtn" class="btn btn-outline-secondary" type="button">Save as Draft</button>
<button id="saveFinalBtn" class="btn btn-primary" type="button">Save as Final</button>
</div>
</form>
</div>
</div>
@@ -194,6 +201,12 @@
</main>
</div>
<!-- Desktop floating save bar — JS shows/hides this; CSS hides it on mobile -->
<div id="desktopSaveActions" class="desktop-save-bar">
<button id="saveDraftBtn" class="btn btn-outline-secondary shadow-sm" type="button">Save as Draft</button>
<button id="saveFinalBtn" class="btn btn-primary shadow-sm" type="button">Save as Final</button>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script type="module" src="/user-app.js"></script>
</body>