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'); $search = $request->input('search');
$clientFilter = $request->input('client'); $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 = [ $eagerLoad = [
'contract' => function ($q) { 'contract' => function ($q) {
$q->with([ $q->with([
@ -97,6 +102,11 @@ public function completedToday(Request $request): \Inertia\Response
$search = $request->input('search'); $search = $request->input('search');
$clientFilter = $request->input('client'); $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(); $start = now()->startOfDay();
$end = now()->endOfDay(); $end = now()->endOfDay();

View File

@ -25,6 +25,7 @@ import {
} from "@/Components/ui/select"; } from "@/Components/ui/select";
import { Switch } from "@/Components/ui/switch"; import { Switch } from "@/Components/ui/switch";
import { Button } from "@/Components/ui/button"; import { Button } from "@/Components/ui/button";
import { ScrollArea } from "@/Components/ui/scroll-area";
const props = defineProps({ const props = defineProps({
show: { type: Boolean, default: false }, show: { type: Boolean, default: false },
@ -452,11 +453,57 @@ const open = computed({
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<form @submit.prevent="onSubmit" class="space-y-4"> <ScrollArea class="max-h-[65vh] pr-1">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4"> <form @submit.prevent="onSubmit" class="space-y-4 pr-3">
<FormField v-slot="{ value, handleChange }" name="profile_id"> <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> <FormItem>
<FormLabel>Profil</FormLabel> <FormLabel>Pogodba</FormLabel>
<Select :model-value="value" @update:model-value="handleChange"> <Select :model-value="value" @update:model-value="handleChange">
<FormControl> <FormControl>
<SelectTrigger> <SelectTrigger>
@ -465,18 +512,22 @@ const open = computed({
</FormControl> </FormControl>
<SelectContent> <SelectContent>
<SelectItem :value="null"></SelectItem> <SelectItem :value="null"></SelectItem>
<SelectItem v-for="p in pageSmsProfiles" :key="p.id" :value="p.id"> <SelectItem v-for="c in contractsForCase" :key="c.uuid" :value="c.uuid">
{{ p.name || "Profil #" + p.id }} {{ c.reference || c.uuid }}
</SelectItem> </SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
<p class="mt-1 text-xs text-gray-500">
Uporabi podatke pogodbe (in računa) za zapolnitev {contract.*} in
{account.*} mest.
</p>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
</FormField> </FormField>
<FormField v-slot="{ value, handleChange }" name="sender_id"> <FormField v-slot="{ value, handleChange }" name="template_id">
<FormItem> <FormItem>
<FormLabel>Pošiljatelj</FormLabel> <FormLabel>Predloga</FormLabel>
<Select :model-value="value" @update:model-value="handleChange"> <Select :model-value="value" @update:model-value="handleChange">
<FormControl> <FormControl>
<SelectTrigger> <SelectTrigger>
@ -485,125 +536,77 @@ const open = computed({
</FormControl> </FormControl>
<SelectContent> <SelectContent>
<SelectItem :value="null"></SelectItem> <SelectItem :value="null"></SelectItem>
<SelectItem <SelectItem v-for="t in pageSmsTemplates" :key="t.id" :value="t.id">
v-for="s in sendersForSelectedProfile" {{ t.name || "Predloga #" + t.id }}
:key="s.id"
:value="s.id"
>
{{ s.name || s.phone || "Sender #" + s.id }}
</SelectItem> </SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
</FormField> </FormField>
</div>
<FormField v-slot="{ value, handleChange }" name="contract_uuid"> <FormField v-slot="{ componentField }" name="message">
<FormItem> <FormItem>
<FormLabel>Pogodba</FormLabel> <FormLabel>Vsebina sporočila</FormLabel>
<Select :model-value="value" @update:model-value="handleChange">
<FormControl> <FormControl>
<SelectTrigger> <Textarea
<SelectValue placeholder="—" /> rows="4"
</SelectTrigger> placeholder="Vpišite SMS vsebino..."
v-bind="componentField"
/>
</FormControl> </FormControl>
<SelectContent> <FormMessage />
<SelectItem :value="null"></SelectItem> </FormItem>
<SelectItem v-for="c in contractsForCase" :key="c.uuid" :value="c.uuid"> </FormField>
{{ 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="template_id"> <!-- Live counters -->
<FormItem> <div class="text-xs text-gray-600 flex flex-col gap-1">
<FormLabel>Predloga</FormLabel> <div>
<Select :model-value="value" @update:model-value="handleChange"> <span class="font-medium">Znakov:</span>
<FormControl> <span class="font-mono">{{ charCount }}</span>
<SelectTrigger> <span class="mx-2">|</span>
<SelectValue placeholder="—" /> <span class="font-medium">Kodiranje:</span>
</SelectTrigger> <span>{{ smsEncoding }}</span>
</FormControl> <span class="mx-2">|</span>
<SelectContent> <span class="font-medium">Deli SMS:</span>
<SelectItem :value="null"></SelectItem> <span class="font-mono">{{ segments }}</span>
<SelectItem v-for="t in pageSmsTemplates" :key="t.id" :value="t.id"> <span class="mx-2">|</span>
{{ t.name || "Predloga #" + t.id }} <span class="font-medium">Krediti:</span>
</SelectItem> <span class="font-mono">{{ creditsNeeded }}</span>
</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>
</div> </div>
</FormItem> <div>
</FormField> <span class="font-medium">Omejitev:</span>
</form> <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> <DialogFooter>
<Button variant="outline" @click="closeSmsDialog" :disabled="processing"> <Button variant="outline" @click="closeSmsDialog" :disabled="processing">

View File

@ -308,7 +308,7 @@ const closeSearch = () => (searchOpen.value = false);
</div> </div>
<!-- Page Heading --> <!-- 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"> <div class="max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8 space-y-2">
<Breadcrumbs <Breadcrumbs
v-if="$page.props.breadcrumbs && $page.props.breadcrumbs.length" 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 { Badge } from "@/Components/ui/badge";
import { Separator } from "@/Components/ui/separator"; import { Separator } from "@/Components/ui/separator";
import { import {
Collapsible, Accordion,
CollapsibleContent, AccordionContent,
CollapsibleTrigger, AccordionItem,
} from "@/Components/ui/collapsible"; AccordionTrigger,
} from "@/Components/ui/accordion";
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
@ -46,7 +47,6 @@ import { Checkbox } from "@/Components/ui/checkbox";
import { import {
ArrowLeft, ArrowLeft,
CheckCircle2, CheckCircle2,
ChevronDown,
FileText, FileText,
Calendar, Calendar,
Euro, Euro,
@ -321,7 +321,7 @@ const clientSummary = computed(() => {
<div class="py-4 sm:py-6"> <div class="py-4 sm:py-6">
<div class="mx-auto max-w-5xl px-2 sm:px-4 space-y-4"> <div class="mx-auto max-w-5xl px-2 sm:px-4 space-y-4">
<!-- Client details (account holder) --> <!-- Client details (account holder) -->
<Card> <Card class="gap-3">
<CardHeader> <CardHeader>
<CardTitle class="flex items-center gap-2 text-base"> <CardTitle class="flex items-center gap-2 text-base">
<Building2 class="w-5 h-5 text-gray-500" /> <Building2 class="w-5 h-5 text-gray-500" />
@ -340,8 +340,8 @@ const clientSummary = computed(() => {
</Card> </Card>
<!-- Person (case person) --> <!-- Person (case person) -->
<Card> <Card class="gap-3">
<CardHeader> <CardHeader class="px-3">
<CardTitle class="flex items-center gap-2 text-base"> <CardTitle class="flex items-center gap-2 text-base">
<User class="w-5 h-5 text-gray-500" /> <User class="w-5 h-5 text-gray-500" />
<span class="truncate">{{ client_case.person.full_name }}</span> <span class="truncate">{{ client_case.person.full_name }}</span>
@ -354,7 +354,7 @@ const clientSummary = computed(() => {
{{ client_case.person.description }} {{ client_case.person.description }}
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent class="px-3">
<Separator class="mb-4" /> <Separator class="mb-4" />
<PersonDetailPhone <PersonDetailPhone
:types="types" :types="types"
@ -365,35 +365,32 @@ const clientSummary = computed(() => {
</Card> </Card>
<!-- Contracts assigned to me --> <!-- Contracts assigned to me -->
<Card> <Card class="p-0 pt-3 gap-1">
<CardHeader> <CardHeader class="px-4">
<CardTitle class="flex items-center gap-2"> <CardTitle class="flex items-center gap-2">
<FileText class="w-5 h-5" /> <FileText class="w-5 h-5" />
Pogodbe Pogodbe
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
<CardContent class="space-y-3"> <CardContent class="p-2">
<Card <Card
v-for="c in contracts" v-for="c in contracts"
:key="c.uuid || c.id" :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 --> <!-- Contract header: reference + type badge -->
<CardHeader class="pb-2"> <CardHeader class="p-3 pb-2">
<div class="flex items-center gap-2 flex-wrap"> <div class="flex items-center flex-wrap">
<CardTitle class="text-base font-semibold"> <CardTitle class="text-base font-semibold">
{{ c.reference || c.uuid }} {{ c.reference || "Šifra pogodbe ni določena" }}
</CardTitle> </CardTitle>
<Badge v-if="c.type?.name" variant="secondary" class="text-[11px]">
{{ c.type.name }}
</Badge>
</div> </div>
</CardHeader> </CardHeader>
<!-- Balance row --> <!-- Balance row -->
<div <div
v-if="c.account" 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"> <div class="flex items-center gap-2 text-red-500">
<Euro class="w-4 h-4 shrink-0" /> <Euro class="w-4 h-4 shrink-0" />
@ -413,75 +410,76 @@ const clientSummary = computed(() => {
v-if=" v-if="
c.description || c.last_object || (c.meta && Object.keys(c.meta).length) 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 --> <!-- Description + Meta Accordion -->
<template v-if="c.description"> <template v-if="c.description || (c.meta && Object.keys(c.meta).length)">
<Separator class="mb-3" /> <Separator />
<Collapsible> <Accordion type="multiple" class="w-full">
<CollapsibleTrigger <AccordionItem
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" v-if="c.description"
value="description"
class="border-b-0"
> >
<ChevronDown <AccordionTrigger
class="w-3.5 h-3.5 transition-transform duration-200 group-data-[state=open]:rotate-180 shrink-0" class="px-3 py-2 text-xs font-medium uppercase tracking-wide hover:no-underline"
/>
<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"
> >
{{ c.description }} Opis
</p> </AccordionTrigger>
</CollapsibleContent> <AccordionContent class="px-3 pb-3">
</Collapsible> <p
</template> 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"
<!-- 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"
> >
<span {{ c.description }}
class="text-xs text-gray-500 dark:text-gray-400 shrink-0" </p>
>{{ val?.title || key }}</span </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>
</div> </AccordionTrigger>
</CollapsibleContent> <AccordionContent class="pb-2">
</Collapsible> <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> </template>
<!-- Last object --> <!-- Last object -->

File diff suppressed because it is too large Load Diff