Files
decision/frontend/app/components/decisions/DecisionVigor.vue
T
YvvandClaude Fable 5 e164b5f6c1 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>
2026-08-11 13:47:20 +02:00

270 lines
7.9 KiB
Vue

<script setup lang="ts">
// <!-- ld-v2 --> « En vigueur » block: stewards, measurers, « Ce que ça
// engage », sunset, and the reality check when due — the sought/target/
// observed table ABOVE the three buttons. Measurers record observations here.
import type { Decision } from '~/types/domain'
import {
ENGAGES_LABEL, REVIEW_QUESTION, REVIEW_TITLE, REVIEW_VERDICTS,
} from '~/lexicon'
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 reviewLabels = REVIEW_VERDICTS
const names = (ids: string[]) =>
ids.map(id => col.people.find(p => p.id === id)?.displayName ?? '—').join(', ')
const stewards = computed(() => names(props.decision.stewardIds))
const measurers = computed(() => names(props.decision.measurerIds))
const meId = computed(() => col.me?.id ?? null)
const iMeasure = computed(() =>
meId.value !== null && props.decision.measurerIds.includes(meId.value))
const effects = computed(() => props.decision.brief?.effects ?? [])
const review = computed(() => props.decision.review)
const reviewDue = computed(() =>
review.value !== undefined
&& review.value.verdict === undefined
&& new Date(review.value.dueAt).getTime() <= Date.now())
const reviewDoneLabel = computed(() => {
const r = review.value
if (!r?.verdict) return ''
return `${reviewLabels[r.verdict]} — le ${dateFr(r.decidedAt)}`
})
/** One line for the facts list — done label, or the planned date. */
const reviewLine = computed(() => {
const r = review.value
if (!r) return ''
return r.verdict ? reviewDoneLabel.value : `prévue le ${dateFr(r.dueAt)}`
})
// ── Le geste d'épreuve ──
const reviewNote = ref('')
const feedback = ref('')
async function giveReview(kind: 'confirmed' | 'revise' | 'revoke') {
const note = reviewNote.value.trim()
const result = store.reviewVerdict(props.decision.id, kind, note.length > 0 ? note : undefined)
if ('ok' in result) { feedback.value = result.reason; return }
feedback.value = ''
// revise/revoke chain a child decision under the ORIGINAL protocol — go there.
if (result.id !== props.decision.id) await navigateTo(`/decisions/${result.id}`)
}
// ── Saisie de mesure (measurers) ──
const measureDrafts = ref<Record<number, string>>({})
function saveMeasure(index: number) {
const note = (measureDrafts.value[index] ?? '').trim()
if (note.length === 0 || !meId.value) return
const effect = props.decision.brief?.effects[index]
if (!effect) return
effect.measured = { note, at: col.now(), byId: meId.value }
col.stamp(props.decision)
col.persist()
measureDrafts.value[index] = ''
}
</script>
<template>
<!-- ld-v2 -->
<section class="vig">
<h2 class="vig__title">En vigueur</h2>
<dl class="vig__facts">
<template v-if="stewards">
<dt>Garant·e·s</dt>
<dd>{{ stewards }}</dd>
</template>
<template v-if="measurers">
<dt>Mesureur·e·s</dt>
<dd>{{ measurers }}</dd>
</template>
<template v-if="decision.resources">
<dt>{{ ENGAGES_LABEL }}</dt>
<dd>
{{ decision.resources.note }}
<strong v-if="decision.resources.amount !== undefined">
{{ decision.resources.amount.toLocaleString('fr-FR') }} {{ decision.resources.unit ?? '' }}
</strong>
</dd>
</template>
<template v-if="decision.sunsetAt">
<dt>Échéance de fin</dt>
<dd>{{ dateFr(decision.sunsetAt) }}</dd>
</template>
<template v-if="review">
<dt>{{ REVIEW_TITLE }}</dt>
<dd>{{ reviewLine }}</dd>
</template>
</dl>
<p v-if="review?.note" class="vig__review-note">« {{ review.note }} »</p>
<!-- L'épreuve du réel due -->
<div v-if="reviewDue" class="vig__review">
<h3 class="vig__review-title">{{ REVIEW_TITLE }}</h3>
<p class="vig__review-question">{{ REVIEW_QUESTION }}</p>
<table v-if="effects.length > 0" class="vig__table">
<thead>
<tr>
<th>Effet recherché</th>
<th>Cible</th>
<th>Constaté</th>
</tr>
</thead>
<tbody>
<tr v-for="(effect, i) in effects" :key="i">
<td>{{ effect.label }}</td>
<td>{{ effect.target ?? '—' }}</td>
<td>
<template v-if="effect.measured">
{{ effect.measured.note }}
<span class="vig__measured-at">({{ dateFr(effect.measured.at) }})</span>
</template>
<div v-else-if="iMeasure" class="vig__measure no-print">
<input
v-model="measureDrafts[i]"
type="text"
placeholder="Ce que tu constates…"
>
<button type="button" class="ld-btn ld-btn--ghost" @click="saveMeasure(i)">
Consigner
</button>
</div>
<template v-else></template>
</td>
</tr>
</tbody>
</table>
<p v-else class="vig__hint">
Aucun effet recherché n'avait été formulé — l'épreuve se joue sur la mémoire du collectif.
</p>
<textarea
v-model="reviewNote"
rows="2"
class="no-print"
placeholder="Un mot sur ce que le réel a montré (optionnel)…"
/>
<div class="vig__actions no-print">
<button type="button" class="ld-btn" @click="giveReview('confirmed')">
{{ reviewLabels.confirmed }}
</button>
<button type="button" class="ld-btn ld-btn--ghost" @click="giveReview('revise')">
{{ reviewLabels.revise }}
</button>
<button type="button" class="ld-btn ld-btn--ghost" @click="giveReview('revoke')">
{{ reviewLabels.revoke }}
</button>
</div>
<p v-if="feedback" class="vig__error">{{ feedback }}</p>
</div>
</section>
</template>
<style scoped>
.vig { display: flex; flex-direction: column; gap: 0.875rem; }
.vig__title {
margin: 0;
font-size: 1.0625rem;
font-weight: 800;
letter-spacing: -0.01em;
}
.vig__facts {
margin: 0;
display: grid;
grid-template-columns: auto 1fr;
gap: 0.375rem 1rem;
font-size: 0.9375rem;
}
.vig__facts dt {
font-weight: 700;
color: var(--mood-text-muted);
font-size: 0.875rem;
}
.vig__facts dd { margin: 0; }
.vig__review-note {
margin: 0;
font-style: italic;
color: var(--mood-text-muted);
font-size: 0.9375rem;
}
.vig__review {
padding: 1rem 1.125rem;
border-radius: var(--r-input);
background: color-mix(in srgb, var(--mood-tertiary) 9%, transparent);
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.vig__review-title { margin: 0; font-size: 1rem; font-weight: 800; }
.vig__review-question {
margin: 0;
font-size: 0.9375rem;
font-style: italic;
color: var(--mood-text-muted);
}
.vig__table {
width: 100%;
border-collapse: collapse;
font-size: 0.875rem;
}
.vig__table th {
text-align: left;
font-size: 0.8125rem;
color: var(--mood-text-muted);
padding: 0.375rem 0.5rem;
}
.vig__table td {
padding: 0.375rem 0.5rem;
background: var(--mood-surface);
vertical-align: top;
}
.vig__measured-at { color: var(--mood-text-muted); font-size: 0.8125rem; }
.vig__measure {
display: flex;
gap: 0.375rem;
align-items: center;
flex-wrap: wrap;
}
.vig__measure input {
flex: 1;
min-width: 8rem;
min-height: 2.25rem;
padding: 0.375rem 0.75rem;
font-size: 0.875rem;
}
.vig__review textarea {
width: 100%;
padding: 0.625rem 0.875rem;
font-size: 0.9375rem;
resize: vertical;
}
.vig__actions {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.vig__hint {
margin: 0;
font-size: 0.875rem;
color: var(--mood-text-muted);
}
.vig__error {
margin: 0;
font-size: 0.875rem;
font-weight: 600;
color: var(--mood-error);
}
</style>