Move user menu to sidebar, fixes #35
This commit is contained in:
@@ -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>
|
|
||||||
@@ -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>
|
||||||
@@ -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 />
|
||||||
|
|||||||
@@ -1,31 +1,35 @@
|
|||||||
<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>
|
||||||
|
<DropdownMenuTrigger as-child>
|
||||||
|
<SidebarMenuItem>
|
||||||
<SidebarMenuButton
|
<SidebarMenuButton
|
||||||
class="text-neutral-600 hover:text-neutral-800 dark:text-neutral-300 dark:hover:text-neutral-100"
|
class="pl-0 rounded-[1rem_var(--radius-md)_var(--radius-md)_1rem] group-data-[state=collapsed]:rounded-full">
|
||||||
as-child>
|
<UserInfo :user="props.user" />
|
||||||
<Link :href="item.href">
|
|
||||||
<component :is="item.icon" :class="item.color" stroke-width="1.5" />
|
|
||||||
<span>{{ item.title }}</span>
|
|
||||||
</Link>
|
|
||||||
</SidebarMenuButton>
|
</SidebarMenuButton>
|
||||||
</SidebarMenuItem>
|
</SidebarMenuItem>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="start">
|
||||||
|
<UserMenuContent />
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
</SidebarMenu>
|
</SidebarMenu>
|
||||||
</SidebarGroupContent>
|
</SidebarGroupContent>
|
||||||
</SidebarGroup>
|
</SidebarGroup>
|
||||||
|
|||||||
@@ -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,9 +21,10 @@ 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">
|
||||||
|
<Avatar class="size-8 overflow-hidden rounded-lg">
|
||||||
<AvatarImage v-if="showAvatar" :src="user.avatar!" :alt="user.name" />
|
<AvatarImage v-if="showAvatar" :src="user.avatar!" :alt="user.name" />
|
||||||
<AvatarFallback class="rounded-full text-black dark:text-white">
|
<AvatarFallback class="rounded-full bg-primary text-black dark:text-white">
|
||||||
{{ getInitials(user.name) }}
|
{{ getInitials(user.name) }}
|
||||||
</AvatarFallback>
|
</AvatarFallback>
|
||||||
</Avatar>
|
</Avatar>
|
||||||
@@ -31,4 +33,5 @@ const showAvatar = computed(() => props.user.avatar && props.user.avatar !== '')
|
|||||||
<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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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,13 +63,18 @@ onMounted(() => {
|
|||||||
</template>
|
</template>
|
||||||
</Toaster>
|
</Toaster>
|
||||||
|
|
||||||
|
<div class="overflow-x-auto p-4 lg:p-8 lg:pl-4 print:bg-transparent print:p-0 print:m-0">
|
||||||
|
|
||||||
<SidebarProvider :default-open="isOpen">
|
<SidebarProvider :default-open="isOpen">
|
||||||
<AppSidebar />
|
<AppSidebar />
|
||||||
<AppContent variant="sidebar" class="overflow-x-hidden bg-main">
|
|
||||||
|
<main class="w-full overflow-x-hidden">
|
||||||
<slot />
|
<slot />
|
||||||
</AppContent>
|
</main>
|
||||||
</SidebarProvider>
|
</SidebarProvider>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
<AlertDialog v-model:open="alert.open">
|
<AlertDialog v-model:open="alert.open">
|
||||||
<AlertDialogContent>
|
<AlertDialogContent>
|
||||||
<AlertDialogHeader>
|
<AlertDialogHeader>
|
||||||
@@ -76,7 +87,6 @@ onMounted(() => {
|
|||||||
</AlertDialogFooter>
|
</AlertDialogFooter>
|
||||||
</AlertDialogContent>
|
</AlertDialogContent>
|
||||||
</AlertDialog>
|
</AlertDialog>
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
@@ -7,11 +7,7 @@ import PlaceholderPattern from '../components/PlaceholderPattern.vue';
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
||||||
<Head title="Erfolge" />
|
<AppLayout title="Erfolge">
|
||||||
|
|
||||||
<AppLayout>
|
|
||||||
<div class="flex h-full flex-1 flex-col gap-4 overflow-x-auto rounded-xl p-4">
|
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h1 class="text-xl">Kategorie</h1>
|
<h1 class="text-xl">Kategorie</h1>
|
||||||
<h2 class="text-sm text-neutral-400 mb-4">3 von 10 freigeschaltet</h2>
|
<h2 class="text-sm text-neutral-400 mb-4">3 von 10 freigeschaltet</h2>
|
||||||
@@ -27,7 +23,5 @@ import PlaceholderPattern from '../components/PlaceholderPattern.vue';
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
|
||||||
</AppLayout>
|
</AppLayout>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -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
|
|
||||||
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 -->
|
<!-- View buttons -->
|
||||||
|
<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" />
|
||||||
@@ -138,9 +133,11 @@ const call = (number: string, event: Event) => {
|
|||||||
<Rows3 stroke-width="1.5" />
|
<Rows3 stroke-width="1.5" />
|
||||||
</Button>
|
</Button>
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
|
||||||
<!-- Search field -->
|
<!-- Search field -->
|
||||||
<div class="relative w-full max-w-sm items-center">
|
<template #middle>
|
||||||
<Input ref="search-field" id="search" type="text" placeholder="Filtern" class="px-8 bg-background"
|
<Input ref="search-field" id="search" type="text" placeholder="Filtern" class="px-8 bg-background"
|
||||||
v-model="searchQuery" />
|
v-model="searchQuery" />
|
||||||
<span class="absolute start-0 inset-y-0 flex items-center justify-center px-2">
|
<span class="absolute start-0 inset-y-0 flex items-center justify-center px-2">
|
||||||
@@ -151,13 +148,15 @@ const call = (number: string, event: Event) => {
|
|||||||
<Delete class="size-4 text-muted-foreground" :stroke-width="1.5" />
|
<Delete class="size-4 text-muted-foreground" :stroke-width="1.5" />
|
||||||
</Button>
|
</Button>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</template>
|
||||||
|
|
||||||
<!-- New button -->
|
<!-- 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">
|
||||||
@@ -280,8 +279,6 @@ const call = (number: string, event: Event) => {
|
|||||||
<!-- Invoice detail dialog -->
|
<!-- Invoice detail dialog -->
|
||||||
<CustomerDialog :customerData="activeCustomer" v-model="detailDialogOpen" @save="" @delete="" />
|
<CustomerDialog :customerData="activeCustomer" v-model="detailDialogOpen" @save="" @delete="" />
|
||||||
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</AppLayout>
|
</AppLayout>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,23 +189,18 @@ const deleteInvoice = async (id: number) => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
||||||
<Head title="Rechnungen" />
|
|
||||||
|
|
||||||
<!-- Function Header -->
|
<!-- Function Header -->
|
||||||
<AppLayout>
|
<AppLayout title="Rechnungen">
|
||||||
|
|
||||||
<div
|
|
||||||
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">
|
|
||||||
|
|
||||||
<div id="function-header" class="flex row justify-between items-center mb-4 gap-4">
|
|
||||||
|
|
||||||
|
<AppHeader>
|
||||||
<!-- Year select -->
|
<!-- Year select -->
|
||||||
<div class="flex row items-center">
|
<template #left>
|
||||||
<Button :variant="'ghost'" :disabled="selectedYearIndex >= (years.length - 1)"
|
<Button variant="ghost" :disabled="selectedYearIndex >= (years.length - 1)"
|
||||||
@click="selectedYearIndex++">
|
@click="selectedYearIndex++">
|
||||||
<ChevronLeft />
|
<ChevronLeft />
|
||||||
</Button>
|
</Button>
|
||||||
<Select :size="'sm'" v-model="selectedYearIndex" v-if="years.length > 1">
|
<Select size="sm" v-model="selectedYearIndex" v-if="years.length > 1">
|
||||||
<SelectTrigger class=" hover:bg-accent">
|
<SelectTrigger class=" hover:bg-accent">
|
||||||
<SelectValue placeholder="Jahr" />
|
<SelectValue placeholder="Jahr" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
@@ -223,13 +216,13 @@ const deleteInvoice = async (id: number) => {
|
|||||||
</SelectGroup>
|
</SelectGroup>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
<Button :variant="'ghost'" @click="selectedYearIndex--" :disabled="selectedYearIndex <= 0">
|
<Button variant="ghost" @click="selectedYearIndex--" :disabled="selectedYearIndex <= 0">
|
||||||
<ChevronRight />
|
<ChevronRight />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</template>
|
||||||
|
|
||||||
<!-- Search field -->
|
<!-- Search field -->
|
||||||
<div class="relative w-full max-w-sm items-center">
|
<template #middle>
|
||||||
<Input ref="search-field" id="search" type="text" placeholder="Filtern" class="px-8 bg-background"
|
<Input ref="search-field" id="search" type="text" placeholder="Filtern" class="px-8 bg-background"
|
||||||
v-model="searchQuery" />
|
v-model="searchQuery" />
|
||||||
<span class="absolute start-0 inset-y-0 flex items-center justify-center px-2">
|
<span class="absolute start-0 inset-y-0 flex items-center justify-center px-2">
|
||||||
@@ -240,13 +233,10 @@ const deleteInvoice = async (id: number) => {
|
|||||||
<Delete class="size-4 text-muted-foreground" :stroke-width="1.5" />
|
<Delete class="size-4 text-muted-foreground" :stroke-width="1.5" />
|
||||||
</Button>
|
</Button>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</template>
|
||||||
|
|
||||||
<!-- <Button size="sm" @click="testToast" class="mr-2">
|
|
||||||
Toast
|
|
||||||
</Button> -->
|
|
||||||
|
|
||||||
<!-- New button -->
|
<!-- New button -->
|
||||||
|
<template #right>
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger>
|
<TooltipTrigger>
|
||||||
@@ -265,9 +255,9 @@ const deleteInvoice = async (id: number) => {
|
|||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
|
</template>
|
||||||
|
|
||||||
</div>
|
</AppHeader>
|
||||||
|
|
||||||
|
|
||||||
<!-- Invoice Table -->
|
<!-- Invoice Table -->
|
||||||
<DocumentTable :invoices="filteredInvoices" :onItemClicked="showDetail" />
|
<DocumentTable :invoices="filteredInvoices" :onItemClicked="showDetail" />
|
||||||
@@ -276,7 +266,6 @@ const deleteInvoice = async (id: number) => {
|
|||||||
<InvoiceDialog :invoiceData="activeInvoice" :customers="customersData" v-model="detailDialogOpen"
|
<InvoiceDialog :invoiceData="activeInvoice" :customers="customersData" v-model="detailDialogOpen"
|
||||||
@save="saveInvoice" @delete="deleteInvoice" />
|
@save="saveInvoice" @delete="deleteInvoice" />
|
||||||
|
|
||||||
</div>
|
|
||||||
</AppLayout>
|
</AppLayout>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -25,51 +25,33 @@ 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"
|
|
||||||
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.
|
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>
|
||||||
|
|||||||
Reference in New Issue
Block a user