- 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>
270 lines
7.9 KiB
Vue
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>
|