435 lines
21 KiB
JavaScript
435 lines
21 KiB
JavaScript
import { api } from '../../lib/api.mjs';
|
|
import { formatDateTime, formatDate } from '../../lib/format.mjs';
|
|
import './wo-checklist.mjs';
|
|
import './wo-resource-panel.mjs';
|
|
import './wo-photo-panel.mjs';
|
|
import './wo-map.mjs';
|
|
import './wo-accounting.mjs';
|
|
import './wo-timeline.mjs';
|
|
|
|
const TABS = ['Overview', 'Checklist', 'Resources', 'Photos', 'Accounting', 'Activity'];
|
|
const STATUS_TRANSITIONS = {
|
|
draft: ['assigned','scheduled'],
|
|
assigned: ['scheduled','in_progress'],
|
|
scheduled: ['in_progress'],
|
|
in_progress: ['pending_review','closed'],
|
|
pending_review: ['in_progress','closed'],
|
|
closed: [],
|
|
};
|
|
|
|
class WoDetail extends HTMLElement {
|
|
#woId = null;
|
|
#wo = null;
|
|
#tab = 'Overview';
|
|
#loading = true;
|
|
|
|
static get observedAttributes() { return ['wo-id']; }
|
|
attributeChangedCallback(_, __, val) { this.#woId = val ? +val : null; if (this.shadowRoot) this.#load(); }
|
|
|
|
connectedCallback() {
|
|
this.attachShadow({ mode: 'open' });
|
|
if (this.#woId) this.#load();
|
|
}
|
|
|
|
async #load() {
|
|
this.#loading = true;
|
|
this.#render();
|
|
try { this.#wo = await api.get(`/work-orders/${this.#woId}`); } catch { this.#wo = null; }
|
|
this.#loading = false;
|
|
this.#render();
|
|
}
|
|
|
|
#render() {
|
|
const s = this.shadowRoot;
|
|
s.innerHTML = `<style>${this.#css()}</style>${this.#html()}`;
|
|
this.#bind();
|
|
if (window.lucide) lucide.createIcons({ nodes: [s] });
|
|
}
|
|
|
|
#css() { return `
|
|
:host { display: block; }
|
|
.page-header { display:flex; align-items:center; gap:1rem; margin-bottom:1.25rem; flex-wrap:wrap; }
|
|
.back-btn { background:none; border:1px solid var(--border); color:var(--text); border-radius:var(--radius); padding:.4rem .75rem; font-size:.813rem; font-weight:500; cursor:pointer; display:flex; align-items:center; gap:.3rem; transition:background .15s; flex-shrink:0; }
|
|
.back-btn:hover { background:var(--surface-2); }
|
|
.header-info { flex:1; min-width:0; }
|
|
.wo-number { font-family:monospace; font-size:.813rem; color:var(--text-muted); margin-bottom:.15rem; }
|
|
h1 { font-size:1.2rem; font-weight:700; color:var(--text); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
.header-badges { display:flex; align-items:center; gap:.5rem; margin-top:.35rem; flex-wrap:wrap; }
|
|
.header-actions { display:flex; gap:.5rem; flex-shrink:0; }
|
|
.btn { display:inline-flex; align-items:center; gap:.35rem; padding:.45rem .9rem; border:none; border-radius:var(--radius); font-size:.813rem; font-weight:600; cursor:pointer; transition:opacity .15s,background .15s; white-space:nowrap; }
|
|
.btn-primary { background:var(--teal); color:#fff; }
|
|
.btn-primary:hover { opacity:.88; }
|
|
.btn-ghost { background:transparent; border:1px solid var(--border); color:var(--text); }
|
|
.btn-ghost:hover { background:var(--surface-2); }
|
|
.meta-strip { background:var(--surface); border:1px solid var(--border); border-radius:var(--radius); padding:.9rem 1.25rem; margin-bottom:1rem; display:flex; gap:2rem; flex-wrap:wrap; box-shadow:var(--shadow-sm); }
|
|
.meta-item { display:flex; flex-direction:column; gap:.15rem; }
|
|
.meta-label { font-size:.7rem; font-weight:700; color:var(--text-muted); text-transform:uppercase; letter-spacing:.06em; }
|
|
.meta-value { font-size:.875rem; color:var(--text); font-weight:500; }
|
|
.tab-bar { display:flex; gap:0; border-bottom:2px solid var(--border); margin-bottom:1rem; overflow-x:auto; }
|
|
.tab { padding:.65rem 1rem; font-size:.875rem; font-weight:500; color:var(--text-muted); border:none; background:none; cursor:pointer; border-bottom:2px solid transparent; margin-bottom:-2px; white-space:nowrap; transition:color .15s,border-color .15s; }
|
|
.tab.active { color:var(--teal); border-bottom-color:var(--teal); font-weight:600; }
|
|
.tab:hover:not(.active) { color:var(--text); }
|
|
.tab-content { background:var(--surface); border:1px solid var(--border); border-radius:var(--radius); padding:1.25rem; box-shadow:var(--shadow-sm); }
|
|
.detail-grid { display:grid; grid-template-columns:1fr 1fr; gap:1.25rem; }
|
|
.detail-field { display:flex; flex-direction:column; gap:.25rem; }
|
|
.field-label { font-size:.7rem; font-weight:700; color:var(--text-muted); text-transform:uppercase; letter-spacing:.06em; }
|
|
.field-value { font-size:.875rem; color:var(--text); line-height:1.5; }
|
|
.field-value.empty { color:var(--text-muted); font-style:italic; }
|
|
.text-block { background:var(--surface-2); border-radius:var(--radius-sm); padding:.75rem 1rem; font-size:.875rem; line-height:1.6; color:var(--text); white-space:pre-wrap; }
|
|
.status-menu { position:relative; }
|
|
.status-dropdown { position:absolute; top:calc(100% + .25rem); right:0; background:var(--surface); border:1px solid var(--border); border-radius:var(--radius); box-shadow:var(--shadow-md); z-index:50; min-width:160px; overflow:hidden; }
|
|
.status-opt { display:flex; align-items:center; gap:.5rem; padding:.6rem 1rem; font-size:.875rem; cursor:pointer; border:none; background:none; width:100%; text-align:left; transition:background .15s; color:var(--text); }
|
|
.status-opt:hover { background:var(--surface-2); }
|
|
.status-dot { width:8px; height:8px; border-radius:50%; flex-shrink:0; }
|
|
@media (max-width:768px) { .detail-grid { grid-template-columns:1fr; } .meta-strip { gap:1rem; } }
|
|
.ap-dialog { border:1px solid var(--border); border-radius:var(--radius-lg); padding:0; box-shadow:var(--shadow-lg); width:460px; max-width:96vw; background:var(--surface); color:var(--text); }
|
|
.ap-dialog::backdrop { background:rgba(0,0,0,.45); }
|
|
.ap-header { display:flex; align-items:center; justify-content:space-between; padding:1rem 1.25rem; border-bottom:1px solid var(--border); }
|
|
.ap-title { font-size:.938rem; font-weight:700; }
|
|
.ap-close { background:none; border:none; cursor:pointer; color:var(--text-muted); padding:.25rem; display:flex; }
|
|
.ap-body { padding:1.1rem 1.25rem; display:flex; flex-direction:column; gap:.75rem; }
|
|
.ap-search { width:100%; border:1px solid var(--border); border-radius:var(--radius-sm); padding:.5rem .75rem; font-size:.875rem; background:var(--surface); color:var(--text); box-sizing:border-box; }
|
|
.ap-search:focus { outline:none; border-color:var(--teal); box-shadow:0 0 0 3px rgba(10,126,164,.12); }
|
|
.ap-list { max-height:240px; overflow-y:auto; display:flex; flex-direction:column; gap:.3rem; border:1px solid var(--border); border-radius:var(--radius-sm); padding:.35rem; background:var(--surface); }
|
|
.ap-item { padding:.55rem .75rem; border-radius:var(--radius-sm); cursor:pointer; display:flex; align-items:center; gap:.6rem; transition:background .12s; border:2px solid transparent; }
|
|
.ap-item:hover { background:var(--surface-2); }
|
|
.ap-item.selected { border-color:var(--teal); background:rgba(10,126,164,.06); }
|
|
.ap-item-name { font-size:.875rem; font-weight:500; color:var(--text); }
|
|
.ap-item-meta { font-size:.75rem; color:var(--text-muted); }
|
|
.ap-mode { display:flex; flex-direction:column; gap:.4rem; }
|
|
.ap-mode-label { font-size:.813rem; font-weight:600; color:var(--text); }
|
|
.ap-mode-opts { display:flex; gap:.5rem; }
|
|
.ap-mode-opt { flex:1; border:2px solid var(--border); border-radius:var(--radius-sm); padding:.6rem .75rem; cursor:pointer; background:var(--surface); transition:border-color .12s,background .12s; }
|
|
.ap-mode-opt.selected { border-color:var(--teal); background:rgba(10,126,164,.06); }
|
|
.ap-mode-opt-title { font-size:.813rem; font-weight:600; color:var(--text); }
|
|
.ap-mode-opt-desc { font-size:.75rem; color:var(--text-muted); margin-top:.15rem; }
|
|
.ap-footer { padding:.875rem 1.25rem; border-top:1px solid var(--border); display:flex; justify-content:flex-end; gap:.5rem; align-items:center; }
|
|
.ap-err { font-size:.813rem; color:var(--danger); flex:1; }
|
|
.ap-empty { text-align:center; padding:1.5rem; color:var(--text-muted); font-size:.875rem; }
|
|
`; }
|
|
|
|
#html() {
|
|
if (this.#loading) return `
|
|
<div style="display:flex;align-items:center;justify-content:center;padding:4rem">
|
|
<ui-spinner size="lg"></ui-spinner>
|
|
</div>`;
|
|
|
|
if (!this.#wo) return `
|
|
<ui-empty icon="alert-circle" heading="Work order not found" body="This work order may have been deleted.">
|
|
<button class="btn btn-ghost" id="back" style="margin-top:.5rem">
|
|
<i data-lucide="arrow-left" style="width:14px;height:14px"></i> Back to list
|
|
</button>
|
|
</ui-empty>`;
|
|
|
|
const wo = this.#wo;
|
|
const transitions = STATUS_TRANSITIONS[wo.status] || [];
|
|
const STATUS_COLORS = { draft:'var(--status-draft)', assigned:'var(--status-assigned)', scheduled:'var(--status-scheduled)', in_progress:'var(--status-in_progress)', pending_review:'var(--status-pending_review)', closed:'var(--status-closed)' };
|
|
|
|
return `
|
|
<div class="page-header">
|
|
<button class="back-btn" id="back"><i data-lucide="chevron-left" style="width:15px;height:15px"></i> Work Orders</button>
|
|
<div class="header-info">
|
|
<div class="wo-number">${wo.wo_number}</div>
|
|
<h1>${this.#esc(wo.title)}</h1>
|
|
<div class="header-badges">
|
|
<ui-badge type="status" value="${wo.status}"></ui-badge>
|
|
<ui-badge type="priority" value="${wo.priority}"></ui-badge>
|
|
</div>
|
|
</div>
|
|
<div class="header-actions">
|
|
<button class="btn btn-ghost" id="edit-btn">
|
|
<i data-lucide="pencil" style="width:14px;height:14px"></i> Edit
|
|
</button>
|
|
<button class="btn btn-ghost" id="apply-profile-btn">
|
|
<i data-lucide="layout-template" style="width:14px;height:14px"></i> Apply Profile
|
|
</button>
|
|
${transitions.length ? `
|
|
<div class="status-menu" id="status-menu">
|
|
<button class="btn btn-primary" id="status-btn">
|
|
<i data-lucide="refresh-cw" style="width:14px;height:14px"></i> Change Status
|
|
</button>
|
|
<div class="status-dropdown" id="status-dropdown" style="display:none">
|
|
${transitions.map(s => `
|
|
<button class="status-opt" data-status="${s}">
|
|
<span class="status-dot" style="background:${STATUS_COLORS[s]}"></span>
|
|
${s.replace('_',' ')}
|
|
</button>`).join('')}
|
|
</div>
|
|
</div>` : ''}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="meta-strip">
|
|
<div class="meta-item">
|
|
<span class="meta-label">Site</span>
|
|
<span class="meta-value">${this.#esc(wo.site_name) || '—'}</span>
|
|
</div>
|
|
<div class="meta-item">
|
|
<span class="meta-label">Scheduled Start</span>
|
|
<span class="meta-value">${formatDateTime(wo.scheduled_start)}</span>
|
|
</div>
|
|
<div class="meta-item">
|
|
<span class="meta-label">Scheduled End</span>
|
|
<span class="meta-value">${formatDateTime(wo.scheduled_end)}</span>
|
|
</div>
|
|
<div class="meta-item">
|
|
<span class="meta-label">Created By</span>
|
|
<span class="meta-value">${this.#esc(wo.created_by) || '—'}</span>
|
|
</div>
|
|
<div class="meta-item">
|
|
<span class="meta-label">Created</span>
|
|
<span class="meta-value">${formatDate(wo.created_at)}</span>
|
|
</div>
|
|
${wo.parent_type ? `<div class="meta-item">
|
|
<span class="meta-label">Parent</span>
|
|
<span class="meta-value">${wo.parent_type} #${wo.parent_id}</span>
|
|
</div>` : ''}
|
|
</div>
|
|
|
|
<div class="tab-bar">
|
|
${TABS.map(t => `<button class="tab${this.#tab===t?' active':''}" data-tab="${t}">${t}</button>`).join('')}
|
|
</div>
|
|
|
|
<div class="tab-content" id="tab-content">
|
|
${this.#tabContent(wo)}
|
|
</div>
|
|
|
|
<dialog class="ap-dialog" id="ap-dialog">
|
|
<div class="ap-header">
|
|
<span class="ap-title">Apply Work Order Profile</span>
|
|
<button class="ap-close" id="ap-close"><i data-lucide="x" style="width:16px;height:16px"></i></button>
|
|
</div>
|
|
<div class="ap-body" id="ap-body">
|
|
<input class="ap-search" id="ap-search" placeholder="Search profiles…" autocomplete="off">
|
|
<div class="ap-list" id="ap-profile-list"><div class="ap-empty">Loading profiles…</div></div>
|
|
<div class="ap-mode" id="ap-mode" style="display:none">
|
|
<div class="ap-mode-label">Existing steps detected — how should we proceed?</div>
|
|
<div class="ap-mode-opts">
|
|
<div class="ap-mode-opt selected" data-mode="append">
|
|
<div class="ap-mode-opt-title">Append</div>
|
|
<div class="ap-mode-opt-desc">Add profile steps after existing steps</div>
|
|
</div>
|
|
<div class="ap-mode-opt" data-mode="replace">
|
|
<div class="ap-mode-opt-title">Replace</div>
|
|
<div class="ap-mode-opt-desc">Delete existing steps, load from profile</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="ap-footer">
|
|
<span class="ap-err" id="ap-err"></span>
|
|
<button class="btn btn-ghost" id="ap-cancel">Cancel</button>
|
|
<button class="btn btn-primary" id="ap-confirm" disabled>Apply Profile</button>
|
|
</div>
|
|
</dialog>`;
|
|
}
|
|
|
|
#tabContent(wo) {
|
|
switch (this.#tab) {
|
|
case 'Overview': return `
|
|
<div class="detail-grid">
|
|
<div class="detail-field" style="grid-column:1/-1">
|
|
<div class="field-label">Description</div>
|
|
${wo.description
|
|
? `<div class="text-block">${this.#esc(wo.description)}</div>`
|
|
: '<div class="field-value empty">No description provided</div>'}
|
|
</div>
|
|
<div class="detail-field" style="grid-column:1/-1">
|
|
<div class="field-label">Instructions</div>
|
|
${wo.instructions
|
|
? `<div class="text-block">${this.#esc(wo.instructions)}</div>`
|
|
: '<div class="field-value empty">No instructions provided</div>'}
|
|
</div>
|
|
<div class="detail-field">
|
|
<div class="field-label">Address</div>
|
|
<div class="field-value${wo.address?'':' empty'}">${this.#esc(wo.address) || 'Not set'}</div>
|
|
</div>
|
|
<div class="detail-field">
|
|
<div class="field-label">Access Notes</div>
|
|
${wo.access_notes
|
|
? `<div class="text-block" style="font-size:.813rem">${this.#esc(wo.access_notes)}</div>`
|
|
: '<div class="field-value empty">No access notes</div>'}
|
|
</div>
|
|
${wo.lat && wo.lng ? `
|
|
<div class="detail-field" style="grid-column:1/-1">
|
|
<div class="field-label">Location</div>
|
|
<wo-map
|
|
lat="${wo.lat}"
|
|
lng="${wo.lng}"
|
|
site-name="${this.#esc(wo.site_name)}"
|
|
access-notes="${this.#esc(wo.access_notes)}"
|
|
wo-number="${this.#esc(wo.wo_number)}"
|
|
wo-id="${wo.id}"
|
|
></wo-map>
|
|
</div>` : ''}
|
|
</div>`;
|
|
|
|
case 'Checklist':
|
|
return `<wo-checklist wo-id="${wo.id}"></wo-checklist>`;
|
|
|
|
case 'Resources':
|
|
return `<wo-resource-panel wo-id="${wo.id}"></wo-resource-panel>`;
|
|
|
|
case 'Photos':
|
|
return `<wo-photo-panel wo-id="${wo.id}"></wo-photo-panel>`;
|
|
|
|
case 'Accounting':
|
|
return `<wo-accounting wo-id="${wo.id}"></wo-accounting>`;
|
|
|
|
case 'Activity':
|
|
return `<wo-timeline wo-id="${wo.id}"></wo-timeline>`;
|
|
|
|
default: return '';
|
|
}
|
|
}
|
|
|
|
#bind() {
|
|
const s = this.shadowRoot;
|
|
s.querySelector('#back')?.addEventListener('click', () =>
|
|
window.dispatchEvent(new CustomEvent('wo:navigate', { detail: { path: '/work-orders' } })));
|
|
|
|
s.querySelector('#edit-btn')?.addEventListener('click', () =>
|
|
window.dispatchEvent(new CustomEvent('wo:navigate', { detail: { path: `/work-orders/${this.#woId}/edit` } })));
|
|
|
|
// Tab switching
|
|
s.querySelectorAll('.tab').forEach(tab =>
|
|
tab.addEventListener('click', () => {
|
|
this.#tab = tab.dataset.tab;
|
|
s.querySelectorAll('.tab').forEach(t => t.classList.toggle('active', t.dataset.tab === this.#tab));
|
|
s.querySelector('#tab-content').innerHTML = this.#tabContent(this.#wo);
|
|
if (window.lucide) lucide.createIcons({ nodes: [s.querySelector('#tab-content')] });
|
|
}));
|
|
|
|
// Status dropdown
|
|
const statusBtn = s.querySelector('#status-btn');
|
|
const dropdown = s.querySelector('#status-dropdown');
|
|
statusBtn?.addEventListener('click', e => {
|
|
e.stopPropagation();
|
|
dropdown.style.display = dropdown.style.display === 'none' ? 'block' : 'none';
|
|
});
|
|
document.addEventListener('click', () => { if (dropdown) dropdown.style.display = 'none'; }, { once: false });
|
|
|
|
s.querySelectorAll('.status-opt').forEach(opt =>
|
|
opt.addEventListener('click', async () => {
|
|
dropdown.style.display = 'none';
|
|
try {
|
|
await api.put(`/work-orders/${this.#woId}/status`, { status: opt.dataset.status });
|
|
window.dispatchEvent(new CustomEvent('wo:toast', { detail: { message: `Status changed to ${opt.dataset.status.replace('_',' ')}`, type: 'success' } }));
|
|
this.#load();
|
|
} catch (err) {
|
|
window.dispatchEvent(new CustomEvent('wo:toast', { detail: { message: err.message, type: 'error' } }));
|
|
}
|
|
}));
|
|
|
|
// Apply Profile dialog
|
|
this.#bindApplyProfile(s);
|
|
}
|
|
|
|
#bindApplyProfile(s) {
|
|
const apBtn = s.querySelector('#apply-profile-btn');
|
|
const dialog = s.querySelector('#ap-dialog');
|
|
const closeBtn = s.querySelector('#ap-close');
|
|
const cancelBtn = s.querySelector('#ap-cancel');
|
|
const confirmBtn = s.querySelector('#ap-confirm');
|
|
const searchEl = s.querySelector('#ap-search');
|
|
const listEl = s.querySelector('#ap-profile-list');
|
|
const modeEl = s.querySelector('#ap-mode');
|
|
const errEl = s.querySelector('#ap-err');
|
|
|
|
if (!apBtn) return;
|
|
|
|
let allProfiles = [];
|
|
let selectedId = null;
|
|
let selectedMode = 'append';
|
|
|
|
const renderList = (items) => {
|
|
if (items.length === 0) {
|
|
listEl.innerHTML = '<div class="ap-empty">No profiles found</div>';
|
|
return;
|
|
}
|
|
listEl.innerHTML = items.map(p => `
|
|
<div class="ap-item${selectedId === p.id ? ' selected' : ''}" data-id="${p.id}">
|
|
<div style="flex:1;min-width:0">
|
|
<div class="ap-item-name">${this.#esc(p.name)}</div>
|
|
<div class="ap-item-meta">${p.category ? `${p.category} · ` : ''}${p.step_count} step${p.step_count !== 1 ? 's' : ''} · ${p.default_priority} priority</div>
|
|
</div>
|
|
</div>`).join('');
|
|
|
|
listEl.querySelectorAll('.ap-item').forEach(item => {
|
|
item.addEventListener('click', () => {
|
|
selectedId = +item.dataset.id;
|
|
listEl.querySelectorAll('.ap-item').forEach(i => i.classList.toggle('selected', +i.dataset.id === selectedId));
|
|
confirmBtn.disabled = false;
|
|
});
|
|
});
|
|
};
|
|
|
|
apBtn.addEventListener('click', async () => {
|
|
selectedId = null;
|
|
selectedMode = 'append';
|
|
confirmBtn.disabled = true;
|
|
errEl.textContent = '';
|
|
searchEl.value = '';
|
|
|
|
// Show/hide mode selector based on whether WO already has steps
|
|
try {
|
|
const steps = await api.get(`/work-orders/${this.#woId}/steps`) || [];
|
|
modeEl.style.display = steps.length > 0 ? '' : 'none';
|
|
} catch { modeEl.style.display = 'none'; }
|
|
|
|
// Select append by default
|
|
modeEl.querySelectorAll('.ap-mode-opt').forEach(o => o.classList.toggle('selected', o.dataset.mode === 'append'));
|
|
|
|
listEl.innerHTML = '<div class="ap-empty">Loading…</div>';
|
|
dialog.showModal();
|
|
|
|
try {
|
|
allProfiles = await api.get('/profiles') || [];
|
|
renderList(allProfiles);
|
|
} catch (err) {
|
|
listEl.innerHTML = `<div class="ap-empty">${err.message}</div>`;
|
|
}
|
|
});
|
|
|
|
searchEl.addEventListener('input', () => {
|
|
const q = searchEl.value.toLowerCase();
|
|
renderList(q ? allProfiles.filter(p => p.name.toLowerCase().includes(q) || (p.category || '').toLowerCase().includes(q)) : allProfiles);
|
|
});
|
|
|
|
modeEl.querySelectorAll('.ap-mode-opt').forEach(opt => {
|
|
opt.addEventListener('click', () => {
|
|
selectedMode = opt.dataset.mode;
|
|
modeEl.querySelectorAll('.ap-mode-opt').forEach(o => o.classList.toggle('selected', o.dataset.mode === selectedMode));
|
|
});
|
|
});
|
|
|
|
closeBtn.addEventListener('click', () => dialog.close());
|
|
cancelBtn.addEventListener('click', () => dialog.close());
|
|
|
|
confirmBtn.addEventListener('click', async () => {
|
|
if (!selectedId) return;
|
|
confirmBtn.disabled = true;
|
|
confirmBtn.textContent = 'Applying…';
|
|
errEl.textContent = '';
|
|
try {
|
|
const result = await api.post(`/work-orders/${this.#woId}/apply-profile/${selectedId}`, { mode: selectedMode });
|
|
dialog.close();
|
|
window.dispatchEvent(new CustomEvent('wo:toast', { detail: {
|
|
message: `Profile applied — ${result.steps_added} step${result.steps_added !== 1 ? 's' : ''} ${selectedMode === 'replace' ? 'loaded' : 'added'}`,
|
|
type: 'success'
|
|
}}));
|
|
// Reload WO and switch to Checklist tab
|
|
this.#tab = 'Checklist';
|
|
this.#load();
|
|
} catch (err) {
|
|
errEl.textContent = err.message;
|
|
confirmBtn.disabled = false;
|
|
confirmBtn.textContent = 'Apply Profile';
|
|
}
|
|
});
|
|
}
|
|
|
|
#esc(s) { return (s || '').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); }
|
|
}
|
|
customElements.define('wo-detail', WoDetail);
|