Some work on customers view #3
This commit is contained in:
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
use App\Models\Customer;
|
use App\Models\Customer;
|
||||||
use App\Support\ApiDataTransformer;
|
use App\Support\ApiDataTransformer;
|
||||||
use Pest\ArchPresets\Custom;
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
|
||||||
class CustomerController extends Controller
|
class CustomerController extends Controller
|
||||||
{
|
{
|
||||||
@@ -51,4 +51,20 @@ public static function generateCustomerNumber()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// // Beispiel für das Hochladen eines Kundenlogos
|
||||||
|
// if ($request->hasFile('customer_logo')) {
|
||||||
|
// $path = $request->file('customer_logo')->store('customer_logos', 'public');
|
||||||
|
// // Speichere den Pfad in der Datenbank
|
||||||
|
// $customer->logo_path = $path;
|
||||||
|
// $customer->save();
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Beispiel für das Hochladen eines Kontakt-Avatars
|
||||||
|
// if ($request->hasFile('contact_avatar')) {
|
||||||
|
// $path = $request->file('contact_avatar')->store('contact_avatars', 'public');
|
||||||
|
// // Speichere den Pfad in der Datenbank
|
||||||
|
// $contact->avatar_path = $path;
|
||||||
|
// $contact->save();
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ class Customer extends Model
|
|||||||
'tax_id', // Tax identification number
|
'tax_id', // Tax identification number
|
||||||
'global_id', // Global Location Number (GLN) or other identification
|
'global_id', // Global Location Number (GLN) or other identification
|
||||||
'legal_registration_id', // Legal registration ID
|
'legal_registration_id', // Legal registration ID
|
||||||
|
'url',
|
||||||
'email',
|
'email',
|
||||||
'phone',
|
'phone',
|
||||||
'billing_address',
|
'billing_address',
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ public function definition()
|
|||||||
'tax_id' => $this->faker->numerify('DE##########'),
|
'tax_id' => $this->faker->numerify('DE##########'),
|
||||||
'global_id' => $this->faker->numerify('############'),
|
'global_id' => $this->faker->numerify('############'),
|
||||||
'legal_registration_id' => $this->faker->numerify('##########'),
|
'legal_registration_id' => $this->faker->numerify('##########'),
|
||||||
|
'url' => $this->faker->url(),
|
||||||
'email' => $this->faker->unique()->safeEmail(),
|
'email' => $this->faker->unique()->safeEmail(),
|
||||||
'phone' => $this->faker->phoneNumber(),
|
'phone' => $this->faker->phoneNumber(),
|
||||||
'billing_address' => [
|
'billing_address' => [
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ public function up()
|
|||||||
$table->string('tax_id', 50)->nullable();
|
$table->string('tax_id', 50)->nullable();
|
||||||
$table->string('global_id', 50)->nullable();
|
$table->string('global_id', 50)->nullable();
|
||||||
$table->string('legal_registration_id', 50)->nullable();
|
$table->string('legal_registration_id', 50)->nullable();
|
||||||
|
$table->string('url', 100)->nullable();
|
||||||
$table->string('email', 100)->nullable();
|
$table->string('email', 100)->nullable();
|
||||||
$table->string('phone', 20)->nullable();
|
$table->string('phone', 20)->nullable();
|
||||||
$table->json('billing_address')->nullable();
|
$table->json('billing_address')->nullable();
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ :root {
|
|||||||
--secondary-foreground: hsl(0 0% 9%);
|
--secondary-foreground: hsl(0 0% 9%);
|
||||||
--muted: var(--color-slate-50);
|
--muted: var(--color-slate-50);
|
||||||
--muted-foreground: var(--color-slate-400);
|
--muted-foreground: var(--color-slate-400);
|
||||||
--accent: var(--color-slate-100);
|
--accent: var(--color-stone-50);
|
||||||
--accent-foreground: hsl(0 0% 9%);
|
--accent-foreground: hsl(0 0% 9%);
|
||||||
--destructive: var(--color-red-500);
|
--destructive: var(--color-red-500);
|
||||||
--destructive-foreground: hsl(0 0% 98%);
|
--destructive-foreground: hsl(0 0% 98%);
|
||||||
|
|||||||
@@ -86,13 +86,13 @@ const footerNavItems: NavItem[] = [
|
|||||||
<Sidebar collapsible="icon" variant="inset">
|
<Sidebar collapsible="icon" variant="inset">
|
||||||
|
|
||||||
<SidebarHeader>
|
<SidebarHeader>
|
||||||
<Link :href="dashboard()" class="flex row items-center">
|
<Link :href="dashboard()" class="flex row items-center mt-6">
|
||||||
<AppLogo />
|
<AppLogo />
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger class="w-fit absolute right-3 group-data-[collapsible=icon]:right-5">
|
<TooltipTrigger class="w-fit absolute top-11 right-3 group-data-[collapsible=icon]:right-5">
|
||||||
<SidebarTrigger class="hidden md:flex text-primary-foreground" />
|
<SidebarTrigger class="hidden md:flex text-primary-foreground" />
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
|
|||||||
@@ -34,12 +34,12 @@ watch(() => props.customerData as Customer,
|
|||||||
isDirty.value = false
|
isDirty.value = false
|
||||||
isLoading.value = true
|
isLoading.value = true
|
||||||
|
|
||||||
console.log("customerData", "Dirty: " + isDirty.value, "loading: " + isLoading.value)
|
// console.log("customerData", "Dirty: " + isDirty.value, "loading: " + isLoading.value)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
watch(customer, (newValue) => {
|
watch(customer, (newValue) => {
|
||||||
console.log(newValue)
|
// console.log(newValue)
|
||||||
})
|
})
|
||||||
|
|
||||||
const saveChanges = () => {
|
const saveChanges = () => {
|
||||||
@@ -101,7 +101,7 @@ const cancelChanges = (event: Event | null) => {
|
|||||||
|
|
||||||
|
|
||||||
<div class="overflow-y-auto px-6">
|
<div class="overflow-y-auto px-6">
|
||||||
<div id="document-header"
|
<div
|
||||||
class="block sticky top-0 py-4 bg-slate-100 bg-white dark:bg-neutral-800 z-1 flex items-end gap-12">
|
class="block sticky top-0 py-4 bg-slate-100 bg-white dark:bg-neutral-800 z-1 flex items-end gap-12">
|
||||||
<div class="grow">
|
<div class="grow">
|
||||||
<DialogTitle class="text-xl text-primary-foreground font-bold">Edit profile</DialogTitle>
|
<DialogTitle class="text-xl text-primary-foreground font-bold">Edit profile</DialogTitle>
|
||||||
@@ -110,6 +110,14 @@ const cancelChanges = (event: Event | null) => {
|
|||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="flex-none md:flex gap-12 mt-6 2 p-6 bg-slate-100 dark:bg-neutral-900 rounded-lg">
|
||||||
|
Customer Form hier
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="px-4 mt-6">
|
||||||
|
Contacts hier
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogFooter></DialogFooter>
|
<DialogFooter></DialogFooter>
|
||||||
|
|||||||
@@ -84,8 +84,10 @@ watch(invoice,
|
|||||||
(newValue, oldValue) => {
|
(newValue, oldValue) => {
|
||||||
|
|
||||||
if (isLoading.value) {
|
if (isLoading.value) {
|
||||||
|
if (!invoice.value) return;
|
||||||
|
|
||||||
// Initial load of invoice data
|
// Initial load of invoice data
|
||||||
if (invoice.value && !invoice.value.billingData) {
|
if (!invoice.value.billingData) {
|
||||||
// Set default billing data from customer
|
// Set default billing data from customer
|
||||||
invoice.value.billingData = {
|
invoice.value.billingData = {
|
||||||
companyName: invoice.value.customer?.companyName || "",
|
companyName: invoice.value.customer?.companyName || "",
|
||||||
@@ -104,7 +106,7 @@ watch(invoice,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (invoice.value && invoice.value.customer?.id !== 0) {
|
if (invoice.value.customer?.id !== 0) {
|
||||||
importCustomer.value = invoice.value.customer as Customer
|
importCustomer.value = invoice.value.customer as Customer
|
||||||
|
|
||||||
// console.log("billingData contact", invoice.value?.billingData?.contactFirstName, invoice.value?.billingData?.contactLastName)
|
// console.log("billingData contact", invoice.value?.billingData?.contactFirstName, invoice.value?.billingData?.contactLastName)
|
||||||
@@ -120,7 +122,6 @@ watch(invoice,
|
|||||||
}
|
}
|
||||||
|
|
||||||
value.value = fromDate(new Date(invoice.value.invoiceDate), getLocalTimeZone())
|
value.value = fromDate(new Date(invoice.value.invoiceDate), getLocalTimeZone())
|
||||||
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
isDirty.value = true
|
isDirty.value = true
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ const recalculatePositions = () => {
|
|||||||
|
|
||||||
<!-- Preis -->
|
<!-- Preis -->
|
||||||
<TableCell class="w-1/8 text-right tabular-nums">
|
<TableCell class="w-1/8 text-right tabular-nums">
|
||||||
<NumberInput :modelValue="Number(element.price)"
|
<NumberInput v-model="element.price"
|
||||||
class="bg-transparent p-1 h-7! dark:bg-transparent hover:bg-background/66 dark:hover:bg-background/66 rounded shadow-none!" />
|
class="bg-transparent p-1 h-7! dark:bg-transparent hover:bg-background/66 dark:hover:bg-background/66 rounded shadow-none!" />
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
|||||||
v-bind="forwarded"
|
v-bind="forwarded"
|
||||||
:class="
|
:class="
|
||||||
cn(
|
cn(
|
||||||
'fixed left-1/2 top-1/2 z-50 grid w-full max-w-lg -translate-x-1/2 -translate-y-1/2 gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-bottom-[5%] data-[state=open]:slide-in-from-bottom-[5%] sm:rounded-lg',
|
'fixed left-1/2 top-1/2 z-50 grid max-w-lg -translate-x-1/2 -translate-y-1/2 gap-8 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-bottom-[5%] data-[state=open]:slide-in-from-bottom-[5%] sm:rounded-lg',
|
||||||
props.class,
|
props.class,
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ export default defineComponent({
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<vue-number class="file:text-foreground placeholder:text-muted-foreground dark:bg-input/30 border-input flex h-9 min-w-4 rounded-md bg-input px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive tabular-nums h-6 py-0 px-1 m-0 bg-transparent border-none dark:bg-transparent hover:border-1
|
<vue-number class="file:text-foreground border-input flex min-w-4 rounded-lg text-base transition-[color,box-shadow] outline-none disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive tabular-nums h-6 py-0 px-1 m-0 bg-transparent border-none dark:bg-transparent hover:border-1
|
||||||
dark:hover:border-1 placeholder:text-muted-foreground/30 shadow-none w-28 text-right" v-model="value"
|
dark:hover:border-1 placeholder:text-muted-foreground/30 shadow-none w-28 text-right" v-model="value"
|
||||||
v-bind="number" />
|
v-bind="number" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -11,7 +11,9 @@ import { randomInt, bgColorForString } from '@/lib/utils'
|
|||||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import { Button } from '@/components/ui/button'
|
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 Fuse from 'fuse.js';
|
||||||
import { getInitials } from '@/composables/useInitials';
|
import { getInitials } from '@/composables/useInitials';
|
||||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, } from '@/components/ui/card'
|
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, } from '@/components/ui/card'
|
||||||
@@ -46,12 +48,12 @@ onMounted(async () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
watch(activeCustomer, () => {
|
watch(activeCustomer, () => {
|
||||||
console.log(activeCustomer.value?.companyName)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const fuse = computed(() => {
|
const fuse = computed(() => {
|
||||||
return new Fuse(customersData.value, {
|
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 {
|
try {
|
||||||
await navigator.clipboard.writeText(
|
let copy = '';
|
||||||
address.lineOne + '\n' +
|
if (companyName) copy += companyName + '\n'
|
||||||
(address.lineTwo ? address.lineTwo + '\n' : '') +
|
if (address) {
|
||||||
address.postalCode + ' ' + address.city
|
copy +=
|
||||||
)
|
address.lineOne + '\n' +
|
||||||
|
(address.lineTwo ? address.lineTwo + '\n' : '') +
|
||||||
|
address.postalCode + ' ' + address.city
|
||||||
|
}
|
||||||
|
await navigator.clipboard.writeText(copy)
|
||||||
toast('Adresse kopiert', { duration: 2000 })
|
toast('Adresse kopiert', { duration: 2000 })
|
||||||
|
|
||||||
} catch (notAllowedError: DOMException) {
|
} catch (notAllowedError: DOMException) {
|
||||||
if (notAllowedError instanceof Error) {
|
if (notAllowedError instanceof Error) {
|
||||||
toast.error(notAllowedError.name, { description: notAllowedError.message })
|
toast.error(notAllowedError.name, { description: notAllowedError.message })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const showDetail = (customer: Customer) => {
|
const showDetail = (customer: Customer) => {
|
||||||
// make a deep copy, so the changes in the dialog won’t affect the data until saved
|
// make a deep copy, so the changes in the dialog won’t affect the data until saved
|
||||||
activeCustomer.value = JSON.parse(JSON.stringify(customer))
|
activeCustomer.value = JSON.parse(JSON.stringify(customer))
|
||||||
detailDialogOpen.value = true
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -93,56 +108,113 @@ const showDetail = (customer: Customer) => {
|
|||||||
<Head title="Dashboard" />
|
<Head title="Dashboard" />
|
||||||
|
|
||||||
<AppLayout :breadcrumbs="breadcrumbs">
|
<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">
|
<!-- Function Header -->
|
||||||
<div class="relative w-full max-w-sm items-center mx-auto mb-6">
|
<div id="function-header" class="flex row justify-between items-center mb-4 gap-4">
|
||||||
<Input ref="search-field" id="search" type="text" placeholder="Suchen" class="px-8"
|
<!-- View buttons -->
|
||||||
v-model="searchQuery" autofocus />
|
<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">
|
<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" />
|
<Search class="size-4 text-muted-foreground" :stroke-width="1.5" />
|
||||||
</span>
|
</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()">
|
<Button :size="'sm'" :variant="'ghost'" @click="searchQuery = ''; searchField.focus()">
|
||||||
<Delete class="size-4 text-muted-foreground" :stroke-width="1.5" />
|
<Delete class="size-4 text-muted-foreground" :stroke-width="1.5" />
|
||||||
</Button>
|
</Button>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button :size="'sm'" :variant="'action'" @click="">
|
<!-- New button -->
|
||||||
<Plus /> Neu
|
<Button size="sm" variant="action" @click="">
|
||||||
|
<Plus stroke-width="1.5" /> Neu
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div class="columns-xs gap-4">
|
<div class="columns-xs gap-6">
|
||||||
|
|
||||||
<Card v-for="customer in filteredCustomers" :key="customer.id"
|
<Card v-for="customer in filteredCustomers" :key="customer.id"
|
||||||
class="mb-4 shadow-xl break-inside-avoid hover:bg-accent" @click="showDetail(customer)">
|
class="relative mb-6 break-inside-avoid hover:bg-accent active:shadow-none overflow-clip"
|
||||||
<CardHeader>
|
@click="showDetail(customer)">
|
||||||
<CardTitle>{{ customer.companyName }}</CardTitle>
|
|
||||||
<CardDescription>Kategorie / Umsatz</CardDescription>
|
<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>
|
</CardHeader>
|
||||||
<CardContent class="text-sm">
|
|
||||||
<address class="relative not-italic my-2">
|
<CardContent class="flex justify-between gap-4 flex-col sm:flex-row pr-4 z-0">
|
||||||
{{ customer.billingAddress.lineOne }}<br />
|
<address class="not-italic">
|
||||||
{{ customer.billingAddress.lineTwo }}<br v-if="customer.billingAddress.lineTwo" />
|
<CardTitle>
|
||||||
{{ customer.billingAddress.postalCode }} {{ customer.billingAddress.city }}
|
{{ customer.companyName }}
|
||||||
<Button class="absolute end-0 top-0" size="sm" variant="ghost"
|
<!-- <Badge variant="secondary">Badge</Badge> -->
|
||||||
@click="addressToClipbard(customer.billingAddress)">
|
</CardTitle>
|
||||||
<Copy :stroke-width="1.5" />
|
<CardDescription class="mt-2">
|
||||||
</Button>
|
{{ customer.billingAddress?.lineOne }}<br />
|
||||||
|
{{ customer.billingAddress?.lineTwo }}<br v-if="customer.billingAddress?.lineTwo" />
|
||||||
|
{{ customer.billingAddress?.postalCode }} {{ customer.billingAddress?.city }}
|
||||||
|
</CardDescription>
|
||||||
</address>
|
</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>
|
</CardContent>
|
||||||
<CardFooter>
|
|
||||||
|
<CardFooter v-if="customer.contacts.length > 0">
|
||||||
<TooltipProvider :delay-duration="0">
|
<TooltipProvider :delay-duration="0">
|
||||||
|
|
||||||
<Tooltip v-for="contact in customer.contacts">
|
<Tooltip v-for="contact in customer.contacts">
|
||||||
<TooltipTrigger>
|
<TooltipTrigger>
|
||||||
<Avatar class="-mr-2 border-2 size-14">
|
<Avatar class="-mr-2 size-14 shadow">
|
||||||
<AvatarImage :src="contact.avatar || ''" />
|
<AvatarImage v-if="contact.avatar"
|
||||||
|
:src="'/storage/uploads/' + contact.avatar" loading="lazy" />
|
||||||
<AvatarFallback
|
<AvatarFallback
|
||||||
:class="bgColorForString(getInitials(contact.firstName + ' ' + contact.lastName))">
|
:class="bgColorForString(getInitials(contact.firstName + ' ' + contact.lastName))">
|
||||||
{{ getInitials(contact.firstName + ' ' + contact.lastName) }}
|
{{ getInitials(contact.firstName + ' ' + contact.lastName) }}
|
||||||
|
|||||||
Vendored
+3
@@ -105,6 +105,7 @@ export interface Customer {
|
|||||||
customerNr: number | null;
|
customerNr: number | null;
|
||||||
vatId: string | null;
|
vatId: string | null;
|
||||||
contacts: Contact[];
|
contacts: Contact[];
|
||||||
|
url?: string;
|
||||||
email?: string;
|
email?: string;
|
||||||
phone?: string;
|
phone?: string;
|
||||||
billingAddress?: Address;
|
billingAddress?: Address;
|
||||||
@@ -112,6 +113,7 @@ export interface Customer {
|
|||||||
status: string;
|
status: string;
|
||||||
avatar?: string | null;
|
avatar?: string | null;
|
||||||
notes?: string;
|
notes?: string;
|
||||||
|
logo: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CustomerType = Customer
|
export type CustomerType = Customer
|
||||||
@@ -126,6 +128,7 @@ export function newCustomer(): Customer {
|
|||||||
billingAddress: newAddress(),
|
billingAddress: newAddress(),
|
||||||
paymentTerms: newPaymentTerms(),
|
paymentTerms: newPaymentTerms(),
|
||||||
status: 'active',
|
status: 'active',
|
||||||
|
logo: null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user