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>
179 lines
4.4 KiB
TypeScript
179 lines
4.4 KiB
TypeScript
/**
|
|
* 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
|
|
},
|
|
},
|
|
})
|