Sprint 1 : scaffolding complet de Glibredecision
Plateforme de decisions collectives pour Duniter/G1. Backend FastAPI async + PostgreSQL (14 tables, 8 routers, 6 services, moteur de vote avec formule d'inertie WoT/Smith/TechComm). Frontend Nuxt 4 + Nuxt UI v3 + Pinia (9 pages, 5 stores). Infrastructure Docker + Woodpecker CI + Traefik. Documentation technique et utilisateur (15 fichiers). Seed : Licence G1, Engagement Forgeron v2.0.0, 4 protocoles de vote. 30 tests unitaires (formules, mode params, vote nuance) -- tous verts. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
180
frontend/app/stores/auth.ts
Normal file
180
frontend/app/stores/auth.ts
Normal file
@@ -0,0 +1,180 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
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
|
||||
// In production, signFn would use the Duniter keypair to produce an Ed25519 signature.
|
||||
// For development, we use a placeholder signature.
|
||||
let signature: string
|
||||
if (signFn) {
|
||||
signature = await signFn(challengeRes.challenge)
|
||||
} else {
|
||||
// Development placeholder -- backend currently accepts any signature
|
||||
signature = 'dev_signature_placeholder'
|
||||
}
|
||||
|
||||
// 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) {
|
||||
this.error = err?.data?.detail || err?.message || 'Session invalide'
|
||||
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('glibredecision_token')
|
||||
if (stored) {
|
||||
this.token = stored
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/** @internal Persist token to localStorage */
|
||||
_persistToken() {
|
||||
if (import.meta.client && this.token) {
|
||||
localStorage.setItem('glibredecision_token', this.token)
|
||||
}
|
||||
},
|
||||
|
||||
/** @internal Clear token from localStorage */
|
||||
_clearToken() {
|
||||
if (import.meta.client) {
|
||||
localStorage.removeItem('glibredecision_token')
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// Note: hydration from localStorage happens in app.vue onMounted
|
||||
// via auth.hydrateFromStorage() before calling auth.fetchMe().
|
||||
138
frontend/app/stores/decisions.ts
Normal file
138
frontend/app/stores/decisions.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
/**
|
||||
* Decisions store: decision processes and their steps.
|
||||
*
|
||||
* Maps to the backend /api/v1/decisions endpoints.
|
||||
*/
|
||||
|
||||
export interface DecisionStep {
|
||||
id: string
|
||||
decision_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 Decision {
|
||||
id: string
|
||||
title: string
|
||||
description: string | null
|
||||
context: string | null
|
||||
decision_type: string
|
||||
status: string
|
||||
voting_protocol_id: string | null
|
||||
created_by_id: string | null
|
||||
created_at: string
|
||||
updated_at: string
|
||||
steps: DecisionStep[]
|
||||
}
|
||||
|
||||
export interface DecisionCreate {
|
||||
title: string
|
||||
description?: string | null
|
||||
context?: string | null
|
||||
decision_type: string
|
||||
voting_protocol_id?: string | null
|
||||
}
|
||||
|
||||
interface DecisionsState {
|
||||
list: Decision[]
|
||||
current: Decision | null
|
||||
loading: boolean
|
||||
error: string | null
|
||||
}
|
||||
|
||||
export const useDecisionsStore = defineStore('decisions', {
|
||||
state: (): DecisionsState => ({
|
||||
list: [],
|
||||
current: null,
|
||||
loading: false,
|
||||
error: null,
|
||||
}),
|
||||
|
||||
getters: {
|
||||
byStatus: (state) => {
|
||||
return (status: string) => state.list.filter(d => d.status === status)
|
||||
},
|
||||
activeDecisions: (state): Decision[] => {
|
||||
return state.list.filter(d => d.status === 'active' || d.status === 'in_progress')
|
||||
},
|
||||
completedDecisions: (state): Decision[] => {
|
||||
return state.list.filter(d => d.status === 'completed' || d.status === 'closed')
|
||||
},
|
||||
},
|
||||
|
||||
actions: {
|
||||
/**
|
||||
* Fetch all decisions with optional filters.
|
||||
*/
|
||||
async fetchAll(params?: { decision_type?: string; status?: string }) {
|
||||
this.loading = true
|
||||
this.error = null
|
||||
|
||||
try {
|
||||
const { $api } = useApi()
|
||||
const query: Record<string, string> = {}
|
||||
if (params?.decision_type) query.decision_type = params.decision_type
|
||||
if (params?.status) query.status = params.status
|
||||
|
||||
this.list = await $api<Decision[]>('/decisions/', { query })
|
||||
} catch (err: any) {
|
||||
this.error = err?.data?.detail || err?.message || 'Erreur lors du chargement des decisions'
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Fetch a single decision by ID with all its steps.
|
||||
*/
|
||||
async fetchById(id: string) {
|
||||
this.loading = true
|
||||
this.error = null
|
||||
|
||||
try {
|
||||
const { $api } = useApi()
|
||||
this.current = await $api<Decision>(`/decisions/${id}`)
|
||||
} catch (err: any) {
|
||||
this.error = err?.data?.detail || err?.message || 'Decision introuvable'
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Create a new decision.
|
||||
*/
|
||||
async create(payload: DecisionCreate) {
|
||||
this.loading = true
|
||||
this.error = null
|
||||
|
||||
try {
|
||||
const { $api } = useApi()
|
||||
const decision = await $api<Decision>('/decisions/', {
|
||||
method: 'POST',
|
||||
body: payload,
|
||||
})
|
||||
this.list.unshift(decision)
|
||||
return decision
|
||||
} catch (err: any) {
|
||||
this.error = err?.data?.detail || err?.message || 'Erreur lors de la creation de la decision'
|
||||
throw err
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Clear the current decision.
|
||||
*/
|
||||
clearCurrent() {
|
||||
this.current = null
|
||||
},
|
||||
},
|
||||
})
|
||||
149
frontend/app/stores/documents.ts
Normal file
149
frontend/app/stores/documents.ts
Normal file
@@ -0,0 +1,149 @@
|
||||
/**
|
||||
* 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 DocumentCreate {
|
||||
slug: string
|
||||
title: string
|
||||
doc_type: string
|
||||
description?: string | null
|
||||
version?: string
|
||||
}
|
||||
|
||||
interface DocumentsState {
|
||||
list: Document[]
|
||||
current: Document | null
|
||||
items: DocumentItem[]
|
||||
loading: boolean
|
||||
error: string | null
|
||||
}
|
||||
|
||||
export const useDocumentsStore = defineStore('documents', {
|
||||
state: (): DocumentsState => ({
|
||||
list: [],
|
||||
current: null,
|
||||
items: [],
|
||||
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
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Clear the current document and items.
|
||||
*/
|
||||
clearCurrent() {
|
||||
this.current = null
|
||||
this.items = []
|
||||
},
|
||||
},
|
||||
})
|
||||
100
frontend/app/stores/protocols.ts
Normal file
100
frontend/app/stores/protocols.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
|
||||
interface ProtocolsState {
|
||||
protocols: VotingProtocol[]
|
||||
formulas: FormulaConfig[]
|
||||
loading: boolean
|
||||
error: string | null
|
||||
}
|
||||
|
||||
export const useProtocolsStore = defineStore('protocols', {
|
||||
state: (): ProtocolsState => ({
|
||||
protocols: [],
|
||||
formulas: [],
|
||||
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
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
178
frontend/app/stores/votes.ts
Normal file
178
frontend/app/stores/votes.ts
Normal file
@@ -0,0 +1,178 @@
|
||||
/**
|
||||
* 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 VoteCreate {
|
||||
session_id: string
|
||||
vote_value: string
|
||||
nuanced_level?: number | null
|
||||
comment?: string | null
|
||||
signature: string
|
||||
signed_payload: string
|
||||
}
|
||||
|
||||
interface VotesState {
|
||||
currentSession: VoteSession | null
|
||||
votes: Vote[]
|
||||
result: VoteResult | null
|
||||
loading: boolean
|
||||
error: string | null
|
||||
}
|
||||
|
||||
export const useVotesStore = defineStore('votes', {
|
||||
state: (): VotesState => ({
|
||||
currentSession: null,
|
||||
votes: [],
|
||||
result: null,
|
||||
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
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Clear the current session state.
|
||||
*/
|
||||
clearSession() {
|
||||
this.currentSession = null
|
||||
this.votes = []
|
||||
this.result = null
|
||||
},
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user