web: start: add dynamique menu
Signed-off-by: GnomeZworc <nicolas.boufidjeline@g3e.fr>
This commit is contained in:
parent
50a290d808
commit
b67e79973c
9 changed files with 343 additions and 8 deletions
|
|
@ -39,6 +39,7 @@ local_pages:
|
||||||
- vpcs
|
- vpcs
|
||||||
- subnets
|
- subnets
|
||||||
- vms
|
- vms
|
||||||
|
- account
|
||||||
|
|
||||||
# Remote pages fetched from git — same mechanism as components.
|
# Remote pages fetched from git — same mechanism as components.
|
||||||
#
|
#
|
||||||
|
|
@ -68,6 +69,10 @@ menu:
|
||||||
icon: vm
|
icon: vm
|
||||||
children:
|
children:
|
||||||
- vms
|
- vms
|
||||||
|
- type: group
|
||||||
|
label: Comptes
|
||||||
|
icon: account
|
||||||
|
children: []
|
||||||
- type: separator
|
- type: separator
|
||||||
- dashboard
|
- dashboard
|
||||||
- type: section
|
- type: section
|
||||||
|
|
|
||||||
|
|
@ -33,12 +33,17 @@ function resolveIcon(name) {
|
||||||
|
|
||||||
class SideNav extends HTMLElement {
|
class SideNav extends HTMLElement {
|
||||||
#items = []
|
#items = []
|
||||||
#expanded = new Set() // labels of open groups
|
#expanded = new Set()
|
||||||
#hamburger = null
|
#hamburger = null
|
||||||
#backdrop = null
|
#backdrop = null
|
||||||
#mqHandler = null
|
#mqHandler = null
|
||||||
#hashHandler = null
|
#hashHandler = null
|
||||||
#isMobile = false
|
#isMobile = false
|
||||||
|
#readyResolve = null
|
||||||
|
|
||||||
|
// Resolves once navigation.json is loaded and the nav is rendered.
|
||||||
|
// Await this before calling setGroupChildren().
|
||||||
|
ready = new Promise(r => { this.#readyResolve = r })
|
||||||
|
|
||||||
async connectedCallback() {
|
async connectedCallback() {
|
||||||
this.attachShadow({ mode: 'open' })
|
this.attachShadow({ mode: 'open' })
|
||||||
|
|
@ -57,6 +62,7 @@ class SideNav extends HTMLElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.#render()
|
this.#render()
|
||||||
|
this.#readyResolve()
|
||||||
|
|
||||||
this.#mqHandler = () => this.#applyMode()
|
this.#mqHandler = () => this.#applyMode()
|
||||||
this.#hashHandler = () => this.#updateActive()
|
this.#hashHandler = () => this.#updateActive()
|
||||||
|
|
@ -72,6 +78,17 @@ class SideNav extends HTMLElement {
|
||||||
this.#removeBackdrop()
|
this.#removeBackdrop()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Public API ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
// Inject dynamic children into an existing group (identified by label).
|
||||||
|
// Call after awaiting nav.ready.
|
||||||
|
setGroupChildren(label, children) {
|
||||||
|
const group = this.#items.find(i => i.type === 'group' && i.label === label)
|
||||||
|
if (!group) return
|
||||||
|
group.children = children
|
||||||
|
this.#render()
|
||||||
|
}
|
||||||
|
|
||||||
// ── Rendu des items ─────────────────────────────────────────────────────────
|
// ── Rendu des items ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
#currentHref() {
|
#currentHref() {
|
||||||
|
|
|
||||||
37
web/core/menu.js
Normal file
37
web/core/menu.js
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
// Dynamic menu loader.
|
||||||
|
// Fetches data for groups declared with empty children in navigation.json
|
||||||
|
// and injects them into side-nav after it is ready.
|
||||||
|
//
|
||||||
|
// Imported non-blocking from index.html — does not delay routing.
|
||||||
|
// Replace MOCK_* with real api-client calls when the backend is ready.
|
||||||
|
|
||||||
|
// ── Mock data ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const MOCK_ACCOUNTS = [
|
||||||
|
{ id: 'tresorerie', name: 'Trésorerie' },
|
||||||
|
{ id: 'charges', name: 'Charges' },
|
||||||
|
{ id: 'produits', name: 'Produits' },
|
||||||
|
{ id: 'fournisseurs', name: 'Fournisseurs' },
|
||||||
|
]
|
||||||
|
|
||||||
|
async function fetchAccounts() {
|
||||||
|
// TODO: replace with real call
|
||||||
|
// const api = document.querySelector('api-client')
|
||||||
|
// return await api.finance.list('/accounts')
|
||||||
|
await new Promise(r => setTimeout(r, 500)) // simulate network latency
|
||||||
|
return MOCK_ACCOUNTS
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Injection ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const nav = document.querySelector('side-nav')
|
||||||
|
if (!nav) throw new Error('menu.js: side-nav not found in DOM')
|
||||||
|
|
||||||
|
await nav.ready
|
||||||
|
|
||||||
|
const accounts = await fetchAccounts()
|
||||||
|
nav.setGroupChildren('Comptes', accounts.map(a => ({
|
||||||
|
label: a.name,
|
||||||
|
href: `#/account?id=${a.id}`,
|
||||||
|
icon: 'account',
|
||||||
|
})))
|
||||||
|
|
@ -17,7 +17,8 @@ const MODULE_CACHE = new Map()
|
||||||
let activeCSS = null
|
let activeCSS = null
|
||||||
|
|
||||||
function currentRoute() {
|
function currentRoute() {
|
||||||
return window.location.hash.replace(/^#\//, '') || PAGES[0]?.route || ''
|
const hash = window.location.hash.replace(/^#\//, '') || PAGES[0]?.route || ''
|
||||||
|
return hash.split('?')[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
async function render(routeName) {
|
async function render(routeName) {
|
||||||
|
|
|
||||||
|
|
@ -108,6 +108,9 @@
|
||||||
// then router renders the current route.
|
// then router renders the current route.
|
||||||
await import('./core/registry.js')
|
await import('./core/registry.js')
|
||||||
await import('./core/router.js')
|
await import('./core/router.js')
|
||||||
|
|
||||||
|
// Non-blocking: populates dynamic menu groups after routing.
|
||||||
|
import('./core/menu.js')
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
|
|
|
||||||
121
web/pages/account/account.css
Normal file
121
web/pages/account/account.css
Normal file
|
|
@ -0,0 +1,121 @@
|
||||||
|
#account-page {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Header ────────────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
#account-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16px;
|
||||||
|
background: #1e1e2e;
|
||||||
|
border: 1px solid #313244;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 20px 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#account-name {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #cdd6f4;
|
||||||
|
margin: 0 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#account-id {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #6c7086;
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
#account-balance-block {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-end;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.balance-label {
|
||||||
|
font-size: 11px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
color: #6c7086;
|
||||||
|
}
|
||||||
|
|
||||||
|
#account-balance {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #a6e3a1;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
|
||||||
|
#account-balance.negative { color: #f38ba8; }
|
||||||
|
|
||||||
|
/* ── Transactions ───────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
#account-transactions {
|
||||||
|
background: #1e1e2e;
|
||||||
|
border: 1px solid #313244;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 20px 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#account-transactions h3 {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #a6adc8;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
margin: 0 0 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#tx-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#tx-table th {
|
||||||
|
text-align: left;
|
||||||
|
color: #6c7086;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 0 12px 10px;
|
||||||
|
border-bottom: 1px solid #313244;
|
||||||
|
}
|
||||||
|
|
||||||
|
#tx-table td {
|
||||||
|
padding: 10px 12px;
|
||||||
|
color: #a6adc8;
|
||||||
|
border-bottom: 1px solid #1e1e2e;
|
||||||
|
}
|
||||||
|
|
||||||
|
#tx-table tbody tr:last-child td { border-bottom: none; }
|
||||||
|
#tx-table tbody tr:hover td { background: #313244; }
|
||||||
|
|
||||||
|
.amount-col { text-align: right; }
|
||||||
|
|
||||||
|
td.amount { text-align: right; font-variant-numeric: tabular-nums; font-weight: 500; }
|
||||||
|
td.amount.positive { color: #a6e3a1; }
|
||||||
|
td.amount.negative { color: #f38ba8; }
|
||||||
|
|
||||||
|
td.date { color: #6c7086; font-size: 13px; white-space: nowrap; }
|
||||||
|
|
||||||
|
/* ── States ─────────────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
#account-loading {
|
||||||
|
color: #6c7086;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#account-error {
|
||||||
|
background: #313244;
|
||||||
|
border: 1px solid #f38ba8;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
color: #f38ba8;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
117
web/pages/account/account.js
Normal file
117
web/pages/account/account.js
Normal file
|
|
@ -0,0 +1,117 @@
|
||||||
|
// Account page.
|
||||||
|
// Reads ?id=<account-id> from the URL hash and renders account details.
|
||||||
|
// Replace fetchAccount() with a real api-client call when backend is ready.
|
||||||
|
|
||||||
|
// ── Mock data ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const MOCK_DB = {
|
||||||
|
tresorerie: {
|
||||||
|
name: 'Trésorerie',
|
||||||
|
balance: 48_230.50,
|
||||||
|
currency: 'EUR',
|
||||||
|
transactions: [
|
||||||
|
{ date: '2026-06-03', label: 'Virement client Martin SA', amount: +12_000.00 },
|
||||||
|
{ date: '2026-06-01', label: 'Virement client Dupont SARL', amount: +5_000.00 },
|
||||||
|
{ date: '2026-05-30', label: 'Loyer bureau juin', amount: -2_500.00 },
|
||||||
|
{ date: '2026-05-28', label: 'Facture EDF', amount: -340.20 },
|
||||||
|
{ date: '2026-05-25', label: 'Abonnement SaaS infra', amount: -189.00 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
charges: {
|
||||||
|
name: 'Charges',
|
||||||
|
balance: 12_890.00,
|
||||||
|
currency: 'EUR',
|
||||||
|
transactions: [
|
||||||
|
{ date: '2026-06-01', label: 'Salaires mai', amount: -8_500.00 },
|
||||||
|
{ date: '2026-05-28', label: 'Assurance professionnelle', amount: -420.00 },
|
||||||
|
{ date: '2026-05-20', label: 'Formation équipe', amount: -1_200.00 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
produits: {
|
||||||
|
name: 'Produits',
|
||||||
|
balance: 67_450.00,
|
||||||
|
currency: 'EUR',
|
||||||
|
transactions: [
|
||||||
|
{ date: '2026-06-02', label: 'Facture client #2024-089', amount: +18_000.00 },
|
||||||
|
{ date: '2026-05-29', label: 'Facture client #2024-088', amount: +9_500.00 },
|
||||||
|
{ date: '2026-05-22', label: 'Avoir client Dupont', amount: -500.00 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
fournisseurs: {
|
||||||
|
name: 'Fournisseurs',
|
||||||
|
balance: -8_340.00,
|
||||||
|
currency: 'EUR',
|
||||||
|
transactions: [
|
||||||
|
{ date: '2026-06-01', label: 'Facture Tech Components SAS', amount: -3_200.00 },
|
||||||
|
{ date: '2026-05-27', label: 'Règlement fournisseur Leroy', amount: +2_000.00 },
|
||||||
|
{ date: '2026-05-20', label: 'Facture Bureau & Co', amount: -640.00 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchAccount(id) {
|
||||||
|
// TODO: replace with real call
|
||||||
|
// const api = document.querySelector('api-client')
|
||||||
|
// return await api.finance.get(`/accounts/${id}`)
|
||||||
|
await new Promise(r => setTimeout(r, 300))
|
||||||
|
const data = MOCK_DB[id]
|
||||||
|
if (!data) throw new Error(`Compte introuvable : ${id}`)
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Helpers ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function formatAmount(amount, currency) {
|
||||||
|
return new Intl.NumberFormat('fr-FR', { style: 'currency', currency }).format(amount)
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDate(iso) {
|
||||||
|
return new Intl.DateTimeFormat('fr-FR', { day: '2-digit', month: 'short', year: 'numeric' }).format(new Date(iso))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Init ─────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export function init(main) {
|
||||||
|
const params = new URLSearchParams(window.location.hash.split('?')[1])
|
||||||
|
const id = params.get('id')
|
||||||
|
|
||||||
|
const loading = main.querySelector('#account-loading')
|
||||||
|
const error = main.querySelector('#account-error')
|
||||||
|
|
||||||
|
if (!id) {
|
||||||
|
loading.style.display = 'none'
|
||||||
|
error.style.display = 'block'
|
||||||
|
error.textContent = 'Aucun compte sélectionné.'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchAccount(id)
|
||||||
|
.then(data => render(main, id, data))
|
||||||
|
.catch(err => {
|
||||||
|
loading.style.display = 'none'
|
||||||
|
error.style.display = 'block'
|
||||||
|
error.textContent = err.message
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function render(main, id, data) {
|
||||||
|
main.querySelector('#account-loading').style.display = 'none'
|
||||||
|
|
||||||
|
main.querySelector('#account-name').textContent = data.name
|
||||||
|
main.querySelector('#account-id').textContent = id
|
||||||
|
|
||||||
|
const balanceEl = main.querySelector('#account-balance')
|
||||||
|
balanceEl.textContent = formatAmount(data.balance, data.currency)
|
||||||
|
balanceEl.classList.toggle('negative', data.balance < 0)
|
||||||
|
|
||||||
|
const tbody = main.querySelector('#tx-body')
|
||||||
|
tbody.innerHTML = data.transactions.map(tx => `
|
||||||
|
<tr>
|
||||||
|
<td class="date">${formatDate(tx.date)}</td>
|
||||||
|
<td>${tx.label}</td>
|
||||||
|
<td class="amount ${tx.amount >= 0 ? 'positive' : 'negative'}">
|
||||||
|
${formatAmount(tx.amount, data.currency)}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
`).join('')
|
||||||
|
}
|
||||||
29
web/pages/account/index.html
Normal file
29
web/pages/account/index.html
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
<div id="account-page">
|
||||||
|
<div id="account-header">
|
||||||
|
<div id="account-title">
|
||||||
|
<h2 id="account-name"></h2>
|
||||||
|
<span id="account-id"></span>
|
||||||
|
</div>
|
||||||
|
<div id="account-balance-block">
|
||||||
|
<span class="balance-label">Solde</span>
|
||||||
|
<span id="account-balance"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section id="account-transactions">
|
||||||
|
<h3>Transactions récentes</h3>
|
||||||
|
<table id="tx-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Date</th>
|
||||||
|
<th>Libellé</th>
|
||||||
|
<th class="amount-col">Montant</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="tx-body"></tbody>
|
||||||
|
</table>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div id="account-error" style="display:none"></div>
|
||||||
|
<div id="account-loading">Chargement…</div>
|
||||||
|
</div>
|
||||||
5
web/pages/account/manifest.json
Normal file
5
web/pages/account/manifest.json
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"route": "account",
|
||||||
|
"label": "Compte",
|
||||||
|
"icon": "account"
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue