work on customer editor #6

This commit is contained in:
2025-11-21 08:39:34 +01:00
parent c167d7759e
commit 451c4912a5
5 changed files with 291 additions and 129 deletions
+214 -78
View File
@@ -1,9 +1,25 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, watch, onMounted, onUpdated, useTemplateRef } from "vue" import { ref, computed, watch } from "vue"
import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, } from '@/components/ui/dialog'; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogFooter, DialogClose } from "@/components/ui/dialog"
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/crm-button'
import { Textarea } from "@/components/ui/textarea";
import { Input } from '@/components/ui/input';
import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput, InputGroupText, InputGroupTextarea } from '@/components/ui/crm-input-group'
import { Customer } from '@/types' import { Customer } from '@/types'
import { Check, Ellipsis, Trash } from "lucide-vue-next" import { toast } from "vue-sonner"
import { alertStore } from "@/stores/alertStore"
import { Trash2, Loader2, Ellipsis, Check, ImportIcon, Mail, Notebook, Send, CornerDownLeft, Plus, CirclePlus, Phone, PhoneCall } from "lucide-vue-next"
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'
import DialogCloseButton from "./DialogCloseButton/DialogCloseButton.vue";
import { Table, TableCell, TableFooter, TableHead, TableHeader, TableRow, } from '@/components/ui/table';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
import { Separator } from '@/components/ui/separator'
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
import { bgColorForString, toLocalDate, randomDate, loremIpsum } from '@/lib/utils'
import { getInitials } from '@/composables/useInitials';
import GrowingTextarea from "./ui/growing-textarea/GrowingTextarea.vue";
import { Kbd, KbdGroup } from '@/components/ui/kbd'
import Checkbox from "./ui/checkbox/Checkbox.vue";
const props = defineProps<{ const props = defineProps<{
@@ -16,6 +32,9 @@ const emit = defineEmits(['update:modelValue', 'update:open', 'save', 'cancel',
const customer = ref<Customer | null>(props.customerData) const customer = ref<Customer | null>(props.customerData)
const isDirty = ref(false); const isDirty = ref(false);
const isLoading = ref(false); const isLoading = ref(false);
const isSaving = ref(false)
const alert = alertStore()
const isTakingNote = ref(false)
const isOpen = computed({ const isOpen = computed({
get: () => props.modelValue, get: () => props.modelValue,
@@ -25,49 +44,60 @@ const isOpen = computed({
}) })
// watch for new external invoice data // watch for new external invoice data
watch(() => props.customerData as Customer, watch(() => props.modelValue, (open) => {
(newValue, oldValue) => { // on open
if (newValue == oldValue) return if (open) {
customer.value = newValue
// Set initial state for a newly opened document // Set initial state for a newly opened document
isDirty.value = false isDirty.value = false
isLoading.value = true isLoading.value = true
// console.log("customerData", "Dirty: " + isDirty.value, "loading: " + isLoading.value) customer.value = props.customerData
} }
)
watch(customer, (newValue) => {
// console.log(newValue)
}) })
const saveChanges = () => { watch(customer, (newValue) => {
// if (invoice.value) { // isDirty.value = true
// emit('save', invoice.value) })
// isOpen.value = false
// } const save = () => {
if (customer.value) {
// add spinner to save button
isSaving.value = true
try {
// TODO: do the actual saving
emit('save', invoice.value)
} catch (error) {
toast.error("Rechnung konnte nicht gespeichert werden", {
description: (error as Error).message,
})
} finally {
// remove spinner from save button
isSaving.value = false
setTimeout(() => { isDirty.value = false }, 1000)
}
}
} }
const cancelChanges = (event: Event | null) => { const cancel = (event: Event | null) => {
// if (isDirty.value) { if (!event) return
// alert.value.title = "Wirklich schließen?"
// alert.value.message = "Es gibt ungespeicherte Änderungen, die dann verloren gehen." event.preventDefault()
// alert.value.cancelText = "Abbrechen" event.returnValue = true
// alert.value.onCancel = () => {
// event?.preventDefault() if (isDirty.value) {
// event.returnValue = true alert.show(
// alert.value.open = false "Wirklich schließen?",
// } "Es gibt ungespeicherte Änderungen, die dann verloren gehen.",
// alert.value.confirmText = "Schließen"
// alert.value.onConfirm = () => {
// emit('cancel')
// isOpen.value = false
// alert.value.open = false
// }
// alert.value.open = true
// } else
{ {
actionText: "Änderungen verwerfen",
onAction: () => {
emit('cancel')
isOpen.value = false
}
}
)
} else {
emit('cancel') emit('cancel')
isOpen.value = false isOpen.value = false
} }
@@ -80,7 +110,7 @@ const handleLogoUpload = (event: Event) => {
// Hier könntest du die Datei validieren (Größe, Typ, etc.) // Hier könntest du die Datei validieren (Größe, Typ, etc.)
if (file.size > 2 * 1024 * 1024) { // 2 MB if (file.size > 2 * 1024 * 1024) { // 2 MB
alert('Die Datei ist zu groß. Maximal 2MB erlaubt.'); toast.warning('Die Datei ist zu groß', { description: 'Maximal 2 MB erlaubt' });
return; return;
} }
@@ -102,44 +132,69 @@ const handleLogoUpload = (event: Event) => {
<template> <template>
<Dialog id="customer-dialog" v-model:open="isOpen"> <Dialog id="customer-dialog" v-model:open="isOpen">
<!-- auto_480px weniger breit, wenn customer.id == 0 -->
<DialogContent <DialogContent
class="sm:max-w-[min((100%-2rem),1152px)] grid-rows-[auto_minmax(0,1fr)_auto] p-0 h-[calc(100dvh-2rem)]" class="sm:max-w-[min((100%-2rem),1344px)] grid-rows-[auto_minmax(0,1fr)_auto] h-[calc(100dvh-2rem)] gap-0 p-0 outline-none"
@escapeKeyDown="cancelChanges" @interactOutside="cancelChanges"> @escapeKeyDown="cancel" @interactOutside="cancel">
<DialogHeader class="px-3 pt-3 flex flex-row justify-end"> <DialogHeader class="p-4 md:p-6 lg:p-12 pb-0! flex flex-row items-center gap-8">
<DialogTitle class="sr-only">Kunde</DialogTitle>
<div v-if="customer && customer.id > 0" class="hidden md:flex mr-4 gap-2"> <div class="flex flex-col grow">
<Button :size="'sm'" variant="action" @click="" class="hidden" :class="{ flex: isDirty }"> <DialogTitle class="text-primary-foreground font-bold text-left">
<Check :strokeWidth="1.5" class="text-current" /> <Input v-if="customer" v-model="customer.companyName as string"
<span>Speichern</span> class="text-primary-foreground text-lg! w-full text-ellipsis px-0 bg-transparent dark:bg-transparent hover:bg-accent dark:hover:bg-accent/30 border-none shadow-none"
type="text" placeholder="Firmenname" />
</DialogTitle>
<DialogDescription class="sr-only">
{{ customer?.companyName || '' }}
</DialogDescription>
</div>
<div class="flex gap-2 items-center">
<TooltipProvider>
<!-- Save -->
<Button v-if="customer && isDirty" class="grow md:grow-0" size="sm" @click="save"
:disabled="isSaving">
<Loader2 v-if="isSaving" stroke-width="1.5" class="animate-spin" />
<Check v-else stroke-width="1.5" />
Speichern
</Button> </Button>
<Button :size="'sm'" variant="destructive" @click="">
<Trash :strokeWidth="1.5" class="text-current" /> <!-- Ellipsis menu -->
<span>Löschen</span> <DropdownMenu v-if="customer && customer.id > 0">
</Button> <DropdownMenuTrigger>
<Button :size="'sm'" variant="ghost" @click=""> <Button variant="ghost" size="sm" class="px-0! w-7 ml-2">
<Ellipsis class="text-muted-foreground" /> <Ellipsis class="size-4" stroke-width="1.5" />
</Button> </Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<!-- Delete -->
<DropdownMenuItem
class="flex justify-between text-destructive! hover:bg-red-100! dark:hover:bg-red-950!"
@click="">
<div class="flex items-center gap-3">
<Trash2 :strokeWidth="1.5" class="text-current" />
<span class="mr-2">Löschen</span>
</div>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TooltipProvider>
<DialogClose as-child>
<DialogCloseButton />
</DialogClose>
</div> </div>
</DialogHeader> </DialogHeader>
<div class="overflow-y-auto px-6"> <div class="grid grid-cols-[auto_480px] px-6 md:p-12 gap-6">
<div
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">
<DialogTitle class="text-xl text-primary-foreground font-bold">Edit profile</DialogTitle>
<DialogDescription>
Make changes to your profile here. Click save when you're done.
</DialogDescription>
</div>
</div>
<main class="overflow-y-auto pt-0! pr-3" v-if="customer">
<div class="flex-none md:flex gap-12 p-6 bg-slate-100 dark:bg-neutral-900 rounded-lg">
<div class="flex-none md:flex gap-12 mt-6 p-6 bg-slate-100 dark:bg-neutral-900 rounded-lg">
<div class="space-y-4 w-full">
<!-- Logo Upload --> <!-- Logo Upload -->
<div> <div>
@@ -161,14 +216,11 @@ const handleLogoUpload = (event: Event) => {
</div> </div>
</div> </div>
<!-- Firmenname -->
<div>
<label
class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Firmenname</label>
<input type="text" v-model="customer.companyName"
class="w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-neutral-800 dark:border-neutral-700">
</div> </div>
<div class="space-y-4 w-full">
<!-- Kunden-Nummer --> <!-- Kunden-Nummer -->
<div> <div>
<label <label
@@ -290,22 +342,106 @@ const handleLogoUpload = (event: Event) => {
</select> </select>
</div> </div>
<!-- Notizen -->
<div class="mt-6">
<label
class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Notizen</label>
<textarea v-model="customer.notes" rows="4"
class="w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-neutral-800 dark:border-neutral-700"></textarea>
</div>
</div>
</div> </div>
<div class="px-4 mt-6"> <div class="px-4 mt-6">
Contacts hier Contacts hier
</div> </div>
</main>
<aside class="bg-sidebar rounded h-full min-h-0 p-6 flex flex-col">
<!-- Todo -->
<div>
<div class="flex justify-between items-center mb-6">
<h2 class="font-bold">Nächste Schritte</h2>
<Button variant="ghost">
<Plus stroke-width="1.5" />
</Button>
</div>
<ul class="flex flex-col">
<li v-for="i in 3">
<div
class="grid grid-cols-[calc(var(--spacing)_*_6)_auto] gap-y-0 gap-x-2 items-start border-b-1 border-foreground/20 py-2.5 pl-1 pr-2">
<div
class="mt-1 row-span-2 w-5 aspect-square rounded-full border-muted-foreground border-1 flex items-center justify-center">
<div
class="w-3.5 relative aspect-square rounded-full bg-transparent has-[input:checked]:bg-muted-foreground">
<input type="checkbox" class="absolute -inset-2 opacity-0">
</div>
</div>
<div class="flex items-center gap-2">
<Input :value="loremIpsum(Math.random() * 5 + 1)"
class="my-0 px-0 text-base! h-6 border-0 outline-0 shadow-none" />
<!-- <PhoneCall stroke-width="1.5" :size="16" class="text-muted-foreground" /> -->
<!-- <Check stroke-width="1.5" :size="16" class="text-muted-foreground" /> -->
<Mail stroke-width="1.5" :size="16" class="text-muted-foreground" />
</div>
<div class="text-sm text-muted-foreground">{{ toLocalDate(randomDate()) }}</div>
</div>
</li>
</ul>
</div>
<Separator class="bg-transparent my-6" />
<!-- Write note -->
<div>
<div class="flex justify-between items-center mb-6">
<h2 class="font-bold">Notizen</h2>
<Button variant="ghost" @click="isTakingNote = !isTakingNote">
<Plus stroke-width="1.5" />
</Button>
</div>
<div :class="{ 'h-0': !isTakingNote }"
class="my-6 flex flex-col items-end gap-2 overflow-hidden transition-[height] h-[calc-size(auto)]">
<Textarea v-if="customer" v-model="customer.notes" class="bg-background resize-none"
placeholder="Neue Notiz"></Textarea>
<div class="flex gap-3">
<KbdGroup class="ml-2">
<Kbd class="visible-mac"></Kbd>
<Kbd class="visible-pc">Ctrl</Kbd>
<Kbd>
<CornerDownLeft stroke-width="1.5" class="h-3 w-3" />
</Kbd>
</KbdGroup>
<Button class="w-20" size="sm" variant="outline">
<Send stroke-width="1.5" />
</Button>
</div>
</div>
</div>
<!-- Notes -->
<div class="overflow-y-auto flex flex-col gap-6">
<article v-for="i in 5">
<div class="text-muted-foreground text-sm font-medium flex gap-3 items-center mb-1">
<Avatar class="size-6 text-xs">
<AvatarImage :src="'https://i.pravatar.cc/100?img=' + i" loading="lazy" />
<AvatarFallback :class="bgColorForString(getInitials('Daniel Stock'))">
{{ getInitials('Daniel Stock') }}
</AvatarFallback>
</Avatar>
<span>{{ toLocalDate(randomDate()) }}</span>
</div>
<div class="text-sm">
{{ loremIpsum(Math.random() * 30) }}
</div>
</article>
</div>
</aside>
</div> </div>
<DialogFooter></DialogFooter>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
</template> </template>
@@ -24,7 +24,7 @@ import { Input } from '@/components/ui/input';
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu' import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'
import { StatusBadge, statusBadgeLabels } from '@/components/ui/status-badge' import { StatusBadge, statusBadgeLabels } from '@/components/ui/status-badge'
import LineItemTable from '@/components/documents/LineItemTable.vue' import LineItemTable from '@/components/documents/LineItemTable.vue'
import { Eye, FileText, Trash2, BookUser, User, CodeXml, MessageCircleQuestion, Loader2, Ellipsis, Check, FileCheck, Ban } from "lucide-vue-next" import { Eye, FileText, Trash2, BookUser, User, CodeXml, MessageCircleQuestion, Loader2, Ellipsis, Check, FileCheck, Ban, Logs } from "lucide-vue-next"
import { alertStore } from "@/stores/alertStore" import { alertStore } from "@/stores/alertStore"
import { GrowingTextarea } from '../ui/growing-textarea' import { GrowingTextarea } from '../ui/growing-textarea'
import { toast } from "vue-sonner" import { toast } from "vue-sonner"
@@ -478,7 +478,7 @@ const updateLineItems = (newItems: LineItem[]) => {
class="sm:max-w-[min((100%-2rem),1152px)] grid-rows-[auto_minmax(0,1fr)_auto] h-[calc(100dvh-2rem)] gap-0 p-0 outline-none" class="sm:max-w-[min((100%-2rem),1152px)] grid-rows-[auto_minmax(0,1fr)_auto] h-[calc(100dvh-2rem)] gap-0 p-0 outline-none"
@escapeKeyDown="cancel" @interactOutside="cancel"> @escapeKeyDown="cancel" @interactOutside="cancel">
<DialogHeader class="p-4 md:p-6 lg:p-12 pb-0 md:pb-2 lg:pb-8 flex flex-row items-start gap-6"> <DialogHeader class="p-4 md:p-6 lg:p-12 pb-0 md:pb-2 lg:pb-8 flex flex-row items-start gap-12">
<div class="flex flex-col grow"> <div class="flex flex-col grow">
<DialogTitle class="text-primary-foreground font-bold text-left"> <DialogTitle class="text-primary-foreground font-bold text-left">
@@ -545,7 +545,7 @@ const updateLineItems = (newItems: LineItem[]) => {
<!-- Ellipsis menu --> <!-- Ellipsis menu -->
<DropdownMenu> <DropdownMenu v-if="invoice && invoice.id > 0">
<DropdownMenuTrigger> <DropdownMenuTrigger>
<Button variant="ghost" size="sm" class="px-0! w-7 ml-2"> <Button variant="ghost" size="sm" class="px-0! w-7 ml-2">
<Ellipsis class="size-4" stroke-width="1.5" /> <Ellipsis class="size-4" stroke-width="1.5" />
@@ -598,6 +598,17 @@ const updateLineItems = (newItems: LineItem[]) => {
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<!-- Audit -->
<DropdownMenuItem v-if="invoice && invoice.paymentStatus != 'draft'"
class="flex justify-between" @click="" disabled>
<div class="flex items-center gap-3">
<Logs stroke-width="1.5" class="text-muted-foreground" />
<span>Audit</span>
</div>
</DropdownMenuItem>
<DropdownMenuSeparator />
<!-- Cancel --> <!-- Cancel -->
<DropdownMenuItem <DropdownMenuItem
v-if="invoice && ['issued', 'due', 'reminded'].includes(invoice.paymentStatus)" v-if="invoice && ['issued', 'due', 'reminded'].includes(invoice.paymentStatus)"
+17
View File
@@ -106,3 +106,20 @@ export function testToast() {
}, delay * 5) }, delay * 5)
} }
export function loremIpsum(numWords: number): string {
let text = `Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.
Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat.
Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi.
Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat.`
return text.split(" ").splice(0, numWords).join(" ");
}
export function randomDate(): Date {
let d = new Date()
d.setDate(d.getDate() - Math.random() * 20)
return d
}
+37 -38
View File
@@ -3,12 +3,12 @@
import { ref, onMounted, computed, watch } from 'vue' import { ref, onMounted, computed, watch } from 'vue'
import AppLayout from '@/layouts/AppLayout.vue' import AppLayout from '@/layouts/AppLayout.vue'
import AppHeader from '@/components/AppHeader.vue' import AppHeader from '@/components/AppHeader.vue'
import { Address } from '@/types' import { Address, Customer } from '@/types'
import { Customer } from '@/types' import { newCustomer } from '@/types/index.d'
import { bgColorForString } from '@/lib/utils' import { 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/crm-button'
import { ButtonGroup } from '@/components/ui/button-group' import { ButtonGroup } from '@/components/ui/button-group'
import { Delete, Globe, House, LayoutGrid, Mail, Phone, Plus, Rows3, Search, Smartphone } from "lucide-vue-next" import { Delete, Globe, House, LayoutGrid, Mail, Phone, Plus, Rows3, Search, Smartphone } from "lucide-vue-next"
import Fuse from 'fuse.js'; import Fuse from 'fuse.js';
@@ -19,6 +19,7 @@ import { TooltipArrow } from 'reka-ui'
import CustomerDialog from '@/components/CustomerDialog.vue' import CustomerDialog from '@/components/CustomerDialog.vue'
import { toast } from 'vue-sonner' import { toast } from 'vue-sonner'
import { SocialIcon } from '@/components/ui/social-icon' import { SocialIcon } from '@/components/ui/social-icon'
import { Kbd, KbdGroup } from '@/components/ui/kbd'
interface Props { interface Props {
customersData: Customer[]; customersData: Customer[];
@@ -56,6 +57,15 @@ const filteredCustomers = computed(() => {
return fuse.value.search(searchQuery.value).map(result => result.item); return fuse.value.search(searchQuery.value).map(result => result.item);
}) })
const createCustomer = () => {
editCustomer(newCustomer())
}
const editCustomer = (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 addressToClipbard = async function (companyName: string | null, address: Address | null, event: Event) { const addressToClipbard = async function (companyName: string | null, address: Address | null, event: Event) {
event.stopPropagation(); event.stopPropagation();
@@ -76,11 +86,6 @@ const addressToClipbard = async function (companyName: string | null, address: A
} }
} }
} }
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 mail = (email: string, event: Event) => { const mail = (email: string, event: Event) => {
event.stopPropagation(); event.stopPropagation();
@@ -122,14 +127,14 @@ const call = (number: string, event: Event) => {
<AppHeader> <AppHeader>
<!-- View buttons --> <!-- View buttons -->
<template #left> <template #left>
<ButtonGroup aria-label="Button group"> <!-- <ButtonGroup aria-label="Button group">
<Button variant="pressed" size="sm"> <Button variant="pressed" size="sm">
<LayoutGrid stroke-width="1.5" /> <LayoutGrid stroke-width="1.5" />
</Button> </Button>
<Button variant="outline" size="sm"> <Button variant="outline" size="sm">
<Rows3 stroke-width="1.5" /> <Rows3 stroke-width="1.5" />
</Button> </Button>
</ButtonGroup> </ButtonGroup> -->
</template> </template>
@@ -149,10 +154,26 @@ const call = (number: string, event: Event) => {
<!-- New button --> <!-- New button -->
<template #right> <template #right>
<Button size="sm" variant="action" @click=""> <TooltipProvider>
<Plus stroke-width="1.5" /> Neu <Tooltip>
<TooltipTrigger>
<Button size="sm" variant="action" @click="createCustomer">
<Plus />
Neu
</Button> </Button>
</TooltipTrigger>
<TooltipContent>
<span>Neuen Kunden anlegen</span>
<KbdGroup class="ml-2">
<Kbd class="visible-mac"></Kbd>
<Kbd class="visible-pc">Ctrl</Kbd>
<Kbd>N</Kbd>
</KbdGroup>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</template> </template>
</AppHeader> </AppHeader>
@@ -160,14 +181,14 @@ const call = (number: string, event: Event) => {
<Card v-for="customer in filteredCustomers" :key="customer.id" <Card v-for="customer in filteredCustomers" :key="customer.id"
class="relative mb-6 break-inside-avoid hover:border-slate-300 dark:hover:bg-neutral-800/90 dark:hover:border-neutral-600 overflow-clip" class="relative mb-6 break-inside-avoid hover:border-slate-300 dark:hover:bg-neutral-800/90 dark:hover:border-neutral-600 overflow-clip"
@click="showDetail(customer)"> @click="editCustomer(customer)">
<CardHeader v-if="customer.logo" class="z-0"> <CardHeader v-if="customer.logo" class="z-0">
<img :src="'storage/uploads/' + customer.logo" alt="Logo {{ customer.companyName }}" <img :src="'storage/uploads/' + customer.logo" alt="Logo {{ customer.companyName }}"
class="max-h-8 max-w-[50%]"> class="max-h-8 max-w-[50%]">
</CardHeader> </CardHeader>
<CardContent class="flex justify-between gap-4 flex-col sm:flex-row pr-4 z-0"> <CardContent class="flex justify-between gap-8 flex-col sm:flex-row pr-4 z-0">
<address class="not-italic"> <address class="not-italic">
<CardTitle> <CardTitle>
{{ customer.companyName }} {{ customer.companyName }}
@@ -224,7 +245,7 @@ const call = (number: string, event: Event) => {
<Tooltip v-for="contact in customer.contacts"> <Tooltip v-for="contact in customer.contacts">
<TooltipTrigger> <TooltipTrigger>
<Avatar class="size-14 shadow -mr-2"> <Avatar class="size-14 border-2 border-background -mr-2">
<AvatarImage v-if="contact.avatar" :src="'/storage/uploads/' + contact.avatar" <AvatarImage v-if="contact.avatar" :src="'/storage/uploads/' + contact.avatar"
loading="lazy" /> loading="lazy" />
<AvatarFallback <AvatarFallback
@@ -279,26 +300,4 @@ const call = (number: string, event: Event) => {
</AppLayout> </AppLayout>
</template> </template>
<style> <style></style>
/* Remove close X */
[data-slot=dialog-content] button.ring-offset-background {
/* display: none; */
border-radius: 100%;
position: absolute;
left: 1rem;
width: 1rem;
height: 1rem;
color: var(--color-destructive);
}
/* Backdrop */
[data-slot=dialog-overlay] {
backdrop-filter: blur(var(--blur-sm));
}
/* hover:not(:has(*:hover)) */
[data-slot="card"]:hover:has(a:hover),
[data-slot="card"]:hover:has(button:hover) {
background-color: var(--card);
}
</style>
-1
View File
@@ -11,7 +11,6 @@ import DocumentTable from '@/components/documents/DocumentTable.vue'
import { ChevronLeft, ChevronRight, Plus, Search, Delete } from "lucide-vue-next" import { ChevronLeft, ChevronRight, Plus, Search, Delete } from "lucide-vue-next"
import Fuse from 'fuse.js'; import Fuse from 'fuse.js';
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { toast } from 'vue-sonner'
import SelectSeparator from '@/components/ui/select/SelectSeparator.vue' import SelectSeparator from '@/components/ui/select/SelectSeparator.vue'
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip' import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
import { Kbd, KbdGroup } from '@/components/ui/kbd' import { Kbd, KbdGroup } from '@/components/ui/kbd'