v2 : nettoyage des orphelins v1 + documentation livrable
- 43 fichiers v1 supprimés (composants documents/protocols/sanctuary/toolbox, stores auth/documents/groups/mandates/organizations/protocols/votes, composables api/notifications/formula/websocket, utils doublons du moteur) - nuxt.config épuré (polkadot retiré, KaTeX et apiBase gardés), meta v2 - README.md, CONTRIBUTING.md, CLAUDE.md réécrits pour la v2 - Build zéro erreur, 342/342 tests verts Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,219 +0,0 @@
|
||||
/**
|
||||
* Auth store: manages Duniter Ed25519 challenge-response authentication.
|
||||
*
|
||||
* Persists the session token in localStorage for SPA rehydration.
|
||||
* The identity object mirrors the backend IdentityOut schema.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Sign a challenge using the injected Duniter/Substrate wallet extension
|
||||
* (Cesium2, polkadot.js extension, Talisman, etc.).
|
||||
*
|
||||
* The extension signs <Bytes>{challenge}</Bytes> to match the backend verifier.
|
||||
*/
|
||||
// TODO: trustWallet — remplacer par postMessage vers l'iframe trustWallet (librodrome)
|
||||
// Protocole prévu : window.postMessage({ type: 'LD_SIGN_REQUEST', address, challenge })
|
||||
// → trustWallet répond { type: 'LD_SIGN_RESPONSE', signature }
|
||||
async function _signWithExtension(address: string, challenge: string): Promise<string> {
|
||||
const { web3Enable, web3FromAddress } = await import('@polkadot/extension-dapp')
|
||||
const { stringToHex } = await import('@polkadot/util')
|
||||
|
||||
const extensions = await web3Enable('libreDecision')
|
||||
if (!extensions.length) {
|
||||
throw new Error('Aucune extension Duniter détectée. Installez Cesium² ou Polkadot.js.')
|
||||
}
|
||||
|
||||
let injector
|
||||
try {
|
||||
injector = await web3FromAddress(address)
|
||||
} catch {
|
||||
throw new Error(`Adresse ${address.slice(0, 10)}… introuvable dans l'extension.`)
|
||||
}
|
||||
|
||||
if (!injector.signer?.signRaw) {
|
||||
throw new Error("L'extension ne supporte pas la signature de messages bruts.")
|
||||
}
|
||||
|
||||
const { signature } = await injector.signer.signRaw({
|
||||
address,
|
||||
data: stringToHex(challenge),
|
||||
type: 'bytes',
|
||||
})
|
||||
return signature
|
||||
}
|
||||
|
||||
export interface DuniterIdentity {
|
||||
id: string
|
||||
address: string
|
||||
display_name: string | null
|
||||
wot_status: string
|
||||
is_smith: boolean
|
||||
is_techcomm: boolean
|
||||
}
|
||||
|
||||
interface AuthState {
|
||||
token: string | null
|
||||
identity: DuniterIdentity | null
|
||||
loading: boolean
|
||||
error: string | null
|
||||
}
|
||||
|
||||
export const useAuthStore = defineStore('auth', {
|
||||
state: (): AuthState => ({
|
||||
token: null,
|
||||
identity: null,
|
||||
loading: false,
|
||||
error: null,
|
||||
}),
|
||||
|
||||
getters: {
|
||||
isAuthenticated: (state): boolean => !!state.token && !!state.identity,
|
||||
isSmith: (state): boolean => state.identity?.is_smith ?? false,
|
||||
isTechComm: (state): boolean => state.identity?.is_techcomm ?? false,
|
||||
displayName: (state): string => {
|
||||
if (!state.identity) return ''
|
||||
return state.identity.display_name || state.identity.address.slice(0, 12) + '...'
|
||||
},
|
||||
},
|
||||
|
||||
actions: {
|
||||
/**
|
||||
* Initiate the challenge-response login flow.
|
||||
*
|
||||
* Steps:
|
||||
* 1. POST /auth/challenge with the Duniter SS58 address
|
||||
* 2. Client signs the challenge with Ed25519 private key
|
||||
* 3. POST /auth/verify with address + signature + challenge
|
||||
* 4. Store the returned token and identity
|
||||
*/
|
||||
async login(address: string, signFn?: (challenge: string) => Promise<string>) {
|
||||
this.loading = true
|
||||
this.error = null
|
||||
|
||||
try {
|
||||
const { $api } = useApi()
|
||||
|
||||
// Step 1: Request challenge
|
||||
const challengeRes = await $api<{ challenge: string; expires_at: string }>(
|
||||
'/auth/challenge',
|
||||
{
|
||||
method: 'POST',
|
||||
body: { address },
|
||||
},
|
||||
)
|
||||
|
||||
// Step 2: Sign the challenge via polkadot.js / Cesium2 extension
|
||||
let signature: string
|
||||
if (signFn) {
|
||||
signature = await signFn(challengeRes.challenge)
|
||||
} else {
|
||||
signature = await _signWithExtension(address, challengeRes.challenge)
|
||||
}
|
||||
|
||||
// Step 3: Verify and get token
|
||||
const verifyRes = await $api<{ token: string; identity: DuniterIdentity }>(
|
||||
'/auth/verify',
|
||||
{
|
||||
method: 'POST',
|
||||
body: {
|
||||
address,
|
||||
signature,
|
||||
challenge: challengeRes.challenge,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
// Step 4: Store credentials
|
||||
this.token = verifyRes.token
|
||||
this.identity = verifyRes.identity
|
||||
this._persistToken()
|
||||
|
||||
return verifyRes
|
||||
} catch (err: any) {
|
||||
this.error = err?.data?.detail || err?.message || 'Erreur de connexion'
|
||||
throw err
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Fetch the currently authenticated identity from the backend.
|
||||
* Used on app init to validate a persisted token.
|
||||
*/
|
||||
async fetchMe() {
|
||||
if (!this.token) return
|
||||
|
||||
this.loading = true
|
||||
this.error = null
|
||||
|
||||
try {
|
||||
const { $api } = useApi()
|
||||
const identity = await $api<DuniterIdentity>('/auth/me')
|
||||
this.identity = identity
|
||||
} catch (err: any) {
|
||||
const status = (err as any)?.status ?? 0
|
||||
this.error = err?.message || 'Session invalide'
|
||||
// N'effacer le token que sur 401/403 (session réellement invalide)
|
||||
// Les erreurs réseau ou 5xx sont transitoires — conserver la session
|
||||
if (status === 401 || status === 403) {
|
||||
this.token = null
|
||||
this.identity = null
|
||||
this._clearToken()
|
||||
}
|
||||
throw err
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Log out: invalidate session on server and clear local state.
|
||||
*/
|
||||
async logout() {
|
||||
try {
|
||||
if (this.token) {
|
||||
const { $api } = useApi()
|
||||
await $api('/auth/logout', { method: 'POST' })
|
||||
}
|
||||
} catch {
|
||||
// Ignore errors during logout -- clear local state regardless
|
||||
} finally {
|
||||
this.token = null
|
||||
this.identity = null
|
||||
this.error = null
|
||||
this._clearToken()
|
||||
navigateTo('/login')
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Hydrate the token from localStorage on app init.
|
||||
*/
|
||||
hydrateFromStorage() {
|
||||
if (import.meta.client) {
|
||||
const stored = localStorage.getItem('libredecision_token')
|
||||
if (stored) {
|
||||
this.token = stored
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/** @internal Persist token to localStorage */
|
||||
_persistToken() {
|
||||
if (import.meta.client && this.token) {
|
||||
localStorage.setItem('libredecision_token', this.token)
|
||||
}
|
||||
},
|
||||
|
||||
/** @internal Clear token from localStorage */
|
||||
_clearToken() {
|
||||
if (import.meta.client) {
|
||||
localStorage.removeItem('libredecision_token')
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// Note: hydration from localStorage happens in app.vue onMounted
|
||||
// via auth.hydrateFromStorage() before calling auth.fetchMe().
|
||||
@@ -1,313 +0,0 @@
|
||||
/**
|
||||
* 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
|
||||
section_tag: string | null
|
||||
inertia_preset: string
|
||||
is_permanent_vote: boolean
|
||||
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
|
||||
genesis_json: 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[]
|
||||
allItemVersions: Record<string, ItemVersion[]>
|
||||
loadingVersions: boolean
|
||||
loading: boolean
|
||||
error: string | null
|
||||
}
|
||||
|
||||
export const useDocumentsStore = defineStore('documents', {
|
||||
state: (): DocumentsState => ({
|
||||
list: [],
|
||||
current: null,
|
||||
items: [],
|
||||
versions: [],
|
||||
allItemVersions: {},
|
||||
loadingVersions: false,
|
||||
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
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Fetch all versions for every item in a document (parallel).
|
||||
* Used to compute the "projected" view (document as-if all active votes passed).
|
||||
*/
|
||||
async fetchAllItemVersions(slug: string, itemIds: string[]) {
|
||||
this.loadingVersions = true
|
||||
try {
|
||||
const { $api } = useApi()
|
||||
const results = await Promise.allSettled(
|
||||
itemIds.map(id => $api<ItemVersion[]>(`/documents/${slug}/items/${id}/versions`)),
|
||||
)
|
||||
const map: Record<string, ItemVersion[]> = {}
|
||||
itemIds.forEach((id, i) => {
|
||||
const r = results[i]
|
||||
map[id] = r?.status === 'fulfilled' ? r.value : []
|
||||
})
|
||||
this.allItemVersions = map
|
||||
} finally {
|
||||
this.loadingVersions = false
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 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 = []
|
||||
this.allItemVersions = {}
|
||||
this.loadingVersions = false
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -1,109 +0,0 @@
|
||||
export interface GroupMember {
|
||||
id: string
|
||||
display_name: string
|
||||
identity_id: string | null
|
||||
added_at: string
|
||||
}
|
||||
|
||||
export interface Group {
|
||||
id: string
|
||||
name: string
|
||||
description: string | null
|
||||
organization_id: string | null
|
||||
created_at: string
|
||||
members: GroupMember[]
|
||||
}
|
||||
|
||||
export interface GroupSummary {
|
||||
id: string
|
||||
name: string
|
||||
description: string | null
|
||||
organization_id: string | null
|
||||
member_count: number
|
||||
}
|
||||
|
||||
export interface GroupCreate {
|
||||
name: string
|
||||
description?: string | null
|
||||
}
|
||||
|
||||
export interface GroupMemberCreate {
|
||||
display_name: string
|
||||
identity_id?: string | null
|
||||
}
|
||||
|
||||
export const useGroupsStore = defineStore('groups', () => {
|
||||
const { $api } = useApi()
|
||||
|
||||
const list = ref<GroupSummary[]>([])
|
||||
const loading = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
|
||||
async function fetchAll() {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
list.value = await $api<GroupSummary[]>('/groups/')
|
||||
} catch (e: any) {
|
||||
error.value = e?.message ?? 'Erreur chargement groupes'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function getGroup(id: string): Promise<Group | null> {
|
||||
try {
|
||||
return await $api<Group>(`/groups/${id}`)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async function create(payload: GroupCreate): Promise<Group | null> {
|
||||
try {
|
||||
const group = await $api<Group>('/groups/', { method: 'POST', body: payload })
|
||||
await fetchAll()
|
||||
return group
|
||||
} catch (e: any) {
|
||||
error.value = e?.message ?? 'Erreur création groupe'
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async function remove(id: string): Promise<boolean> {
|
||||
try {
|
||||
await $api(`/groups/${id}`, { method: 'DELETE' })
|
||||
list.value = list.value.filter(g => g.id !== id)
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async function addMember(groupId: string, payload: GroupMemberCreate): Promise<GroupMember | null> {
|
||||
try {
|
||||
const member = await $api<GroupMember>(`/groups/${groupId}/members`, {
|
||||
method: 'POST',
|
||||
body: payload,
|
||||
})
|
||||
const g = list.value.find(g => g.id === groupId)
|
||||
if (g) g.member_count++
|
||||
return member
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async function removeMember(groupId: string, memberId: string): Promise<boolean> {
|
||||
try {
|
||||
await $api(`/groups/${groupId}/members/${memberId}`, { method: 'DELETE' })
|
||||
const g = list.value.find(g => g.id === groupId)
|
||||
if (g) g.member_count--
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return { list, loading, error, fetchAll, getGroup, create, remove, addMember, removeMember }
|
||||
})
|
||||
@@ -1,219 +0,0 @@
|
||||
export interface MandateStep {
|
||||
id: string
|
||||
mandate_id: string
|
||||
step_order: number
|
||||
step_type: string
|
||||
title: string | null
|
||||
description: string | null
|
||||
status: string
|
||||
vote_session_id: string | null
|
||||
outcome: string | null
|
||||
created_at: string
|
||||
}
|
||||
|
||||
export interface Mandate {
|
||||
id: string
|
||||
title: string
|
||||
origin_id: string | null
|
||||
origin_display_name: string | null
|
||||
description: string | null
|
||||
mandate_type: string
|
||||
status: string
|
||||
mandatee_id: string | null
|
||||
mandatee_display_name: string | null
|
||||
decision_id: string | null
|
||||
starts_at: string | null
|
||||
ends_at: string | null
|
||||
created_at: string
|
||||
updated_at: string
|
||||
steps: MandateStep[]
|
||||
}
|
||||
|
||||
export interface MandateCreate {
|
||||
title: string
|
||||
origin_id?: string | null
|
||||
description?: string | null
|
||||
mandate_type: string
|
||||
nomination_mode?: string
|
||||
decision_id?: string | null
|
||||
starts_at?: string | null
|
||||
ends_at?: string | null
|
||||
}
|
||||
|
||||
export interface MandateUpdate {
|
||||
title?: string
|
||||
origin_id?: string | null
|
||||
description?: string | null
|
||||
mandate_type?: string
|
||||
starts_at?: string | null
|
||||
ends_at?: string | null
|
||||
}
|
||||
|
||||
export interface MandateStepCreate {
|
||||
step_order: number
|
||||
step_type: string
|
||||
title?: string | null
|
||||
description?: string | null
|
||||
}
|
||||
|
||||
interface MandatesState {
|
||||
list: Mandate[]
|
||||
current: Mandate | null
|
||||
loading: boolean
|
||||
error: string | null
|
||||
}
|
||||
|
||||
export const useMandatesStore = defineStore('mandates', {
|
||||
state: (): MandatesState => ({
|
||||
list: [],
|
||||
current: null,
|
||||
loading: false,
|
||||
error: null,
|
||||
}),
|
||||
|
||||
getters: {
|
||||
byStatus: (state) => (status: string) => state.list.filter(m => m.status === status),
|
||||
activeMandates: (state): Mandate[] => state.list.filter(m => m.status === 'active'),
|
||||
completedMandates: (state): Mandate[] => state.list.filter(m => m.status === 'completed'),
|
||||
},
|
||||
|
||||
actions: {
|
||||
async fetchAll(params?: { mandate_type?: string; status?: string }) {
|
||||
this.loading = true
|
||||
this.error = null
|
||||
try {
|
||||
const { $api } = useApi()
|
||||
const query: Record<string, string> = {}
|
||||
if (params?.mandate_type) query.mandate_type = params.mandate_type
|
||||
if (params?.status) query.status = params.status
|
||||
this.list = await $api<Mandate[]>('/mandates/', { query })
|
||||
} catch (err: any) {
|
||||
this.error = err?.data?.detail || err?.message || 'Erreur lors du chargement des mandats'
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
async fetchById(id: string) {
|
||||
this.loading = true
|
||||
this.error = null
|
||||
try {
|
||||
const { $api } = useApi()
|
||||
this.current = await $api<Mandate>(`/mandates/${id}`)
|
||||
} catch (err: any) {
|
||||
this.error = err?.data?.detail || err?.message || 'Mandat introuvable'
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
async create(payload: MandateCreate) {
|
||||
this.loading = true
|
||||
this.error = null
|
||||
try {
|
||||
const { $api } = useApi()
|
||||
const mandate = await $api<Mandate>('/mandates/', { method: 'POST', body: payload })
|
||||
this.list.unshift(mandate)
|
||||
return mandate
|
||||
} catch (err: any) {
|
||||
this.error = err?.data?.detail || err?.message || 'Erreur lors de la creation du mandat'
|
||||
throw err
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
async update(id: string, data: MandateUpdate) {
|
||||
this.error = null
|
||||
try {
|
||||
const { $api } = useApi()
|
||||
const updated = await $api<Mandate>(`/mandates/${id}`, { method: 'PUT', body: data })
|
||||
if (this.current?.id === id) this.current = updated
|
||||
const idx = this.list.findIndex(m => m.id === id)
|
||||
if (idx >= 0) this.list[idx] = updated
|
||||
return updated
|
||||
} catch (err: any) {
|
||||
this.error = err?.data?.detail || err?.message || 'Erreur lors de la mise a jour du mandat'
|
||||
throw err
|
||||
}
|
||||
},
|
||||
|
||||
async delete(id: string) {
|
||||
this.error = null
|
||||
try {
|
||||
const { $api } = useApi()
|
||||
await $api(`/mandates/${id}`, { method: 'DELETE' })
|
||||
this.list = this.list.filter(m => m.id !== id)
|
||||
if (this.current?.id === id) this.current = null
|
||||
} catch (err: any) {
|
||||
this.error = err?.data?.detail || err?.message || 'Erreur lors de la suppression du mandat'
|
||||
throw err
|
||||
}
|
||||
},
|
||||
|
||||
async advance(id: string) {
|
||||
this.error = null
|
||||
try {
|
||||
const { $api } = useApi()
|
||||
const updated = await $api<Mandate>(`/mandates/${id}/advance`, { method: 'POST' })
|
||||
if (this.current?.id === id) this.current = updated
|
||||
const idx = this.list.findIndex(m => m.id === id)
|
||||
if (idx >= 0) this.list[idx] = updated
|
||||
return updated
|
||||
} catch (err: any) {
|
||||
this.error = err?.data?.detail || err?.message || 'Erreur lors de l\'avancement du mandat'
|
||||
throw err
|
||||
}
|
||||
},
|
||||
|
||||
async addStep(id: string, step: MandateStepCreate) {
|
||||
this.error = null
|
||||
try {
|
||||
const { $api } = useApi()
|
||||
const newStep = await $api<MandateStep>(`/mandates/${id}/steps`, { method: 'POST', body: step })
|
||||
if (this.current?.id === id) this.current.steps.push(newStep)
|
||||
return newStep
|
||||
} catch (err: any) {
|
||||
this.error = err?.data?.detail || err?.message || 'Erreur lors de l\'ajout de l\'etape'
|
||||
throw err
|
||||
}
|
||||
},
|
||||
|
||||
async assignMandatee(id: string, mandateeId: string) {
|
||||
this.error = null
|
||||
try {
|
||||
const { $api } = useApi()
|
||||
const updated = await $api<Mandate>(`/mandates/${id}/assign`, {
|
||||
method: 'POST',
|
||||
body: { mandatee_id: mandateeId },
|
||||
})
|
||||
if (this.current?.id === id) this.current = updated
|
||||
const idx = this.list.findIndex(m => m.id === id)
|
||||
if (idx >= 0) this.list[idx] = updated
|
||||
return updated
|
||||
} catch (err: any) {
|
||||
this.error = err?.data?.detail || err?.message || 'Erreur lors de l\'assignation du mandataire'
|
||||
throw err
|
||||
}
|
||||
},
|
||||
|
||||
async revoke(id: string) {
|
||||
this.error = null
|
||||
try {
|
||||
const { $api } = useApi()
|
||||
const updated = await $api<Mandate>(`/mandates/${id}/revoke`, { method: 'POST' })
|
||||
if (this.current?.id === id) this.current = updated
|
||||
const idx = this.list.findIndex(m => m.id === id)
|
||||
if (idx >= 0) this.list[idx] = updated
|
||||
return updated
|
||||
} catch (err: any) {
|
||||
this.error = err?.data?.detail || err?.message || 'Erreur lors de la revocation du mandat'
|
||||
throw err
|
||||
}
|
||||
},
|
||||
|
||||
clearCurrent() {
|
||||
this.current = null
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -1,71 +0,0 @@
|
||||
export interface Organization {
|
||||
id: string
|
||||
name: string
|
||||
slug: string
|
||||
org_type: string
|
||||
is_transparent: boolean
|
||||
color: string | null
|
||||
icon: string | null
|
||||
description: string | null
|
||||
created_at: string
|
||||
}
|
||||
|
||||
interface OrgState {
|
||||
organizations: Organization[]
|
||||
activeSlug: string | null
|
||||
loading: boolean
|
||||
error: string | null
|
||||
}
|
||||
|
||||
export const useOrganizationsStore = defineStore('organizations', {
|
||||
state: (): OrgState => ({
|
||||
organizations: [],
|
||||
activeSlug: null,
|
||||
loading: false,
|
||||
error: null,
|
||||
}),
|
||||
|
||||
getters: {
|
||||
active: (state): Organization | null =>
|
||||
state.organizations.find(o => o.slug === state.activeSlug) ?? state.organizations[0] ?? null,
|
||||
|
||||
hasOrganizations: (state): boolean => state.organizations.length > 0,
|
||||
},
|
||||
|
||||
actions: {
|
||||
async fetchOrganizations() {
|
||||
this.loading = true
|
||||
this.error = null
|
||||
try {
|
||||
const { $api } = useApi()
|
||||
const orgs = await $api<Organization[]>('/organizations/')
|
||||
// Duniter G1 first, then alphabetical
|
||||
this.organizations = orgs.sort((a, b) => {
|
||||
if (a.slug === 'duniter-g1') return -1
|
||||
if (b.slug === 'duniter-g1') return 1
|
||||
return a.name.localeCompare(b.name)
|
||||
})
|
||||
// Restore persisted active slug, or default to first org
|
||||
const stored = import.meta.client ? localStorage.getItem('libredecision_org') : null
|
||||
if (stored && this.organizations.some(o => o.slug === stored)) {
|
||||
this.activeSlug = stored
|
||||
} else if (this.organizations[0]) {
|
||||
this.activeSlug = this.organizations[0].slug
|
||||
}
|
||||
} catch (err: any) {
|
||||
this.error = err?.message || 'Erreur lors du chargement des organisations'
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
setActive(slug: string) {
|
||||
if (this.organizations.some(o => o.slug === slug)) {
|
||||
this.activeSlug = slug
|
||||
if (import.meta.client) {
|
||||
localStorage.setItem('libredecision_org', slug)
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -1,228 +0,0 @@
|
||||
/**
|
||||
* Protocols store: voting protocols and formula configurations.
|
||||
*
|
||||
* Maps to the backend /api/v1/protocols endpoints.
|
||||
*/
|
||||
|
||||
export interface FormulaConfig {
|
||||
id: string
|
||||
name: string
|
||||
description: string | null
|
||||
duration_days: number
|
||||
majority_pct: number
|
||||
base_exponent: number
|
||||
gradient_exponent: number
|
||||
constant_base: number
|
||||
smith_exponent: number | null
|
||||
techcomm_exponent: number | null
|
||||
nuanced_min_participants: number | null
|
||||
nuanced_threshold_pct: number | null
|
||||
created_at: string
|
||||
}
|
||||
|
||||
export interface VotingProtocol {
|
||||
id: string
|
||||
name: string
|
||||
description: string | null
|
||||
vote_type: string
|
||||
formula_config_id: string
|
||||
mode_params: string | null
|
||||
is_meta_governed: boolean
|
||||
created_at: string
|
||||
formula_config: FormulaConfig
|
||||
}
|
||||
|
||||
export interface ProtocolCreate {
|
||||
name: string
|
||||
description: string | null
|
||||
vote_type: string
|
||||
formula_config_id: string
|
||||
}
|
||||
|
||||
export interface FormulaCreate {
|
||||
name: string
|
||||
description?: string | null
|
||||
duration_days: number
|
||||
majority_pct: number
|
||||
base_exponent: number
|
||||
gradient_exponent: number
|
||||
constant_base: number
|
||||
smith_exponent?: number | null
|
||||
techcomm_exponent?: number | null
|
||||
nuanced_min_participants?: number | null
|
||||
nuanced_threshold_pct?: number | null
|
||||
}
|
||||
|
||||
export interface SimulateParams {
|
||||
wot_size: number
|
||||
total_votes: number
|
||||
majority_pct: number
|
||||
base_exponent: number
|
||||
gradient_exponent: number
|
||||
constant_base: number
|
||||
smith_wot_size?: number
|
||||
smith_exponent?: number
|
||||
techcomm_size?: number
|
||||
techcomm_exponent?: number
|
||||
}
|
||||
|
||||
export interface SimulateResult {
|
||||
threshold: number
|
||||
smith_threshold: number | null
|
||||
techcomm_threshold: number | null
|
||||
inertia_factor: number
|
||||
required_ratio: number
|
||||
}
|
||||
|
||||
interface ProtocolsState {
|
||||
protocols: VotingProtocol[]
|
||||
formulas: FormulaConfig[]
|
||||
currentProtocol: VotingProtocol | null
|
||||
loading: boolean
|
||||
error: string | null
|
||||
}
|
||||
|
||||
export const useProtocolsStore = defineStore('protocols', {
|
||||
state: (): ProtocolsState => ({
|
||||
protocols: [],
|
||||
formulas: [],
|
||||
currentProtocol: null,
|
||||
loading: false,
|
||||
error: null,
|
||||
}),
|
||||
|
||||
getters: {
|
||||
binaryProtocols: (state): VotingProtocol[] => {
|
||||
return state.protocols.filter(p => p.vote_type === 'binary')
|
||||
},
|
||||
nuancedProtocols: (state): VotingProtocol[] => {
|
||||
return state.protocols.filter(p => p.vote_type === 'nuanced')
|
||||
},
|
||||
metaGovernedProtocols: (state): VotingProtocol[] => {
|
||||
return state.protocols.filter(p => p.is_meta_governed)
|
||||
},
|
||||
},
|
||||
|
||||
actions: {
|
||||
/**
|
||||
* Fetch all voting protocols with their formula configurations.
|
||||
*/
|
||||
async fetchProtocols(params?: { vote_type?: string }) {
|
||||
this.loading = true
|
||||
this.error = null
|
||||
|
||||
try {
|
||||
const { $api } = useApi()
|
||||
const query: Record<string, string> = {}
|
||||
if (params?.vote_type) query.vote_type = params.vote_type
|
||||
|
||||
this.protocols = await $api<VotingProtocol[]>('/protocols/', { query })
|
||||
} catch (err: any) {
|
||||
this.error = err?.data?.detail || err?.message || 'Erreur lors du chargement des protocoles'
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Fetch all formula configurations.
|
||||
*/
|
||||
async fetchFormulas() {
|
||||
this.loading = true
|
||||
this.error = null
|
||||
|
||||
try {
|
||||
const { $api } = useApi()
|
||||
this.formulas = await $api<FormulaConfig[]>('/protocols/formulas')
|
||||
} catch (err: any) {
|
||||
this.error = err?.data?.detail || err?.message || 'Erreur lors du chargement des formules'
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Fetch a single protocol by ID.
|
||||
*/
|
||||
async fetchProtocolById(id: string) {
|
||||
this.loading = true
|
||||
this.error = null
|
||||
|
||||
try {
|
||||
const { $api } = useApi()
|
||||
this.currentProtocol = await $api<VotingProtocol>(`/protocols/${id}`)
|
||||
} catch (err: any) {
|
||||
this.error = err?.data?.detail || err?.message || 'Protocole introuvable'
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Create a new voting protocol.
|
||||
*/
|
||||
async createProtocol(data: ProtocolCreate) {
|
||||
this.loading = true
|
||||
this.error = null
|
||||
|
||||
try {
|
||||
const { $api } = useApi()
|
||||
const protocol = await $api<VotingProtocol>('/protocols/', {
|
||||
method: 'POST',
|
||||
body: data,
|
||||
})
|
||||
this.protocols.push(protocol)
|
||||
return protocol
|
||||
} catch (err: any) {
|
||||
this.error = err?.data?.detail || err?.message || 'Erreur lors de la creation du protocole'
|
||||
throw err
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Create a new formula configuration.
|
||||
*/
|
||||
async createFormula(data: FormulaCreate) {
|
||||
this.loading = true
|
||||
this.error = null
|
||||
|
||||
try {
|
||||
const { $api } = useApi()
|
||||
const formula = await $api<FormulaConfig>('/protocols/formulas', {
|
||||
method: 'POST',
|
||||
body: data,
|
||||
})
|
||||
this.formulas.push(formula)
|
||||
return formula
|
||||
} catch (err: any) {
|
||||
this.error = err?.data?.detail || err?.message || 'Erreur lors de la creation de la formule'
|
||||
throw err
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Simulate formula computation on the backend.
|
||||
*/
|
||||
async simulate(params: SimulateParams) {
|
||||
this.loading = true
|
||||
this.error = null
|
||||
|
||||
try {
|
||||
const { $api } = useApi()
|
||||
return await $api<SimulateResult>('/protocols/simulate', {
|
||||
method: 'POST',
|
||||
body: params,
|
||||
})
|
||||
} catch (err: any) {
|
||||
this.error = err?.data?.detail || err?.message || 'Erreur lors de la simulation'
|
||||
throw err
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -1,297 +0,0 @@
|
||||
/**
|
||||
* Votes store: vote sessions, individual votes, and result computation.
|
||||
*
|
||||
* Maps to the backend /api/v1/votes endpoints.
|
||||
*/
|
||||
|
||||
export interface Vote {
|
||||
id: string
|
||||
session_id: string
|
||||
voter_id: string
|
||||
vote_value: string
|
||||
nuanced_level: number | null
|
||||
comment: string | null
|
||||
signature: string
|
||||
signed_payload: string
|
||||
voter_wot_status: string
|
||||
voter_is_smith: boolean
|
||||
voter_is_techcomm: boolean
|
||||
is_active: boolean
|
||||
created_at: string
|
||||
}
|
||||
|
||||
export interface VoteSession {
|
||||
id: string
|
||||
decision_id: string | null
|
||||
item_version_id: string | null
|
||||
voting_protocol_id: string
|
||||
wot_size: number
|
||||
smith_size: number
|
||||
techcomm_size: number
|
||||
starts_at: string
|
||||
ends_at: string
|
||||
status: string
|
||||
votes_for: number
|
||||
votes_against: number
|
||||
votes_total: number
|
||||
smith_votes_for: number
|
||||
techcomm_votes_for: number
|
||||
threshold_required: number
|
||||
result: string | null
|
||||
chain_recorded: boolean
|
||||
chain_tx_hash: string | null
|
||||
created_at: string
|
||||
}
|
||||
|
||||
export interface VoteResult {
|
||||
session_id: string
|
||||
status: string
|
||||
votes_for: number
|
||||
votes_against: number
|
||||
votes_total: number
|
||||
wot_size: number
|
||||
smith_size: number
|
||||
techcomm_size: number
|
||||
smith_votes_for: number
|
||||
techcomm_votes_for: number
|
||||
threshold_required: number
|
||||
result: string
|
||||
smith_threshold: number | null
|
||||
smith_pass: boolean
|
||||
techcomm_threshold: number | null
|
||||
techcomm_pass: boolean
|
||||
}
|
||||
|
||||
export interface ThresholdDetails {
|
||||
wot_threshold: number
|
||||
smith_threshold: number | null
|
||||
techcomm_threshold: number | null
|
||||
wot_pass: boolean
|
||||
smith_pass: boolean | null
|
||||
techcomm_pass: boolean | null
|
||||
inertia_factor: number
|
||||
required_ratio: number
|
||||
}
|
||||
|
||||
export interface VoteCreate {
|
||||
session_id: string
|
||||
vote_value: string
|
||||
nuanced_level?: number | null
|
||||
comment?: string | null
|
||||
signature: string
|
||||
signed_payload: string
|
||||
}
|
||||
|
||||
export interface VoteSessionCreate {
|
||||
decision_id?: string | null
|
||||
item_version_id?: string | null
|
||||
voting_protocol_id: string
|
||||
wot_size?: number
|
||||
smith_size?: number
|
||||
techcomm_size?: number
|
||||
}
|
||||
|
||||
export interface SessionFilters {
|
||||
status?: string
|
||||
voting_protocol_id?: string
|
||||
decision_id?: string
|
||||
}
|
||||
|
||||
interface VotesState {
|
||||
currentSession: VoteSession | null
|
||||
votes: Vote[]
|
||||
result: VoteResult | null
|
||||
thresholdDetails: ThresholdDetails | null
|
||||
sessions: VoteSession[]
|
||||
loading: boolean
|
||||
error: string | null
|
||||
}
|
||||
|
||||
export const useVotesStore = defineStore('votes', {
|
||||
state: (): VotesState => ({
|
||||
currentSession: null,
|
||||
votes: [],
|
||||
result: null,
|
||||
thresholdDetails: null,
|
||||
sessions: [],
|
||||
loading: false,
|
||||
error: null,
|
||||
}),
|
||||
|
||||
getters: {
|
||||
isSessionOpen: (state): boolean => {
|
||||
if (!state.currentSession) return false
|
||||
return state.currentSession.status === 'open' && new Date(state.currentSession.ends_at) > new Date()
|
||||
},
|
||||
participationRate: (state): number => {
|
||||
if (!state.currentSession || state.currentSession.wot_size === 0) return 0
|
||||
return (state.currentSession.votes_total / state.currentSession.wot_size) * 100
|
||||
},
|
||||
forPercentage: (state): number => {
|
||||
if (!state.currentSession || state.currentSession.votes_total === 0) return 0
|
||||
return (state.currentSession.votes_for / state.currentSession.votes_total) * 100
|
||||
},
|
||||
},
|
||||
|
||||
actions: {
|
||||
/**
|
||||
* Fetch a vote session by ID with its votes and result.
|
||||
*/
|
||||
async fetchSession(sessionId: string) {
|
||||
this.loading = true
|
||||
this.error = null
|
||||
|
||||
try {
|
||||
const { $api } = useApi()
|
||||
|
||||
const [session, votes, result] = await Promise.all([
|
||||
$api<VoteSession>(`/votes/sessions/${sessionId}`),
|
||||
$api<Vote[]>(`/votes/sessions/${sessionId}/votes`),
|
||||
$api<VoteResult>(`/votes/sessions/${sessionId}/result`),
|
||||
])
|
||||
|
||||
this.currentSession = session
|
||||
this.votes = votes
|
||||
this.result = result
|
||||
} catch (err: any) {
|
||||
this.error = err?.data?.detail || err?.message || 'Session de vote introuvable'
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Submit a vote to the current session.
|
||||
*/
|
||||
async submitVote(payload: VoteCreate) {
|
||||
this.loading = true
|
||||
this.error = null
|
||||
|
||||
try {
|
||||
const { $api } = useApi()
|
||||
const vote = await $api<Vote>(`/votes/sessions/${payload.session_id}/vote`, {
|
||||
method: 'POST',
|
||||
body: payload,
|
||||
})
|
||||
|
||||
// Update local state
|
||||
this.votes.push(vote)
|
||||
|
||||
// Refresh session tallies and result
|
||||
if (this.currentSession) {
|
||||
const [session, result] = await Promise.all([
|
||||
$api<VoteSession>(`/votes/sessions/${payload.session_id}`),
|
||||
$api<VoteResult>(`/votes/sessions/${payload.session_id}/result`),
|
||||
])
|
||||
this.currentSession = session
|
||||
this.result = result
|
||||
}
|
||||
|
||||
return vote
|
||||
} catch (err: any) {
|
||||
this.error = err?.data?.detail || err?.message || 'Erreur lors du vote'
|
||||
throw err
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Fetch threshold details for a session.
|
||||
*/
|
||||
async fetchThresholdDetails(sessionId: string) {
|
||||
this.loading = true
|
||||
this.error = null
|
||||
|
||||
try {
|
||||
const { $api } = useApi()
|
||||
this.thresholdDetails = await $api<ThresholdDetails>(
|
||||
`/votes/sessions/${sessionId}/threshold`,
|
||||
)
|
||||
} catch (err: any) {
|
||||
this.error = err?.data?.detail || err?.message || 'Erreur lors du chargement des details du seuil'
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Close a vote session.
|
||||
*/
|
||||
async closeSession(sessionId: string) {
|
||||
this.loading = true
|
||||
this.error = null
|
||||
|
||||
try {
|
||||
const { $api } = useApi()
|
||||
const session = await $api<VoteSession>(`/votes/sessions/${sessionId}/close`, {
|
||||
method: 'POST',
|
||||
})
|
||||
this.currentSession = session
|
||||
|
||||
// Refresh result after closing
|
||||
this.result = await $api<VoteResult>(`/votes/sessions/${sessionId}/result`)
|
||||
} catch (err: any) {
|
||||
this.error = err?.data?.detail || err?.message || 'Erreur lors de la fermeture de la session'
|
||||
throw err
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Fetch a list of vote sessions with optional filters.
|
||||
*/
|
||||
async fetchSessions(filters?: SessionFilters) {
|
||||
this.loading = true
|
||||
this.error = null
|
||||
|
||||
try {
|
||||
const { $api } = useApi()
|
||||
const query: Record<string, string> = {}
|
||||
if (filters?.status) query.status = filters.status
|
||||
if (filters?.voting_protocol_id) query.voting_protocol_id = filters.voting_protocol_id
|
||||
if (filters?.decision_id) query.decision_id = filters.decision_id
|
||||
|
||||
this.sessions = await $api<VoteSession[]>('/votes/sessions', { query })
|
||||
} catch (err: any) {
|
||||
this.error = err?.data?.detail || err?.message || 'Erreur lors du chargement des sessions'
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Create a new vote session.
|
||||
*/
|
||||
async createSession(data: VoteSessionCreate) {
|
||||
this.loading = true
|
||||
this.error = null
|
||||
|
||||
try {
|
||||
const { $api } = useApi()
|
||||
const session = await $api<VoteSession>('/votes/sessions', {
|
||||
method: 'POST',
|
||||
body: data,
|
||||
})
|
||||
this.sessions.push(session)
|
||||
return session
|
||||
} catch (err: any) {
|
||||
this.error = err?.data?.detail || err?.message || 'Erreur lors de la creation de la session'
|
||||
throw err
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Clear the current session state.
|
||||
*/
|
||||
clearSession() {
|
||||
this.currentSession = null
|
||||
this.votes = []
|
||||
this.result = null
|
||||
this.thresholdDetails = null
|
||||
},
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user