126 lines
5.3 KiB
JavaScript
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,'&').replace(/</g,'<').replace(/>/g,'>'); }
|
|
}
|
|
|
|
customElements.define('wo-accounting', WoAccounting);
|