Changes to phone view, fixed infinity scroll issues with page refresh, updated design a bit

This commit is contained in:
Simon Pocrnjič 2026-04-18 12:28:15 +02:00
parent 8f8c5c5a12
commit 92f54f7103
5 changed files with 727 additions and 753 deletions

View File

@ -17,6 +17,11 @@ public function index(Request $request): \Inertia\Response
$search = $request->input('search');
$clientFilter = $request->input('client');
// On full page loads, always start from page 1
if (! $request->header('X-Inertia-Partial-Data')) {
$request->merge(['pending' => 1, 'processed' => 1]);
}
$eagerLoad = [
'contract' => function ($q) {
$q->with([
@ -97,6 +102,11 @@ public function completedToday(Request $request): \Inertia\Response
$search = $request->input('search');
$clientFilter = $request->input('client');
// On full page loads, always start from page 1
if (! $request->header('X-Inertia-Partial-Data')) {
$request->merge(['completed' => 1]);
}
$start = now()->startOfDay();
$end = now()->endOfDay();

View File

@ -25,6 +25,7 @@ import {
} from "@/Components/ui/select";
import { Switch } from "@/Components/ui/switch";
import { Button } from "@/Components/ui/button";
import { ScrollArea } from "@/Components/ui/scroll-area";
const props = defineProps({
show: { type: Boolean, default: false },
@ -452,11 +453,57 @@ const open = computed({
</DialogDescription>
</DialogHeader>
<form @submit.prevent="onSubmit" class="space-y-4">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormField v-slot="{ value, handleChange }" name="profile_id">
<ScrollArea class="max-h-[65vh] pr-1">
<form @submit.prevent="onSubmit" class="space-y-4 pr-3">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormField v-slot="{ value, handleChange }" name="profile_id">
<FormItem>
<FormLabel>Profil</FormLabel>
<Select :model-value="value" @update:model-value="handleChange">
<FormControl>
<SelectTrigger>
<SelectValue placeholder="—" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem :value="null"></SelectItem>
<SelectItem v-for="p in pageSmsProfiles" :key="p.id" :value="p.id">
{{ p.name || "Profil #" + p.id }}
</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
</FormField>
<FormField v-slot="{ value, handleChange }" name="sender_id">
<FormItem>
<FormLabel>Pošiljatelj</FormLabel>
<Select :model-value="value" @update:model-value="handleChange">
<FormControl>
<SelectTrigger>
<SelectValue placeholder="—" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem :value="null"></SelectItem>
<SelectItem
v-for="s in sendersForSelectedProfile"
:key="s.id"
:value="s.id"
>
{{ s.name || s.phone || "Sender #" + s.id }}
</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
</FormField>
</div>
<FormField v-slot="{ value, handleChange }" name="contract_uuid">
<FormItem>
<FormLabel>Profil</FormLabel>
<FormLabel>Pogodba</FormLabel>
<Select :model-value="value" @update:model-value="handleChange">
<FormControl>
<SelectTrigger>
@ -465,18 +512,22 @@ const open = computed({
</FormControl>
<SelectContent>
<SelectItem :value="null"></SelectItem>
<SelectItem v-for="p in pageSmsProfiles" :key="p.id" :value="p.id">
{{ p.name || "Profil #" + p.id }}
<SelectItem v-for="c in contractsForCase" :key="c.uuid" :value="c.uuid">
{{ c.reference || c.uuid }}
</SelectItem>
</SelectContent>
</Select>
<p class="mt-1 text-xs text-gray-500">
Uporabi podatke pogodbe (in računa) za zapolnitev {contract.*} in
{account.*} mest.
</p>
<FormMessage />
</FormItem>
</FormField>
<FormField v-slot="{ value, handleChange }" name="sender_id">
<FormField v-slot="{ value, handleChange }" name="template_id">
<FormItem>
<FormLabel>Pošiljatelj</FormLabel>
<FormLabel>Predloga</FormLabel>
<Select :model-value="value" @update:model-value="handleChange">
<FormControl>
<SelectTrigger>
@ -485,125 +536,77 @@ const open = computed({
</FormControl>
<SelectContent>
<SelectItem :value="null"></SelectItem>
<SelectItem
v-for="s in sendersForSelectedProfile"
:key="s.id"
:value="s.id"
>
{{ s.name || s.phone || "Sender #" + s.id }}
<SelectItem v-for="t in pageSmsTemplates" :key="t.id" :value="t.id">
{{ t.name || "Predloga #" + t.id }}
</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
</FormField>
</div>
<FormField v-slot="{ value, handleChange }" name="contract_uuid">
<FormItem>
<FormLabel>Pogodba</FormLabel>
<Select :model-value="value" @update:model-value="handleChange">
<FormField v-slot="{ componentField }" name="message">
<FormItem>
<FormLabel>Vsebina sporočila</FormLabel>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="—" />
</SelectTrigger>
<Textarea
rows="4"
placeholder="Vpišite SMS vsebino..."
v-bind="componentField"
/>
</FormControl>
<SelectContent>
<SelectItem :value="null"></SelectItem>
<SelectItem v-for="c in contractsForCase" :key="c.uuid" :value="c.uuid">
{{ c.reference || c.uuid }}
</SelectItem>
</SelectContent>
</Select>
<p class="mt-1 text-xs text-gray-500">
Uporabi podatke pogodbe (in računa) za zapolnitev {contract.*} in
{account.*} mest.
</p>
<FormMessage />
</FormItem>
</FormField>
<FormMessage />
</FormItem>
</FormField>
<FormField v-slot="{ value, handleChange }" name="template_id">
<FormItem>
<FormLabel>Predloga</FormLabel>
<Select :model-value="value" @update:model-value="handleChange">
<FormControl>
<SelectTrigger>
<SelectValue placeholder="—" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem :value="null"></SelectItem>
<SelectItem v-for="t in pageSmsTemplates" :key="t.id" :value="t.id">
{{ t.name || "Predloga #" + t.id }}
</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
</FormField>
<FormField v-slot="{ componentField }" name="message">
<FormItem>
<FormLabel>Vsebina sporočila</FormLabel>
<FormControl>
<Textarea
rows="4"
placeholder="Vpišite SMS vsebino..."
v-bind="componentField"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
<!-- Live counters -->
<div class="text-xs text-gray-600 flex flex-col gap-1">
<div>
<span class="font-medium">Znakov:</span>
<span class="font-mono">{{ charCount }}</span>
<span class="mx-2">|</span>
<span class="font-medium">Kodiranje:</span>
<span>{{ smsEncoding }}</span>
<span class="mx-2">|</span>
<span class="font-medium">Deli SMS:</span>
<span class="font-mono">{{ segments }}</span>
<span class="mx-2">|</span>
<span class="font-medium">Krediti:</span>
<span class="font-mono">{{ creditsNeeded }}</span>
</div>
<div>
<span class="font-medium">Omejitev:</span>
<span class="font-mono">{{ maxAllowed }}</span>
<span class="mx-2">|</span>
<span class="font-medium">Preostanek:</span>
<span class="font-mono" :class="{ 'text-red-600': remaining === 0 }">
{{ remaining }}
</span>
</div>
<p class="text-[11px] text-gray-500 leading-snug">
Dolžina 160 znakov velja samo pri pošiljanju sporočil, ki vsebujejo znake, ki
ne zahtevajo enkodiranja. Če npr. želite pošiljati šumnike, ki niso del
7-bitne abecede GSM, morate uporabiti Unicode enkodiranje (UCS2). V tem
primeru je največja dolžina enega SMS sporočila 70 znakov (pri daljših
sporočilih 67 znakov na del), medtem ko je pri GSM7 160 znakov (pri daljših
sporočilih 153 znakov na del). Razširjeni znaki (^{{ "{" }}}}\\[]~| in )
štejejo dvojno. Največja dovoljena dolžina po ponudniku: 640 (GSM7) oziroma
320 (UCS2) znakov.
</p>
</div>
<FormField v-slot="{ value, handleChange }" name="delivery_report">
<FormItem class="flex flex-row items-start space-x-3 space-y-0">
<FormControl>
<Switch :model-value="value" @update:model-value="handleChange" />
</FormControl>
<div class="space-y-1 leading-none">
<FormLabel>Zahtevaj poročilo o dostavi</FormLabel>
<!-- Live counters -->
<div class="text-xs text-gray-600 flex flex-col gap-1">
<div>
<span class="font-medium">Znakov:</span>
<span class="font-mono">{{ charCount }}</span>
<span class="mx-2">|</span>
<span class="font-medium">Kodiranje:</span>
<span>{{ smsEncoding }}</span>
<span class="mx-2">|</span>
<span class="font-medium">Deli SMS:</span>
<span class="font-mono">{{ segments }}</span>
<span class="mx-2">|</span>
<span class="font-medium">Krediti:</span>
<span class="font-mono">{{ creditsNeeded }}</span>
</div>
</FormItem>
</FormField>
</form>
<div>
<span class="font-medium">Omejitev:</span>
<span class="font-mono">{{ maxAllowed }}</span>
<span class="mx-2">|</span>
<span class="font-medium">Preostanek:</span>
<span class="font-mono" :class="{ 'text-red-600': remaining === 0 }">
{{ remaining }}
</span>
</div>
<p class="text-[11px] text-gray-500 leading-snug">
Dolžina 160 znakov velja samo pri pošiljanju sporočil, ki vsebujejo znake,
ki ne zahtevajo enkodiranja. Če npr. želite pošiljati šumnike, ki niso del
7-bitne abecede GSM, morate uporabiti Unicode enkodiranje (UCS2). V tem
primeru je največja dolžina enega SMS sporočila 70 znakov (pri daljših
sporočilih 67 znakov na del), medtem ko je pri GSM7 160 znakov (pri daljših
sporočilih 153 znakov na del). Razširjeni znaki (^{{ "{" }}}}\\[]~| in )
štejejo dvojno. Največja dovoljena dolžina po ponudniku: 640 (GSM7) oziroma
320 (UCS2) znakov.
</p>
</div>
<FormField v-slot="{ value, handleChange }" name="delivery_report">
<FormItem class="flex flex-row items-start space-x-3 space-y-0">
<FormControl>
<Switch :model-value="value" @update:model-value="handleChange" />
</FormControl>
<div class="space-y-1 leading-none">
<FormLabel>Zahtevaj poročilo o dostavi</FormLabel>
</div>
</FormItem>
</FormField>
</form>
</ScrollArea>
<DialogFooter>
<Button variant="outline" @click="closeSmsDialog" :disabled="processing">

View File

@ -308,7 +308,7 @@ const closeSearch = () => (searchOpen.value = false);
</div>
<!-- Page Heading -->
<header v-if="$slots.header" class="bg-white border-b border-gray-200 shadow-sm">
<header v-if="$slots.header" class="sticky top-16 z-20 bg-white border-b border-gray-200 shadow-sm dark:bg-gray-900 dark:border-gray-700">
<div class="max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8 space-y-2">
<Breadcrumbs
v-if="$page.props.breadcrumbs && $page.props.breadcrumbs.length"

View File

@ -18,10 +18,11 @@ import { Button } from "@/Components/ui/button";
import { Badge } from "@/Components/ui/badge";
import { Separator } from "@/Components/ui/separator";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/Components/ui/collapsible";
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/Components/ui/accordion";
import {
Dialog,
DialogContent,
@ -46,7 +47,6 @@ import { Checkbox } from "@/Components/ui/checkbox";
import {
ArrowLeft,
CheckCircle2,
ChevronDown,
FileText,
Calendar,
Euro,
@ -321,7 +321,7 @@ const clientSummary = computed(() => {
<div class="py-4 sm:py-6">
<div class="mx-auto max-w-5xl px-2 sm:px-4 space-y-4">
<!-- Client details (account holder) -->
<Card>
<Card class="gap-3">
<CardHeader>
<CardTitle class="flex items-center gap-2 text-base">
<Building2 class="w-5 h-5 text-gray-500" />
@ -340,8 +340,8 @@ const clientSummary = computed(() => {
</Card>
<!-- Person (case person) -->
<Card>
<CardHeader>
<Card class="gap-3">
<CardHeader class="px-3">
<CardTitle class="flex items-center gap-2 text-base">
<User class="w-5 h-5 text-gray-500" />
<span class="truncate">{{ client_case.person.full_name }}</span>
@ -354,7 +354,7 @@ const clientSummary = computed(() => {
{{ client_case.person.description }}
</CardDescription>
</CardHeader>
<CardContent>
<CardContent class="px-3">
<Separator class="mb-4" />
<PersonDetailPhone
:types="types"
@ -365,35 +365,32 @@ const clientSummary = computed(() => {
</Card>
<!-- Contracts assigned to me -->
<Card>
<CardHeader>
<Card class="p-0 pt-3 gap-1">
<CardHeader class="px-4">
<CardTitle class="flex items-center gap-2">
<FileText class="w-5 h-5" />
Pogodbe
</CardTitle>
</CardHeader>
<CardContent class="space-y-3">
<CardContent class="p-2">
<Card
v-for="c in contracts"
:key="c.uuid || c.id"
class="border-l-4 border-l-indigo-500 overflow-hidden"
class="overflow-hidden p-0 gap-3"
>
<!-- Contract header: reference + type badge -->
<CardHeader class="pb-2">
<div class="flex items-center gap-2 flex-wrap">
<CardHeader class="p-3 pb-2">
<div class="flex items-center flex-wrap">
<CardTitle class="text-base font-semibold">
{{ c.reference || c.uuid }}
{{ c.reference || "Šifra pogodbe ni določena" }}
</CardTitle>
<Badge v-if="c.type?.name" variant="secondary" class="text-[11px]">
{{ c.type.name }}
</Badge>
</div>
</CardHeader>
<!-- Balance row -->
<div
v-if="c.account"
class="mx-4 mb-3 rounded-xl bg-red-50 dark:bg-red-950/20 border border-red-100 dark:border-red-900 px-4 py-3 flex items-center justify-between"
class="mx-3 rounded-xl bg-red-50 dark:bg-red-950/20 border border-red-100 dark:border-red-900 px-2 py-2 flex items-center justify-between"
>
<div class="flex items-center gap-2 text-red-500">
<Euro class="w-4 h-4 shrink-0" />
@ -413,75 +410,76 @@ const clientSummary = computed(() => {
v-if="
c.description || c.last_object || (c.meta && Object.keys(c.meta).length)
"
class="pt-0 px-4 space-y-0"
class="pt-0 px-0 space-y-0"
>
<!-- Description -->
<template v-if="c.description">
<Separator class="mb-3" />
<Collapsible>
<CollapsibleTrigger
class="flex items-center gap-1.5 text-xs text-gray-500 hover:text-gray-700 dark:hover:text-gray-300 group w-full py-1"
<!-- Description + Meta Accordion -->
<template v-if="c.description || (c.meta && Object.keys(c.meta).length)">
<Separator />
<Accordion type="multiple" class="w-full">
<AccordionItem
v-if="c.description"
value="description"
class="border-b-0"
>
<ChevronDown
class="w-3.5 h-3.5 transition-transform duration-200 group-data-[state=open]:rotate-180 shrink-0"
/>
<span class="uppercase tracking-wide font-medium">Opis</span>
</CollapsibleTrigger>
<CollapsibleContent>
<p
class="mt-1.5 mb-2 text-sm text-gray-700 dark:text-gray-300 whitespace-pre-line rounded-lg bg-gray-50 dark:bg-gray-800/50 px-3 py-2.5"
<AccordionTrigger
class="px-3 py-2 text-xs font-medium uppercase tracking-wide hover:no-underline"
>
{{ c.description }}
</p>
</CollapsibleContent>
</Collapsible>
</template>
<!-- Meta -->
<template v-if="c.meta && Object.keys(c.meta).length">
<Separator class="mb-3" :class="c.description ? 'mt-2' : 'mt-0'" />
<Collapsible>
<CollapsibleTrigger
class="flex items-center gap-1.5 text-xs text-gray-500 hover:text-gray-700 dark:hover:text-gray-300 group w-full py-1"
>
<ChevronDown
class="w-3.5 h-3.5 transition-transform duration-200 group-data-[state=open]:rotate-180 shrink-0"
/>
<span class="uppercase tracking-wide font-medium"
>Dodatni podatki</span
>
<span class="ml-auto text-gray-400 font-normal">{{
Object.keys(c.meta).length
}}</span>
</CollapsibleTrigger>
<CollapsibleContent>
<div
class="mt-1.5 mb-2 divide-y divide-gray-100 dark:divide-gray-700 rounded-lg border border-gray-100 dark:border-gray-700 overflow-hidden"
>
<div
v-for="(val, key) in c.meta"
:key="key"
class="flex items-center justify-between gap-3 px-3 py-2 bg-white dark:bg-gray-900 even:bg-gray-50/60 dark:even:bg-gray-800/40"
Opis
</AccordionTrigger>
<AccordionContent class="px-3 pb-3">
<p
class="text-sm text-gray-700 dark:text-gray-300 whitespace-pre-line rounded-lg bg-gray-50 dark:bg-gray-800/50 px-3 py-2.5"
>
<span
class="text-xs text-gray-500 dark:text-gray-400 shrink-0"
>{{ val?.title || key }}</span
{{ c.description }}
</p>
</AccordionContent>
</AccordionItem>
<AccordionItem
v-if="c.meta && Object.keys(c.meta).length"
value="meta"
class="border-b-0"
:class="c.description ? 'border-t' : ''"
>
<AccordionTrigger
class="px-3 py-2 text-xs font-medium uppercase tracking-wide hover:no-underline"
>
<div>
<span class="mr-1">Dodatni podatki</span>
<Badge
class="bg-blue-500 text-white dark:bg-blue-600 h-5 min-w-5 rounded-full px-2 font-mono tabular-nums"
>{{ Object.keys(c.meta).length }}</Badge
>
<span
class="text-xs font-semibold text-gray-800 dark:text-gray-200 text-right"
>
<template v-if="val?.type === 'date'">{{
formatDateShort(val.value) || val.value || "—"
}}</template>
<template v-else-if="val?.type === 'number'">{{
val.value ?? "—"
}}</template>
<template v-else>{{ val?.value ?? val ?? "" }}</template>
</span>
</div>
</div>
</CollapsibleContent>
</Collapsible>
</AccordionTrigger>
<AccordionContent class="pb-2">
<div
class="divide-y divide-gray-100 dark:divide-gray-700 rounded-lg border border-gray-100 dark:border-gray-700 overflow-hidden"
>
<div
v-for="(val, key) in c.meta"
:key="key"
class="flex items-center justify-between gap-3 px-3 py-2 bg-white dark:bg-gray-900 even:bg-gray-50/60 dark:even:bg-gray-800/40"
>
<span
class="text-xs text-gray-500 dark:text-gray-400 shrink-0"
>{{ val?.title || key }}</span
>
<span
class="text-xs font-semibold text-gray-800 dark:text-gray-200 text-right"
>
<template v-if="val?.type === 'date'">{{
formatDateShort(val.value) || val.value || "—"
}}</template>
<template v-else-if="val?.type === 'number'">{{
val.value ?? "—"
}}</template>
<template v-else>{{ val?.value ?? val ?? "" }}</template>
</span>
</div>
</div>
</AccordionContent>
</AccordionItem>
</Accordion>
</template>
<!-- Last object -->

File diff suppressed because it is too large Load Diff