Add products module #46
This commit is contained in:
@@ -9,13 +9,21 @@ class LineItemController extends Controller
|
||||
{
|
||||
public function index($invoiceId)
|
||||
{
|
||||
$items = LineItem::select()
|
||||
$items = LineItem::with('unit')
|
||||
->select('line_items.*')
|
||||
->where('invoice_id', $invoiceId)
|
||||
->orderBy('position', 'desc')
|
||||
->get();
|
||||
|
||||
return $items->map(function ($item) {
|
||||
return ApiDataTransformer::snakeToCamel($item->toArray());
|
||||
$itemArray = $item->toArray();
|
||||
|
||||
if ($item->unit) {
|
||||
$itemArray['unit_name'] = $item->unit->name;
|
||||
$itemArray['unit_symbol'] = $item->unit->symbol;
|
||||
}
|
||||
|
||||
return ApiDataTransformer::snakeToCamel($itemArray);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\ProductCategory;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class ProductCategoryController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display a listing of the resource.
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the form for creating a new resource.
|
||||
*/
|
||||
public function create()
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a newly created resource in storage.
|
||||
*/
|
||||
public function store(Request $request)
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the specified resource.
|
||||
*/
|
||||
public function show(ProductCategory $productCategory)
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the form for editing the specified resource.
|
||||
*/
|
||||
public function edit(ProductCategory $productCategory)
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the specified resource in storage.
|
||||
*/
|
||||
public function update(Request $request, ProductCategory $productCategory)
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the specified resource from storage.
|
||||
*/
|
||||
public function destroy(ProductCategory $productCategory)
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Product;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Support\ApiDataTransformer;
|
||||
use Inertia\Inertia;
|
||||
|
||||
|
||||
class ProductController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display a listing of the resource.
|
||||
*/
|
||||
public function show()
|
||||
{
|
||||
return Inertia::render(
|
||||
'Products',
|
||||
['productsData' => $this->index()]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Display a listing of the resource.
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
$products = Product::with(['category', 'unit'])->orderBy('title', 'asc')->get();
|
||||
return ApiDataTransformer::snakeToCamel($products->toArray());
|
||||
}
|
||||
|
||||
public function single($id)
|
||||
{
|
||||
$products = Product::with(['category', 'unit'])->findOrFail($id);
|
||||
return ApiDataTransformer::snakeToCamel($products->toArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the form for creating a new resource.
|
||||
*/
|
||||
public function create()
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a newly created resource in storage.
|
||||
*/
|
||||
public function store(Request $request)
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the form for editing the specified resource.
|
||||
*/
|
||||
public function edit(Product $product)
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the specified resource in storage.
|
||||
*/
|
||||
public function update(Request $request, Product $product)
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the specified resource from storage.
|
||||
*/
|
||||
public function destroy(Product $product)
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
||||
@@ -15,8 +15,8 @@ class LineItem extends Model
|
||||
'is_section',
|
||||
'title',
|
||||
'description',
|
||||
'unit_id',
|
||||
'quantity',
|
||||
'unit',
|
||||
'price',
|
||||
];
|
||||
|
||||
@@ -24,4 +24,9 @@ public function invoice()
|
||||
{
|
||||
return $this->belongsTo(Invoice::class);
|
||||
}
|
||||
|
||||
public function unit()
|
||||
{
|
||||
return $this->belongsTo(Unit::class);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use App\Models\ProductCategory;
|
||||
|
||||
class Product extends Model
|
||||
{
|
||||
/** @use HasFactory<\Database\Factories\ProductFactory> */
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'title',
|
||||
'description',
|
||||
'notes',
|
||||
'vendor_url',
|
||||
'category_id',
|
||||
'price',
|
||||
'margin',
|
||||
'unit_id',
|
||||
'image',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'price' => 'decimal:2',
|
||||
'margin' => 'decimal:2'
|
||||
];
|
||||
|
||||
public function category()
|
||||
{
|
||||
return $this->belongsTo(ProductCategory::class);
|
||||
}
|
||||
|
||||
public function unit()
|
||||
{
|
||||
return $this->belongsTo(Unit::class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
|
||||
class ProductCategory extends Model
|
||||
{
|
||||
/** @use HasFactory<\Database\Factories\ProductFactory> */
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = ['name'];
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Unit extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = ['name', 'symbol'];
|
||||
|
||||
public function products()
|
||||
{
|
||||
return $this->hasMany(Product::class);
|
||||
}
|
||||
|
||||
public function lineItems()
|
||||
{
|
||||
return $this->hasMany(LineItem::class);
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace Database\Factories;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
use App\Models\Unit;
|
||||
|
||||
class LineItemFactory extends Factory
|
||||
{
|
||||
@@ -16,7 +17,7 @@ public function definition(): array
|
||||
'title' => $this->faker->words(3, true),
|
||||
'description' => $this->faker->sentence(),
|
||||
'quantity' => $this->faker->numberBetween(1, 10),
|
||||
'unit' => $this->faker->randomElement(['Stück', 'Stunden', 'Tage', 'pauschal']),
|
||||
'unit_id' => Unit::factory(),
|
||||
'price' => $this->faker->randomFloat(2, 10, 500),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
use App\Models\ProductCategory;
|
||||
|
||||
/**
|
||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\ProductCategory>
|
||||
*/
|
||||
class ProductCategoryFactory extends Factory
|
||||
{
|
||||
protected $model = ProductCategory::class;
|
||||
|
||||
/**
|
||||
* Define the model's default state.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'name' => $this->faker->unique()->word()
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
use App\Models\Product;
|
||||
use App\Models\ProductCategory;
|
||||
use App\Models\Unit;
|
||||
|
||||
class ProductFactory extends Factory
|
||||
{
|
||||
protected $model = Product::class;
|
||||
|
||||
public function definition()
|
||||
{
|
||||
return [
|
||||
'title' => $this->faker->words(3, true),
|
||||
'description' => $this->faker->optional()->paragraph(),
|
||||
'notes' => $this->faker->optional()->sentence(),
|
||||
'vendor_url' => $this->faker->optional()->url(),
|
||||
'category_id' => ProductCategory::factory(),
|
||||
'unit_id' => Unit::factory(), // Neue Einheit
|
||||
'price' => $this->faker->optional()->randomFloat(2, 10, 1000),
|
||||
'margin' => $this->faker->randomFloat(2, 0.1, 0.5),
|
||||
'image' => $this->faker->optional()->imageUrl(640, 480, 'products', true)
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
class UnitFactory extends Factory
|
||||
{
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'name' => $this->faker->unique()->randomElement([
|
||||
'Stück',
|
||||
'Stunden',
|
||||
'Tage',
|
||||
'pauschal'
|
||||
]),
|
||||
'symbol' => $this->faker->lexify('??'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration {
|
||||
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('customers', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->enum('type', ['private', 'business']);
|
||||
$table->string('company_name', 100)->nullable();
|
||||
$table->string('vat_id', 50)->nullable();
|
||||
$table->string('tax_id', 50)->nullable(); // Tax identification number
|
||||
$table->string('global_id', 50)->nullable(); // Global Location Number (GLN)
|
||||
$table->string('legal_registration_id', 50)->nullable(); // Legal registration ID
|
||||
$table->string('email', 100)->unique();
|
||||
$table->string('phone', 20)->nullable();
|
||||
$table->json('billing_address'); // Structured as per ZUGFeRD standard
|
||||
$table->integer('payment_terms')->default(14);
|
||||
$table->enum('status', ['active', 'inactive', 'prospect'])->default('active');
|
||||
$table->text('notes')->nullable();
|
||||
$table->timestamps();
|
||||
// $table->index('email');
|
||||
});
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('customers');
|
||||
}
|
||||
};
|
||||
@@ -19,7 +19,7 @@ public function up(): void
|
||||
$table->string('title')->nullable();
|
||||
$table->string('description')->nullable();
|
||||
$table->integer('quantity')->default(1);
|
||||
$table->string('unit')->default('Stunden');
|
||||
$table->foreignId('unit_id')->nullable()->constrained()->nullOnDelete();
|
||||
$table->decimal('price', 10, 2)->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('products', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('title', 64);
|
||||
$table->text('description')->nullable();
|
||||
$table->text('notes')->nullable();
|
||||
$table->string('vendor_url', 100)->nullable();
|
||||
$table->foreignId('category_id')->constrained()->nullOnDelete()->nullable();
|
||||
$table->decimal('price', 10, 2)->nullable();
|
||||
$table->decimal('margin', 2, 2)->default(0.2);
|
||||
$table->foreignId('unit_id')->nullable()->constrained()->nullOnDelete();
|
||||
$table->string('image')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['title', 'description']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('products');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('product_categories', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name', 64);
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['name']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('product_categories');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('units', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name', 50)->unique();
|
||||
$table->string('symbol', 10)->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
// Füge Standard-Einheiten hinzu
|
||||
DB::table('units')->insert([
|
||||
['name' => 'Stück', 'symbol' => 'Stk'],
|
||||
['name' => 'Stunden', 'symbol' => 'h'],
|
||||
['name' => 'Tage', 'symbol' => 'd'],
|
||||
['name' => 'pauschal', 'symbol' => 'p'],
|
||||
]);
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('units');
|
||||
}
|
||||
};
|
||||
@@ -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(),
|
||||
|
||||
@@ -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>
|
||||
Vendored
+71
-1
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@
|
||||
use App\Http\Controllers\InvoiceController;
|
||||
use App\Http\Controllers\LineItemController;
|
||||
use App\Http\Controllers\PaymentTermsController;
|
||||
use App\Http\Controllers\ProductController;
|
||||
use App\Http\Controllers\SettingController;
|
||||
use App\Mail\OrderConfirmation;
|
||||
|
||||
@@ -23,6 +24,9 @@
|
||||
|
||||
Route::delete('/notes/{id}', [NoteController::class, 'delete']);
|
||||
|
||||
Route::get('/products/', [ProductController::class, 'index']);
|
||||
Route::get('/products/{id}', [ProductController::class, 'single']);
|
||||
|
||||
Route::get('/invoices/summary', [InvoiceController::class, 'summaryAll']);
|
||||
Route::get('/invoices/summaryThisYear', [InvoiceController::class, 'summaryThisYear']);
|
||||
Route::get('/invoices/summaryBeforeThisYear', [InvoiceController::class, 'summaryBeforeThisYear']);
|
||||
|
||||
+7
-2
@@ -4,6 +4,7 @@
|
||||
use Inertia\Inertia;
|
||||
use App\Http\Controllers\InvoiceController;
|
||||
use App\Http\Controllers\CustomerController;
|
||||
use App\Http\Controllers\ProductController;
|
||||
|
||||
Route::middleware('auth')->group(function () {
|
||||
|
||||
@@ -46,11 +47,15 @@
|
||||
Route::get('invoice/{id}/pdf', [InvoiceController::class, 'exportPdf'])->name('invoiceExportPdf');
|
||||
Route::get('invoice/{id}/xml', [InvoiceController::class, 'exportXml'])->name('invoiceExportXml');
|
||||
|
||||
// Timesheets
|
||||
// Products
|
||||
Route::get('products', [ProductController::class, 'show'])->name('products');
|
||||
|
||||
|
||||
|
||||
Route::get('timesheets', function () {
|
||||
return Inertia::render('Timesheets');
|
||||
})->name('timesheets');
|
||||
|
||||
|
||||
// Procedural Documentation
|
||||
Route::get('proceduralDocumentation', function () {
|
||||
return Inertia::render('ProceduralDocumentation');
|
||||
|
||||
Reference in New Issue
Block a user