Intégration fonctionnalites V1
This commit is contained in:
@@ -0,0 +1,490 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user