- Fix accents manquants dans 7 pages UI (décisions, boîte à outils, etc.) - Titres accueil enrichis : Décisions structurantes, Documents de référence, Mandats et nominations, Protocoles et fonctionnement - Retrait Embarquement Forgeron du seed (n'est pas une Decision) - 2 protocoles opérationnels dans Protocoles : Embarquement Forgeron (lié à l'Acte d'engagement) + Soumission Runtime Upgrade (lié à la Décision Runtime Upgrade) avec timeline et liens croisés signalétiques - Décision Runtime Upgrade : badge on-chain + lien protocole + contexte - Document [slug] : lien protocole dans la section Qualification Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
475 lines
12 KiB
Vue
475 lines
12 KiB
Vue
<script setup lang="ts">
|
|
/**
|
|
* Decisions — page index.
|
|
*
|
|
* Utilise SectionLayout avec status filters, recherche, tri,
|
|
* et sidebar "Boîte à outils" affichant les protocoles de vote.
|
|
*/
|
|
const decisions = useDecisionsStore()
|
|
const protocols = useProtocolsStore()
|
|
const auth = useAuthStore()
|
|
|
|
const activeStatus = ref<string | null>(null)
|
|
const searchQuery = ref('')
|
|
const sortBy = ref<'date' | 'title' | 'status'>('date')
|
|
|
|
const sortOptions = [
|
|
{ label: 'Date', value: 'date' },
|
|
{ label: 'Titre', value: 'title' },
|
|
{ label: 'Statut', value: 'status' },
|
|
]
|
|
|
|
onMounted(async () => {
|
|
await Promise.all([
|
|
decisions.fetchAll(),
|
|
protocols.fetchProtocols(),
|
|
])
|
|
})
|
|
|
|
/** Status filter pills with counts. */
|
|
const statuses = computed(() => [
|
|
{ id: 'draft', label: 'En prépa', count: decisions.list.filter(d => d.status === 'draft').length },
|
|
{ id: 'voting', label: 'En vote', count: decisions.list.filter(d => d.status === 'voting' || d.status === 'qualification' || d.status === 'review').length },
|
|
{ id: 'executed', label: 'En vigueur', count: decisions.list.filter(d => d.status === 'executed').length },
|
|
{ id: 'closed', label: 'Clos', count: decisions.list.filter(d => d.status === 'closed').length },
|
|
])
|
|
|
|
/** Map for the voting pill — include qualification/review under "En vote". */
|
|
const statusGroupMap: Record<string, string[]> = {
|
|
draft: ['draft'],
|
|
voting: ['qualification', 'review', 'voting'],
|
|
executed: ['executed'],
|
|
closed: ['closed'],
|
|
}
|
|
|
|
/** Filtered and sorted decisions. */
|
|
const filteredDecisions = computed(() => {
|
|
let list = [...decisions.list]
|
|
|
|
// Filter by status group
|
|
if (activeStatus.value && statusGroupMap[activeStatus.value]) {
|
|
const statuses = statusGroupMap[activeStatus.value]
|
|
list = list.filter(d => statuses.includes(d.status))
|
|
}
|
|
|
|
// Filter by search query (client-side)
|
|
if (searchQuery.value.trim()) {
|
|
const q = searchQuery.value.toLowerCase()
|
|
list = list.filter(d => d.title.toLowerCase().includes(q))
|
|
}
|
|
|
|
// Sort
|
|
switch (sortBy.value) {
|
|
case 'title':
|
|
list.sort((a, b) => a.title.localeCompare(b.title, 'fr'))
|
|
break
|
|
case 'status':
|
|
list.sort((a, b) => a.status.localeCompare(b.status))
|
|
break
|
|
case 'date':
|
|
default:
|
|
list.sort((a, b) => new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime())
|
|
break
|
|
}
|
|
|
|
return list
|
|
})
|
|
|
|
const typeLabel = (decisionType: string) => {
|
|
switch (decisionType) {
|
|
case 'runtime_upgrade': return 'Runtime upgrade'
|
|
case 'document_change': return 'Modif. document'
|
|
case 'mandate_vote': return 'Vote de mandat'
|
|
case 'parameter_change': return 'Param. change'
|
|
case 'other': return 'Autre'
|
|
default: return decisionType
|
|
}
|
|
}
|
|
|
|
function formatDate(dateStr: string): string {
|
|
return new Date(dateStr).toLocaleDateString('fr-FR', {
|
|
day: 'numeric',
|
|
month: 'short',
|
|
year: 'numeric',
|
|
})
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<SectionLayout
|
|
title="Décisions"
|
|
subtitle="Processus de décision collectifs"
|
|
:statuses="statuses"
|
|
:active-status="activeStatus"
|
|
@update:active-status="activeStatus = $event"
|
|
>
|
|
<!-- Search / sort bar -->
|
|
<template #search>
|
|
<div class="search-field">
|
|
<UIcon name="i-lucide-search" class="search-field__icon" />
|
|
<input
|
|
v-model="searchQuery"
|
|
type="text"
|
|
class="search-field__input"
|
|
placeholder="Rechercher une décision..."
|
|
/>
|
|
</div>
|
|
<select v-model="sortBy" class="sort-select">
|
|
<option v-for="opt in sortOptions" :key="opt.value" :value="opt.value">
|
|
{{ opt.label }}
|
|
</option>
|
|
</select>
|
|
<NuxtLink
|
|
v-if="auth.isAuthenticated"
|
|
to="/decisions/new"
|
|
class="action-btn"
|
|
>
|
|
<UIcon name="i-lucide-plus" class="text-xs" />
|
|
<span>Nouvelle</span>
|
|
</NuxtLink>
|
|
</template>
|
|
|
|
<!-- Main content: decision list -->
|
|
<template #default>
|
|
<!-- Error state -->
|
|
<div v-if="decisions.error" class="flex items-center gap-3 p-4 rounded-lg" style="background: var(--mood-surface); border: 1px solid var(--mood-border);">
|
|
<UIcon name="i-lucide-alert-circle" class="text-xl" style="color: var(--mood-error);" />
|
|
<p style="color: var(--mood-text);">{{ decisions.error }}</p>
|
|
</div>
|
|
|
|
<!-- Loading state -->
|
|
<div v-else-if="decisions.loading" class="space-y-3">
|
|
<LoadingSkeleton v-for="i in 5" :key="i" :lines="2" card />
|
|
</div>
|
|
|
|
<!-- Empty state -->
|
|
<div
|
|
v-else-if="filteredDecisions.length === 0"
|
|
class="text-center py-12"
|
|
style="color: var(--mood-text-muted);"
|
|
>
|
|
<UIcon name="i-lucide-scale" class="text-4xl mb-3 block mx-auto" />
|
|
<p>Aucune décision trouvée</p>
|
|
<p v-if="searchQuery || activeStatus" class="text-sm mt-1">
|
|
Essayez de modifier vos filtres
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Decision cards -->
|
|
<div v-else class="space-y-3">
|
|
<div
|
|
v-for="decision in filteredDecisions"
|
|
:key="decision.id"
|
|
class="decision-card"
|
|
@click="navigateTo(`/decisions/${decision.id}`)"
|
|
>
|
|
<div class="decision-card__header">
|
|
<div class="decision-card__title-block">
|
|
<h3 class="decision-card__title">
|
|
{{ decision.title }}
|
|
</h3>
|
|
<p v-if="decision.description" class="decision-card__description">
|
|
{{ decision.description }}
|
|
</p>
|
|
</div>
|
|
<StatusBadge :status="decision.status" type="decision" />
|
|
</div>
|
|
|
|
<div class="decision-card__meta">
|
|
<span class="decision-card__type-badge">
|
|
{{ typeLabel(decision.decision_type) }}
|
|
</span>
|
|
<span
|
|
v-if="decision.decision_type === 'runtime_upgrade'"
|
|
class="decision-card__onchain-badge"
|
|
>
|
|
<UIcon name="i-lucide-link" class="text-xs" />
|
|
on-chain
|
|
</span>
|
|
<span class="decision-card__steps">
|
|
<UIcon name="i-lucide-layers" class="text-xs" />
|
|
{{ decision.steps.length }} étape{{ decision.steps.length !== 1 ? 's' : '' }}
|
|
</span>
|
|
<span class="decision-card__date">
|
|
<UIcon name="i-lucide-clock" class="text-xs" />
|
|
{{ formatDate(decision.created_at) }}
|
|
</span>
|
|
</div>
|
|
<!-- Protocol link for runtime_upgrade -->
|
|
<NuxtLink
|
|
v-if="decision.decision_type === 'runtime_upgrade'"
|
|
to="/protocols"
|
|
class="decision-card__protocol-link"
|
|
@click.stop
|
|
>
|
|
<UIcon name="i-lucide-git-branch" class="text-xs" />
|
|
<span>Protocole : Soumission Runtime Upgrade</span>
|
|
<UIcon name="i-lucide-arrow-right" class="text-xs" />
|
|
</NuxtLink>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<!-- Toolbox sidebar -->
|
|
<template #toolbox>
|
|
<ToolboxVignette
|
|
title="Vote majoritaire WoT"
|
|
:bullets="['Seuil adaptatif à la participation', 'Formule g1vote inertielle']"
|
|
:actions="[
|
|
{ label: 'Simuler', icon: 'i-lucide-calculator', to: '/protocols/formulas', primary: true },
|
|
]"
|
|
/>
|
|
<ToolboxVignette
|
|
title="Vote nuancé"
|
|
:bullets="['6 niveaux de préférence', 'Seuil de satisfaction 80%']"
|
|
:actions="[
|
|
{ label: 'Voir', icon: 'i-lucide-bar-chart-3', emit: 'nuance' },
|
|
]"
|
|
/>
|
|
<ToolboxVignette
|
|
title="Mandature"
|
|
:bullets="['Élection en binôme', 'Transparence et révocation']"
|
|
:actions="[
|
|
{ label: 'Mandats', icon: 'i-lucide-user-check', to: '/mandates', primary: true },
|
|
]"
|
|
/>
|
|
</template>
|
|
</SectionLayout>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.decision-card {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.5rem;
|
|
padding: 1rem;
|
|
background: var(--mood-surface);
|
|
border-radius: 16px;
|
|
cursor: pointer;
|
|
transition: transform 0.12s ease, box-shadow 0.12s ease;
|
|
}
|
|
|
|
@media (min-width: 640px) {
|
|
.decision-card {
|
|
gap: 0.625rem;
|
|
padding: 1.25rem;
|
|
}
|
|
}
|
|
|
|
.decision-card:hover {
|
|
transform: translateY(-3px);
|
|
box-shadow: 0 8px 24px var(--mood-shadow);
|
|
}
|
|
|
|
.decision-card:active {
|
|
transform: translateY(0);
|
|
}
|
|
|
|
.decision-card__header {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
justify-content: space-between;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.decision-card__title-block {
|
|
flex: 1;
|
|
min-width: 0;
|
|
}
|
|
|
|
.decision-card__title {
|
|
font-size: 0.9375rem;
|
|
font-weight: 700;
|
|
color: var(--mood-text);
|
|
line-height: 1.3;
|
|
}
|
|
|
|
@media (min-width: 640px) {
|
|
.decision-card__title {
|
|
font-size: 1.0625rem;
|
|
}
|
|
}
|
|
|
|
.decision-card__description {
|
|
margin-top: 0.25rem;
|
|
font-size: 0.8125rem;
|
|
color: var(--mood-text-muted);
|
|
line-height: 1.5;
|
|
display: -webkit-box;
|
|
-webkit-line-clamp: 2;
|
|
-webkit-box-orient: vertical;
|
|
overflow: hidden;
|
|
}
|
|
|
|
@media (min-width: 640px) {
|
|
.decision-card__description {
|
|
font-size: 0.875rem;
|
|
}
|
|
}
|
|
|
|
.decision-card__meta {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
align-items: center;
|
|
gap: 0.375rem;
|
|
}
|
|
|
|
@media (min-width: 640px) {
|
|
.decision-card__meta {
|
|
gap: 0.5rem;
|
|
}
|
|
}
|
|
|
|
.decision-card__steps {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 0.25rem;
|
|
font-size: 0.8125rem;
|
|
color: var(--mood-text-muted);
|
|
}
|
|
|
|
.decision-card__date {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 0.25rem;
|
|
font-size: 0.75rem;
|
|
color: var(--mood-text-muted);
|
|
margin-left: auto;
|
|
opacity: 0.7;
|
|
}
|
|
|
|
@media (min-width: 640px) {
|
|
.decision-card__date {
|
|
font-size: 0.8125rem;
|
|
}
|
|
}
|
|
|
|
.decision-card__type-badge {
|
|
font-size: 0.6875rem;
|
|
font-weight: 700;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.04em;
|
|
padding: 3px 10px;
|
|
border-radius: 20px;
|
|
background: var(--mood-accent-soft);
|
|
color: var(--mood-accent);
|
|
}
|
|
|
|
.decision-card__onchain-badge {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 0.25rem;
|
|
font-size: 0.625rem;
|
|
font-weight: 700;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.04em;
|
|
padding: 3px 8px;
|
|
border-radius: 20px;
|
|
background: color-mix(in srgb, var(--mood-success) 15%, transparent);
|
|
color: var(--mood-success);
|
|
}
|
|
|
|
.decision-card__protocol-link {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 0.375rem;
|
|
padding: 0.375rem 0.75rem;
|
|
font-size: 0.75rem;
|
|
font-weight: 600;
|
|
border-radius: 20px;
|
|
text-decoration: none;
|
|
background: color-mix(in srgb, var(--mood-tertiary, var(--mood-accent)) 10%, transparent);
|
|
color: var(--mood-tertiary, var(--mood-accent));
|
|
transition: transform 0.12s ease, box-shadow 0.12s ease;
|
|
width: fit-content;
|
|
}
|
|
|
|
.decision-card__protocol-link:hover {
|
|
transform: translateY(-1px);
|
|
box-shadow: 0 2px 8px var(--mood-shadow);
|
|
}
|
|
|
|
/* --- Modern search / sort / action --- */
|
|
.search-field {
|
|
flex: 1;
|
|
min-width: 10rem;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
padding: 0.625rem 1rem;
|
|
background: var(--mood-accent-soft);
|
|
border-radius: 12px;
|
|
transition: box-shadow 0.15s ease;
|
|
}
|
|
.search-field:focus-within {
|
|
box-shadow: 0 0 0 2.5px var(--mood-accent-soft);
|
|
}
|
|
.search-field__icon {
|
|
color: var(--mood-text-muted);
|
|
opacity: 0.5;
|
|
font-size: 0.875rem;
|
|
flex-shrink: 0;
|
|
}
|
|
.search-field__input {
|
|
flex: 1;
|
|
background: none;
|
|
font-size: 0.9375rem;
|
|
color: var(--mood-text);
|
|
min-width: 0;
|
|
}
|
|
.search-field__input::placeholder {
|
|
color: var(--mood-text-muted);
|
|
opacity: 0.4;
|
|
}
|
|
|
|
.sort-select {
|
|
padding: 0.625rem 1rem;
|
|
font-size: 0.875rem;
|
|
font-weight: 600;
|
|
color: var(--mood-text);
|
|
background: var(--mood-accent-soft);
|
|
border-radius: 12px;
|
|
cursor: pointer;
|
|
appearance: none;
|
|
-webkit-appearance: none;
|
|
min-width: 5.5rem;
|
|
}
|
|
|
|
.action-btn {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 0.375rem;
|
|
padding: 0.625rem 1.25rem;
|
|
font-size: 0.875rem;
|
|
font-weight: 700;
|
|
color: var(--mood-accent-text);
|
|
background: var(--mood-accent);
|
|
border-radius: 20px;
|
|
cursor: pointer;
|
|
text-decoration: none;
|
|
transition: transform 0.12s ease, box-shadow 0.12s ease;
|
|
white-space: nowrap;
|
|
}
|
|
.action-btn:hover {
|
|
transform: translateY(-1px);
|
|
box-shadow: 0 4px 12px var(--mood-shadow);
|
|
}
|
|
.action-btn:active {
|
|
transform: translateY(0);
|
|
}
|
|
|
|
.toolbox-section-title {
|
|
font-size: 0.8125rem;
|
|
font-weight: 700;
|
|
color: var(--mood-text-muted);
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.04em;
|
|
margin-bottom: 0.25rem;
|
|
}
|
|
|
|
.toolbox-empty-text {
|
|
font-size: 0.8125rem;
|
|
color: var(--mood-text-muted);
|
|
}
|
|
</style>
|