New Initial Commit
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources;
|
||||
|
||||
use App\Filament\Resources\CategoryResource\Pages;
|
||||
use App\Models\Category;
|
||||
use Filament\Forms;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Actions;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class CategoryResource extends Resource
|
||||
{
|
||||
protected static ?string $model = Category::class;
|
||||
|
||||
protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-tag';
|
||||
|
||||
public static function form(Schema $schema): Schema
|
||||
{
|
||||
return $schema
|
||||
->schema([
|
||||
Forms\Components\TextInput::make('name')
|
||||
->required()
|
||||
->maxLength(255),
|
||||
Forms\Components\Textarea::make('description')
|
||||
->maxLength(65535)
|
||||
->columnSpanFull(),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
Tables\Columns\TextColumn::make('name')
|
||||
->searchable(),
|
||||
Tables\Columns\TextColumn::make('products_count')
|
||||
->counts('products')
|
||||
->label('Products'),
|
||||
Tables\Columns\TextColumn::make('created_at')
|
||||
->dateTime()
|
||||
->sortable()
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
])
|
||||
->filters([
|
||||
//
|
||||
])
|
||||
->actions([
|
||||
//
|
||||
])
|
||||
->bulkActions([
|
||||
//
|
||||
]);
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => Pages\ManageCategories::route('/'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\CategoryResource\Pages;
|
||||
|
||||
use App\Filament\Resources\CategoryResource;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\ManageRecords;
|
||||
|
||||
class ManageCategories extends ManageRecords
|
||||
{
|
||||
protected static string $resource = CategoryResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\CreateAction::make(),
|
||||
];
|
||||
}
|
||||
|
||||
protected function getTableActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\EditAction::make(),
|
||||
Actions\DeleteAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources;
|
||||
|
||||
use App\Models\HeroImage;
|
||||
use App\Filament\Resources\HeroImageResource\Pages;
|
||||
use Filament\Forms;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Schemas\Components\Section;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class HeroImageResource extends Resource
|
||||
{
|
||||
protected static ?string $model = HeroImage::class;
|
||||
|
||||
protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-photo';
|
||||
|
||||
protected static ?string $navigationLabel = 'Hero Images';
|
||||
|
||||
protected static ?string $modelLabel = 'Hero Image';
|
||||
|
||||
protected static ?string $pluralModelLabel = 'Hero Images';
|
||||
|
||||
public static function form(Schema $schema): Schema
|
||||
{
|
||||
return $schema
|
||||
->schema([
|
||||
Forms\Components\Select::make('page')
|
||||
->label('Page')
|
||||
->options(HeroImage::getPages())
|
||||
->required()
|
||||
->helperText('Choose which page this hero image will appear on'),
|
||||
|
||||
Forms\Components\FileUpload::make('image_path')
|
||||
->label('Hero Image')
|
||||
->image()
|
||||
->required()
|
||||
->disk('public')
|
||||
->directory('hero-images')
|
||||
->maxSize(5120)
|
||||
->acceptedFileTypes(['image/jpeg', 'image/png', 'image/webp', 'image/gif'])
|
||||
->helperText('Upload a high-quality image (max 5MB). Recommended size: 1920x1080px'),
|
||||
|
||||
Forms\Components\TextInput::make('title')
|
||||
->label('Heading')
|
||||
->required()
|
||||
->maxLength(255)
|
||||
->helperText('Main heading text displayed on the hero image'),
|
||||
|
||||
Forms\Components\Textarea::make('description')
|
||||
->label('Description')
|
||||
->maxLength(500)
|
||||
->rows(3)
|
||||
->helperText('Subheading text displayed below the title'),
|
||||
|
||||
Forms\Components\TextInput::make('button_text')
|
||||
->label('Button Text')
|
||||
->maxLength(100)
|
||||
->helperText('Text for the call-to-action button (optional)'),
|
||||
|
||||
Forms\Components\TextInput::make('button_link')
|
||||
->label('Button Link')
|
||||
->url()
|
||||
->maxLength(255)
|
||||
->helperText('URL or page anchor (e.g., #products or /products)'),
|
||||
|
||||
Forms\Components\TextInput::make('sort_order')
|
||||
->label('Sort Order')
|
||||
->numeric()
|
||||
->default(0)
|
||||
->helperText('Lower numbers appear first in the slider'),
|
||||
|
||||
Forms\Components\Toggle::make('is_active')
|
||||
->label('Active')
|
||||
->default(true)
|
||||
->helperText('Disable to hide this image from the website'),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
Tables\Columns\TextColumn::make('page')
|
||||
->label('Page')
|
||||
->sortable()
|
||||
->formatStateUsing(fn($state) => HeroImage::getPages()[$state] ?? $state)
|
||||
->badge(),
|
||||
|
||||
Tables\Columns\ImageColumn::make('image_path')
|
||||
->label('Image')
|
||||
->size(80),
|
||||
|
||||
Tables\Columns\TextColumn::make('title')
|
||||
->label('Title')
|
||||
->searchable()
|
||||
->limit(50),
|
||||
|
||||
Tables\Columns\TextColumn::make('sort_order')
|
||||
->label('Order')
|
||||
->sortable()
|
||||
->numeric(),
|
||||
|
||||
Tables\Columns\IconColumn::make('is_active')
|
||||
->label('Active')
|
||||
->boolean()
|
||||
->sortable(),
|
||||
|
||||
Tables\Columns\TextColumn::make('created_at')
|
||||
->label('Created')
|
||||
->dateTime('M d, Y')
|
||||
->sortable()
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
])
|
||||
->filters([
|
||||
Tables\Filters\SelectFilter::make('page')
|
||||
->options(HeroImage::getPages()),
|
||||
|
||||
Tables\Filters\TernaryFilter::make('is_active')
|
||||
->label('Active')
|
||||
->native(false),
|
||||
])
|
||||
->actions([
|
||||
//
|
||||
])
|
||||
->bulkActions([
|
||||
//
|
||||
])
|
||||
->defaultSort('page', 'asc')
|
||||
->defaultSort('sort_order', 'asc');
|
||||
}
|
||||
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [
|
||||
//
|
||||
];
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => Pages\ListHeroImages::route('/'),
|
||||
'create' => Pages\CreateHeroImage::route('/create'),
|
||||
'edit' => Pages\EditHeroImage::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\HeroImageResource\Pages;
|
||||
|
||||
use App\Filament\Resources\HeroImageResource;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
|
||||
class CreateHeroImage extends CreateRecord
|
||||
{
|
||||
protected static string $resource = HeroImageResource::class;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\HeroImageResource\Pages;
|
||||
|
||||
use App\Filament\Resources\HeroImageResource;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
|
||||
class EditHeroImage extends EditRecord
|
||||
{
|
||||
protected static string $resource = HeroImageResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\DeleteAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\HeroImageResource\Pages;
|
||||
|
||||
use App\Filament\Resources\HeroImageResource;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
|
||||
class ListHeroImages extends ListRecords
|
||||
{
|
||||
protected static string $resource = HeroImageResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\CreateAction::make()
|
||||
->label('Add New Hero Image'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources;
|
||||
|
||||
use App\Filament\Resources\OrderResource\Pages;
|
||||
use App\Models\Order;
|
||||
use Filament\Forms;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Actions;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class OrderResource extends Resource
|
||||
{
|
||||
protected static ?string $model = Order::class;
|
||||
|
||||
protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-shopping-bag';
|
||||
|
||||
public static function form(Schema $schema): Schema
|
||||
{
|
||||
return $schema
|
||||
->schema([
|
||||
Forms\Components\TextInput::make('order_number')
|
||||
->required()
|
||||
->disabled()
|
||||
->maxLength(255),
|
||||
Forms\Components\TextInput::make('customer_name')
|
||||
->required()
|
||||
->maxLength(255),
|
||||
Forms\Components\TextInput::make('customer_email')
|
||||
->email()
|
||||
->required()
|
||||
->maxLength(255),
|
||||
Forms\Components\TextInput::make('customer_phone')
|
||||
->tel()
|
||||
->maxLength(20),
|
||||
Forms\Components\Textarea::make('shipping_address')
|
||||
->required()
|
||||
->maxLength(500)
|
||||
->columnSpanFull(),
|
||||
Forms\Components\Textarea::make('notes')
|
||||
->maxLength(500)
|
||||
->columnSpanFull(),
|
||||
Forms\Components\TextInput::make('total')
|
||||
->required()
|
||||
->numeric()
|
||||
->prefix('R'),
|
||||
Forms\Components\Select::make('status')
|
||||
->options([
|
||||
'pending' => 'Pending',
|
||||
'processing' => 'Processing',
|
||||
'completed' => 'Completed',
|
||||
'cancelled' => 'Cancelled',
|
||||
])
|
||||
->required(),
|
||||
Forms\Components\Select::make('payment_status')
|
||||
->options([
|
||||
'pending' => 'Pending',
|
||||
'paid' => 'Paid',
|
||||
'failed' => 'Failed',
|
||||
])
|
||||
->required(),
|
||||
Forms\Components\TextInput::make('payment_method')
|
||||
->maxLength(50),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
Tables\Columns\TextColumn::make('order_number')
|
||||
->searchable(),
|
||||
Tables\Columns\TextColumn::make('customer_name')
|
||||
->searchable(),
|
||||
Tables\Columns\TextColumn::make('customer_email')
|
||||
->searchable(),
|
||||
Tables\Columns\TextColumn::make('total')
|
||||
->money('ZAR')
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('status')
|
||||
->badge()
|
||||
->color(fn (string $state): string => match ($state) {
|
||||
'pending' => 'warning',
|
||||
'processing' => 'info',
|
||||
'completed' => 'success',
|
||||
'cancelled' => 'danger',
|
||||
}),
|
||||
Tables\Columns\TextColumn::make('payment_status')
|
||||
->badge()
|
||||
->color(fn (string $state): string => match ($state) {
|
||||
'pending' => 'warning',
|
||||
'paid' => 'success',
|
||||
'failed' => 'danger',
|
||||
}),
|
||||
Tables\Columns\TextColumn::make('created_at')
|
||||
->dateTime()
|
||||
->sortable(),
|
||||
])
|
||||
->filters([
|
||||
Tables\Filters\SelectFilter::make('status')
|
||||
->options([
|
||||
'pending' => 'Pending',
|
||||
'processing' => 'Processing',
|
||||
'completed' => 'Completed',
|
||||
'cancelled' => 'Cancelled',
|
||||
]),
|
||||
Tables\Filters\SelectFilter::make('payment_status')
|
||||
->options([
|
||||
'pending' => 'Pending',
|
||||
'paid' => 'Paid',
|
||||
'failed' => 'Failed',
|
||||
]),
|
||||
])
|
||||
->actions([
|
||||
//
|
||||
])
|
||||
->bulkActions([
|
||||
//
|
||||
])
|
||||
->defaultSort('created_at', 'desc');
|
||||
}
|
||||
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [
|
||||
//
|
||||
];
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => Pages\ListOrders::route('/'),
|
||||
'create' => Pages\CreateOrder::route('/create'),
|
||||
'view' => Pages\ViewOrder::route('/{record}'),
|
||||
'edit' => Pages\EditOrder::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\OrderResource\Pages;
|
||||
|
||||
use App\Filament\Resources\OrderResource;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
|
||||
class CreateOrder extends CreateRecord
|
||||
{
|
||||
protected static string $resource = OrderResource::class;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\OrderResource\Pages;
|
||||
|
||||
use App\Filament\Resources\OrderResource;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
|
||||
class EditOrder extends EditRecord
|
||||
{
|
||||
protected static string $resource = OrderResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\ViewAction::make(),
|
||||
Actions\DeleteAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\OrderResource\Pages;
|
||||
|
||||
use App\Filament\Resources\OrderResource;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
|
||||
class ListOrders extends ListRecords
|
||||
{
|
||||
protected static string $resource = OrderResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\CreateAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\OrderResource\Pages;
|
||||
|
||||
use App\Filament\Resources\OrderResource;
|
||||
use App\Filament\Resources\OrderResource\Widgets\OrderItemsTable;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\ViewRecord;
|
||||
use Illuminate\Contracts\Support\Htmlable;
|
||||
|
||||
class ViewOrder extends ViewRecord
|
||||
{
|
||||
protected static string $resource = OrderResource::class;
|
||||
|
||||
public function getTitle(): string
|
||||
{
|
||||
return 'Order #' . $this->record->order_number;
|
||||
}
|
||||
|
||||
public function getHeading(): string | Htmlable
|
||||
{
|
||||
return 'Order #' . $this->record->order_number;
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\EditAction::make(),
|
||||
];
|
||||
}
|
||||
|
||||
protected function getHeaderWidgets(): array
|
||||
{
|
||||
return [
|
||||
OrderItemsTable::class,
|
||||
];
|
||||
}
|
||||
|
||||
public function getHeaderWidgetsColumns(): int | array
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\OrderResource\Widgets;
|
||||
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Table;
|
||||
use Filament\Widgets\TableWidget as BaseWidget;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class OrderItemsTable extends BaseWidget
|
||||
{
|
||||
public ?Model $record = null;
|
||||
|
||||
protected static ?string $heading = 'Order Items';
|
||||
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->query(
|
||||
fn () => $this->record->items()->getQuery()
|
||||
)
|
||||
->columns([
|
||||
Tables\Columns\TextColumn::make('product.name')
|
||||
->label('Product'),
|
||||
Tables\Columns\TextColumn::make('printStock.name')
|
||||
->label('Print Stock')
|
||||
->default('N/A'),
|
||||
Tables\Columns\TextColumn::make('type')
|
||||
->badge(),
|
||||
Tables\Columns\TextColumn::make('specifications')
|
||||
->label('Specifications')
|
||||
->getStateUsing(function ($record) {
|
||||
if ($record->type === 'wallpaper') {
|
||||
return "Length: {$record->length}m";
|
||||
} elseif ($record->type === 'mural') {
|
||||
$area = number_format($record->width * $record->height, 2);
|
||||
return "{$record->width}m × {$record->height}m ({$area}m²)";
|
||||
}
|
||||
return 'N/A';
|
||||
}),
|
||||
Tables\Columns\TextColumn::make('price')
|
||||
->money('ZAR')
|
||||
->alignEnd(),
|
||||
])
|
||||
->paginated(false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources;
|
||||
|
||||
use App\Filament\Resources\PrintStockResource\Pages;
|
||||
use App\Models\PrintStock;
|
||||
use Filament\Forms;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Actions;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class PrintStockResource extends Resource
|
||||
{
|
||||
protected static ?string $model = PrintStock::class;
|
||||
|
||||
protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-cube';
|
||||
|
||||
protected static ?string $navigationLabel = 'Print Stocks';
|
||||
|
||||
public static function form(Schema $schema): Schema
|
||||
{
|
||||
return $schema
|
||||
->schema([
|
||||
Forms\Components\TextInput::make('name')
|
||||
->required()
|
||||
->maxLength(255),
|
||||
Forms\Components\Textarea::make('description')
|
||||
->maxLength(65535)
|
||||
->columnSpanFull(),
|
||||
Forms\Components\TextInput::make('cost_per_meter')
|
||||
->required()
|
||||
->numeric()
|
||||
->prefix('R')
|
||||
->label('Cost per Meter'),
|
||||
Forms\Components\TextInput::make('cost_per_m2')
|
||||
->required()
|
||||
->numeric()
|
||||
->prefix('R')
|
||||
->label('Cost per m²'),
|
||||
Forms\Components\Select::make('type')
|
||||
->options([
|
||||
'wallpaper' => 'Wallpaper',
|
||||
'mural' => 'Mural',
|
||||
'both' => 'Both',
|
||||
])
|
||||
->required(),
|
||||
Forms\Components\TextInput::make('width')
|
||||
->label('Width (meters)')
|
||||
->numeric()
|
||||
->step(0.01)
|
||||
->minValue(0.1)
|
||||
->default(1)
|
||||
->helperText('Width of wallpaper roll in meters'),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
Tables\Columns\TextColumn::make('name')
|
||||
->searchable(),
|
||||
Tables\Columns\TextColumn::make('cost_per_meter')
|
||||
->money('ZAR')
|
||||
->label('Cost/m')
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('cost_per_m2')
|
||||
->money('ZAR')
|
||||
->label('Cost/m²')
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('type')
|
||||
->badge()
|
||||
->color(fn (string $state): string => match ($state) {
|
||||
'wallpaper' => 'success',
|
||||
'mural' => 'info',
|
||||
'both' => 'warning',
|
||||
}),
|
||||
Tables\Columns\TextColumn::make('created_at')
|
||||
->dateTime()
|
||||
->sortable()
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
])
|
||||
->filters([
|
||||
Tables\Filters\SelectFilter::make('type')
|
||||
->options([
|
||||
'wallpaper' => 'Wallpaper',
|
||||
'mural' => 'Mural',
|
||||
'both' => 'Both',
|
||||
]),
|
||||
])
|
||||
->actions([
|
||||
//
|
||||
])
|
||||
->bulkActions([
|
||||
//
|
||||
]);
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => Pages\ManagePrintStocks::route('/'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\PrintStockResource\Pages;
|
||||
|
||||
use App\Filament\Resources\PrintStockResource;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\ManageRecords;
|
||||
|
||||
class ManagePrintStocks extends ManageRecords
|
||||
{
|
||||
protected static string $resource = PrintStockResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\CreateAction::make(),
|
||||
];
|
||||
}
|
||||
|
||||
protected function getTableActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\EditAction::make(),
|
||||
Actions\DeleteAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources;
|
||||
|
||||
use App\Filament\Resources\ProductResource\Pages;
|
||||
use App\Models\Product;
|
||||
use Filament\Forms;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Schemas\Components\Section;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Actions;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class ProductResource extends Resource
|
||||
{
|
||||
protected static ?string $model = Product::class;
|
||||
|
||||
protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-rectangle-stack';
|
||||
|
||||
public static function form(Schema $schema): Schema
|
||||
{
|
||||
return $schema
|
||||
->schema([
|
||||
Forms\Components\TextInput::make('name')
|
||||
->required()
|
||||
->maxLength(255),
|
||||
Forms\Components\Select::make('category_id')
|
||||
->relationship('category', 'name')
|
||||
->required(),
|
||||
Forms\Components\Textarea::make('description')
|
||||
->maxLength(65535)
|
||||
->columnSpanFull(),
|
||||
Forms\Components\TextInput::make('price')
|
||||
->required()
|
||||
->numeric()
|
||||
->prefix('R'),
|
||||
Forms\Components\TextInput::make('stock')
|
||||
->required()
|
||||
->numeric()
|
||||
->default(100),
|
||||
Forms\Components\FileUpload::make('images')
|
||||
->multiple()
|
||||
->disk('public')
|
||||
->directory('products')
|
||||
->reorderable()
|
||||
->maxSize(5120)
|
||||
->acceptedFileTypes(['image/jpeg', 'image/png', 'image/webp']),
|
||||
Forms\Components\Select::make('type')
|
||||
->options([
|
||||
'wallpaper' => 'Wallpaper',
|
||||
'mural' => 'Mural',
|
||||
])
|
||||
->required(),
|
||||
Forms\Components\Select::make('printStocks')
|
||||
->relationship('printStocks', 'name')
|
||||
->multiple()
|
||||
->preload(),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
Tables\Columns\ImageColumn::make('images.image_path')
|
||||
->label('Image')
|
||||
->disk('public')
|
||||
->getStateUsing(function ($record) {
|
||||
return $record->images->first()?->image_path;
|
||||
})
|
||||
->defaultImageUrl('/images/placeholder.png')
|
||||
->circular()
|
||||
->size(60),
|
||||
Tables\Columns\TextColumn::make('name')
|
||||
->searchable(),
|
||||
Tables\Columns\TextColumn::make('category.name')
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('price')
|
||||
->money('ZAR')
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('stock')
|
||||
->numeric()
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('type')
|
||||
->badge()
|
||||
->color(fn (string $state): string => match ($state) {
|
||||
'wallpaper' => 'success',
|
||||
'mural' => 'info',
|
||||
}),
|
||||
Tables\Columns\TextColumn::make('created_at')
|
||||
->dateTime()
|
||||
->sortable()
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
])
|
||||
->filters([
|
||||
Tables\Filters\SelectFilter::make('type')
|
||||
->options([
|
||||
'wallpaper' => 'Wallpaper',
|
||||
'mural' => 'Mural',
|
||||
]),
|
||||
Tables\Filters\SelectFilter::make('category')
|
||||
->relationship('category', 'name'),
|
||||
])
|
||||
->actions([
|
||||
//
|
||||
])
|
||||
->bulkActions([
|
||||
//
|
||||
]);
|
||||
}
|
||||
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [
|
||||
//
|
||||
];
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => Pages\ListProducts::route('/'),
|
||||
'create' => Pages\CreateProduct::route('/create'),
|
||||
'edit' => Pages\EditProduct::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\ProductResource\Pages;
|
||||
|
||||
use App\Filament\Resources\ProductResource;
|
||||
use App\Models\ProductImage;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
|
||||
class CreateProduct extends CreateRecord
|
||||
{
|
||||
protected static string $resource = ProductResource::class;
|
||||
|
||||
protected function mutateFormDataBeforeSave(array $data): array
|
||||
{
|
||||
// Store images in a temporary property
|
||||
if (isset($data['images']) && is_array($data['images'])) {
|
||||
$this->pendingImages = $data['images'];
|
||||
}
|
||||
unset($data['images']);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
protected function afterSave(): void
|
||||
{
|
||||
if (!isset($this->pendingImages)) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($this->pendingImages as $index => $imagePath) {
|
||||
ProductImage::create([
|
||||
'product_id' => $this->record->id,
|
||||
'image_path' => $imagePath,
|
||||
'sort_order' => $index,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\ProductResource\Pages;
|
||||
|
||||
use App\Filament\Resources\ProductResource;
|
||||
use App\Models\ProductImage;
|
||||
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 mutateFormDataBeforeFill(array $data): array
|
||||
{
|
||||
// Load existing images for the form
|
||||
$data['images'] = $this->record->images()->orderBy('sort_order')->pluck('image_path')->toArray();
|
||||
return $data;
|
||||
}
|
||||
|
||||
protected function mutateFormDataBeforeSave(array $data): array
|
||||
{
|
||||
// Store images for processing after save
|
||||
$this->pendingImages = $data['images'] ?? [];
|
||||
unset($data['images']);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
protected function afterSave(): void
|
||||
{
|
||||
if (!isset($this->pendingImages)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$newImages = $this->pendingImages;
|
||||
$existingImages = $this->record->images()->pluck('image_path')->toArray();
|
||||
|
||||
// Find images to delete (in existing but not in new)
|
||||
$imagesToDelete = array_diff($existingImages, $newImages);
|
||||
foreach ($imagesToDelete as $imagePath) {
|
||||
$this->record->images()->where('image_path', $imagePath)->delete();
|
||||
}
|
||||
|
||||
// Add new images and update sort order
|
||||
foreach ($newImages as $index => $imagePath) {
|
||||
ProductImage::firstOrCreate(
|
||||
[
|
||||
'product_id' => $this->record->id,
|
||||
'image_path' => $imagePath,
|
||||
],
|
||||
[
|
||||
'sort_order' => $index,
|
||||
]
|
||||
)->update(['sort_order' => $index]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\ProductResource\Pages;
|
||||
|
||||
use App\Filament\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,62 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Widgets;
|
||||
|
||||
use App\Filament\Resources\OrderResource;
|
||||
use App\Models\Order;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Table;
|
||||
use Filament\Widgets\TableWidget as BaseWidget;
|
||||
|
||||
class LatestOrders extends BaseWidget
|
||||
{
|
||||
protected static ?int $sort = 2;
|
||||
|
||||
protected int | string | array $columnSpan = 'full';
|
||||
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->query(OrderResource::getEloquentQuery()->latest()->limit(10))
|
||||
->defaultPaginationPageOption(5)
|
||||
->defaultSort('created_at', 'desc')
|
||||
->columns([
|
||||
Tables\Columns\TextColumn::make('order_number')
|
||||
->label('Order #')
|
||||
->searchable()
|
||||
->sortable()
|
||||
->url(fn (Order $record): string => OrderResource::getUrl('view', ['record' => $record]))
|
||||
->color('primary'),
|
||||
Tables\Columns\TextColumn::make('customer_name')
|
||||
->searchable()
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('total')
|
||||
->money('ZAR')
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('status')
|
||||
->badge()
|
||||
->color(fn (string $state): string => match ($state) {
|
||||
'pending' => 'warning',
|
||||
'processing' => 'info',
|
||||
'completed' => 'success',
|
||||
'cancelled' => 'danger',
|
||||
}),
|
||||
Tables\Columns\TextColumn::make('payment_status')
|
||||
->badge()
|
||||
->color(fn (string $state): string => match ($state) {
|
||||
'pending' => 'warning',
|
||||
'paid' => 'success',
|
||||
'failed' => 'danger',
|
||||
}),
|
||||
Tables\Columns\TextColumn::make('created_at')
|
||||
->dateTime()
|
||||
->sortable()
|
||||
->since(),
|
||||
]);
|
||||
}
|
||||
|
||||
protected function getTableHeading(): string
|
||||
{
|
||||
return 'Latest Orders';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Widgets;
|
||||
|
||||
use App\Models\Order;
|
||||
use Filament\Widgets\ChartWidget;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class RevenueChart extends ChartWidget
|
||||
{
|
||||
protected ?string $heading = 'Revenue (Last 7 Days)';
|
||||
|
||||
protected static ?int $sort = 3;
|
||||
|
||||
protected function getData(): array
|
||||
{
|
||||
$data = Order::where('payment_status', 'paid')
|
||||
->where('created_at', '>=', now()->subDays(7))
|
||||
->select(
|
||||
DB::raw('DATE(created_at) as date'),
|
||||
DB::raw('SUM(total) as revenue')
|
||||
)
|
||||
->groupBy('date')
|
||||
->orderBy('date')
|
||||
->get();
|
||||
|
||||
$labels = [];
|
||||
$revenues = [];
|
||||
|
||||
// Fill in last 7 days
|
||||
for ($i = 6; $i >= 0; $i--) {
|
||||
$date = now()->subDays($i)->format('Y-m-d');
|
||||
$dayName = now()->subDays($i)->format('D');
|
||||
$labels[] = $dayName;
|
||||
|
||||
$dayData = $data->firstWhere('date', $date);
|
||||
$revenues[] = $dayData ? $dayData->revenue : 0;
|
||||
}
|
||||
|
||||
return [
|
||||
'datasets' => [
|
||||
[
|
||||
'label' => 'Revenue (R)',
|
||||
'data' => $revenues,
|
||||
'backgroundColor' => 'rgba(59, 130, 246, 0.1)',
|
||||
'borderColor' => 'rgb(59, 130, 246)',
|
||||
'fill' => true,
|
||||
],
|
||||
],
|
||||
'labels' => $labels,
|
||||
];
|
||||
}
|
||||
|
||||
protected function getType(): string
|
||||
{
|
||||
return 'line';
|
||||
}
|
||||
|
||||
protected function getOptions(): array
|
||||
{
|
||||
return [
|
||||
'scales' => [
|
||||
'y' => [
|
||||
'beginAtZero' => true,
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Widgets;
|
||||
|
||||
use App\Models\Order;
|
||||
use App\Models\Product;
|
||||
use Filament\Widgets\StatsOverviewWidget;
|
||||
use Filament\Widgets\StatsOverviewWidget\Stat;
|
||||
|
||||
class StatsOverview extends StatsOverviewWidget
|
||||
{
|
||||
protected static ?int $sort = 1;
|
||||
|
||||
protected function getStats(): array
|
||||
{
|
||||
$totalOrders = Order::count();
|
||||
$pendingOrders = Order::where('status', 'pending')->count();
|
||||
$processingOrders = Order::where('status', 'processing')->count();
|
||||
$lowStockProducts = Product::where('stock', '<', 10)->count();
|
||||
|
||||
$totalRevenue = Order::where('payment_status', 'paid')->sum('total');
|
||||
$monthlyRevenue = Order::where('payment_status', 'paid')
|
||||
->whereMonth('created_at', now()->month)
|
||||
->whereYear('created_at', now()->year)
|
||||
->sum('total');
|
||||
|
||||
$todayOrders = Order::whereDate('created_at', today())->count();
|
||||
$todayRevenue = Order::where('payment_status', 'paid')
|
||||
->whereDate('created_at', today())
|
||||
->sum('total');
|
||||
|
||||
return [
|
||||
Stat::make('Pending Orders', $pendingOrders)
|
||||
->description('Awaiting processing')
|
||||
->descriptionIcon('heroicon-m-clock')
|
||||
->color('warning')
|
||||
->chart([7, 5, 10, 5, $pendingOrders]),
|
||||
|
||||
Stat::make('Processing Orders', $processingOrders)
|
||||
->description('Currently being processed')
|
||||
->descriptionIcon('heroicon-m-arrow-path')
|
||||
->color('info'),
|
||||
|
||||
Stat::make('Low Stock Alert', $lowStockProducts)
|
||||
->description('Products with less than 10 units')
|
||||
->descriptionIcon('heroicon-m-exclamation-triangle')
|
||||
->color($lowStockProducts > 0 ? 'danger' : 'success'),
|
||||
|
||||
Stat::make('Today\'s Orders', $todayOrders)
|
||||
->description('Orders placed today')
|
||||
->descriptionIcon('heroicon-m-shopping-bag')
|
||||
->color('success'),
|
||||
|
||||
Stat::make('Today\'s Revenue', 'R' . number_format($todayRevenue, 2))
|
||||
->description(now()->format('l, F j'))
|
||||
->descriptionIcon('heroicon-m-banknotes')
|
||||
->color('success'),
|
||||
|
||||
Stat::make('Monthly Revenue', 'R' . number_format($monthlyRevenue, 2))
|
||||
->description(now()->format('F Y'))
|
||||
->descriptionIcon('heroicon-m-chart-bar')
|
||||
->color('info'),
|
||||
|
||||
Stat::make('Total Revenue', 'R' . number_format($totalRevenue, 2))
|
||||
->description('All time revenue')
|
||||
->descriptionIcon('heroicon-m-currency-dollar')
|
||||
->color('success'),
|
||||
|
||||
Stat::make('Total Orders', $totalOrders)
|
||||
->description('All time orders')
|
||||
->descriptionIcon('heroicon-m-shopping-cart')
|
||||
->color('primary'),
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user