Move user menu to sidebar, fixes #35

This commit is contained in:
2025-11-14 17:45:57 +01:00
parent 81f0c1ce56
commit f00117ed26
13 changed files with 328 additions and 365 deletions
+135 -138
View File
@@ -1,19 +1,18 @@
<script setup lang="ts">
import { ref, onMounted, computed, useTemplateRef, watch } from 'vue'
import { ref, onMounted, computed, watch } from 'vue'
import AppLayout from '@/layouts/AppLayout.vue'
import { customers } from '@/routes'
import AppHeader from '@/components/AppHeader.vue'
import { Address } from '@/types'
import { Head } from '@inertiajs/vue3'
import api from '@/axios'
import { Customer, Contact } from '@/types'
import { randomInt, bgColorForString } from '@/lib/utils'
import { Customer } from '@/types'
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 { Badge } from '@/components/ui/badge'
import { ButtonGroup, ButtonGroupSeparator, ButtonGroupText, } from '@/components/ui/button-group'
import { Copy, Delete, Edit, Globe, House, LayoutGrid, LayoutList, Mail, Phone, Plus, Rows3, Rows4, Search, Smartphone } from "lucide-vue-next"
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';
import { getInitials } from '@/composables/useInitials';
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, } from '@/components/ui/card'
@@ -121,15 +120,11 @@ const call = (number: string, event: Event) => {
<template>
<Head title="Dashboard" />
<AppLayout title="Kunden">
<AppLayout>
<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">
<!-- Function Header -->
<div id="function-header" class="flex row justify-between items-center mb-4 gap-4">
<!-- View buttons -->
<AppHeader>
<!-- View buttons -->
<template #left>
<ButtonGroup aria-label="Button group">
<Button variant="pressed" size="sm">
<LayoutGrid stroke-width="1.5" />
@@ -138,150 +133,152 @@ const call = (number: string, event: Event) => {
<Rows3 stroke-width="1.5" />
</Button>
</ButtonGroup>
</template>
<!-- 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 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>
<!-- New button -->
<!-- Search field -->
<template #middle>
<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 mr-1">
<Button :size="'sm'" :variant="'ghost'" @click="searchQuery = ''; searchField.focus()">
<Delete class="size-4 text-muted-foreground" :stroke-width="1.5" />
</Button>
</span>
</template>
<!-- New button -->
<template #right>
<Button size="sm" variant="action" @click="">
<Plus stroke-width="1.5" /> Neu
</Button>
</div>
</template>
</AppHeader>
<div class="columns-xs gap-6">
<div class="columns-xs gap-6">
<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)">
<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)">
<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 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">
<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>
<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>
<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 v-if="customer.contacts.length > 0">
<TooltipProvider :delay-duration="0">
<Tooltip v-for="contact in customer.contacts">
<div class="flex items-start">
<TooltipProvider>
<Tooltip v-if="customer.url">
<TooltipTrigger>
<Avatar class="size-14 shadow -mr-2">
<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) }}
</AvatarFallback>
</Avatar>
<Button variant="ghost" size="sm"
@click="(event: Event) => browse(customer.url as string, event)">
<Globe stroke-width="1.5" />
</Button>
</TooltipTrigger>
<TooltipContent class="p-4 -mr-2" :side-offset="-1">
<p class="font-bold">
<span v-if="contact.academicTitle">{{ contact.academicTitle }}</span>
<span>{{ contact.firstName + ' ' + contact.lastName }}</span>
</p>
<p v-if="contact.jobTitle" class="text-muted-foreground">{{ contact.jobTitle }}</p>
<ButtonGroup class="mt-4">
<!-- E-mail -->
<Button size="sm" v-if="contact.email"
@click="(event: Event) => mail(contact.email as string, event)">
<Mail stroke-width="1.5" @click="" />
</Button>
<!-- Phone -->
<Button size="sm" v-if="contact.phone"
@click="(event: Event) => call(contact.phone as string, event)">
<Phone stroke-width="1.5" @click="" />
</Button>
<!-- Mobile phone -->
<Button size="sm" v-if="contact.mobilePhone"
@click="(event: Event) => call(contact.mobilePhone as string, event)">
<Smartphone stroke-width="1.5" @click="" />
</Button>
<!-- Online accounts -->
<Button size="sm" v-for="account in contact.onlineAccounts"
@click="(event: Event) => browse(account.url as string, event)">
<SocialIcon :variant="account.platform" />
</Button>
</ButtonGroup>
<TooltipArrow :height="8" :width="16"
class="fill-popover drop-shadow-(--shadow-arrow) stroke-[0.5px] stroke-border -mt-[1px]" />
<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>
</CardFooter>
</Card>
</div>
</CardContent>
</div>
<CardFooter v-if="customer.contacts.length > 0">
<TooltipProvider :delay-duration="0">
<!-- Invoice detail dialog -->
<CustomerDialog :customerData="activeCustomer" v-model="detailDialogOpen" @save="" @delete="" />
<Tooltip v-for="contact in customer.contacts">
<TooltipTrigger>
<Avatar class="size-14 shadow -mr-2">
<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) }}
</AvatarFallback>
</Avatar>
</TooltipTrigger>
<TooltipContent class="p-4 -mr-2" :side-offset="-1">
<p class="font-bold">
<span v-if="contact.academicTitle">{{ contact.academicTitle }}</span>
<span>{{ contact.firstName + ' ' + contact.lastName }}</span>
</p>
<p v-if="contact.jobTitle" class="text-muted-foreground">{{ contact.jobTitle }}</p>
<ButtonGroup class="mt-4">
<!-- E-mail -->
<Button size="sm" v-if="contact.email"
@click="(event: Event) => mail(contact.email as string, event)">
<Mail stroke-width="1.5" @click="" />
</Button>
<!-- Phone -->
<Button size="sm" v-if="contact.phone"
@click="(event: Event) => call(contact.phone as string, event)">
<Phone stroke-width="1.5" @click="" />
</Button>
<!-- Mobile phone -->
<Button size="sm" v-if="contact.mobilePhone"
@click="(event: Event) => call(contact.mobilePhone as string, event)">
<Smartphone stroke-width="1.5" @click="" />
</Button>
<!-- Online accounts -->
<Button size="sm" v-for="account in contact.onlineAccounts"
@click="(event: Event) => browse(account.url as string, event)">
<SocialIcon :variant="account.platform" />
</Button>
</ButtonGroup>
<TooltipArrow :height="8" :width="16"
class="fill-popover drop-shadow-(--shadow-arrow) stroke-[0.5px] stroke-border -mt-[1px]" />
</TooltipContent>
</Tooltip>
</TooltipProvider>
</CardFooter>
</Card>
</div>
<!-- Invoice detail dialog -->
<CustomerDialog :customerData="activeCustomer" v-model="detailDialogOpen" @save="" @delete="" />
</AppLayout>
</template>