Initial commit: SejeteralO water tarification platform
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>
This commit is contained in:
89
frontend/app/pages/admin/communes/[slug]/votes.vue
Normal file
89
frontend/app/pages/admin/communes/[slug]/votes.vue
Normal file
@@ -0,0 +1,89 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user