Files
backend/resources/js/Pages/Supervisor/Organizations/Index.vue
T

491 lines
19 KiB
Vue

<script setup>
import AddressAutocomplete from '@/Components/Supervisor/AddressAutocomplete.vue';
import AlertBanner from '@/Components/Supervisor/AlertBanner.vue';
import DataTable from '@/Components/Supervisor/DataTable.vue';
import FormField from '@/Components/Supervisor/FormField.vue';
import FormModal from '@/Components/Supervisor/FormModal.vue';
import Pagination from '@/Components/Supervisor/Pagination.vue';
import StatusBadge from '@/Components/Supervisor/StatusBadge.vue';
import TableHeaderCell from '@/Components/Supervisor/TableHeaderCell.vue';
import { useListFilters } from '@/Components/Supervisor/useListFilters.js';
import {
activeStatusColors,
cardClass,
filterInputClass,
filterSelectClass,
getStatusColor,
inputClass,
rowClass,
tdClass,
} from '@/Components/Supervisor/ui.js';
import ConfirmDeleteModal from '@/Components/ConfirmDeleteModal.vue';
import PrimaryButton from '@/Components/PrimaryButton.vue';
import SupervisorLayout from '@/Layouts/SupervisorLayout.vue';
import { Head, useForm } from '@inertiajs/vue3';
import { computed, onMounted, ref } from 'vue';
const props = defineProps({
establishments: {
type: [Array, Object],
default: () => [],
},
viewMode: {
type: String,
default: 'list',
},
canManageEstablishments: {
type: Boolean,
default: false,
},
canCreateEstablishments: {
type: Boolean,
default: false,
},
filters: {
type: Object,
default: () => ({}),
},
});
const { localFilters, toggleSort } = useListFilters('supervisor.organizations.index', {
search: props.filters.search ?? '',
is_active: props.filters.is_active ?? '',
sort: props.filters.sort ?? 'name',
direction: props.filters.direction ?? 'asc',
per_page: props.filters.per_page ?? 10,
}, { debounceKeys: ['search'] });
const handleSort = (key) => toggleSort(key);
const isSingleView = computed(() => props.viewMode === 'single');
const singleEstablishment = computed(() => {
if (!isSingleView.value || !Array.isArray(props.establishments)) {
return null;
}
return props.establishments[0] ?? null;
});
const establishmentRows = computed(() => {
if (isSingleView.value) {
return Array.isArray(props.establishments) ? props.establishments : [];
}
return props.establishments?.data ?? [];
});
const establishmentListCount = computed(() => {
if (isSingleView.value) {
return establishmentRows.value.length;
}
return props.establishments?.total ?? establishmentRows.value.length;
});
const editingEstablishmentId = ref(null);
const showEstablishmentForm = ref(false);
const toggleTarget = ref(null);
const toggleForm = useForm({});
const establishmentFormTitle = computed(() => {
if (editingEstablishmentId.value || isSingleView.value) {
return 'Modifier l\'enseigne';
}
return 'Nouvelle enseigne';
});
const establishmentSubmitLabel = computed(() => {
if (editingEstablishmentId.value || isSingleView.value) {
return 'Enregistrer';
}
return 'Créer';
});
const establishmentToForm = (establishment) => ({
name: establishment?.name ?? '',
address: establishment?.address ?? '',
city: establishment?.city ?? '',
zip_code: establishment?.zip_code ?? '',
timezone: establishment?.timezone ?? 'Europe/Paris',
is_active: establishment?.is_active ?? true,
});
const emptyEstablishmentForm = () => ({
name: '',
address: '',
city: '',
zip_code: '',
timezone: 'Europe/Paris',
is_active: true,
});
const establishmentForm = useForm(
isSingleView.value && singleEstablishment.value
? establishmentToForm(singleEstablishment.value)
: emptyEstablishmentForm(),
);
const startCreateEstablishment = () => {
editingEstablishmentId.value = null;
establishmentForm.defaults(emptyEstablishmentForm());
establishmentForm.reset();
showEstablishmentForm.value = true;
};
const startEditEstablishment = (establishment) => {
editingEstablishmentId.value = establishment.id;
establishmentForm.defaults(establishmentToForm(establishment));
establishmentForm.reset();
showEstablishmentForm.value = true;
};
onMounted(() => {
if (!isSingleView.value && establishmentListCount.value === 0 && props.canCreateEstablishments) {
startCreateEstablishment();
}
});
const cancelEstablishmentForm = () => {
showEstablishmentForm.value = false;
editingEstablishmentId.value = null;
if (isSingleView.value && singleEstablishment.value) {
establishmentForm.defaults(establishmentToForm(singleEstablishment.value));
}
establishmentForm.reset();
};
const submitEstablishment = () => {
const targetId = isSingleView.value ? singleEstablishment.value?.id : editingEstablishmentId.value;
const wasActive = isSingleView.value
? singleEstablishment.value?.is_active
: establishmentRows.value.find((e) => e.id === editingEstablishmentId.value)?.is_active;
if (targetId && wasActive && !establishmentForm.is_active) {
toggleTarget.value = { action: 'deactivate', source: 'form' };
return;
}
if (targetId && !wasActive && establishmentForm.is_active) {
toggleTarget.value = { action: 'reactivate', source: 'form' };
return;
}
performSubmit();
};
const isReactivateAction = computed(() => toggleTarget.value?.action === 'reactivate');
const toggleModalTitle = computed(() =>
isReactivateAction.value ? 'Réactiver l\'enseigne' : 'Désactiver l\'enseigne',
);
const toggleModalMessage = computed(() => {
if (!toggleTarget.value) return '';
if (toggleTarget.value.action === 'reactivate') {
if (toggleTarget.value.source === 'toggle') {
return `Voulez-vous réactiver « ${toggleTarget.value.establishment.name} » ? Elle redeviendra visible par les utilisateurs.`;
}
return 'Voulez-vous réactiver cette enseigne ? Elle redeviendra visible par les utilisateurs.';
}
if (toggleTarget.value.source === 'toggle') {
return `Voulez-vous désactiver « ${toggleTarget.value.establishment.name} » ? Elle ne sera plus visible par les utilisateurs.`;
}
return 'Voulez-vous désactiver cette enseigne ? Elle ne sera plus visible par les utilisateurs.';
});
const toggleModalWarning = computed(() =>
isReactivateAction.value
? 'Vérifiez que le nom, l\'adresse et le fuseau horaire sont corrects avant de réactiver.'
: 'Vous pourrez la réactiver à tout moment.',
);
const toggleModalConfirmLabel = computed(() =>
isReactivateAction.value ? 'Réactiver' : 'Désactiver',
);
const toggleFormErrors = computed(() => Object.values(toggleForm.errors));
const performSubmit = () => {
const targetId = isSingleView.value ? singleEstablishment.value?.id : editingEstablishmentId.value;
if (targetId) {
establishmentForm.put(
route('supervisor.organizations.establishments.update', targetId),
{
onSuccess: () => {
closeToggleModal();
cancelEstablishmentForm();
},
},
);
} else {
establishmentForm.post(route('supervisor.organizations.establishments.store'), {
onSuccess: () => {
closeToggleModal();
cancelEstablishmentForm();
},
});
}
};
const requestToggle = (establishment) => {
toggleTarget.value = {
action: establishment.is_active ? 'deactivate' : 'reactivate',
source: 'toggle',
establishment,
};
};
const closeToggleModal = () => {
toggleTarget.value = null;
};
const confirmToggle = () => {
if (!toggleTarget.value) return;
if (toggleTarget.value.source === 'toggle') {
toggleForm.patch(
route('supervisor.organizations.establishments.toggle-active', toggleTarget.value.establishment.id),
{
preserveScroll: true,
onSuccess: closeToggleModal,
},
);
} else {
performSubmit();
}
};
const formatAddress = (establishment) => {
const parts = [establishment.address, establishment.zip_code, establishment.city].filter(Boolean);
return parts.join(', ');
};
</script>
<template>
<Head title="Enseignes" />
<SupervisorLayout title="Enseignes">
<template #actions>
<PrimaryButton
v-if="!isSingleView && canCreateEstablishments"
@click="startCreateEstablishment"
>
Nouvelle enseigne
</PrimaryButton>
</template>
<AlertBanner v-if="toggleFormErrors.length > 0">
<ul class="space-y-1">
<li v-for="(error, index) in toggleFormErrors" :key="index">{{ error }}</li>
</ul>
</AlertBanner>
<section v-if="isSingleView && singleEstablishment">
<div class="mb-6 flex items-center justify-between">
<p class="text-sm text-slate-500">Informations de votre laverie</p>
<StatusBadge
:label="singleEstablishment.is_active ? 'Active' : 'Inactive'"
:color-class="getStatusColor(singleEstablishment.is_active ? 'active' : 'inactive', activeStatusColors)"
/>
</div>
<div :class="[cardClass, 'p-6']">
<dl class="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div>
<dt class="text-xs font-medium uppercase tracking-wide text-slate-400">Nom</dt>
<dd class="mt-1 text-sm font-medium text-slate-900">{{ singleEstablishment.name }}</dd>
</div>
<div>
<dt class="text-xs font-medium uppercase tracking-wide text-slate-400">Adresse</dt>
<dd class="mt-1 text-sm text-slate-700">{{ formatAddress(singleEstablishment) }}</dd>
</div>
<div>
<dt class="text-xs font-medium uppercase tracking-wide text-slate-400">Fuseau horaire</dt>
<dd class="mt-1 text-sm text-slate-700">{{ singleEstablishment.timezone }}</dd>
</div>
<div v-if="singleEstablishment.latitude != null && singleEstablishment.longitude != null">
<dt class="text-xs font-medium uppercase tracking-wide text-slate-400">Coordonnées GPS</dt>
<dd class="mt-1 text-sm text-slate-700">
{{ singleEstablishment.latitude }}, {{ singleEstablishment.longitude }}
</dd>
<p class="mt-0.5 text-xs text-slate-400">Calculées automatiquement à partir de l'adresse.</p>
</div>
</dl>
<div v-if="canManageEstablishments" class="mt-6">
<PrimaryButton @click="startEditEstablishment(singleEstablishment)">
Modifier
</PrimaryButton>
</div>
</div>
<p v-if="!canManageEstablishments" class="mt-4 rounded-xl bg-slate-50 px-4 py-3 text-sm text-slate-500">
Consultation seule — contactez votre administrateur pour modifier ces informations.
</p>
</section>
<section v-else>
<p class="mb-4 text-sm text-slate-500">
{{ establishmentListCount }} enseigne(s)
</p>
<DataTable>
<template #head>
<tr>
<TableHeaderCell
label="Nom"
sortable
sort-key="name"
:active-sort="localFilters.sort"
:sort-direction="localFilters.direction"
@sort="handleSort"
>
<input
v-model="localFilters.search"
type="text"
placeholder="Nom, adresse…"
:class="filterInputClass"
/>
</TableHeaderCell>
<TableHeaderCell
label="Adresse"
sortable
sort-key="city"
:active-sort="localFilters.sort"
:sort-direction="localFilters.direction"
@sort="handleSort"
/>
<TableHeaderCell
label="Machines"
sortable
sort-key="machines_count"
:active-sort="localFilters.sort"
:sort-direction="localFilters.direction"
@sort="handleSort"
/>
<TableHeaderCell
label="Statut"
sortable
sort-key="is_active"
:active-sort="localFilters.sort"
:sort-direction="localFilters.direction"
@sort="handleSort"
>
<select v-model="localFilters.is_active" :class="filterSelectClass">
<option value="">Tous</option>
<option value="1">Active</option>
<option value="0">Inactive</option>
</select>
</TableHeaderCell>
<TableHeaderCell v-if="canManageEstablishments" label="Actions" align="right" />
</tr>
</template>
<tr v-if="establishmentRows.length === 0">
<td :colspan="canManageEstablishments ? 5 : 4" class="px-6 py-12 text-center text-sm text-slate-400">
Aucune enseigne trouvée.
</td>
</tr>
<tr v-for="establishment in establishmentRows" :key="establishment.id" :class="rowClass">
<td :class="[tdClass, 'font-medium text-slate-800']">{{ establishment.name }}</td>
<td :class="tdClass">{{ formatAddress(establishment) }}</td>
<td :class="tdClass">{{ establishment.machines_count }}</td>
<td :class="tdClass">
<StatusBadge
:label="establishment.is_active ? 'Active' : 'Inactive'"
:color-class="getStatusColor(establishment.is_active ? 'active' : 'inactive', activeStatusColors)"
/>
</td>
<td v-if="canManageEstablishments" :class="[tdClass, 'text-right']">
<button class="font-medium text-indigo-600 transition hover:text-indigo-800" @click="startEditEstablishment(establishment)">
Modifier
</button>
<button
class="ms-3 font-medium transition"
:class="establishment.is_active ? 'text-orange-600 hover:text-orange-800' : 'text-emerald-600 hover:text-emerald-800'"
:disabled="toggleForm.processing || establishmentForm.processing"
@click="requestToggle(establishment)"
>
{{ establishment.is_active ? 'Désactiver' : 'Réactiver' }}
</button>
</td>
</tr>
<template #footer>
<Pagination
:paginator="establishments"
:per-page="localFilters.per_page"
@update:per-page="(value) => { localFilters.per_page = value; }"
/>
</template>
</DataTable>
</section>
<FormModal
:show="showEstablishmentForm"
:title="establishmentFormTitle"
:submit-label="establishmentSubmitLabel"
:processing="establishmentForm.processing"
max-width="3xl"
@close="cancelEstablishmentForm"
@submit="submitEstablishment"
>
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
<FormField label="Nom">
<input v-model="establishmentForm.name" type="text" required :class="inputClass" />
</FormField>
<FormField label="Adresse" field-class="sm:col-span-2">
<AddressAutocomplete
v-model:address="establishmentForm.address"
v-model:city="establishmentForm.city"
v-model:zip-code="establishmentForm.zip_code"
required
/>
<p class="mt-1 text-xs text-slate-400">
Les coordonnées GPS seront calculées automatiquement à l'enregistrement.
</p>
</FormField>
<FormField label="Code postal">
<input v-model="establishmentForm.zip_code" type="text" :class="inputClass" />
</FormField>
<FormField label="Ville">
<input v-model="establishmentForm.city" type="text" :class="inputClass" />
</FormField>
<FormField label="Fuseau horaire">
<input v-model="establishmentForm.timezone" type="text" required :class="inputClass" />
</FormField>
<div class="sm:col-span-2 lg:col-span-3">
<label class="flex items-center gap-2 text-sm text-slate-700">
<input v-model="establishmentForm.is_active" type="checkbox" class="rounded border-slate-300 text-indigo-600 focus:ring-indigo-500/20" />
Enseigne active
</label>
<p class="mt-1 text-xs text-slate-400">
Décochez pour désactiver cette enseigne sans la supprimer.
</p>
</div>
</div>
</FormModal>
<ConfirmDeleteModal
:show="toggleTarget !== null"
:title="toggleModalTitle"
:message="toggleModalMessage"
:confirm-label="toggleModalConfirmLabel"
:warning="toggleModalWarning"
:danger="!isReactivateAction"
:processing="toggleForm.processing || establishmentForm.processing"
@close="closeToggleModal"
@confirm="confirmToggle"
/>
</SupervisorLayout>
</template>