Refactor Lucide icon rendering by replacing nodes with root across components and fix unintended character encoding issues in placeholders and text.

This commit is contained in:
2026-05-17 17:30:04 -04:00
parent 17e05cb61d
commit 309f19520b
27 changed files with 694 additions and 1622 deletions
+629 -1557
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -92,7 +92,7 @@ function showLoginPage() {
</div> </div>
</div>`; </div>`;
if (window.lucide) lucide.createIcons({ nodes: [root] }); if (window.lucide) lucide.createIcons({ root: root });
root.querySelector('#login-form').addEventListener('submit', async e => { root.querySelector('#login-form').addEventListener('submit', async e => {
e.preventDefault(); e.preventDefault();
+1 -1
View File
@@ -35,7 +35,7 @@ class AppMobileNav extends HTMLElement {
<span>${t.label}</span> <span>${t.label}</span>
</a>`; </a>`;
}).join('')}`; }).join('')}`;
if (window.lucide) lucide.createIcons({ nodes: [this] }); if (window.lucide) lucide.createIcons({ root: this });
} }
} }
customElements.define('app-mobile-nav', AppMobileNav); customElements.define('app-mobile-nav', AppMobileNav);
+1 -1
View File
@@ -19,7 +19,7 @@ class AppRoot extends HTMLElement {
if (main) { if (main) {
main.innerHTML = html; main.innerHTML = html;
main.scrollTop = 0; main.scrollTop = 0;
if (window.lucide) lucide.createIcons({ nodes: [main] }); if (window.lucide) lucide.createIcons({ root: main });
} }
} }
} }
+1 -1
View File
@@ -131,7 +131,7 @@ class AppSidebar extends HTMLElement {
</button> </button>
</div>`; </div>`;
if (window.lucide) lucide.createIcons({ nodes: [this] }); if (window.lucide) lucide.createIcons({ root: this });
this.querySelector('#toggle-btn')?.addEventListener('click', () => this.#toggle()); this.querySelector('#toggle-btn')?.addEventListener('click', () => this.#toggle());
this.querySelector('#logout-btn')?.addEventListener('click', () => { this.querySelector('#logout-btn')?.addEventListener('click', () => {
clearToken(); clearToken();
+1 -1
View File
@@ -85,7 +85,7 @@ class AppTopbar extends HTMLElement {
</button> </button>
</div>` : ''}`; </div>` : ''}`;
if (window.lucide) lucide.createIcons({ nodes: [this] }); if (window.lucide) lucide.createIcons({ root: this });
this.querySelector('#user-menu-btn')?.addEventListener('click', e => { this.querySelector('#user-menu-btn')?.addEventListener('click', e => {
e.stopPropagation(); e.stopPropagation();
+1 -1
View File
@@ -71,7 +71,7 @@ class EquipmentForm extends HTMLElement {
<button class="btn-primary" id="dlg-save">${isEdit ? 'Save Changes' : 'Add Equipment'}</button> <button class="btn-primary" id="dlg-save">${isEdit ? 'Save Changes' : 'Add Equipment'}</button>
</div>`; </div>`;
if (window.lucide) lucide.createIcons({ nodes: [d] }); if (window.lucide) lucide.createIcons({ root: d });
d.querySelector('#dlg-close').addEventListener('click', () => d.close()); d.querySelector('#dlg-close').addEventListener('click', () => d.close());
d.querySelector('#dlg-cancel').addEventListener('click', () => d.close()); d.querySelector('#dlg-cancel').addEventListener('click', () => d.close());
+3 -3
View File
@@ -1,4 +1,4 @@
import { api } from '../../lib/api.mjs'; import { api } from '../../lib/api.mjs';
import './equipment-form.mjs'; import './equipment-form.mjs';
class EquipmentList extends HTMLElement { class EquipmentList extends HTMLElement {
@@ -64,7 +64,7 @@ class EquipmentList extends HTMLElement {
<div class="page-header"> <div class="page-header">
<h1>Equipment</h1> <h1>Equipment</h1>
<div class="toolbar"> <div class="toolbar">
<input class="search-input" id="search" placeholder="Search equipment" value="${this.#esc(this.#search)}"> <input class="search-input" id="search" placeholder="Search equipment…" value="${this.#esc(this.#search)}">
<button class="btn-primary" id="new-btn"> <button class="btn-primary" id="new-btn">
<i data-lucide="plus" style="width:14px;height:14px"></i> Add Equipment <i data-lucide="plus" style="width:14px;height:14px"></i> Add Equipment
</button> </button>
@@ -102,7 +102,7 @@ class EquipmentList extends HTMLElement {
<equipment-form id="equipment-form"></equipment-form>`; <equipment-form id="equipment-form"></equipment-form>`;
if (window.lucide) lucide.createIcons({ nodes: [s] }); if (window.lucide) lucide.createIcons({ root: s });
const searchEl = s.querySelector('#search'); const searchEl = s.querySelector('#search');
let debounce; let debounce;
+1 -1
View File
@@ -71,7 +71,7 @@ class MaterialForm extends HTMLElement {
<button class="btn-primary" id="dlg-save">${isEdit ? 'Save Changes' : 'Add Material'}</button> <button class="btn-primary" id="dlg-save">${isEdit ? 'Save Changes' : 'Add Material'}</button>
</div>`; </div>`;
if (window.lucide) lucide.createIcons({ nodes: [d] }); if (window.lucide) lucide.createIcons({ root: d });
d.querySelector('#dlg-close').addEventListener('click', () => d.close()); d.querySelector('#dlg-close').addEventListener('click', () => d.close());
d.querySelector('#dlg-cancel').addEventListener('click', () => d.close()); d.querySelector('#dlg-cancel').addEventListener('click', () => d.close());
+3 -3
View File
@@ -1,4 +1,4 @@
import { api } from '../../lib/api.mjs'; import { api } from '../../lib/api.mjs';
import './material-form.mjs'; import './material-form.mjs';
class MaterialList extends HTMLElement { class MaterialList extends HTMLElement {
@@ -65,7 +65,7 @@ class MaterialList extends HTMLElement {
<div class="page-header"> <div class="page-header">
<h1>Materials</h1> <h1>Materials</h1>
<div class="toolbar"> <div class="toolbar">
<input class="search-input" id="search" placeholder="Search materials" value="${this.#esc(this.#search)}"> <input class="search-input" id="search" placeholder="Search materials…" value="${this.#esc(this.#search)}">
<button class="btn-primary" id="new-btn"> <button class="btn-primary" id="new-btn">
<i data-lucide="plus" style="width:14px;height:14px"></i> Add Material <i data-lucide="plus" style="width:14px;height:14px"></i> Add Material
</button> </button>
@@ -103,7 +103,7 @@ class MaterialList extends HTMLElement {
<material-form id="material-form"></material-form>`; <material-form id="material-form"></material-form>`;
if (window.lucide) lucide.createIcons({ nodes: [s] }); if (window.lucide) lucide.createIcons({ root: s });
const searchEl = s.querySelector('#search'); const searchEl = s.querySelector('#search');
let debounce; let debounce;
+1 -1
View File
@@ -70,7 +70,7 @@ class PeopleForm extends HTMLElement {
<button class="btn-primary" id="dlg-save">${isEdit ? 'Save Changes' : 'Add Person'}</button> <button class="btn-primary" id="dlg-save">${isEdit ? 'Save Changes' : 'Add Person'}</button>
</div>`; </div>`;
if (window.lucide) lucide.createIcons({ nodes: [d] }); if (window.lucide) lucide.createIcons({ root: d });
d.querySelector('#dlg-close').addEventListener('click', () => d.close()); d.querySelector('#dlg-close').addEventListener('click', () => d.close());
d.querySelector('#dlg-cancel').addEventListener('click', () => d.close()); d.querySelector('#dlg-cancel').addEventListener('click', () => d.close());
+3 -3
View File
@@ -1,4 +1,4 @@
import { api } from '../../lib/api.mjs'; import { api } from '../../lib/api.mjs';
import './people-form.mjs'; import './people-form.mjs';
class PeopleList extends HTMLElement { class PeopleList extends HTMLElement {
@@ -76,7 +76,7 @@ class PeopleList extends HTMLElement {
<div class="page-header"> <div class="page-header">
<h1>People</h1> <h1>People</h1>
<div class="toolbar"> <div class="toolbar">
<input class="search-input" id="search" placeholder="Search people" value="${this.#esc(this.#search)}"> <input class="search-input" id="search" placeholder="Search people…" value="${this.#esc(this.#search)}">
<button class="btn-primary" id="new-btn"> <button class="btn-primary" id="new-btn">
<i data-lucide="user-plus" style="width:14px;height:14px"></i> Add Person <i data-lucide="user-plus" style="width:14px;height:14px"></i> Add Person
</button> </button>
@@ -115,7 +115,7 @@ class PeopleList extends HTMLElement {
<people-form id="people-form"></people-form>`; <people-form id="people-form"></people-form>`;
if (window.lucide) lucide.createIcons({ nodes: [s] }); if (window.lucide) lucide.createIcons({ root: s });
const searchEl = s.querySelector('#search'); const searchEl = s.querySelector('#search');
let debounce; let debounce;
+1 -1
View File
@@ -110,7 +110,7 @@ class ProfileForm extends HTMLElement {
<button class="btn-primary" id="dlg-save">${isEdit ? 'Save Changes' : 'Create Profile'}</button> <button class="btn-primary" id="dlg-save">${isEdit ? 'Save Changes' : 'Create Profile'}</button>
</div>`; </div>`;
if (window.lucide) lucide.createIcons({ nodes: [d] }); if (window.lucide) lucide.createIcons({ root: d });
// Local step state (not saved until the profile save) // Local step state (not saved until the profile save)
const profileId = profile?.id || null; const profileId = profile?.id || null;
+4 -4
View File
@@ -1,4 +1,4 @@
import { api } from '../../lib/api.mjs'; import { api } from '../../lib/api.mjs';
import './profile-form.mjs'; import './profile-form.mjs';
const PRIORITY_COLORS = { low: '#64748B', normal: '#0A7EA4', high: '#E07B39', urgent: '#C0392B' }; const PRIORITY_COLORS = { low: '#64748B', normal: '#0A7EA4', high: '#E07B39', urgent: '#C0392B' };
@@ -68,7 +68,7 @@ class ProfileList extends HTMLElement {
<div class="page-header"> <div class="page-header">
<h1>Work Order Profiles</h1> <h1>Work Order Profiles</h1>
<div class="toolbar"> <div class="toolbar">
<input class="search-input" id="search" placeholder="Search profiles" value="${this.#esc(this.#search)}"> <input class="search-input" id="search" placeholder="Search profiles…" value="${this.#esc(this.#search)}">
<button class="btn-primary" id="new-btn"> <button class="btn-primary" id="new-btn">
<i data-lucide="plus" style="width:14px;height:14px"></i> New Profile <i data-lucide="plus" style="width:14px;height:14px"></i> New Profile
</button> </button>
@@ -92,7 +92,7 @@ class ProfileList extends HTMLElement {
<span class="pri-dot" style="background:${PRIORITY_COLORS[p.default_priority] || PRIORITY_COLORS.normal}"></span> <span class="pri-dot" style="background:${PRIORITY_COLORS[p.default_priority] || PRIORITY_COLORS.normal}"></span>
<span>${p.default_priority}</span> <span>${p.default_priority}</span>
<span class="step-count"><i data-lucide="list-checks" style="width:11px;height:11px;vertical-align:middle"></i> ${p.step_count} step${p.step_count !== 1 ? 's' : ''}</span> <span class="step-count"><i data-lucide="list-checks" style="width:11px;height:11px;vertical-align:middle"></i> ${p.step_count} step${p.step_count !== 1 ? 's' : ''}</span>
${p.default_duration_hours ? `<span> ${p.default_duration_hours}h</span>` : ''} ${p.default_duration_hours ? `<span>⏱ ${p.default_duration_hours}h</span>` : ''}
</div> </div>
</div> </div>
<span class="status-badge ${p.active ? 'status-active' : 'status-inactive'}">${p.active ? 'Active' : 'Inactive'}</span> <span class="status-badge ${p.active ? 'status-active' : 'status-inactive'}">${p.active ? 'Active' : 'Inactive'}</span>
@@ -109,7 +109,7 @@ class ProfileList extends HTMLElement {
<profile-form id="profile-form"></profile-form>`; <profile-form id="profile-form"></profile-form>`;
if (window.lucide) lucide.createIcons({ nodes: [s] }); if (window.lucide) lucide.createIcons({ root: s });
const searchEl = s.querySelector('#search'); const searchEl = s.querySelector('#search');
let debounce; let debounce;
+1 -1
View File
@@ -68,7 +68,7 @@ class VehicleForm extends HTMLElement {
<button class="btn-primary" id="dlg-save">${isEdit ? 'Save Changes' : 'Add Vehicle'}</button> <button class="btn-primary" id="dlg-save">${isEdit ? 'Save Changes' : 'Add Vehicle'}</button>
</div>`; </div>`;
if (window.lucide) lucide.createIcons({ nodes: [d] }); if (window.lucide) lucide.createIcons({ root: d });
d.querySelector('#dlg-close').addEventListener('click', () => d.close()); d.querySelector('#dlg-close').addEventListener('click', () => d.close());
d.querySelector('#dlg-cancel').addEventListener('click', () => d.close()); d.querySelector('#dlg-cancel').addEventListener('click', () => d.close());
+3 -3
View File
@@ -1,4 +1,4 @@
import { api } from '../../lib/api.mjs'; import { api } from '../../lib/api.mjs';
import './vehicle-form.mjs'; import './vehicle-form.mjs';
class VehicleList extends HTMLElement { class VehicleList extends HTMLElement {
@@ -63,7 +63,7 @@ class VehicleList extends HTMLElement {
<div class="page-header"> <div class="page-header">
<h1>Vehicles</h1> <h1>Vehicles</h1>
<div class="toolbar"> <div class="toolbar">
<input class="search-input" id="search" placeholder="Search vehicles" value="${this.#esc(this.#search)}"> <input class="search-input" id="search" placeholder="Search vehicles…" value="${this.#esc(this.#search)}">
<button class="btn-primary" id="new-btn"> <button class="btn-primary" id="new-btn">
<i data-lucide="plus" style="width:14px;height:14px"></i> Add Vehicle <i data-lucide="plus" style="width:14px;height:14px"></i> Add Vehicle
</button> </button>
@@ -101,7 +101,7 @@ class VehicleList extends HTMLElement {
<vehicle-form id="vehicle-form"></vehicle-form>`; <vehicle-form id="vehicle-form"></vehicle-form>`;
if (window.lucide) lucide.createIcons({ nodes: [s] }); if (window.lucide) lucide.createIcons({ root: s });
const searchEl = s.querySelector('#search'); const searchEl = s.querySelector('#search');
let debounce; let debounce;
+1 -1
View File
@@ -47,7 +47,7 @@ class UiDialog extends HTMLElement {
this.shadowRoot.querySelector('.close-btn').addEventListener('click', () => this.close()); this.shadowRoot.querySelector('.close-btn').addEventListener('click', () => this.close());
dialog.addEventListener('click', e => { if (e.target === dialog) this.close(); }); dialog.addEventListener('click', e => { if (e.target === dialog) this.close(); });
document.addEventListener('keydown', e => { if (e.key === 'Escape') this.close(); }); document.addEventListener('keydown', e => { if (e.key === 'Escape') this.close(); });
if (window.lucide) lucide.createIcons({ nodes: [this.shadowRoot] }); if (window.lucide) lucide.createIcons({ root: this.shadowRoot });
} }
open() { this.shadowRoot?.querySelector('dialog')?.showModal(); } open() { this.shadowRoot?.querySelector('dialog')?.showModal(); }
+1 -1
View File
@@ -29,7 +29,7 @@ class UiEmpty extends HTMLElement {
${body ? `<p>${body}</p>` : ''} ${body ? `<p>${body}</p>` : ''}
<slot></slot> <slot></slot>
</div>`; </div>`;
if (window.lucide) lucide.createIcons({ nodes: [this.shadowRoot] }); if (window.lucide) lucide.createIcons({ root: this.shadowRoot });
} }
} }
customElements.define('ui-empty', UiEmpty); customElements.define('ui-empty', UiEmpty);
+1 -1
View File
@@ -22,7 +22,7 @@ class UiToastContainer extends HTMLElement {
}); });
toast.innerHTML = `<i data-lucide="${icons[type] || 'info'}" style="width:16px;height:16px;flex-shrink:0"></i><span>${message}</span>`; toast.innerHTML = `<i data-lucide="${icons[type] || 'info'}" style="width:16px;height:16px;flex-shrink:0"></i><span>${message}</span>`;
this.appendChild(toast); this.appendChild(toast);
if (window.lucide) lucide.createIcons({ nodes: [toast] }); if (window.lucide) lucide.createIcons({ root: toast });
const style = document.createElement('style'); const style = document.createElement('style');
style.textContent = `@keyframes toastIn{from{opacity:0;transform:translateX(1rem)}to{opacity:1;transform:none}}`; style.textContent = `@keyframes toastIn{from{opacity:0;transform:translateX(1rem)}to{opacity:1;transform:none}}`;
+6 -6
View File
@@ -1,4 +1,4 @@
import { api } from '../../lib/api.mjs'; import { api } from '../../lib/api.mjs';
class WoChecklist extends HTMLElement { class WoChecklist extends HTMLElement {
#woId = null; #woId = null;
@@ -120,7 +120,7 @@ class WoChecklist extends HTMLElement {
${allDone ? ` ${allDone ? `
<div class="all-done"> <div class="all-done">
<i data-lucide="check-circle-2" style="width:18px;height:18px;flex-shrink:0"></i> <i data-lucide="check-circle-2" style="width:18px;height:18px;flex-shrink:0"></i>
All ${total} steps complete ready for review! All ${total} steps complete — ready for review!
</div>` : ''} </div>` : ''}
<div class="progress-wrap"> <div class="progress-wrap">
@@ -133,7 +133,7 @@ class WoChecklist extends HTMLElement {
<div class="steps-list"> <div class="steps-list">
${total === 0 ${total === 0
? '<div class="empty-steps">No steps yet add your first step below</div>' ? '<div class="empty-steps">No steps yet — add your first step below</div>'
: this.#steps.map(st => ` : this.#steps.map(st => `
<div class="step-row${st.completed ? ' done' : ''}"> <div class="step-row${st.completed ? ' done' : ''}">
<div class="step-check-wrap"> <div class="step-check-wrap">
@@ -150,7 +150,7 @@ class WoChecklist extends HTMLElement {
<div class="step-meta"> <div class="step-meta">
<i data-lucide="user-check" style="width:12px;height:12px"></i> <i data-lucide="user-check" style="width:12px;height:12px"></i>
${this.#esc(st.completed_by)} ${this.#esc(st.completed_by)}
${st.completed_at ? '· ' + new Date(st.completed_at).toLocaleString() : ''} ${st.completed_at ? '· ' + new Date(st.completed_at).toLocaleString() : ''}
</div>` : ''} </div>` : ''}
${st.notes ? `<div class="step-note">"${this.#esc(st.notes)}"</div>` : ''} ${st.notes ? `<div class="step-note">"${this.#esc(st.notes)}"</div>` : ''}
</div> </div>
@@ -169,14 +169,14 @@ class WoChecklist extends HTMLElement {
<div class="add-section"> <div class="add-section">
<div class="add-section-label">Add Step</div> <div class="add-section-label">Add Step</div>
<div class="add-row"> <div class="add-row">
<input class="add-input" id="new-step-title" placeholder="Step title" maxlength="200"> <input class="add-input" id="new-step-title" placeholder="Step title…" maxlength="200">
<button class="add-btn" id="add-step-btn"> <button class="add-btn" id="add-step-btn">
<i data-lucide="plus" style="width:14px;height:14px;vertical-align:middle"></i> Add <i data-lucide="plus" style="width:14px;height:14px;vertical-align:middle"></i> Add
</button> </button>
</div> </div>
</div>`; </div>`;
if (window.lucide) lucide.createIcons({ nodes: [s] }); if (window.lucide) lucide.createIcons({ root: s });
s.querySelectorAll('.step-check').forEach(cb => { s.querySelectorAll('.step-check').forEach(cb => {
cb.addEventListener('change', () => { cb.addEventListener('change', () => {
+2 -2
View File
@@ -43,7 +43,7 @@ class WoDetail extends HTMLElement {
const s = this.shadowRoot; const s = this.shadowRoot;
s.innerHTML = `<style>${this.#css()}</style>${this.#html()}`; s.innerHTML = `<style>${this.#css()}</style>${this.#html()}`;
this.#bind(); this.#bind();
if (window.lucide) lucide.createIcons({ nodes: [s] }); if (window.lucide) lucide.createIcons({ root: s });
} }
#css() { return ` #css() { return `
@@ -297,7 +297,7 @@ class WoDetail extends HTMLElement {
this.#tab = tab.dataset.tab; this.#tab = tab.dataset.tab;
s.querySelectorAll('.tab').forEach(t => t.classList.toggle('active', t.dataset.tab === this.#tab)); s.querySelectorAll('.tab').forEach(t => t.classList.toggle('active', t.dataset.tab === this.#tab));
s.querySelector('#tab-content').innerHTML = this.#tabContent(this.#wo); s.querySelector('#tab-content').innerHTML = this.#tabContent(this.#wo);
if (window.lucide) lucide.createIcons({ nodes: [s.querySelector('#tab-content')] }); if (window.lucide) lucide.createIcons({ root: s.querySelector('#tab-content') });
})); }));
// Status dropdown // Status dropdown
+9 -9
View File
@@ -1,4 +1,4 @@
import { api } from '../../lib/api.mjs'; import { api } from '../../lib/api.mjs';
import { toLocalDatetime } from '../../lib/format.mjs'; import { toLocalDatetime } from '../../lib/format.mjs';
const STATUSES = ['draft','assigned','scheduled','in_progress','pending_review','closed']; const STATUSES = ['draft','assigned','scheduled','in_progress','pending_review','closed'];
@@ -30,7 +30,7 @@ class WoForm extends HTMLElement {
const s = this.shadowRoot; const s = this.shadowRoot;
s.innerHTML = `<style>${this.#css()}</style>${this.#html(wo, isNew)}`; s.innerHTML = `<style>${this.#css()}</style>${this.#html(wo, isNew)}`;
this.#bind(); this.#bind();
if (window.lucide) lucide.createIcons({ nodes: [s] }); if (window.lucide) lucide.createIcons({ root: s });
} }
#css() { return ` #css() { return `
@@ -97,14 +97,14 @@ class WoForm extends HTMLElement {
</button>` : ''} </button>` : ''}
</div> </div>
<label>Title *<input name="title" required value="${v('title')}" placeholder="Brief description of the work"></label> <label>Title *<input name="title" required value="${v('title')}" placeholder="Brief description of the work"></label>
<label>Description<textarea name="description" rows="3" placeholder="What needs to be done">${v('description')}</textarea></label> <label>Description<textarea name="description" rows="3" placeholder="What needs to be done…">${v('description')}</textarea></label>
<label>Instructions<textarea name="instructions" rows="5" placeholder="Step-by-step instructions for the field crew">${v('instructions')}</textarea></label> <label>Instructions<textarea name="instructions" rows="5" placeholder="Step-by-step instructions for the field crew…">${v('instructions')}</textarea></label>
</div> </div>
<div class="card"> <div class="card">
<div class="card-title">Location</div> <div class="card-title">Location</div>
<label>Site Name<input name="site_name" value="${v('site_name')}" placeholder="e.g. Downtown Office"></label> <label>Site Name<input name="site_name" value="${v('site_name')}" placeholder="e.g. Downtown Office"></label>
<label>Address<input name="address" value="${v('address')}" placeholder="Street address"></label> <label>Address<input name="address" value="${v('address')}" placeholder="Street address"></label>
<label>Access Notes<textarea name="access_notes" rows="2" placeholder="Gate codes, road conditions, parking info">${v('access_notes')}</textarea></label> <label>Access Notes<textarea name="access_notes" rows="2" placeholder="Gate codes, road conditions, parking info…">${v('access_notes')}</textarea></label>
</div> </div>
</div> </div>
@@ -164,8 +164,8 @@ class WoForm extends HTMLElement {
<button class="lp-close" id="lp-close"><i data-lucide="x" style="width:16px;height:16px"></i></button> <button class="lp-close" id="lp-close"><i data-lucide="x" style="width:16px;height:16px"></i></button>
</div> </div>
<div class="lp-body"> <div class="lp-body">
<input class="lp-search" id="lp-search" placeholder="Search profiles" autocomplete="off"> <input class="lp-search" id="lp-search" placeholder="Search profiles…" autocomplete="off">
<div class="lp-list" id="lp-list"><div class="lp-empty">Loading</div></div> <div class="lp-list" id="lp-list"><div class="lp-empty">Loading…</div></div>
</div> </div>
<div class="lp-footer"> <div class="lp-footer">
<button class="btn btn-ghost" id="lp-cancel">Cancel</button> <button class="btn btn-ghost" id="lp-cancel">Cancel</button>
@@ -262,7 +262,7 @@ class WoForm extends HTMLElement {
listEl.innerHTML = items.map(p => ` listEl.innerHTML = items.map(p => `
<div class="lp-item${selectedProfile?.id === p.id ? ' selected' : ''}" data-id="${p.id}"> <div class="lp-item${selectedProfile?.id === p.id ? ' selected' : ''}" data-id="${p.id}">
<div class="lp-item-name">${this.#esc(p.name)}</div> <div class="lp-item-name">${this.#esc(p.name)}</div>
<div class="lp-item-meta">${p.category ? `${p.category} · ` : ''}${p.step_count} step${p.step_count !== 1 ? 's' : ''} · ${p.default_priority} priority${p.default_duration_hours ? ` · ${p.default_duration_hours}h` : ''}</div> <div class="lp-item-meta">${p.category ? `${p.category} · ` : ''}${p.step_count} step${p.step_count !== 1 ? 's' : ''} · ${p.default_priority} priority${p.default_duration_hours ? ` · ${p.default_duration_hours}h` : ''}</div>
</div>`).join(''); </div>`).join('');
listEl.querySelectorAll('.lp-item').forEach(item => { listEl.querySelectorAll('.lp-item').forEach(item => {
item.addEventListener('click', () => { item.addEventListener('click', () => {
@@ -277,7 +277,7 @@ class WoForm extends HTMLElement {
selectedProfile = null; selectedProfile = null;
confirmBtn.disabled = true; confirmBtn.disabled = true;
searchEl.value = ''; searchEl.value = '';
listEl.innerHTML = '<div class="lp-empty">Loading</div>'; listEl.innerHTML = '<div class="lp-empty">Loading…</div>';
dialog.showModal(); dialog.showModal();
try { try {
allProfiles = await api.get('/profiles') || []; allProfiles = await api.get('/profiles') || [];
+5 -5
View File
@@ -1,4 +1,4 @@
import { api } from '../../lib/api.mjs'; import { api } from '../../lib/api.mjs';
import { formatDateTime } from '../../lib/format.mjs'; import { formatDateTime } from '../../lib/format.mjs';
const STATUSES = ['draft','assigned','scheduled','in_progress','pending_review','closed']; const STATUSES = ['draft','assigned','scheduled','in_progress','pending_review','closed'];
@@ -38,7 +38,7 @@ class WoList extends HTMLElement {
const s = this.shadowRoot; const s = this.shadowRoot;
s.innerHTML = `<style>${this.#css()}</style>${this.#html()}`; s.innerHTML = `<style>${this.#css()}</style>${this.#html()}`;
this.#bind(); this.#bind();
if (window.lucide) lucide.createIcons({ nodes: [s] }); if (window.lucide) lucide.createIcons({ root: s });
} }
#css() { return ` #css() { return `
@@ -139,7 +139,7 @@ class WoList extends HTMLElement {
return `<div class="filter-bar"> return `<div class="filter-bar">
<div class="search-wrap"> <div class="search-wrap">
<i data-lucide="search"></i> <i data-lucide="search"></i>
<input class="search-input" id="search" type="search" placeholder="Search work orders" value="${this.#filters.search}"> <input class="search-input" id="search" type="search" placeholder="Search work orders…" value="${this.#filters.search}">
</div> </div>
<select class="filter" id="filter-status"> <select class="filter" id="filter-status">
<option value="">All statuses</option> <option value="">All statuses</option>
@@ -167,7 +167,7 @@ class WoList extends HTMLElement {
<tr class="wo-row" data-id="${wo.id}"> <tr class="wo-row" data-id="${wo.id}">
<td class="wo-num">${wo.wo_number}</td> <td class="wo-num">${wo.wo_number}</td>
<td class="wo-title">${this.#esc(wo.title)}</td> <td class="wo-title">${this.#esc(wo.title)}</td>
<td>${this.#esc(wo.site_name) || '<span style="color:var(--text-muted)"></span>'}</td> <td>${this.#esc(wo.site_name) || '<span style="color:var(--text-muted)">—</span>'}</td>
<td><ui-badge type="status" value="${wo.status}"></ui-badge></td> <td><ui-badge type="status" value="${wo.status}"></ui-badge></td>
<td> <td>
<span class="priority-dot" style="background:${PRI_COLOR[wo.priority]}"></span> <span class="priority-dot" style="background:${PRI_COLOR[wo.priority]}"></span>
@@ -200,7 +200,7 @@ class WoList extends HTMLElement {
</div> </div>
<ui-badge type="status" value="${wo.status}"></ui-badge> <ui-badge type="status" value="${wo.status}"></ui-badge>
</div> </div>
<div class="card-meta">${this.#esc(wo.site_name) || ''} · ${wo.steps_done}/${wo.step_count} steps</div> <div class="card-meta">${this.#esc(wo.site_name) || '—'} · ${wo.steps_done}/${wo.step_count} steps</div>
</div>`).join('')} </div>`).join('')}
</div>`; </div>`;
} }
+1 -1
View File
@@ -54,7 +54,7 @@ class WoMap extends HTMLElement {
</div>` : ''} </div>` : ''}
</div>`; </div>`;
if (window.lucide) lucide.createIcons({ nodes: [this] }); if (window.lucide) lucide.createIcons({ root: this });
if (lat && lng) { if (lat && lng) {
this.#initMap(lat, lng, siteName, woNumber); this.#initMap(lat, lng, siteName, woNumber);
@@ -202,7 +202,7 @@ class WoPhotoPanel extends HTMLElement {
</div> </div>
</dialog>`; </dialog>`;
if (window.lucide) lucide.createIcons({ nodes: [s] }); if (window.lucide) lucide.createIcons({ root: s });
this.#bindEvents(filtered); this.#bindEvents(filtered);
} }
@@ -261,7 +261,7 @@ class WoPhotoPanel extends HTMLElement {
preview.innerHTML = Array.from(files).map(f => preview.innerHTML = Array.from(files).map(f =>
`<div class="preview-item"><i data-lucide="image" style="width:13px;height:13px"></i> ${this.#esc(f.name)}</div>` `<div class="preview-item"><i data-lucide="image" style="width:13px;height:13px"></i> ${this.#esc(f.name)}</div>`
).join(''); ).join('');
if (window.lucide) lucide.createIcons({ nodes: [preview] }); if (window.lucide) lucide.createIcons({ root: preview });
btn.disabled = files.length === 0; btn.disabled = files.length === 0;
} }
@@ -282,7 +282,7 @@ class WoPhotoPanel extends HTMLElement {
<button class="lightbox-next"><i data-lucide="chevron-right" style="width:20px;height:20px"></i></button>` : ''} <button class="lightbox-next"><i data-lucide="chevron-right" style="width:20px;height:20px"></i></button>` : ''}
${p.caption ? `<div class="lightbox-caption">${this.#esc(p.caption)}</div>` : ''}`; ${p.caption ? `<div class="lightbox-caption">${this.#esc(p.caption)}</div>` : ''}`;
s.appendChild(lb); s.appendChild(lb);
if (window.lucide) lucide.createIcons({ nodes: [lb] }); if (window.lucide) lucide.createIcons({ root: lb });
lb.querySelector('.lightbox-close').addEventListener('click', () => lb.remove()); lb.querySelector('.lightbox-close').addEventListener('click', () => lb.remove());
lb.addEventListener('click', e => { if (e.target === lb) lb.remove(); }); lb.addEventListener('click', e => { if (e.target === lb) lb.remove(); });
lb.querySelector('.lightbox-prev')?.addEventListener('click', e => { e.stopPropagation(); show(i - 1); }); lb.querySelector('.lightbox-prev')?.addEventListener('click', e => { e.stopPropagation(); show(i - 1); });
@@ -1,4 +1,4 @@
import { api } from '../../lib/api.mjs'; import { api } from '../../lib/api.mjs';
const SECTIONS = [ const SECTIONS = [
{ type: 'person', label: 'People', icon: 'users', endpoint: 'people' }, { type: 'person', label: 'People', icon: 'users', endpoint: 'people' },
@@ -67,8 +67,8 @@ class WoResourcePanel extends HTMLElement {
} }
#resourceName(item, type) { #resourceName(item, type) {
if (type === 'vehicle') return `${item.unit_number}${item.description ? ' ' + item.description : ''}`; if (type === 'vehicle') return `${item.unit_number}${item.description ? ' – ' + item.description : ''}`;
return item.name || item.unit_number || ''; return item.name || item.unit_number || '—';
} }
#sectionHTML(section) { #sectionHTML(section) {
@@ -166,7 +166,7 @@ class WoResourcePanel extends HTMLElement {
<button class="dialog-close" id="picker-close"><i data-lucide="x" style="width:16px;height:16px"></i></button> <button class="dialog-close" id="picker-close"><i data-lucide="x" style="width:16px;height:16px"></i></button>
</div> </div>
<div class="dialog-body"> <div class="dialog-body">
<input class="picker-search" id="picker-search" placeholder="Search" autocomplete="off"> <input class="picker-search" id="picker-search" placeholder="Search…" autocomplete="off">
<div class="picker-list" id="picker-list"></div> <div class="picker-list" id="picker-list"></div>
<div class="qty-row" id="qty-row" style="display:none"> <div class="qty-row" id="qty-row" style="display:none">
<label class="qty-label" for="qty-input">Quantity:</label> <label class="qty-label" for="qty-input">Quantity:</label>
@@ -179,7 +179,7 @@ class WoResourcePanel extends HTMLElement {
</div> </div>
</dialog>`; </dialog>`;
if (window.lucide) lucide.createIcons({ nodes: [s] }); if (window.lucide) lucide.createIcons({ root: s });
this.#bindEvents(); this.#bindEvents();
} }
@@ -250,7 +250,7 @@ class WoResourcePanel extends HTMLElement {
container.innerHTML = available.map(item => { container.innerHTML = available.map(item => {
const name = type === 'vehicle' const name = type === 'vehicle'
? `${item.unit_number}${item.description ? ' ' + item.description : ''}` ? `${item.unit_number}${item.description ? ' – ' + item.description : ''}`
: item.name; : item.name;
const sub = item.role || item.vehicle_type || item.category || (item.part_number ? `#${item.part_number}` : '') || ''; const sub = item.role || item.vehicle_type || item.category || (item.part_number ? `#${item.part_number}` : '') || '';
return ` return `
+4 -4
View File
@@ -1,4 +1,4 @@
import { api } from '../../lib/api.mjs'; import { api } from '../../lib/api.mjs';
const ACTION_ICONS = { const ACTION_ICONS = {
status_change: 'refresh-cw', status_change: 'refresh-cw',
@@ -122,7 +122,7 @@ class WoTimeline extends HTMLElement {
${this.#detailHTML(e)} ${this.#detailHTML(e)}
<div class="entry-meta"> <div class="entry-meta">
<span class="entry-user">${this.#esc(e.performed_by)}</span> <span class="entry-user">${this.#esc(e.performed_by)}</span>
<span>·</span> <span>·</span>
<span title="${new Date(e.performed_at).toLocaleString()}">${this.#relativeTime(e.performed_at)}</span> <span title="${new Date(e.performed_at).toLocaleString()}">${this.#relativeTime(e.performed_at)}</span>
</div> </div>
</div> </div>
@@ -134,7 +134,7 @@ class WoTimeline extends HTMLElement {
<i data-lucide="rotate-cw" style="width:13px;height:13px"></i> Refresh <i data-lucide="rotate-cw" style="width:13px;height:13px"></i> Refresh
</button>`; </button>`;
if (window.lucide) lucide.createIcons({ nodes: [s] }); if (window.lucide) lucide.createIcons({ root: s });
s.querySelector('#refresh-btn').addEventListener('click', () => this.#load()); s.querySelector('#refresh-btn').addEventListener('click', () => this.#load());
} }
@@ -143,7 +143,7 @@ class WoTimeline extends HTMLElement {
return `<div class="entry-detail"> return `<div class="entry-detail">
<span class="status-change"> <span class="status-change">
<span class="status-badge">${this.#esc(e.old_value.replace('_',' '))}</span> <span class="status-badge">${this.#esc(e.old_value.replace('_',' '))}</span>
<span class="status-arrow"></span> <span class="status-arrow">→</span>
<span class="status-badge" style="background:var(--surface-2)">${this.#esc(e.new_value.replace('_',' '))}</span> <span class="status-badge" style="background:var(--surface-2)">${this.#esc(e.new_value.replace('_',' '))}</span>
</span> </span>
</div>`; </div>`;