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>
This commit is contained in:
142
frontend/app/components/votes/VoteBinary.vue
Normal file
142
frontend/app/components/votes/VoteBinary.vue
Normal file
@@ -0,0 +1,142 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* Binary vote component: Pour / Contre.
|
||||
*
|
||||
* Displays two large buttons for binary voting with confirmation modal.
|
||||
* Integrates with the votes store and auth store for submission and access control.
|
||||
*/
|
||||
const props = defineProps<{
|
||||
sessionId: string
|
||||
disabled?: boolean
|
||||
}>()
|
||||
|
||||
const auth = useAuthStore()
|
||||
const votes = useVotesStore()
|
||||
|
||||
const submitting = ref(false)
|
||||
const pendingVote = ref<'pour' | 'contre' | null>(null)
|
||||
const showConfirm = ref(false)
|
||||
|
||||
/** Check if the current user has already voted in this session. */
|
||||
const userVote = computed(() => {
|
||||
if (!auth.identity) return null
|
||||
return votes.votes.find(v => v.voter_id === auth.identity!.id && v.is_active)
|
||||
})
|
||||
|
||||
const isDisabled = computed(() => {
|
||||
return props.disabled || !auth.isAuthenticated || !votes.isSessionOpen || submitting.value
|
||||
})
|
||||
|
||||
function requestVote(value: 'pour' | 'contre') {
|
||||
if (isDisabled.value) return
|
||||
pendingVote.value = value
|
||||
showConfirm.value = true
|
||||
}
|
||||
|
||||
async function confirmVote() {
|
||||
if (!pendingVote.value) return
|
||||
showConfirm.value = false
|
||||
submitting.value = true
|
||||
|
||||
try {
|
||||
await votes.submitVote({
|
||||
session_id: props.sessionId,
|
||||
vote_value: pendingVote.value,
|
||||
signature: 'pending',
|
||||
signed_payload: 'pending',
|
||||
})
|
||||
} finally {
|
||||
submitting.value = false
|
||||
pendingVote.value = null
|
||||
}
|
||||
}
|
||||
|
||||
function cancelVote() {
|
||||
showConfirm.value = false
|
||||
pendingVote.value = null
|
||||
}
|
||||
|
||||
const confirmLabel = computed(() => {
|
||||
return pendingVote.value === 'pour'
|
||||
? 'Confirmer le vote POUR'
|
||||
: 'Confirmer le vote CONTRE'
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="space-y-4">
|
||||
<!-- Vote buttons -->
|
||||
<div class="flex gap-4">
|
||||
<UButton
|
||||
size="xl"
|
||||
:color="userVote?.vote_value === 'pour' ? 'success' : 'neutral'"
|
||||
:variant="userVote?.vote_value === 'pour' ? 'solid' : 'outline'"
|
||||
:disabled="isDisabled"
|
||||
:loading="submitting && pendingVote === 'pour'"
|
||||
icon="i-lucide-thumbs-up"
|
||||
class="flex-1 justify-center py-6 text-lg"
|
||||
@click="requestVote('pour')"
|
||||
>
|
||||
Pour
|
||||
</UButton>
|
||||
|
||||
<UButton
|
||||
size="xl"
|
||||
:color="userVote?.vote_value === 'contre' ? 'error' : 'neutral'"
|
||||
:variant="userVote?.vote_value === 'contre' ? 'solid' : 'outline'"
|
||||
:disabled="isDisabled"
|
||||
:loading="submitting && pendingVote === 'contre'"
|
||||
icon="i-lucide-thumbs-down"
|
||||
class="flex-1 justify-center py-6 text-lg"
|
||||
@click="requestVote('contre')"
|
||||
>
|
||||
Contre
|
||||
</UButton>
|
||||
</div>
|
||||
|
||||
<!-- Status messages -->
|
||||
<div v-if="!auth.isAuthenticated" class="text-sm text-amber-600 dark:text-amber-400 text-center">
|
||||
Connectez-vous pour voter
|
||||
</div>
|
||||
<div v-else-if="!votes.isSessionOpen" class="text-sm text-gray-500 text-center">
|
||||
Cette session de vote est fermee
|
||||
</div>
|
||||
<div v-else-if="userVote" class="text-sm text-green-600 dark:text-green-400 text-center">
|
||||
Vous avez vote : {{ userVote.vote_value === 'pour' ? 'Pour' : 'Contre' }}
|
||||
</div>
|
||||
|
||||
<!-- Error display -->
|
||||
<div v-if="votes.error" class="text-sm text-red-500 text-center">
|
||||
{{ votes.error }}
|
||||
</div>
|
||||
|
||||
<!-- Confirmation modal -->
|
||||
<UModal v-model:open="showConfirm">
|
||||
<template #content>
|
||||
<div class="p-6 space-y-4">
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
Confirmation du vote
|
||||
</h3>
|
||||
<p class="text-gray-600 dark:text-gray-400">
|
||||
Vous etes sur le point de voter
|
||||
<strong :class="pendingVote === 'pour' ? 'text-green-600' : 'text-red-600'">
|
||||
{{ pendingVote === 'pour' ? 'POUR' : 'CONTRE' }}
|
||||
</strong>.
|
||||
Cette action est definitive.
|
||||
</p>
|
||||
<div class="flex justify-end gap-3">
|
||||
<UButton variant="ghost" color="neutral" @click="cancelVote">
|
||||
Annuler
|
||||
</UButton>
|
||||
<UButton
|
||||
:color="pendingVote === 'pour' ? 'success' : 'error'"
|
||||
@click="confirmVote"
|
||||
>
|
||||
{{ confirmLabel }}
|
||||
</UButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</UModal>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user