Add products module #46

This commit is contained in:
2025-11-26 10:05:43 +01:00
parent 914613e3ea
commit c55fc78c36
21 changed files with 607 additions and 50 deletions
+11 -5
View File
@@ -2,12 +2,12 @@
import NavFooter from '@/components/NavFooter.vue';
import NavMain from '@/components/NavMain.vue';
import { Sidebar, SidebarContent, SidebarFooter, SidebarHeader, SidebarTrigger } from '@/components/ui/sidebar';
import { dashboard, crm, offers, invoices, newInvoice, timesheets, customers, leads, achievements } from '@/routes';
import { dashboard, crm, offers, invoices, newInvoice, products, timesheets, customers, leads, achievements } from '@/routes';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
import { Kbd, KbdGroup } from '@/components/ui/kbd'
import { type NavItem, type NavGroup } from '@/types';
import { InertiaLinkProps, Link, usePage } from '@inertiajs/vue3';
import { Kanban, Euro, Trophy, Calculator, BookUser, Timer, Headset, Plus } from 'lucide-vue-next';
import { type NavGroup } from '@/types';
import { Link, usePage } from '@inertiajs/vue3';
import { Kanban, Euro, Trophy, Calculator, BookUser, Timer, ShoppingBasket, Headset, Plus } from 'lucide-vue-next';
import AppLogo from './AppLogo.vue';
import { computed } from 'vue';
@@ -45,7 +45,7 @@ const mainNavGroups: NavGroup[] = [
],
},
{
title: 'Finanzvorgänge',
title: 'Rechnungswesen',
items: [
{
title: 'Angebote',
@@ -53,6 +53,12 @@ const mainNavGroups: NavGroup[] = [
icon: Calculator,
color: 'text-cyan-600',
},
{
title: 'Produkte',
href: products(),
icon: ShoppingBasket,
color: 'text-yellow-400',
},
{
title: 'Rechnungen',
href: invoices(),
+99
View File
@@ -0,0 +1,99 @@
<script setup lang="ts">
import { ref, computed } from 'vue'
import AppLayout from '@/layouts/AppLayout.vue'
import AppHeader from '@/components/AppHeader.vue'
import { Product } from '@/types';
import Fuse from 'fuse.js';
import { Input } from '@/components/ui/crm-input'
import { Button } from '@/components/ui/crm-button'
import { Delete, Search, Plus } from "lucide-vue-next"
import PlaceholderPattern from '@/components/PlaceholderPattern.vue';
import { toCurrency, toRoundedCurrency } from '@/lib/utils';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
import { Kbd, KbdGroup } from '@/components/ui/kbd'
interface Props {
productsData: Product[];
}
const props = defineProps<Props>();
const searchQuery = ref('')
const searchField = ref()
const fuse = computed(() => {
return new Fuse(props.productsData, {
keys: ['title', 'description', 'notes'],
threshold: 0.3
});
})
const filteredProducts = computed(() => {
if (!searchQuery.value) {
return props.productsData;
}
return fuse.value.search(searchQuery.value).map(result => result.item);
})
</script>
<template>
<AppLayout title="Produkte">
<AppHeader>
<template #left></template>
<template #middle>
<!-- Search field -->
<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>
<template #right>
<!-- New button -->
<TooltipProvider>
<Tooltip>
<TooltipTrigger>
<Button size="sm" variant="action" @click="">
<Plus />
Neu
</Button>
</TooltipTrigger>
<TooltipContent>
<span>Neuen Kunden anlegen</span>
<KbdGroup class="ml-2">
<Kbd class="visible-mac"> N</Kbd>
<Kbd class="visible-pc">Ctrl N</Kbd>
</KbdGroup>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</template>
</AppHeader>
<div class="flex flex-wrap gap-8 lg:gap-12">
<div v-for="product in filteredProducts"
class="w-[calc(50%-4*var(--spacing))] md:w-[calc(33%-5.333*var(--spacing))] lg:w-[calc(25%-9*var(--spacing))] xl:w-[calc(20%-9.6*var(--spacing))] flex flex-col gap-4">
<div class="w-full relative aspect-4/3 overflow-hidden rounded-lg">
<img v-if="product.image" :src="'storage/uploads/products/' + product.image"
class="size-full object-cover" loading="lazy" />
<PlaceholderPattern v-else />
</div>
<div>
<h2 class="truncate">{{ product.title }}</h2>
<p class="text-muted-foreground text-sm truncate">{{ product.description }}</p>
</div>
<span class="grow flex items-end text-sm font-bold">{{ toCurrency((product?.price || 0) * (1 +
Number(product.margin))) }}</span>
</div>
</div>
</AppLayout>
</template>
+71 -1
View File
@@ -279,4 +279,74 @@ export function newPaymentTerms(): PaymentTerms {
isFixed: false,
days: 14
}
}
}
export interface ProductCategory {
id: number;
name: string;
createdAt: Date;
updatedAt: Date;
}
export function newProductCategory(): ProductCategory {
return {
id: 0,
name: '',
createdAt: new Date(),
updatedAt: new Date()
}
}
export interface Unit {
id: number;
name: string;
symbol: string | null;
createdAt: Date;
updatedAt: Date;
}
export function newUnit(): Unit {
return {
id: 0,
name: '',
symbol: null,
createdAt: new Date(),
updatedAt: new Date()
}
}
export interface Product {
id: number;
title: string;
description: string | null;
notes: string | null;
vendorUrl: string | null;
categoryId: number | null;
category: ProductCategory | null;
price: number | null;
margin: number;
unitId: number | null;
unit: Unit | null;
image: string | null;
createdAt: Date;
updatedAt: Date;
}
export function newProduct(): Product {
return {
id: 0,
title: '',
description: null,
notes: null,
vendorUrl: null,
categoryId: null,
category: null,
price: null,
margin: 0.2,
unitId: null,
unit: null,
image: null,
createdAt: new Date(),
updatedAt: new Date()
}
}