work on customer editor #6
This commit is contained in:
@@ -1,9 +1,25 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch, onMounted, onUpdated, useTemplateRef } from "vue"
|
||||
import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, } from '@/components/ui/dialog';
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { ref, computed, watch } from "vue"
|
||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogFooter, DialogClose } from "@/components/ui/dialog"
|
||||
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 { 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<{
|
||||
@@ -16,6 +32,9 @@ const emit = defineEmits(['update:modelValue', 'update:open', 'save', 'cancel',
|
||||
const customer = ref<Customer | null>(props.customerData)
|
||||
const isDirty = ref(false);
|
||||
const isLoading = ref(false);
|
||||
const isSaving = ref(false)
|
||||
const alert = alertStore()
|
||||
const isTakingNote = ref(false)
|
||||
|
||||
const isOpen = computed({
|
||||
get: () => props.modelValue,
|
||||
@@ -25,49 +44,60 @@ const isOpen = computed({
|
||||
})
|
||||
|
||||
// watch for new external invoice data
|
||||
watch(() => props.customerData as Customer,
|
||||
(newValue, oldValue) => {
|
||||
if (newValue == oldValue) return
|
||||
customer.value = newValue
|
||||
watch(() => props.modelValue, (open) => {
|
||||
// on open
|
||||
if (open) {
|
||||
|
||||
// Set initial state for a newly opened document
|
||||
isDirty.value = false
|
||||
isLoading.value = true
|
||||
|
||||
// console.log("customerData", "Dirty: " + isDirty.value, "loading: " + isLoading.value)
|
||||
customer.value = props.customerData
|
||||
}
|
||||
)
|
||||
|
||||
watch(customer, (newValue) => {
|
||||
// console.log(newValue)
|
||||
})
|
||||
|
||||
const saveChanges = () => {
|
||||
// if (invoice.value) {
|
||||
// emit('save', invoice.value)
|
||||
// isOpen.value = false
|
||||
// }
|
||||
watch(customer, (newValue) => {
|
||||
// isDirty.value = true
|
||||
})
|
||||
|
||||
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) => {
|
||||
// if (isDirty.value) {
|
||||
// alert.value.title = "Wirklich schließen?"
|
||||
// alert.value.message = "Es gibt ungespeicherte Änderungen, die dann verloren gehen."
|
||||
// alert.value.cancelText = "Abbrechen"
|
||||
// alert.value.onCancel = () => {
|
||||
// event?.preventDefault()
|
||||
// event.returnValue = true
|
||||
// alert.value.open = false
|
||||
// }
|
||||
// alert.value.confirmText = "Schließen"
|
||||
// alert.value.onConfirm = () => {
|
||||
// emit('cancel')
|
||||
// isOpen.value = false
|
||||
// alert.value.open = false
|
||||
// }
|
||||
// alert.value.open = true
|
||||
// } else
|
||||
{
|
||||
const cancel = (event: Event | null) => {
|
||||
if (!event) return
|
||||
|
||||
event.preventDefault()
|
||||
event.returnValue = true
|
||||
|
||||
if (isDirty.value) {
|
||||
alert.show(
|
||||
"Wirklich schließen?",
|
||||
"Es gibt ungespeicherte Änderungen, die dann verloren gehen.",
|
||||
{
|
||||
actionText: "Änderungen verwerfen",
|
||||
onAction: () => {
|
||||
emit('cancel')
|
||||
isOpen.value = false
|
||||
}
|
||||
}
|
||||
)
|
||||
} else {
|
||||
emit('cancel')
|
||||
isOpen.value = false
|
||||
}
|
||||
@@ -79,8 +109,8 @@ const handleLogoUpload = (event: Event) => {
|
||||
const file = target.files[0];
|
||||
|
||||
// Hier könntest du die Datei validieren (Größe, Typ, etc.)
|
||||
if (file.size > 2 * 1024 * 1024) { // 2MB
|
||||
alert('Die Datei ist zu groß. Maximal 2MB erlaubt.');
|
||||
if (file.size > 2 * 1024 * 1024) { // 2 MB
|
||||
toast.warning('Die Datei ist zu groß', { description: 'Maximal 2 MB erlaubt' });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -102,44 +132,69 @@ const handleLogoUpload = (event: Event) => {
|
||||
|
||||
<template>
|
||||
<Dialog id="customer-dialog" v-model:open="isOpen">
|
||||
<!-- auto_480px weniger breit, wenn customer.id == 0 -->
|
||||
<DialogContent
|
||||
class="sm:max-w-[min((100%-2rem),1152px)] grid-rows-[auto_minmax(0,1fr)_auto] p-0 h-[calc(100dvh-2rem)]"
|
||||
@escapeKeyDown="cancelChanges" @interactOutside="cancelChanges">
|
||||
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="cancel" @interactOutside="cancel">
|
||||
|
||||
<DialogHeader class="px-3 pt-3 flex flex-row justify-end">
|
||||
<DialogTitle class="sr-only">Kunde</DialogTitle>
|
||||
<DialogHeader class="p-4 md:p-6 lg:p-12 pb-0! flex flex-row items-center gap-8">
|
||||
|
||||
<div v-if="customer && customer.id > 0" class="hidden md:flex mr-4 gap-2">
|
||||
<Button :size="'sm'" variant="action" @click="" class="hidden" :class="{ flex: isDirty }">
|
||||
<Check :strokeWidth="1.5" class="text-current" />
|
||||
<span>Speichern</span>
|
||||
</Button>
|
||||
<Button :size="'sm'" variant="destructive" @click="">
|
||||
<Trash :strokeWidth="1.5" class="text-current" />
|
||||
<span>Löschen</span>
|
||||
</Button>
|
||||
<Button :size="'sm'" variant="ghost" @click="">
|
||||
<Ellipsis class="text-muted-foreground" />
|
||||
</Button>
|
||||
<div class="flex flex-col grow">
|
||||
<DialogTitle class="text-primary-foreground font-bold text-left">
|
||||
<Input v-if="customer" v-model="customer.companyName as string"
|
||||
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>
|
||||
|
||||
<!-- Ellipsis menu -->
|
||||
<DropdownMenu v-if="customer && customer.id > 0">
|
||||
<DropdownMenuTrigger>
|
||||
<Button variant="ghost" size="sm" class="px-0! w-7 ml-2">
|
||||
<Ellipsis class="size-4" stroke-width="1.5" />
|
||||
</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>
|
||||
</DialogHeader>
|
||||
|
||||
|
||||
<div class="overflow-y-auto px-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>
|
||||
<div class="grid grid-cols-[auto_480px] px-6 md:p-12 gap-6">
|
||||
|
||||
<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 -->
|
||||
<div>
|
||||
@@ -161,13 +216,10 @@ const handleLogoUpload = (event: Event) => {
|
||||
</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 -->
|
||||
<div>
|
||||
@@ -290,22 +342,106 @@ const handleLogoUpload = (event: Event) => {
|
||||
</select>
|
||||
</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 class="px-4 mt-6">
|
||||
Contacts hier
|
||||
</div>
|
||||
<div class="px-4 mt-6">
|
||||
Contacts hier
|
||||
</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>
|
||||
|
||||
<DialogFooter></DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
@@ -24,7 +24,7 @@ import { Input } from '@/components/ui/input';
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'
|
||||
import { StatusBadge, statusBadgeLabels } from '@/components/ui/status-badge'
|
||||
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 { GrowingTextarea } from '../ui/growing-textarea'
|
||||
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"
|
||||
@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">
|
||||
<DialogTitle class="text-primary-foreground font-bold text-left">
|
||||
@@ -545,7 +545,7 @@ const updateLineItems = (newItems: LineItem[]) => {
|
||||
|
||||
|
||||
<!-- Ellipsis menu -->
|
||||
<DropdownMenu>
|
||||
<DropdownMenu v-if="invoice && invoice.id > 0">
|
||||
<DropdownMenuTrigger>
|
||||
<Button variant="ghost" size="sm" class="px-0! w-7 ml-2">
|
||||
<Ellipsis class="size-4" stroke-width="1.5" />
|
||||
@@ -598,6 +598,17 @@ const updateLineItems = (newItems: LineItem[]) => {
|
||||
|
||||
<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 -->
|
||||
<DropdownMenuItem
|
||||
v-if="invoice && ['issued', 'due', 'reminded'].includes(invoice.paymentStatus)"
|
||||
|
||||
@@ -106,3 +106,20 @@ export function testToast() {
|
||||
}, 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
|
||||
}
|
||||
@@ -3,12 +3,12 @@
|
||||
import { ref, onMounted, computed, watch } from 'vue'
|
||||
import AppLayout from '@/layouts/AppLayout.vue'
|
||||
import AppHeader from '@/components/AppHeader.vue'
|
||||
import { Address } from '@/types'
|
||||
import { Customer } from '@/types'
|
||||
import { Address, Customer } from '@/types'
|
||||
import { newCustomer } from '@/types/index.d'
|
||||
import { 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 { Button } from '@/components/ui/crm-button'
|
||||
import { ButtonGroup } from '@/components/ui/button-group'
|
||||
import { Delete, Globe, House, LayoutGrid, Mail, Phone, Plus, Rows3, Search, Smartphone } from "lucide-vue-next"
|
||||
import Fuse from 'fuse.js';
|
||||
@@ -19,6 +19,7 @@ import { TooltipArrow } from 'reka-ui'
|
||||
import CustomerDialog from '@/components/CustomerDialog.vue'
|
||||
import { toast } from 'vue-sonner'
|
||||
import { SocialIcon } from '@/components/ui/social-icon'
|
||||
import { Kbd, KbdGroup } from '@/components/ui/kbd'
|
||||
|
||||
interface Props {
|
||||
customersData: Customer[];
|
||||
@@ -56,6 +57,15 @@ const filteredCustomers = computed(() => {
|
||||
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 won’t 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) {
|
||||
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 won’t affect the data until saved
|
||||
activeCustomer.value = JSON.parse(JSON.stringify(customer))
|
||||
detailDialogOpen.value = true
|
||||
}
|
||||
|
||||
const mail = (email: string, event: Event) => {
|
||||
event.stopPropagation();
|
||||
@@ -122,14 +127,14 @@ const call = (number: string, event: Event) => {
|
||||
<AppHeader>
|
||||
<!-- View buttons -->
|
||||
<template #left>
|
||||
<ButtonGroup aria-label="Button group">
|
||||
<!-- <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>
|
||||
</ButtonGroup> -->
|
||||
</template>
|
||||
|
||||
|
||||
@@ -149,10 +154,26 @@ const call = (number: string, event: Event) => {
|
||||
|
||||
<!-- New button -->
|
||||
<template #right>
|
||||
<Button size="sm" variant="action" @click="">
|
||||
<Plus stroke-width="1.5" /> Neu
|
||||
</Button>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<Button size="sm" variant="action" @click="createCustomer">
|
||||
<Plus />
|
||||
Neu
|
||||
</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>
|
||||
|
||||
</AppHeader>
|
||||
|
||||
|
||||
@@ -160,14 +181,14 @@ const call = (number: string, event: Event) => {
|
||||
|
||||
<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"
|
||||
@click="showDetail(customer)">
|
||||
@click="editCustomer(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="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">
|
||||
<CardTitle>
|
||||
{{ customer.companyName }}
|
||||
@@ -224,7 +245,7 @@ const call = (number: string, event: Event) => {
|
||||
|
||||
<Tooltip v-for="contact in customer.contacts">
|
||||
<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"
|
||||
loading="lazy" />
|
||||
<AvatarFallback
|
||||
@@ -279,26 +300,4 @@ const call = (number: string, event: Event) => {
|
||||
</AppLayout>
|
||||
</template>
|
||||
|
||||
<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>
|
||||
<style></style>
|
||||
@@ -11,7 +11,6 @@ import DocumentTable from '@/components/documents/DocumentTable.vue'
|
||||
import { ChevronLeft, ChevronRight, Plus, Search, Delete } from "lucide-vue-next"
|
||||
import Fuse from 'fuse.js';
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { toast } from 'vue-sonner'
|
||||
import SelectSeparator from '@/components/ui/select/SelectSeparator.vue'
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
|
||||
import { Kbd, KbdGroup } from '@/components/ui/kbd'
|
||||
|
||||
Reference in New Issue
Block a user