updated search and fix error in template where it removed client from template when saving in edit
This commit is contained in:
parent
bab9d6561f
commit
020c8ce61b
|
|
@ -294,6 +294,12 @@ public function update(Request $request, ImportTemplate $template)
|
|||
if (! empty($raw['client_uuid'] ?? null)) {
|
||||
$raw['client_id'] = Client::where('uuid', $raw['client_uuid'])->value('id');
|
||||
}
|
||||
// If template already has mappings, lock client assignment on backend as well
|
||||
// to prevent accidental clearing when client_uuid/client_id not sent.
|
||||
$hasMappings = $template->mappings()->exists();
|
||||
if ($hasMappings) {
|
||||
unset($raw['client_id'], $raw['client_uuid']);
|
||||
}
|
||||
$data = validator($raw, [
|
||||
'name' => 'required|string|max:100',
|
||||
'description' => 'nullable|string|max:255',
|
||||
|
|
@ -338,26 +344,28 @@ public function update(Request $request, ImportTemplate $template)
|
|||
}
|
||||
}
|
||||
|
||||
$template->update([
|
||||
// Finalize meta (ensure payments entities forced if enabled)
|
||||
$finalMeta = $newMeta;
|
||||
if (! empty($finalMeta['payments_import'])) {
|
||||
$finalMeta['entities'] = ['contracts', 'accounts', 'payments'];
|
||||
}
|
||||
|
||||
$update = [
|
||||
'name' => $data['name'],
|
||||
'description' => $data['description'] ?? null,
|
||||
'source_type' => $data['source_type'],
|
||||
'default_record_type' => $data['default_record_type'] ?? null,
|
||||
'client_id' => $data['client_id'] ?? null,
|
||||
// Only set client_id if explicitly present and not locked, otherwise keep existing
|
||||
'is_active' => $data['is_active'] ?? $template->is_active,
|
||||
'reactivate' => $data['reactivate'] ?? $template->reactivate,
|
||||
'sample_headers' => $data['sample_headers'] ?? $template->sample_headers,
|
||||
'meta' => (function () use ($newMeta) {
|
||||
// If payments import mode is enabled, force entities sequence in meta
|
||||
$meta = $newMeta;
|
||||
$payments = (bool) ($meta['payments_import'] ?? false);
|
||||
if ($payments) {
|
||||
$meta['entities'] = ['contracts', 'accounts', 'payments'];
|
||||
}
|
||||
|
||||
return $meta;
|
||||
})(),
|
||||
]);
|
||||
'meta' => $finalMeta,
|
||||
];
|
||||
if (! $hasMappings && array_key_exists('client_id', $data)) {
|
||||
$update['client_id'] = $data['client_id'];
|
||||
}
|
||||
// When locked, do not touch client_id (prevents clearing to null)
|
||||
$template->update($update);
|
||||
|
||||
return redirect()->route('importTemplates.edit', ['template' => $template->uuid])
|
||||
->with('success', 'Template updated');
|
||||
|
|
|
|||
|
|
@ -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
resources/js/Pages/Testing/Index.vue
Normal file
22
resources/js/Pages/Testing/Index.vue
Normal 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>
|
||||
|
|
@ -61,8 +61,10 @@
|
|||
})->name('dashboard');
|
||||
|
||||
Route::get('testing', function () {
|
||||
return Inertia::render('Testing', []);
|
||||
});
|
||||
return Inertia::render('Testing/Index', [
|
||||
'example' => 'Hello World',
|
||||
]);
|
||||
})->name('testing.index');
|
||||
|
||||
// Phone page
|
||||
Route::get('phone', [PhoneViewController::class, 'index'])->name('phone.index');
|
||||
|
|
@ -88,7 +90,7 @@
|
|||
$builder->join('client_cases', 'person.id', '=', 'client_cases.person_id')
|
||||
->leftJoin('person_addresses', 'person.id', '=', 'person_addresses.person_id')
|
||||
->leftJoin('person_phones', 'person.id', '=', 'person_phones.person_id')
|
||||
->select('person.*', 'client_cases.uuid as case_uuid')
|
||||
->select('person.*', 'client_cases.uuid as case_uuid', 'client_cases.id as case_id')
|
||||
->limit($request->input('limit'));
|
||||
})
|
||||
->get();
|
||||
|
|
@ -100,9 +102,21 @@
|
|||
$contractCases = \App\Models\Contract::query()
|
||||
->join('client_cases', 'contracts.client_case_id', '=', 'client_cases.id')
|
||||
->join('person', 'client_cases.person_id', '=', 'person.id')
|
||||
// portable case-insensitive match across drivers
|
||||
->leftJoin('contract_segment', function ($j) {
|
||||
$j->on('contract_segment.contract_id', '=', 'contracts.id')
|
||||
->where('contract_segment.active', true);
|
||||
})
|
||||
->leftJoin('segments', 'segments.id', '=', 'contract_segment.segment_id')
|
||||
// case-insensitive reference match
|
||||
->whereRaw('LOWER(contracts.reference) LIKE ?', ['%'.mb_strtolower($query).'%'])
|
||||
->select('person.*', 'client_cases.uuid as case_uuid')
|
||||
->select(
|
||||
'person.*',
|
||||
'client_cases.uuid as case_uuid',
|
||||
'client_cases.id as case_id',
|
||||
'contracts.reference as contract_reference',
|
||||
\DB::raw("COALESCE(json_agg(DISTINCT jsonb_build_object('id', segments.id, 'name', segments.name)) FILTER (WHERE segments.id IS NOT NULL), '[]') as contract_segments")
|
||||
)
|
||||
->groupBy('person.id', 'client_cases.uuid', 'client_cases.id', 'contracts.reference')
|
||||
->limit($limit)
|
||||
->get();
|
||||
|
||||
|
|
@ -110,8 +124,46 @@
|
|||
$clientCases = $clientCases
|
||||
->concat($contractCases)
|
||||
->unique('case_uuid')
|
||||
->values()
|
||||
->take($limit);
|
||||
->values();
|
||||
|
||||
// Collect all case ids for segment lookup (for non-contract matches)
|
||||
$caseIds = $clientCases->pluck('case_id')->filter()->unique()->values();
|
||||
if ($caseIds->isNotEmpty()) {
|
||||
$caseSegments = \DB::table('client_cases')
|
||||
->join('contracts', 'contracts.client_case_id', '=', 'client_cases.id')
|
||||
->join('contract_segment', function ($j) {
|
||||
$j->on('contract_segment.contract_id', '=', 'contracts.id')
|
||||
->where('contract_segment.active', true);
|
||||
})
|
||||
->join('segments', 'segments.id', '=', 'contract_segment.segment_id')
|
||||
->whereIn('client_cases.id', $caseIds)
|
||||
->select(
|
||||
'client_cases.id as case_id',
|
||||
\DB::raw("COALESCE(json_agg(DISTINCT jsonb_build_object('id', segments.id, 'name', segments.name)) FILTER (WHERE segments.id IS NOT NULL), '[]') as segments_json")
|
||||
)
|
||||
->groupBy('client_cases.id')
|
||||
->get()
|
||||
->keyBy('case_id');
|
||||
|
||||
$clientCases = $clientCases->map(function ($row) use ($contractCases, $caseSegments) {
|
||||
$contractHit = $contractCases->firstWhere('case_uuid', $row->case_uuid);
|
||||
if ($contractHit) {
|
||||
$row->contract_reference = $contractHit->contract_reference;
|
||||
$segmentsJson = $contractHit->contract_segments ?? '[]';
|
||||
$row->contract_segments = is_string($segmentsJson) ? json_decode($segmentsJson, true) : (array) $segmentsJson;
|
||||
} else {
|
||||
$segRow = $caseSegments->get($row->case_id);
|
||||
if ($segRow) {
|
||||
$row->case_segments = json_decode($segRow->segments_json, true) ?? [];
|
||||
} else {
|
||||
$row->case_segments = [];
|
||||
}
|
||||
}
|
||||
return $row;
|
||||
})->take($limit);
|
||||
} else {
|
||||
$clientCases = $clientCases->take($limit);
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user