@mostajs/aop-ui-html 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -0
- package/llms.txt +6 -0
- package/package.json +26 -0
- package/src/index.js +80 -0
package/README.md
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# @mostajs/aop-ui-html
|
|
2
|
+
|
|
3
|
+
**Auteur** : Dr Hamid MADANI <drmdh@msn.com> · AGPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
Vues **HTML server-rendered** (no-build) de veille d appels d offres : liste+facettes, fiche+analyse IA, dashboard, éditeur d alerte. Compose `@mostajs/aop-classify`. Stack `mosta-aop-stack`.
|
package/llms.txt
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# @mostajs/aop-ui-html — fiche LLM
|
|
2
|
+
> Vues HTML server-rendered de veille AO (no-build) : liste, fiche, dashboard, éditeur d alerte.
|
|
3
|
+
|
|
4
|
+
## EXPORTS
|
|
5
|
+
aopStyles() · tenderBadge(t) · tenderCard(t,{href}) · tenderList({tenders,facets,filters,baseHref,detailHref}) · tenderDetail({tender,analysis,documents}) · veilleDashboard({total,byTemperature,byStatus}) · alertForm({alert,action})
|
|
6
|
+
Compose @mostajs/aop-classify (couleurs/emoji température). Fragments échappés. Stack mosta-aop-stack.
|
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mostajs/aop-ui-html",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Vues HTML server-rendered de veille d'appels d'offres (liste, fiche, dashboard, alerte) — no-build, compose @mostajs/aop-classify.",
|
|
5
|
+
"author": "Dr Hamid MADANI <drmdh@msn.com>",
|
|
6
|
+
"license": "AGPL-3.0-or-later",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"main": "src/index.js",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": "./src/index.js"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"src",
|
|
14
|
+
"README.md",
|
|
15
|
+
"llms.txt"
|
|
16
|
+
],
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@mostajs/aop-classify": "^0.1.0"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@mostajs/mjs-unit": "^0.3.0"
|
|
22
|
+
},
|
|
23
|
+
"scripts": {
|
|
24
|
+
"test": "node test-scripts/unit/aop-ui-html.test.mjs && node examples/run.mjs"
|
|
25
|
+
}
|
|
26
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// @mostajs/aop-ui-html — vues HTML server-rendered de veille AO (no-build, fragments).
|
|
2
|
+
// COMPOSE @mostajs/aop-classify (couleurs/emoji). Pour apps no-build. @author Dr Hamid MADANI <drmdh@msn.com> · AGPL-3.0
|
|
3
|
+
import { temperatureColor, temperatureEmoji } from '@mostajs/aop-classify';
|
|
4
|
+
|
|
5
|
+
const esc = (s) => String(s ?? '').replace(/[&<>"]/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c]));
|
|
6
|
+
const money = (v, cur = 'EUR') => (v == null ? '—' : `${Number(v).toLocaleString('fr-FR')} ${cur}`);
|
|
7
|
+
const day = (d) => (d ? String(d).slice(0, 10) : '—');
|
|
8
|
+
|
|
9
|
+
export function aopStyles() {
|
|
10
|
+
return `<style>
|
|
11
|
+
.aop-badge{display:inline-block;padding:2px 8px;border-radius:10px;color:#fff;font-size:12px;font-weight:600}
|
|
12
|
+
.aop-card{border:1px solid #e5e7eb;border-radius:10px;padding:12px 14px;margin:8px 0}
|
|
13
|
+
.aop-card h3{margin:0 0 4px} .aop-meta{font-size:12px;color:#6b7280}
|
|
14
|
+
.aop-facets{display:flex;gap:8px;flex-wrap:wrap;margin:8px 0} .aop-facets a{font-size:12px;text-decoration:none;border:1px solid #ddd;border-radius:8px;padding:3px 8px;color:#374151}
|
|
15
|
+
.aop-kpi{display:inline-block;min-width:90px;text-align:center;border:1px solid #e5e7eb;border-radius:10px;padding:10px;margin:4px}
|
|
16
|
+
.aop-kpi b{display:block;font-size:22px}
|
|
17
|
+
</style>`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Badge de température (emoji + couleur). */
|
|
21
|
+
export function tenderBadge(tender = {}) {
|
|
22
|
+
const t = tender.classification?.temperature || 'cold';
|
|
23
|
+
return `<span class="aop-badge" style="background:${temperatureColor(t)}">${temperatureEmoji(t)} ${esc(t)}</span>`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Une carte AO dans la liste. */
|
|
27
|
+
export function tenderCard(tender = {}, { href = '#' } = {}) {
|
|
28
|
+
const c = tender.classification || {};
|
|
29
|
+
return `<div class="aop-card">
|
|
30
|
+
<h3><a href="${esc(href)}">${esc(tender.title)}</a> ${tenderBadge(tender)}</h3>
|
|
31
|
+
<p class="aop-meta">${esc(tender.organization?.name || '')} · échéance <strong>${day(tender.dates?.deadline)}</strong> · ${money(tender.financial?.estimatedValue, tender.financial?.currency)} · score ${c.score ?? '—'}</p>
|
|
32
|
+
</div>`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Liste filtrable + facettes (issues de aop-search). */
|
|
36
|
+
export function tenderList({ tenders = [], facets = null, filters = {}, baseHref = '?', detailHref = (t) => `?id=${t.id}` } = {}) {
|
|
37
|
+
const facet = (key, obj) => Object.entries(obj || {}).map(([k, n]) => `<a href="${esc(baseHref)}&${key}=${esc(k)}">${esc(k)} (${n})</a>`).join('');
|
|
38
|
+
const f = facets ? `<div class="aop-facets">${facet('temperature', facets.temperature)}${facet('status', facets.status)}${facet('type', facets.type)}</div>` : '';
|
|
39
|
+
return `${aopStyles()}<section><h1>📋 Appels d'offres (${tenders.length})</h1>
|
|
40
|
+
<form method="get" action="${esc(baseHref)}" class="aop-no-print"><input name="q" value="${esc(filters.q || '')}" placeholder="Rechercher…"><button>🔎</button></form>
|
|
41
|
+
${f}${tenders.map((t) => tenderCard(t, { href: detailHref(t) })).join('') || '<p class="aop-meta">Aucun résultat.</p>'}</section>`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Fiche détaillée + analyse IA (aop-scoring-ai) + documents. */
|
|
45
|
+
export function tenderDetail({ tender = {}, analysis = null, documents = [] } = {}) {
|
|
46
|
+
const ai = analysis ? `<section><h2>🤖 Analyse</h2>
|
|
47
|
+
<p><strong>${esc(analysis.goNoGo || '')}</strong> · score ${analysis.score ?? '—'}</p>
|
|
48
|
+
<p>${esc(analysis.summary || '')}</p>
|
|
49
|
+
${(analysis.recommendations || []).length ? `<ul>${analysis.recommendations.map((r) => `<li>${esc(r)}</li>`).join('')}</ul>` : ''}</section>` : '';
|
|
50
|
+
const docs = documents.length ? `<section><h2>📁 Documents</h2><ul>${documents.map((d) => `<li>${esc(d.title)} <span class="aop-meta">(${esc(d.type)})</span></li>`).join('')}</ul></section>` : '';
|
|
51
|
+
return `${aopStyles()}<section><h1>${esc(tender.title)} ${tenderBadge(tender)}</h1>
|
|
52
|
+
<p class="aop-meta">${esc(tender.organization?.name || '')} · ${esc(tender.procedure || '')} · échéance <strong>${day(tender.dates?.deadline)}</strong> · ${money(tender.financial?.estimatedValue, tender.financial?.currency)}</p>
|
|
53
|
+
<p>${esc(tender.description || '')}</p>
|
|
54
|
+
${(tender.cpvCodes || []).length ? `<p class="aop-meta">CPV : ${tender.cpvCodes.map(esc).join(', ')}</p>` : ''}</section>${ai}${docs}`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Tableau de bord veille (répartition par température). */
|
|
58
|
+
export function veilleDashboard({ total = 0, byTemperature = {}, byStatus = {} } = {}) {
|
|
59
|
+
const kpi = (label, n, color) => `<div class="aop-kpi" style="border-color:${color || '#e5e7eb'}"><b style="color:${color || '#111'}">${n}</b>${esc(label)}</div>`;
|
|
60
|
+
return `${aopStyles()}<section><h1>📊 Veille — tableau de bord</h1>
|
|
61
|
+
<div>${kpi('Total', total)} ${['hot', 'warm', 'cold', 'frozen'].map((t) => kpi(`${temperatureEmoji(t)} ${t}`, byTemperature[t] || 0, temperatureColor(t))).join(' ')}</div>
|
|
62
|
+
<p class="aop-meta">Actifs : ${byStatus.active || 0} · expirés : ${byStatus.expired || 0} · attribués : ${byStatus.awarded || 0}</p></section>`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Éditeur d'alerte (critères de veille). */
|
|
66
|
+
export function alertForm({ alert = {}, action = '/alerts/save' } = {}) {
|
|
67
|
+
const c = alert.criteria || {};
|
|
68
|
+
return `${aopStyles()}<section><h1>🔔 Alerte de veille</h1>
|
|
69
|
+
<form method="post" action="${esc(action)}" style="display:flex;flex-direction:column;gap:8px;max-width:480px">
|
|
70
|
+
<label>Nom <input name="name" value="${esc(alert.name || '')}" required></label>
|
|
71
|
+
<label>Mots-clés (séparés par ,) <input name="keywords" value="${esc((c.keywords || []).join(', '))}"></label>
|
|
72
|
+
<label>Exclure <input name="excludeKeywords" value="${esc((c.excludeKeywords || []).join(', '))}"></label>
|
|
73
|
+
<label>Codes CPV <input name="cpvCodes" value="${esc((c.cpvCodes || []).join(', '))}"></label>
|
|
74
|
+
<label>Budget min <input name="budgetMin" type="number" value="${esc(c.budget?.min ?? '')}"></label>
|
|
75
|
+
<label>Budget max <input name="budgetMax" type="number" value="${esc(c.budget?.max ?? '')}"></label>
|
|
76
|
+
<label>Fréquence <select name="frequency">${['immediate', 'daily', 'weekly', 'monthly'].map((f) => `<option${alert.notifications?.frequency === f ? ' selected' : ''}>${f}</option>`).join('')}</select></label>
|
|
77
|
+
<button>💾 Enregistrer l'alerte</button>
|
|
78
|
+
</form></section>`;
|
|
79
|
+
}
|
|
80
|
+
export default { aopStyles, tenderBadge, tenderCard, tenderList, tenderDetail, veilleDashboard, alertForm };
|