f56d84e76b
- origin TEXT → origin_id UUID FK duniter_identities (migration e3f4a5b6c7d8) - GET /auth/identities?q= : recherche d'identités par nom/adresse - MandateCreate.nomination_mode : auto (auto-assign auteur), collective, postpone - Wizard new.vue : champ origine = picker identité, checkbox "Démarrer maintenant" - [id].vue : modal "Assigner" = search-picker (résout UUID vs adresse SS58), affiche origin_display_name + mandatee_display_name, inputs natifs (<input>/<textarea>) - Erreurs API visibles dans l'UI (plus de catch silencieux) - test_mandate_flows.py : 17 tests intégration SQLite réels (origin, nomination, assign, lifecycle, revocation, interactions croisées) — 241 tests total OK Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1282 lines
50 KiB
Vue
1282 lines
50 KiB
Vue
<script setup lang="ts">
|
||
import type { MandateCreate } from '~/stores/mandates'
|
||
|
||
const mandates = useMandatesStore()
|
||
const { $api } = useApi()
|
||
|
||
// ── Wizard ────────────────────────────────────────────────────────────────────
|
||
type WizardStep = 'nomination' | 'info' | 'summary'
|
||
const wizardStep = ref<WizardStep>('nomination')
|
||
|
||
// ── Étape 1 : Processus de nomination ────────────────────────────────────────
|
||
type NominationCase = 'self' | 'collective'
|
||
const nominationCase = ref<NominationCase | null>(null)
|
||
|
||
// Cas 1 — Auto-désignation
|
||
type SelfMode = 'period' | 'meeting'
|
||
const selfMode = ref<SelfMode>('period')
|
||
const selfPeriodParams = reactive({ duration_days: 14, quorum_pct: 30, majority_pct: 51 })
|
||
const selfMeetingName = ref('')
|
||
const selfMeetingMajority = ref(51)
|
||
|
||
// Cas 2 — Désignation collective
|
||
type CandidacyMode = 'direct' | 'candidacy'
|
||
const candidacyMode = ref<CandidacyMode>('direct')
|
||
const selectedModalityId = ref<string | null>(null)
|
||
|
||
interface ModalityParam {
|
||
id: string
|
||
label: string
|
||
unit: string
|
||
min: number
|
||
max: number
|
||
default: number
|
||
}
|
||
interface Modality {
|
||
id: string
|
||
icon: string
|
||
title: string
|
||
desc: string
|
||
recommended: boolean
|
||
recommendedReason?: string
|
||
params: ModalityParam[]
|
||
requiresCandidacy: boolean
|
||
}
|
||
|
||
const MODALITIES: Modality[] = [
|
||
{
|
||
id: 'vote_simple',
|
||
icon: 'i-lucide-check-square',
|
||
title: 'Vote de désignation',
|
||
desc: 'Vote direct parmi les membres. Simple et rapide pour les petits groupes.',
|
||
recommended: false,
|
||
params: [
|
||
{ id: 'duration_days', label: 'Durée du vote', unit: 'jours', min: 1, max: 30, default: 7 },
|
||
{ id: 'quorum_pct', label: 'Quorum', unit: '%', min: 10, max: 100, default: 30 },
|
||
{ id: 'majority_pct', label: 'Seuil de majorité', unit: '%', min: 51, max: 100, default: 51 },
|
||
],
|
||
requiresCandidacy: false,
|
||
},
|
||
{
|
||
id: 'vote_wot',
|
||
icon: 'i-lucide-network',
|
||
title: 'Vote WoT',
|
||
desc: 'Vote pondéré par la Toile de Confiance. Le poids de chaque voix dépend de la distance certificationnelle.',
|
||
recommended: true,
|
||
recommendedReason: 'Recommandé pour les mandats impliquant la communauté G1 au-delà de 10 personnes.',
|
||
params: [
|
||
{ id: 'duration_days', label: 'Durée du vote', unit: 'jours', min: 1, max: 60, default: 14 },
|
||
{ id: 'quorum_pct', label: 'Quorum WoT', unit: '% de la WoT', min: 5, max: 100, default: 20 },
|
||
{ id: 'majority_pct', label: 'Seuil', unit: '%', min: 51, max: 100, default: 51 },
|
||
],
|
||
requiresCandidacy: false,
|
||
},
|
||
{
|
||
id: 'vote_smith',
|
||
icon: 'i-lucide-hammer',
|
||
title: 'Vote Smith',
|
||
desc: 'Réservé aux Forgerons (critère de Smith). Pour les décisions techniques du réseau.',
|
||
recommended: false,
|
||
params: [
|
||
{ id: 'duration_days', label: 'Durée du vote', unit: 'jours', min: 1, max: 30, default: 14 },
|
||
{ id: 'smith_min', label: 'Forgerons minimum', unit: 'forgerons', min: 1, max: 20, default: 3 },
|
||
],
|
||
requiresCandidacy: false,
|
||
},
|
||
{
|
||
id: 'candidacy_vote_majoritaire',
|
||
icon: 'i-lucide-user-check',
|
||
title: 'Candidature + Vote majoritaire',
|
||
desc: 'Appel à candidature ouvert, puis vote binaire (Pour/Contre) pour chaque candidat.',
|
||
recommended: false,
|
||
params: [
|
||
{ id: 'candidacy_days', label: 'Fenêtre de candidature', unit: 'jours', min: 1, max: 30, default: 7 },
|
||
{ id: 'duration_days', label: 'Durée du vote', unit: 'jours', min: 1, max: 30, default: 7 },
|
||
{ id: 'quorum_pct', label: 'Quorum', unit: '%', min: 10, max: 100, default: 30 },
|
||
{ id: 'majority_pct', label: 'Seuil', unit: '%', min: 51, max: 100, default: 51 },
|
||
],
|
||
requiresCandidacy: true,
|
||
},
|
||
{
|
||
id: 'candidacy_vote_preferentiel',
|
||
icon: 'i-lucide-list-ordered',
|
||
title: 'Candidature + Vote préférentiel',
|
||
desc: 'Candidature ouverte, puis classement des candidats par les votants (Condorcet ou Borda).',
|
||
recommended: true,
|
||
recommendedReason: 'Recommandé dès 3 candidats pour éviter les effets de vote utile.',
|
||
params: [
|
||
{ id: 'candidacy_days', label: 'Fenêtre de candidature', unit: 'jours', min: 1, max: 30, default: 7 },
|
||
{ id: 'duration_days', label: 'Durée du vote', unit: 'jours', min: 1, max: 60, default: 14 },
|
||
],
|
||
requiresCandidacy: true,
|
||
},
|
||
{
|
||
id: 'candidacy_vote_wot',
|
||
icon: 'i-lucide-network',
|
||
title: 'Candidature + Vote WoT',
|
||
desc: 'Candidature ouverte, puis vote pondéré par la Toile de Confiance.',
|
||
recommended: false,
|
||
params: [
|
||
{ id: 'candidacy_days', label: 'Fenêtre de candidature', unit: 'jours', min: 1, max: 30, default: 7 },
|
||
{ id: 'duration_days', label: 'Durée du vote', unit: 'jours', min: 1, max: 60, default: 14 },
|
||
{ id: 'majority_pct', label: 'Seuil WoT', unit: '%', min: 51, max: 100, default: 51 },
|
||
],
|
||
requiresCandidacy: true,
|
||
},
|
||
]
|
||
|
||
const visibleModalities = computed(() =>
|
||
MODALITIES.filter(m => m.requiresCandidacy === (candidacyMode.value === 'candidacy'))
|
||
)
|
||
|
||
const paramValues = reactive<Record<string, Record<string, number>>>({})
|
||
function getParam(modalityId: string, paramId: string): number {
|
||
if (!paramValues[modalityId]) {
|
||
const m = MODALITIES.find(x => x.id === modalityId)
|
||
if (m) paramValues[modalityId] = Object.fromEntries(m.params.map(p => [p.id, p.default]))
|
||
}
|
||
return paramValues[modalityId]?.[paramId] ?? 0
|
||
}
|
||
function setParam(modalityId: string, paramId: string, value: number) {
|
||
if (!paramValues[modalityId]) getParam(modalityId, paramId)
|
||
paramValues[modalityId]![paramId] = value
|
||
}
|
||
|
||
const canGoToInfo = computed(() => {
|
||
if (!nominationCase.value) return false
|
||
if (nominationCase.value === 'collective') return selectedModalityId.value !== null
|
||
return true
|
||
})
|
||
|
||
// ── Étape 2 : Infos du mandat ────────────────────────────────────────────────
|
||
type MandateType = 'statutory' | 'functional'
|
||
const mandateType = ref<MandateType>('functional')
|
||
const title = ref('')
|
||
const description = ref('')
|
||
|
||
// Origin : identité Duniter (non texte libre)
|
||
interface IdentityResult { id: string; address: string; display_name: string | null }
|
||
const originQuery = ref('')
|
||
const originResults = ref<IdentityResult[]>([])
|
||
const originId = ref<string | null>(null)
|
||
const originSearching = ref(false)
|
||
let originTimer: ReturnType<typeof setTimeout> | null = null
|
||
|
||
async function searchOrigin(q: string) {
|
||
if (q.length < 2) { originResults.value = []; return }
|
||
originSearching.value = true
|
||
try {
|
||
originResults.value = await $api<IdentityResult[]>('/auth/identities', { query: { q } })
|
||
} catch { originResults.value = [] } finally { originSearching.value = false }
|
||
}
|
||
function onOriginInput(q: string) {
|
||
originQuery.value = q
|
||
originId.value = null
|
||
if (originTimer) clearTimeout(originTimer)
|
||
originTimer = setTimeout(() => searchOrigin(q), 300)
|
||
}
|
||
function selectOrigin(r: IdentityResult) {
|
||
originId.value = r.id
|
||
originQuery.value = r.display_name || r.address
|
||
originResults.value = []
|
||
}
|
||
|
||
type DurationMode = 'relative' | 'dates'
|
||
const durationMode = ref<DurationMode>('relative')
|
||
const durationValue = ref(3)
|
||
const durationUnit = ref<'days' | 'weeks' | 'months'>('months')
|
||
const startsAt = ref('')
|
||
const endsAt = ref('')
|
||
|
||
function computedDates(): { starts_at: string | null; ends_at: string | null } {
|
||
if (durationMode.value === 'dates') {
|
||
return { starts_at: startsAt.value || null, ends_at: endsAt.value || null }
|
||
}
|
||
const now = new Date()
|
||
const end = new Date(now)
|
||
if (durationUnit.value === 'days') end.setDate(end.getDate() + durationValue.value)
|
||
else if (durationUnit.value === 'weeks') end.setDate(end.getDate() + durationValue.value * 7)
|
||
else end.setMonth(end.getMonth() + durationValue.value)
|
||
return { starts_at: now.toISOString(), ends_at: end.toISOString() }
|
||
}
|
||
|
||
const durationLabel = computed(() => {
|
||
if (durationMode.value === 'dates') {
|
||
if (!startsAt.value && !endsAt.value) return 'Dates non définies'
|
||
return `${startsAt.value ? new Date(startsAt.value).toLocaleDateString('fr-FR') : '?'} → ${endsAt.value ? new Date(endsAt.value).toLocaleDateString('fr-FR') : '?'}`
|
||
}
|
||
const u = durationUnit.value === 'days' ? 'jour' : durationUnit.value === 'weeks' ? 'semaine' : 'mois'
|
||
return `${durationValue.value} ${u}${durationValue.value > 1 && durationUnit.value !== 'months' ? 's' : ''}`
|
||
})
|
||
|
||
const canGoToSummary = computed(() =>
|
||
title.value.trim().length > 0 && (
|
||
durationMode.value === 'relative' || (startsAt.value !== '' && endsAt.value !== '')
|
||
)
|
||
)
|
||
|
||
const MANDATE_TYPE_OPTIONS = [
|
||
{
|
||
value: 'statutory',
|
||
label: 'Statutaire',
|
||
desc: 'Lié aux statuts de l\'organisation (Président, Trésorier, Secrétaire…)',
|
||
icon: 'i-lucide-landmark',
|
||
},
|
||
{
|
||
value: 'functional',
|
||
label: 'Fonctionnel',
|
||
desc: 'Mission ou rôle opérationnel lié au fonctionnement de l\'organisation',
|
||
icon: 'i-lucide-settings-2',
|
||
},
|
||
]
|
||
|
||
// ── Génération des étapes ─────────────────────────────────────────────────────
|
||
interface StepDef { step_type: string; title: string; description: string }
|
||
|
||
const stepsToCreate = computed<StepDef[]>(() => {
|
||
const steps: StepDef[] = []
|
||
steps.push({
|
||
step_type: 'formulation',
|
||
title: 'Rédaction du mandat',
|
||
description: 'Définition du périmètre, des objectifs et des attendus.',
|
||
})
|
||
|
||
if (nominationCase.value === 'self') {
|
||
if (selfMode.value === 'period') {
|
||
steps.push({
|
||
step_type: 'vote',
|
||
title: 'Ratification sur période',
|
||
description: `Vote de ratification ouvert à tous les adhérents — ${selfPeriodParams.duration_days}j, quorum ${selfPeriodParams.quorum_pct}%, seuil ${selfPeriodParams.majority_pct}%.`,
|
||
})
|
||
} else {
|
||
const mn = selfMeetingName.value.trim() || 'réunion'
|
||
steps.push({
|
||
step_type: 'vote',
|
||
title: `Ratification en réunion`,
|
||
description: `Ratification ponctuelle lors de "${mn}" — présents votants, seuil ${selfMeetingMajority.value}%.`,
|
||
})
|
||
}
|
||
steps.push({ step_type: 'assignment', title: 'Prise de mandat', description: 'Entrée en mandat après ratification.' })
|
||
} else if (nominationCase.value === 'collective' && selectedModalityId.value) {
|
||
const m = MODALITIES.find(x => x.id === selectedModalityId.value)
|
||
if (m?.requiresCandidacy) {
|
||
const cd = getParam(m.id, 'candidacy_days')
|
||
steps.push({ step_type: 'candidacy', title: 'Appel à candidature', description: `Fenêtre de candidature : ${cd} jour${cd > 1 ? 's' : ''}.` })
|
||
}
|
||
if (m) {
|
||
const pv = paramValues[m.id] ?? {}
|
||
const paramStr = Object.entries(pv)
|
||
.filter(([k]) => k !== 'candidacy_days')
|
||
.map(([k, v]) => { const p = m.params.find(x => x.id === k); return p ? `${p.label} : ${v} ${p.unit}` : '' })
|
||
.filter(Boolean).join(', ')
|
||
steps.push({ step_type: 'vote', title: `Élection — ${m.title}`, description: paramStr })
|
||
}
|
||
steps.push({ step_type: 'assignment', title: 'Désignation du mandataire', description: 'Le candidat élu entre en mandat.' })
|
||
}
|
||
|
||
steps.push({ step_type: 'reporting', title: 'Compte-rendu', description: 'Rapport d\'activité en fin de mandat.' })
|
||
steps.push({ step_type: 'completion', title: 'Clôture', description: 'Validation de la fin du mandat.' })
|
||
return steps
|
||
})
|
||
|
||
// ── Récap helpers ─────────────────────────────────────────────────────────────
|
||
const nominationSummary = computed(() => {
|
||
if (nominationCase.value === 'self') {
|
||
if (selfMode.value === 'period') return `Auto-désignation — ratification sur ${selfPeriodParams.duration_days}j (quorum ${selfPeriodParams.quorum_pct}%)`
|
||
const mn = selfMeetingName.value.trim() || 'réunion'
|
||
return `Auto-désignation — ratification ponctuelle en "${mn}"`
|
||
}
|
||
const m = MODALITIES.find(x => x.id === selectedModalityId.value)
|
||
return `Désignation collective — ${m?.title ?? ''}`
|
||
})
|
||
|
||
// ── Création ──────────────────────────────────────────────────────────────────
|
||
const startImmediately = ref(true)
|
||
const submitting = ref(false)
|
||
const submitError = ref<string | null>(null)
|
||
|
||
const auth = useAuthStore()
|
||
|
||
async function createMandate() {
|
||
submitting.value = true
|
||
submitError.value = null
|
||
try {
|
||
const dates = computedDates()
|
||
const nominationMode = nominationCase.value === 'self' ? 'auto' : 'collective'
|
||
|
||
const mandate = await mandates.create({
|
||
title: title.value.trim(),
|
||
origin_id: originId.value,
|
||
description: description.value.trim() || null,
|
||
mandate_type: mandateType.value,
|
||
nomination_mode: nominationMode,
|
||
starts_at: dates.starts_at,
|
||
ends_at: dates.ends_at,
|
||
})
|
||
if (!mandate) throw new Error('Erreur création mandat')
|
||
|
||
// Créer les étapes
|
||
for (let i = 0; i < stepsToCreate.value.length; i++) {
|
||
const s = stepsToCreate.value[i]!
|
||
await $api(`/mandates/${mandate.id}/steps`, {
|
||
method: 'POST',
|
||
body: { step_order: i, step_type: s.step_type, title: s.title, description: s.description },
|
||
})
|
||
}
|
||
|
||
// Démarrer le processus si demandé
|
||
if (startImmediately.value) {
|
||
await $api(`/mandates/${mandate.id}/advance`, { method: 'POST' })
|
||
}
|
||
|
||
navigateTo(`/mandates/${mandate.id}`)
|
||
} catch (e: any) {
|
||
submitError.value = e?.data?.detail ?? e?.message ?? 'Erreur lors de la création'
|
||
} finally {
|
||
submitting.value = false
|
||
}
|
||
}
|
||
|
||
// ── Navigation ────────────────────────────────────────────────────────────────
|
||
function goBack() {
|
||
if (wizardStep.value === 'summary') wizardStep.value = 'info'
|
||
else if (wizardStep.value === 'info') wizardStep.value = 'nomination'
|
||
}
|
||
|
||
function selectNomination(c: NominationCase) {
|
||
nominationCase.value = c
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<div class="mwiz">
|
||
|
||
<!-- Nav -->
|
||
<div class="mwiz__nav">
|
||
<button v-if="wizardStep !== 'nomination'" class="mwiz__back" @click="goBack">
|
||
<UIcon name="i-lucide-arrow-left" class="text-sm" />
|
||
<span>Retour</span>
|
||
</button>
|
||
<NuxtLink v-else to="/mandates" class="mwiz__back">
|
||
<UIcon name="i-lucide-arrow-left" class="text-sm" />
|
||
<span>Mandats</span>
|
||
</NuxtLink>
|
||
|
||
<div class="mwiz__progress">
|
||
<span class="mwiz__pill" :class="{ 'mwiz__pill--active': wizardStep === 'nomination', 'mwiz__pill--done': wizardStep !== 'nomination' }">
|
||
1 Nomination
|
||
</span>
|
||
<span class="mwiz__sep">›</span>
|
||
<span class="mwiz__pill" :class="{ 'mwiz__pill--active': wizardStep === 'info', 'mwiz__pill--done': wizardStep === 'summary' }">
|
||
2 Mandat
|
||
</span>
|
||
<span class="mwiz__sep">›</span>
|
||
<span class="mwiz__pill" :class="{ 'mwiz__pill--active': wizardStep === 'summary' }">
|
||
3 Récap
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
<Transition name="slide-fade" mode="out-in">
|
||
|
||
<!-- ─── ÉTAPE 1 : Nomination ────────────────────────────────────────── -->
|
||
<div v-if="wizardStep === 'nomination'" key="nomination" class="mwiz__step">
|
||
<div class="mwiz__header">
|
||
<h1 class="mwiz__title">Processus de nomination</h1>
|
||
<p class="mwiz__subtitle">Choisissez comment le mandataire sera désigné</p>
|
||
</div>
|
||
|
||
<!-- Deux grandes cartes -->
|
||
<div class="mwiz__nom-grid">
|
||
|
||
<!-- Cas 1 : Auto-désignation -->
|
||
<div
|
||
class="mwiz__nom-card"
|
||
:class="{ 'mwiz__nom-card--active': nominationCase === 'self' }"
|
||
@click="selectNomination('self')"
|
||
>
|
||
<div class="mwiz__nom-head">
|
||
<div class="mwiz__nom-icon">
|
||
<UIcon name="i-lucide-user-check" class="text-2xl" />
|
||
</div>
|
||
<div class="mwiz__nom-body">
|
||
<p class="mwiz__nom-title">Auto-désignation</p>
|
||
<p class="mwiz__nom-desc">
|
||
Une personne demande un mandat pour elle-même afin de mener sa mission de façon légitime.
|
||
Un vote de ratification valide la prise de mandat.
|
||
</p>
|
||
</div>
|
||
<div class="mwiz__nom-check">
|
||
<UIcon v-if="nominationCase === 'self'" name="i-lucide-circle-check" class="text-lg" />
|
||
<UIcon v-else name="i-lucide-circle" class="text-lg" />
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Sous-options ratification -->
|
||
<Transition name="slide-down">
|
||
<div v-if="nominationCase === 'self'" class="mwiz__ratif" @click.stop>
|
||
|
||
<div class="mwiz__ratif-toggle">
|
||
<button
|
||
class="mwiz__ratif-btn"
|
||
:class="{ 'mwiz__ratif-btn--active': selfMode === 'period' }"
|
||
@click="selfMode = 'period'"
|
||
>
|
||
<UIcon name="i-lucide-calendar-range" class="text-sm" />
|
||
Sur période
|
||
</button>
|
||
<button
|
||
class="mwiz__ratif-btn"
|
||
:class="{ 'mwiz__ratif-btn--active': selfMode === 'meeting' }"
|
||
@click="selfMode = 'meeting'"
|
||
>
|
||
<UIcon name="i-lucide-users" class="text-sm" />
|
||
Ponctuelle (réunion)
|
||
</button>
|
||
</div>
|
||
|
||
<!-- Params : sur période -->
|
||
<Transition name="slide-down">
|
||
<div v-if="selfMode === 'period'" class="mwiz__param-row">
|
||
<div class="mwiz__param-field">
|
||
<label class="mwiz__label-sm">Durée du vote</label>
|
||
<div class="mwiz__param-input-row">
|
||
<input v-model.number="selfPeriodParams.duration_days" type="number" class="mwiz__param-input" min="1" max="60" />
|
||
<span class="mwiz__param-unit">jours</span>
|
||
</div>
|
||
</div>
|
||
<div class="mwiz__param-field">
|
||
<label class="mwiz__label-sm">Quorum</label>
|
||
<div class="mwiz__param-input-row">
|
||
<input v-model.number="selfPeriodParams.quorum_pct" type="number" class="mwiz__param-input" min="10" max="100" />
|
||
<span class="mwiz__param-unit">%</span>
|
||
</div>
|
||
</div>
|
||
<div class="mwiz__param-field">
|
||
<label class="mwiz__label-sm">Seuil de majorité</label>
|
||
<div class="mwiz__param-input-row">
|
||
<input v-model.number="selfPeriodParams.majority_pct" type="number" class="mwiz__param-input" min="51" max="100" />
|
||
<span class="mwiz__param-unit">%</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</Transition>
|
||
|
||
<!-- Params : ponctuelle réunion -->
|
||
<Transition name="slide-down">
|
||
<div v-if="selfMode === 'meeting'" class="mwiz__meeting-params">
|
||
<div class="mwiz__param-field" style="flex: 1; min-width: 200px;">
|
||
<label class="mwiz__label-sm">Nom de la réunion</label>
|
||
<input
|
||
v-model="selfMeetingName"
|
||
type="text"
|
||
class="mwiz__input"
|
||
lang="fr"
|
||
spellcheck="true"
|
||
placeholder="ex : Visio du lundi, AG 2025…"
|
||
/>
|
||
</div>
|
||
<div class="mwiz__param-field">
|
||
<label class="mwiz__label-sm">Seuil de majorité</label>
|
||
<div class="mwiz__param-input-row">
|
||
<input v-model.number="selfMeetingMajority" type="number" class="mwiz__param-input" min="51" max="100" />
|
||
<span class="mwiz__param-unit">% des présents</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</Transition>
|
||
|
||
<p class="mwiz__ratif-note">
|
||
<UIcon name="i-lucide-info" class="text-xs" />
|
||
<span v-if="selfMode === 'period'">Vote ouvert à tous les adhérents de l'organisation.</span>
|
||
<span v-else>Vote restreint aux personnes présentes lors de la réunion.</span>
|
||
</p>
|
||
</div>
|
||
</Transition>
|
||
</div>
|
||
|
||
<!-- Cas 2 : Désignation collective -->
|
||
<div
|
||
class="mwiz__nom-card"
|
||
:class="{ 'mwiz__nom-card--active': nominationCase === 'collective' }"
|
||
@click="selectNomination('collective')"
|
||
>
|
||
<div class="mwiz__nom-head">
|
||
<div class="mwiz__nom-icon">
|
||
<UIcon name="i-lucide-users-round" class="text-2xl" />
|
||
</div>
|
||
<div class="mwiz__nom-body">
|
||
<p class="mwiz__nom-title">Désignation par le collectif</p>
|
||
<p class="mwiz__nom-desc">
|
||
L'organisation identifie un besoin ou une mission, et procède à une nomination collective.
|
||
Avec ou sans appel à candidature.
|
||
</p>
|
||
</div>
|
||
<div class="mwiz__nom-check">
|
||
<UIcon v-if="nominationCase === 'collective'" name="i-lucide-circle-check" class="text-lg" />
|
||
<UIcon v-else name="i-lucide-circle" class="text-lg" />
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Boîte à outils contextuelle -->
|
||
<Transition name="slide-down">
|
||
<div v-if="nominationCase === 'collective'" class="mwiz__toolbox" @click.stop>
|
||
|
||
<p class="mwiz__toolbox-title">
|
||
<UIcon name="i-lucide-wrench" class="text-sm" />
|
||
Boîte à outils — Modalités d'élection
|
||
</p>
|
||
|
||
<!-- Toggle candidature -->
|
||
<div class="mwiz__mode-toggle">
|
||
<button
|
||
class="mwiz__mode-btn"
|
||
:class="{ 'mwiz__mode-btn--active': candidacyMode === 'direct' }"
|
||
@click="candidacyMode = 'direct'; selectedModalityId = null"
|
||
>
|
||
<UIcon name="i-lucide-vote" class="text-sm" />
|
||
Sans candidature
|
||
</button>
|
||
<button
|
||
class="mwiz__mode-btn"
|
||
:class="{ 'mwiz__mode-btn--active': candidacyMode === 'candidacy' }"
|
||
@click="candidacyMode = 'candidacy'; selectedModalityId = null"
|
||
>
|
||
<UIcon name="i-lucide-user-plus" class="text-sm" />
|
||
Avec candidature
|
||
</button>
|
||
</div>
|
||
|
||
<!-- Modalités -->
|
||
<div class="mwiz__modalities">
|
||
<div
|
||
v-for="m in visibleModalities"
|
||
:key="m.id"
|
||
class="mwiz__modality"
|
||
:class="{
|
||
'mwiz__modality--selected': selectedModalityId === m.id,
|
||
'mwiz__modality--recommended': m.recommended,
|
||
}"
|
||
@click.stop="selectedModalityId = m.id"
|
||
>
|
||
<div class="mwiz__modality-head">
|
||
<div class="mwiz__modality-icon">
|
||
<UIcon :name="m.icon" class="text-lg" />
|
||
</div>
|
||
<div class="mwiz__modality-info">
|
||
<p class="mwiz__modality-title">{{ m.title }}</p>
|
||
<p class="mwiz__modality-desc">{{ m.desc }}</p>
|
||
</div>
|
||
<div class="mwiz__modality-check">
|
||
<UIcon v-if="selectedModalityId === m.id" name="i-lucide-circle-check" class="text-lg" />
|
||
<UIcon v-else name="i-lucide-circle" class="text-lg" />
|
||
</div>
|
||
</div>
|
||
|
||
<span v-if="m.recommended" class="mwiz__modality-badge">
|
||
<UIcon name="i-lucide-sparkles" class="text-xs" />
|
||
Recommandé
|
||
</span>
|
||
<p v-if="m.recommended && m.recommendedReason" class="mwiz__modality-reason">{{ m.recommendedReason }}</p>
|
||
|
||
<Transition name="slide-down">
|
||
<div v-if="selectedModalityId === m.id" class="mwiz__modality-params">
|
||
<div v-for="p in m.params" :key="p.id" class="mwiz__param-field">
|
||
<label class="mwiz__label-sm">{{ p.label }}</label>
|
||
<div class="mwiz__param-input-row">
|
||
<input
|
||
:value="getParam(m.id, p.id)"
|
||
type="number"
|
||
class="mwiz__param-input"
|
||
:min="p.min" :max="p.max"
|
||
@input="setParam(m.id, p.id, +($event.target as HTMLInputElement).value)"
|
||
/>
|
||
<span class="mwiz__param-unit">{{ p.unit }}</span>
|
||
<input
|
||
:value="getParam(m.id, p.id)"
|
||
type="range"
|
||
class="mwiz__param-slider"
|
||
:min="p.min" :max="p.max"
|
||
@input="setParam(m.id, p.id, +($event.target as HTMLInputElement).value)"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</Transition>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
</Transition>
|
||
</div>
|
||
</div>
|
||
|
||
<button class="mwiz__submit" :disabled="!canGoToInfo" @click="wizardStep = 'info'">
|
||
Continuer — Infos du mandat
|
||
<UIcon name="i-lucide-arrow-right" class="text-sm" />
|
||
</button>
|
||
</div>
|
||
|
||
<!-- ─── ÉTAPE 2 : Infos du mandat ──────────────────────────────────── -->
|
||
<div v-else-if="wizardStep === 'info'" key="info" class="mwiz__step">
|
||
<div class="mwiz__header">
|
||
<h1 class="mwiz__title">Infos du mandat</h1>
|
||
<p class="mwiz__subtitle">Définissez le périmètre et la durée du mandat</p>
|
||
</div>
|
||
|
||
<div class="mwiz__form">
|
||
|
||
<!-- Type de mandat -->
|
||
<div class="mwiz__section">
|
||
<p class="mwiz__label">Type de mandat</p>
|
||
<div class="mwiz__type-grid">
|
||
<button
|
||
v-for="opt in MANDATE_TYPE_OPTIONS"
|
||
:key="opt.value"
|
||
class="mwiz__type-card"
|
||
:class="{ 'mwiz__type-card--active': mandateType === opt.value }"
|
||
@click="mandateType = opt.value as MandateType"
|
||
>
|
||
<UIcon :name="opt.icon" class="text-xl" />
|
||
<div>
|
||
<p class="mwiz__type-card-label">{{ opt.label }}</p>
|
||
<p class="mwiz__type-card-desc">{{ opt.desc }}</p>
|
||
</div>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Nom -->
|
||
<div class="mwiz__section">
|
||
<label class="mwiz__label">Nom du mandat <span class="mwiz__req">*</span></label>
|
||
<input
|
||
v-model="title"
|
||
type="text"
|
||
class="mwiz__input"
|
||
lang="fr"
|
||
spellcheck="true"
|
||
placeholder="ex : Responsable communication, Délégué technique G1…"
|
||
/>
|
||
</div>
|
||
|
||
<!-- Origine (personne) -->
|
||
<div class="mwiz__section">
|
||
<label class="mwiz__label">Proposé par <span class="mwiz__optional">(optionnel)</span></label>
|
||
<div class="relative">
|
||
<input
|
||
:value="originQuery"
|
||
type="text"
|
||
class="mwiz__input"
|
||
placeholder="Rechercher un membre de la communauté…"
|
||
@input="onOriginInput(($event.target as HTMLInputElement).value)"
|
||
/>
|
||
<div
|
||
v-if="originResults.length"
|
||
class="absolute z-10 mt-1 w-full bg-white dark:bg-gray-900 rounded-xl border border-gray-200 dark:border-gray-700 shadow-lg overflow-hidden"
|
||
>
|
||
<button
|
||
v-for="r in originResults"
|
||
:key="r.id"
|
||
class="w-full flex items-center gap-3 px-4 py-2 text-left text-sm hover:bg-gray-50 dark:hover:bg-gray-800"
|
||
@click="selectOrigin(r)"
|
||
>
|
||
<UIcon name="i-lucide-user" class="text-gray-400 shrink-0" />
|
||
<div>
|
||
<p class="font-medium text-gray-900 dark:text-white">{{ r.display_name || r.address }}</p>
|
||
<p class="text-xs text-gray-500 font-mono">{{ r.address.slice(0, 20) }}…</p>
|
||
</div>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<p v-if="originId" class="mwiz__hint text-green-600">
|
||
<UIcon name="i-lucide-check-circle" class="text-xs" /> {{ originQuery }} sélectionné
|
||
</p>
|
||
</div>
|
||
|
||
<!-- Description -->
|
||
<div class="mwiz__section">
|
||
<label class="mwiz__label">Description <span class="mwiz__optional">(optionnel)</span></label>
|
||
<textarea
|
||
v-model="description"
|
||
class="mwiz__textarea"
|
||
rows="3"
|
||
lang="fr"
|
||
spellcheck="true"
|
||
placeholder="Périmètre, responsabilités, attendus du mandataire…"
|
||
/>
|
||
</div>
|
||
|
||
<!-- Durée -->
|
||
<div class="mwiz__section">
|
||
<p class="mwiz__label">Durée du mandat</p>
|
||
<div class="mwiz__dur-toggle">
|
||
<button class="mwiz__dur-btn" :class="{ 'mwiz__dur-btn--active': durationMode === 'relative' }" @click="durationMode = 'relative'">
|
||
<UIcon name="i-lucide-clock" class="text-sm" /> Durée
|
||
</button>
|
||
<button class="mwiz__dur-btn" :class="{ 'mwiz__dur-btn--active': durationMode === 'dates' }" @click="durationMode = 'dates'">
|
||
<UIcon name="i-lucide-calendar" class="text-sm" /> Dates fixes
|
||
</button>
|
||
</div>
|
||
|
||
<div v-if="durationMode === 'relative'" class="mwiz__dur-relative">
|
||
<input v-model.number="durationValue" type="number" class="mwiz__dur-number" min="1" max="999" />
|
||
<div class="mwiz__type-grid mwiz__type-grid--sm">
|
||
<button class="mwiz__type-btn" :class="{ 'mwiz__type-btn--active': durationUnit === 'days' }" @click="durationUnit = 'days'">Jours</button>
|
||
<button class="mwiz__type-btn" :class="{ 'mwiz__type-btn--active': durationUnit === 'weeks' }" @click="durationUnit = 'weeks'">Semaines</button>
|
||
<button class="mwiz__type-btn" :class="{ 'mwiz__type-btn--active': durationUnit === 'months' }" @click="durationUnit = 'months'">Mois</button>
|
||
</div>
|
||
</div>
|
||
<div v-else class="mwiz__dur-dates">
|
||
<div class="mwiz__dur-date-field">
|
||
<label class="mwiz__label-sm">Début</label>
|
||
<input v-model="startsAt" type="date" class="mwiz__input" />
|
||
</div>
|
||
<UIcon name="i-lucide-arrow-right" class="text-sm mwiz__dur-arrow" />
|
||
<div class="mwiz__dur-date-field">
|
||
<label class="mwiz__label-sm">Fin</label>
|
||
<input v-model="endsAt" type="date" class="mwiz__input" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<button class="mwiz__submit" :disabled="!canGoToSummary" @click="wizardStep = 'summary'">
|
||
Récapitulatif
|
||
<UIcon name="i-lucide-arrow-right" class="text-sm" />
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ─── ÉTAPE 3 : Récapitulatif ───────────────────────────────────── -->
|
||
<div v-else-if="wizardStep === 'summary'" key="summary" class="mwiz__step">
|
||
<div class="mwiz__header">
|
||
<h1 class="mwiz__title">Récapitulatif</h1>
|
||
<p class="mwiz__subtitle">Vérifiez avant de créer le mandat</p>
|
||
</div>
|
||
|
||
<div class="mwiz__recap">
|
||
|
||
<!-- Nomination -->
|
||
<div class="mwiz__recap-block">
|
||
<p class="mwiz__recap-label">Nomination</p>
|
||
<p class="mwiz__recap-value">{{ nominationSummary }}</p>
|
||
</div>
|
||
|
||
<!-- Mandat -->
|
||
<div class="mwiz__recap-block">
|
||
<p class="mwiz__recap-label">Mandat</p>
|
||
<p class="mwiz__recap-title">{{ title }}</p>
|
||
<p class="mwiz__recap-meta">
|
||
<UIcon :name="MANDATE_TYPE_OPTIONS.find(o => o.value === mandateType)?.icon ?? 'i-lucide-circle'" class="text-xs" />
|
||
{{ MANDATE_TYPE_OPTIONS.find(o => o.value === mandateType)?.label }}
|
||
</p>
|
||
<p v-if="originId" class="mwiz__recap-meta">Proposé par : {{ originQuery }}</p>
|
||
<p v-if="description" class="mwiz__recap-meta">{{ description }}</p>
|
||
<p class="mwiz__recap-meta">
|
||
<UIcon name="i-lucide-clock" class="text-xs" />
|
||
{{ durationLabel }}
|
||
</p>
|
||
</div>
|
||
|
||
<!-- Étapes générées -->
|
||
<div class="mwiz__recap-block">
|
||
<p class="mwiz__recap-label">Étapes générées ({{ stepsToCreate.length }})</p>
|
||
<ol class="mwiz__steps-list">
|
||
<li v-for="(s, i) in stepsToCreate" :key="i" class="mwiz__steps-item">
|
||
<span class="mwiz__steps-num">{{ i + 1 }}</span>
|
||
<div>
|
||
<p class="mwiz__steps-title">{{ s.title }}</p>
|
||
<p class="mwiz__steps-desc">{{ s.description }}</p>
|
||
</div>
|
||
</li>
|
||
</ol>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Option démarrage -->
|
||
<div class="mwiz__start-option">
|
||
<label class="mwiz__start-label">
|
||
<input v-model="startImmediately" type="checkbox" class="mwiz__checkbox" />
|
||
<span>Démarrer le processus de nomination immédiatement</span>
|
||
</label>
|
||
<p class="mwiz__start-hint">
|
||
<template v-if="startImmediately">Le mandat passera en phase de nomination dès la création.</template>
|
||
<template v-else>Le mandat restera en brouillon — à démarrer manuellement depuis la fiche.</template>
|
||
</p>
|
||
</div>
|
||
|
||
<p v-if="submitError" class="mwiz__error">{{ submitError }}</p>
|
||
|
||
<div class="mwiz__recap-actions">
|
||
<button class="mwiz__back-btn" @click="goBack">
|
||
<UIcon name="i-lucide-arrow-left" class="text-sm" /> Modifier
|
||
</button>
|
||
<button class="mwiz__create-btn" :disabled="submitting" @click="createMandate">
|
||
<UIcon v-if="submitting" name="i-lucide-loader-2" class="animate-spin text-sm" />
|
||
<UIcon v-else name="i-lucide-check" class="text-sm" />
|
||
{{ submitting ? 'Création…' : 'Créer le mandat' }}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
</Transition>
|
||
</div>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.mwiz {
|
||
max-width: 760px;
|
||
margin: 0 auto;
|
||
padding: 1.5rem 1rem 4rem;
|
||
}
|
||
|
||
/* Nav */
|
||
.mwiz__nav {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 1rem;
|
||
margin-bottom: 2rem;
|
||
flex-wrap: wrap;
|
||
}
|
||
.mwiz__back {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 0.375rem;
|
||
font-size: 0.875rem;
|
||
color: var(--mood-muted);
|
||
cursor: pointer;
|
||
transition: color 0.1s;
|
||
}
|
||
.mwiz__back:hover { color: var(--mood-text); }
|
||
.mwiz__progress {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.375rem;
|
||
margin-left: auto;
|
||
flex-wrap: wrap;
|
||
}
|
||
.mwiz__sep { color: var(--mood-muted); font-size: 0.75rem; }
|
||
.mwiz__pill {
|
||
font-size: 0.75rem;
|
||
font-weight: 600;
|
||
padding: 0.25rem 0.625rem;
|
||
border-radius: 20px;
|
||
color: var(--mood-muted);
|
||
background: var(--mood-surface);
|
||
}
|
||
.mwiz__pill--active { background: var(--mood-accent); color: var(--mood-accent-text); }
|
||
.mwiz__pill--done { background: var(--mood-accent-soft); color: var(--mood-accent); }
|
||
|
||
/* Header */
|
||
.mwiz__header { margin-bottom: 2rem; }
|
||
.mwiz__title { font-size: clamp(1.375rem, 3vw, 1.75rem); font-weight: 800; color: var(--mood-text); margin-bottom: 0.375rem; }
|
||
.mwiz__subtitle { font-size: 0.9375rem; color: var(--mood-muted); }
|
||
|
||
/* Form */
|
||
.mwiz__form { display: flex; flex-direction: column; gap: 1.5rem; }
|
||
.mwiz__section { display: flex; flex-direction: column; gap: 0.5rem; }
|
||
.mwiz__label { font-size: 0.9375rem; font-weight: 600; color: var(--mood-text); }
|
||
.mwiz__label-sm { font-size: 0.8125rem; font-weight: 600; color: var(--mood-muted); }
|
||
.mwiz__req { color: var(--mood-accent); }
|
||
.mwiz__optional { font-weight: 400; font-size: 0.8125rem; color: var(--mood-muted); }
|
||
.mwiz__input {
|
||
width: 100%;
|
||
padding: 0.6875rem 1rem;
|
||
font-size: 0.9375rem;
|
||
color: var(--mood-text);
|
||
background: var(--mood-accent-soft);
|
||
border-radius: 12px;
|
||
outline: none;
|
||
}
|
||
.mwiz__textarea {
|
||
width: 100%;
|
||
padding: 0.6875rem 1rem;
|
||
font-size: 0.9375rem;
|
||
color: var(--mood-text);
|
||
background: var(--mood-accent-soft);
|
||
border-radius: 12px;
|
||
outline: none;
|
||
resize: vertical;
|
||
font-family: inherit;
|
||
}
|
||
|
||
/* Type grid (buttons) */
|
||
.mwiz__type-grid { display: flex; gap: 0.5rem; flex-wrap: wrap; }
|
||
.mwiz__type-grid--sm .mwiz__type-btn { padding: 0.375rem 0.75rem; font-size: 0.875rem; }
|
||
.mwiz__type-btn {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 0.375rem;
|
||
padding: 0.5rem 1rem;
|
||
font-size: 0.9375rem;
|
||
font-weight: 600;
|
||
color: var(--mood-muted);
|
||
background: var(--mood-surface);
|
||
border-radius: 16px;
|
||
cursor: pointer;
|
||
transition: background 0.1s, color 0.1s, transform 0.1s;
|
||
}
|
||
.mwiz__type-btn:hover { transform: translateY(-1px); }
|
||
.mwiz__type-btn--active { background: var(--mood-accent); color: var(--mood-accent-text); }
|
||
|
||
/* Mandate type cards (Statutaire / Fonctionnel) */
|
||
.mwiz__type-card {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: 0.875rem;
|
||
padding: 1rem 1.25rem;
|
||
background: var(--mood-surface);
|
||
border-radius: 14px;
|
||
cursor: pointer;
|
||
text-align: left;
|
||
flex: 1;
|
||
min-width: 200px;
|
||
transition: transform 0.12s ease, box-shadow 0.12s ease;
|
||
color: var(--mood-muted);
|
||
}
|
||
.mwiz__type-card:hover { transform: translateY(-2px); box-shadow: 0 4px 14px var(--mood-shadow); }
|
||
.mwiz__type-card--active {
|
||
box-shadow: 0 0 0 2px var(--mood-accent), 0 4px 14px var(--mood-shadow);
|
||
background: var(--mood-accent-soft);
|
||
color: var(--mood-accent);
|
||
}
|
||
.mwiz__type-card-label { font-weight: 700; font-size: 0.9375rem; color: var(--mood-text); margin-bottom: 0.125rem; }
|
||
.mwiz__type-card-desc { font-size: 0.8125rem; color: var(--mood-muted); line-height: 1.45; }
|
||
|
||
/* Duration */
|
||
.mwiz__dur-toggle {
|
||
display: inline-flex;
|
||
background: var(--mood-surface);
|
||
border-radius: 12px;
|
||
padding: 0.25rem;
|
||
gap: 0.25rem;
|
||
margin-bottom: 0.75rem;
|
||
}
|
||
.mwiz__dur-btn {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 0.25rem;
|
||
padding: 0.375rem 0.875rem;
|
||
font-size: 0.875rem;
|
||
font-weight: 600;
|
||
color: var(--mood-muted);
|
||
border-radius: 10px;
|
||
cursor: pointer;
|
||
transition: background 0.1s, color 0.1s;
|
||
}
|
||
.mwiz__dur-btn--active { background: var(--mood-accent); color: var(--mood-accent-text); }
|
||
.mwiz__dur-relative { display: flex; align-items: center; gap: 0.75rem; flex-wrap: wrap; }
|
||
.mwiz__dur-number {
|
||
width: 5rem;
|
||
padding: 0.5rem 0.75rem;
|
||
font-size: 1.125rem;
|
||
font-weight: 700;
|
||
text-align: center;
|
||
background: var(--mood-accent-soft);
|
||
border-radius: 10px;
|
||
outline: none;
|
||
color: var(--mood-text);
|
||
}
|
||
.mwiz__dur-dates { display: flex; align-items: center; gap: 1rem; flex-wrap: wrap; }
|
||
.mwiz__dur-date-field { display: flex; flex-direction: column; gap: 0.25rem; flex: 1; min-width: 140px; }
|
||
.mwiz__dur-arrow { color: var(--mood-muted); }
|
||
|
||
/* Submit */
|
||
.mwiz__submit {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
padding: 0.75rem 1.75rem;
|
||
font-size: 1rem;
|
||
font-weight: 700;
|
||
color: var(--mood-accent-text);
|
||
background: var(--mood-accent);
|
||
border-radius: 20px;
|
||
cursor: pointer;
|
||
transition: transform 0.12s ease, box-shadow 0.12s ease;
|
||
align-self: flex-start;
|
||
margin-top: 0.75rem;
|
||
}
|
||
.mwiz__submit:hover:not(:disabled) { transform: translateY(-2px); box-shadow: 0 4px 16px var(--mood-shadow); }
|
||
.mwiz__submit:disabled { opacity: 0.4; cursor: not-allowed; }
|
||
|
||
/* Nomination cards */
|
||
.mwiz__nom-grid {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 1rem;
|
||
margin-bottom: 0;
|
||
}
|
||
.mwiz__nom-card {
|
||
background: var(--mood-surface);
|
||
border-radius: 16px;
|
||
overflow: hidden;
|
||
cursor: pointer;
|
||
transition: transform 0.12s ease, box-shadow 0.12s ease;
|
||
border: 2px solid transparent;
|
||
}
|
||
.mwiz__nom-card:hover { transform: translateY(-2px); box-shadow: 0 6px 20px var(--mood-shadow); }
|
||
.mwiz__nom-card--active { border-color: var(--mood-accent); background: var(--mood-accent-soft); }
|
||
.mwiz__nom-head {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: 0.875rem;
|
||
padding: 1.25rem;
|
||
}
|
||
.mwiz__nom-icon { color: var(--mood-accent); flex-shrink: 0; }
|
||
.mwiz__nom-body { flex: 1; }
|
||
.mwiz__nom-title { font-weight: 700; font-size: 1rem; color: var(--mood-text); margin-bottom: 0.25rem; }
|
||
.mwiz__nom-desc { font-size: 0.875rem; color: var(--mood-muted); line-height: 1.5; }
|
||
.mwiz__nom-check { color: var(--mood-accent); margin-top: 0.125rem; flex-shrink: 0; }
|
||
|
||
/* Ratification sub-panel */
|
||
.mwiz__ratif {
|
||
padding: 1.25rem;
|
||
padding-top: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 1rem;
|
||
}
|
||
.mwiz__ratif-toggle {
|
||
display: inline-flex;
|
||
background: color-mix(in srgb, var(--mood-accent) 10%, transparent);
|
||
border-radius: 12px;
|
||
padding: 0.25rem;
|
||
gap: 0.25rem;
|
||
}
|
||
.mwiz__ratif-btn {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 0.375rem;
|
||
padding: 0.5rem 1rem;
|
||
font-size: 0.875rem;
|
||
font-weight: 600;
|
||
color: var(--mood-muted);
|
||
border-radius: 10px;
|
||
cursor: pointer;
|
||
transition: background 0.1s, color 0.1s;
|
||
}
|
||
.mwiz__ratif-btn--active { background: var(--mood-accent); color: var(--mood-accent-text); }
|
||
.mwiz__ratif-note {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.375rem;
|
||
font-size: 0.8125rem;
|
||
color: var(--mood-muted);
|
||
font-style: italic;
|
||
}
|
||
.mwiz__meeting-params {
|
||
display: flex;
|
||
gap: 1rem;
|
||
flex-wrap: wrap;
|
||
align-items: flex-end;
|
||
}
|
||
|
||
/* Params */
|
||
.mwiz__param-row { display: flex; gap: 1rem; flex-wrap: wrap; }
|
||
.mwiz__param-field { display: flex; flex-direction: column; gap: 0.375rem; min-width: 100px; }
|
||
.mwiz__param-input-row { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; }
|
||
.mwiz__param-input {
|
||
width: 5rem;
|
||
padding: 0.375rem 0.5rem;
|
||
font-size: 0.9375rem;
|
||
font-weight: 700;
|
||
text-align: center;
|
||
background: color-mix(in srgb, var(--mood-accent) 15%, transparent);
|
||
border-radius: 8px;
|
||
outline: none;
|
||
color: var(--mood-text);
|
||
}
|
||
.mwiz__param-unit { font-size: 0.8125rem; color: var(--mood-muted); white-space: nowrap; }
|
||
.mwiz__param-slider { flex: 1; min-width: 80px; accent-color: var(--mood-accent); }
|
||
|
||
/* Toolbox (collective) */
|
||
.mwiz__toolbox {
|
||
padding: 1.25rem;
|
||
padding-top: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 1rem;
|
||
}
|
||
.mwiz__toolbox-title {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
font-size: 0.875rem;
|
||
font-weight: 700;
|
||
color: var(--mood-accent);
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.04em;
|
||
}
|
||
.mwiz__mode-toggle {
|
||
display: inline-flex;
|
||
background: color-mix(in srgb, var(--mood-accent) 10%, transparent);
|
||
border-radius: 14px;
|
||
padding: 0.3125rem;
|
||
gap: 0.25rem;
|
||
}
|
||
.mwiz__mode-btn {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 0.375rem;
|
||
padding: 0.5rem 1.125rem;
|
||
font-size: 0.9375rem;
|
||
font-weight: 600;
|
||
color: var(--mood-muted);
|
||
border-radius: 10px;
|
||
cursor: pointer;
|
||
transition: background 0.1s, color 0.1s;
|
||
}
|
||
.mwiz__mode-btn--active { background: var(--mood-accent); color: var(--mood-accent-text); }
|
||
|
||
/* Modalities */
|
||
.mwiz__modalities { display: flex; flex-direction: column; gap: 0.625rem; }
|
||
.mwiz__modality {
|
||
background: color-mix(in srgb, var(--mood-bg) 60%, transparent);
|
||
border-radius: 14px;
|
||
padding: 1rem 1.125rem;
|
||
cursor: pointer;
|
||
transition: transform 0.12s ease, box-shadow 0.12s ease;
|
||
border-left: 3px solid transparent;
|
||
}
|
||
.mwiz__modality:hover { transform: translateY(-1px); box-shadow: 0 3px 10px var(--mood-shadow); }
|
||
.mwiz__modality--selected { border-left-color: var(--mood-accent); box-shadow: 0 3px 10px var(--mood-shadow); }
|
||
.mwiz__modality--recommended { border-left-color: var(--mood-success, var(--mood-accent)); }
|
||
.mwiz__modality--selected.mwiz__modality--recommended { border-left-color: var(--mood-accent); }
|
||
.mwiz__modality-head { display: flex; align-items: flex-start; gap: 0.75rem; }
|
||
.mwiz__modality-icon {
|
||
width: 2.25rem;
|
||
height: 2.25rem;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background: var(--mood-accent-soft);
|
||
color: var(--mood-accent);
|
||
border-radius: 10px;
|
||
flex-shrink: 0;
|
||
}
|
||
.mwiz__modality-info { flex: 1; }
|
||
.mwiz__modality-title { font-weight: 700; font-size: 0.9rem; color: var(--mood-text); margin-bottom: 0.125rem; }
|
||
.mwiz__modality-desc { font-size: 0.8rem; color: var(--mood-muted); line-height: 1.4; }
|
||
.mwiz__modality-check { color: var(--mood-accent); margin-top: 0.125rem; flex-shrink: 0; }
|
||
.mwiz__modality-badge {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 0.25rem;
|
||
margin-top: 0.5rem;
|
||
font-size: 0.75rem;
|
||
font-weight: 700;
|
||
color: var(--mood-success, var(--mood-accent));
|
||
background: color-mix(in srgb, var(--mood-success, var(--mood-accent)) 12%, transparent);
|
||
border-radius: 12px;
|
||
padding: 0.1875rem 0.625rem;
|
||
}
|
||
.mwiz__modality-reason { font-size: 0.8rem; color: var(--mood-muted); margin-top: 0.25rem; font-style: italic; }
|
||
.mwiz__modality-params {
|
||
margin-top: 0.875rem;
|
||
padding-top: 0.875rem;
|
||
border-top: 1px solid var(--mood-border, rgba(0,0,0,0.06));
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 1rem;
|
||
}
|
||
|
||
/* Recap */
|
||
.mwiz__recap { display: flex; flex-direction: column; gap: 1.25rem; margin-bottom: 2rem; }
|
||
.mwiz__recap-block {
|
||
background: var(--mood-surface);
|
||
border-radius: 16px;
|
||
padding: 1.25rem;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 0.375rem;
|
||
}
|
||
.mwiz__recap-label { font-size: 0.75rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em; color: var(--mood-muted); margin-bottom: 0.25rem; }
|
||
.mwiz__recap-title { font-size: 1.125rem; font-weight: 800; color: var(--mood-text); }
|
||
.mwiz__recap-value { font-size: 0.9375rem; font-weight: 600; color: var(--mood-text); }
|
||
.mwiz__recap-meta { font-size: 0.875rem; color: var(--mood-muted); display: flex; align-items: center; gap: 0.375rem; }
|
||
|
||
/* Steps list */
|
||
.mwiz__steps-list { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: 0.625rem; }
|
||
.mwiz__steps-item { display: flex; gap: 0.75rem; align-items: flex-start; }
|
||
.mwiz__steps-num {
|
||
width: 1.5rem; height: 1.5rem;
|
||
display: flex; align-items: center; justify-content: center;
|
||
font-size: 0.75rem; font-weight: 700;
|
||
background: var(--mood-accent-soft);
|
||
color: var(--mood-accent);
|
||
border-radius: 50%;
|
||
flex-shrink: 0;
|
||
margin-top: 0.125rem;
|
||
}
|
||
.mwiz__steps-title { font-weight: 600; font-size: 0.875rem; color: var(--mood-text); }
|
||
.mwiz__steps-desc { font-size: 0.8125rem; color: var(--mood-muted); }
|
||
|
||
/* Recap actions */
|
||
.mwiz__recap-actions { display: flex; gap: 1rem; align-items: center; }
|
||
.mwiz__back-btn {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 0.375rem;
|
||
padding: 0.625rem 1.25rem;
|
||
font-size: 0.9375rem;
|
||
font-weight: 600;
|
||
color: var(--mood-muted);
|
||
background: var(--mood-surface);
|
||
border-radius: 16px;
|
||
cursor: pointer;
|
||
transition: transform 0.1s;
|
||
}
|
||
.mwiz__back-btn:hover { transform: translateY(-1px); }
|
||
.mwiz__create-btn {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
padding: 0.75rem 1.75rem;
|
||
font-size: 1rem;
|
||
font-weight: 700;
|
||
color: var(--mood-accent-text);
|
||
background: var(--mood-accent);
|
||
border-radius: 20px;
|
||
cursor: pointer;
|
||
transition: transform 0.12s ease, box-shadow 0.12s ease;
|
||
}
|
||
.mwiz__create-btn:hover:not(:disabled) { transform: translateY(-2px); box-shadow: 0 4px 16px var(--mood-shadow); }
|
||
.mwiz__create-btn:disabled { opacity: 0.4; cursor: not-allowed; }
|
||
|
||
.mwiz__error { color: var(--mood-danger, #e53e3e); font-size: 0.875rem; padding: 0.5rem 0; }
|
||
|
||
/* Start option */
|
||
.mwiz__start-option {
|
||
background: var(--mood-surface);
|
||
border-radius: 16px;
|
||
padding: 1.125rem 1.25rem;
|
||
margin-bottom: 1rem;
|
||
}
|
||
.mwiz__start-label {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.625rem;
|
||
font-weight: 600;
|
||
font-size: 0.9375rem;
|
||
color: var(--mood-text);
|
||
cursor: pointer;
|
||
}
|
||
.mwiz__checkbox {
|
||
width: 1.125rem;
|
||
height: 1.125rem;
|
||
accent-color: var(--mood-accent);
|
||
cursor: pointer;
|
||
flex-shrink: 0;
|
||
}
|
||
.mwiz__start-hint { font-size: 0.8125rem; color: var(--mood-muted); margin-top: 0.375rem; padding-left: 1.75rem; }
|
||
.mwiz__hint { font-size: 0.8125rem; margin-top: 0.25rem; }
|
||
|
||
/* Transitions */
|
||
.slide-fade-enter-active, .slide-fade-leave-active { transition: all 0.2s ease; }
|
||
.slide-fade-enter-from { opacity: 0; transform: translateX(20px); }
|
||
.slide-fade-leave-to { opacity: 0; transform: translateX(-20px); }
|
||
.slide-down-enter-active, .slide-down-leave-active { transition: all 0.22s ease; overflow: hidden; }
|
||
.slide-down-enter-from, .slide-down-leave-to { opacity: 0; max-height: 0; }
|
||
.slide-down-enter-to, .slide-down-leave-from { max-height: 800px; }
|
||
</style>
|