- Systeme de themes adaptatifs : Peps (light chaud), Zen (light calme), Chagrine (dark violet), Grave (dark ambre) avec CSS custom properties - Dashboard d'accueil orienté onboarding avec cartes-portes et teaser boite a outils - SectionLayout reutilisable : liste + sidebar toolbox + status pills cliquables (En prepa / En vote / En vigueur / Clos) - ToolboxVignette : vignettes Contexte / Tutos / Choisir / Demarrer - Seed : Acte engagement certification + forgeron, Runtime Upgrade (decision on-chain), 3 modalites de vote (majoritaire, quadratique, permanent) - Backend adapte SQLite (Uuid portable, 204 fix, pool conditionnel) - Correction noms composants (pathPrefix: false), pinia/nuxt ^0.11 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
465 lines
13 KiB
Vue
465 lines
13 KiB
Vue
<script setup lang="ts">
|
|
import type { DecisionStep, DecisionStepCreate } from '~/stores/decisions'
|
|
|
|
const route = useRoute()
|
|
const decisions = useDecisionsStore()
|
|
|
|
const decisionId = computed(() => route.params.id as string)
|
|
|
|
onMounted(async () => {
|
|
await decisions.fetchById(decisionId.value)
|
|
})
|
|
|
|
onUnmounted(() => {
|
|
decisions.clearCurrent()
|
|
})
|
|
|
|
watch(decisionId, async (newId) => {
|
|
if (newId) {
|
|
await decisions.fetchById(newId)
|
|
}
|
|
})
|
|
|
|
// --- Status helpers ---
|
|
|
|
const statusColor = (status: string) => {
|
|
switch (status) {
|
|
case 'draft': return 'warning'
|
|
case 'qualification': return 'info'
|
|
case 'review': return 'info'
|
|
case 'voting': return 'primary'
|
|
case 'executed': return 'success'
|
|
case 'closed': return 'neutral'
|
|
case 'pending': return 'warning'
|
|
case 'active': return 'success'
|
|
case 'in_progress': return 'success'
|
|
case 'completed': return 'info'
|
|
default: return 'neutral'
|
|
}
|
|
}
|
|
|
|
const statusLabel = (status: string) => {
|
|
switch (status) {
|
|
case 'draft': return 'Brouillon'
|
|
case 'qualification': return 'Qualification'
|
|
case 'review': return 'Revue'
|
|
case 'voting': return 'En vote'
|
|
case 'executed': return 'Execute'
|
|
case 'closed': return 'Clos'
|
|
case 'pending': return 'En attente'
|
|
case 'active': return 'Actif'
|
|
case 'in_progress': return 'En cours'
|
|
case 'completed': return 'Termine'
|
|
default: return status
|
|
}
|
|
}
|
|
|
|
const typeLabel = (decisionType: string) => {
|
|
switch (decisionType) {
|
|
case 'runtime_upgrade': return 'Runtime upgrade'
|
|
case 'document_change': return 'Modification de document'
|
|
case 'mandate_vote': return 'Vote de mandat'
|
|
case 'parameter_change': return 'Changement de parametre'
|
|
case 'other': return 'Autre'
|
|
default: return decisionType
|
|
}
|
|
}
|
|
|
|
function formatDate(dateStr: string): string {
|
|
return new Date(dateStr).toLocaleDateString('fr-FR', {
|
|
day: 'numeric',
|
|
month: 'long',
|
|
year: 'numeric',
|
|
})
|
|
}
|
|
|
|
// --- Terminal state check ---
|
|
|
|
const terminalStatuses = ['executed', 'closed']
|
|
const isTerminal = computed(() => {
|
|
if (!decisions.current) return true
|
|
return terminalStatuses.includes(decisions.current.status)
|
|
})
|
|
|
|
const isDraft = computed(() => decisions.current?.status === 'draft')
|
|
|
|
// --- Advance action ---
|
|
|
|
const advancing = ref(false)
|
|
|
|
async function handleAdvance() {
|
|
advancing.value = true
|
|
try {
|
|
await decisions.advance(decisionId.value)
|
|
} catch {
|
|
// Error handled by store
|
|
} finally {
|
|
advancing.value = false
|
|
}
|
|
}
|
|
|
|
// --- Create vote session ---
|
|
|
|
async function handleCreateVoteSession(step: DecisionStep) {
|
|
try {
|
|
await decisions.createVoteSession(decisionId.value, step.id)
|
|
} catch {
|
|
// Error handled by store
|
|
}
|
|
}
|
|
|
|
// --- Edit modal ---
|
|
|
|
const showEditModal = ref(false)
|
|
const editData = ref({
|
|
title: '',
|
|
description: '' as string | null,
|
|
context: '' as string | null,
|
|
})
|
|
const saving = ref(false)
|
|
|
|
function openEdit() {
|
|
if (!decisions.current) return
|
|
editData.value = {
|
|
title: decisions.current.title,
|
|
description: decisions.current.description,
|
|
context: decisions.current.context,
|
|
}
|
|
showEditModal.value = true
|
|
}
|
|
|
|
async function saveEdit() {
|
|
saving.value = true
|
|
try {
|
|
await decisions.update(decisionId.value, editData.value)
|
|
showEditModal.value = false
|
|
} catch {
|
|
// Error handled by store
|
|
} finally {
|
|
saving.value = false
|
|
}
|
|
}
|
|
|
|
// --- Delete ---
|
|
|
|
const showDeleteConfirm = ref(false)
|
|
const deleting = ref(false)
|
|
|
|
async function handleDelete() {
|
|
deleting.value = true
|
|
try {
|
|
await decisions.delete(decisionId.value)
|
|
navigateTo('/decisions')
|
|
} catch {
|
|
// Error handled by store
|
|
} finally {
|
|
deleting.value = false
|
|
showDeleteConfirm.value = false
|
|
}
|
|
}
|
|
|
|
// --- Add step ---
|
|
|
|
const showAddStep = ref(false)
|
|
const newStep = ref<DecisionStepCreate>({
|
|
step_type: 'qualification',
|
|
title: '',
|
|
description: '',
|
|
})
|
|
const addingStep = ref(false)
|
|
|
|
const stepTypeOptions = [
|
|
{ label: 'Qualification', value: 'qualification' },
|
|
{ label: 'Revue', value: 'review' },
|
|
{ label: 'Vote', value: 'vote' },
|
|
{ label: 'Execution', value: 'execution' },
|
|
{ label: 'Compte rendu', value: 'reporting' },
|
|
]
|
|
|
|
async function handleAddStep() {
|
|
addingStep.value = true
|
|
try {
|
|
await decisions.addStep(decisionId.value, newStep.value)
|
|
showAddStep.value = false
|
|
newStep.value = { step_type: 'qualification', title: '', description: '' }
|
|
} catch {
|
|
// Error handled by store
|
|
} finally {
|
|
addingStep.value = false
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div class="space-y-6">
|
|
<!-- Back link -->
|
|
<div>
|
|
<UButton
|
|
to="/decisions"
|
|
variant="ghost"
|
|
color="neutral"
|
|
icon="i-lucide-arrow-left"
|
|
label="Retour aux decisions"
|
|
size="sm"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Loading state -->
|
|
<template v-if="decisions.loading">
|
|
<div class="space-y-4">
|
|
<USkeleton class="h-8 w-96" />
|
|
<USkeleton class="h-4 w-64" />
|
|
<div class="space-y-3 mt-8">
|
|
<USkeleton v-for="i in 4" :key="i" class="h-20 w-full" />
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<!-- Error state -->
|
|
<template v-else-if="decisions.error">
|
|
<UCard>
|
|
<div class="flex items-center gap-3 text-red-500">
|
|
<UIcon name="i-lucide-alert-circle" class="text-xl" />
|
|
<p>{{ decisions.error }}</p>
|
|
</div>
|
|
</UCard>
|
|
</template>
|
|
|
|
<!-- Decision detail -->
|
|
<template v-else-if="decisions.current">
|
|
<!-- Header with actions -->
|
|
<div class="flex items-start justify-between">
|
|
<div>
|
|
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">
|
|
{{ decisions.current.title }}
|
|
</h1>
|
|
<div class="flex items-center gap-3 mt-2">
|
|
<UBadge variant="subtle" color="primary">
|
|
{{ typeLabel(decisions.current.decision_type) }}
|
|
</UBadge>
|
|
<UBadge :color="statusColor(decisions.current.status)" variant="subtle">
|
|
{{ statusLabel(decisions.current.status) }}
|
|
</UBadge>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Action buttons -->
|
|
<div class="flex items-center gap-2">
|
|
<UButton
|
|
v-if="!isTerminal"
|
|
icon="i-lucide-fast-forward"
|
|
label="Avancer la decision"
|
|
color="primary"
|
|
variant="soft"
|
|
size="sm"
|
|
:loading="advancing"
|
|
@click="handleAdvance"
|
|
/>
|
|
<UButton
|
|
icon="i-lucide-pen-line"
|
|
label="Modifier"
|
|
variant="soft"
|
|
color="neutral"
|
|
size="sm"
|
|
@click="openEdit"
|
|
/>
|
|
<UButton
|
|
v-if="isDraft"
|
|
icon="i-lucide-trash-2"
|
|
label="Supprimer"
|
|
variant="soft"
|
|
color="error"
|
|
size="sm"
|
|
@click="showDeleteConfirm = true"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Description & Context -->
|
|
<UCard v-if="decisions.current.description || decisions.current.context">
|
|
<div class="space-y-4">
|
|
<div v-if="decisions.current.description">
|
|
<h3 class="text-sm font-semibold text-gray-500 uppercase tracking-wide mb-1">Description</h3>
|
|
<p class="text-sm text-gray-700 dark:text-gray-300 whitespace-pre-wrap">
|
|
{{ decisions.current.description }}
|
|
</p>
|
|
</div>
|
|
<div v-if="decisions.current.context">
|
|
<h3 class="text-sm font-semibold text-gray-500 uppercase tracking-wide mb-1">Contexte</h3>
|
|
<p class="text-sm text-gray-700 dark:text-gray-300 whitespace-pre-wrap">
|
|
{{ decisions.current.context }}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</UCard>
|
|
|
|
<!-- Metadata -->
|
|
<UCard>
|
|
<div class="grid grid-cols-2 md:grid-cols-3 gap-4 text-sm">
|
|
<div>
|
|
<p class="text-gray-500">Cree le</p>
|
|
<p class="font-medium text-gray-900 dark:text-white">
|
|
{{ formatDate(decisions.current.created_at) }}
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<p class="text-gray-500">Mis a jour le</p>
|
|
<p class="font-medium text-gray-900 dark:text-white">
|
|
{{ formatDate(decisions.current.updated_at) }}
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<p class="text-gray-500">Nombre d'etapes</p>
|
|
<p class="font-medium text-gray-900 dark:text-white">
|
|
{{ decisions.current.steps.length }}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</UCard>
|
|
|
|
<!-- Steps timeline -->
|
|
<div>
|
|
<div class="flex items-center justify-between mb-4">
|
|
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">
|
|
Etapes du processus
|
|
</h2>
|
|
<UButton
|
|
v-if="!isTerminal"
|
|
icon="i-lucide-plus"
|
|
label="Ajouter une etape"
|
|
variant="soft"
|
|
color="primary"
|
|
size="sm"
|
|
@click="showAddStep = true"
|
|
/>
|
|
</div>
|
|
|
|
<DecisionWorkflow
|
|
:steps="decisions.current.steps"
|
|
:current-status="decisions.current.status"
|
|
@create-vote-session="handleCreateVoteSession"
|
|
/>
|
|
</div>
|
|
</template>
|
|
|
|
<!-- Edit modal -->
|
|
<UModal v-model:open="showEditModal">
|
|
<template #content>
|
|
<div class="p-6 space-y-4">
|
|
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">
|
|
Modifier la decision
|
|
</h3>
|
|
|
|
<div class="space-y-1">
|
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">Titre</label>
|
|
<UInput v-model="editData.title" />
|
|
</div>
|
|
|
|
<div class="space-y-1">
|
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">Description</label>
|
|
<UTextarea v-model="editData.description" :rows="4" />
|
|
</div>
|
|
|
|
<div class="space-y-1">
|
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">Contexte</label>
|
|
<UTextarea v-model="editData.context" :rows="3" />
|
|
</div>
|
|
|
|
<div class="flex justify-end gap-2 pt-4 border-t border-gray-200 dark:border-gray-700">
|
|
<UButton
|
|
label="Annuler"
|
|
variant="ghost"
|
|
color="neutral"
|
|
@click="showEditModal = false"
|
|
/>
|
|
<UButton
|
|
label="Enregistrer"
|
|
icon="i-lucide-save"
|
|
color="primary"
|
|
:loading="saving"
|
|
:disabled="!editData.title?.trim()"
|
|
@click="saveEdit"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</UModal>
|
|
|
|
<!-- Delete confirmation modal -->
|
|
<UModal v-model:open="showDeleteConfirm">
|
|
<template #content>
|
|
<div class="p-6 space-y-4">
|
|
<h3 class="text-lg font-semibold text-red-600">
|
|
Confirmer la suppression
|
|
</h3>
|
|
<p class="text-sm text-gray-600 dark:text-gray-400">
|
|
Etes-vous sur de vouloir supprimer cette decision ? Cette action est irreversible.
|
|
</p>
|
|
<div class="flex justify-end gap-2 pt-4 border-t border-gray-200 dark:border-gray-700">
|
|
<UButton
|
|
label="Annuler"
|
|
variant="ghost"
|
|
color="neutral"
|
|
@click="showDeleteConfirm = false"
|
|
/>
|
|
<UButton
|
|
label="Supprimer"
|
|
icon="i-lucide-trash-2"
|
|
color="error"
|
|
:loading="deleting"
|
|
@click="handleDelete"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</UModal>
|
|
|
|
<!-- Add step modal -->
|
|
<UModal v-model:open="showAddStep">
|
|
<template #content>
|
|
<div class="p-6 space-y-4">
|
|
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">
|
|
Ajouter une etape
|
|
</h3>
|
|
|
|
<div class="space-y-1">
|
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
|
Type d'etape <span class="text-red-500">*</span>
|
|
</label>
|
|
<USelect
|
|
v-model="newStep.step_type"
|
|
:items="stepTypeOptions"
|
|
/>
|
|
</div>
|
|
|
|
<div class="space-y-1">
|
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">Titre</label>
|
|
<UInput v-model="newStep.title" placeholder="Titre de l'etape..." />
|
|
</div>
|
|
|
|
<div class="space-y-1">
|
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">Description</label>
|
|
<UTextarea v-model="newStep.description" :rows="3" placeholder="Description de l'etape..." />
|
|
</div>
|
|
|
|
<div class="flex justify-end gap-2 pt-4 border-t border-gray-200 dark:border-gray-700">
|
|
<UButton
|
|
label="Annuler"
|
|
variant="ghost"
|
|
color="neutral"
|
|
@click="showAddStep = false"
|
|
/>
|
|
<UButton
|
|
label="Ajouter"
|
|
icon="i-lucide-plus"
|
|
color="primary"
|
|
:loading="addingStep"
|
|
@click="handleAddStep"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</UModal>
|
|
</div>
|
|
</template>
|