350 lines
12 KiB
Vue
350 lines
12 KiB
Vue
<script setup>
|
|
import AppLayout from "@/Layouts/AppLayout.vue";
|
|
import DataTable from "@/Components/DataTable/DataTableNew2.vue";
|
|
import { Link, router } from "@inertiajs/vue3";
|
|
import { ref, computed, watch } from "vue";
|
|
import {
|
|
Card,
|
|
CardContent,
|
|
CardDescription,
|
|
CardHeader,
|
|
CardTitle,
|
|
} from "@/Components/ui/card";
|
|
import { Button } from "@/Components/ui/button";
|
|
import { Label } from "@/Components/ui/label";
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from "@/Components/ui/select";
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuTrigger,
|
|
} from "@/Components/ui/dropdown-menu";
|
|
import { Checkbox } from "@/Components/ui/checkbox";
|
|
import { Bell, BellOff, Check, ChevronDown, X, Inbox } from "lucide-vue-next";
|
|
import TableActions from "@/Components/DataTable/TableActions.vue";
|
|
import ActionMenuItem from "@/Components/DataTable/ActionMenuItem.vue";
|
|
import { number } from "zod";
|
|
import { toNumber } from "lodash";
|
|
|
|
const props = defineProps({
|
|
activities: { type: Object, required: true },
|
|
today: { type: String, required: true },
|
|
clients: { type: Array, default: () => [] },
|
|
});
|
|
|
|
function fmtDate(d) {
|
|
if (!d) return "";
|
|
try {
|
|
return new Date(d).toLocaleDateString("sl-SI");
|
|
} catch {
|
|
return String(d);
|
|
}
|
|
}
|
|
|
|
function fmtEUR(value) {
|
|
if (value === null || value === undefined) return "—";
|
|
const num = typeof value === "string" ? Number(value) : value;
|
|
if (Number.isNaN(num)) return String(value);
|
|
const formatted = new Intl.NumberFormat("de-DE", {
|
|
style: "currency",
|
|
currency: "EUR",
|
|
minimumFractionDigits: 2,
|
|
maximumFractionDigits: 2,
|
|
}).format(num);
|
|
return formatted.replace("\u00A0", " ");
|
|
}
|
|
|
|
// Client filter
|
|
const urlParams = new URLSearchParams(window.location.search);
|
|
const initialClient = urlParams.get("client") || urlParams.get("client_id") || "";
|
|
const selectedClient = ref(initialClient);
|
|
|
|
const clientOptions = computed(() => {
|
|
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 || ""));
|
|
});
|
|
|
|
watch(selectedClient, (val) => {
|
|
const query = {};
|
|
if (val) query.client = val;
|
|
router.get(route("notifications.unread"), query, {
|
|
preserveState: true,
|
|
preserveScroll: true,
|
|
only: ["activities"],
|
|
replace: true,
|
|
});
|
|
});
|
|
|
|
// Row selection - connected to DataTableNew2's built-in selection
|
|
const selectedRows = ref([]);
|
|
const dataTableRef = ref(null);
|
|
|
|
function handleSelectionChange(selectedKeys) {
|
|
selectedRows.value = selectedKeys.map((val, i) => {
|
|
const nu = toNumber(val);
|
|
return props.activities.data[val].id;
|
|
});
|
|
|
|
console.log(selectedRows.value);
|
|
}
|
|
|
|
// Mark as read actions
|
|
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 = [];
|
|
// Clear the selection state in DataTable
|
|
if (dataTableRef.value) {
|
|
dataTableRef.value.clearSelection();
|
|
}
|
|
},
|
|
}
|
|
);
|
|
}
|
|
|
|
// Table columns definition (select column is auto-generated by enableRowSelection)
|
|
const columns = [
|
|
{ key: "what", label: "Zadeva", sortable: false },
|
|
{ key: "partner", label: "Partner", sortable: false },
|
|
{ key: "balance", label: "Stanje", sortable: false, align: "right" },
|
|
{ key: "due", label: "Zapadlost", sortable: false },
|
|
{ key: "actions", label: "", sortable: false, hideable: false, align: "center" },
|
|
];
|
|
|
|
const rows = computed(() => props.activities?.data || []);
|
|
</script>
|
|
|
|
<template>
|
|
<AppLayout title="Obvestila">
|
|
<template #header></template>
|
|
<div class="py-12">
|
|
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
|
<Card>
|
|
<CardHeader>
|
|
<div class="flex items-center justify-between">
|
|
<div class="flex items-center gap-3">
|
|
<div
|
|
class="flex h-10 w-10 items-center justify-center rounded-lg bg-primary/10"
|
|
>
|
|
<Bell class="h-5 w-5 text-primary" />
|
|
</div>
|
|
<div>
|
|
<CardTitle>Neprikazana obvestila</CardTitle>
|
|
<CardDescription>Do danes: {{ fmtDate(today) }}</CardDescription>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent class="p-0">
|
|
<!-- Client Filter -->
|
|
<div class="mb-6 px-6 flex items-end gap-3">
|
|
<div class="flex-1 max-w-sm space-y-2">
|
|
<Label for="client-filter">Partner</Label>
|
|
<div class="flex items-center gap-2">
|
|
<Select v-model="selectedClient">
|
|
<SelectTrigger id="client-filter">
|
|
<SelectValue placeholder="Vsi partnerji" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem
|
|
v-for="opt in clientOptions"
|
|
:key="opt.value || opt.label"
|
|
:value="opt.value"
|
|
>
|
|
{{ opt.label }}
|
|
</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
<Button
|
|
v-if="selectedClient"
|
|
variant="ghost"
|
|
size="icon"
|
|
@click="selectedClient = ''"
|
|
title="Počisti filter"
|
|
>
|
|
<X class="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Data Table -->
|
|
<DataTable
|
|
ref="dataTableRef"
|
|
:columns="columns"
|
|
:data="rows"
|
|
:meta="activities"
|
|
route-name="notifications.unread"
|
|
page-param-name="unread-page"
|
|
:only-props="['activities']"
|
|
:page-size="15"
|
|
:page-size-options="[10, 15, 25, 50]"
|
|
:show-pagination="true"
|
|
:show-toolbar="true"
|
|
:hoverable="true"
|
|
:enable-row-selection="true"
|
|
row-key="id"
|
|
empty-text="Trenutno ni neprikazanih obvestil."
|
|
@selection:change="handleSelectionChange"
|
|
>
|
|
<!-- Bulk Actions Toolbar -->
|
|
<template #toolbar-filters>
|
|
<div v-if="selectedRows.length" class="flex items-center gap-3">
|
|
<div class="text-sm text-muted-foreground">
|
|
Izbrano:
|
|
<span class="font-medium text-foreground">{{
|
|
selectedRows.length
|
|
}}</span>
|
|
</div>
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger as-child>
|
|
<Button variant="outline" size="sm" class="gap-2">
|
|
Akcije
|
|
<ChevronDown class="h-4 w-4" />
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="start">
|
|
<DropdownMenuItem @click="markReadBulk">
|
|
<Check class="h-4 w-4" />
|
|
Označi kot prebrano
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
</div>
|
|
</template>
|
|
|
|
<!-- What Column -->
|
|
<template #cell-what="{ row }">
|
|
<div class="font-medium truncate">
|
|
<template v-if="row.contract?.uuid">
|
|
<span class="text-muted-foreground">Pogodba:</span>
|
|
<Link
|
|
v-if="row.contract?.client_case?.uuid"
|
|
:href="
|
|
route('clientCase.show', {
|
|
client_case: row.contract.client_case.uuid,
|
|
})
|
|
"
|
|
class="ml-1 text-primary hover:underline"
|
|
>
|
|
{{ row.contract?.reference || "—" }}
|
|
</Link>
|
|
<span v-else class="ml-1">{{ row.contract?.reference || "—" }}</span>
|
|
</template>
|
|
<template v-else>
|
|
<span class="text-muted-foreground">Primer:</span>
|
|
<Link
|
|
v-if="row.client_case?.uuid"
|
|
:href="
|
|
route('clientCase.show', { client_case: row.client_case.uuid })
|
|
"
|
|
class="ml-1 text-primary hover:underline"
|
|
>
|
|
{{ row.client_case?.person?.full_name || "—" }}
|
|
</Link>
|
|
<span v-else class="ml-1">{{
|
|
row.client_case?.person?.full_name || "—"
|
|
}}</span>
|
|
</template>
|
|
</div>
|
|
</template>
|
|
|
|
<!-- Partner Column -->
|
|
<template #cell-partner="{ row }">
|
|
<div class="truncate">
|
|
{{
|
|
row.contract?.client_case?.client?.person?.full_name ||
|
|
row.client_case?.client?.person?.full_name ||
|
|
"—"
|
|
}}
|
|
</div>
|
|
</template>
|
|
|
|
<!-- Balance Column -->
|
|
<template #cell-balance="{ row }">
|
|
<div class="text-right font-medium">
|
|
<span v-if="row.contract">{{
|
|
fmtEUR(row.contract?.account?.balance_amount)
|
|
}}</span>
|
|
<span v-else class="text-muted-foreground">—</span>
|
|
</div>
|
|
</template>
|
|
|
|
<!-- Due Date Column -->
|
|
<template #cell-due="{ row }">
|
|
<div class="text-sm">
|
|
{{ fmtDate(row.due_date) }}
|
|
</div>
|
|
</template>
|
|
|
|
<!-- Actions Column -->
|
|
<template #cell-actions="{ row }">
|
|
<TableActions>
|
|
<ActionMenuItem @click="markRead(row.id)" label="Označi kot prebrano">
|
|
<BellOff class="mr-2 h-4 w-4" />
|
|
Označi kot prebrano
|
|
</ActionMenuItem>
|
|
</TableActions>
|
|
</template>
|
|
|
|
<!-- Empty State -->
|
|
<template #empty>
|
|
<div class="flex flex-col items-center justify-center py-12 text-center">
|
|
<div
|
|
class="flex h-20 w-20 items-center justify-center rounded-full bg-muted"
|
|
>
|
|
<Inbox class="h-10 w-10 text-muted-foreground" />
|
|
</div>
|
|
<h3 class="mt-4 text-lg font-semibold">Ni neprikazanih obvestil</h3>
|
|
<p class="mt-2 text-sm text-muted-foreground">
|
|
Trenutno nimate nobenih neprikazanih obvestil.
|
|
</p>
|
|
</div>
|
|
</template>
|
|
</DataTable>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
</AppLayout>
|
|
</template>
|