Some work on customers view #3

This commit is contained in:
2025-11-04 13:51:31 +01:00
parent 3c2d35f458
commit 82e04acc2c
13 changed files with 154 additions and 51 deletions
+110 -38
View File
@@ -11,7 +11,9 @@ import { randomInt, bgColorForString } from '@/lib/utils'
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
import { Input } from '@/components/ui/input'
import { Button } from '@/components/ui/button'
import { Copy, Delete, Edit, Phone, Plus, Search } from "lucide-vue-next"
import { Badge } from '@/components/ui/badge'
import { ButtonGroup, ButtonGroupSeparator, ButtonGroupText, } from '@/components/ui/button-group'
import { Copy, Delete, Edit, Globe, House, LayoutGrid, LayoutList, Phone, Plus, Rows3, Rows4, Search } from "lucide-vue-next"
import Fuse from 'fuse.js';
import { getInitials } from '@/composables/useInitials';
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, } from '@/components/ui/card'
@@ -46,12 +48,12 @@ onMounted(async () => {
})
watch(activeCustomer, () => {
console.log(activeCustomer.value?.companyName)
})
const fuse = computed(() => {
return new Fuse(customersData.value, {
keys: ['companyName', 'firstName', 'lastName', 'email', 'phone'],
keys: ['companyName', 'contacts.firstName', 'contacts.lastName'],
threshold: 0.3
});
})
@@ -64,28 +66,41 @@ const filteredCustomers = computed(() => {
})
const addressToClipbard = async function (address: Address) {
const addressToClipbard = async function (companyName: string | null, address: Address | null, event: Event) {
event.stopPropagation();
try {
await navigator.clipboard.writeText(
address.lineOne + '\n' +
(address.lineTwo ? address.lineTwo + '\n' : '') +
address.postalCode + ' ' + address.city
)
let copy = '';
if (companyName) copy += companyName + '\n'
if (address) {
copy +=
address.lineOne + '\n' +
(address.lineTwo ? address.lineTwo + '\n' : '') +
address.postalCode + ' ' + address.city
}
await navigator.clipboard.writeText(copy)
toast('Adresse kopiert', { duration: 2000 })
} catch (notAllowedError: DOMException) {
if (notAllowedError instanceof Error) {
toast.error(notAllowedError.name, { description: notAllowedError.message })
}
}
}
const showDetail = (customer: Customer) => {
// make a deep copy, so the changes in the dialog wont affect the data until saved
activeCustomer.value = JSON.parse(JSON.stringify(customer))
detailDialogOpen.value = true
}
const browse = (url: string, event: Event) => {
event.stopPropagation();
window.open(url, '_blank')
}
const call = (number: string, event: Event) => {
event.stopPropagation();
window.open('tel:' + number, '_self')
}
</script>
<template>
@@ -93,56 +108,113 @@ const showDetail = (customer: Customer) => {
<Head title="Dashboard" />
<AppLayout :breadcrumbs="breadcrumbs">
<div class="flex h-full flex-1 flex-col gap-4 overflow-x-auto p-4 bg-slate-50 dark:bg-neutral-900/40">
<div
class="flex h-full flex-1 flex-col gap-4 overflow-x-auto p-4 lg:p-8 print:bg-transparent print:p-0 print:m-0">
<div class="flex">
<div class="relative w-full max-w-sm items-center mx-auto mb-6">
<Input ref="search-field" id="search" type="text" placeholder="Suchen" class="px-8"
v-model="searchQuery" autofocus />
<!-- Function Header -->
<div id="function-header" class="flex row justify-between items-center mb-4 gap-4">
<!-- View buttons -->
<ButtonGroup aria-label="Button group">
<Button variant="pressed" size="sm">
<LayoutGrid stroke-width="1.5" />
</Button>
<Button variant="outline" size="sm">
<Rows3 stroke-width="1.5" />
</Button>
</ButtonGroup>
<!-- Search field -->
<div class="relative w-full max-w-sm items-center">
<Input ref="search-field" id="search" type="text" placeholder="Filtern" class="px-8 bg-background"
v-model="searchQuery" />
<span class="absolute start-0 inset-y-0 flex items-center justify-center px-2">
<Search class="size-4 text-muted-foreground" :stroke-width="1.5" />
</span>
<span class="absolute end-0 inset-y-0 flex items-center justify-center px-0">
<span class="absolute end-0 inset-y-0 flex items-center justify-center px-0 mr-1">
<Button :size="'sm'" :variant="'ghost'" @click="searchQuery = ''; searchField.focus()">
<Delete class="size-4 text-muted-foreground" :stroke-width="1.5" />
</Button>
</span>
</div>
<Button :size="'sm'" :variant="'action'" @click="">
<Plus /> Neu
<!-- New button -->
<Button size="sm" variant="action" @click="">
<Plus stroke-width="1.5" /> Neu
</Button>
</div>
<div class="columns-xs gap-4">
<div class="columns-xs gap-6">
<Card v-for="customer in filteredCustomers" :key="customer.id"
class="mb-4 shadow-xl break-inside-avoid hover:bg-accent" @click="showDetail(customer)">
<CardHeader>
<CardTitle>{{ customer.companyName }}</CardTitle>
<CardDescription>Kategorie / Umsatz</CardDescription>
class="relative mb-6 break-inside-avoid hover:bg-accent active:shadow-none overflow-clip"
@click="showDetail(customer)">
<CardHeader v-if="customer.logo" class="z-0">
<img :src="'storage/uploads/' + customer.logo" alt="Logo {{ customer.companyName }}"
class="max-h-8 max-w-[50%]">
</CardHeader>
<CardContent class="text-sm">
<address class="relative not-italic my-2">
{{ customer.billingAddress.lineOne }}<br />
{{ customer.billingAddress.lineTwo }}<br v-if="customer.billingAddress.lineTwo" />
{{ customer.billingAddress.postalCode }} {{ customer.billingAddress.city }}
<Button class="absolute end-0 top-0" size="sm" variant="ghost"
@click="addressToClipbard(customer.billingAddress)">
<Copy :stroke-width="1.5" />
</Button>
<CardContent class="flex justify-between gap-4 flex-col sm:flex-row pr-4 z-0">
<address class="not-italic">
<CardTitle>
{{ customer.companyName }}
<!-- <Badge variant="secondary">Badge</Badge> -->
</CardTitle>
<CardDescription class="mt-2">
{{ customer.billingAddress?.lineOne }}<br />
{{ customer.billingAddress?.lineTwo }}<br v-if="customer.billingAddress?.lineTwo" />
{{ customer.billingAddress?.postalCode }} {{ customer.billingAddress?.city }}
</CardDescription>
</address>
<a href="https://www.tooloop.de">www.tooloop.de</a><br />
<a href="tel://+4982165079983">+49 821 650799-83</a><br />
<div class="flex items-start">
<TooltipProvider>
<Tooltip v-if="customer.url">
<TooltipTrigger>
<Button variant="ghost" size="sm"
@click="(event: Event) => browse(customer.url as string, event)">
<Globe stroke-width="1.5" />
</Button>
</TooltipTrigger>
<TooltipContent>
{{ customer.url }} öffnen
</TooltipContent>
</Tooltip>
<Tooltip v-if="customer.phone">
<TooltipTrigger>
<Button variant="ghost" size="sm"
@click="(event: Event) => call(customer.phone as string, event)">
<Phone stroke-width="1.5" />
</Button>
</TooltipTrigger>
<TooltipContent>
{{ customer.phone }} anrufen
</TooltipContent>
</Tooltip>
<Tooltip v-if="customer.billingAddress">
<TooltipTrigger>
<Button variant="ghost" size="sm"
@click="(event: Event) => addressToClipbard(customer.companyName, customer.billingAddress as Address | null, event)">
<House stroke-width="1.5" />
</Button>
</TooltipTrigger>
<TooltipContent>
Adresse in Zwischenablage kopieren
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
</CardContent>
<CardFooter>
<CardFooter v-if="customer.contacts.length > 0">
<TooltipProvider :delay-duration="0">
<Tooltip v-for="contact in customer.contacts">
<TooltipTrigger>
<Avatar class="-mr-2 border-2 size-14">
<AvatarImage :src="contact.avatar || ''" />
<Avatar class="-mr-2 size-14 shadow">
<AvatarImage v-if="contact.avatar"
:src="'/storage/uploads/' + contact.avatar" loading="lazy" />
<AvatarFallback
:class="bgColorForString(getInitials(contact.firstName + ' ' + contact.lastName))">
{{ getInitials(contact.firstName + ' ' + contact.lastName) }}