Erster Upload

This commit is contained in:
Malek Tellissi
2026-09-11 01:17:47 +02:00
commit 400172cdde
171 changed files with 18844 additions and 0 deletions
@@ -0,0 +1,54 @@
<?php
namespace App\Console\Commands;
use App\Models\Product;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\File;
class DeleteUnusedUploads extends Command {
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'uploads:cron';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Deletes unused image uploads';
public function __construct() {
parent::__construct();
}
/**
* Execute the console command.
*/
public function handle() {
// Define the absolute path to the uploads directory
$uploadDirectory = storage_path('app/public/product-images/');
// Get all files in the directory
$files = File::allFiles($uploadDirectory);
// Loop through each file in the directory
foreach ($files as $file) {
// Get the relative path of the filev
$relativePath = 'product-images/' . str_replace($uploadDirectory, '', $file->getPathname());
// If the file doesn't exist in the database, delete it
$products = Product::whereJsonContains('images', $relativePath)->get();
if (count($products) < 1) {
echo time() . ' Deleting ' . $relativePath . PHP_EOL;
File::delete($file); // Delete the file from server
}
}
}
}
+122
View File
@@ -0,0 +1,122 @@
<?php
namespace App\Console\Commands;
use App\Models\Packing;
use App\Models\Product;
use App\Models\ProductInventory;
use App\Models\ProductSize;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
class ImportShopify extends Command
{
protected $signature = 'import:shopify {file : Pfad zur Shopify Inventory-CSV}';
protected $description = 'Importiert Produkte aus einem Shopify Inventory-Export';
public function handle(): int
{
$file = $this->argument('file');
if (! file_exists($file)) {
$this->error("Datei nicht gefunden: $file");
return self::FAILURE;
}
$rows = $this->parseCsv($file);
if (empty($rows)) {
$this->error('CSV ist leer oder konnte nicht gelesen werden.');
return self::FAILURE;
}
$byHandle = collect($rows)->groupBy('Handle');
$this->info("Gefunden: {$byHandle->count()} Produkte, " . count($rows) . ' Varianten');
$stats = ['products' => 0, 'sizes' => 0, 'inventory' => 0, 'skipped' => 0];
DB::transaction(function () use ($byHandle, &$stats) {
foreach ($byHandle as $variants) {
$first = $variants->first();
$product = Product::firstOrCreate(
['title' => $first['Title']],
['recipes_id' => null],
);
if ($product->wasRecentlyCreated) {
$stats['products']++;
}
foreach ($variants as $row) {
$packingRaw = $row['Option1 Value'];
// Format: "Standbodenbeutel 50 g" oder "Metalldose 40 g"
if (! preg_match('/^(.+?)\s+(\d+(?:[.,]\d+)?)\s*g$/i', $packingRaw, $m)) {
$this->warn(" ⚠ Verpackungsformat nicht erkannt: '$packingRaw' übersprungen");
$stats['skipped']++;
continue;
}
$packing = Packing::firstOrCreate([
'name' => trim($m[1]),
'size' => (float) str_replace(',', '.', $m[2]),
]);
$sku = $row['SKU'] ?: null;
$size = ProductSize::firstOrCreate(
['product_id' => $product->id, 'packing_id' => $packing->id],
['sku' => $sku, 'price' => 0],
);
if ($size->wasRecentlyCreated) {
$stats['sizes']++;
}
$onHand = (int) $row['On hand (current)'];
for ($i = 0; $i < $onHand; $i++) {
ProductInventory::create([
'product_id' => $product->id,
'size_id' => $size->id,
]);
$stats['inventory']++;
}
}
}
});
$this->info("{$stats['products']} Produkte erstellt");
$this->info("{$stats['sizes']} Varianten erstellt");
$this->info("{$stats['inventory']} Lagereinträge erstellt");
if ($stats['skipped'] > 0) {
$this->warn("{$stats['skipped']} Zeilen übersprungen (Verpackungsformat unbekannt)");
}
return self::SUCCESS;
}
private function parseCsv(string $file): array
{
$rows = [];
$headers = null;
$handle = fopen($file, 'r');
if ($handle === false) {
return [];
}
while (($data = fgetcsv($handle, 0, ',')) !== false) {
if ($headers === null) {
$headers = array_map('trim', $data);
continue;
}
if (count($data) === count($headers)) {
$rows[] = array_combine($headers, $data);
}
}
fclose($handle);
return $rows;
}
}
@@ -0,0 +1,142 @@
<?php
namespace App\Filament\User\Resources;
use App\Filament\User\Resources\BatchResource\Pages;
use App\Models\Batch;
use App\Models\Recipe;
use Filament\Forms;
use Filament\Forms\Form;
use Filament\Forms\Get;
use Filament\Resources\Resource;
use Filament\Tables;
use Filament\Tables\Table;
class BatchResource extends Resource
{
protected static ?string $model = Batch::class;
protected static ?string $navigationIcon = 'heroicon-o-qr-code';
protected static ?string $navigationLabel = 'Chargen (LOT)';
protected static ?string $modelLabel = 'Charge';
protected static ?string $pluralModelLabel = 'Chargen';
protected static ?int $navigationSort = 5;
public static function form(Form $form): Form
{
return $form->schema([
Forms\Components\Section::make('Charge')->schema([
Forms\Components\TextInput::make('lot_number')
->label('LOT-Nummer')
->required()
->unique(ignoreRecord: true)
->maxLength(255),
Forms\Components\Select::make('product_id')
->label('Produkt')
->relationship('product', 'title')
->searchable()
->preload()
->required()
->live(),
Forms\Components\Select::make('recipe_id')
->label('Rezeptur')
->relationship('recipe', 'name')
->searchable()
->preload()
->required()
->live(),
Forms\Components\DatePicker::make('produced_at')
->label('Produktionsdatum'),
Forms\Components\DatePicker::make('expires_at')
->label('Mindesthaltbarkeitsdatum'),
])->columns(2),
Forms\Components\Section::make('Herkunft der Gewürze')->schema([
Forms\Components\Repeater::make('origins')
->label('')
->relationship()
->schema([
Forms\Components\Select::make('spice_id')
->label('Gewürz')
->relationship('spice', 'name')
->searchable()
->preload()
->required(),
Forms\Components\Select::make('supplier_id')
->label('Lieferant')
->relationship('supplier', 'name')
->searchable()
->preload()
->nullable(),
Forms\Components\TextInput::make('origin_country')
->label('Herkunftsland')
->maxLength(100),
Forms\Components\TextInput::make('farmer')
->label('Landwirt / Betrieb')
->maxLength(255),
Forms\Components\Textarea::make('notes')
->label('Infos zum Gewürz')
->rows(2)
->columnSpanFull(),
])
->columns(2)
->addActionLabel('Gewürz hinzufügen')
->reorderable(false),
]),
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
Tables\Columns\TextColumn::make('lot_number')
->label('LOT-Nummer')
->searchable()
->sortable(),
Tables\Columns\TextColumn::make('product.title')
->label('Produkt')
->searchable(),
Tables\Columns\TextColumn::make('recipe.name')
->label('Rezeptur'),
Tables\Columns\TextColumn::make('produced_at')
->label('Produktion')
->date('d.m.Y')
->sortable(),
Tables\Columns\TextColumn::make('expires_at')
->label('MHD')
->date('d.m.Y')
->sortable(),
])
->defaultSort('produced_at', 'desc')
->filters([
Tables\Filters\SelectFilter::make('product')
->relationship('product', 'title')
->label('Produkt'),
])
->actions([
Tables\Actions\EditAction::make(),
Tables\Actions\DeleteAction::make(),
]);
}
public static function getPages(): array
{
return [
'index' => Pages\ListBatches::route('/'),
'create' => Pages\CreateBatch::route('/create'),
'edit' => Pages\EditBatch::route('/{record}/edit'),
];
}
}
@@ -0,0 +1,11 @@
<?php
namespace App\Filament\User\Resources\BatchResource\Pages;
use App\Filament\User\Resources\BatchResource;
use Filament\Resources\Pages\CreateRecord;
class CreateBatch extends CreateRecord
{
protected static string $resource = BatchResource::class;
}
@@ -0,0 +1,17 @@
<?php
namespace App\Filament\User\Resources\BatchResource\Pages;
use App\Filament\User\Resources\BatchResource;
use Filament\Actions;
use Filament\Resources\Pages\EditRecord;
class EditBatch extends EditRecord
{
protected static string $resource = BatchResource::class;
protected function getHeaderActions(): array
{
return [Actions\DeleteAction::make()];
}
}
@@ -0,0 +1,17 @@
<?php
namespace App\Filament\User\Resources\BatchResource\Pages;
use App\Filament\User\Resources\BatchResource;
use Filament\Actions;
use Filament\Resources\Pages\ListRecords;
class ListBatches extends ListRecords
{
protected static string $resource = BatchResource::class;
protected function getHeaderActions(): array
{
return [Actions\CreateAction::make()];
}
}
@@ -0,0 +1,90 @@
<?php
namespace App\Filament\User\Resources;
use App\Filament\User\Resources\PackingResource\Pages;
use App\Filament\User\Resources\PackingResource\RelationManagers;
use App\Models\Packing;
use Filament\Forms;
use Filament\Forms\Form;
use Filament\Resources\Resource;
use Filament\Tables;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\SoftDeletingScope;
class PackingResource extends Resource
{
protected static ?string $model = Packing::class;
protected static ?string $navgationLabel = 'Verpackung & Größe';
protected static ?string $modelLabel = 'Verpackung & Größe';
protected static ?string $slug = 'verpackung';
protected static ?string $navigationIcon = 'heroicon-o-shopping-bag';
protected static ?string $navigationGroup = 'Ressourcen';
public static function form(Form $form): Form
{
return $form
->schema([
Forms\Components\TextInput::make('name')
->required()
->maxLength(255),
Forms\Components\TextInput::make('size')
->label('Größe')
->required()
->numeric(),
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
Tables\Columns\TextColumn::make('name')
->searchable(),
Tables\Columns\TextColumn::make('size')
->label('Größe')
->numeric()
->sortable(),
Tables\Columns\TextColumn::make('created_at')
->dateTime()
->sortable()
->toggleable(isToggledHiddenByDefault: true),
Tables\Columns\TextColumn::make('updated_at')
->dateTime()
->sortable()
->toggleable(isToggledHiddenByDefault: true),
])
->filters([
//
])
->actions([
Tables\Actions\EditAction::make(),
])
->bulkActions([
Tables\Actions\BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(),
]),
]);
}
public static function getRelations(): array
{
return [
//
];
}
public static function getPages(): array
{
return [
'index' => Pages\ListPackings::route('/'),
'create' => Pages\CreatePacking::route('/create'),
'edit' => Pages\EditPacking::route('/{record}/edit'),
];
}
public static function getPluralLabel(): string
{
return 'Verpackung & Größe'; // Setze hier deinen eigenen Pluralbegriff ein
}
}
@@ -0,0 +1,12 @@
<?php
namespace App\Filament\User\Resources\PackingResource\Pages;
use App\Filament\User\Resources\PackingResource;
use Filament\Actions;
use Filament\Resources\Pages\CreateRecord;
class CreatePacking extends CreateRecord
{
protected static string $resource = PackingResource::class;
}
@@ -0,0 +1,21 @@
<?php
namespace App\Filament\User\Resources\PackingResource\Pages;
use App\Filament\User\Resources\PackingResource;
use Filament\Actions;
use Filament\Resources\Pages\EditRecord;
class EditPacking extends EditRecord
{
protected static string $resource = PackingResource::class;
protected function getHeaderActions(): array
{
return [
Actions\DeleteAction::make(),
];
}
}
@@ -0,0 +1,19 @@
<?php
namespace App\Filament\User\Resources\PackingResource\Pages;
use App\Filament\User\Resources\PackingResource;
use Filament\Actions;
use Filament\Resources\Pages\ListRecords;
class ListPackings extends ListRecords
{
protected static string $resource = PackingResource::class;
protected function getHeaderActions(): array
{
return [
Actions\CreateAction::make(),
];
}
}
@@ -0,0 +1,118 @@
<?php
namespace App\Filament\User\Resources;
use App\Filament\User\Resources\ProductInventoryResource\Pages;
use Filament\Forms;
use Filament\Forms\Form;
use Filament\Resources\Resource;
use Filament\Tables;
use Filament\Tables\Table;
use App\Models\Product;
use App\Models\ProductSize;
use App\Models\ProductInventory;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\DB;
class ProductInventoryResource extends Resource
{
protected static ?string $model = ProductInventory::class;
protected static ?string $navigationIcon = 'heroicon-o-inbox';
protected static ?string $navigationLabel = 'Inventar';
protected static ?string $modelLabel = 'Inventar';
protected static ?string $slug = 'inventar';
protected static ?string $navigationGroup = 'Lager';
public static function form(Form $form): Form
{
return $form
->schema([
Forms\Components\Select::make('product_id')
->label('Produkt')
->native(false)
->searchable()
->options(Product::all()->pluck('title', 'id'))
->reactive()
->afterStateUpdated(fn(callable $set) => $set('size_id', null))
->required()
->disabled(fn($record) => !is_null($record)),
Forms\Components\Select::make('size_id')
->label('Größe')
->native(false)
->options(function (callable $get) {
$productId = $get('product_id');
if (!$productId) {
return [];
}
return ProductSize::where('product_id', $productId)
->get()
->mapWithKeys(function ($product) {
return [$product->id => "{$product->description} ({$product->price}€)"];
});
})
->required()
->disabled(fn($record) => !is_null($record))
->reactive(),
Forms\Components\TextInput::make('amount')
->label('Mange')
->numeric(false)
->step(1)
->rules(['min:1'])
->default(1)
->required()
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
Tables\Columns\TextColumn::make('product.title')->label('Produkt'),
Tables\Columns\TextColumn::make('size')->label('Größe')
->formatStateUsing(fn(ProductInventory $record) => "{$record->size->description} ({$record->size->price}€)"),
Tables\Columns\TextColumn::make('total')->label('Anzahl'),
])
->emptyStateHeading('Kein Inventar')
->recordUrl(null);
}
protected function getTableQuery(): Builder {
return ProductInventory::select(
DB::raw('MIN(id) as id'),
DB::raw('product_id'),
DB::raw('size_id'),
DB::raw('count(*) as total')
)
->where()
->groupBy('size_id');
}
public static function getEloquentQuery(): Builder {
return parent::getEloquentQuery()
->whereNull('out_at')
->select(DB::raw('MIN(id) AS id'), DB::raw('MIN(product_id) AS product_id'), 'size_id', DB::raw('COUNT(*) as total'))
->groupBy('size_id')
->orderBy('product_id');
}
public static function getPages(): array
{
return [
'index' => Pages\ListProductInventory::route('/'),
'create' => Pages\CreateProductInventory::route('/create'),
'edit' => Pages\EditProductInventory::route('/{record}/edit'),
];
}
public static function getPluralLabel(): string
{
return 'Inventar';
}
}
@@ -0,0 +1,69 @@
<?php
namespace App\Filament\User\Resources\ProductInventoryResource\Pages;
use App\Filament\User\Resources\ProductInventoryResource;
use Filament\Resources\Pages\CreateRecord;
use Illuminate\Database\Eloquent\Model;
use Filament\Notifications\Notification;
use App\Models\Product;
use App\Models\Spice;
use App\Models\ProductSize;
class CreateProductInventory extends CreateRecord
{
protected static string $resource = ProductInventoryResource::class;
protected function handleRecordCreation(array $data): Model
{
$amount = $data['amount'] ?? 1;
unset($data['amount']);
$product = Product::find($data['product_id']);
if (!$product) {
return false;
}
$size = ProductSize::find($data['size_id']);
if (!$size) {
return false;
}
$recipe = $product->recipe;
$packing = $size->packing;
$totalAmount = $packing->size;
foreach ($recipe->spices as $spice) {
$percentage = $spice->pivot->amount_in_percent;
$amountNeeded = (($percentage / 100) * $totalAmount) * $amount;
if (!$spice || $spice->quantity < $amountNeeded) {
Notification::make()
->warning()
->title('Fehler!')
->body('Nicht genügend '.htmlentities($spice->name).' für dieses Produkt in dieser Größe lagernd (bräuchte '.$amountNeeded.' '.htmlentities($spice->unit).')')
->send();
$this->halt();
return false;
}
}
$records = [];
for($i = 0; $i < $amount; $i++) {
$records[] = static::getModel()::create($data);
}
foreach ($recipe->spices as $spice) {
$percentage = $spice->pivot->amount_in_percent;
$amountNeeded = (($percentage / 100) * $totalAmount) * $amount;
Spice::findOrFail($spice->id)->decrement('quantity', $amountNeeded);
}
return $records[0];
}
protected function getRedirectUrl(): string
{
return $this->previousUrl ?? $this->getResource()::getUrl('index');
}
}
@@ -0,0 +1,13 @@
<?php
namespace App\Filament\User\Resources\ProductInventoryResource\Pages;
use App\Filament\User\Resources\ProductInventoryResource;
use Filament\Actions;
use Filament\Resources\Pages\EditRecord;
class EditProductInventory extends EditRecord
{
protected static string $resource = ProductInventoryResource::class;
}
@@ -0,0 +1,21 @@
<?php
namespace App\Filament\User\Resources\ProductInventoryResource\Pages;
use App\Filament\User\Resources\ProductInventoryResource;
use App\Models\ProductInventory;
use Filament\Actions;
use Filament\Resources\Pages\ListRecords;
use Illuminate\Database\Eloquent\Model;
class ListProductInventory extends ListRecords
{
protected static string $resource = ProductInventoryResource::class;
protected function getHeaderActions(): array
{
return [
Actions\CreateAction::make(),
];
}
}
@@ -0,0 +1,171 @@
<?php
namespace App\Filament\User\Resources;
use App\Filament\User\Resources\ProductResource\Pages;
use App\Filament\User\Resources\ProductResource\RelationManagers;
use App\Models\Product;
use Filament\Forms;
use Filament\Forms\Form;
use Filament\Forms\Components\FileUpload;
use Filament\Forms\Components\Repeater;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Resources\Resource;
use Filament\Tables;
use Filament\Tables\Table;
use App\Models\Packing;
class ProductResource extends Resource {
protected static ?string $model = Product::class;
protected static ?string $navigationIcon = 'heroicon-o-inbox';
protected static ?string $navigationLabel = 'Produkte';
protected static ?string $modelLabel = 'Produkt';
protected static ?string $slug = 'produkt';
protected static ?string $navigationGroup = 'Ressourcen';
public static function form(Form $form): Form {
return $form
->schema([
Forms\Components\TextInput::make('title')
->label('Name')
->required(),
FileUpload::make('images')
->label('Bilder')
->multiple()
->image()
->maxFiles(15)
->directory('product-images')
->columnSpanFull(),
Forms\Components\Textarea::make('description')
->label('Beschreibung'),
Forms\Components\Textarea::make('package_content')
->label('Inhaltsangabe'),
Forms\Components\Select::make('recipes_id')
->label('Rezept')
->native(false)
->searchable()
->relationship('recipe', 'name')
->required(),
Repeater::make('sizes')
->schema([
TextInput::make('gtin')
->label('GTIN'),
TextInput::make('description')
->label('Bezeichnung'),
Select::make('packing_id')
->label('Verpackung')
->options(function () {
return Packing::all()->mapWithKeys(function ($packing) {
return [$packing->id => $packing->name . ' (' . $packing->size . ')'];
});
})
->searchable()
->required(),
TextInput::make('price')
->label('Preis (in EUR)')
->numeric()
->required(),
])
->label('Größen & Preise')
->columns(2)
->addActionLabel('Option hinzufügen')
->afterStateHydrated(function ($state, $set, $record) {
if ($record) {
// Load the spices relationship with pivot data
$record->load('sizes');
// Map the spices and their pivot data to the format the repeater expects
$sizesData = $record->sizes->map(function ($size) {
return [
'packing_id' => $size->id,
'price' => $size->pivot->price,
'description' => $size->pivot->description,
];
})->toArray();
// Set the state for the repeater field to the mapped spices data
$set('sizes', $sizesData);
}
return $state;
}),
]);
}
public static function table(Table $table): Table {
return $table
->columns([
Tables\Columns\ImageColumn::make('images')
->label('Bild')
->limit(1),
Tables\Columns\TextColumn::make('title')
->label('Name')
->sortable()
->searchable(),
Tables\Columns\TextColumn::make('recipe.name')
->label('Rezept'),
]);
}
public static function getPages(): array {
return [
'index' => Pages\ListProducts::route('/'),
'create' => Pages\CreateProduct::route('/create'),
'edit' => Pages\EditProduct::route('/{record}/edit'),
];
}
public static function getPluralLabel(): string {
return 'Produkte';
}
protected function mutateFormDataBeforeCreate(array $data): array {
if (isset($data['images'])) {
$data['images'] = json_encode($data['images']);
}
return $data;
}
protected function mutateFormDataBeforeUpdate(array $data): array {
if (isset($data['images'])) {
$data['images'] = json_encode($data['images']);
}
return $data;
}
public static function getRecord($id) {
return Product::with('sizes')->findOrFail($id); // Eager load spices relationship
}
public static function getTableRecordTitle() {
return function ($record) {
$images = json_decode($record->images, true);
return implode(', ', $images);
};
}
public static function getActions(): array {
return [
Actions\DeleteAction::make(),
Action::make('deleteImage')
->action(function ($record, $data) {
$images = json_decode($record->images, true);
$index = array_search($data['image'], $images);
if ($index !== false) {
unset($images[$index]);
$record->update(['images' => array_values($images)]);
}
})
->form([
Forms\Components\Select::make('image')
->options(function ($record) {
return array_combine($record->images, $record->images);
})
->required(),
]),
];
}
}
@@ -0,0 +1,28 @@
<?php
namespace App\Filament\User\Resources\ProductResource\Pages;
use App\Filament\User\Resources\ProductResource;
use Filament\Resources\Pages\CreateRecord;
use Illuminate\Database\Eloquent\Model;
use App\Models\ProductSize;
class CreateProduct extends CreateRecord
{
protected static string $resource = ProductResource::class;
protected function handleRecordCreation(array $data): Model {
$record = static::getModel()::create($data);
foreach ($data['sizes'] as $size) {
ProductSize::create([
'product_id' => $record->id,
'packing_id' => $size['packing_id'],
'description' => $size['description'],
'price' => $size['price'],
]);
}
return $record;
}
}
@@ -0,0 +1,37 @@
<?php
namespace App\Filament\User\Resources\ProductResource\Pages;
use App\Filament\User\Resources\ProductResource;
use Filament\Actions;
use Filament\Resources\Pages\EditRecord;
class EditProduct extends EditRecord
{
protected static string $resource = ProductResource::class;
protected function getHeaderActions(): array
{
return [
Actions\DeleteAction::make(),
];
}
protected function afterSave(): void {
$this->syncSizes();
}
protected function syncSizes(): void {
$product = $this->record;
$sizes = collect($this->data['sizes'] ?? [])
->mapWithKeys(function ($size) {
return [
$size['packing_id'] => ['description' => $size['description'], 'price' =>$size['price']],
];
})
->toArray();
$product->sizes()->sync($sizes);
}
}
@@ -0,0 +1,19 @@
<?php
namespace App\Filament\User\Resources\ProductResource\Pages;
use App\Filament\User\Resources\ProductResource;
use Filament\Actions;
use Filament\Resources\Pages\ListRecords;
class ListProducts extends ListRecords
{
protected static string $resource = ProductResource::class;
protected function getHeaderActions(): array
{
return [
Actions\CreateAction::make(),
];
}
}
@@ -0,0 +1,121 @@
<?php
namespace App\Filament\User\Resources;
use App\Filament\User\Resources\ProductSaleResource\Pages;
use App\Models\ProductSale;
use App\Models\Product;
use App\Models\ProductSize;
use Filament\Forms;
use Filament\Forms\Form;
use Filament\Resources\Resource;
use Filament\Tables;
use Filament\Tables\Table;
use Filament\Forms\Components\TextInput;
use App\Rules\ProductsHaveEnoughStock;
use Filament\Forms\Components\DatePicker;
use Filament\Tables\Columns\TextColumn;
class ProductSaleResource extends Resource {
protected static ?string $model = ProductSale::class;
protected static ?string $navigationIcon = 'heroicon-o-arrow-up-on-square';
protected static ?string $navgationLabel = 'Warenausgang';
protected static ?string $modelLabel = 'Warenausgang';
protected static ?string $slug = 'warenausgang';
protected static ?string $navigationGroup = 'Waren Ein/Ausgang';
public $product_id;
public $size_id;
public $amount;
public static function form(Form $form): Form {
return $form
->schema([
Forms\Components\Select::make('product_id')
->label('Produkt')
->native(false)
->searchable()
->options(Product::all()->pluck('title', 'id'))
->reactive()
->afterStateUpdated(fn(callable $set) => $set('size_id', null))
->disabled(fn($record) => !is_null($record))
->required(),
Forms\Components\Select::make('size_id')
->label('Größe')
->native(false)
->options(function (callable $get) {
$productId = $get('product_id');
if (!$productId) {
return [];
}
return ProductSize::where('product_id', $productId)
->get()
->mapWithKeys(function ($product) {
return [$product->id => "{$product->description} ({$product->price}€)"];
});
})
->required()
->disabled(fn($record) => !is_null($record))
->reactive(),
TextInput::make('amount')
->label('Verkaufsmenge')
->numeric()
->step(1)
->required()
->disabled(fn($record) => !is_null($record))
->rules(['min:1']),
DatePicker::make('sold_at')
->label('Verkaufsdatum')
->default(now())
->disabled(fn($record) => !is_null($record))
->required(),
TextInput::make('comment')
->label('Kommentar')
->helperText('Verkaufsquelle (Bsp. Shopify, etc.)'),
]);
}
public static function table(Table $table): Table {
return $table
->columns([
Tables\Columns\TextColumn::make('product.title')->label('Produkt'),
Tables\Columns\TextColumn::make('size')->label('Größe')
->formatStateUsing(fn(ProductSale $record) => "{$record->size->description} ({$record->size->price}€)"),
TextColumn::make('amount')->label('Verkaufsmenge'),
TextColumn::make('sold_at')->label('Verkaufsdatum')->date('d.m.Y'),
TextColumn::make('comment')->label('Kommentar'),
])
->actions([
Tables\Actions\EditAction::make(),
])
->bulkActions([
Tables\Actions\DeleteBulkAction::make(),
])
->emptyStateHeading('Keine Warenausgänge')
->recordUrl(null);
}
public static function getRelations(): array {
return [];
}
public static function getPages(): array {
return [
'index' => Pages\ListProductSales::route('/'),
'create' => Pages\CreateProductSale::route('/create'),
'edit' => Pages\EditProductSale::route('/{record}/edit'),
];
}
public static function getPluralLabel(): string {
return 'Warenausgang';
}
}
@@ -0,0 +1,41 @@
<?php
namespace App\Filament\User\Resources\ProductSaleResource\Pages;
use App\Filament\User\Resources\ProductSaleResource;
use Filament\Resources\Pages\CreateRecord;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\DB;
use Filament\Notifications\Notification;
class CreateProductSale extends CreateRecord
{
protected static string $resource = ProductSaleResource::class;
protected function handleRecordCreation(array $data): Model
{
$count = DB::table('products_inventory')
->where('product_id', '=', $data['product_id'])
->where('size_id', '=', $data['size_id'])
->count();
if ($count < $data['amount']) {
Notification::make()
->warning()
->title('Fehler!')
->body('Nicht genügend Produkte in dieser Größe lagernd.')
->send();
$this->halt();
return false;
}
$record = static::getModel()::create($data);
DB::table('products_inventory')
->where('product_id', '=', $data['product_id'])
->where('size_id', '=', $data['size_id'])
->limit($data['amount'] ?? 1)
->update(['out_at' => now()]);
return $record;
}
}
@@ -0,0 +1,20 @@
<?php
namespace App\Filament\User\Resources\ProductSaleResource\Pages;
use App\Filament\User\Resources\ProductSaleResource;
use Filament\Resources\Pages\EditRecord;
class EditProductSale extends EditRecord
{
protected static string $resource = ProductSaleResource::class;
protected function getHeaderActions(): array
{
return [
\Filament\Actions\DeleteAction::make(),
];
}
}
@@ -0,0 +1,19 @@
<?php
namespace App\Filament\User\Resources\ProductSaleResource\Pages;
use App\Filament\User\Resources\ProductSaleResource;
use Filament\Actions;
use Filament\Resources\Pages\ListRecords;
class ListProductSales extends ListRecords
{
protected static string $resource = ProductSaleResource::class;
protected function getHeaderActions(): array
{
return [
Actions\CreateAction::make(),
];
}
}
@@ -0,0 +1,55 @@
<?php
namespace App\Filament\User\Resources\ProductSaleResource\Widgets;
use Leandrocfe\FilamentApexCharts\Widgets\ApexChartWidget;
use Flowframe\Trend\Trend;
use Flowframe\Trend\TrendValue;
use App\Models\ProductSale;
class SalesChart extends ApexChartWidget {
protected static ?string $chartId = 'productSalesChart';
protected static ?string $heading = 'Verkäufe dieses Quartal';
protected function getOptions(): array {
$data = Trend::model(ProductSale::class)
->between(
start: now()->startOfQuarter(),
end: now()->endOfQuarter(),
)
->perWeek()
->count();
return [
'chart' => [
'type' => 'line',
'height' => 300,
],
'series' => [
[
'name' => 'Anzahl Verkäufe',
'data' => $data->map(fn(TrendValue $value) => $value->aggregate),
],
],
'xaxis' => [
'categories' => $data->map(fn(TrendValue $value) => $value->date),
'labels' => [
'style' => [
'fontFamily' => 'inherit',
],
],
],
'yaxis' => [
'labels' => [
'style' => [
'fontFamily' => 'inherit',
],
],
],
'colors' => ['#f59e0b'],
'stroke' => [
'curve' => 'smooth',
],
];
}
}
@@ -0,0 +1,124 @@
<?php
namespace App\Filament\User\Resources;
use App\Filament\User\Resources\RecipeResource\Pages;
use App\Models\Recipe;
use App\Models\RecipeSpice;
use App\Models\Spice;
use Filament\Forms;
use Filament\Forms\Form;
use Filament\Resources\Resource;
use Filament\Tables;
use Filament\Tables\Table;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\Repeater;
use Filament\Tables\Columns\TextColumn;
use App\Rules\SumEquals100;
class RecipeResource extends Resource
{
protected static ?string $model = Recipe::class;
protected static ?string $navigationIcon = 'heroicon-o-book-open';
protected static ?string $navgationLabel = 'Rezepte';
protected static ?string $modelLabel = 'Rezepte';
protected static ?string $slug = 'rezept';
protected static ?string $navigationGroup = 'Ressourcen';
public static function form(Form $form): Form
{
return $form
->schema([
TextInput::make('name')
->required()
->label('Name'),
Textarea::make('description')
->label('Beschreibung'),
Repeater::make('spices')
->schema([
Select::make('spice_id')
->label('Rohstoff')
->options(Spice::all()->pluck('name', 'id'))
->searchable()
->required(),
TextInput::make('amount_in_percent')
->label('Anteil (in Prozent)')
->numeric()
->required()
->rules(['min:1', 'max:100']),
])
->label('Inhalt')
->columns(2)
->addActionLabel('Inhaltsstoff hinzufügen')
->afterStateHydrated(function ($state, $set, $record) {
if ($record) {
// Load the spices relationship with pivot data
$record->load('spices');
// Map the spices and their pivot data to the format the repeater expects
$spicesData = $record->spices->map(function ($spice) {
return [
'spice_id' => $spice->id,
'amount_in_percent' => $spice->pivot->amount_in_percent,
];
})->toArray();
// Set the state for the repeater field to the mapped spices data
$set('spices', $spicesData);
}
return $state;
})
->rules([new SumEquals100()]),
]);
}
/*
*/
public static function table(Table $table): Table
{
return $table
->columns([
TextColumn::make('name')
->label('Name')
->sortable()
->searchable(),
TextColumn::make('description')
->label('Beschreibung')
->limit(50),
TextColumn::make('spices.name')
->label('Inhalt')
->formatStateUsing(function ($state, $record) {
$spices = $record->spices->take(3)->map(function ($spice) {
return $spice->name . ' (' . round($spice->pivot->amount_in_percent) . '%)';
});
if ($record->spices->count() > 3) {
$spices->push('... weitere');
}
return $spices->implode(', ');
})
->limit(50),
]);
}
public static function getRecord($id) {
return Recipe::with('spices')->findOrFail($id); // Eager load spices relationship
}
public static function getPages(): array
{
return [
'index' => Pages\ListRecipes::route('/'),
'create' => Pages\CreateRecipe::route('/create'),
'edit' => Pages\EditRecipe::route('/{record}/edit'),
];
}
public static function getPluralLabel(): string
{
return 'Rezepte';
}
}
@@ -0,0 +1,28 @@
<?php
namespace App\Filament\User\Resources\RecipeResource\Pages;
use App\Filament\User\Resources\RecipeResource;
use Filament\Resources\Pages\CreateRecord;
use Illuminate\Database\Eloquent\Model;
use App\Models\RecipeSpice;
class CreateRecipe extends CreateRecord
{
protected static string $resource = RecipeResource::class;
protected function handleRecordCreation(array $data): Model
{
$record = static::getModel()::create($data);
foreach ($data['spices'] as $spice) {
RecipeSpice::create([
'recipe_id' => $record->id,
'spice_id' => $spice['spice_id'],
'amount_in_percent' => $spice['amount_in_percent'],
]);
}
return $record;
}
}
@@ -0,0 +1,31 @@
<?php
namespace App\Filament\User\Resources\RecipeResource\Pages;
use App\Filament\User\Resources\RecipeResource;
use Filament\Resources\Pages\EditRecord;
class EditRecipe extends EditRecord
{
protected static string $resource = RecipeResource::class;
protected function afterSave(): void
{
$this->syncSpices();
}
protected function syncSpices(): void
{
$recipe = $this->record;
$spices = collect($this->data['spices'] ?? [])
->mapWithKeys(function ($spice) {
return [
$spice['spice_id'] => ['amount_in_percent' => $spice['amount_in_percent']],
];
})
->toArray();
$recipe->spices()->sync($spices);
}
}
@@ -0,0 +1,19 @@
<?php
namespace App\Filament\User\Resources\RecipeResource\Pages;
use App\Filament\User\Resources\RecipeResource;
use Filament\Actions;
use Filament\Resources\Pages\ListRecords;
class ListRecipes extends ListRecords
{
protected static string $resource = RecipeResource::class;
protected function getHeaderActions(): array
{
return [
Actions\CreateAction::make(),
];
}
}
@@ -0,0 +1,97 @@
<?php
namespace App\Filament\User\Resources;
use App\Filament\User\Resources\SpiceResource\Pages;
use App\Filament\User\Resources\SpiceResource\RelationManagers;
use App\Models\Spice;
use Filament\Forms;
use Filament\Forms\Form;
use Filament\Resources\Resource;
use Filament\Tables;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\SoftDeletingScope;
class SpiceResource extends Resource
{
protected static ?string $model = Spice::class;
protected static ?string $navgationLabel = 'Rohstoffe';
protected static ?string $modelLabel = 'Rohstoff';
protected static ?string $slug = 'rohstoffe';
protected static ?string $navigationGroup = 'Lager';
protected static ?string $navigationIcon = 'heroicon-o-cube';
public static function form(Form $form): Form
{
return $form
->schema([
Forms\Components\TextInput::make('name')
->required()
->maxLength(255),
Forms\Components\Select::make('unit')
->label('Mengeneinheit')
->native(false)
->searchable()
->options([
'grams' => '(Kilo-)Gramm',
'pieces' => 'Stück',
'liter' => 'Liter',
])
->required(),
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
Tables\Columns\TextColumn::make('name')
->searchable(),
Tables\Columns\TextColumn::make('quantity')->label('Menge lagernd')
->formatStateUsing(function (Spice $record) {
if ($record->unit == 'grams') {
return ($record->quantity >= 1000) ? round($record->quantity / 1000, 2) . 'kg' : $record->quantity . 'g';
}elseif($record->unit == 'pieces'){
return $record->quantity . ' Stück';
}elseif($record->unit == 'liter'){
return $record->quantity . ' Liter';
}else{
return $record->quantity . ' ' . ucfirst($record->unit);
}
}),
])
->filters([
//
])
->actions([
Tables\Actions\EditAction::make(),
])
->bulkActions([
Tables\Actions\BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(),
]),
]);
}
public static function getRelations(): array
{
return [
//
];
}
public static function getPages(): array
{
return [
'index' => Pages\ListSpices::route('/'),
'create' => Pages\CreateSpice::route('/create'),
'edit' => Pages\EditSpice::route('/{record}/edit'),
];
}
public static function getPluralLabel(): string
{
return 'Rohstoffe'; // Setze hier deinen eigenen Pluralbegriff ein
}
}
@@ -0,0 +1,12 @@
<?php
namespace App\Filament\User\Resources\SpiceResource\Pages;
use App\Filament\User\Resources\SpiceResource;
use Filament\Actions;
use Filament\Resources\Pages\CreateRecord;
class CreateSpice extends CreateRecord
{
protected static string $resource = SpiceResource::class;
}
@@ -0,0 +1,19 @@
<?php
namespace App\Filament\User\Resources\SpiceResource\Pages;
use App\Filament\User\Resources\SpiceResource;
use Filament\Actions;
use Filament\Resources\Pages\EditRecord;
class EditSpice extends EditRecord
{
protected static string $resource = SpiceResource::class;
protected function getHeaderActions(): array
{
return [
Actions\DeleteAction::make(),
];
}
}
@@ -0,0 +1,19 @@
<?php
namespace App\Filament\User\Resources\SpiceResource\Pages;
use App\Filament\User\Resources\SpiceResource;
use Filament\Actions;
use Filament\Resources\Pages\ListRecords;
class ListSpices extends ListRecords
{
protected static string $resource = SpiceResource::class;
protected function getHeaderActions(): array
{
return [
Actions\CreateAction::make(),
];
}
}
@@ -0,0 +1,94 @@
<?php
namespace App\Filament\User\Resources\SpiceResource\Widgets;
use Leandrocfe\FilamentApexCharts\Widgets\ApexChartWidget;
use Filament\Support\RawJs;
use App\Models\Spice;
class SpiceChart extends ApexChartWidget {
protected static ?string $chartId = 'spiceChart';
protected static ?string $heading = 'Rohstoffmengen im Lager';
protected function getOptions(): array {
$data = Spice::all();
$data_array = [];
$labels = [];
foreach ($data as $label) {
if ($label->unit == 'grams') {
$data_array[] = round($label->quantity / 1000, 2);
$labels[] = $label->name . ' (kg)';
} else {
$data_array[] = $label->quantity;
if ($label->unit == 'pieces') {
$labels[] = $label->name . ' (Stück)';
} elseif ($label->unit == 'liter') {
$labels[] = $label->name . ' (Liter)';
} else {
$labels[] = $label->name . ' (' . ucfirst($label->unit) . ')';
}
}
}
return [
'chart' => [
'type' => 'bar',
'height' => 300,
],
'series' => [
[
'name' => 'BasicBarChart',
'data' => $data_array,
],
],
'xaxis' => [
'categories' => $labels,
'labels' => [
'style' => [
'fontFamily' => 'inherit',
],
],
],
'yaxis' => [
'labels' => [
'style' => [
'fontFamily' => 'inherit',
],
],
],
'colors' => ['#f59e0b'],
'plotOptions' => [
'bar' => [
'borderRadius' => 3,
'horizontal' => false,
],
],
];
}
protected function extraJsOptions(): ?RawJs {
return RawJs::make(<<<'JS'
{
xaxis: {
labels: {
formatter: function (val, timestamp, opts) {
return val.replace('( ', '(')
}
}
},
dataLabels: {
enabled: true,
formatter: function (val, opt) {
return val+''+opt.w.globals.labels[opt.dataPointIndex].match(/\((.*?)\)/)[1]
},
dropShadow: {
enabled: true
},
}
}
JS);
}
}
@@ -0,0 +1,121 @@
<?php
namespace App\Filament\User\Resources;
use App\Filament\User\Resources\SpiceSupplierResource\Pages;
use App\Filament\User\Resources\SpiceSupplierResource\RelationManagers;
use App\Models\SpiceSupplier;
use Filament\Forms;
use Filament\Forms\Form;
use Filament\Resources\Resource;
use Filament\Tables;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\SoftDeletingScope;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\DatePicker;
use Filament\Tables\Columns\TextColumn;
class SpiceSupplierResource extends Resource
{
protected static ?string $model = SpiceSupplier::class;
protected static ?string $navigationIcon = 'heroicon-o-arrow-down-on-square';
protected static ?string $navgationLabel = 'Rohstofflieferungen';
protected static ?string $modelLabel = 'Rohstofflieferungen';
protected static ?string $slug = 'rohstofflieferung';
protected static ?string $navigationGroup = 'Waren Ein/Ausgang';
public static function form(Form $form): Form
{
return $form
->schema([
Select::make('spice_id')
->label('Rohstoff')
->relationship('spice', 'name')
->required()
->preload()
->searchable(),
Select::make('supplier_id')
->label('Lieferant')
->relationship('supplier', 'name')
->required()
->preload()
->searchable(),
TextInput::make('amount')
->label('Liefermenge')
->numeric()
->required()
->rules(['min:0.01'])
->disabled(fn($record) => !is_null($record)),
DatePicker::make('supplied_at')
->label('Lieferdatum')
->default(now())
->required(),
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
TextColumn::make('spice.name')->label('Rohstoff')->searchable(),
TextColumn::make('supplier.name')->label('Lieferant')->searchable(),
TextColumn::make('amount')->label('Liefermenge'),
TextColumn::make('supplied_at')->label('Lieferdatum')->date('d.m.Y'), // Lieferdatum in der Tabelle anzeigen
])
->actions([
Tables\Actions\EditAction::make(),
])
->bulkActions([
Tables\Actions\DeleteBulkAction::make(),
]);
}
protected static function getTableActions(): array
{
return [
DeleteAction::make()
->before(function ($record, DeleteAction $action) {
// Custom check logic
if (true) {
// Notify user about the restriction
Notification::make()
->title('Deletion Error')
->body("This record cannot be deleted because it has related records.")
->status('danger')
->send();
// Cancel the deletion
$action->cancel();
}
}),
];
}
public static function getRelations(): array
{
return [];
}
public static function getPages(): array
{
return [
'index' => Pages\ListSpiceSuppliers::route('/'),
'create' => Pages\CreateSpiceSupplier::route('/create'),
'edit' => Pages\EditSpiceSupplier::route('/{record}/edit'),
];
}
public static function getPluralLabel(): string
{
return 'Rohstofflieferungen';
}
}
@@ -0,0 +1,20 @@
<?php
namespace App\Filament\User\Resources\SpiceSupplierResource\Pages;
use App\Filament\User\Resources\SpiceSupplierResource;
use Filament\Resources\Pages\CreateRecord;
use Illuminate\Database\Eloquent\Model;
use App\Models\Spice;
class CreateSpiceSupplier extends CreateRecord
{
protected static string $resource = SpiceSupplierResource::class;
protected function handleRecordCreation(array $data): Model {
$record = static::getModel()::create($data);
Spice::findOrFail($data['spice_id'])->increment('quantity', $data['amount']);
return $record;
}
}
@@ -0,0 +1,33 @@
<?php
namespace App\Filament\User\Resources\SpiceSupplierResource\Pages;
use App\Filament\User\Resources\SpiceSupplierResource;
use Filament\Resources\Pages\EditRecord;
use Filament\Notifications\Notification;
use App\Models\Spice;
class EditSpiceSupplier extends EditRecord
{
protected static string $resource = SpiceSupplierResource::class;
protected function getHeaderActions(): array
{
return [
\Filament\Actions\DeleteAction::make()->before(function ($record) {
if (!$record->spice || $record->spice->quantity < $record->amount) {
Notification::make()
->warning()
->title('Fehler!')
->body('Nicht mehr genügend ' . htmlentities($record->spice->name) . ' im Lager (bräuchte mindestens Liefermenge um Lieferung rückgängig zu machen)')
->send();
$this->halt();
return false;
}
Spice::findOrFail($record->spice->id)->decrement('quantity', $record->amount);
}),
];
}
}
@@ -0,0 +1,19 @@
<?php
namespace App\Filament\User\Resources\SpiceSupplierResource\Pages;
use App\Filament\User\Resources\SpiceSupplierResource;
use Filament\Actions;
use Filament\Resources\Pages\ListRecords;
class ListSpiceSuppliers extends ListRecords
{
protected static string $resource = SpiceSupplierResource::class;
protected function getHeaderActions(): array
{
return [
Actions\CreateAction::make(),
];
}
}
@@ -0,0 +1,83 @@
<?php
namespace App\Filament\User\Resources;
use App\Filament\User\Resources\SupplierResource\Pages;
use App\Filament\User\Resources\SupplierResource\RelationManagers;
use App\Models\Supplier;
use Filament\Forms;
use Filament\Forms\Form;
use Filament\Resources\Resource;
use Filament\Tables;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\SoftDeletingScope;
class SupplierResource extends Resource
{
protected static ?string $model = Supplier::class;
protected static ?string $navigationIcon = 'heroicon-o-user';
protected static ?string $navigationLabel = 'Lieferanten';
protected static ?string $modelLabel = 'Lieferant';
protected static ?string $slug = 'lieferant';
protected static ?string $navigationGroup = 'CRM';
public static function form(Form $form): Form
{
return $form
->schema([
Forms\Components\TextInput::make('name')
->required()
->maxLength(255),
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
Tables\Columns\TextColumn::make('name')
->searchable(),
Tables\Columns\TextColumn::make('created_at')
->dateTime()
->sortable()
->toggleable(isToggledHiddenByDefault: true),
Tables\Columns\TextColumn::make('updated_at')
->dateTime()
->sortable()
->toggleable(isToggledHiddenByDefault: true),
])
->filters([
//
])
->actions([
Tables\Actions\EditAction::make(),
])
->bulkActions([
Tables\Actions\BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(),
]),
]);
}
public static function getRelations(): array
{
return [
//
];
}
public static function getPages(): array
{
return [
'index' => Pages\ListSuppliers::route('/'),
'create' => Pages\CreateSupplier::route('/create'),
'edit' => Pages\EditSupplier::route('/{record}/edit'),
];
}
public static function getPluralLabel(): string
{
return 'Lieferanten';
}
}
@@ -0,0 +1,12 @@
<?php
namespace App\Filament\User\Resources\SupplierResource\Pages;
use App\Filament\User\Resources\SupplierResource;
use Filament\Actions;
use Filament\Resources\Pages\CreateRecord;
class CreateSupplier extends CreateRecord
{
protected static string $resource = SupplierResource::class;
}
@@ -0,0 +1,19 @@
<?php
namespace App\Filament\User\Resources\SupplierResource\Pages;
use App\Filament\User\Resources\SupplierResource;
use Filament\Actions;
use Filament\Resources\Pages\EditRecord;
class EditSupplier extends EditRecord
{
protected static string $resource = SupplierResource::class;
protected function getHeaderActions(): array
{
return [
Actions\DeleteAction::make(),
];
}
}
@@ -0,0 +1,19 @@
<?php
namespace App\Filament\User\Resources\SupplierResource\Pages;
use App\Filament\User\Resources\SupplierResource;
use Filament\Actions;
use Filament\Resources\Pages\ListRecords;
class ListSuppliers extends ListRecords
{
protected static string $resource = SupplierResource::class;
protected function getHeaderActions(): array
{
return [
Actions\CreateAction::make(),
];
}
}
+19
View File
@@ -0,0 +1,19 @@
<?php
namespace App\Filament\Widgets;
use App\Models\Product;
use App\Models\ProductInventory;
use App\Models\Spice;
use Filament\Widgets\StatsOverviewWidget as BaseWidget;
use Filament\Widgets\StatsOverviewWidget\Stat;
class StatsOverview extends BaseWidget {
protected function getStats(): array {
return [
Stat::make('Produkte', Product::count())->description('angelegt'),
Stat::make('Inventar', ProductInventory::count())->description('Produkte'),
Stat::make('Rohstoffe', Spice::count())->description('angelegt'),
];
}
}
@@ -0,0 +1,27 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Resources\LotResource;
use App\Models\Batch;
use Illuminate\Http\JsonResponse;
class LotController extends Controller
{
public function show(string $lotNumber): LotResource|JsonResponse
{
$batch = Batch::with([
'product',
'recipe.spices',
'origins.spice',
'origins.supplier',
])->where('lot_number', $lotNumber)->first();
if (! $batch) {
return response()->json(['error' => 'LOT-Nummer nicht gefunden.'], 404);
}
return new LotResource($batch);
}
}
+8
View File
@@ -0,0 +1,8 @@
<?php
namespace App\Http\Controllers;
abstract class Controller
{
//
}
+39
View File
@@ -0,0 +1,39 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class LotResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'lot_number' => $this->lot_number,
'produced_at' => $this->produced_at?->toDateString(),
'expires_at' => $this->expires_at?->toDateString(),
'product' => [
'name' => $this->product->title,
'description' => $this->product->description,
],
'recipe' => [
'name' => $this->recipe->name,
],
'spices' => $this->recipe->spices->map(function ($spice) {
$origin = $this->origins->firstWhere('spice_id', $spice->id);
return [
'name' => $spice->name,
'percentage' => (float) $spice->pivot->amount_in_percent,
'origin' => $origin ? [
'country' => $origin->origin_country,
'farmer' => $origin->farmer,
'supplier' => $origin->supplier?->name,
'notes' => $origin->notes,
] : null,
];
})->values(),
];
}
}
+30
View File
@@ -0,0 +1,30 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Batch extends Model
{
protected $fillable = ['lot_number', 'product_id', 'recipe_id', 'produced_at', 'expires_at'];
protected $casts = [
'produced_at' => 'date',
'expires_at' => 'date',
];
public function product()
{
return $this->belongsTo(Product::class);
}
public function recipe()
{
return $this->belongsTo(Recipe::class);
}
public function origins()
{
return $this->hasMany(BatchOrigin::class);
}
}
+25
View File
@@ -0,0 +1,25 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class BatchOrigin extends Model
{
protected $fillable = ['batch_id', 'spice_id', 'supplier_id', 'origin_country', 'farmer', 'notes'];
public function batch()
{
return $this->belongsTo(Batch::class);
}
public function spice()
{
return $this->belongsTo(Spice::class);
}
public function supplier()
{
return $this->belongsTo(Supplier::class);
}
}
+20
View File
@@ -0,0 +1,20 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Packing extends Model
{
use HasFactory;
protected $fillable = ['name', 'size'];
public function products() {
return $this->belongsToMany(Product::class, 'products_sizes')
->withPivot('price')
->withPivot('description')
->withTimestamps();
}
}
+39
View File
@@ -0,0 +1,39 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Product extends Model
{
protected $fillable = ['title', 'description', 'images', 'package_content', 'recipes_id'];
protected $casts = [
'images' => 'array',
];
public function recipe()
{
return $this->belongsTo(Recipe::class, 'recipes_id');
}
public function sizes()
{
return $this->belongsToMany(Packing::class, 'products_sizes')
->withPivot('price')
->withPivot('description')
->withTimestamps();
}
public function inventories()
{
return $this->hasMany(ProductInventory::class);
}
public function batches()
{
return $this->hasMany(Batch::class);
}
}
+22
View File
@@ -0,0 +1,22 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class ProductInventory extends Model
{
protected $fillable = ['product_id', 'size_id', 'out_at'];
protected $table = 'products_inventory';
public function product()
{
return $this->belongsTo(product::class);
}
public function size()
{
return $this->belongsTo(ProductSize::class);
}
}
+28
View File
@@ -0,0 +1,28 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class ProductSale extends Model
{
protected $table = 'products_sales';
protected $fillable = [
'product_id',
'size_id',
'amount',
'sold_at',
];
// Beziehungen
public function product()
{
return $this->belongsTo(Product::class);
}
public function size()
{
return $this->belongsTo(ProductSize::class);
}
}
+25
View File
@@ -0,0 +1,25 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class ProductSize extends Model
{
use HasFactory;
protected $table = 'products_sizes';
protected $fillable = ['product_id', 'packing_id', 'sku', 'price', 'description'];
public function product() {
return $this->belongsTo(Product::class);
}
public function packing() {
return $this->belongsTo(Packing::class);
}
public function inventories() {
return $this->hasMany(ProductInventory::class);
}
}
+22
View File
@@ -0,0 +1,22 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Recipe extends Model
{
use HasFactory;
protected $fillable = ['name', 'description'];
public function spices()
{
return $this->belongsToMany(Spice::class, 'recipes_spices')
->withPivot('amount_in_percent')
->withTimestamps();
}
}
+13
View File
@@ -0,0 +1,13 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class RecipeSpice extends Model
{
use HasFactory;
protected $table = 'recipes_spices';
protected $fillable = ['recipe_id', 'spice_id', 'amount_in_percent'];
}
+17
View File
@@ -0,0 +1,17 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Spice extends Model
{
protected $fillable = ['name', 'quantity', 'unit'];
public function recipes()
{
return $this->belongsToMany(Recipe::class, 'recipes_spices')
->withPivot('amount_in_percent')
->withTimestamps();
}
}
+31
View File
@@ -0,0 +1,31 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class SpiceSupplier extends Model
{
use HasFactory;
protected $table = 'spices_suppliers'; // explizit, weil der Standard sonst 'spice_suppliers' wäre
protected $fillable = [
'spice_id',
'supplier_id',
'amount',
'supplied_at',
];
// Beziehungen
public function spice()
{
return $this->belongsTo(Spice::class);
}
public function supplier()
{
return $this->belongsTo(Supplier::class);
}
}
+17
View File
@@ -0,0 +1,17 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Supplier extends Model
{
use HasFactory;
protected $fillable = ['name'];
public function spiceSuppliers()
{
return $this->hasMany(SpiceSupplier::class);
}
}
+53
View File
@@ -0,0 +1,53 @@
<?php
namespace App\Models;
use Filament\Models\Contracts\FilamentUser;
use Filament\Panel;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
class User extends Authenticatable implements FilamentUser
{
use HasFactory, Notifiable;
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
'name',
'email',
'password',
];
/**
* The attributes that should be hidden for serialization.
*
* @var array<int, string>
*/
protected $hidden = [
'password',
'remember_token',
];
/**
* Get the attributes that should be cast.
*
* @return array<string, string>
*/
protected function casts(): array
{
return [
'email_verified_at' => 'datetime',
'password' => 'hashed',
];
}
public function canAccessPanel(Panel $panel): bool
{
return true; // allow all authenticated users access to the dashboard
}
}
+48
View File
@@ -0,0 +1,48 @@
<?php
namespace App\Observers;
use App\Models\Product;
class ProductObserver
{
/**
* Handle the Product "created" event.
*/
public function created(Product $product): void
{
}
/**
* Handle the Product "updated" event.
*/
public function updated(Product $product): void
{
//
}
/**
* Handle the Product "deleted" event.
*/
public function deleted(Product $product): void
{
//
}
/**
* Handle the Product "restored" event.
*/
public function restored(Product $product): void
{
//
}
/**
* Handle the Product "force deleted" event.
*/
public function forceDeleted(Product $product): void
{
//
}
}
+12
View File
@@ -0,0 +1,12 @@
<?php
namespace App\Policies;
use App\Models\User;
use App\Models\ProductSale;
class ProductSalePolicy {
public function delete(User $user, ProductSale $productsale) {
return false;
}
}
+10
View File
@@ -0,0 +1,10 @@
<?php
namespace App\Policies;
use App\Models\User;
use App\Models\SpiceSupplier;
class SpiceSupplierPolicy {
}
+26
View File
@@ -0,0 +1,26 @@
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use App\Models\Product; // Richtiges Product-Model importieren
use App\Observers\ProductObserver; // ProductObserver importieren
class AppServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*/
public function register(): void
{
//
}
/**
* Bootstrap any application services.
*/
public function boot(): void
{
Product::observe(ProductObserver::class);
}
}
@@ -0,0 +1,72 @@
<?php
namespace App\Providers\Filament;
use Filament\Http\Middleware\Authenticate;
use Filament\Http\Middleware\DisableBladeIconComponents;
use Filament\Http\Middleware\DispatchServingFilamentEvent;
use Filament\Pages;
use Filament\Panel;
use Filament\PanelProvider;
use Filament\Support\Colors\Color;
use Filament\Widgets;
use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse;
use Illuminate\Cookie\Middleware\EncryptCookies;
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken;
use Illuminate\Routing\Middleware\SubstituteBindings;
use Illuminate\Session\Middleware\AuthenticateSession;
use Illuminate\Session\Middleware\StartSession;
use Illuminate\View\Middleware\ShareErrorsFromSession;
use App\Filament\Widgets\StatsOverview;
use App\Filament\User\Resources\ProductSaleResource\Widgets\SalesChart;
use App\Filament\User\Resources\SpiceResource\Widgets\SpiceChart;
use Leandrocfe\FilamentApexCharts\FilamentApexChartsPlugin;
class UserPanelProvider extends PanelProvider
{
public function panel(Panel $panel): Panel
{
return $panel
->id('user')
->default()
->path('dashboard')
->font('Jost')
->login()
->colors([
'primary' => '#33ccff'
])
->favicon(url:'../images/favicon.png')
->discoverResources(in: app_path('Filament/User/Resources'), for: 'App\\Filament\\User\\Resources')
->discoverPages(in: app_path('Filament/User/Pages'), for: 'App\\Filament\\User\\Pages')
->pages([
Pages\Dashboard::class,
])
->plugins([
FilamentApexChartsPlugin::make()
])
->discoverWidgets(in: app_path('Filament/User/Widgets'), for: 'App\\Filament\\User\\Widgets')
->widgets([
StatsOverview::class,
SalesChart::class,
SpiceChart::class,
])
->middleware([
EncryptCookies::class,
AddQueuedCookiesToResponse::class,
StartSession::class,
AuthenticateSession::class,
ShareErrorsFromSession::class,
VerifyCsrfToken::class,
SubstituteBindings::class,
DisableBladeIconComponents::class,
DispatchServingFilamentEvent::class,
])
->authMiddleware([
Authenticate::class,
])
->resources([
config('filament-logger.activity_resource')
]);
}
}
+15
View File
@@ -0,0 +1,15 @@
<?php
namespace App\Rules;
use Illuminate\Contracts\Validation\Rule;
class SumEquals100 implements Rule {
public function passes($attribute, $value) {
$sum = collect($value)->sum('amount_in_percent');
return $sum == 100;
}
public function message() {
return 'Die Summe aller Zutaten muss exakt 100% entsprechen.';
}
}