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
-21
View File
@@ -1,21 +0,0 @@
<script setup lang="ts">
import { SidebarInset } from '@/components/ui/sidebar';
import { computed } from 'vue';
interface Props {
variant?: 'header' | 'sidebar';
class?: string;
}
const props = defineProps<Props>();
const className = computed(() => props.class);
</script>
<template>
<SidebarInset v-if="props.variant === 'sidebar'" :class="className">
<slot />
</SidebarInset>
<main v-else class="mx-auto flex h-full w-full max-w-7xl flex-1 flex-col gap-4 rounded-xl" :class="className">
<slot />
</main>
</template>
+25
View File
@@ -0,0 +1,25 @@
<script setup lang="ts">
import { cn } from '@/lib/utils';
import { HTMLAttributes } from 'vue';
const props = defineProps<{
class?: HTMLAttributes;
}>();
</script>
<template>
<div :class="cn('grid grid-cols-[2fr_3fr_2fr] mb-12 gap-4', props.class)">
<div class="place-self-stretch flex row items-center justify-start">
<slot name="left" />
</div>
<div class="relative w-full items-center">
<slot name="middle" />
</div>
<div class="place-self-stretch flex row items-center justify-end">
<slot name="right" />
</div>
</div>
</template>
+8 -13
View File
@@ -5,11 +5,14 @@ import { Sidebar, SidebarContent, SidebarFooter, SidebarHeader, SidebarTrigger }
import { dashboard, crm, offers, invoices, newInvoice, timesheets, customers, leads, achievements } from '@/routes'; import { dashboard, crm, offers, invoices, newInvoice, timesheets, customers, leads, achievements } from '@/routes';
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'
import { edit } from '@/routes/profile';
import { type NavItem, type NavGroup } from '@/types'; import { type NavItem, type NavGroup } from '@/types';
import { Link } from '@inertiajs/vue3'; import { InertiaLinkProps, Link, usePage } from '@inertiajs/vue3';
import { Kanban, Euro, Contact, Trophy, Calculator, Settings, Target, BookUser, Timer, Headset, IdCard, Plus } from 'lucide-vue-next'; import { Kanban, Euro, Trophy, Calculator, BookUser, Timer, Headset, Plus } from 'lucide-vue-next';
import AppLogo from './AppLogo.vue'; import AppLogo from './AppLogo.vue';
import { computed } from 'vue';
const page = usePage();
const auth = computed(() => page.props.auth);
const mainNavGroups: NavGroup[] = [ const mainNavGroups: NavGroup[] = [
{ {
@@ -72,14 +75,6 @@ const mainNavGroups: NavGroup[] = [
} }
]; ];
const footerNavItems: NavItem[] = [
{
title: 'Einstellungen',
href: edit(),
icon: Settings,
color: 'text-gray-500',
}
];
</script> </script>
<template> <template>
@@ -107,12 +102,12 @@ const footerNavItems: NavItem[] = [
</TooltipProvider> </TooltipProvider>
</SidebarHeader> </SidebarHeader>
<SidebarContent> <SidebarContent class="flex min-h-0 flex-1 flex-col gap-2 overflow-y-auto overflow-x-hidden">
<NavMain :groups="mainNavGroups" /> <NavMain :groups="mainNavGroups" />
</SidebarContent> </SidebarContent>
<SidebarFooter> <SidebarFooter>
<NavFooter :items="footerNavItems" /> <NavFooter :user="auth.user" />
</SidebarFooter> </SidebarFooter>
</Sidebar> </Sidebar>
<slot /> <slot />
+23 -19
View File
@@ -1,32 +1,36 @@
<script setup lang="ts"> <script setup lang="ts">
import { SidebarGroup, SidebarGroupContent, SidebarMenu, SidebarMenuButton, SidebarMenuItem } from '@/components/ui/sidebar'; import { SidebarGroup, SidebarGroupContent, SidebarMenu, SidebarMenuButton, SidebarMenuItem } from '@/components/ui/sidebar';
import { toUrl } from '@/lib/utils'; import { User } from '@/types';
import { type NavItem } from '@/types'; import { defineProps } from 'vue';
import { Link } from '@inertiajs/vue3'; import UserInfo from '@/components/UserInfo.vue';
import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger } from '@/components/ui/dropdown-menu';
import UserMenuContent from '@/components/UserMenuContent.vue';
interface Props { const props = defineProps<{
items: NavItem[]; user: User;
class?: string; class?: string;
} }>()
defineProps<Props>();
</script> </script>
<template> <template>
<SidebarGroup :class="`group-data-[collapsible=icon]:p-0 ${$props.class || ''}`"> <SidebarGroup class="p-0">
<SidebarGroupContent> <SidebarGroupContent>
<SidebarMenu> <SidebarMenu>
<SidebarMenuItem v-for="item in items" :key="item.title"> <DropdownMenu>
<SidebarMenuButton <DropdownMenuTrigger as-child>
class="text-neutral-600 hover:text-neutral-800 dark:text-neutral-300 dark:hover:text-neutral-100" <SidebarMenuItem>
as-child> <SidebarMenuButton
<Link :href="item.href"> class="pl-0 rounded-[1rem_var(--radius-md)_var(--radius-md)_1rem] group-data-[state=collapsed]:rounded-full">
<component :is="item.icon" :class="item.color" stroke-width="1.5" /> <UserInfo :user="props.user" />
<span>{{ item.title }}</span> </SidebarMenuButton>
</Link> </SidebarMenuItem>
</SidebarMenuButton> </DropdownMenuTrigger>
</SidebarMenuItem> <DropdownMenuContent align="start">
<UserMenuContent />
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenu> </SidebarMenu>
</SidebarGroupContent> </SidebarGroupContent>
</SidebarGroup> </SidebarGroup>
</template> </template>
+12 -9
View File
@@ -3,6 +3,7 @@ import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { useInitials } from '@/composables/useInitials'; import { useInitials } from '@/composables/useInitials';
import type { User } from '@/types'; import type { User } from '@/types';
import { computed } from 'vue'; import { computed } from 'vue';
import { Kbd, KbdGroup } from '@/components/ui/kbd'
interface Props { interface Props {
user: User; user: User;
@@ -20,15 +21,17 @@ const showAvatar = computed(() => props.user.avatar && props.user.avatar !== '')
</script> </script>
<template> <template>
<Avatar class="h-10 w-10 overflow-hidden rounded-lg"> <div class="flex items-center gap-2">
<AvatarImage v-if="showAvatar" :src="user.avatar!" :alt="user.name" /> <Avatar class="size-8 overflow-hidden rounded-lg">
<AvatarFallback class="rounded-full text-black dark:text-white"> <AvatarImage v-if="showAvatar" :src="user.avatar!" :alt="user.name" />
{{ getInitials(user.name) }} <AvatarFallback class="rounded-full bg-primary text-black dark:text-white">
</AvatarFallback> {{ getInitials(user.name) }}
</Avatar> </AvatarFallback>
</Avatar>
<div class="grid flex-1 text-left text-sm leading-tight"> <div class="grid flex-1 text-left text-sm leading-tight">
<span class="truncate font-medium">{{ user.name }}</span> <span class="truncate font-medium">{{ user.name }}</span>
<span v-if="showEmail" class="truncate text-xs text-muted-foreground">{{ user.email }}</span> <span v-if="showEmail" class="truncate text-xs text-muted-foreground">{{ user.email }}</span>
</div>
</div> </div>
</template> </template>
+19 -21
View File
@@ -1,45 +1,43 @@
<script setup lang="ts"> <script setup lang="ts">
import UserInfo from '@/components/UserInfo.vue';
import { DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator } from '@/components/ui/dropdown-menu'; import { DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator } from '@/components/ui/dropdown-menu';
import { logout } from '@/routes'; import { logout } from '@/routes';
import { edit } from '@/routes/profile'; import { edit } from '@/routes/profile';
import type { User } from '@/types';
import { Link, router } from '@inertiajs/vue3'; import { Link, router } from '@inertiajs/vue3';
import { LogOut, Settings } from 'lucide-vue-next'; import { LogOut, Settings, User } from 'lucide-vue-next';
import axios from 'axios'; import axios from 'axios';
import { Kbd, KbdGroup } from '@/components/ui/kbd';
interface Props {
user: User;
}
const handleLogout = () => { const handleLogout = () => {
router.flushAll(); router.flushAll();
localStorage.removeItem('sanctum_token'); localStorage.removeItem('sanctum_token');
delete axios.defaults.headers.common['Authorization']; delete axios.defaults.headers.common['Authorization'];
}; };
defineProps<Props>();
</script> </script>
<template> <template>
<DropdownMenuLabel class="p-0 font-normal">
<div class="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
<UserInfo :user="user" :show-email="true" />
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuGroup> <DropdownMenuGroup>
<DropdownMenuItem :as-child="true">
<Link class="block w-full" :href="edit()" prefetch as="button"> <DropdownMenuItem as-child>
<Settings class="mr-2 h-4 w-4" /> <Link :href="edit()" prefetch class="flex items-center justify-between">
Settings <div class="flex items-center gap-3">
<Settings stroke-width="1.5" />
<span class="mr-4">Einstellungen</span>
</div>
<KbdGroup>
<Kbd class="visible-mac"></Kbd>
<Kbd class="visible-pc">Ctrl</Kbd>
<Kbd>,</Kbd>
</KbdGroup>
</Link> </Link>
</DropdownMenuItem> </DropdownMenuItem>
</DropdownMenuGroup> </DropdownMenuGroup>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuItem :as-child="true">
<DropdownMenuItem as-child>
<Link class="block w-full" :href="logout()" @click="handleLogout" as="button" data-test="logout-button"> <Link class="block w-full" :href="logout()" @click="handleLogout" as="button" data-test="logout-button">
<LogOut class="mr-2 h-4 w-4" /> <LogOut stroke-width="1.5" class="mr-2 h-4 w-4" />
Log out Log out
</Link> </Link>
</DropdownMenuItem> </DropdownMenuItem>
@@ -29,7 +29,7 @@ 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"
import { SendMailDialog } from "../ui/send-mail-dialog" import { SendMailDialog } from "../ui/send-mail-dialog"
import { Kbd, KbdGroup } from "../ui/kbd" import { Kbd, KbdGroup } from '@/components/ui/kbd';
import DialogClose from "../ui/dialog/DialogClose.vue" import DialogClose from "../ui/dialog/DialogClose.vue"
import DialogCloseButton from "../DialogCloseButton/DialogCloseButton.vue" import DialogCloseButton from "../DialogCloseButton/DialogCloseButton.vue"
@@ -385,7 +385,7 @@ const updateTotalAmount = () => {
<!-- Preview --> <!-- Preview -->
<DropdownMenuItem v-if="invoice?.paymentStatus == 'draft'" <DropdownMenuItem v-if="invoice?.paymentStatus == 'draft'"
class="flex items-center justify-between" @click="preview"> class="flex items-center justify-between" @click="preview">
<div class="flex items-center gap-4"> <div class="flex items-center gap-3">
<Eye :strokeWidth="1.5" class="text-current" /> <Eye :strokeWidth="1.5" class="text-current" />
<span class="mr-4">Vorschau</span> <span class="mr-4">Vorschau</span>
</div> </div>
@@ -399,7 +399,7 @@ const updateTotalAmount = () => {
<!-- PDF --> <!-- PDF -->
<DropdownMenuItem v-if="invoice && invoice.paymentStatus != 'draft'" class="flex justify-between" <DropdownMenuItem v-if="invoice && invoice.paymentStatus != 'draft'" class="flex justify-between"
@click="downloadPdf"> @click="downloadPdf">
<div class="flex items-center gap-4"> <div class="flex items-center gap-3">
<FileText stroke-width="1.5" class="text-muted-foreground" /> <FileText stroke-width="1.5" class="text-muted-foreground" />
<div class="mr-4 flex flex-col"> <div class="mr-4 flex flex-col">
<span>PDF exportieren</span> <span>PDF exportieren</span>
@@ -416,7 +416,7 @@ const updateTotalAmount = () => {
<!-- XML --> <!-- XML -->
<DropdownMenuItem v-if="invoice && invoice.paymentStatus != 'draft'" class="flex justify-between" <DropdownMenuItem v-if="invoice && invoice.paymentStatus != 'draft'" class="flex justify-between"
@click="downloadXml"> @click="downloadXml">
<div class="flex items-center gap-4"> <div class="flex items-center gap-3">
<CodeXml stroke-width="1.5" class="text-muted-foreground" /> <CodeXml stroke-width="1.5" class="text-muted-foreground" />
<div class="mr-4 flex flex-col"> <div class="mr-4 flex flex-col">
<span>XML exportieren</span> <span>XML exportieren</span>
@@ -431,7 +431,7 @@ const updateTotalAmount = () => {
<DropdownMenuItem <DropdownMenuItem
v-if="invoice && ['issued', 'due', 'reminded'].includes(invoice.paymentStatus)" v-if="invoice && ['issued', 'due', 'reminded'].includes(invoice.paymentStatus)"
class="flex justify-between" @click="deleteInvoice"> class="flex justify-between" @click="deleteInvoice">
<div class="flex items-center gap-2"> <div class="flex items-center gap-3">
<!-- <FileX stroke-width="1.5" class="text-muted-foreground"/> --> <!-- <FileX stroke-width="1.5" class="text-muted-foreground"/> -->
<Ban :strokeWidth="1.5" class="text-current" /> <Ban :strokeWidth="1.5" class="text-current" />
<span class="mr-2">Stornieren</span> <span class="mr-2">Stornieren</span>
@@ -442,7 +442,7 @@ const updateTotalAmount = () => {
<DropdownMenuItem <DropdownMenuItem
class="flex justify-between text-destructive! hover:bg-red-100! dark:hover:bg-red-950!" class="flex justify-between text-destructive! hover:bg-red-100! dark:hover:bg-red-950!"
@click="deleteInvoice"> @click="deleteInvoice">
<div class="flex items-center gap-2"> <div class="flex items-center gap-3">
<Trash2 :strokeWidth="1.5" class="text-current" /> <Trash2 :strokeWidth="1.5" class="text-current" />
<span class="mr-2">Löschen</span> <span class="mr-2">Löschen</span>
</div> </div>
+18 -8
View File
@@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import AppContent from '@/components/AppContent.vue'; import { Head } from '@inertiajs/vue3'
import AppSidebar from '@/components/AppSidebar.vue'; import AppSidebar from '@/components/AppSidebar.vue';
import { onMounted } from 'vue'; import { onMounted } from 'vue';
import 'vue-sonner/style.css' import 'vue-sonner/style.css'
@@ -13,6 +13,9 @@ import { usePage } from '@inertiajs/vue3';
const isOpen = usePage().props.sidebarOpen; const isOpen = usePage().props.sidebarOpen;
const alert = alertStore() const alert = alertStore()
const props = defineProps<{
title: string;
}>();
onMounted(() => { onMounted(() => {
if (navigator.platform.toUpperCase().indexOf('MAC') >= 0) { if (navigator.platform.toUpperCase().indexOf('MAC') >= 0) {
@@ -23,6 +26,9 @@ onMounted(() => {
</script> </script>
<template> <template>
<Head :title="props.title" />
<Toaster position="top-right" :expand="true" closeButton :visible-toasts="6" :offset="'1rem'" :toastOptions="{ <Toaster position="top-right" :expand="true" closeButton :visible-toasts="6" :offset="'1rem'" :toastOptions="{
unstyled: true, unstyled: true,
classes: { classes: {
@@ -57,12 +63,17 @@ onMounted(() => {
</template> </template>
</Toaster> </Toaster>
<SidebarProvider :default-open="isOpen"> <div class="overflow-x-auto p-4 lg:p-8 lg:pl-4 print:bg-transparent print:p-0 print:m-0">
<AppSidebar />
<AppContent variant="sidebar" class="overflow-x-hidden bg-main"> <SidebarProvider :default-open="isOpen">
<slot /> <AppSidebar />
</AppContent>
</SidebarProvider> <main class="w-full overflow-x-hidden">
<slot />
</main>
</SidebarProvider>
</div>
<AlertDialog v-model:open="alert.open"> <AlertDialog v-model:open="alert.open">
<AlertDialogContent> <AlertDialogContent>
@@ -76,7 +87,6 @@ onMounted(() => {
</AlertDialogFooter> </AlertDialogFooter>
</AlertDialogContent> </AlertDialogContent>
</AlertDialog> </AlertDialog>
</template> </template>
<style> <style>
+11 -17
View File
@@ -7,27 +7,21 @@ import PlaceholderPattern from '../components/PlaceholderPattern.vue';
<template> <template>
<Head title="Erfolge" /> <AppLayout title="Erfolge">
<div>
<h1 class="text-xl">Kategorie</h1>
<h2 class="text-sm text-neutral-400 mb-4">3 von 10 freigeschaltet</h2>
<AppLayout> <div class="flex flex-row flex-wrap gap-4">
<div class="flex h-full flex-1 flex-col gap-4 overflow-x-auto rounded-xl p-4"> <div v-for="n in 10" class="relative flex flex-col items-center">
<div
<div> class="w-48 relative aspect-square overflow-hidden rounded-full border border-sidebar-border/70 dark:border-sidebar-border">
<h1 class="text-xl">Kategorie</h1> <PlaceholderPattern />
<h2 class="text-sm text-neutral-400 mb-4">3 von 10 freigeschaltet</h2>
<div class="flex flex-row flex-wrap gap-4">
<div v-for="n in 10" class="relative flex flex-col items-center">
<div
class="w-48 relative aspect-square overflow-hidden rounded-full border border-sidebar-border/70 dark:border-sidebar-border">
<PlaceholderPattern />
</div>
<label class="">Erfolg {{ n
}}</label>
</div> </div>
<label class="">Erfolg {{ n
}}</label>
</div> </div>
</div> </div>
</div> </div>
</AppLayout> </AppLayout>
</template> </template>
+135 -138
View File
@@ -1,19 +1,18 @@
<script setup lang="ts"> <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 AppLayout from '@/layouts/AppLayout.vue'
import { customers } from '@/routes' import AppHeader from '@/components/AppHeader.vue'
import { Address } from '@/types' import { Address } from '@/types'
import { Head } from '@inertiajs/vue3' import { Head } from '@inertiajs/vue3'
import api from '@/axios' import api from '@/axios'
import { Customer, Contact } from '@/types' import { Customer } from '@/types'
import { randomInt, 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/button'
import { Badge } from '@/components/ui/badge' import { ButtonGroup } from '@/components/ui/button-group'
import { ButtonGroup, ButtonGroupSeparator, ButtonGroupText, } from '@/components/ui/button-group' import { Delete, Globe, House, LayoutGrid, Mail, Phone, Plus, Rows3, Search, Smartphone } from "lucide-vue-next"
import { Copy, Delete, Edit, Globe, House, LayoutGrid, LayoutList, Mail, Phone, Plus, Rows3, Rows4, Search, Smartphone } 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'
@@ -121,15 +120,11 @@ const call = (number: string, event: Event) => {
<template> <template>
<Head title="Dashboard" /> <AppLayout title="Kunden">
<AppLayout> <AppHeader>
<div <!-- View buttons -->
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"> <template #left>
<!-- Function Header -->
<div id="function-header" class="flex row justify-between items-center mb-4 gap-4">
<!-- View buttons -->
<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" />
@@ -138,150 +133,152 @@ const call = (number: string, event: Event) => {
<Rows3 stroke-width="1.5" /> <Rows3 stroke-width="1.5" />
</Button> </Button>
</ButtonGroup> </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=""> <Button size="sm" variant="action" @click="">
<Plus stroke-width="1.5" /> Neu <Plus stroke-width="1.5" /> Neu
</Button> </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" <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="showDetail(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-4 flex-col sm:flex-row pr-4 z-0">
<address class="not-italic"> <address class="not-italic">
<CardTitle> <CardTitle>
{{ customer.companyName }} {{ customer.companyName }}
<!-- <Badge variant="secondary">Badge</Badge> --> <!-- <Badge variant="secondary">Badge</Badge> -->
</CardTitle> </CardTitle>
<CardDescription class="mt-2"> <CardDescription class="mt-2">
{{ customer.billingAddress?.lineOne }}<br /> {{ customer.billingAddress?.lineOne }}<br />
{{ customer.billingAddress?.lineTwo }}<br v-if="customer.billingAddress?.lineTwo" /> {{ customer.billingAddress?.lineTwo }}<br v-if="customer.billingAddress?.lineTwo" />
{{ customer.billingAddress?.postalCode }} {{ customer.billingAddress?.city }} {{ customer.billingAddress?.postalCode }} {{ customer.billingAddress?.city }}
</CardDescription> </CardDescription>
</address> </address>
<div class="flex items-start"> <div class="flex items-start">
<TooltipProvider> <TooltipProvider>
<Tooltip v-if="customer.url"> <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">
<TooltipTrigger> <TooltipTrigger>
<Avatar class="size-14 shadow -mr-2"> <Button variant="ghost" size="sm"
<AvatarImage v-if="contact.avatar" :src="'/storage/uploads/' + contact.avatar" @click="(event: Event) => browse(customer.url as string, event)">
loading="lazy" /> <Globe stroke-width="1.5" />
<AvatarFallback </Button>
:class="bgColorForString(getInitials(contact.firstName + ' ' + contact.lastName))">
{{ getInitials(contact.firstName + ' ' + contact.lastName) }}
</AvatarFallback>
</Avatar>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent class="p-4 -mr-2" :side-offset="-1"> <TooltipContent>
<p class="font-bold"> {{ customer.url }} öffnen
<span v-if="contact.academicTitle">{{ contact.academicTitle }}</span> </TooltipContent>
<span>{{ contact.firstName + ' ' + contact.lastName }}</span> </Tooltip>
</p> <Tooltip v-if="customer.phone">
<p v-if="contact.jobTitle" class="text-muted-foreground">{{ contact.jobTitle }}</p> <TooltipTrigger>
<Button variant="ghost" size="sm"
<ButtonGroup class="mt-4"> @click="(event: Event) => call(customer.phone as string, event)">
<!-- E-mail --> <Phone stroke-width="1.5" />
<Button size="sm" v-if="contact.email" </Button>
@click="(event: Event) => mail(contact.email as string, event)"> </TooltipTrigger>
<Mail stroke-width="1.5" @click="" /> <TooltipContent>
</Button> {{ customer.phone }} anrufen
<!-- Phone --> </TooltipContent>
<Button size="sm" v-if="contact.phone" </Tooltip>
@click="(event: Event) => call(contact.phone as string, event)"> <Tooltip v-if="customer.billingAddress">
<Phone stroke-width="1.5" @click="" /> <TooltipTrigger>
</Button> <Button variant="ghost" size="sm"
<!-- Mobile phone --> @click="(event: Event) => addressToClipbard(customer.companyName, customer.billingAddress as Address | null, event)">
<Button size="sm" v-if="contact.mobilePhone" <House stroke-width="1.5" />
@click="(event: Event) => call(contact.mobilePhone as string, event)"> </Button>
<Smartphone stroke-width="1.5" @click="" /> </TooltipTrigger>
</Button> <TooltipContent>
<!-- Online accounts --> Adresse in Zwischenablage kopieren
<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> </TooltipContent>
</Tooltip> </Tooltip>
</TooltipProvider> </TooltipProvider>
</CardFooter> </div>
</Card> </CardContent>
</div> <CardFooter v-if="customer.contacts.length > 0">
<TooltipProvider :delay-duration="0">
<!-- Invoice detail dialog --> <Tooltip v-for="contact in customer.contacts">
<CustomerDialog :customerData="activeCustomer" v-model="detailDialogOpen" @save="" @delete="" /> <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> </div>
<!-- Invoice detail dialog -->
<CustomerDialog :customerData="activeCustomer" v-model="detailDialogOpen" @save="" @delete="" />
</AppLayout> </AppLayout>
</template> </template>
+2 -4
View File
@@ -27,10 +27,8 @@ if (token) {
<template> <template>
<Head title="Dashboard" /> <AppLayout title="Dashboard">
<div class="grid gap-12 md:grid-cols-2 h-full md:p-8">
<AppLayout>
<div class="grid gap-12 md:grid-cols-2 h-full p-6 md:p-8">
<div class="relative overflow-y-auto"> <div class="relative overflow-y-auto">
Geplante Aktivitäten Geplante Aktivitäten
+54 -65
View File
@@ -1,12 +1,10 @@
<script setup lang="ts"> <script setup lang="ts">
import AppLayout from '@/layouts/AppLayout.vue' import { computed, ref, onMounted, watch, defineProps, HTMLAttributes } from 'vue'
import { invoices } from '@/routes'
import { type Invoice, type Customer, type Address } from '@/types' import { type Invoice, type Customer, type Address } from '@/types'
import { newInvoice } from '@/types/index.d' import { newInvoice } from '@/types/index.d'
import { Head } from '@inertiajs/vue3' import axios from 'axios'
import { computed, ref, onMounted, watch } from 'vue' import AppLayout from '@/layouts/AppLayout.vue'
import axios, { Axios, AxiosError, AxiosResponse } from 'axios'
import { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectTrigger, SelectValue, } from '@/components/ui/select' import { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectTrigger, SelectValue, } from '@/components/ui/select'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import DocumentTable from '@/components/documents/DocumentTable.vue' import DocumentTable from '@/components/documents/DocumentTable.vue'
@@ -15,11 +13,11 @@ import InvoiceDialog from '@/components/documents/InvoiceDialog.vue'
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 { toast } from 'vue-sonner'
import { testToast } from '@/lib/utils'
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'
import { statusBadgeLabels } from '@/components/ui/status-badge' import { statusBadgeLabels } from '@/components/ui/status-badge'
import AppHeader from '@/components/AppHeader.vue'
const invoicesData = ref([] as Invoice[]) const invoicesData = ref([] as Invoice[])
const activeInvoice = ref<Invoice | null>(null) const activeInvoice = ref<Invoice | null>(null)
@@ -191,62 +189,54 @@ const deleteInvoice = async (id: number) => {
<template> <template>
<Head title="Rechnungen" />
<!-- Function Header --> <!-- Function Header -->
<AppLayout> <AppLayout title="Rechnungen">
<div <AppHeader>
class="flex h-full flex-1 flex-col gap-4 overflow-x-auto p-4 lg:p-8 lg:pl-4 print:bg-transparent print:p-0 print:m-0"> <!-- Year select -->
<template #left>
<Button variant="ghost" :disabled="selectedYearIndex >= (years.length - 1)"
@click="selectedYearIndex++">
<ChevronLeft />
</Button>
<Select size="sm" v-model="selectedYearIndex" v-if="years.length > 1">
<SelectTrigger class=" hover:bg-accent">
<SelectValue placeholder="Jahr" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem :value="null">
<SelectLabel>Alle</SelectLabel>
</SelectItem>
<SelectSeparator />
<SelectItem v-for="(year, index) in years" :value="index">
<SelectLabel>{{ year }}</SelectLabel>
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
<Button variant="ghost" @click="selectedYearIndex--" :disabled="selectedYearIndex <= 0">
<ChevronRight />
</Button>
</template>
<div id="function-header" class="flex row justify-between items-center mb-4 gap-4"> <!-- Search field -->
<template #middle>
<!-- Year select --> <Input ref="search-field" id="search" type="text" placeholder="Filtern" class="px-8 bg-background"
<div class="flex row items-center"> v-model="searchQuery" />
<Button :variant="'ghost'" :disabled="selectedYearIndex >= (years.length - 1)" <span class="absolute start-0 inset-y-0 flex items-center justify-center px-2">
@click="selectedYearIndex++"> <Search class="size-4 text-muted-foreground" :stroke-width="1.5" />
<ChevronLeft /> </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> </Button>
<Select :size="'sm'" v-model="selectedYearIndex" v-if="years.length > 1"> </span>
<SelectTrigger class=" hover:bg-accent"> </template>
<SelectValue placeholder="Jahr" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem :value="null">
<SelectLabel>Alle</SelectLabel>
</SelectItem>
<SelectSeparator />
<SelectItem v-for="(year, index) in years" :value="index">
<SelectLabel>{{ year }}</SelectLabel>
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
<Button :variant="'ghost'" @click="selectedYearIndex--" :disabled="selectedYearIndex <= 0">
<ChevronRight />
</Button>
</div>
<!-- Search field --> <!-- New button -->
<div class="relative w-full max-w-sm items-center"> <template #right>
<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>
<!-- <Button size="sm" @click="testToast" class="mr-2">
Toast
</Button> -->
<!-- New button -->
<TooltipProvider> <TooltipProvider>
<Tooltip> <Tooltip>
<TooltipTrigger> <TooltipTrigger>
@@ -265,18 +255,17 @@ const deleteInvoice = async (id: number) => {
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
</TooltipProvider> </TooltipProvider>
</template>
</div> </AppHeader>
<!-- Invoice Table -->
<DocumentTable :invoices="filteredInvoices" :onItemClicked="showDetail" />
<!-- Invoice Table --> <!-- Invoice detail dialog -->
<DocumentTable :invoices="filteredInvoices" :onItemClicked="showDetail" /> <InvoiceDialog :invoiceData="activeInvoice" :customers="customersData" v-model="detailDialogOpen"
@save="saveInvoice" @delete="deleteInvoice" />
<!-- Invoice detail dialog -->
<InvoiceDialog :invoiceData="activeInvoice" :customers="customersData" v-model="detailDialogOpen"
@save="saveInvoice" @delete="deleteInvoice" />
</div>
</AppLayout> </AppLayout>
</template> </template>
+15 -44
View File
@@ -25,52 +25,34 @@ const user = page.props.auth.user;
</script> </script>
<template> <template>
<AppLayout> <AppLayout title="Profile settings">
<Head title="Profile settings" />
<SettingsLayout> <SettingsLayout>
<div class="flex flex-col space-y-6"> <div class="flex flex-col space-y-6">
<HeadingSmall title="Profile information" description="Update your name and email address" /> <HeadingSmall title="Profile information" description="Update your name and email address" />
<Form v-bind="ProfileController.update.form()" class="space-y-6" v-slot="{ errors, processing, recentlySuccessful }"> <Form v-bind="ProfileController.update.form()" class="space-y-6"
v-slot="{ errors, processing, recentlySuccessful }">
<div class="grid gap-2"> <div class="grid gap-2">
<Label for="name">Name</Label> <Label for="name">Name</Label>
<Input <Input id="name" class="mt-1 block w-full" name="name" :default-value="user.name" required
id="name" autocomplete="name" placeholder="Full name" />
class="mt-1 block w-full"
name="name"
:default-value="user.name"
required
autocomplete="name"
placeholder="Full name"
/>
<InputError class="mt-2" :message="errors.name" /> <InputError class="mt-2" :message="errors.name" />
</div> </div>
<div class="grid gap-2"> <div class="grid gap-2">
<Label for="email">Email address</Label> <Label for="email">Email address</Label>
<Input <Input id="email" type="email" class="mt-1 block w-full" name="email"
id="email" :default-value="user.email" required autocomplete="username" placeholder="Email address" />
type="email"
class="mt-1 block w-full"
name="email"
:default-value="user.email"
required
autocomplete="username"
placeholder="Email address"
/>
<InputError class="mt-2" :message="errors.email" /> <InputError class="mt-2" :message="errors.email" />
</div> </div>
<div v-if="mustVerifyEmail && !user.email_verified_at"> <div v-if="mustVerifyEmail && !user.email_verified_at">
<p class="-mt-4 text-sm text-muted-foreground"> <p class="-mt-4 text-sm text-muted-foreground">
Your email address is unverified. Your email address is unverified.
<Link <Link :href="send()" as="button"
:href="send()" class="text-foreground underline decoration-neutral-300 underline-offset-4 transition-colors duration-300 ease-out hover:decoration-current! dark:decoration-neutral-500">
as="button" Click here to resend the verification email.
class="text-foreground underline decoration-neutral-300 underline-offset-4 transition-colors duration-300 ease-out hover:decoration-current! dark:decoration-neutral-500"
>
Click here to resend the verification email.
</Link> </Link>
</p> </p>
@@ -81,28 +63,17 @@ const user = page.props.auth.user;
<div class="grid gap-2"> <div class="grid gap-2">
<Label for="avatar">Avatar</Label> <Label for="avatar">Avatar</Label>
<Input <Input id="avatar" type="avatar" class="mt-1 block w-full" name="avatar"
id="avatar" :default-value="user.avatar" required autocomplete="username"
type="avatar" placeholder="avatar address" />
class="mt-1 block w-full"
name="avatar"
:default-value="user.avatar"
required
autocomplete="username"
placeholder="avatar address"
/>
<InputError class="mt-2" :message="errors.email" /> <InputError class="mt-2" :message="errors.email" />
</div> </div>
<div class="flex items-center gap-4"> <div class="flex items-center gap-4">
<Button :disabled="processing" data-test="update-profile-button">Save</Button> <Button :disabled="processing" data-test="update-profile-button">Save</Button>
<Transition <Transition enter-active-class="transition ease-in-out" enter-from-class="opacity-0"
enter-active-class="transition ease-in-out" leave-active-class="transition ease-in-out" leave-to-class="opacity-0">
enter-from-class="opacity-0"
leave-active-class="transition ease-in-out"
leave-to-class="opacity-0"
>
<p v-show="recentlySuccessful" class="text-sm text-neutral-600">Saved.</p> <p v-show="recentlySuccessful" class="text-sm text-neutral-600">Saved.</p>
</Transition> </Transition>
</div> </div>