diff --git a/app/Http/Controllers/LineItemController.php b/app/Http/Controllers/LineItemController.php index 6629216..219e1c2 100644 --- a/app/Http/Controllers/LineItemController.php +++ b/app/Http/Controllers/LineItemController.php @@ -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); }); } } diff --git a/app/Http/Controllers/ProductCategoryController.php b/app/Http/Controllers/ProductCategoryController.php new file mode 100644 index 0000000..45cd4f1 --- /dev/null +++ b/app/Http/Controllers/ProductCategoryController.php @@ -0,0 +1,65 @@ + $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) + { + // + } +} diff --git a/app/Models/LineItem.php b/app/Models/LineItem.php index d07fbe5..ef4bdae 100644 --- a/app/Models/LineItem.php +++ b/app/Models/LineItem.php @@ -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); + } } diff --git a/app/Models/Product.php b/app/Models/Product.php new file mode 100644 index 0000000..66020bc --- /dev/null +++ b/app/Models/Product.php @@ -0,0 +1,40 @@ + */ + 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); + } +} diff --git a/app/Models/ProductCategory.php b/app/Models/ProductCategory.php new file mode 100644 index 0000000..a3bb56b --- /dev/null +++ b/app/Models/ProductCategory.php @@ -0,0 +1,15 @@ + */ + use HasFactory; + + protected $fillable = ['name']; +} diff --git a/app/Models/Unit.php b/app/Models/Unit.php new file mode 100644 index 0000000..d297c3f --- /dev/null +++ b/app/Models/Unit.php @@ -0,0 +1,23 @@ +hasMany(Product::class); + } + + public function lineItems() + { + return $this->hasMany(LineItem::class); + } +} diff --git a/database/factories/LineItemFactory.php b/database/factories/LineItemFactory.php index 911ee36..c2cd1ff 100644 --- a/database/factories/LineItemFactory.php +++ b/database/factories/LineItemFactory.php @@ -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), ]; } diff --git a/database/factories/ProductCategroyFactory.php b/database/factories/ProductCategroyFactory.php new file mode 100644 index 0000000..09b0df3 --- /dev/null +++ b/database/factories/ProductCategroyFactory.php @@ -0,0 +1,26 @@ + + */ +class ProductCategoryFactory extends Factory +{ + protected $model = ProductCategory::class; + + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + 'name' => $this->faker->unique()->word() + ]; + } +} \ No newline at end of file diff --git a/database/factories/ProductFactory.php b/database/factories/ProductFactory.php new file mode 100644 index 0000000..b016e84 --- /dev/null +++ b/database/factories/ProductFactory.php @@ -0,0 +1,28 @@ + $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) + ]; + } +} \ No newline at end of file diff --git a/database/factories/UnitFactory.php b/database/factories/UnitFactory.php new file mode 100644 index 0000000..ebecf5b --- /dev/null +++ b/database/factories/UnitFactory.php @@ -0,0 +1,21 @@ + $this->faker->unique()->randomElement([ + 'Stück', + 'Stunden', + 'Tage', + 'pauschal' + ]), + 'symbol' => $this->faker->lexify('??'), + ]; + } +} diff --git a/database/migrations/2025_09_17_000000 b/database/migrations/2025_09_17_000000 deleted file mode 100644 index 16c4350..0000000 --- a/database/migrations/2025_09_17_000000 +++ /dev/null @@ -1,37 +0,0 @@ -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'); - } -}; \ No newline at end of file diff --git a/database/migrations/2025_09_18_193251_create_line_items_table.php b/database/migrations/2025_09_18_193251_create_line_items_table.php index c6f3e5b..c9112c9 100644 --- a/database/migrations/2025_09_18_193251_create_line_items_table.php +++ b/database/migrations/2025_09_18_193251_create_line_items_table.php @@ -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(); }); diff --git a/database/migrations/2025_11_25_133828_create_products_table.php b/database/migrations/2025_11_25_133828_create_products_table.php new file mode 100644 index 0000000..8710896 --- /dev/null +++ b/database/migrations/2025_11_25_133828_create_products_table.php @@ -0,0 +1,38 @@ +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'); + } +}; diff --git a/database/migrations/2025_11_25_151351_create_product_categories_table.php b/database/migrations/2025_11_25_151351_create_product_categories_table.php new file mode 100644 index 0000000..fb61b9f --- /dev/null +++ b/database/migrations/2025_11_25_151351_create_product_categories_table.php @@ -0,0 +1,30 @@ +id(); + $table->string('name', 64); + $table->timestamps(); + + $table->index(['name']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('product_categories'); + } +}; diff --git a/database/migrations/2025_11_25_153801_create_units_table.php b/database/migrations/2025_11_25_153801_create_units_table.php new file mode 100644 index 0000000..2c7f0c9 --- /dev/null +++ b/database/migrations/2025_11_25_153801_create_units_table.php @@ -0,0 +1,32 @@ +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'); + } +}; \ No newline at end of file diff --git a/resources/js/components/AppSidebar.vue b/resources/js/components/AppSidebar.vue index 0d73aeb..fadf962 100644 --- a/resources/js/components/AppSidebar.vue +++ b/resources/js/components/AppSidebar.vue @@ -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(), diff --git a/resources/js/pages/Products.vue b/resources/js/pages/Products.vue new file mode 100644 index 0000000..2eaf260 --- /dev/null +++ b/resources/js/pages/Products.vue @@ -0,0 +1,99 @@ + + + diff --git a/resources/js/types/index.d.ts b/resources/js/types/index.d.ts index e148adc..ba8c554 100644 --- a/resources/js/types/index.d.ts +++ b/resources/js/types/index.d.ts @@ -279,4 +279,74 @@ export function newPaymentTerms(): PaymentTerms { isFixed: false, days: 14 } -} \ No newline at end of file +} + +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() + } +} \ No newline at end of file diff --git a/routes/api.php b/routes/api.php index 5b4bbb6..9628f2f 100644 --- a/routes/api.php +++ b/routes/api.php @@ -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']); diff --git a/routes/web.php b/routes/web.php index 2f9672e..6a5458d 100644 --- a/routes/web.php +++ b/routes/web.php @@ -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');