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:
Yvv
2026-02-21 15:26:02 +01:00
commit b30e54a8f7
67 changed files with 16723 additions and 0 deletions

View File

@@ -0,0 +1,77 @@
<template>
<div>
<div class="page-header">
<NuxtLink :to="`/admin/communes/${slug}`" style="color: var(--color-text-muted);">&larr; {{ slug }}</NuxtLink>
<h1>Paramètres tarifs</h1>
</div>
<div v-if="saved" class="alert alert-success">Paramètres enregistrés.</div>
<div v-if="error" class="alert alert-error">{{ error }}</div>
<div class="card" style="max-width: 600px;">
<form @submit.prevent="save">
<div class="form-group">
<label>Recettes cibles ()</label>
<input v-model.number="form.recettes" type="number" class="form-input" step="1000" min="0" />
</div>
<div class="grid grid-2">
<div class="form-group">
<label>Abonnement RP/PRO ()</label>
<input v-model.number="form.abop" type="number" class="form-input" step="1" min="0" />
</div>
<div class="form-group">
<label>Abonnement RS ()</label>
<input v-model.number="form.abos" type="number" class="form-input" step="1" min="0" />
</div>
</div>
<div class="grid grid-2">
<div class="form-group">
<label>Prix max/ ()</label>
<input v-model.number="form.pmax" type="number" class="form-input" step="0.5" min="0" />
</div>
<div class="form-group">
<label>Volume max ()</label>
<input v-model.number="form.vmax" type="number" class="form-input" step="100" min="0" />
</div>
</div>
<button type="submit" class="btn btn-primary" :disabled="loading">
Enregistrer
</button>
</form>
</div>
</div>
</template>
<script setup lang="ts">
definePageMeta({ middleware: 'admin' })
const route = useRoute()
const api = useApi()
const slug = route.params.slug as string
const form = reactive({ recettes: 75000, abop: 100, abos: 100, pmax: 20, vmax: 2100 })
const loading = ref(false)
const saved = ref(false)
const error = ref('')
onMounted(async () => {
try {
const params = await api.get<typeof form>(`/communes/${slug}/params`)
Object.assign(form, params)
} catch {}
})
async function save() {
loading.value = true
saved.value = false
error.value = ''
try {
await api.put(`/communes/${slug}/params`, form)
saved.value = true
} catch (e: any) {
error.value = e.message
} finally {
loading.value = false
}
}
</script>