Merge branch 'master' into Development
This commit is contained in:
@@ -174,7 +174,7 @@ function goToPageInput() {
|
||||
|
||||
<template>
|
||||
<div class="w-full">
|
||||
<div v-if="showToolbar" class="mb-3 flex items-center justify-between gap-3">
|
||||
<div v-if="showToolbar" class="mb-3 flex items-center gap-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<input
|
||||
type="text"
|
||||
@@ -183,7 +183,8 @@ function goToPageInput() {
|
||||
v-model="internalSearch"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<slot name="toolbar-extra" />
|
||||
<div class="ml-auto flex items-center gap-2">
|
||||
<label class="text-sm text-gray-600">Na stran</label>
|
||||
<select
|
||||
class="rounded border-gray-300 text-sm"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup>
|
||||
import { computed, onMounted, ref, watch } from "vue";
|
||||
import { usePage, Link } from "@inertiajs/vue3";
|
||||
import { usePage, Link, router } from "@inertiajs/vue3";
|
||||
import Dropdown from "@/Components/Dropdown.vue";
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
import { faBell } from "@fortawesome/free-solid-svg-icons";
|
||||
@@ -53,7 +53,7 @@ watch(
|
||||
}
|
||||
);
|
||||
|
||||
async function markRead(item) {
|
||||
function markRead(item) {
|
||||
const idx = items.value.findIndex((i) => i.id === item.id);
|
||||
if (idx === -1) {
|
||||
return;
|
||||
@@ -61,15 +61,21 @@ async function markRead(item) {
|
||||
|
||||
// Optimistically remove
|
||||
const removed = items.value.splice(idx, 1)[0];
|
||||
|
||||
try {
|
||||
await window.axios.post(route("notifications.activity.read"), {
|
||||
activity_id: item.id,
|
||||
});
|
||||
} catch (e) {
|
||||
// Rollback on failure
|
||||
items.value.splice(idx, 0, removed);
|
||||
}
|
||||
|
||||
router.patch(
|
||||
route("notifications.activity.read"),
|
||||
{ activity_id: item.id },
|
||||
{
|
||||
onSuccess: () => {
|
||||
// Item successfully marked as read
|
||||
},
|
||||
onError: () => {
|
||||
// Rollback on failure
|
||||
items.value.splice(idx, 0, removed);
|
||||
},
|
||||
preserveScroll: true
|
||||
}
|
||||
);
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -35,6 +35,15 @@ const filteredSenders = computed(() => {
|
||||
return props.senders.filter((s) => s.profile_id === form.profile_id);
|
||||
});
|
||||
|
||||
function onTemplateChange() {
|
||||
const template = props.templates.find(t => t.id === form.template_id)
|
||||
if (template?.content) {
|
||||
form.body = template.content
|
||||
} else {
|
||||
form.body = ''
|
||||
}
|
||||
}
|
||||
|
||||
function submitCreate() {
|
||||
const lines = (form.numbers || "")
|
||||
.split(/\r?\n/)
|
||||
@@ -77,71 +86,39 @@ function submitCreate() {
|
||||
}
|
||||
|
||||
// Contracts mode state & actions
|
||||
const contracts = ref({
|
||||
data: [],
|
||||
meta: { current_page: 1, last_page: 1, per_page: 25, total: 0 },
|
||||
});
|
||||
const segmentId = ref(null);
|
||||
const search = ref("");
|
||||
const clientId = ref(null);
|
||||
const onlyMobile = ref(false);
|
||||
const onlyValidated = ref(false);
|
||||
const loadingContracts = ref(false);
|
||||
const selectedContractIds = ref(new Set());
|
||||
const deletingId = ref(null);
|
||||
const creatingFromContracts = ref(false);
|
||||
|
||||
const allOnPageSelected = computed(() => {
|
||||
const ids = (contracts.value.data || []).map((c) => c.id);
|
||||
if (!ids.length) return false;
|
||||
return ids.every((id) => selectedContractIds.value.has(id));
|
||||
});
|
||||
|
||||
function pageContractIds() {
|
||||
return (contracts.value.data || []).map((c) => c.id);
|
||||
}
|
||||
|
||||
function toggleSelectAllOnPage() {
|
||||
const ids = pageContractIds();
|
||||
if (!ids.length) return;
|
||||
const set = new Set(selectedContractIds.value);
|
||||
if (allOnPageSelected.value) {
|
||||
// deselect all visible
|
||||
ids.forEach((id) => set.delete(id));
|
||||
} else {
|
||||
// select all visible
|
||||
ids.forEach((id) => set.add(id));
|
||||
}
|
||||
selectedContractIds.value = new Set(Array.from(set));
|
||||
}
|
||||
const contracts = ref({ data: [], meta: { current_page: 1, last_page: 1, per_page: 25, total: 0 } })
|
||||
const segmentId = ref(null)
|
||||
const search = ref('')
|
||||
const clientId = ref(null)
|
||||
const startDateFrom = ref('')
|
||||
const startDateTo = ref('')
|
||||
const promiseDateFrom = ref('')
|
||||
const promiseDateTo = ref('')
|
||||
const onlyMobile = ref(false)
|
||||
const onlyValidated = ref(false)
|
||||
const loadingContracts = ref(false)
|
||||
const selectedContractIds = ref(new Set())
|
||||
const perPage = ref(25)
|
||||
|
||||
async function loadContracts(url = null) {
|
||||
if (!segmentId.value) {
|
||||
contracts.value = {
|
||||
data: [],
|
||||
meta: { current_page: 1, last_page: 1, per_page: 25, total: 0 },
|
||||
};
|
||||
return;
|
||||
}
|
||||
loadingContracts.value = true;
|
||||
loadingContracts.value = true
|
||||
try {
|
||||
const target =
|
||||
url ||
|
||||
`${route("admin.packages.contracts")}?segment_id=${encodeURIComponent(
|
||||
segmentId.value
|
||||
)}${search.value ? `&q=${encodeURIComponent(search.value)}` : ""}${
|
||||
clientId.value ? `&client_id=${encodeURIComponent(clientId.value)}` : ""
|
||||
}${onlyMobile.value ? `&only_mobile=1` : ""}${
|
||||
onlyValidated.value ? `&only_validated=1` : ""
|
||||
}`;
|
||||
const res = await fetch(target, {
|
||||
headers: { "X-Requested-With": "XMLHttpRequest" },
|
||||
});
|
||||
const json = await res.json();
|
||||
contracts.value = {
|
||||
data: json.data || [],
|
||||
meta: json.meta || { current_page: 1, last_page: 1, per_page: 25, total: 0 },
|
||||
};
|
||||
const params = new URLSearchParams()
|
||||
if (segmentId.value) params.append('segment_id', segmentId.value)
|
||||
if (search.value) params.append('q', search.value)
|
||||
if (clientId.value) params.append('client_id', clientId.value)
|
||||
if (startDateFrom.value) params.append('start_date_from', startDateFrom.value)
|
||||
if (startDateTo.value) params.append('start_date_to', startDateTo.value)
|
||||
if (promiseDateFrom.value) params.append('promise_date_from', promiseDateFrom.value)
|
||||
if (promiseDateTo.value) params.append('promise_date_to', promiseDateTo.value)
|
||||
if (onlyMobile.value) params.append('only_mobile', '1')
|
||||
if (onlyValidated.value) params.append('only_validated', '1')
|
||||
params.append('per_page', perPage.value)
|
||||
|
||||
const target = url || `${route('admin.packages.contracts')}?${params.toString()}`
|
||||
const res = await fetch(target, { headers: { 'X-Requested-With': 'XMLHttpRequest' } })
|
||||
const json = await res.json()
|
||||
contracts.value = { data: json.data || [], meta: json.meta || { current_page: 1, last_page: 1, per_page: 25, total: 0 } }
|
||||
} finally {
|
||||
loadingContracts.value = false;
|
||||
}
|
||||
@@ -176,18 +153,52 @@ function deletePackage(pkg) {
|
||||
});
|
||||
}
|
||||
|
||||
function toggleSelectAll() {
|
||||
const currentPageIds = contracts.value.data.map(c => c.id)
|
||||
const allSelected = currentPageIds.every(id => selectedContractIds.value.has(id))
|
||||
|
||||
if (allSelected) {
|
||||
// Deselect all on current page
|
||||
currentPageIds.forEach(id => selectedContractIds.value.delete(id))
|
||||
} else {
|
||||
// Select all on current page
|
||||
currentPageIds.forEach(id => selectedContractIds.value.add(id))
|
||||
}
|
||||
|
||||
// Force reactivity
|
||||
selectedContractIds.value = new Set(Array.from(selectedContractIds.value))
|
||||
}
|
||||
|
||||
const allCurrentPageSelected = computed(() => {
|
||||
if (!contracts.value.data.length) return false
|
||||
return contracts.value.data.every(c => selectedContractIds.value.has(c.id))
|
||||
})
|
||||
|
||||
const someCurrentPageSelected = computed(() => {
|
||||
if (!contracts.value.data.length) return false
|
||||
return contracts.value.data.some(c => selectedContractIds.value.has(c.id)) && !allCurrentPageSelected.value
|
||||
})
|
||||
|
||||
function goContractsPage(delta) {
|
||||
const { current_page } = contracts.value.meta;
|
||||
const nextPage = current_page + delta;
|
||||
if (nextPage < 1 || nextPage > contracts.value.meta.last_page) return;
|
||||
const base = `${route("admin.packages.contracts")}?segment_id=${encodeURIComponent(
|
||||
segmentId.value
|
||||
)}${search.value ? `&q=${encodeURIComponent(search.value)}` : ""}${
|
||||
clientId.value ? `&client_id=${encodeURIComponent(clientId.value)}` : ""
|
||||
}${onlyMobile.value ? `&only_mobile=1` : ""}${
|
||||
onlyValidated.value ? `&only_validated=1` : ""
|
||||
}&page=${nextPage}`;
|
||||
loadContracts(base);
|
||||
const { current_page } = contracts.value.meta
|
||||
const nextPage = current_page + delta
|
||||
if (nextPage < 1 || nextPage > contracts.value.meta.last_page) return
|
||||
|
||||
const params = new URLSearchParams()
|
||||
if (segmentId.value) params.append('segment_id', segmentId.value)
|
||||
if (search.value) params.append('q', search.value)
|
||||
if (clientId.value) params.append('client_id', clientId.value)
|
||||
if (startDateFrom.value) params.append('start_date_from', startDateFrom.value)
|
||||
if (startDateTo.value) params.append('start_date_to', startDateTo.value)
|
||||
if (promiseDateFrom.value) params.append('promise_date_from', promiseDateFrom.value)
|
||||
if (promiseDateTo.value) params.append('promise_date_to', promiseDateTo.value)
|
||||
if (onlyMobile.value) params.append('only_mobile', '1')
|
||||
if (onlyValidated.value) params.append('only_validated', '1')
|
||||
params.append('per_page', perPage.value)
|
||||
params.append('page', nextPage)
|
||||
|
||||
const base = `${route('admin.packages.contracts')}?${params.toString()}`
|
||||
loadContracts(base)
|
||||
}
|
||||
|
||||
function submitCreateFromContracts() {
|
||||
@@ -283,10 +294,7 @@ function submitCreateFromContracts() {
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs text-gray-500 mb-1">Predloga</label>
|
||||
<select
|
||||
v-model.number="form.template_id"
|
||||
class="w-full rounded border-gray-300 text-sm"
|
||||
>
|
||||
<select v-model.number="form.template_id" @change="onTemplateChange" class="w-full rounded border-gray-300 text-sm">
|
||||
<option :value="null">—</option>
|
||||
<option v-for="t in templates" :key="t.id" :value="t.id">{{ t.name }}</option>
|
||||
</select>
|
||||
@@ -331,88 +339,134 @@ function submitCreateFromContracts() {
|
||||
|
||||
<!-- Contracts mode -->
|
||||
<template v-else>
|
||||
<div>
|
||||
<label class="block text-xs text-gray-500 mb-1">Segment</label>
|
||||
<select
|
||||
v-model.number="segmentId"
|
||||
@change="loadContracts()"
|
||||
class="w-full rounded border-gray-300 text-sm"
|
||||
>
|
||||
<option :value="null">—</option>
|
||||
<option v-for="s in segments" :key="s.id" :value="s.id">
|
||||
{{ s.name }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs text-gray-500 mb-1">Stranka</label>
|
||||
<select
|
||||
v-model.number="clientId"
|
||||
@change="loadContracts()"
|
||||
class="w-full rounded border-gray-300 text-sm"
|
||||
>
|
||||
<option :value="null">—</option>
|
||||
<option v-for="c in clients" :key="c.id" :value="c.id">{{ c.name }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="sm:col-span-2">
|
||||
<label class="block text-xs text-gray-500 mb-1">Iskanje</label>
|
||||
<div class="flex gap-2">
|
||||
<input
|
||||
v-model="search"
|
||||
@keyup.enter="loadContracts()"
|
||||
type="text"
|
||||
class="w-full rounded border-gray-300 text-sm"
|
||||
placeholder="referenca..."
|
||||
/>
|
||||
<button @click="loadContracts()" class="px-3 py-1.5 rounded border text-sm">
|
||||
Išči
|
||||
<div class="sm:col-span-3 space-y-4">
|
||||
<!-- Basic filters -->
|
||||
<div class="grid sm:grid-cols-3 gap-4">
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-700 mb-1">Segment</label>
|
||||
<select v-model.number="segmentId" @change="loadContracts()" class="w-full rounded border-gray-300 text-sm">
|
||||
<option :value="null">Vsi segmenti</option>
|
||||
<option v-for="s in segments" :key="s.id" :value="s.id">{{ s.name }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-700 mb-1">Stranka</label>
|
||||
<select v-model.number="clientId" @change="loadContracts()" class="w-full rounded border-gray-300 text-sm">
|
||||
<option :value="null">Vse stranke</option>
|
||||
<option v-for="c in clients" :key="c.id" :value="c.id">{{ c.name }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-700 mb-1">Iskanje po referenci</label>
|
||||
<input v-model="search" @keyup.enter="loadContracts()" type="text" class="w-full rounded border-gray-300 text-sm" placeholder="Vnesi referenco...">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Date range filters -->
|
||||
<div class="border-t pt-4">
|
||||
<h4 class="text-xs font-semibold text-gray-700 mb-3">Datumski filtri</h4>
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<div class="text-xs font-medium text-gray-600 mb-2">Datum začetka pogodbe</div>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<div>
|
||||
<label class="block text-xs text-gray-500 mb-1">Od</label>
|
||||
<input v-model="startDateFrom" @change="loadContracts()" type="date" class="w-full rounded border-gray-300 text-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs text-gray-500 mb-1">Do</label>
|
||||
<input v-model="startDateTo" @change="loadContracts()" type="date" class="w-full rounded border-gray-300 text-sm">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-xs font-medium text-gray-600 mb-2">Datum obljube plačila</div>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<div>
|
||||
<label class="block text-xs text-gray-500 mb-1">Od</label>
|
||||
<input v-model="promiseDateFrom" @change="loadContracts()" type="date" class="w-full rounded border-gray-300 text-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs text-gray-500 mb-1">Do</label>
|
||||
<input v-model="promiseDateTo" @change="loadContracts()" type="date" class="w-full rounded border-gray-300 text-sm">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Phone filters -->
|
||||
<div class="border-t pt-4">
|
||||
<h4 class="text-xs font-semibold text-gray-700 mb-3">Telefonski filtri</h4>
|
||||
<div class="flex items-center gap-6 text-sm text-gray-700">
|
||||
<label class="inline-flex items-center gap-2">
|
||||
<input type="checkbox" v-model="onlyMobile" @change="loadContracts()" class="rounded border-gray-300">
|
||||
Samo mobilne številke
|
||||
</label>
|
||||
<label class="inline-flex items-center gap-2">
|
||||
<input type="checkbox" v-model="onlyValidated" @change="loadContracts()" class="rounded border-gray-300">
|
||||
Samo potrjene številke
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Action buttons -->
|
||||
<div class="flex items-center gap-2 pt-2">
|
||||
<button @click="loadContracts()" class="px-4 py-2 rounded bg-indigo-600 text-white text-sm font-medium hover:bg-indigo-700">
|
||||
Išči pogodbe
|
||||
</button>
|
||||
<button
|
||||
@click="segmentId = null; clientId = null; search = ''; startDateFrom = ''; startDateTo = ''; promiseDateFrom = ''; promiseDateTo = ''; onlyMobile = false; onlyValidated = false; contracts.value = { data: [], meta: { current_page: 1, last_page: 1, per_page: 25, total: 0 } }"
|
||||
class="px-4 py-2 rounded border border-gray-300 text-gray-700 text-sm font-medium hover:bg-gray-50"
|
||||
>
|
||||
Počisti filtre
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sm:col-span-3 flex items-center gap-6 text-sm text-gray-700">
|
||||
<label class="inline-flex items-center gap-2">
|
||||
<input type="checkbox" v-model="onlyMobile" @change="loadContracts()" />
|
||||
Samo s mobilno številko
|
||||
</label>
|
||||
<label class="inline-flex items-center gap-2">
|
||||
<input type="checkbox" v-model="onlyValidated" @change="loadContracts()" />
|
||||
Telefonska številka mora biti potrjena
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Results table -->
|
||||
<div class="sm:col-span-3">
|
||||
<div class="overflow-hidden rounded border bg-white">
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead class="bg-gray-50">
|
||||
<tr class="text-xs text-gray-500">
|
||||
<th class="px-3 py-2 w-8">
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="allOnPageSelected"
|
||||
:disabled="!contracts.data?.length"
|
||||
@change="toggleSelectAllOnPage"
|
||||
title="Izberi vse na strani"
|
||||
/>
|
||||
<th class="px-3 py-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="allCurrentPageSelected"
|
||||
:indeterminate="someCurrentPageSelected"
|
||||
@change="toggleSelectAll"
|
||||
:disabled="!contracts.data.length"
|
||||
class="rounded"
|
||||
title="Izberi vse na tej strani"
|
||||
>
|
||||
</th>
|
||||
<th class="px-3 py-2 text-left">Pogodba</th>
|
||||
<th class="px-3 py-2 text-left">Primer</th>
|
||||
<th class="px-3 py-2 text-left">Stranka</th>
|
||||
<th class="px-3 py-2 text-left">Datum začetka</th>
|
||||
<th class="px-3 py-2 text-left">Zadnja obljuba</th>
|
||||
<th class="px-3 py-2 text-left">Izbrana številka</th>
|
||||
<th class="px-3 py-2 text-left">Opomba</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200" v-if="!loadingContracts">
|
||||
<tr v-for="c in contracts.data" :key="c.id" class="text-sm">
|
||||
<tr v-for="c in contracts.data" :key="c.id" class="text-sm hover:bg-gray-50">
|
||||
<td class="px-3 py-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="selectedContractIds.has(c.id)"
|
||||
@change="toggleSelectContract(c.id)"
|
||||
/>
|
||||
<input type="checkbox" :checked="selectedContractIds.has(c.id)" @change="toggleSelectContract(c.id)" class="rounded">
|
||||
</td>
|
||||
<td class="px-3 py-2">
|
||||
<div class="font-mono text-xs text-gray-600">{{ c.uuid }}</div>
|
||||
<div class="text-xs text-gray-800">{{ c.reference }}</div>
|
||||
<a
|
||||
v-if="c.case?.uuid"
|
||||
:href="route('clientCase.show', c.case.uuid)"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="text-xs font-medium text-indigo-600 hover:text-indigo-800 hover:underline"
|
||||
>
|
||||
{{ c.reference }}
|
||||
</a>
|
||||
<div v-else class="text-xs font-medium text-gray-800">{{ c.reference }}</div>
|
||||
</td>
|
||||
<td class="px-3 py-2">
|
||||
<div class="text-xs text-gray-800">
|
||||
@@ -422,6 +476,12 @@ function submitCreateFromContracts() {
|
||||
<td class="px-3 py-2">
|
||||
<div class="text-xs text-gray-800">{{ c.client?.name || "—" }}</div>
|
||||
</td>
|
||||
<td class="px-3 py-2">
|
||||
<div class="text-xs text-gray-700">{{ c.start_date ? new Date(c.start_date).toLocaleDateString('sl-SI') : '—' }}</div>
|
||||
</td>
|
||||
<td class="px-3 py-2">
|
||||
<div class="text-xs text-gray-700">{{ c.promise_date ? new Date(c.promise_date).toLocaleDateString('sl-SI') : '—' }}</div>
|
||||
</td>
|
||||
<td class="px-3 py-2">
|
||||
<div v-if="c.selected_phone" class="text-xs">
|
||||
{{ c.selected_phone.number }}
|
||||
@@ -437,55 +497,45 @@ function submitCreateFromContracts() {
|
||||
{{ c.no_phone_reason || "—" }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="!contracts.data?.length">
|
||||
<td colspan="6" class="px-3 py-8 text-center text-sm text-gray-500">
|
||||
Ni rezultatov.
|
||||
</td>
|
||||
<tr v-if="!contracts.data?.length">
|
||||
<td colspan="8" class="px-3 py-8 text-center text-sm text-gray-500">
|
||||
Ni rezultatov. Poskusite z drugimi filtri.
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody v-else>
|
||||
<tr>
|
||||
<td colspan="6" class="px-3 py-6 text-center text-sm text-gray-500">
|
||||
Nalaganje...
|
||||
</td>
|
||||
</tr>
|
||||
<tr><td colspan="8" class="px-3 py-6 text-center text-sm text-gray-500">Nalaganje...</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="mt-3 flex items-center justify-between text-sm">
|
||||
<div class="text-gray-600">
|
||||
Prikazano stran {{ contracts.meta.current_page }} od
|
||||
{{ contracts.meta.last_page }} (skupaj {{ contracts.meta.total }})
|
||||
<div class="text-gray-600 flex items-center gap-4">
|
||||
<span v-if="contracts.data.length">
|
||||
Prikazano stran {{ contracts.meta.current_page }} od {{ contracts.meta.last_page }} (skupaj {{ contracts.meta.total }})
|
||||
</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<label class="text-xs text-gray-500">Na stran:</label>
|
||||
<select v-model.number="perPage" @change="loadContracts()" class="rounded border-gray-300 text-xs py-1">
|
||||
<option :value="10">10</option>
|
||||
<option :value="25">25</option>
|
||||
<option :value="50">50</option>
|
||||
<option :value="100">100</option>
|
||||
<option :value="200">200</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-x-2">
|
||||
<button
|
||||
@click="goContractsPage(-1)"
|
||||
:disabled="contracts.meta.current_page <= 1"
|
||||
class="px-3 py-1.5 rounded border text-sm disabled:opacity-50"
|
||||
>
|
||||
Nazaj
|
||||
</button>
|
||||
<button
|
||||
@click="goContractsPage(1)"
|
||||
:disabled="contracts.meta.current_page >= contracts.meta.last_page"
|
||||
class="px-3 py-1.5 rounded border text-sm disabled:opacity-50"
|
||||
>
|
||||
Naprej
|
||||
</button>
|
||||
<button @click="goContractsPage(-1)" :disabled="contracts.meta.current_page <= 1" class="px-3 py-1.5 rounded border text-sm disabled:opacity-50 disabled:cursor-not-allowed">Nazaj</button>
|
||||
<button @click="goContractsPage(1)" :disabled="contracts.meta.current_page >= contracts.meta.last_page" class="px-3 py-1.5 rounded border text-sm disabled:opacity-50 disabled:cursor-not-allowed">Naprej</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sm:col-span-3 flex items-center justify-end gap-2">
|
||||
<div class="text-sm text-gray-600 mr-auto">
|
||||
Izbrano: {{ selectedContractIds.size }}
|
||||
<div class="sm:col-span-3 flex items-center justify-between gap-2 pt-4 border-t">
|
||||
<div class="text-sm text-gray-600">
|
||||
<span class="font-medium">Izbrano: {{ selectedContractIds.size }}</span>
|
||||
<span v-if="selectedContractIds.size > 0" class="ml-2 text-gray-500">({{ selectedContractIds.size === 1 ? '1 pogodba' : `${selectedContractIds.size} pogodb` }})</span>
|
||||
</div>
|
||||
<button
|
||||
@click="submitCreateFromContracts"
|
||||
:disabled="selectedContractIds.size === 0 || creatingFromContracts"
|
||||
class="px-3 py-1.5 rounded bg-emerald-600 text-white text-sm disabled:opacity-50"
|
||||
>
|
||||
Ustvari paket
|
||||
</button>
|
||||
<button @click="submitCreateFromContracts" :disabled="selectedContractIds.size === 0" class="px-4 py-2 rounded bg-emerald-600 text-white text-sm font-medium disabled:opacity-50 disabled:cursor-not-allowed hover:bg-emerald-700">Ustvari paket</button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
<script setup>
|
||||
import AdminLayout from "@/Layouts/AdminLayout.vue";
|
||||
import { useForm, Link } from "@inertiajs/vue3";
|
||||
import { useForm, Link, router } from "@inertiajs/vue3";
|
||||
import { ref, computed } from "vue";
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
import { faMagnifyingGlass, faFloppyDisk } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faMagnifyingGlass, faFloppyDisk, faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import DialogModal from "@/Components/DialogModal.vue";
|
||||
|
||||
const props = defineProps({
|
||||
users: Array,
|
||||
@@ -65,6 +66,49 @@ const filteredUsers = computed(() => {
|
||||
});
|
||||
|
||||
const anyDirty = computed(() => Object.values(forms).some((f) => f.dirty));
|
||||
|
||||
// Create user modal
|
||||
const showCreateModal = ref(false);
|
||||
const createForm = useForm({
|
||||
name: "",
|
||||
email: "",
|
||||
password: "",
|
||||
password_confirmation: "",
|
||||
roles: [],
|
||||
});
|
||||
|
||||
function openCreateModal() {
|
||||
createForm.reset();
|
||||
createForm.clearErrors();
|
||||
showCreateModal.value = true;
|
||||
}
|
||||
|
||||
function closeCreateModal() {
|
||||
showCreateModal.value = false;
|
||||
createForm.reset();
|
||||
}
|
||||
|
||||
function submitCreateUser() {
|
||||
createForm.post(route("admin.users.store"), {
|
||||
preserveScroll: true,
|
||||
onSuccess: () => {
|
||||
closeCreateModal();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function toggleCreateRole(roleId) {
|
||||
const exists = createForm.roles.includes(roleId);
|
||||
createForm.roles = exists
|
||||
? createForm.roles.filter((id) => id !== roleId)
|
||||
: [...createForm.roles, roleId];
|
||||
}
|
||||
|
||||
function toggleUserActive(userId) {
|
||||
router.patch(route("admin.users.toggle-active", { user: userId }), {}, {
|
||||
preserveScroll: true,
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -127,6 +171,14 @@ const anyDirty = computed(() => Object.values(forms).some((f) => f.dirty));
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<button
|
||||
type="button"
|
||||
@click="openCreateModal"
|
||||
class="inline-flex items-center gap-2 px-3 py-1.5 rounded-md text-xs font-medium bg-indigo-600 border-indigo-600 text-white hover:bg-indigo-500"
|
||||
>
|
||||
<FontAwesomeIcon :icon="faPlus" class="w-4 h-4" />
|
||||
Ustvari uporabnika
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@click="submitAll"
|
||||
@@ -151,6 +203,9 @@ const anyDirty = computed(() => Object.values(forms).some((f) => f.dirty));
|
||||
<th class="p-2 text-left font-medium text-[11px] uppercase tracking-wide">
|
||||
Uporabnik
|
||||
</th>
|
||||
<th class="p-2 text-center font-medium text-[11px] uppercase tracking-wide">
|
||||
Status
|
||||
</th>
|
||||
<th
|
||||
v-for="role in props.roles"
|
||||
:key="role.id"
|
||||
@@ -172,6 +227,7 @@ const anyDirty = computed(() => Object.values(forms).some((f) => f.dirty));
|
||||
:class="[
|
||||
'border-t border-slate-100',
|
||||
idx % 2 === 1 ? 'bg-slate-50/40' : 'bg-white',
|
||||
!user.active && 'opacity-60',
|
||||
]"
|
||||
>
|
||||
<td class="p-2 whitespace-nowrap align-top">
|
||||
@@ -191,6 +247,19 @@ const anyDirty = computed(() => Object.values(forms).some((f) => f.dirty));
|
||||
{{ user.email }}
|
||||
</div>
|
||||
</td>
|
||||
<td class="p-2 text-center align-top">
|
||||
<button
|
||||
@click="toggleUserActive(user.id)"
|
||||
class="inline-flex items-center px-2 py-1 rounded text-xs font-medium transition"
|
||||
:class="
|
||||
user.active
|
||||
? 'bg-emerald-100 text-emerald-700 hover:bg-emerald-200'
|
||||
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
|
||||
"
|
||||
>
|
||||
{{ user.active ? 'Aktiven' : 'Neaktiven' }}
|
||||
</button>
|
||||
</td>
|
||||
<td
|
||||
v-for="role in props.roles"
|
||||
:key="role.id"
|
||||
@@ -223,7 +292,7 @@ const anyDirty = computed(() => Object.values(forms).some((f) => f.dirty));
|
||||
</tr>
|
||||
<tr v-if="!filteredUsers.length">
|
||||
<td
|
||||
:colspan="props.roles.length + 2"
|
||||
:colspan="props.roles.length + 3"
|
||||
class="p-6 text-center text-sm text-gray-500"
|
||||
>
|
||||
Ni rezultatov
|
||||
@@ -265,5 +334,107 @@ const anyDirty = computed(() => Object.values(forms).some((f) => f.dirty));
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Create User Modal -->
|
||||
<DialogModal :show="showCreateModal" @close="closeCreateModal" max-width="2xl">
|
||||
<template #title>Ustvari novega uporabnika</template>
|
||||
<template #content>
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Ime</label>
|
||||
<input
|
||||
v-model="createForm.name"
|
||||
type="text"
|
||||
class="w-full rounded-md border-gray-300 focus:border-indigo-500 focus:ring-indigo-500"
|
||||
placeholder="Ime uporabnika"
|
||||
/>
|
||||
<div v-if="createForm.errors.name" class="text-red-600 text-xs mt-1">
|
||||
{{ createForm.errors.name }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">E-pošta</label>
|
||||
<input
|
||||
v-model="createForm.email"
|
||||
type="email"
|
||||
class="w-full rounded-md border-gray-300 focus:border-indigo-500 focus:ring-indigo-500"
|
||||
placeholder="uporabnik@example.com"
|
||||
/>
|
||||
<div v-if="createForm.errors.email" class="text-red-600 text-xs mt-1">
|
||||
{{ createForm.errors.email }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Geslo</label>
|
||||
<input
|
||||
v-model="createForm.password"
|
||||
type="password"
|
||||
class="w-full rounded-md border-gray-300 focus:border-indigo-500 focus:ring-indigo-500"
|
||||
placeholder="********"
|
||||
/>
|
||||
<div v-if="createForm.errors.password" class="text-red-600 text-xs mt-1">
|
||||
{{ createForm.errors.password }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Potrdi geslo</label>
|
||||
<input
|
||||
v-model="createForm.password_confirmation"
|
||||
type="password"
|
||||
class="w-full rounded-md border-gray-300 focus:border-indigo-500 focus:ring-indigo-500"
|
||||
placeholder="********"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">Vloge</label>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<label
|
||||
v-for="role in props.roles"
|
||||
:key="'create-role-' + role.id"
|
||||
class="inline-flex items-center gap-2 px-3 py-2 rounded-md border cursor-pointer transition"
|
||||
:class="
|
||||
createForm.roles.includes(role.id)
|
||||
? 'bg-indigo-50 border-indigo-600 text-indigo-700'
|
||||
: 'bg-white border-gray-300 text-gray-700 hover:bg-gray-50'
|
||||
"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
class="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
|
||||
:checked="createForm.roles.includes(role.id)"
|
||||
@change="toggleCreateRole(role.id)"
|
||||
/>
|
||||
<span class="text-sm font-medium">{{ role.name }}</span>
|
||||
</label>
|
||||
</div>
|
||||
<div v-if="createForm.errors.roles" class="text-red-600 text-xs mt-1">
|
||||
{{ createForm.errors.roles }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<button
|
||||
type="button"
|
||||
@click="closeCreateModal"
|
||||
class="px-4 py-2 rounded-md text-sm font-medium bg-white border border-gray-300 text-gray-700 hover:bg-gray-50"
|
||||
>
|
||||
Prekliči
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@click="submitCreateUser"
|
||||
:disabled="createForm.processing"
|
||||
class="ml-3 px-4 py-2 rounded-md text-sm font-medium bg-indigo-600 text-white hover:bg-indigo-500 disabled:opacity-40 disabled:cursor-not-allowed"
|
||||
>
|
||||
<span v-if="createForm.processing">Ustvarjanje...</span>
|
||||
<span v-else>Ustvari</span>
|
||||
</button>
|
||||
</template>
|
||||
</DialogModal>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,211 @@
|
||||
<script setup>
|
||||
import { ref, computed, watch } from "vue";
|
||||
import { useForm } from "@inertiajs/vue3";
|
||||
import DialogModal from "@/Components/DialogModal.vue";
|
||||
|
||||
const props = defineProps({
|
||||
show: { type: Boolean, default: false },
|
||||
contract: { type: Object, default: null },
|
||||
clientCase: { type: Object, required: true },
|
||||
});
|
||||
|
||||
const emit = defineEmits(["close"]);
|
||||
|
||||
const metaFields = ref([]);
|
||||
|
||||
// Extract meta fields when contract changes
|
||||
watch(
|
||||
() => props.contract,
|
||||
(c) => {
|
||||
if (!c) {
|
||||
metaFields.value = [];
|
||||
return;
|
||||
}
|
||||
metaFields.value = extractMetaFields(c?.meta || {});
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
function extractMetaFields(meta, parentKey = "") {
|
||||
const results = [];
|
||||
const visit = (node, path) => {
|
||||
if (node === null || node === undefined) {
|
||||
return;
|
||||
}
|
||||
if (Array.isArray(node)) {
|
||||
node.forEach((el, idx) => visit(el, `${path}[${idx}]`));
|
||||
return;
|
||||
}
|
||||
if (typeof node === "object") {
|
||||
const hasValue = Object.prototype.hasOwnProperty.call(node, "value");
|
||||
const hasTitle = Object.prototype.hasOwnProperty.call(node, "title");
|
||||
if (hasValue || hasTitle) {
|
||||
const title = (node.title || path || "Meta").toString().trim();
|
||||
const type = node.type || (typeof node.value === "number" ? "number" : "text");
|
||||
results.push({
|
||||
path,
|
||||
title,
|
||||
value: node.value ?? "",
|
||||
type,
|
||||
});
|
||||
return;
|
||||
}
|
||||
for (const [k, v] of Object.entries(node)) {
|
||||
const newPath = path ? `${path}.${k}` : k;
|
||||
visit(v, newPath);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (path) {
|
||||
results.push({ path, title: path, value: node, type: "text" });
|
||||
}
|
||||
};
|
||||
visit(meta, "");
|
||||
return results;
|
||||
}
|
||||
|
||||
const form = useForm({
|
||||
meta: {},
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.show,
|
||||
(val) => {
|
||||
if (val && props.contract) {
|
||||
// Rebuild meta structure for form
|
||||
const metaObj = {};
|
||||
metaFields.value.forEach((field) => {
|
||||
setNestedValue(metaObj, field.path, {
|
||||
title: field.title,
|
||||
value: field.value,
|
||||
type: field.type,
|
||||
});
|
||||
});
|
||||
form.meta = metaObj;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
function setNestedValue(obj, path, value) {
|
||||
const parts = path.split(/\.|\[|\]/).filter(Boolean);
|
||||
let current = obj;
|
||||
for (let i = 0; i < parts.length - 1; i++) {
|
||||
const part = parts[i];
|
||||
if (!current[part]) {
|
||||
const nextPart = parts[i + 1];
|
||||
current[part] = /^\d+$/.test(nextPart) ? [] : {};
|
||||
}
|
||||
current = current[part];
|
||||
}
|
||||
current[parts[parts.length - 1]] = value;
|
||||
}
|
||||
|
||||
function updateFieldValue(field, newValue) {
|
||||
field.value = newValue;
|
||||
}
|
||||
|
||||
function closeDialog() {
|
||||
emit("close");
|
||||
}
|
||||
|
||||
function submitForm() {
|
||||
if (!props.contract?.uuid) return;
|
||||
|
||||
// Rebuild meta object from fields
|
||||
const metaObj = {};
|
||||
metaFields.value.forEach((field) => {
|
||||
setNestedValue(metaObj, field.path, {
|
||||
title: field.title,
|
||||
value: field.value,
|
||||
type: field.type,
|
||||
});
|
||||
});
|
||||
|
||||
form.meta = metaObj;
|
||||
|
||||
form.patch(
|
||||
route("clientCase.contract.patchMeta", {
|
||||
client_case: props.clientCase.uuid,
|
||||
uuid: props.contract.uuid,
|
||||
}),
|
||||
{
|
||||
preserveScroll: true,
|
||||
onSuccess: () => {
|
||||
closeDialog();
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function formatInputType(type) {
|
||||
if (type === "date") return "date";
|
||||
if (type === "number") return "number";
|
||||
return "text";
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogModal :show="show" max-width="2xl" @close="closeDialog">
|
||||
<template #title>
|
||||
Uredi meta podatke
|
||||
<span v-if="contract" class="text-gray-500 font-normal">
|
||||
- {{ contract.reference }}
|
||||
</span>
|
||||
</template>
|
||||
<template #content>
|
||||
<div v-if="metaFields.length === 0" class="text-sm text-gray-500">
|
||||
Ni meta podatkov za urejanje.
|
||||
</div>
|
||||
<div v-else class="space-y-3">
|
||||
<div
|
||||
v-for="(field, idx) in metaFields"
|
||||
:key="idx"
|
||||
class="grid grid-cols-3 gap-3 items-start"
|
||||
>
|
||||
<div class="col-span-1">
|
||||
<label class="block text-sm font-medium text-gray-700">
|
||||
{{ field.title }}
|
||||
</label>
|
||||
<div class="text-xs text-gray-400 mt-0.5">{{ field.path }}</div>
|
||||
</div>
|
||||
<div class="col-span-2">
|
||||
<input
|
||||
v-if="
|
||||
field.type !== 'text' || field.type === 'date' || field.type === 'number'
|
||||
"
|
||||
:type="formatInputType(field.type)"
|
||||
v-model="field.value"
|
||||
class="w-full rounded-md border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 text-sm"
|
||||
/>
|
||||
<textarea
|
||||
v-else
|
||||
v-model="field.value"
|
||||
rows="2"
|
||||
class="w-full rounded-md border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 text-sm"
|
||||
></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="form.errors.meta" class="mt-3 text-sm text-red-600">
|
||||
{{ form.errors.meta }}
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<button
|
||||
type="button"
|
||||
class="px-4 py-2 text-sm rounded-md border border-gray-300 text-gray-700 bg-white hover:bg-gray-50 mr-2"
|
||||
@click="closeDialog"
|
||||
>
|
||||
Prekliči
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="px-4 py-2 text-sm rounded-md bg-indigo-600 text-white hover:bg-indigo-700 disabled:opacity-50"
|
||||
:disabled="form.processing || metaFields.length === 0"
|
||||
@click="submitForm"
|
||||
>
|
||||
Shrani
|
||||
</button>
|
||||
</template>
|
||||
</DialogModal>
|
||||
</template>
|
||||
@@ -54,7 +54,7 @@ function applyDateFilter() {
|
||||
if (selectedSegment.value) {
|
||||
params.segment = String(selectedSegment.value);
|
||||
} else {
|
||||
delete params.segment;
|
||||
delete params.segments;
|
||||
}
|
||||
delete params.page;
|
||||
router.get(route("client.contracts", { uuid: props.client.uuid }), params, {
|
||||
@@ -173,6 +173,7 @@ function formatDate(value) {
|
||||
:show-filters="true"
|
||||
:has-active-filters="!!(dateRange?.start || dateRange?.end || selectedSegment)"
|
||||
:columns="[
|
||||
{ key: 'select', label: '', sortable: false, width: '50px' },
|
||||
{ key: 'reference', label: 'Referenca', sortable: false },
|
||||
{ key: 'customer', label: 'Stranka', sortable: false },
|
||||
{ key: 'start', label: 'Začetek', sortable: false },
|
||||
@@ -233,22 +234,30 @@ function formatDate(value) {
|
||||
</div>
|
||||
</template>
|
||||
<template #cell-reference="{ row }">
|
||||
<Link :href="route('clientCase.show', caseShowParams(row))" class="text-indigo-600 hover:underline">
|
||||
<Link
|
||||
:href="route('clientCase.show', caseShowParams(row))"
|
||||
class="text-indigo-600 hover:underline"
|
||||
>
|
||||
{{ row.reference }}
|
||||
</Link>
|
||||
</template>
|
||||
<template #cell-customer="{ row }">
|
||||
{{ row.client_case?.person?.full_name || '-' }}
|
||||
{{ row.client_case?.person?.full_name || "-" }}
|
||||
</template>
|
||||
<template #cell-start="{ row }">
|
||||
{{ formatDate(row.start_date) }}
|
||||
</template>
|
||||
<template #cell-segment="{ row }">
|
||||
{{ row.segments?.[0]?.name || '-' }}
|
||||
{{ row.segments?.[0]?.name || "-" }}
|
||||
</template>
|
||||
<template #cell-balance="{ row }">
|
||||
<div class="text-right">
|
||||
{{ new Intl.NumberFormat('sl-SI', { style: 'currency', currency: 'EUR' }).format(Number(row.account?.balance_amount ?? 0)) }}
|
||||
{{
|
||||
new Intl.NumberFormat("sl-SI", {
|
||||
style: "currency",
|
||||
currency: "EUR",
|
||||
}).format(Number(row.account?.balance_amount ?? 0))
|
||||
}}
|
||||
</div>
|
||||
</template>
|
||||
</DataTable>
|
||||
|
||||
@@ -131,6 +131,7 @@ watch(
|
||||
{ value: 'emails', label: 'Emails' },
|
||||
{ value: 'accounts', label: 'Accounts' },
|
||||
{ value: 'contracts', label: 'Contracts' },
|
||||
{ value: 'case_objects', label: 'Case Objects' },
|
||||
{ value: 'payments', label: 'Payments' },
|
||||
]"
|
||||
:multiple="true"
|
||||
|
||||
@@ -4,6 +4,7 @@ import SectionTitle from "@/Components/SectionTitle.vue";
|
||||
import DataTableServer from "@/Components/DataTable/DataTableServer.vue";
|
||||
import { Link, router } from "@inertiajs/vue3";
|
||||
import { ref, computed, watch } from "vue";
|
||||
import Dropdown from "@/Components/Dropdown.vue";
|
||||
|
||||
const props = defineProps({
|
||||
activities: { type: Object, required: true },
|
||||
@@ -40,19 +41,23 @@ const selectedClient = ref(initialClient);
|
||||
|
||||
const clientOptions = computed(() => {
|
||||
// Prefer server-provided clients list; fallback to deriving from rows
|
||||
const list = Array.isArray(props.clients) && props.clients.length
|
||||
? props.clients
|
||||
: (Array.isArray(props.activities?.data) ? props.activities.data : [])
|
||||
.map((row) => {
|
||||
const client = row.contract?.client_case?.client || row.client_case?.client;
|
||||
if (!client?.uuid) return null;
|
||||
return { value: client.uuid, label: client.person?.full_name || "(neznana stranka)" };
|
||||
})
|
||||
.filter(Boolean)
|
||||
.reduce((acc, cur) => {
|
||||
if (!acc.find((x) => x.value === cur.value)) acc.push(cur);
|
||||
return acc;
|
||||
}, []);
|
||||
const list =
|
||||
Array.isArray(props.clients) && props.clients.length
|
||||
? props.clients
|
||||
: (Array.isArray(props.activities?.data) ? props.activities.data : [])
|
||||
.map((row) => {
|
||||
const client = row.contract?.client_case?.client || row.client_case?.client;
|
||||
if (!client?.uuid) return null;
|
||||
return {
|
||||
value: client.uuid,
|
||||
label: client.person?.full_name || "(neznana stranka)",
|
||||
};
|
||||
})
|
||||
.filter(Boolean)
|
||||
.reduce((acc, cur) => {
|
||||
if (!acc.find((x) => x.value === cur.value)) acc.push(cur);
|
||||
return acc;
|
||||
}, []);
|
||||
return list.sort((a, b) => (a.label || "").localeCompare(b.label || ""));
|
||||
});
|
||||
|
||||
@@ -67,11 +72,67 @@ watch(selectedClient, (val) => {
|
||||
});
|
||||
});
|
||||
|
||||
async function markRead(id) {
|
||||
try {
|
||||
await window.axios.post(route("notifications.activity.read"), { activity_id: id });
|
||||
router.reload({ only: ["activities"] });
|
||||
} catch (e) {}
|
||||
const selectedRows = ref([]);
|
||||
|
||||
function toggleSelectAll() {
|
||||
if (selectedRows.value.length === (props.activities.data?.length || 0)) {
|
||||
selectedRows.value = [];
|
||||
} else {
|
||||
selectedRows.value = (props.activities.data || []).map((row) => row.id);
|
||||
}
|
||||
}
|
||||
|
||||
function toggleRowSelection(id) {
|
||||
const idx = selectedRows.value.indexOf(id);
|
||||
if (idx > -1) {
|
||||
selectedRows.value.splice(idx, 1);
|
||||
} else {
|
||||
selectedRows.value.push(id);
|
||||
}
|
||||
}
|
||||
|
||||
function isRowSelected(id) {
|
||||
return selectedRows.value.includes(id);
|
||||
}
|
||||
|
||||
function isAllSelected() {
|
||||
return (
|
||||
(props.activities.data?.length || 0) > 0 &&
|
||||
selectedRows.value.length === (props.activities.data?.length || 0)
|
||||
);
|
||||
}
|
||||
|
||||
function isIndeterminate() {
|
||||
return (
|
||||
selectedRows.value.length > 0 &&
|
||||
selectedRows.value.length < (props.activities.data?.length || 0)
|
||||
);
|
||||
}
|
||||
|
||||
function markRead(id) {
|
||||
router.patch(
|
||||
route("notifications.activity.read"),
|
||||
{ activity_id: id },
|
||||
{
|
||||
only: ["activities"],
|
||||
preserveScroll: true,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function markReadBulk() {
|
||||
if (!selectedRows.value.length) return;
|
||||
router.patch(
|
||||
route("notifications.activity.read"),
|
||||
{ activity_ids: selectedRows.value },
|
||||
{
|
||||
only: ["activities"],
|
||||
preserveScroll: true,
|
||||
onSuccess: () => {
|
||||
selectedRows.value = [];
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -92,14 +153,20 @@ async function markRead(id) {
|
||||
<!-- Filters -->
|
||||
<div class="mb-4 flex items-center gap-3">
|
||||
<div class="flex-1 max-w-sm">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Partner</label>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1"
|
||||
>Partner</label
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<select
|
||||
v-model="selectedClient"
|
||||
class="block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 text-sm"
|
||||
>
|
||||
<option value="">Vsi partnerji</option>
|
||||
<option v-for="opt in clientOptions" :key="opt.value || opt.label" :value="opt.value">
|
||||
<option
|
||||
v-for="opt in clientOptions"
|
||||
:key="opt.value || opt.label"
|
||||
:value="opt.value"
|
||||
>
|
||||
{{ opt.label }}
|
||||
</option>
|
||||
</select>
|
||||
@@ -117,6 +184,7 @@ async function markRead(id) {
|
||||
|
||||
<DataTableServer
|
||||
:columns="[
|
||||
{ key: 'select', label: '', sortable: false, width: '50px' },
|
||||
{ key: 'what', label: 'Zadeva', sortable: false },
|
||||
{ key: 'partner', label: 'Partner', sortable: false },
|
||||
{
|
||||
@@ -140,6 +208,61 @@ async function markRead(id) {
|
||||
:only-props="['activities']"
|
||||
:query="{ client: selectedClient || undefined }"
|
||||
>
|
||||
<template #toolbar-extra>
|
||||
<div v-if="selectedRows.length" class="flex items-center gap-2">
|
||||
<div class="text-sm text-gray-700">
|
||||
Izbrano: <span class="font-medium">{{ selectedRows.length }}</span>
|
||||
</div>
|
||||
<Dropdown width="48" align="left">
|
||||
<template #trigger>
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center px-3 py-1.5 text-sm font-medium rounded-md border border-gray-300 text-gray-700 bg-white hover:bg-gray-50"
|
||||
>
|
||||
Akcije
|
||||
<svg
|
||||
class="ml-1 h-4 w-4"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M5.23 7.21a.75.75 0 011.06.02L10 10.94l3.71-3.71a.75.75 0 111.06 1.06l-4.24 4.24a.75.75 0 01-1.06 0L5.21 8.29a.75.75 0 01.02-1.08z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</template>
|
||||
<template #content>
|
||||
<button
|
||||
type="button"
|
||||
class="w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
|
||||
@click="markReadBulk"
|
||||
>
|
||||
Označi kot prebrano
|
||||
</button>
|
||||
</template>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</template>
|
||||
<template #header-select>
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="isAllSelected()"
|
||||
:indeterminate="isIndeterminate()"
|
||||
@change="toggleSelectAll"
|
||||
class="rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
|
||||
/>
|
||||
</template>
|
||||
<template #cell-select="{ row }">
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="isRowSelected(row.id)"
|
||||
@change="toggleRowSelection(row.id)"
|
||||
class="rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
|
||||
/>
|
||||
</template>
|
||||
<template #cell-what="{ row }">
|
||||
<div class="font-medium text-gray-800 truncate">
|
||||
<template v-if="row.contract?.uuid">
|
||||
@@ -175,9 +298,9 @@ async function markRead(id) {
|
||||
<template #cell-partner="{ row }">
|
||||
<div class="truncate">
|
||||
{{
|
||||
(row.contract?.client_case?.client?.person?.full_name) ||
|
||||
(row.client_case?.client?.person?.full_name) ||
|
||||
'—'
|
||||
row.contract?.client_case?.client?.person?.full_name ||
|
||||
row.client_case?.client?.person?.full_name ||
|
||||
"—"
|
||||
}}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user