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:
56
frontend/app/stores/auth.ts
Normal file
56
frontend/app/stores/auth.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
interface AuthState {
|
||||
token: string | null
|
||||
role: string | null
|
||||
communeSlug: string | null
|
||||
}
|
||||
|
||||
export const useAuthStore = defineStore('auth', {
|
||||
state: (): AuthState => ({
|
||||
token: null,
|
||||
role: null,
|
||||
communeSlug: null,
|
||||
}),
|
||||
|
||||
getters: {
|
||||
isAuthenticated: (state) => !!state.token,
|
||||
isAdmin: (state) => state.role === 'super_admin' || state.role === 'commune_admin',
|
||||
isSuperAdmin: (state) => state.role === 'super_admin',
|
||||
isCitizen: (state) => state.role === 'citizen',
|
||||
},
|
||||
|
||||
actions: {
|
||||
setAuth(token: string, role: string, communeSlug?: string) {
|
||||
this.token = token
|
||||
this.role = role
|
||||
this.communeSlug = communeSlug || null
|
||||
|
||||
if (import.meta.client) {
|
||||
localStorage.setItem('sejeteralo_token', token)
|
||||
localStorage.setItem('sejeteralo_role', role)
|
||||
if (communeSlug) localStorage.setItem('sejeteralo_commune', communeSlug)
|
||||
}
|
||||
},
|
||||
|
||||
logout() {
|
||||
this.token = null
|
||||
this.role = null
|
||||
this.communeSlug = null
|
||||
|
||||
if (import.meta.client) {
|
||||
localStorage.removeItem('sejeteralo_token')
|
||||
localStorage.removeItem('sejeteralo_role')
|
||||
localStorage.removeItem('sejeteralo_commune')
|
||||
}
|
||||
},
|
||||
|
||||
restore() {
|
||||
if (import.meta.client) {
|
||||
this.token = localStorage.getItem('sejeteralo_token')
|
||||
this.role = localStorage.getItem('sejeteralo_role')
|
||||
this.communeSlug = localStorage.getItem('sejeteralo_commune')
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
48
frontend/app/stores/commune.ts
Normal file
48
frontend/app/stores/commune.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
interface Commune {
|
||||
id: number
|
||||
name: string
|
||||
slug: string
|
||||
description: string
|
||||
is_active: boolean
|
||||
}
|
||||
|
||||
interface TariffParams {
|
||||
abop: number
|
||||
abos: number
|
||||
recettes: number
|
||||
pmax: number
|
||||
vmax: number
|
||||
}
|
||||
|
||||
export const useCommuneStore = defineStore('commune', {
|
||||
state: () => ({
|
||||
communes: [] as Commune[],
|
||||
current: null as Commune | null,
|
||||
params: null as TariffParams | null,
|
||||
loading: false,
|
||||
}),
|
||||
|
||||
actions: {
|
||||
async fetchCommunes() {
|
||||
this.loading = true
|
||||
try {
|
||||
const api = useApi()
|
||||
this.communes = await api.get<Commune[]>('/communes/')
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
async fetchCommune(slug: string) {
|
||||
const api = useApi()
|
||||
this.current = await api.get<Commune>(`/communes/${slug}`)
|
||||
},
|
||||
|
||||
async fetchParams(slug: string) {
|
||||
const api = useApi()
|
||||
this.params = await api.get<TariffParams>(`/communes/${slug}/params`)
|
||||
},
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user