updated search and fix error in template where it removed client from template when saving in edit

This commit is contained in:
Simon Pocrnjič
2025-10-05 20:42:51 +02:00
parent bab9d6561f
commit 020c8ce61b
4 changed files with 336 additions and 88 deletions
+234 -68
View File
@@ -1,91 +1,257 @@
<script setup>
import { FwbInput } from 'flowbite-vue';
import axios from 'axios';
import { debounce } from 'lodash';
import { SearchIcon } from '@/Utilities/Icons';
import { onMounted, onUnmounted, ref, watch } from 'vue';
import { Link } from '@inertiajs/vue3';
import { FwbInput } from "flowbite-vue";
import axios from "axios";
import { debounce } from "lodash";
import { SearchIcon } from "@/Utilities/Icons";
import { onMounted, onUnmounted, ref, watch } from "vue";
import { Link } from "@inertiajs/vue3";
const props = defineProps({
open: { type: Boolean, default: false },
open: { type: Boolean, default: false },
});
const emit = defineEmits(['update:open']);
const emit = defineEmits(["update:open"]);
const query = ref('');
const query = 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));
watch(
() => props.open,
(v) => {
isOpen.value = v;
if (v) focusInput();
}
);
watch(isOpen, (v) => emit("update:open", v));
const searching = debounce((value) => {
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(() => {})
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);
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);
const focusInput = () =>
setTimeout(() => inputWrap.value?.querySelector("input")?.focus(), 0);
function onKeydown(e) {
if (e.key === 'Escape') { isOpen.value = false; }
if (e.key === "Escape") {
isOpen.value = false;
}
}
onMounted(() => window.addEventListener('keydown', onKeydown));
onUnmounted(() => window.removeEventListener('keydown', onKeydown));
onMounted(() => window.addEventListener("keydown", onKeydown));
onUnmounted(() => window.removeEventListener("keydown", onKeydown));
</script>
<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>
<!-- 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>
<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>
<teleport to="body">
<transition name="fade">
<div v-if="isOpen" class="fixed inset-0 z-50">
<div
class="absolute inset-0 bg-gradient-to-br from-slate-900/60 to-slate-800/60 backdrop-blur-sm"
@click="isOpen = false"
></div>
<div
class="absolute inset-0 flex items-start justify-center p-4 pt-20 sm:pt-28"
@click.self="isOpen = false"
>
<div
class="w-full max-w-3xl rounded-2xl border border-white/10 bg-white/80 dark:bg-slate-900/85 backdrop-blur-xl shadow-2xl ring-1 ring-black/5 overflow-hidden"
role="dialog"
aria-modal="true"
>
<div
class="p-4 border-b border-slate-200/60 dark:border-slate-700/60"
ref="inputWrap"
>
<div class="relative">
<FwbInput
v-model="query"
placeholder="Išči po naročnikih ali primerih (Ctrl+K za zapiranje)"
size="md"
class="w-full [&>div]:rounded-xl"
>
<template #prefix>
<SearchIcon />
</template>
</FwbInput>
<button
v-if="query"
@click="query = ''"
class="absolute right-2 top-1/2 -translate-y-1/2 text-xs text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-200"
>
ESC
</button>
</div>
</div>
</transition>
</teleport>
<!-- no inline trigger here; AppLayout provides the button and opens this modal -->
<div
class="max-h-[65vh] overflow-y-auto scrollbar-thin scrollbar-track-transparent scrollbar-thumb-slate-300 dark:scrollbar-thumb-slate-600"
>
<div
v-if="!query"
class="p-8 text-sm text-slate-500 dark:text-slate-400 text-center space-y-2"
>
<p>Začni tipkati za iskanje.</p>
<p class="text-xs">
Namig: uporabi
<kbd
class="px-1.5 py-0.5 bg-slate-100 dark:bg-slate-700 rounded font-mono text-[10px]"
>Ctrl</kbd
>
+
<kbd
class="px-1.5 py-0.5 bg-slate-100 dark:bg-slate-700 rounded font-mono text-[10px]"
>K</kbd
>
</p>
</div>
<div v-else class="divide-y divide-slate-200/70 dark:divide-slate-700/50">
<div v-if="result.clients.length" class="py-3">
<div
class="flex items-center justify-between px-5 pb-1 text-[11px] font-semibold tracking-wide uppercase text-slate-500 dark:text-slate-400"
>
<span>Naročniki</span>
<span
class="rounded bg-slate-100 dark:bg-slate-700 text-slate-600 dark:text-slate-300 px-2 py-0.5 text-[10px]"
>{{ result.clients.length }}</span
>
</div>
<ul role="list" class="px-2 space-y-1">
<li v-for="client in result.clients" :key="client.client_uuid">
<Link
:href="route('client.show', { uuid: client.client_uuid })"
class="group flex items-center gap-3 w-full rounded-lg px-3 py-2 text-sm hover:bg-indigo-50/70 dark:hover:bg-slate-700/60 transition"
@click="isOpen = false"
>
<span
class="shrink-0 w-6 h-6 rounded bg-indigo-100 text-indigo-600 flex items-center justify-center text-[11px] font-semibold group-hover:bg-indigo-200"
>C</span
>
<span
class="text-slate-700 dark:text-slate-200 group-hover:text-slate-900 dark:group-hover:text-white"
>{{ client.full_name }}</span
>
</Link>
</li>
</ul>
</div>
<div v-if="result.client_cases.length" class="py-3">
<div
class="flex items-center justify-between px-5 pb-1 text-[11px] font-semibold tracking-wide uppercase text-slate-500 dark:text-slate-400"
>
<span>Primeri</span>
<span
class="rounded bg-slate-100 dark:bg-slate-700 text-slate-600 dark:text-slate-300 px-2 py-0.5 text-[10px]"
>{{ result.client_cases.length }}</span
>
</div>
<ul role="list" class="px-2 space-y-1">
<li
v-for="clientcase in result.client_cases"
:key="clientcase.case_uuid"
class="rounded-xl border border-slate-200/70 dark:border-slate-700/50 bg-white/70 dark:bg-slate-800/70 px-4 py-3 shadow-sm hover:shadow-md transition flex flex-col gap-1"
>
<div class="flex items-center gap-2">
<Link
:href="
route('clientCase.show', {
client_case: clientcase.case_uuid,
})
"
class="text-left font-medium hover:underline leading-tight text-slate-800 dark:text-slate-100"
@click="isOpen = false"
>
{{ clientcase.full_name }}
</Link>
<template v-if="clientcase.contract_reference">
<span
class="font-mono text-[11px] tracking-tight text-indigo-600 bg-indigo-50 border border-indigo-200 rounded px-1.5 py-0.5 whitespace-nowrap shadow-sm"
>
{{ clientcase.contract_reference }}
</span>
</template>
</div>
<div
v-if="
clientcase.contract_segments &&
clientcase.contract_segments.length
"
class="flex flex-wrap gap-1 mt-1"
>
<Link
v-for="seg in clientcase.contract_segments"
:key="seg.id || seg.name || seg"
:href="
route('clientCase.show', {
client_case: clientcase.case_uuid,
}) +
'?segment=' +
(seg.id || seg)
"
class="group/seg text-[10px] uppercase tracking-wide bg-gradient-to-br from-purple-50 to-purple-100 text-purple-700 border border-purple-200 px-1.5 py-0.5 rounded hover:from-purple-100 hover:to-purple-200 hover:border-purple-300 transition"
@click="isOpen = false"
>
{{ seg.name || seg }}
</Link>
</div>
<div
v-else-if="
clientcase.case_segments && clientcase.case_segments.length
"
class="flex flex-wrap gap-1 mt-1"
>
<Link
v-for="seg in clientcase.case_segments"
:key="seg.id || seg.name"
:href="
route('clientCase.show', {
client_case: clientcase.case_uuid,
}) +
'?segment=' +
(seg.id || seg)
"
class="text-[10px] uppercase tracking-wide bg-slate-100 text-slate-600 border border-slate-200 px-1.5 py-0.5 rounded hover:bg-slate-200 hover:text-slate-700 transition"
@click="isOpen = false"
>
{{ seg.name }}
</Link>
</div>
</li>
</ul>
</div>
<div
v-if="!result.clients.length && !result.client_cases.length"
class="p-8 text-center text-sm text-slate-500 dark:text-slate-400"
>
Ni rezultatov.
</div>
</div>
</div>
</div>
</div>
</div>
</transition>
</teleport>
</template>
<style>
.fade-enter-active, .fade-leave-active { transition: opacity .15s; }
.fade-enter-from, .fade-leave-to { opacity: 0; }
</style>
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.15s;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style>
+22
View File
@@ -0,0 +1,22 @@
<script setup>
import AppLayout from '@/Layouts/AppLayout.vue'
const props = defineProps({
example: { type: String, default: 'Demo' },
})
</script>
<template>
<AppLayout title="Testing Sandbox">
<div class="space-y-6">
<div class="prose dark:prose-invert max-w-none">
<h1 class="text-2xl font-semibold">Testing Page</h1>
<p>This page is for quick UI or component experiments. Remove or adapt as needed.</p>
</div>
<div class="rounded-lg border border-slate-200 dark:border-slate-700 bg-white/70 dark:bg-slate-800/60 p-4 shadow-sm">
<h2 class="text-sm font-semibold tracking-wide uppercase text-slate-500 dark:text-slate-400 mb-3">Example Area</h2>
<p class="text-slate-700 dark:text-slate-200 text-sm">Prop example value: <span class="font-mono">{{ props.example }}</span></p>
</div>
</div>
</AppLayout>
</template>