This repository has been archived on 2025-12-04. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
Caramel-CRM-Backup/resources/js/pages/Customers.vue
T

184 lines
7.0 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { ref, onMounted, computed, useTemplateRef, watch } from 'vue'
import AppLayout from '@/layouts/AppLayout.vue'
import { customers } from '@/routes'
import { Address, type BreadcrumbItem } from '@/types'
import { Head } from '@inertiajs/vue3'
import api from '@/axios'
import { Customer, Contact } from '@/types'
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 Fuse from 'fuse.js';
import { getInitials } from '@/composables/useInitials';
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, } from '@/components/ui/card'
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
import CustomerDialog from '@/components/CustomerDialog.vue'
const breadcrumbs: BreadcrumbItem[] = [
{
title: 'Kunden',
href: customers().url,
},
]
const customersData = ref([] as Customer[])
const searchQuery = ref('')
const searchField = ref()
const activeCustomer = ref<Customer | null>(null)
const detailDialogOpen = ref(false)
onMounted(async () => {
try {
const response = await api.get('/customers');
customersData.value = (response.data as Customer[]).toSorted((a, b) => a.companyName.localeCompare(b.companyName));;
searchField.value = document.getElementById('search')
searchField.value.focus()
} catch (error) {
// TODO: toast, depends on #33
}
})
watch(activeCustomer, () => {
console.log(activeCustomer.value?.companyName)
})
const fuse = computed(() => {
return new Fuse(customersData.value, {
keys: ['companyName', 'firstName', 'lastName', 'email', 'phone'],
});
})
const filteredCustomers = computed(() => {
if (!searchQuery.value) {
return customersData.value;
}
return fuse.value.search(searchQuery.value).map(result => result.item);
})
const addressToClipbard = async function (address: Address) {
try {
await navigator.clipboard.writeText(
address.lineOne + '\n' +
(address.lineTwo ? address.lineTwo + '\n' : '') +
address.postalCode + ' ' + address.city
// TODO: toast, depends on #33
)
} catch (error) {
if (error instanceof Error)
// TODO: toast, depends on #33
console.error(error.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
}
</script>
<template>
<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">
<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 />
<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">
<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
</Button>
</div>
<div class="columns-xs gap-4">
<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>
</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>
</address>
<a href="https://www.tooloop.de">www.tooloop.de</a><br />
<a href="tel://+4982165079983">+49 821 650799-83</a><br />
</CardContent>
<CardFooter>
<TooltipProvider>
<Tooltip v-for="contact in customer.contacts">
<TooltipTrigger>
<Avatar class="-mr-2 border-2 size-14">
<AvatarImage :src="contact.avatar || ''" />
<AvatarFallback
:class="bgColorForString(getInitials(contact.firstName + ' ' + contact.lastName))">
{{ getInitials(contact.firstName + ' ' + contact.lastName) }}
</AvatarFallback>
</Avatar>
</TooltipTrigger>
<TooltipContent>
<p>{{ contact.firstName + ' ' + contact.lastName }}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</CardFooter>
</Card>
</div>
<!-- Invoice detail dialog -->
<CustomerDialog :customerData="activeCustomer" v-model="detailDialogOpen" @save="" @delete="" />
</div>
</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));
}
</style>