Files
workorders/web/app.mjs
T

149 lines
7.4 KiB
JavaScript

// ── Register all custom elements ──────────────────────────────────────────────
import './components/shared/ui-badge.mjs';
import './components/shared/ui-button.mjs';
import './components/shared/ui-spinner.mjs';
import './components/shared/ui-toast.mjs';
import './components/shared/ui-empty.mjs';
import './components/shared/ui-dialog.mjs';
import './components/layout/app-root.mjs';
import './components/layout/app-sidebar.mjs';
import './components/layout/app-topbar.mjs';
import './components/layout/app-mobile-nav.mjs';
import './components/work-orders/wo-list.mjs';
import './components/work-orders/wo-kanban.mjs';
import './components/work-orders/wo-form.mjs';
import './components/work-orders/wo-detail.mjs';
import './components/work-orders/wo-checklist.mjs';
import './components/work-orders/wo-resource-panel.mjs';
import './components/work-orders/wo-photo-panel.mjs';
import './components/work-orders/wo-map.mjs';
import './components/work-orders/wo-accounting.mjs';
import './components/work-orders/wo-timeline.mjs';
import './components/registry/people-list.mjs';
import './components/registry/vehicle-list.mjs';
import './components/registry/equipment-list.mjs';
import './components/registry/material-list.mjs';
import './components/registry/profile-list.mjs';
import { getUser, setToken, clearToken } from './lib/auth.mjs';
import { api } from './lib/api.mjs';
import { router } from './lib/router.mjs';
import { showToast } from './components/shared/ui-toast.mjs';
const root = document.getElementById('root');
window.addEventListener('auth:expired', () => { clearToken(); showLoginPage(); });
window.addEventListener('auth:logout', () => { clearToken(); showLoginPage(); });
const user = getUser();
if (user) {
startApp();
} else {
showLoginPage();
}
// ── Login page ────────────────────────────────────────────────────────────────
function showLoginPage() {
root.innerHTML = `
<style>
.login-wrap {
min-height: 100vh; display: flex; align-items: center; justify-content: center;
background: var(--bg); padding: 1rem;
}
.login-card {
background: var(--surface); border: 1px solid var(--border);
border-radius: var(--radius-lg); padding: 2.5rem 2rem;
width: 100%; max-width: 380px; box-shadow: var(--shadow-md);
}
.login-brand {
display: flex; align-items: center; justify-content: center; gap: .75rem;
margin-bottom: 1.75rem;
}
.login-icon {
width: 40px; height: 40px; background: var(--teal); border-radius: var(--radius);
display: flex; align-items: center; justify-content: center; color: #fff;
}
.login-title { font-size: 1.25rem; font-weight: 700; color: var(--text); }
.login-card form { display: flex; flex-direction: column; gap: 1rem; }
.login-card label { display: flex; flex-direction: column; gap: .3rem; font-size: .875rem; font-weight: 600; color: var(--text); }
.login-card input { border: 1px solid var(--border); border-radius: var(--radius-sm); padding: .6rem .75rem; background: var(--surface); color: var(--text); font-size: .938rem; width: 100%; transition: border-color .15s; }
.login-card input:focus { outline: none; border-color: var(--teal); box-shadow: 0 0 0 3px rgba(10,126,164,.15); }
.login-btn {
background: var(--teal); color: #fff; border: none; border-radius: var(--radius);
padding: .65rem; font-size: .938rem; font-weight: 600; cursor: pointer;
transition: opacity .15s; display: flex; align-items: center; justify-content: center; gap: .4rem;
}
.login-btn:hover { opacity: .88; }
.login-btn:disabled { opacity: .5; cursor: not-allowed; }
.login-error { color: var(--danger); font-size: .813rem; text-align: center; min-height: 1.25rem; }
</style>
<div class="login-wrap">
<div class="login-card">
<div class="login-brand">
<div class="login-icon"><i data-lucide="clipboard-list" style="width:20px;height:20px"></i></div>
<span class="login-title">Work Orders</span>
</div>
<form id="login-form">
<label>Username or Email<input id="login-user" type="text" autocomplete="username" required placeholder="admin"></label>
<label>Password<input id="login-pass" type="password" autocomplete="current-password" required placeholder="••••••••"></label>
<div class="login-error" id="login-error"></div>
<button type="submit" class="login-btn" id="login-btn">Sign In</button>
</form>
</div>
</div>`;
if (window.lucide) lucide.createIcons({ nodes: [root] });
root.querySelector('#login-form').addEventListener('submit', async e => {
e.preventDefault();
const btn = root.querySelector('#login-btn');
const err = root.querySelector('#login-error');
const username = root.querySelector('#login-user').value.trim();
const password = root.querySelector('#login-pass').value;
btn.disabled = true;
btn.textContent = 'Signing in…';
err.textContent = '';
try {
const res = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password }),
});
const json = await res.json();
if (!res.ok) throw new Error(json.error || 'Login failed');
setToken(json.data.token);
startApp();
} catch (ex) {
err.textContent = ex.message;
btn.disabled = false;
btn.textContent = 'Sign In';
}
});
}
// ── Main app ──────────────────────────────────────────────────────────────────
function startApp() {
root.innerHTML = '<app-root></app-root>';
const appRoot = root.querySelector('app-root');
router
.on('/', () => appRoot.setPage('<wo-list></wo-list>'))
.on('/work-orders', () => appRoot.setPage('<wo-list></wo-list>'))
.on('/work-orders/new', () => appRoot.setPage('<wo-form></wo-form>'))
.on('/work-orders/:id/edit', ({ id }) => appRoot.setPage(`<wo-form wo-id="${id}"></wo-form>`))
.on('/work-orders/:id', ({ id }) => appRoot.setPage(`<wo-detail wo-id="${id}"></wo-detail>`))
.on('/registry/people', () => appRoot.setPage('<people-list></people-list>'))
.on('/registry/vehicles', () => appRoot.setPage('<vehicle-list></vehicle-list>'))
.on('/registry/equipment', () => appRoot.setPage('<equipment-list></equipment-list>'))
.on('/registry/materials', () => appRoot.setPage('<material-list></material-list>'))
.on('/registry/profiles', () => appRoot.setPage('<profile-list></profile-list>'))
.on('/reports', () => appRoot.setPage('<p style="padding:2rem;color:var(--text-muted)">Reports — Phase 3</p>'))
.on('/users', () => appRoot.setPage('<p style="padding:2rem;color:var(--text-muted)">User management — Phase 3</p>'))
.on('/settings', () => appRoot.setPage('<p style="padding:2rem;color:var(--text-muted)">Settings — Phase 4</p>'))
.start();
// Global navigation events from WO components
window.addEventListener('wo:navigate', e => router.navigate(e.detail.path));
window.addEventListener('wo:toast', e => showToast(e.detail.message, e.detail.type));
}