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:
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