synch correction and db update
This commit is contained in:
+372
-266
@@ -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
File diff suppressed because it is too large
Load Diff
+170
-12
@@ -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;
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user