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,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(),
];
}
}