Files
workorders/web/components/work-orders/wo-accounting.mjs
T

126 lines
5.3 KiB
JavaScript

import { api } from '../../lib/api.mjs';
const CODE_TYPES = [
{ key: 'gl_account', label: 'GL Account Code', placeholder: 'e.g. 6100-Operations' },
{ key: 'cost_center', label: 'Cost Center / Department', placeholder: 'e.g. CC-4200' },
{ key: 'wbs', label: 'Project WBS / Phase', placeholder: 'e.g. P-1234-A' },
{ key: 'billing_ref', label: 'Billing Reference', placeholder: 'e.g. INV-2024-001' },
];
class WoAccounting extends HTMLElement {
#woId = null;
#codes = {};
#saving = false;
#loading = true;
static get observedAttributes() { return ['wo-id']; }
connectedCallback() {
if (!this.shadowRoot) this.attachShadow({ mode: 'open' });
if (this.#woId) this.#load();
}
attributeChangedCallback(_, __, val) {
this.#woId = val ? +val : null;
if (this.shadowRoot && this.#woId) this.#load();
}
async #load() {
this.#loading = true;
this.#render();
try {
const rows = await api.get(`/work-orders/${this.#woId}/accounting`) || [];
this.#codes = {};
rows.forEach(r => { this.#codes[r.code_type] = { value: r.code_value, description: r.description }; });
} catch { this.#codes = {}; }
this.#loading = false;
this.#render();
}
async #save() {
if (this.#saving) return;
this.#saving = true;
this.#updateSaveIndicator('saving');
try {
const s = this.shadowRoot;
const payload = CODE_TYPES
.filter(ct => {
const v = s.querySelector(`[data-key="${ct.key}"] .code-value`)?.value?.trim();
return v;
})
.map(ct => ({
code_type: ct.key,
code_value: s.querySelector(`[data-key="${ct.key}"] .code-value`).value.trim(),
description: s.querySelector(`[data-key="${ct.key}"] .code-desc`).value.trim(),
}));
await api.put(`/work-orders/${this.#woId}/accounting`, payload);
this.#updateSaveIndicator('saved');
} catch (err) {
this.#updateSaveIndicator('error');
window.dispatchEvent(new CustomEvent('wo:toast', { detail: { message: err.message, type: 'error' } }));
} finally {
this.#saving = false;
}
}
#updateSaveIndicator(state) {
const el = this.shadowRoot?.querySelector('#save-indicator');
if (!el) return;
const msgs = { saving: '⋯ Saving…', saved: '✓ Saved', error: '✗ Error' };
const colors = { saving: 'var(--text-muted)', saved: 'var(--success)', error: 'var(--danger)' };
el.textContent = msgs[state];
el.style.color = colors[state];
if (state !== 'saving') setTimeout(() => { if (el) el.textContent = ''; }, 2500);
}
#render() {
const s = this.shadowRoot;
if (this.#loading) {
s.innerHTML = `<style>:host{display:block;padding:2rem;text-align:center}</style><ui-spinner></ui-spinner>`;
return;
}
s.innerHTML = `
<style>
:host { display: block; }
.header-row { display: flex; align-items: center; justify-content: space-between; margin-bottom: 1.25rem; }
.section-title { font-size: .813rem; font-weight: 700; color: var(--text-muted); text-transform: uppercase; letter-spacing: .06em; }
#save-indicator { font-size: .813rem; font-weight: 500; transition: color .2s; }
.code-grid { display: flex; flex-direction: column; gap: 1rem; }
.code-row { display: grid; grid-template-columns: 1fr 1.5fr; gap: .75rem; align-items: start; }
.code-label { font-size: .813rem; font-weight: 600; color: var(--text); padding-top: .55rem; }
.code-inputs { display: flex; flex-direction: column; gap: .4rem; }
.code-value, .code-desc { width: 100%; border: 1px solid var(--border); border-radius: var(--radius-sm); padding: .5rem .75rem; font-size: .875rem; background: var(--surface); color: var(--text); transition: border-color .15s; box-sizing: border-box; }
.code-value:focus, .code-desc:focus { outline: none; border-color: var(--teal); box-shadow: 0 0 0 3px rgba(10,126,164,.12); }
.code-desc { font-size: .813rem; color: var(--text-muted); }
.code-desc::placeholder { font-style: italic; }
.divider { height: 1px; background: var(--border-lt); }
@media (max-width: 768px) { .code-row { grid-template-columns: 1fr; } }
</style>
<div class="header-row">
<span class="section-title">Accounting Codes</span>
<span id="save-indicator"></span>
</div>
<div class="code-grid">
${CODE_TYPES.map((ct, i) => `
${i > 0 ? '<div class="divider"></div>' : ''}
<div class="code-row" data-key="${ct.key}">
<div class="code-label">${ct.label}</div>
<div class="code-inputs">
<input class="code-value" placeholder="${ct.placeholder}" value="${this.#esc(this.#codes[ct.key]?.value || '')}">
<input class="code-desc" placeholder="Description…" value="${this.#esc(this.#codes[ct.key]?.description || '')}">
</div>
</div>`).join('')}
</div>`;
s.querySelectorAll('.code-value, .code-desc').forEach(input => {
input.addEventListener('blur', () => this.#save());
input.addEventListener('keydown', e => { if (e.key === 'Enter') input.blur(); });
});
}
#esc(s) { return (s || '').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }
}
customElements.define('wo-accounting', WoAccounting);