Mager updated
This commit is contained in:
+246
-209
@@ -1,12 +1,10 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { onMounted, onUnmounted, ref, watch, computed } from 'vue';
|
||||
import { Head, Link, router, usePage } from '@inertiajs/vue3';
|
||||
import ApplicationMark from '@/Components/ApplicationMark.vue';
|
||||
import Banner from '@/Components/Banner.vue';
|
||||
import Dropdown from '@/Components/Dropdown.vue';
|
||||
import DropdownLink from '@/Components/DropdownLink.vue';
|
||||
import NavLink from '@/Components/NavLink.vue';
|
||||
import ResponsiveNavLink from '@/Components/ResponsiveNavLink.vue';
|
||||
import Breadcrumbs from '@/Components/Breadcrumbs.vue';
|
||||
import GlobalSearch from './Partials/GlobalSearch.vue';
|
||||
|
||||
@@ -14,13 +12,100 @@ const props = defineProps({
|
||||
title: String,
|
||||
});
|
||||
|
||||
// Collapsible sidebar state (persisted when user explicitly toggles)
|
||||
const sidebarCollapsed = ref(false);
|
||||
const hasSavedSidebarPref = ref(false);
|
||||
// Mobile off-canvas state
|
||||
const isMobile = ref(false);
|
||||
const mobileSidebarOpen = ref(false);
|
||||
function applyAutoCollapse() {
|
||||
if (typeof window === 'undefined') return;
|
||||
isMobile.value = window.innerWidth < 1024; // Tailwind lg breakpoint
|
||||
sidebarCollapsed.value = isMobile.value;
|
||||
}
|
||||
function handleResize() {
|
||||
if (typeof window !== 'undefined') {
|
||||
isMobile.value = window.innerWidth < 1024;
|
||||
if (!isMobile.value) mobileSidebarOpen.value = false; // close drawer when switching to desktop
|
||||
}
|
||||
if (!hasSavedSidebarPref.value) applyAutoCollapse();
|
||||
}
|
||||
onMounted(() => {
|
||||
try {
|
||||
const saved = localStorage.getItem('sidebarCollapsed');
|
||||
if (saved !== null) {
|
||||
hasSavedSidebarPref.value = true;
|
||||
sidebarCollapsed.value = saved === '1';
|
||||
} else {
|
||||
applyAutoCollapse();
|
||||
}
|
||||
} catch {}
|
||||
window.addEventListener('resize', handleResize);
|
||||
});
|
||||
onUnmounted(() => window.removeEventListener('resize', handleResize));
|
||||
watch(sidebarCollapsed, (v) => {
|
||||
if (!hasSavedSidebarPref.value) return; // don't persist auto behavior
|
||||
try { localStorage.setItem('sidebarCollapsed', v ? '1' : '0'); } catch {}
|
||||
});
|
||||
|
||||
const showingNavigationDropdown = ref(false);
|
||||
// Global search modal state
|
||||
const searchOpen = ref(false);
|
||||
const openSearch = () => (searchOpen.value = true);
|
||||
const closeSearch = () => (searchOpen.value = false);
|
||||
|
||||
// Keyboard shortcut: Ctrl+K / Cmd+K to open search
|
||||
function onKeydown(e) {
|
||||
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'k') {
|
||||
e.preventDefault();
|
||||
openSearch();
|
||||
}
|
||||
if (e.key === 'Escape' && mobileSidebarOpen.value) {
|
||||
mobileSidebarOpen.value = false;
|
||||
}
|
||||
}
|
||||
onMounted(() => window.addEventListener('keydown', onKeydown));
|
||||
onUnmounted(() => window.removeEventListener('keydown', onKeydown));
|
||||
|
||||
function toggleSidebar() {
|
||||
hasSavedSidebarPref.value = true; // user explicitly chose
|
||||
sidebarCollapsed.value = !sidebarCollapsed.value;
|
||||
}
|
||||
|
||||
function toggleMobileSidebar() {
|
||||
mobileSidebarOpen.value = !mobileSidebarOpen.value;
|
||||
}
|
||||
|
||||
function handleSidebarToggleClick() {
|
||||
if (isMobile.value) toggleMobileSidebar();
|
||||
else toggleSidebar();
|
||||
}
|
||||
|
||||
const logout = () => {
|
||||
router.post(route('logout'));
|
||||
};
|
||||
|
||||
// Flash toast notifications
|
||||
const page = usePage();
|
||||
const flash = computed(() => page.props.flash || {});
|
||||
const showToast = ref(false);
|
||||
const toastMessage = ref('');
|
||||
const toastType = ref('success');
|
||||
watch(
|
||||
() => [flash.value.success, flash.value.error, flash.value.warning, flash.value.info],
|
||||
([s, e, w, i]) => {
|
||||
const message = s || e || w || i;
|
||||
const type = s ? 'success' : e ? 'error' : w ? 'warning' : i ? 'info' : null;
|
||||
if (message && type) {
|
||||
toastMessage.value = message;
|
||||
toastType.value = type;
|
||||
showToast.value = true;
|
||||
// auto-hide after 3s
|
||||
setTimeout(() => (showToast.value = false), 3000);
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -29,228 +114,180 @@ const logout = () => {
|
||||
|
||||
<Banner />
|
||||
|
||||
<div class="min-h-screen bg-gray-100">
|
||||
<nav class="bg-white border-b border-gray-100">
|
||||
<!-- Primary Navigation Menu -->
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex justify-between h-16">
|
||||
<div class="flex">
|
||||
<!-- Logo -->
|
||||
<div class="shrink-0 flex items-center">
|
||||
<Link :href="route('dashboard')">
|
||||
<ApplicationMark class="block h-9 w-auto" />
|
||||
</Link>
|
||||
</div>
|
||||
<div class="min-h-screen bg-gray-100 flex">
|
||||
<!-- Mobile backdrop -->
|
||||
<div v-if="isMobile && mobileSidebarOpen" class="fixed inset-0 z-40 bg-black/30" @click="mobileSidebarOpen=false"></div>
|
||||
|
||||
<!-- Navigation Links -->
|
||||
<div class="hidden space-x-8 sm:-my-px sm:ms-10 sm:flex">
|
||||
<NavLink :href="route('dashboard')" :active="route().current('dashboard')">
|
||||
Nadzorna plošča
|
||||
</NavLink>
|
||||
</div>
|
||||
|
||||
<div class="hidden space-x-8 sm:-my-px sm:ms-10 sm:flex">
|
||||
<NavLink :href="route('client')" :active="route().current('client') || route().current('client.*')">
|
||||
Naročniki
|
||||
</NavLink>
|
||||
</div>
|
||||
<div class="hidden space-x-8 sm:-my-px sm:ms-10 sm:flex">
|
||||
<NavLink :href="route('clientCase')" :active="route().current('clientCase') || route().current('clientCase.*')">
|
||||
Primeri
|
||||
</NavLink>
|
||||
</div>
|
||||
<div class="hidden space-x-8 sm:-my-px sm:ms-10 sm:flex">
|
||||
<NavLink :href="route('settings')" :active="route().current('settings') || route().current('settings.*')">
|
||||
Nastavitve
|
||||
</NavLink>
|
||||
</div>
|
||||
<div class="hidden space-x-8 sm:-my-px sm:items-center sm:ms-10 sm:flex">
|
||||
<GlobalSearch />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hidden sm:flex sm:items-center sm:ms-6">
|
||||
<!-- Settings Dropdown -->
|
||||
|
||||
<div class="ms-3 relative">
|
||||
<Dropdown align="right" width="48">
|
||||
<template #trigger>
|
||||
<button v-if="$page.props.jetstream.managesProfilePhotos" class="flex text-sm border-2 border-transparent rounded-full focus:outline-none focus:border-gray-300 transition">
|
||||
<img class="h-8 w-8 rounded-full object-cover" :src="$page.props.auth.user.profile_photo_url" :alt="$page.props.auth.user.name">
|
||||
</button>
|
||||
|
||||
<span v-else class="inline-flex rounded-md">
|
||||
<button type="button" class="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-gray-500 bg-white hover:text-gray-700 focus:outline-none focus:bg-gray-50 active:bg-gray-50 transition ease-in-out duration-150">
|
||||
{{ $page.props.auth.user.name }}
|
||||
<svg class="ms-2 -me-0.5 h-4 w-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5" />
|
||||
</svg>
|
||||
</button>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<template #content>
|
||||
<!-- Account Management -->
|
||||
<div class="block px-4 py-2 text-xs text-gray-400">
|
||||
Nastavitve računa
|
||||
</div>
|
||||
|
||||
<DropdownLink :href="route('profile.show')">
|
||||
Profil
|
||||
</DropdownLink>
|
||||
|
||||
<DropdownLink v-if="$page.props.jetstream.hasApiFeatures" :href="route('api-tokens.index')">
|
||||
API Tokens
|
||||
</DropdownLink>
|
||||
|
||||
<div class="border-t border-gray-200" />
|
||||
|
||||
<!-- Authentication -->
|
||||
<form @submit.prevent="logout">
|
||||
<DropdownLink as="button">
|
||||
Izpis
|
||||
</DropdownLink>
|
||||
</form>
|
||||
</template>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Hamburger -->
|
||||
<div class="-me-2 flex items-center sm:hidden">
|
||||
<button class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-gray-500 hover:bg-gray-100 focus:outline-none focus:bg-gray-100 focus:text-gray-500 transition duration-150 ease-in-out" @click="showingNavigationDropdown = ! showingNavigationDropdown">
|
||||
<svg
|
||||
class="h-6 w-6"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
:class="{'hidden': showingNavigationDropdown, 'inline-flex': ! showingNavigationDropdown }"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M4 6h16M4 12h16M4 18h16"
|
||||
/>
|
||||
<path
|
||||
:class="{'hidden': ! showingNavigationDropdown, 'inline-flex': showingNavigationDropdown }"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Sidebar -->
|
||||
<aside :class="[
|
||||
sidebarCollapsed ? 'w-16' : 'w-64',
|
||||
'bg-white border-r border-gray-200 transition-all duration-200 z-50',
|
||||
// Off-canvas behavior on mobile
|
||||
isMobile ? 'fixed inset-y-0 left-0 transform ' + (mobileSidebarOpen ? 'translate-x-0' : '-translate-x-full') : 'relative translate-x-0'
|
||||
]">
|
||||
<div class="h-16 px-4 flex items-center justify-between border-b">
|
||||
<Link :href="route('dashboard')" class="flex items-center gap-2">
|
||||
<ApplicationMark class="h-8 w-auto" />
|
||||
<span v-if="!sidebarCollapsed" class="text-sm font-semibold">Teren</span>
|
||||
</Link>
|
||||
</div>
|
||||
<nav class="py-4">
|
||||
<ul class="space-y-1">
|
||||
<li>
|
||||
<Link :href="route('dashboard')" :class="['flex items-center gap-3 px-4 py-2 text-sm hover:bg-gray-100', route().current('dashboard') ? 'bg-gray-100 text-gray-900' : 'text-gray-600']" title="Nadzorna plošča">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" class="w-5 h-5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M2.25 12l8.954-8.955a1.125 1.125 0 011.592 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.5c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125v4.5h4.125c.621 0 1.125-.504 1.125-1.125V9.75" />
|
||||
</svg>
|
||||
<span v-if="!sidebarCollapsed">Nadzorna plošča</span>
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link :href="route('client')" :class="['flex items-center gap-3 px-4 py-2 text-sm hover:bg-gray-100', route().current('client') || route().current('client.*') ? 'bg-gray-100 text-gray-900' : 'text-gray-600']" title="Naročniki">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" class="w-5 h-5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15 19.128a9.38 9.38 0 003.745-.479 3.375 3.375 0 00-6.49-1.072M15 19.128V18a4.5 4.5 0 00-4.5-4.5H8.25A4.5 4.5 0 003.75 18v1.128M15 19.128V21m0-1.872V21M6.75 7.5a3 3 0 116 0 3 3 0 01-6 0z" />
|
||||
</svg>
|
||||
<span v-if="!sidebarCollapsed">Naročniki</span>
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link :href="route('clientCase')" :class="['flex items-center gap-3 px-4 py-2 text-sm hover:bg-gray-100', route().current('clientCase') || route().current('clientCase.*') ? 'bg-gray-100 text-gray-900' : 'text-gray-600']" title="Primeri">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" class="w-5 h-5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-6a2.25 2.25 0 00-2.25-2.25H8.25A2.25 2.25 0 006 8.25v7.5A2.25 2.25 0 008.25 18h9a2.25 2.25 0 002.25-2.25z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M9 9h6m-6 3h6m-6 3h3" />
|
||||
</svg>
|
||||
<span v-if="!sidebarCollapsed">Primeri</span>
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link :href="route('imports.index')" :class="['flex items-center gap-3 px-4 py-2 text-sm hover:bg-gray-100', (route().current('imports.index') || route().current('imports.*')) ? 'bg-gray-100 text-gray-900' : 'text-gray-600']" title="Uvozi">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" class="w-5 h-5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M7.5 10.5L12 6l4.5 4.5M12 6v12" />
|
||||
</svg>
|
||||
<span v-if="!sidebarCollapsed">Uvozi</span>
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link :href="route('importTemplates.index')" :class="['flex items-center gap-3 px-4 py-2 text-sm hover:bg-gray-100', route().current('importTemplates.index') ? 'bg-gray-100 text-gray-900' : 'text-gray-600']" title="Uvozne predloge">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" class="w-5 h-5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 4.5h5.25l1.5 2.25H20.25A1.5 1.5 0 0121.75 8.25v9A2.25 2.25 0 0119.5 19.5H4.5A2.25 2.25 0 012.25 17.25V6A1.5 1.5 0 013.75 4.5z" />
|
||||
</svg>
|
||||
<span v-if="!sidebarCollapsed">Uvozne predloge</span>
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link :href="route('importTemplates.create')" :class="['flex items-center gap-3 px-4 py-2 text-sm hover:bg-gray-100', route().current('importTemplates.create') ? 'bg-gray-100 text-gray-900' : 'text-gray-600']" title="Nova uvozna predloga">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" class="w-5 h-5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
|
||||
</svg>
|
||||
<span v-if="!sidebarCollapsed">Nova uvozna predloga</span>
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link :href="route('settings')" :class="['flex items-center gap-3 px-4 py-2 text-sm hover:bg-gray-100', route().current('settings') || route().current('settings.*') ? 'bg-gray-100 text-gray-900' : 'text-gray-600']" title="Nastavitve">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" class="w-5 h-5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M10.343 3.94c.09-.542.56-.94 1.11-.94h1.093c.55 0 1.02.398 1.11.94l.149.894c.07.424.384.764.78.93l.8.334c.486.203.682.78.4 1.223l-.5.805c-.214.343-.17.784.108 1.09l.596.654c.36.395.37 1.002.024 1.41l-.657.76c-.285.33-.347.79-.158 1.182l.3.65c.216.468-.02 1.02-.507 1.21l-.89.345c-.4.155-.68.52-.74.94l-.12.89c-.08.55-.54.96-1.09.96h-1.09c-.55 0-1.01-.41-1.09-.96l-.12-.89c-.06-.42-.34-.785-.74-.94l-.89-.345c-.49-.19-.72-.74-.507-1.21l.3-.65c.19-.392.127-.852-.158-1.182l-.657-.76a1.125 1.125 0 01.033-1.58l.596-.654c.278-.306.322-.747.108-1.09l-.5-.805c-.282-.443-.086-1.02.4-1.223l.8-.334c.396-.166.71-.506.78-.93l.149-.894zM12 15.75a3.75 3.75 0 100-7.5 3.75 3.75 0 000 7.5z" />
|
||||
</svg>
|
||||
<span v-if="!sidebarCollapsed">Nastavitve</span>
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<!-- Responsive Navigation Menu -->
|
||||
<div :class="{'block': showingNavigationDropdown, 'hidden': ! showingNavigationDropdown}" class="sm:hidden">
|
||||
<div class="pt-2 pb-3 space-y-1">
|
||||
<ResponsiveNavLink :href="route('dashboard')" :active="route().current('dashboard')">
|
||||
Nadzorna plošča
|
||||
</ResponsiveNavLink>
|
||||
<ResponsiveNavLink :href="route('client')" :active="route().current('client')">
|
||||
Naročniki
|
||||
</ResponsiveNavLink>
|
||||
<ResponsiveNavLink :href="route('clientCase')" :active="route().current('clientCase')">
|
||||
Primeri
|
||||
</ResponsiveNavLink>
|
||||
<ResponsiveNavLink :href="route('settings')" :active="route().current('settings')">
|
||||
Nastavitve
|
||||
</ResponsiveNavLink>
|
||||
<!-- Main column -->
|
||||
<div class="flex-1 flex flex-col min-w-0">
|
||||
<!-- Top bar -->
|
||||
<div class="h-16 bg-white border-b border-gray-100 px-4 flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<!-- Sidebar toggle -->
|
||||
<button
|
||||
@click="handleSidebarToggleClick()"
|
||||
class="inline-flex items-center justify-center w-9 h-9 rounded-md text-gray-500 hover:text-gray-700 hover:bg-gray-100"
|
||||
:title="sidebarCollapsed ? 'Razširi meni' : 'Skrči meni'"
|
||||
aria-label="Toggle sidebar"
|
||||
>
|
||||
<!-- Hamburger (Bars) icon -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" />
|
||||
</svg>
|
||||
</button>
|
||||
<!-- Search trigger -->
|
||||
<button @click="openSearch" class="inline-flex items-center gap-2 px-3 py-2 text-sm rounded-md border border-gray-200 text-gray-500 hover:text-gray-700 hover:border-gray-300">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-4.35-4.35m0 0A7.5 7.5 0 1010.5 18.5a7.5 7.5 0 006.15-1.85z" />
|
||||
</svg>
|
||||
<span class="hidden sm:inline">Globalni iskalnik</span>
|
||||
<kbd class="hidden sm:inline ml-2 text-[10px] px-1.5 py-0.5 rounded border bg-gray-50">Ctrl K</kbd>
|
||||
</button>
|
||||
</div>
|
||||
<!-- Responsive Settings Options -->
|
||||
<div class="pt-4 pb-1 border-t border-gray-200">
|
||||
<div class="flex items-center px-4">
|
||||
<div v-if="$page.props.jetstream.managesProfilePhotos" class="shrink-0 me-3">
|
||||
<img class="h-10 w-10 rounded-full object-cover" :src="$page.props.auth.user.profile_photo_url" :alt="$page.props.auth.user.name">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="font-medium text-base text-gray-800">
|
||||
{{ $page.props.auth.user.name }}
|
||||
</div>
|
||||
<div class="font-medium text-sm text-gray-500">
|
||||
{{ $page.props.auth.user.email }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<div class="ms-3 relative">
|
||||
<Dropdown align="right" width="48">
|
||||
<template #trigger>
|
||||
<button v-if="$page.props.jetstream.managesProfilePhotos" class="flex text-sm border-2 border-transparent rounded-full focus:outline-none focus:border-gray-300 transition">
|
||||
<img class="h-8 w-8 rounded-full object-cover" :src="$page.props.auth.user.profile_photo_url" :alt="$page.props.auth.user.name">
|
||||
</button>
|
||||
|
||||
<div class="mt-3 space-y-1">
|
||||
<ResponsiveNavLink :href="route('profile.show')" :active="route().current('profile.show')">
|
||||
Profil
|
||||
</ResponsiveNavLink>
|
||||
<span v-else class="inline-flex rounded-md">
|
||||
<button type="button" class="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-gray-500 bg-white hover:text-gray-700 focus:outline-none focus:bg-gray-50 active:bg-gray-50 transition ease-in-out duration-150">
|
||||
{{ $page.props.auth.user.name }}
|
||||
<svg class="ms-2 -me-0.5 h-4 w-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5" />
|
||||
</svg>
|
||||
</button>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<ResponsiveNavLink v-if="$page.props.jetstream.hasApiFeatures" :href="route('api-tokens.index')" :active="route().current('api-tokens.index')">
|
||||
API Tokens
|
||||
</ResponsiveNavLink>
|
||||
<template #content>
|
||||
<div class="block px-4 py-2 text-xs text-gray-400">Nastavitve računa</div>
|
||||
|
||||
<!-- Authentication -->
|
||||
<form method="POST" @submit.prevent="logout">
|
||||
<ResponsiveNavLink as="button">
|
||||
Izpis
|
||||
</ResponsiveNavLink>
|
||||
</form>
|
||||
<DropdownLink :href="route('profile.show')">Profil</DropdownLink>
|
||||
<DropdownLink v-if="$page.props.jetstream.hasApiFeatures" :href="route('api-tokens.index')">API Tokens</DropdownLink>
|
||||
|
||||
<!-- Team Management -->
|
||||
<template v-if="$page.props.jetstream.hasTeamFeatures">
|
||||
<div class="border-t border-gray-200" />
|
||||
|
||||
<div class="block px-4 py-2 text-xs text-gray-400">
|
||||
Manage Team
|
||||
</div>
|
||||
|
||||
<!-- Team Settings -->
|
||||
<ResponsiveNavLink :href="route('teams.show', $page.props.auth.user.current_team)" :active="route().current('teams.show')">
|
||||
Team Settings
|
||||
</ResponsiveNavLink>
|
||||
|
||||
<ResponsiveNavLink v-if="$page.props.jetstream.canCreateTeams" :href="route('teams.create')" :active="route().current('teams.create')">
|
||||
Create New Team
|
||||
</ResponsiveNavLink>
|
||||
|
||||
<!-- Team Switcher -->
|
||||
<template v-if="$page.props.auth.user.all_teams.length > 1">
|
||||
<div class="border-t border-gray-200" />
|
||||
|
||||
<div class="block px-4 py-2 text-xs text-gray-400">
|
||||
Switch Teams
|
||||
</div>
|
||||
|
||||
<template v-for="team in $page.props.auth.user.all_teams" :key="team.id">
|
||||
<form @submit.prevent="switchToTeam(team)">
|
||||
<ResponsiveNavLink as="button">
|
||||
<div class="flex items-center">
|
||||
<svg v-if="team.id == $page.props.auth.user.current_team_id" class="me-2 h-5 w-5 text-green-400" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<div>{{ team.name }}</div>
|
||||
</div>
|
||||
</ResponsiveNavLink>
|
||||
</form>
|
||||
</template>
|
||||
<form @submit.prevent="logout">
|
||||
<DropdownLink as="button">Izpis</DropdownLink>
|
||||
</form>
|
||||
</template>
|
||||
</template>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Page Heading -->
|
||||
<header v-if="$slots.header" class="bg-white shadow">
|
||||
<div class="max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8">
|
||||
<Breadcrumbs :breadcrumbs="$page.props.breadcrumbs"></Breadcrumbs>
|
||||
|
||||
</div>
|
||||
|
||||
</header>
|
||||
<!-- Page Heading -->
|
||||
<header v-if="$slots.header" class="bg-white border-b shadow-sm">
|
||||
<div class="max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8 space-y-2">
|
||||
<Breadcrumbs v-if="$page.props.breadcrumbs && $page.props.breadcrumbs.length" :breadcrumbs="$page.props.breadcrumbs" />
|
||||
<slot name="header" />
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Page Content -->
|
||||
<main>
|
||||
<slot />
|
||||
</main>
|
||||
<!-- Page Content -->
|
||||
<main class="p-4">
|
||||
<slot />
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Global Search Modal -->
|
||||
<GlobalSearch :open="searchOpen" @update:open="(v)=>searchOpen=v" />
|
||||
|
||||
<!-- Simple Toast -->
|
||||
<transition name="fade">
|
||||
<div
|
||||
v-if="showToast"
|
||||
class="fixed bottom-4 right-4 z-[100] px-4 py-3 rounded shadow-lg text-white"
|
||||
:class="{
|
||||
'bg-emerald-600': toastType==='success',
|
||||
'bg-red-600': toastType==='error',
|
||||
'bg-amber-500': toastType==='warning',
|
||||
'bg-blue-600': toastType==='info',
|
||||
}"
|
||||
>
|
||||
{{ toastMessage }}
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,72 +1,91 @@
|
||||
<script setup>
|
||||
import { FwbInput, FwbListGroup, FwbListGroupItem } from 'flowbite-vue';
|
||||
import { FwbInput } from 'flowbite-vue';
|
||||
import axios from 'axios';
|
||||
import { debounce } from 'lodash';
|
||||
import { SearchIcon } from '@/Utilities/Icons';
|
||||
import { ref, watch } from 'vue';
|
||||
import Dropdown from '@/Components/Dropdown.vue';
|
||||
import DropdownLink from '@/Components/DropdownLink.vue';
|
||||
import { onMounted, onUnmounted, ref, watch } from 'vue';
|
||||
import { Link } from '@inertiajs/vue3';
|
||||
|
||||
const props = defineProps({
|
||||
css: String
|
||||
open: { type: Boolean, default: false },
|
||||
});
|
||||
const emit = defineEmits(['update:open']);
|
||||
|
||||
const query = ref('');
|
||||
const result = ref([]);
|
||||
const result = ref({ clients: [], client_cases: [] });
|
||||
const isOpen = ref(props.open);
|
||||
|
||||
watch(() => props.open, (v) => { isOpen.value = v; if (v) focusInput(); });
|
||||
watch(isOpen, (v) => emit('update:open', v));
|
||||
|
||||
const searching = debounce((value) => {
|
||||
axios.get(
|
||||
route('search'),
|
||||
{
|
||||
params: {
|
||||
query: value,
|
||||
limit: 5,
|
||||
tag: ''
|
||||
}
|
||||
}
|
||||
)
|
||||
.then(function(res) {
|
||||
result.value = res.data
|
||||
list.value = false;
|
||||
console.log(res);
|
||||
})
|
||||
.catch(function(error){
|
||||
console.log(error)
|
||||
})
|
||||
.finally(function(){
|
||||
if (!value || !value.trim()) { result.value = { clients: [], client_cases: [] }; return; }
|
||||
axios.get(route('search'), { params: { query: value, limit: 8, tag: '' } })
|
||||
.then(res => { result.value = res.data; })
|
||||
.catch(() => {})
|
||||
}, 250);
|
||||
|
||||
});
|
||||
}, 300);
|
||||
|
||||
watch(
|
||||
() => query.value,
|
||||
(val) => searching(val)
|
||||
);
|
||||
watch(() => query.value, (val) => searching(val));
|
||||
|
||||
const inputWrap = ref(null);
|
||||
const focusInput = () => setTimeout(() => inputWrap.value?.querySelector('input')?.focus(), 0);
|
||||
|
||||
function onKeydown(e) {
|
||||
if (e.key === 'Escape') { isOpen.value = false; }
|
||||
}
|
||||
onMounted(() => window.addEventListener('keydown', onKeydown));
|
||||
onUnmounted(() => window.removeEventListener('keydown', onKeydown));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Dropdown align="left" :contentClasses="['py-1 bg-white lg:w-60']">
|
||||
<template #trigger>
|
||||
<fwb-input
|
||||
v-model="query"
|
||||
placeholder="Iskalnik..."
|
||||
size="sm"
|
||||
class="lg:w-60"
|
||||
>
|
||||
<template #prefix>
|
||||
<SearchIcon />
|
||||
</template>
|
||||
<teleport to="body">
|
||||
<transition name="fade">
|
||||
<div v-if="isOpen" class="fixed inset-0 z-50">
|
||||
<!-- Backdrop -->
|
||||
<div class="absolute inset-0 bg-black/30" @click="isOpen = false"></div>
|
||||
|
||||
</fwb-input>
|
||||
</template>
|
||||
<template #content>
|
||||
<div class="block px-4 py-2 text-xs text-gray-400">Naročnik</div>
|
||||
<!-- Dialog (click outside closes) -->
|
||||
<div class="absolute inset-0 flex items-start sm:items-start justify-center p-4 pt-8 sm:pt-16" @click.self="isOpen = false">
|
||||
<div class="w-full max-w-2xl bg-white rounded-lg shadow-xl overflow-hidden">
|
||||
<div class="p-3 border-b" ref="inputWrap">
|
||||
<FwbInput v-model="query" placeholder="Išči po naročnikih in primerih..." size="md" class="w-full">
|
||||
<template #prefix>
|
||||
<SearchIcon />
|
||||
</template>
|
||||
</FwbInput>
|
||||
</div>
|
||||
<div class="max-h-[60vh] overflow-auto">
|
||||
<div v-if="!query" class="p-6 text-sm text-gray-500">Začni tipkati za iskanje. Namig: pritisni Ctrl+K kjerkoli.</div>
|
||||
<div v-else>
|
||||
<div class="px-4 py-2 text-xs text-gray-500">Naročniki</div>
|
||||
<ul>
|
||||
<li v-for="client in result.clients" :key="client.client_uuid">
|
||||
<Link :href="route('client.show', {uuid: client.client_uuid})" class="block px-4 py-2 hover:bg-gray-50" @click="isOpen=false">
|
||||
{{ client.full_name }}
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<DropdownLink v-for="client in result.clients" :href="route('client.show', {uuid: client.client_uuid})">{{ client.full_name }}</DropdownLink>
|
||||
<div class="border-t border-gray-200" />
|
||||
<div class="block px-4 py-2 text-xs text-gray-400">Cases</div>
|
||||
<DropdownLink v-for="clientcase in result.client_cases" :href="route('clientCase.show', {uuid: clientcase.case_uuid})">{{ clientcase.full_name }}</DropdownLink>
|
||||
</template>
|
||||
</Dropdown>
|
||||
</template>
|
||||
<div class="px-4 py-2 mt-2 text-xs text-gray-500">Primeri</div>
|
||||
<ul>
|
||||
<li v-for="clientcase in result.client_cases" :key="clientcase.case_uuid">
|
||||
<Link :href="route('clientCase.show', {uuid: clientcase.case_uuid})" class="block px-4 py-2 hover:bg-gray-50" @click="isOpen=false">
|
||||
{{ clientcase.full_name }}
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</teleport>
|
||||
|
||||
<!-- no inline trigger here; AppLayout provides the button and opens this modal -->
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.fade-enter-active, .fade-leave-active { transition: opacity .15s; }
|
||||
.fade-enter-from, .fade-leave-to { opacity: 0; }
|
||||
</style>
|
||||
Reference in New Issue
Block a user