Files
decision/frontend/app/stores/votes.ts
Yvv cede2a585f Sprint 3 : protocoles de vote et boite a outils
Backend:
- Sessions de vote : list, close, tally, threshold details, auto-expiration
- Protocoles : update, simulate, meta-gouvernance, formulas CRUD
- Service vote enrichi : close_session, get_threshold_details, nuanced breakdown
- Schemas : ThresholdDetailOut, VoteResultOut, FormulaSimulationRequest/Result
- WebSocket broadcast sur chaque vote + fermeture session
- 25 nouveaux tests (threshold details, close, nuanced, simulation)

Frontend:
- 5 composants vote : VoteBinary, VoteNuanced, ThresholdGauge, FormulaDisplay, VoteHistory
- 3 composants protocoles : ProtocolPicker, FormulaEditor, ModeParamsDisplay
- Simulateur de formules interactif (page /protocols/formulas)
- Page detail protocole (/protocols/[id])
- Composable useWebSocket (live updates)
- Composable useVoteFormula (calcul client-side reactif)
- Integration KaTeX pour rendu LaTeX des formules

Documentation:
- API reference : 8 nouveaux endpoints documentes
- Formules : tables d'inertie, parametres detailles, simulation API
- Guide vote : vote binaire/nuance, jauge, historique, simulateur, meta-gouvernance

55 tests passes (+ 1 skipped), 126 fichiers total.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 13:29:31 +01:00

298 lines
7.6 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 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
},
},
})