Barre horizontale en haut de l'app qui liste les sites autorises de l'utilisateur et permet de switcher d'un click. Consomme le composant MalioSiteSelector de @malio/layer-ui 1.4.0 (upgrade depuis 1.3.0). Composables : - useModules (shared) : consomme /api/modules, expose isModuleActive. Pattern aligne sur useSidebar. - useCurrentSite (layer sites) : singleton state, switchSite optimistic avec rollback sur erreur, garde anti-double-submit, propagation au store auth via action setCurrentSite dediee. Composant : - SiteSelector.vue : wrapper thin autour de MalioSiteSelector. Texte blanc uniforme (conforme maquette Figma) avec taille 24px forcee via labelClass="text-2xl". aria-label du group via ariaGroupLabel i18n. Integration : - Middleware auth.global.ts : chargement parallele sidebar + modules. - layouts/default.vue : render conditionnel si module Sites actif ET user.sites.length > 0. - logout.vue : reset des 3 composables (sidebar, modules, currentSite) dans un try/finally. - nuxt.config.ts : auto-detection des composables/ de chaque layer module (necessaire car imports.dirs explicite override les defaults Nuxt). Couleurs fixtures finales : Chatellerault #056CF2, Saint-Jean #F3CB00, Pommevic #74BF04. Charge aux admins de choisir des teintes foncees (texte blanc non contrastable via calcul WCAG, design choisi). Tests : 40 Vitest (color, useModules, useSidebar, useCurrentSite, SiteSelector) incluant garde anti-regression pour useI18n hors setup. 182/182 PHPUnit backend, avec et sans module actif. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
186 lines
5.9 KiB
Vue
186 lines
5.9 KiB
Vue
<template>
|
|
<MalioDrawer
|
|
:model-value="modelValue"
|
|
:title="isEditMode ? t('admin.sites.editSite') : t('admin.sites.createSite')"
|
|
drawer-class="w-full max-w-lg"
|
|
@update:model-value="emit('update:modelValue', $event)"
|
|
>
|
|
<form class="flex flex-col gap-6 p-4" @submit.prevent="handleSave">
|
|
<MalioInputText
|
|
v-model="form.name"
|
|
:label="t('admin.sites.form.name')"
|
|
input-class="w-full"
|
|
required
|
|
/>
|
|
|
|
<MalioInputText
|
|
v-model="form.street"
|
|
:label="t('admin.sites.form.street')"
|
|
input-class="w-full"
|
|
required
|
|
/>
|
|
|
|
<MalioInputText
|
|
v-model="form.complement"
|
|
:label="t('admin.sites.form.complement')"
|
|
:placeholder="t('admin.sites.form.complementPlaceholder')"
|
|
input-class="w-full"
|
|
/>
|
|
|
|
<!-- Code postal FR : masque "#####" (5 chiffres stricts) +
|
|
maxLength en double securite. La regex backend validera la
|
|
forme finale, le masque empeche juste la saisie de
|
|
caracteres non numeriques. -->
|
|
<MalioInputText
|
|
v-model="form.postalCode"
|
|
:label="t('admin.sites.form.postalCode')"
|
|
input-class="w-full"
|
|
mask="#####"
|
|
max-length="5"
|
|
required
|
|
/>
|
|
|
|
<MalioInputText
|
|
v-model="form.city"
|
|
:label="t('admin.sites.form.city')"
|
|
input-class="w-full"
|
|
required
|
|
/>
|
|
|
|
<!-- Champ couleur avec preview puce -->
|
|
<div>
|
|
<label class="mb-1 block text-sm font-semibold text-neutral-700">
|
|
{{ t('admin.sites.form.color') }}
|
|
</label>
|
|
<div class="flex items-center gap-3">
|
|
<MalioInputText
|
|
v-model="form.color"
|
|
placeholder="#RRGGBB"
|
|
input-class="w-full font-mono"
|
|
required
|
|
/>
|
|
<span
|
|
:style="{ backgroundColor: isValidHex ? form.color : 'transparent' }"
|
|
class="inline-block size-10 shrink-0 rounded-lg border border-neutral-200"
|
|
:class="{ 'border-dashed': !isValidHex }"
|
|
/>
|
|
</div>
|
|
<p v-if="form.color && !isValidHex" class="mt-1 text-xs text-red-600">
|
|
{{ t('admin.sites.form.colorInvalid') }}
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Boutons -->
|
|
<div class="flex justify-end gap-3 border-t border-neutral-200 pt-4">
|
|
<MalioButton
|
|
v-if="isEditMode"
|
|
:label="t('common.delete')"
|
|
variant="danger"
|
|
icon-name="mdi:delete-outline"
|
|
icon-position="left"
|
|
@click="emit('delete')"
|
|
/>
|
|
<MalioButton
|
|
v-else
|
|
:label="t('common.cancel')"
|
|
variant="tertiary"
|
|
@click="emit('update:modelValue', false)"
|
|
/>
|
|
<MalioButton
|
|
:label="t('common.save')"
|
|
variant="primary"
|
|
:disabled="saving || !isValidHex"
|
|
@click="handleSave"
|
|
/>
|
|
</div>
|
|
</form>
|
|
</MalioDrawer>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import type { Site } from '~/shared/types/sites'
|
|
import { isValidSiteColor } from '~/shared/utils/color'
|
|
|
|
const { t } = useI18n()
|
|
const api = useApi()
|
|
|
|
const props = defineProps<{
|
|
modelValue: boolean
|
|
site: Site | null
|
|
}>()
|
|
|
|
const emit = defineEmits<{
|
|
'update:modelValue': [value: boolean]
|
|
saved: []
|
|
delete: []
|
|
}>()
|
|
|
|
const saving = ref(false)
|
|
|
|
const form = ref({
|
|
name: '',
|
|
street: '',
|
|
complement: '',
|
|
postalCode: '',
|
|
city: '',
|
|
color: '#000000',
|
|
})
|
|
|
|
const isEditMode = computed(() => props.site !== null)
|
|
|
|
// Validation locale du format hex #RRGGBB avant envoi backend.
|
|
const isValidHex = computed(() => isValidSiteColor(form.value.color))
|
|
|
|
// Remplir le formulaire quand le site change
|
|
watch(() => props.site, (site) => {
|
|
if (site) {
|
|
form.value.name = site.name
|
|
form.value.street = site.street
|
|
form.value.complement = site.complement ?? ''
|
|
form.value.postalCode = site.postalCode
|
|
form.value.city = site.city
|
|
form.value.color = site.color
|
|
} else {
|
|
form.value.name = ''
|
|
form.value.street = ''
|
|
form.value.complement = ''
|
|
form.value.postalCode = ''
|
|
form.value.city = ''
|
|
form.value.color = '#056CF2'
|
|
}
|
|
}, { immediate: true })
|
|
|
|
async function handleSave() {
|
|
if (!isValidHex.value) return
|
|
saving.value = true
|
|
try {
|
|
// Le champ complement est optionnel cote DB : on envoie null si vide
|
|
// pour que le backend stocke NULL plutot qu'une chaine vide.
|
|
const trimmedComplement = form.value.complement.trim()
|
|
const payload = {
|
|
name: form.value.name,
|
|
street: form.value.street,
|
|
complement: trimmedComplement === '' ? null : trimmedComplement,
|
|
postalCode: form.value.postalCode,
|
|
city: form.value.city,
|
|
color: form.value.color,
|
|
}
|
|
|
|
if (isEditMode.value && props.site) {
|
|
await api.patch(`/sites/${props.site.id}`, payload, {
|
|
toastSuccessMessage: t('admin.sites.toast.updated'),
|
|
})
|
|
} else {
|
|
await api.post('/sites', payload, {
|
|
toastSuccessMessage: t('admin.sites.toast.created'),
|
|
})
|
|
}
|
|
|
|
emit('saved')
|
|
emit('update:modelValue', false)
|
|
} finally {
|
|
saving.value = false
|
|
}
|
|
}
|
|
</script>
|