189 lines
5.9 KiB
Vue
189 lines
5.9 KiB
Vue
<script setup>
|
|
import { computed, onMounted, ref, watch } from "vue";
|
|
import { usePage, Link, router } from "@inertiajs/vue3";
|
|
import { BellIcon } from "lucide-vue-next";
|
|
import { Badge } from "@/Components/ui/badge";
|
|
import { Button } from "@/Components/ui/button";
|
|
import { Popover, PopoverContent, PopoverTrigger } from "@/Components/ui/popover";
|
|
import { ScrollArea } from "@/Components/ui/scroll-area";
|
|
import { Separator } from "@/Components/ui/separator";
|
|
|
|
const page = usePage();
|
|
const due = computed(
|
|
() => page.props.notifications?.dueToday || { count: 0, items: [], date: null }
|
|
);
|
|
|
|
// Local, optimistically-updated list of items and derived count
|
|
const items = ref([]);
|
|
const count = computed(() => items.value.length);
|
|
|
|
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);
|
|
}
|
|
// de-DE locale: dot thousands, comma decimals, trailing Euro symbol
|
|
const formatted = new Intl.NumberFormat("de-DE", {
|
|
style: "currency",
|
|
currency: "EUR",
|
|
minimumFractionDigits: 2,
|
|
maximumFractionDigits: 2,
|
|
}).format(num);
|
|
// Replace non-breaking space with normal space for consistency
|
|
return formatted.replace("\u00A0", " ");
|
|
}
|
|
|
|
onMounted(() => {
|
|
items.value = [...(due.value.items || [])];
|
|
});
|
|
|
|
watch(
|
|
() => due.value.items,
|
|
(val) => {
|
|
items.value = [...(val || [])];
|
|
}
|
|
);
|
|
|
|
function markRead(item) {
|
|
const idx = items.value.findIndex((i) => i.id === item.id);
|
|
if (idx === -1) {
|
|
return;
|
|
}
|
|
|
|
// Optimistically remove
|
|
const removed = items.value.splice(idx, 1)[0];
|
|
|
|
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>
|
|
|
|
<template>
|
|
<Popover>
|
|
<PopoverTrigger as-child>
|
|
<Button variant="ghost" size="default" class="relative">
|
|
<BellIcon />
|
|
|
|
<Badge
|
|
v-if="count"
|
|
class="absolute -top-1 -right-1 h-5 min-w-5 inline-flex items-center justify-center rounded-full px-1 font-mono tabular-nums text-accent"
|
|
variant="destructive"
|
|
>
|
|
{{ count }}
|
|
</Badge>
|
|
</Button>
|
|
</PopoverTrigger>
|
|
|
|
<PopoverContent align="end" class="w-96 p-0">
|
|
<div class="px-4 py-3 flex items-center justify-between border-b">
|
|
<span class="text-sm font-medium">Zapadejo danes</span>
|
|
<Link
|
|
:href="route('notifications.unread')"
|
|
class="text-sm text-primary hover:underline"
|
|
>Vsa obvestila</Link
|
|
>
|
|
</div>
|
|
|
|
<ScrollArea class="h-72">
|
|
<div v-if="!count" class="px-4 py-8 text-center">
|
|
<p class="text-sm text-muted-foreground">Ni zapadlih aktivnosti danes.</p>
|
|
</div>
|
|
<div v-else class="divide-y">
|
|
<div
|
|
v-for="item in items"
|
|
:key="item.id"
|
|
class="px-4 py-3 flex items-start gap-3 hover:bg-accent/50 transition-colors"
|
|
>
|
|
<div class="flex-1 min-w-0 space-y-1">
|
|
<div class="font-medium truncate">
|
|
<template v-if="item.contract?.uuid">
|
|
Pogodba:
|
|
<Link
|
|
v-if="item.contract?.client_case?.uuid"
|
|
:href="
|
|
route('clientCase.show', {
|
|
client_case: item.contract.client_case.uuid,
|
|
})
|
|
"
|
|
class="text-primary hover:underline"
|
|
>
|
|
{{ item.contract?.reference || "—" }}
|
|
</Link>
|
|
<span v-else>{{ item.contract?.reference || "—" }}</span>
|
|
</template>
|
|
<template v-else>
|
|
Primer:
|
|
<Link
|
|
v-if="item.client_case?.uuid"
|
|
:href="
|
|
route('clientCase.show', { client_case: item.client_case.uuid })
|
|
"
|
|
class="text-primary hover:underline"
|
|
>
|
|
{{ item.client_case?.person?.full_name || "—" }}
|
|
</Link>
|
|
<span v-else>{{ item.client_case?.person?.full_name || "—" }}</span>
|
|
</template>
|
|
</div>
|
|
<!-- Partner / Client full name (use contract.client when available; fallback to case.client) -->
|
|
<div
|
|
class="text-xs text-muted-foreground truncate"
|
|
v-if="item.contract?.client?.person?.full_name"
|
|
>
|
|
Partner: {{ item.contract.client.person.full_name }}
|
|
</div>
|
|
<div
|
|
class="text-xs text-muted-foreground truncate"
|
|
v-else-if="item.client_case?.client?.person?.full_name"
|
|
>
|
|
Partner: {{ item.client_case.client.person.full_name }}
|
|
</div>
|
|
<div class="text-sm truncate" v-if="item.contract">
|
|
{{ fmtEUR(item.contract?.account?.balance_amount) }}
|
|
</div>
|
|
</div>
|
|
<div class="flex flex-col items-end gap-1.5 shrink-0">
|
|
<div class="text-xs text-muted-foreground whitespace-nowrap">
|
|
{{ fmtDate(item.due_date) }}
|
|
</div>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
class="h-6 px-2 text-xs"
|
|
@click.stop="markRead(item)"
|
|
title="Skrij obvestilo"
|
|
>
|
|
Skrij
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</ScrollArea>
|
|
</PopoverContent>
|
|
</Popover>
|
|
</template>
|