491 lines
19 KiB
Vue
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>
|