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:
114
frontend/app/components/votes/ThresholdGauge.vue
Normal file
114
frontend/app/components/votes/ThresholdGauge.vue
Normal file
@@ -0,0 +1,114 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* Visual gauge showing votes vs threshold.
|
||||
*
|
||||
* Displays a horizontal progress bar with green (pour) and red (contre) fills,
|
||||
* a vertical threshold marker, participation statistics, and a pass/fail badge.
|
||||
*/
|
||||
const props = defineProps<{
|
||||
votesFor: number
|
||||
votesAgainst: number
|
||||
threshold: number
|
||||
wotSize: number
|
||||
}>()
|
||||
|
||||
const totalVotes = computed(() => props.votesFor + props.votesAgainst)
|
||||
|
||||
/** Percentage of "pour" votes relative to total votes. */
|
||||
const forPct = computed(() => {
|
||||
if (totalVotes.value === 0) return 0
|
||||
return (props.votesFor / totalVotes.value) * 100
|
||||
})
|
||||
|
||||
/** Percentage of "contre" votes relative to total votes. */
|
||||
const againstPct = computed(() => {
|
||||
if (totalVotes.value === 0) return 0
|
||||
return (props.votesAgainst / totalVotes.value) * 100
|
||||
})
|
||||
|
||||
/** Position of the threshold marker as a percentage of total votes. */
|
||||
const thresholdPosition = computed(() => {
|
||||
if (totalVotes.value === 0) return 50
|
||||
// Threshold as a percentage of total votes
|
||||
const pct = (props.threshold / totalVotes.value) * 100
|
||||
return Math.min(pct, 100)
|
||||
})
|
||||
|
||||
/** Whether the vote passes (votes_for >= threshold). */
|
||||
const isPassing = computed(() => props.votesFor >= props.threshold)
|
||||
|
||||
/** Participation rate. */
|
||||
const participationRate = computed(() => {
|
||||
if (props.wotSize === 0) return 0
|
||||
return (totalVotes.value / props.wotSize) * 100
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="space-y-3">
|
||||
<!-- Progress bar -->
|
||||
<div class="relative h-8 bg-gray-200 dark:bg-gray-700 rounded-full overflow-hidden">
|
||||
<!-- Green fill (pour) -->
|
||||
<div
|
||||
class="absolute inset-y-0 left-0 bg-green-500 transition-all duration-500"
|
||||
:style="{ width: `${forPct}%` }"
|
||||
/>
|
||||
<!-- Red fill (contre) -->
|
||||
<div
|
||||
class="absolute inset-y-0 bg-red-500 transition-all duration-500"
|
||||
:style="{ left: `${forPct}%`, width: `${againstPct}%` }"
|
||||
/>
|
||||
|
||||
<!-- Threshold marker -->
|
||||
<div
|
||||
v-if="totalVotes > 0"
|
||||
class="absolute inset-y-0 w-0.5 bg-yellow-400 z-10"
|
||||
:style="{ left: `${thresholdPosition}%` }"
|
||||
>
|
||||
<div class="absolute -top-5 left-1/2 -translate-x-1/2 text-xs font-bold text-yellow-600 dark:text-yellow-400 whitespace-nowrap">
|
||||
Seuil
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Percentage labels inside the bar -->
|
||||
<div class="absolute inset-0 flex items-center px-3 text-xs font-bold text-white">
|
||||
<span v-if="forPct > 10">{{ forPct.toFixed(1) }}%</span>
|
||||
<span class="flex-1" />
|
||||
<span v-if="againstPct > 10">{{ againstPct.toFixed(1) }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Counts and threshold text -->
|
||||
<div class="flex items-center justify-between text-sm">
|
||||
<div class="flex items-center gap-4">
|
||||
<span class="text-green-600 dark:text-green-400 font-medium">
|
||||
{{ votesFor }} pour
|
||||
</span>
|
||||
<span class="text-red-600 dark:text-red-400 font-medium">
|
||||
{{ votesAgainst }} contre
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="font-medium" :class="isPassing ? 'text-green-600 dark:text-green-400' : 'text-gray-600 dark:text-gray-400'">
|
||||
{{ votesFor }} / {{ threshold }} requis
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Participation rate -->
|
||||
<div class="flex items-center justify-between text-xs text-gray-500">
|
||||
<span>
|
||||
{{ totalVotes }} vote{{ totalVotes !== 1 ? 's' : '' }} sur {{ wotSize }} membres
|
||||
({{ participationRate.toFixed(2) }}%)
|
||||
</span>
|
||||
|
||||
<!-- Pass/fail badge -->
|
||||
<UBadge
|
||||
:color="isPassing ? 'success' : 'error'"
|
||||
variant="subtle"
|
||||
size="xs"
|
||||
>
|
||||
{{ isPassing ? 'Adopte' : 'Non adopte' }}
|
||||
</UBadge>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user