Full-stack app for participatory water pricing using Bezier curves. - Backend: FastAPI + SQLAlchemy + SQLite with JWT auth - Frontend: Nuxt 4 + TypeScript with interactive SVG editor - Math engine: cubic Bezier tarification with Cardano solver - Admin: commune management, household import, vote monitoring, CMS - Citizen: interactive curve editor, vote submission - Docker-compose deployment ready Includes fixes for: - Impact table snake_case/camelCase property mismatch - CMS content backend API + frontend editor (was stub) - Admin route protection middleware - Public content display on commune page - Vote confirmation page link fix Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
90 lines
2.9 KiB
Vue
90 lines
2.9 KiB
Vue
<template>
|
|
<div>
|
|
<div class="page-header">
|
|
<NuxtLink :to="`/admin/communes/${slug}`" style="color: var(--color-text-muted);">← {{ slug }}</NuxtLink>
|
|
<h1>Votes</h1>
|
|
</div>
|
|
|
|
<!-- Median -->
|
|
<div v-if="median" class="card" style="margin-bottom: 1.5rem;">
|
|
<h3>Médiane ({{ median.vote_count }} votes)</h3>
|
|
<div class="grid grid-4" style="margin-top: 1rem;">
|
|
<div><strong>vinf:</strong> {{ median.vinf.toFixed(0) }}</div>
|
|
<div><strong>a:</strong> {{ median.a.toFixed(3) }}</div>
|
|
<div><strong>b:</strong> {{ median.b.toFixed(3) }}</div>
|
|
<div><strong>c:</strong> {{ median.c.toFixed(3) }}</div>
|
|
<div><strong>d:</strong> {{ median.d.toFixed(3) }}</div>
|
|
<div><strong>e:</strong> {{ median.e.toFixed(3) }}</div>
|
|
<div><strong>p0:</strong> {{ median.computed_p0.toFixed(2) }} €/m³</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Vote overlay chart placeholder -->
|
|
<div class="card" style="margin-bottom: 1.5rem;">
|
|
<h3>Overlay des courbes</h3>
|
|
<VoteOverlayChart v-if="overlayData.length" :votes="overlayData" :slug="slug" />
|
|
<p v-else style="color: var(--color-text-muted); padding: 2rem; text-align: center;">
|
|
Aucun vote pour le moment.
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Vote list -->
|
|
<div class="card">
|
|
<h3 style="margin-bottom: 1rem;">Liste des votes actifs</h3>
|
|
<table class="table" v-if="votes.length">
|
|
<thead>
|
|
<tr>
|
|
<th>Foyer</th>
|
|
<th>vinf</th>
|
|
<th>a</th>
|
|
<th>b</th>
|
|
<th>c</th>
|
|
<th>d</th>
|
|
<th>e</th>
|
|
<th>p0</th>
|
|
<th>Date</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="v in votes" :key="v.id">
|
|
<td>#{{ v.household_id }}</td>
|
|
<td>{{ v.vinf.toFixed(0) }}</td>
|
|
<td>{{ v.a.toFixed(2) }}</td>
|
|
<td>{{ v.b.toFixed(2) }}</td>
|
|
<td>{{ v.c.toFixed(2) }}</td>
|
|
<td>{{ v.d.toFixed(2) }}</td>
|
|
<td>{{ v.e.toFixed(2) }}</td>
|
|
<td>{{ v.computed_p0?.toFixed(2) }}</td>
|
|
<td>{{ new Date(v.submitted_at).toLocaleDateString() }}</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
<p v-else style="color: var(--color-text-muted);">Aucun vote actif.</p>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
definePageMeta({ middleware: 'admin' })
|
|
|
|
const route = useRoute()
|
|
const api = useApi()
|
|
const slug = route.params.slug as string
|
|
|
|
const votes = ref<any[]>([])
|
|
const median = ref<any>(null)
|
|
const overlayData = ref<any[]>([])
|
|
|
|
onMounted(async () => {
|
|
try {
|
|
[votes.value, overlayData.value] = await Promise.all([
|
|
api.get<any[]>(`/communes/${slug}/votes`),
|
|
api.get<any[]>(`/communes/${slug}/votes/overlay`),
|
|
])
|
|
} catch {}
|
|
try {
|
|
median.value = await api.get(`/communes/${slug}/votes/median`)
|
|
} catch {}
|
|
})
|
|
</script>
|