137 lines
7.0 KiB
JavaScript
137 lines
7.0 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 { 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('<p style="padding:2rem;color:var(--text-muted)">People registry — Phase 2</p>'))
|
|
.on('/registry/vehicles', () => appRoot.setPage('<p style="padding:2rem;color:var(--text-muted)">Vehicles registry — Phase 2</p>'))
|
|
.on('/registry/equipment', () => appRoot.setPage('<p style="padding:2rem;color:var(--text-muted)">Equipment registry — Phase 2</p>'))
|
|
.on('/registry/materials', () => appRoot.setPage('<p style="padding:2rem;color:var(--text-muted)">Materials registry — Phase 2</p>'))
|
|
.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));
|
|
}
|