Files
Yvv 77dceb49c3 Refonte design : 4 humeurs, onboarding, sections avec boite a outils
- 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>
2026-02-28 17:44:48 +01:00

329 lines
9.8 KiB
Vue

<script setup lang="ts">
import type { DocumentItem, VersionProposal } from '~/stores/documents'
const route = useRoute()
const documents = useDocumentsStore()
const auth = useAuthStore()
const slug = computed(() => route.params.slug as string)
const itemId = computed(() => route.params.itemId as string)
const currentItem = computed((): DocumentItem | undefined => {
return documents.items.find(i => i.id === itemId.value)
})
// Modal state for proposing a modification
const showProposeModal = ref(false)
const proposedText = ref('')
const rationale = ref('')
const proposing = ref(false)
// Loading versions
const versionsLoading = ref(false)
onMounted(async () => {
// Fetch document + items if not already loaded
if (!documents.current || documents.current.slug !== slug.value) {
await documents.fetchBySlug(slug.value)
}
// Fetch versions for this item
versionsLoading.value = true
await documents.fetchItemVersions(slug.value, itemId.value)
versionsLoading.value = false
})
onUnmounted(() => {
documents.versions = []
})
watch([slug, itemId], async ([newSlug, newItemId]) => {
if (newSlug && newItemId) {
if (!documents.current || documents.current.slug !== newSlug) {
await documents.fetchBySlug(newSlug)
}
versionsLoading.value = true
await documents.fetchItemVersions(newSlug, newItemId)
versionsLoading.value = false
}
})
function openProposeModal() {
if (currentItem.value) {
proposedText.value = currentItem.value.current_text
}
rationale.value = ''
showProposeModal.value = true
}
async function submitProposal() {
proposing.value = true
try {
const data: VersionProposal = {
proposed_text: proposedText.value,
rationale: rationale.value || null,
}
await documents.proposeVersion(slug.value, itemId.value, data)
showProposeModal.value = false
proposedText.value = ''
rationale.value = ''
} catch {
// Error handled in store
} finally {
proposing.value = false
}
}
async function handleAcceptVersion(versionId: string) {
try {
await documents.acceptVersion(slug.value, itemId.value, versionId)
// Refresh item data
await documents.fetchBySlug(slug.value)
} catch {
// Error handled in store
}
}
async function handleRejectVersion(versionId: string) {
try {
await documents.rejectVersion(slug.value, itemId.value, versionId)
} catch {
// Error handled in store
}
}
const itemTypeLabel = (itemType: string): string => {
switch (itemType) {
case 'clause': return 'Clause'
case 'rule': return 'Regle'
case 'verification': return 'Verification'
case 'preamble': return 'Preambule'
case 'section': return 'Section'
default: return itemType
}
}
function formatDate(dateStr: string): string {
return new Date(dateStr).toLocaleDateString('fr-FR', {
day: 'numeric',
month: 'long',
year: 'numeric',
})
}
</script>
<template>
<div class="space-y-6">
<!-- Breadcrumb -->
<nav class="flex items-center gap-2 text-sm text-gray-500">
<NuxtLink to="/documents" class="hover:text-primary transition-colors">
Documents
</NuxtLink>
<UIcon name="i-lucide-chevron-right" class="text-xs" />
<NuxtLink
v-if="documents.current"
:to="`/documents/${slug}`"
class="hover:text-primary transition-colors"
>
{{ documents.current.title }}
</NuxtLink>
<USkeleton v-else class="h-4 w-32" />
<UIcon name="i-lucide-chevron-right" class="text-xs" />
<span v-if="currentItem" class="text-gray-900 dark:text-white font-medium">
Item {{ currentItem.position }}
</span>
<USkeleton v-else class="h-4 w-20" />
</nav>
<!-- Loading state -->
<template v-if="documents.loading && !currentItem">
<div class="space-y-4">
<USkeleton class="h-8 w-96" />
<USkeleton class="h-4 w-64" />
<USkeleton class="h-48 w-full" />
</div>
</template>
<!-- Error state -->
<template v-else-if="documents.error && !currentItem">
<UCard>
<div class="flex items-center gap-3 text-red-500">
<UIcon name="i-lucide-alert-circle" class="text-xl" />
<p>{{ documents.error }}</p>
</div>
</UCard>
</template>
<!-- Item not found -->
<template v-else-if="!currentItem && !documents.loading">
<UCard>
<div class="text-center py-8">
<UIcon name="i-lucide-file-x" class="text-4xl text-gray-400 mb-3" />
<p class="text-gray-500">Item introuvable</p>
<UButton
:to="`/documents/${slug}`"
label="Retour au document"
variant="soft"
color="primary"
class="mt-4"
/>
</div>
</UCard>
</template>
<!-- Item detail -->
<template v-else-if="currentItem">
<!-- Item header -->
<div class="flex items-start justify-between">
<div>
<div class="flex items-center gap-3 mb-2">
<UBadge variant="solid" color="primary" size="sm">
{{ currentItem.position }}
</UBadge>
<UBadge variant="subtle" color="neutral" size="xs">
{{ itemTypeLabel(currentItem.item_type) }}
</UBadge>
<UBadge
v-if="currentItem.voting_protocol_id"
color="info"
variant="subtle"
size="xs"
>
Sous vote
</UBadge>
</div>
<h1
v-if="currentItem.title"
class="text-2xl font-bold text-gray-900 dark:text-white"
>
{{ currentItem.title }}
</h1>
</div>
<!-- Action buttons -->
<div v-if="auth.isAuthenticated" class="flex items-center gap-2">
<UButton
label="Proposer une modification"
icon="i-lucide-pen-line"
color="primary"
variant="soft"
@click="openProposeModal"
/>
</div>
</div>
<!-- Current text -->
<UCard>
<div class="space-y-3">
<div class="flex items-center gap-2">
<UIcon name="i-lucide-file-text" class="text-gray-400" />
<h2 class="text-sm font-semibold text-gray-500 uppercase">Texte en vigueur</h2>
</div>
<MarkdownRenderer :content="currentItem.current_text" />
<div class="flex items-center gap-4 pt-3 border-t border-gray-100 dark:border-gray-800 text-xs text-gray-400">
<span>Cree le {{ formatDate(currentItem.created_at) }}</span>
<span>Mis a jour le {{ formatDate(currentItem.updated_at) }}</span>
</div>
</div>
</UCard>
<!-- Error banner -->
<UCard v-if="documents.error">
<div class="flex items-center gap-3 text-red-500">
<UIcon name="i-lucide-alert-circle" class="text-xl" />
<p>{{ documents.error }}</p>
</div>
</UCard>
<!-- Version history -->
<div>
<h2 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">
Historique des versions ({{ documents.versions.length }})
</h2>
<template v-if="versionsLoading">
<div class="space-y-3">
<USkeleton v-for="i in 3" :key="i" class="h-32 w-full" />
</div>
</template>
<template v-else-if="documents.versions.length === 0">
<UCard>
<div class="text-center py-6">
<UIcon name="i-lucide-git-branch" class="text-3xl text-gray-400 mb-2" />
<p class="text-gray-500 text-sm">Aucune version proposee pour cet item</p>
<p class="text-xs text-gray-400 mt-1">
Connectez-vous pour proposer une modification
</p>
</div>
</UCard>
</template>
<div v-else class="space-y-4">
<ItemVersionDiff
v-for="version in documents.versions"
:key="version.id"
:version="version"
@accept="handleAcceptVersion"
@reject="handleRejectVersion"
/>
</div>
</div>
</template>
<!-- Propose modification modal -->
<UModal v-model:open="showProposeModal">
<template #content>
<div class="p-6 space-y-4">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">
Proposer une modification
</h3>
<p class="text-sm text-gray-500">
Modifiez le texte ci-dessous et fournissez une justification pour votre proposition.
</p>
<div class="space-y-2">
<label class="text-sm font-medium text-gray-700 dark:text-gray-300">
Texte propose
</label>
<UTextarea
v-model="proposedText"
:rows="8"
placeholder="Saisissez le nouveau texte..."
class="w-full"
/>
</div>
<div class="space-y-2">
<label class="text-sm font-medium text-gray-700 dark:text-gray-300">
Justification
</label>
<UTextarea
v-model="rationale"
:rows="3"
placeholder="Expliquez les raisons de cette modification..."
class="w-full"
/>
</div>
<div class="flex items-center justify-end gap-3 pt-2">
<UButton
label="Annuler"
variant="ghost"
color="neutral"
@click="showProposeModal = false"
/>
<UButton
label="Soumettre la proposition"
icon="i-lucide-send"
color="primary"
:loading="proposing"
:disabled="!proposedText.trim()"
@click="submitProposal"
/>
</div>
</div>
</template>
</UModal>
</div>
</template>