Intégration fonctionnalites V1

This commit is contained in:
bastien
2026-07-04 22:30:18 +02:00
parent 55a558b536
commit 10ee859602
54 changed files with 4802 additions and 1018 deletions
+262 -73
View File
@@ -1,7 +1,8 @@
<script setup>
import ApplicationLogo from '@/Components/ApplicationLogo.vue';
import LaverieLogo from '@/Components/LaverieLogo.vue';
import Dropdown from '@/Components/Dropdown.vue';
import { Link, usePage } from '@inertiajs/vue3';
import { computed } from 'vue';
import { computed, onMounted, ref, watch } from 'vue';
defineProps({
title: {
@@ -10,120 +11,308 @@ defineProps({
},
});
const STORAGE_KEY = 'supervisor-sidebar-collapsed';
const page = usePage();
const supervisor = computed(() => page.props.auth.supervisor);
const sidebarCollapsed = ref(false);
const navItems = [
{ label: 'Tableau de bord', route: 'supervisor.dashboard', icon: 'dashboard' },
{ label: 'Machines', route: 'supervisor.machines.index', icon: 'machines' },
{ label: 'Réservations', route: 'supervisor.bookings.index', icon: 'bookings' },
{ label: 'Lavages', route: 'supervisor.washes.index', icon: 'washes' },
{ label: 'Tarifs', route: 'supervisor.pricing.index', icon: 'pricing' },
{ label: 'Promotions', route: 'supervisor.promotions.index', icon: 'promotions' },
onMounted(() => {
sidebarCollapsed.value = localStorage.getItem(STORAGE_KEY) === '1';
});
watch(sidebarCollapsed, (value) => {
localStorage.setItem(STORAGE_KEY, value ? '1' : '0');
});
const toggleSidebar = () => {
sidebarCollapsed.value = !sidebarCollapsed.value;
};
const navGroups = [
{
label: 'Vue d\'ensemble',
items: [
{ label: 'Tableau de bord', route: 'supervisor.dashboard', icon: 'dashboard' },
],
},
{
label: 'Exploitation',
items: [
{ label: 'Enseignes', route: 'supervisor.organizations.index', icon: 'organizations' },
{ label: 'Machines', route: 'supervisor.machines.index', icon: 'machines' },
{ label: 'Réservations', route: 'supervisor.bookings.index', icon: 'bookings' },
{ label: 'Lavages', route: 'supervisor.washes.index', icon: 'washes' },
],
},
{
label: 'Commercial',
items: [
{ label: 'Tarifs', route: 'supervisor.pricing.index', icon: 'pricing' },
{ label: 'Promotions', route: 'supervisor.promotions.index', icon: 'promotions' },
],
},
];
const roleLabels = {
platform_admin: 'Administrateur',
owner: 'Propriétaire',
manager: 'Gestionnaire',
viewer: 'Lecture seule',
};
const initials = computed(() => {
const name = supervisor.value?.name?.trim() ?? '';
if (!name) return '?';
const parts = name.split(/\s+/);
if (parts.length >= 2) {
return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
}
return name.slice(0, 2).toUpperCase();
});
const isActive = (routeName) => {
if (routeName === 'supervisor.machines.index') {
return route().current('supervisor.machines.*');
}
if (routeName === 'supervisor.organizations.index') {
return route().current('supervisor.organizations.*');
}
return route().current(routeName);
};
const iconPaths = {
dashboard: 'M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6',
organizations: 'M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4',
machines: 'M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15',
bookings: 'M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z',
washes: 'M5 3v4M3 5h4M6 17v4m-2-2h4m5-16l2.286 6.857L21 12l-5.714 2.143L13 21l-2.286-6.857L5 12l5.714-2.143L13 3z',
pricing: 'M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z',
promotions: 'M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z',
};
</script>
<template>
<div class="min-h-screen bg-gray-100">
<div class="min-h-screen bg-slate-50">
<div class="flex min-h-screen">
<!-- Sidebar -->
<aside class="hidden w-64 flex-shrink-0 bg-indigo-900 lg:flex lg:flex-col">
<div class="flex h-16 items-center px-6">
<Link :href="route('supervisor.dashboard')" class="flex items-center gap-2">
<ApplicationLogo class="h-8 w-auto fill-current text-white" />
<span class="text-lg font-semibold text-white">Laverie</span>
</Link>
</div>
<nav class="mt-4 flex-1 space-y-1 px-3">
<Link
v-for="item in navItems"
:key="item.route"
:href="route(item.route)"
class="flex items-center rounded-md px-3 py-2 text-sm font-medium transition"
:class="
isActive(item.route)
? 'bg-indigo-800 text-white'
: 'text-indigo-100 hover:bg-indigo-800 hover:text-white'
"
<aside
class="hidden flex-shrink-0 transition-[width] duration-300 ease-in-out lg:flex lg:flex-col"
:class="sidebarCollapsed ? 'w-[4.75rem]' : 'w-72'"
>
<div class="flex h-full flex-col bg-gradient-to-b from-slate-900 via-slate-900 to-indigo-950 shadow-xl">
<!-- Logo + toggle -->
<div
class="flex items-center border-b border-white/5"
:class="sidebarCollapsed ? 'flex-col justify-center gap-2 py-3 px-2' : 'h-16 justify-between px-4'"
>
{{ item.label }}
</Link>
</nav>
<button
v-if="sidebarCollapsed"
type="button"
class="flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-lg text-slate-400 transition hover:bg-white/10 hover:text-white"
title="Agrandir le menu"
@click="toggleSidebar"
>
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M13 5l7 7-7 7M5 5l7 7-7 7" />
</svg>
</button>
<div class="border-t border-indigo-800 p-4">
<p class="truncate text-sm font-medium text-white">
{{ supervisor?.name }}
</p>
<p class="truncate text-xs text-indigo-300">
{{ supervisor?.email }}
</p>
<Link
:href="route('supervisor.dashboard')"
class="flex items-center rounded-xl transition hover:opacity-90"
:class="sidebarCollapsed ? 'justify-center' : 'gap-3'"
:title="sidebarCollapsed ? 'Laverie' : undefined"
>
<div class="flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-lg bg-white/10 ring-1 ring-white/20">
<LaverieLogo class="h-5 w-5 text-white" />
</div>
<div v-show="!sidebarCollapsed" class="min-w-0">
<span class="text-base font-semibold tracking-tight text-white">Laverie</span>
<p class="text-[10px] font-medium uppercase tracking-widest text-indigo-300/80">
Back-office
</p>
</div>
</Link>
<button
v-if="!sidebarCollapsed"
type="button"
class="flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-lg text-slate-400 transition hover:bg-white/10 hover:text-white"
title="Réduire le menu"
@click="toggleSidebar"
>
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M11 19l-7-7 7-7m8 14l-7-7 7-7" />
</svg>
</button>
</div>
<!-- Navigation -->
<nav
class="mt-2 flex-1 space-y-4 overflow-y-auto pb-4"
:class="sidebarCollapsed ? 'px-2' : 'px-3'"
>
<div v-for="(group, groupIndex) in navGroups" :key="group.label">
<p
v-show="!sidebarCollapsed"
class="mb-2 px-3 text-[11px] font-semibold uppercase tracking-wider text-slate-500"
>
{{ group.label }}
</p>
<div
v-if="sidebarCollapsed && groupIndex > 0"
class="mx-auto mb-2 h-px w-8 bg-white/10"
aria-hidden="true"
/>
<div class="space-y-0.5">
<Link
v-for="item in group.items"
:key="item.route"
:href="route(item.route)"
class="group relative flex items-center rounded-xl text-sm font-medium transition-all duration-150"
:class="[
sidebarCollapsed ? 'justify-center px-0 py-2.5' : 'gap-3 px-3 py-2.5',
isActive(item.route)
? 'bg-white/10 text-white shadow-sm ring-1 ring-white/10'
: 'text-slate-400 hover:bg-white/5 hover:text-white',
]"
:title="sidebarCollapsed ? item.label : undefined"
>
<span
class="flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-lg transition-colors"
:class="
isActive(item.route)
? 'bg-indigo-500/30 text-indigo-200'
: 'bg-white/5 text-slate-500 group-hover:bg-white/10 group-hover:text-slate-300'
"
>
<svg
class="h-4 w-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="1.75"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
:d="iconPaths[item.icon]"
/>
</svg>
</span>
<span v-show="!sidebarCollapsed">{{ item.label }}</span>
<span
v-if="isActive(item.route) && !sidebarCollapsed"
class="ml-auto h-1.5 w-1.5 rounded-full bg-indigo-400"
/>
<!-- Tooltip mode réduit -->
<span
v-if="sidebarCollapsed"
class="pointer-events-none absolute left-full z-50 ml-3 hidden whitespace-nowrap rounded-lg bg-slate-800 px-2.5 py-1.5 text-xs font-medium text-white shadow-lg ring-1 ring-white/10 group-hover:block"
>
{{ item.label }}
</span>
</Link>
</div>
</div>
</nav>
</div>
</aside>
<!-- Main content -->
<div class="flex flex-1 flex-col">
<div class="flex min-w-0 flex-1 flex-col">
<!-- Header -->
<header class="border-b border-gray-200 bg-white shadow-sm">
<header class="sticky top-0 z-10 border-b border-slate-200/80 bg-white/80 backdrop-blur-md">
<div class="flex h-16 items-center justify-between px-4 sm:px-6 lg:px-8">
<div class="flex items-center gap-4">
<div class="flex min-w-0 items-center gap-4">
<Link
:href="route('supervisor.dashboard')"
class="text-lg font-semibold text-gray-800 lg:hidden"
class="text-lg font-semibold text-slate-800 lg:hidden"
>
Laverie
</Link>
<h1 v-if="title" class="text-lg font-semibold text-gray-800">
{{ title }}
</h1>
<div v-if="title" class="min-w-0">
<h1 class="truncate text-lg font-semibold text-slate-900">
{{ title }}
</h1>
</div>
<slot name="header" />
</div>
<div class="flex items-center gap-4">
<span class="hidden text-sm text-gray-600 sm:inline">
{{ supervisor?.name }}
</span>
<Link
:href="route('logout')"
method="post"
as="button"
class="rounded-md bg-gray-100 px-3 py-1.5 text-sm font-medium text-gray-700 hover:bg-gray-200"
>
Déconnexion
</Link>
<div class="flex items-center gap-3">
<slot name="actions" />
<Dropdown align="right" width="64" content-classes="overflow-hidden rounded-xl bg-white py-0 shadow-xl ring-1 ring-slate-200">
<template #trigger>
<button
type="button"
class="flex h-9 w-9 items-center justify-center rounded-full bg-gradient-to-br from-indigo-500 to-violet-600 text-xs font-bold text-white shadow-sm ring-2 ring-white transition hover:opacity-90 focus:outline-none focus:ring-2 focus:ring-indigo-500/30"
:title="supervisor?.name"
>
{{ initials }}
</button>
</template>
<template #content>
<div class="border-b border-slate-100 px-5 py-4">
<p class="truncate text-base font-semibold text-slate-900">
{{ supervisor?.name }}
</p>
<p class="mt-0.5 truncate text-sm text-slate-500">
{{ supervisor?.email }}
</p>
<p class="mt-2 inline-flex rounded-full bg-indigo-50 px-2.5 py-1 text-xs font-medium text-indigo-700">
{{ roleLabels[supervisor?.role] ?? supervisor?.role }}
</p>
</div>
<Link
:href="route('logout')"
method="post"
as="button"
class="flex w-full items-center gap-2.5 px-5 py-3 text-sm font-medium text-red-600 transition hover:bg-red-50"
>
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.75">
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"
/>
</svg>
Déconnexion
</Link>
</template>
</Dropdown>
</div>
</div>
<!-- Mobile nav -->
<nav class="flex gap-1 overflow-x-auto border-t border-gray-100 px-4 py-2 lg:hidden">
<Link
v-for="item in navItems"
:key="item.route"
:href="route(item.route)"
class="whitespace-nowrap rounded-md px-3 py-1.5 text-xs font-medium"
:class="
isActive(item.route)
? 'bg-indigo-100 text-indigo-800'
: 'text-gray-600 hover:bg-gray-100'
"
>
{{ item.label }}
</Link>
<nav class="flex gap-1 overflow-x-auto border-t border-slate-100 px-3 py-2 lg:hidden">
<template v-for="group in navGroups" :key="group.label">
<Link
v-for="item in group.items"
:key="item.route"
:href="route(item.route)"
class="whitespace-nowrap rounded-lg px-3 py-1.5 text-xs font-medium transition"
:class="
isActive(item.route)
? 'bg-indigo-50 text-indigo-700 ring-1 ring-indigo-200'
: 'text-slate-600 hover:bg-slate-100'
"
>
{{ item.label }}
</Link>
</template>
</nav>
</header>
<!-- Flash message -->
<div
v-if="page.props.flash?.success"
class="mx-4 mt-4 rounded-md bg-green-50 px-4 py-3 text-sm text-green-800 sm:mx-6 lg:mx-8"
class="mx-4 mt-4 flex items-center gap-2 rounded-xl border border-green-200 bg-green-50 px-4 py-3 text-sm text-green-800 sm:mx-6 lg:mx-8"
>
<svg class="h-4 w-4 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" />
</svg>
{{ page.props.flash.success }}
</div>