327 lines
17 KiB
Vue
327 lines
17 KiB
Vue
<script setup>
|
|
import LaverieLogo from '@/Components/LaverieLogo.vue';
|
|
import Dropdown from '@/Components/Dropdown.vue';
|
|
import { Link, usePage } from '@inertiajs/vue3';
|
|
import { computed, onMounted, ref, watch } from 'vue';
|
|
|
|
defineProps({
|
|
title: {
|
|
type: String,
|
|
default: '',
|
|
},
|
|
});
|
|
|
|
const STORAGE_KEY = 'supervisor-sidebar-collapsed';
|
|
|
|
const page = usePage();
|
|
const supervisor = computed(() => page.props.auth.supervisor);
|
|
const sidebarCollapsed = ref(false);
|
|
|
|
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-slate-50">
|
|
<div class="flex min-h-screen">
|
|
<!-- Sidebar -->
|
|
<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'"
|
|
>
|
|
<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>
|
|
|
|
<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 min-w-0 flex-1 flex-col">
|
|
<!-- Header -->
|
|
<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 min-w-0 items-center gap-4">
|
|
<Link
|
|
:href="route('supervisor.dashboard')"
|
|
class="text-lg font-semibold text-slate-800 lg:hidden"
|
|
>
|
|
Laverie
|
|
</Link>
|
|
<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-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-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 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>
|
|
|
|
<!-- Page content -->
|
|
<main class="flex-1 p-4 sm:p-6 lg:p-8">
|
|
<slot />
|
|
</main>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|