Backend: - CRUD complet documents/items/versions (update, delete, accept, reject, reorder) - Service IPFS (upload/retrieve/pin via kubo HTTP API) - Service sanctuaire : pipeline SHA-256 + IPFS + on-chain (system.remark) - Verification integrite des entrees sanctuaire - Recherche par reference (document -> entrees sanctuaire) - Serialisation deterministe des documents pour archivage - 14 tests unitaires supplementaires (document service) Frontend: - 9 composants : StatusBadge, MarkdownRenderer, DiffView, ItemCard, ItemVersionDiff, DocumentList, SanctuaryEntry, IPFSLink, ChainAnchor - Page detail item avec historique des versions et diff - Page detail sanctuaire avec verification integrite - Modal de creation de document + proposition de version - Archivage document vers sanctuaire depuis la page detail Documentation: - API reference mise a jour (9 nouveaux endpoints) - Guides utilisateur documents et sanctuaire enrichis Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
282 lines
7.0 KiB
TypeScript
282 lines
7.0 KiB
TypeScript
/**
|
|
* Documents store: reference documents, their items, and item versions.
|
|
*
|
|
* Maps to the backend /api/v1/documents endpoints.
|
|
*/
|
|
|
|
export interface DocumentItem {
|
|
id: string
|
|
document_id: string
|
|
position: string
|
|
item_type: string
|
|
title: string | null
|
|
current_text: string
|
|
voting_protocol_id: string | null
|
|
sort_order: number
|
|
created_at: string
|
|
updated_at: string
|
|
}
|
|
|
|
export interface Document {
|
|
id: string
|
|
slug: string
|
|
title: string
|
|
doc_type: string
|
|
version: string
|
|
status: string
|
|
description: string | null
|
|
ipfs_cid: string | null
|
|
chain_anchor: string | null
|
|
created_at: string
|
|
updated_at: string
|
|
items_count: number
|
|
}
|
|
|
|
export interface ItemVersion {
|
|
id: string
|
|
item_id: string
|
|
version_number: number
|
|
proposed_text: string
|
|
rationale: string | null
|
|
diff: string | null
|
|
status: string
|
|
proposed_by: string | null
|
|
reviewed_by: string | null
|
|
created_at: string
|
|
updated_at: string
|
|
}
|
|
|
|
export interface DocumentCreate {
|
|
slug: string
|
|
title: string
|
|
doc_type: string
|
|
description?: string | null
|
|
version?: string
|
|
}
|
|
|
|
export interface VersionProposal {
|
|
proposed_text: string
|
|
rationale?: string | null
|
|
}
|
|
|
|
interface DocumentsState {
|
|
list: Document[]
|
|
current: Document | null
|
|
items: DocumentItem[]
|
|
versions: ItemVersion[]
|
|
loading: boolean
|
|
error: string | null
|
|
}
|
|
|
|
export const useDocumentsStore = defineStore('documents', {
|
|
state: (): DocumentsState => ({
|
|
list: [],
|
|
current: null,
|
|
items: [],
|
|
versions: [],
|
|
loading: false,
|
|
error: null,
|
|
}),
|
|
|
|
getters: {
|
|
byType: (state) => {
|
|
return (docType: string) => state.list.filter(d => d.doc_type === docType)
|
|
},
|
|
activeDocuments: (state): Document[] => {
|
|
return state.list.filter(d => d.status === 'active')
|
|
},
|
|
draftDocuments: (state): Document[] => {
|
|
return state.list.filter(d => d.status === 'draft')
|
|
},
|
|
},
|
|
|
|
actions: {
|
|
/**
|
|
* Fetch all documents with optional filters.
|
|
*/
|
|
async fetchAll(params?: { doc_type?: string; status?: string }) {
|
|
this.loading = true
|
|
this.error = null
|
|
|
|
try {
|
|
const { $api } = useApi()
|
|
const query: Record<string, string> = {}
|
|
if (params?.doc_type) query.doc_type = params.doc_type
|
|
if (params?.status) query.status = params.status
|
|
|
|
this.list = await $api<Document[]>('/documents/', { query })
|
|
} catch (err: any) {
|
|
this.error = err?.data?.detail || err?.message || 'Erreur lors du chargement des documents'
|
|
} finally {
|
|
this.loading = false
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Fetch a single document by slug and its items.
|
|
*/
|
|
async fetchBySlug(slug: string) {
|
|
this.loading = true
|
|
this.error = null
|
|
|
|
try {
|
|
const { $api } = useApi()
|
|
|
|
const [doc, items] = await Promise.all([
|
|
$api<Document>(`/documents/${slug}`),
|
|
$api<DocumentItem[]>(`/documents/${slug}/items`),
|
|
])
|
|
|
|
this.current = doc
|
|
this.items = items
|
|
} catch (err: any) {
|
|
this.error = err?.data?.detail || err?.message || 'Document introuvable'
|
|
} finally {
|
|
this.loading = false
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Create a new reference document.
|
|
*/
|
|
async createDocument(payload: DocumentCreate) {
|
|
this.loading = true
|
|
this.error = null
|
|
|
|
try {
|
|
const { $api } = useApi()
|
|
const doc = await $api<Document>('/documents/', {
|
|
method: 'POST',
|
|
body: payload,
|
|
})
|
|
this.list.unshift(doc)
|
|
return doc
|
|
} catch (err: any) {
|
|
this.error = err?.data?.detail || err?.message || 'Erreur lors de la creation du document'
|
|
throw err
|
|
} finally {
|
|
this.loading = false
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Fetch all versions for a specific item within a document.
|
|
*/
|
|
async fetchItemVersions(slug: string, itemId: string) {
|
|
this.loading = true
|
|
this.error = null
|
|
|
|
try {
|
|
const { $api } = useApi()
|
|
this.versions = await $api<ItemVersion[]>(
|
|
`/documents/${slug}/items/${itemId}/versions`,
|
|
)
|
|
} catch (err: any) {
|
|
this.error = err?.data?.detail || err?.message || 'Erreur lors du chargement des versions'
|
|
} finally {
|
|
this.loading = false
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Propose a new version for a document item.
|
|
*/
|
|
async proposeVersion(slug: string, itemId: string, data: VersionProposal) {
|
|
this.error = null
|
|
|
|
try {
|
|
const { $api } = useApi()
|
|
const version = await $api<ItemVersion>(
|
|
`/documents/${slug}/items/${itemId}/versions`,
|
|
{
|
|
method: 'POST',
|
|
body: data,
|
|
},
|
|
)
|
|
this.versions.unshift(version)
|
|
return version
|
|
} catch (err: any) {
|
|
this.error = err?.data?.detail || err?.message || 'Erreur lors de la proposition'
|
|
throw err
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Accept a proposed version.
|
|
*/
|
|
async acceptVersion(slug: string, itemId: string, versionId: string) {
|
|
this.error = null
|
|
|
|
try {
|
|
const { $api } = useApi()
|
|
const updated = await $api<ItemVersion>(
|
|
`/documents/${slug}/items/${itemId}/versions/${versionId}/accept`,
|
|
{ method: 'POST' },
|
|
)
|
|
const idx = this.versions.findIndex(v => v.id === versionId)
|
|
if (idx >= 0) this.versions[idx] = updated
|
|
return updated
|
|
} catch (err: any) {
|
|
this.error = err?.data?.detail || err?.message || 'Erreur lors de l\'acceptation'
|
|
throw err
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Reject a proposed version.
|
|
*/
|
|
async rejectVersion(slug: string, itemId: string, versionId: string) {
|
|
this.error = null
|
|
|
|
try {
|
|
const { $api } = useApi()
|
|
const updated = await $api<ItemVersion>(
|
|
`/documents/${slug}/items/${itemId}/versions/${versionId}/reject`,
|
|
{ method: 'POST' },
|
|
)
|
|
const idx = this.versions.findIndex(v => v.id === versionId)
|
|
if (idx >= 0) this.versions[idx] = updated
|
|
return updated
|
|
} catch (err: any) {
|
|
this.error = err?.data?.detail || err?.message || 'Erreur lors du rejet'
|
|
throw err
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Archive a document into the Sanctuary.
|
|
*/
|
|
async archiveDocument(slug: string) {
|
|
this.error = null
|
|
|
|
try {
|
|
const { $api } = useApi()
|
|
const doc = await $api<Document>(
|
|
`/documents/${slug}/archive`,
|
|
{ method: 'POST' },
|
|
)
|
|
// Update current if viewing this document
|
|
if (this.current?.slug === slug) {
|
|
this.current = doc
|
|
}
|
|
// Update in list
|
|
const idx = this.list.findIndex(d => d.slug === slug)
|
|
if (idx >= 0) this.list[idx] = doc
|
|
return doc
|
|
} catch (err: any) {
|
|
this.error = err?.data?.detail || err?.message || 'Erreur lors de l\'archivage'
|
|
throw err
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Clear the current document, items and versions.
|
|
*/
|
|
clearCurrent() {
|
|
this.current = null
|
|
this.items = []
|
|
this.versions = []
|
|
},
|
|
},
|
|
})
|