forked from yvv/decision
v2 : les 14 écrans — le tour complet de la démocratie d'exercice
- Aujourd'hui (Fil 13 sections + capture sticky) + Le chemin (tunnel 2 gestes, Q0 inline, 3 chips, dérogation asymétrique, alternatives réglage/consignation) - Registre + fiche décision (timeline, périmètre premier/second lieu auditable, affluence non-ignorable, S'instruire condensé, éléments + cartographie de clôture, épreuve du réel, Remettre en question, PV A4, gravure) - Salle de vote 5 modalités (consentement, nuancé+histogramme, binaire hérité avec jauge inertielle, Réglage collectif complet — faisceau, médiane basse, Pour moi, Explorer, cristallisation-geste —, élection à départage humain) - Textes (Pacte en clair, document vivant, diff, vue projetée, Atelier des formules porté du v1 sur le moteur unique) + Mandats (faits comptés, feux de la rampe, wizard 3 étapes) + Observatoire (consigner→observer→protocoliser) - Onboarding 7 gabarits + Données locales (export/import, attributs, atelier) - voting→framing gardé (Reformuler d'un réglage figé non cristallisé) - Pages v1 supprimées (login, documents, mandates, protocols, sanctuary, tools, decisions/new) — 342 tests verts, build zéro erreur Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,288 @@
|
||||
<script setup lang="ts">
|
||||
// <!-- ld-v2 --> ÉLÉMENTS tab of a split dossier: element children with states
|
||||
// and deadlines, 0-3 stake weighting by the concerned (Concern.priority — feeds
|
||||
// the closing cartography, never the voting right), auto-generated closing
|
||||
// cartography, and the steward's « dossier complet » closing gesture.
|
||||
import type { Decision } from '~/types/domain'
|
||||
import { DOSSIER_COMPLETE_CARD, STATUS_LABELS } from '~/lexicon'
|
||||
import { TERMINAL_STATUSES } from '~/engine'
|
||||
import { useCollectiveStore } from '~/stores/collective'
|
||||
import { useDecisionsStore } from '~/stores/decisions'
|
||||
import { dateFr } from './decisionUi'
|
||||
|
||||
const props = defineProps<{ decision: Decision }>()
|
||||
|
||||
const col = useCollectiveStore()
|
||||
const store = useDecisionsStore()
|
||||
|
||||
const elements = computed(() =>
|
||||
col.decisions.filter(d =>
|
||||
d.parentDecisionId === props.decision.id && d.chainKind === 'element'))
|
||||
|
||||
const meId = computed(() => col.me?.id ?? null)
|
||||
|
||||
function priorityStats(elementId: string) {
|
||||
const priorities = col.concerns
|
||||
.filter(c => c.decisionId === elementId && c.priority !== undefined)
|
||||
.map(c => c.priority as number)
|
||||
if (priorities.length === 0) return { avg: null as number | null, count: 0 }
|
||||
const avg = priorities.reduce((a, b) => a + b, 0) / priorities.length
|
||||
return { avg: Math.round(avg * 10) / 10, count: priorities.length }
|
||||
}
|
||||
|
||||
function myPriority(elementId: string): number | undefined {
|
||||
if (!meId.value) return undefined
|
||||
return col.concerns.find(
|
||||
c => c.decisionId === elementId && c.personId === meId.value,
|
||||
)?.priority
|
||||
}
|
||||
|
||||
/** « Pondère tes enjeux » — set my priority on this element (0-3). */
|
||||
function setPriority(elementId: string, priority: 0 | 1 | 2 | 3) {
|
||||
if (!meId.value || !col.current) return
|
||||
let mine = col.concerns.find(
|
||||
c => c.decisionId === elementId && c.personId === meId.value,
|
||||
)
|
||||
if (!mine) {
|
||||
const created = store.declareConcern(elementId, meId.value)
|
||||
if ('ok' in created) return
|
||||
mine = created
|
||||
}
|
||||
mine.priority = priority
|
||||
col.stamp(mine)
|
||||
col.persist()
|
||||
}
|
||||
|
||||
const allTerminal = computed(() =>
|
||||
elements.value.length > 0
|
||||
&& elements.value.every(e => TERMINAL_STATUSES.includes(e.status)))
|
||||
|
||||
const isSteward = computed(() => {
|
||||
if (!meId.value) return false
|
||||
if (props.decision.stewardIds.length > 0) {
|
||||
return props.decision.stewardIds.includes(meId.value)
|
||||
}
|
||||
return props.decision.authorId === meId.value
|
||||
})
|
||||
|
||||
const closeError = ref('')
|
||||
function closeDossier() {
|
||||
const result = store.transition(props.decision.id, 'closed')
|
||||
closeError.value = result.ok ? '' : result.reason
|
||||
}
|
||||
|
||||
const PRIORITIES = [0, 1, 2, 3] as const
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- ld-v2 -->
|
||||
<section class="els">
|
||||
<h2 class="els__title">Éléments du dossier</h2>
|
||||
<p class="els__hint">
|
||||
Chaque élément suit son propre chemin — pondère tes enjeux de 0 à 3 :
|
||||
la pondération nourrit la cartographie, jamais le droit de vote.
|
||||
</p>
|
||||
|
||||
<ul class="els__list">
|
||||
<li v-for="el in elements" :key="el.id" class="els__item">
|
||||
<div class="els__item-main">
|
||||
<NuxtLink :to="`/decisions/${el.id}`" class="els__item-title">
|
||||
{{ el.title }}
|
||||
</NuxtLink>
|
||||
<span class="status-pill" :class="`status-${el.status}`">
|
||||
{{ STATUS_LABELS[el.status] }}
|
||||
</span>
|
||||
<LdCountdown
|
||||
v-if="el.windowEndsAt"
|
||||
:ends-at="el.windowEndsAt"
|
||||
:suspended-at="el.windowSuspendedAt"
|
||||
/>
|
||||
</div>
|
||||
<div class="els__weighting no-print">
|
||||
<span class="els__weighting-label">Mes enjeux</span>
|
||||
<button
|
||||
v-for="p in PRIORITIES"
|
||||
:key="p"
|
||||
type="button"
|
||||
class="els__prio"
|
||||
:class="{ 'els__prio--on': myPriority(el.id) === p }"
|
||||
@click="setPriority(el.id, p)"
|
||||
>
|
||||
{{ p }}
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<!-- Cartographie de clôture — auto-générée -->
|
||||
<div class="els__map">
|
||||
<h3 class="els__map-title">Cartographie de clôture</h3>
|
||||
<table class="els__table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Élément</th>
|
||||
<th>État</th>
|
||||
<th>Enjeu moyen</th>
|
||||
<th>Échéance</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="el in elements" :key="el.id">
|
||||
<td>{{ el.title }}</td>
|
||||
<td>
|
||||
<span class="status-pill" :class="`status-${el.status}`">
|
||||
{{ STATUS_LABELS[el.status] }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<template v-if="priorityStats(el.id).avg !== null">
|
||||
{{ priorityStats(el.id).avg }} / 3
|
||||
<span class="els__map-count">({{ priorityStats(el.id).count }})</span>
|
||||
</template>
|
||||
<template v-else>—</template>
|
||||
</td>
|
||||
<td>{{ el.windowEndsAt ? dateFr(el.windowEndsAt) : (el.decidedAt ? dateFr(el.decidedAt) : '—') }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Dossier complet — le geste du garant, jamais automatique -->
|
||||
<div v-if="allTerminal" class="els__complete">
|
||||
<p class="els__complete-text">
|
||||
<UIcon name="i-lucide-flag" />
|
||||
<span>{{ DOSSIER_COMPLETE_CARD }}</span>
|
||||
</p>
|
||||
<button
|
||||
v-if="isSteward"
|
||||
type="button"
|
||||
class="ld-btn no-print"
|
||||
@click="closeDossier()"
|
||||
>
|
||||
<UIcon name="i-lucide-stamp" />
|
||||
<span>Clore le dossier</span>
|
||||
</button>
|
||||
<p v-else class="els__hint">Le geste appartient au garant du dossier.</p>
|
||||
<p v-if="closeError" class="els__error">{{ closeError }}</p>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.els { display: flex; flex-direction: column; gap: 0.875rem; }
|
||||
.els__title {
|
||||
margin: 0;
|
||||
font-size: 1.0625rem;
|
||||
font-weight: 800;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
.els__hint {
|
||||
margin: 0;
|
||||
font-size: 0.875rem;
|
||||
color: var(--mood-text-muted);
|
||||
}
|
||||
.els__list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.625rem;
|
||||
}
|
||||
.els__item {
|
||||
padding: 0.75rem 0.875rem;
|
||||
border-radius: var(--r-input);
|
||||
background: var(--mood-accent-soft);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.els__item-main {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.els__item-title {
|
||||
font-size: 0.9375rem;
|
||||
font-weight: 700;
|
||||
color: var(--mood-text);
|
||||
text-decoration: none;
|
||||
flex: 1;
|
||||
min-width: 10rem;
|
||||
}
|
||||
.els__item-title:hover { color: var(--mood-accent); }
|
||||
.els__weighting {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
}
|
||||
.els__weighting-label {
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 600;
|
||||
color: var(--mood-text-muted);
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
.els__prio {
|
||||
width: 2.25rem;
|
||||
height: 2.25rem;
|
||||
border-radius: 50%;
|
||||
background: var(--mood-surface);
|
||||
font-weight: 800;
|
||||
font-size: 0.9375rem;
|
||||
color: var(--mood-text-muted);
|
||||
cursor: pointer;
|
||||
transition: all 0.12s ease;
|
||||
box-shadow: var(--shadow-card);
|
||||
}
|
||||
.els__prio:hover { transform: translateY(-1px); }
|
||||
.els__prio--on {
|
||||
background: var(--mood-accent);
|
||||
color: var(--mood-accent-text);
|
||||
}
|
||||
.els__map-title {
|
||||
margin: 0 0 0.5rem;
|
||||
font-size: 0.9375rem;
|
||||
font-weight: 800;
|
||||
}
|
||||
.els__table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
.els__table th {
|
||||
text-align: left;
|
||||
font-size: 0.8125rem;
|
||||
color: var(--mood-text-muted);
|
||||
padding: 0.375rem 0.5rem;
|
||||
}
|
||||
.els__table td {
|
||||
padding: 0.375rem 0.5rem;
|
||||
background: var(--mood-accent-soft);
|
||||
}
|
||||
.els__table tbody tr td:first-child { border-radius: var(--r-input) 0 0 var(--r-input); }
|
||||
.els__table tbody tr td:last-child { border-radius: 0 var(--r-input) var(--r-input) 0; }
|
||||
.els__map-count { color: var(--mood-text-muted); font-size: 0.8125rem; }
|
||||
.els__complete {
|
||||
padding: 1rem 1.125rem;
|
||||
border-radius: var(--r-input);
|
||||
background: var(--mood-status-vigueur-bg);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.625rem;
|
||||
}
|
||||
.els__complete-text {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-weight: 800;
|
||||
color: var(--mood-status-vigueur);
|
||||
}
|
||||
.els__error {
|
||||
margin: 0;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: var(--mood-error);
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user