Sprint 4 : decisions et mandats -- workflow complet + vote integration
Backend: 7 nouveaux endpoints (advance, assign, revoke, create-vote-session), services enrichis avec creation de sessions de vote, assignation de mandataire et revocation. 35 nouveaux tests (104 total). Frontend: store mandates, page cadrage decisions, detail mandats, composants DecisionWorkflow, DecisionCadrage, DecisionCard, MandateTimeline, MandateCard. Documentation mise a jour. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -39,6 +39,20 @@ export interface DecisionCreate {
|
||||
voting_protocol_id?: string | null
|
||||
}
|
||||
|
||||
export interface DecisionUpdate {
|
||||
title?: string
|
||||
description?: string | null
|
||||
context?: string | null
|
||||
decision_type?: string
|
||||
voting_protocol_id?: string | null
|
||||
}
|
||||
|
||||
export interface DecisionStepCreate {
|
||||
step_type: string
|
||||
title?: string | null
|
||||
description?: string | null
|
||||
}
|
||||
|
||||
interface DecisionsState {
|
||||
list: Decision[]
|
||||
current: Decision | null
|
||||
@@ -59,10 +73,12 @@ export const useDecisionsStore = defineStore('decisions', {
|
||||
return (status: string) => state.list.filter(d => d.status === status)
|
||||
},
|
||||
activeDecisions: (state): Decision[] => {
|
||||
return state.list.filter(d => d.status === 'active' || d.status === 'in_progress')
|
||||
return state.list.filter(d =>
|
||||
d.status === 'qualification' || d.status === 'review' || d.status === 'voting',
|
||||
)
|
||||
},
|
||||
completedDecisions: (state): Decision[] => {
|
||||
return state.list.filter(d => d.status === 'completed' || d.status === 'closed')
|
||||
return state.list.filter(d => d.status === 'executed' || d.status === 'closed')
|
||||
},
|
||||
},
|
||||
|
||||
@@ -128,6 +144,108 @@ export const useDecisionsStore = defineStore('decisions', {
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Update an existing decision.
|
||||
*/
|
||||
async update(id: string, data: DecisionUpdate) {
|
||||
this.error = null
|
||||
|
||||
try {
|
||||
const { $api } = useApi()
|
||||
const updated = await $api<Decision>(`/decisions/${id}`, {
|
||||
method: 'PUT',
|
||||
body: data,
|
||||
})
|
||||
if (this.current?.id === id) this.current = updated
|
||||
const idx = this.list.findIndex(d => d.id === id)
|
||||
if (idx >= 0) this.list[idx] = updated
|
||||
return updated
|
||||
} catch (err: any) {
|
||||
this.error = err?.data?.detail || err?.message || 'Erreur lors de la mise a jour de la decision'
|
||||
throw err
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete a decision.
|
||||
*/
|
||||
async delete(id: string) {
|
||||
this.error = null
|
||||
|
||||
try {
|
||||
const { $api } = useApi()
|
||||
await $api(`/decisions/${id}`, { method: 'DELETE' })
|
||||
this.list = this.list.filter(d => d.id !== id)
|
||||
if (this.current?.id === id) this.current = null
|
||||
} catch (err: any) {
|
||||
this.error = err?.data?.detail || err?.message || 'Erreur lors de la suppression de la decision'
|
||||
throw err
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Advance the decision to the next step in its workflow.
|
||||
*/
|
||||
async advance(id: string) {
|
||||
this.error = null
|
||||
|
||||
try {
|
||||
const { $api } = useApi()
|
||||
const updated = await $api<Decision>(`/decisions/${id}/advance`, {
|
||||
method: 'POST',
|
||||
})
|
||||
if (this.current?.id === id) this.current = updated
|
||||
const idx = this.list.findIndex(d => d.id === id)
|
||||
if (idx >= 0) this.list[idx] = updated
|
||||
return updated
|
||||
} catch (err: any) {
|
||||
this.error = err?.data?.detail || err?.message || 'Erreur lors de l\'avancement de la decision'
|
||||
throw err
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Add a step to a decision.
|
||||
*/
|
||||
async addStep(id: string, step: DecisionStepCreate) {
|
||||
this.error = null
|
||||
|
||||
try {
|
||||
const { $api } = useApi()
|
||||
const newStep = await $api<DecisionStep>(`/decisions/${id}/steps`, {
|
||||
method: 'POST',
|
||||
body: step,
|
||||
})
|
||||
if (this.current?.id === id) {
|
||||
this.current.steps.push(newStep)
|
||||
}
|
||||
return newStep
|
||||
} catch (err: any) {
|
||||
this.error = err?.data?.detail || err?.message || 'Erreur lors de l\'ajout de l\'etape'
|
||||
throw err
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Create a vote session for a specific step.
|
||||
*/
|
||||
async createVoteSession(decisionId: string, stepId: string) {
|
||||
this.error = null
|
||||
|
||||
try {
|
||||
const { $api } = useApi()
|
||||
const result = await $api<any>(`/decisions/${decisionId}/steps/${stepId}/create-vote-session`, {
|
||||
method: 'POST',
|
||||
})
|
||||
// Refresh decision to get updated step with vote_session_id
|
||||
await this.fetchById(decisionId)
|
||||
return result
|
||||
} catch (err: any) {
|
||||
this.error = err?.data?.detail || err?.message || 'Erreur lors de la creation de la session de vote'
|
||||
throw err
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Clear the current decision.
|
||||
*/
|
||||
|
||||
279
frontend/app/stores/mandates.ts
Normal file
279
frontend/app/stores/mandates.ts
Normal file
@@ -0,0 +1,279 @@
|
||||
/**
|
||||
* Mandates store: governance mandates and their lifecycle steps.
|
||||
*
|
||||
* Maps to the backend /api/v1/mandates endpoints.
|
||||
*/
|
||||
|
||||
export interface MandateStep {
|
||||
id: string
|
||||
mandate_id: string
|
||||
step_order: number
|
||||
step_type: string
|
||||
title: string | null
|
||||
description: string | null
|
||||
status: string
|
||||
vote_session_id: string | null
|
||||
outcome: string | null
|
||||
created_at: string
|
||||
}
|
||||
|
||||
export interface Mandate {
|
||||
id: string
|
||||
title: string
|
||||
description: string | null
|
||||
mandate_type: string
|
||||
status: string
|
||||
mandatee_id: string | null
|
||||
decision_id: string | null
|
||||
starts_at: string | null
|
||||
ends_at: string | null
|
||||
created_at: string
|
||||
updated_at: string
|
||||
steps: MandateStep[]
|
||||
}
|
||||
|
||||
export interface MandateCreate {
|
||||
title: string
|
||||
description?: string | null
|
||||
mandate_type: string
|
||||
decision_id?: string | null
|
||||
starts_at?: string | null
|
||||
ends_at?: string | null
|
||||
}
|
||||
|
||||
export interface MandateUpdate {
|
||||
title?: string
|
||||
description?: string | null
|
||||
mandate_type?: string
|
||||
starts_at?: string | null
|
||||
ends_at?: string | null
|
||||
}
|
||||
|
||||
export interface MandateStepCreate {
|
||||
step_type: string
|
||||
title?: string | null
|
||||
description?: string | null
|
||||
}
|
||||
|
||||
interface MandatesState {
|
||||
list: Mandate[]
|
||||
current: Mandate | null
|
||||
loading: boolean
|
||||
error: string | null
|
||||
}
|
||||
|
||||
export const useMandatesStore = defineStore('mandates', {
|
||||
state: (): MandatesState => ({
|
||||
list: [],
|
||||
current: null,
|
||||
loading: false,
|
||||
error: null,
|
||||
}),
|
||||
|
||||
getters: {
|
||||
byStatus: (state) => {
|
||||
return (status: string) => state.list.filter(m => m.status === status)
|
||||
},
|
||||
activeMandates: (state): Mandate[] => {
|
||||
return state.list.filter(m => m.status === 'active')
|
||||
},
|
||||
completedMandates: (state): Mandate[] => {
|
||||
return state.list.filter(m => m.status === 'completed')
|
||||
},
|
||||
},
|
||||
|
||||
actions: {
|
||||
/**
|
||||
* Fetch all mandates with optional filters.
|
||||
*/
|
||||
async fetchAll(params?: { mandate_type?: string; status?: string }) {
|
||||
this.loading = true
|
||||
this.error = null
|
||||
|
||||
try {
|
||||
const { $api } = useApi()
|
||||
const query: Record<string, string> = {}
|
||||
if (params?.mandate_type) query.mandate_type = params.mandate_type
|
||||
if (params?.status) query.status = params.status
|
||||
|
||||
this.list = await $api<Mandate[]>('/mandates/', { query })
|
||||
} catch (err: any) {
|
||||
this.error = err?.data?.detail || err?.message || 'Erreur lors du chargement des mandats'
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Fetch a single mandate by ID with all its steps.
|
||||
*/
|
||||
async fetchById(id: string) {
|
||||
this.loading = true
|
||||
this.error = null
|
||||
|
||||
try {
|
||||
const { $api } = useApi()
|
||||
this.current = await $api<Mandate>(`/mandates/${id}`)
|
||||
} catch (err: any) {
|
||||
this.error = err?.data?.detail || err?.message || 'Mandat introuvable'
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Create a new mandate.
|
||||
*/
|
||||
async create(payload: MandateCreate) {
|
||||
this.loading = true
|
||||
this.error = null
|
||||
|
||||
try {
|
||||
const { $api } = useApi()
|
||||
const mandate = await $api<Mandate>('/mandates/', {
|
||||
method: 'POST',
|
||||
body: payload,
|
||||
})
|
||||
this.list.unshift(mandate)
|
||||
return mandate
|
||||
} catch (err: any) {
|
||||
this.error = err?.data?.detail || err?.message || 'Erreur lors de la creation du mandat'
|
||||
throw err
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Update an existing mandate.
|
||||
*/
|
||||
async update(id: string, data: MandateUpdate) {
|
||||
this.error = null
|
||||
|
||||
try {
|
||||
const { $api } = useApi()
|
||||
const updated = await $api<Mandate>(`/mandates/${id}`, {
|
||||
method: 'PUT',
|
||||
body: data,
|
||||
})
|
||||
if (this.current?.id === id) this.current = updated
|
||||
const idx = this.list.findIndex(m => m.id === id)
|
||||
if (idx >= 0) this.list[idx] = updated
|
||||
return updated
|
||||
} catch (err: any) {
|
||||
this.error = err?.data?.detail || err?.message || 'Erreur lors de la mise a jour du mandat'
|
||||
throw err
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete a mandate.
|
||||
*/
|
||||
async delete(id: string) {
|
||||
this.error = null
|
||||
|
||||
try {
|
||||
const { $api } = useApi()
|
||||
await $api(`/mandates/${id}`, { method: 'DELETE' })
|
||||
this.list = this.list.filter(m => m.id !== id)
|
||||
if (this.current?.id === id) this.current = null
|
||||
} catch (err: any) {
|
||||
this.error = err?.data?.detail || err?.message || 'Erreur lors de la suppression du mandat'
|
||||
throw err
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Advance the mandate to the next step in its workflow.
|
||||
*/
|
||||
async advance(id: string) {
|
||||
this.error = null
|
||||
|
||||
try {
|
||||
const { $api } = useApi()
|
||||
const updated = await $api<Mandate>(`/mandates/${id}/advance`, {
|
||||
method: 'POST',
|
||||
})
|
||||
if (this.current?.id === id) this.current = updated
|
||||
const idx = this.list.findIndex(m => m.id === id)
|
||||
if (idx >= 0) this.list[idx] = updated
|
||||
return updated
|
||||
} catch (err: any) {
|
||||
this.error = err?.data?.detail || err?.message || 'Erreur lors de l\'avancement du mandat'
|
||||
throw err
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Add a step to a mandate.
|
||||
*/
|
||||
async addStep(id: string, step: MandateStepCreate) {
|
||||
this.error = null
|
||||
|
||||
try {
|
||||
const { $api } = useApi()
|
||||
const newStep = await $api<MandateStep>(`/mandates/${id}/steps`, {
|
||||
method: 'POST',
|
||||
body: step,
|
||||
})
|
||||
if (this.current?.id === id) {
|
||||
this.current.steps.push(newStep)
|
||||
}
|
||||
return newStep
|
||||
} catch (err: any) {
|
||||
this.error = err?.data?.detail || err?.message || 'Erreur lors de l\'ajout de l\'etape'
|
||||
throw err
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Assign a mandatee to the mandate.
|
||||
*/
|
||||
async assignMandatee(id: string, mandateeId: string) {
|
||||
this.error = null
|
||||
|
||||
try {
|
||||
const { $api } = useApi()
|
||||
const updated = await $api<Mandate>(`/mandates/${id}/assign`, {
|
||||
method: 'POST',
|
||||
body: { mandatee_id: mandateeId },
|
||||
})
|
||||
if (this.current?.id === id) this.current = updated
|
||||
const idx = this.list.findIndex(m => m.id === id)
|
||||
if (idx >= 0) this.list[idx] = updated
|
||||
return updated
|
||||
} catch (err: any) {
|
||||
this.error = err?.data?.detail || err?.message || 'Erreur lors de l\'assignation du mandataire'
|
||||
throw err
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Revoke the mandate.
|
||||
*/
|
||||
async revoke(id: string) {
|
||||
this.error = null
|
||||
|
||||
try {
|
||||
const { $api } = useApi()
|
||||
const updated = await $api<Mandate>(`/mandates/${id}/revoke`, {
|
||||
method: 'POST',
|
||||
})
|
||||
if (this.current?.id === id) this.current = updated
|
||||
const idx = this.list.findIndex(m => m.id === id)
|
||||
if (idx >= 0) this.list[idx] = updated
|
||||
return updated
|
||||
} catch (err: any) {
|
||||
this.error = err?.data?.detail || err?.message || 'Erreur lors de la revocation du mandat'
|
||||
throw err
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Clear the current mandate.
|
||||
*/
|
||||
clearCurrent() {
|
||||
this.current = null
|
||||
},
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user