Compare commits

...

39 Commits

Author SHA1 Message Date
Nisse Lommerde 348540f084 More work on shippingEntries
Table groupified + search working despite lack of column; create/edit
form
4 hours ago
Nisse Lommerde 487ea48c14 Work on ordertest, splitting address lines, and shipping entry stuff 3 days ago
Nisse Lommerde 2c0fbfde5b Merge branch 'orders' of git.niisse.net:nisse/topnotch_website into orders 6 days ago
Nisse Lommerde fcb1cef6fd Work on invoice report table (added date groups) 6 days ago
Nisse Lommerde f945ad2f71 Work on invoice report table (added date groups) 1 week ago
Nisse Lommerde b47dd597e1 Improving Order table front-end 1 week ago
Nisse Lommerde cf4a56ee84 Implement invoice report table 1 week ago
Nisse Lommerde 2aaf7ab8a2 WIP work on invoice reports 1 week ago
Nisse Lommerde 73df66d0eb merge 1 week ago
Nisse Lommerde eceaf3e676 Work 1 week ago
Nisse Lommerde bdbc65cedb Work 2 weeks ago
Nisse Lommerde f051f20ad9 Setup Pest for testing and hooks for pint and pest on commit 2 weeks ago
Nisse Lommerde 1ffd38fd53 Work on invoices and table spacing 2 weeks ago
Nisse Lommerde 1f1f783aa9 Optimized product services reports code 2 weeks ago
Nisse Lommerde 7740160b4f Product Services filtering seems to work 2 weeks ago
Nisse Lommerde fcb1eda56f work on reports 2 weeks ago
Nisse Lommerde fdbc2653d4 pre service-type modelisation commitation 2 weeks ago
Nisse Lommerde 162c8839e2 Work on invoice status and reports 2 weeks ago
Nisse Lommerde 2de11bcaba Work on invoice PDF
to do
- split address lines in two
- look into footer
2 weeks ago
Nisse Lommerde 1b891f8350 created invoice seeder 2 weeks ago
Nisse Lommerde 7e2a22e016 Work 3 weeks ago
Nisse Lommerde 562e499d12 More work on invoices 3 weeks ago
Nisse Lommerde 256cc1f7ed Invoice Create and Edit Page mostly done 3 weeks ago
Nisse Lommerde a12e0b29d3 Work on invoices 3 weeks ago
Nisse Lommerde d950955371 Added pre-pro and printed toggles to order 4 weeks ago
Nisse Lommerde 3c98de929e Various little things 4 weeks ago
Nisse Lommerde 913a4477a7 Replace icons with lucide and work on quote 4 weeks ago
Nisse Lommerde b3868e8b0a Order CRUD complete 1 month ago
Nisse Lommerde e79d1839fe Re-worked seeders 1 month ago
Nisse Lommerde 2361ec0b88 Orders are once again writeable 1 month ago
Nisse Lommerde b9346c4466 Work work work 1 month ago
Nisse Lommerde 3e2c5d5fac Convert Filament JSON to model bools 1 month ago
Nisse Lommerde c0053a8969 Add TableRepeater package 1 month ago
Nisse Lommerde a5c40ea161 Add Filament Cluster 1 month ago
Nisse Lommerde 542e1346f4 Added Filament 1 month ago
Nisse Lommerde 08f0a99551 PDF Setup and Order PDF view 2 months ago
Nisse Lommerde 8e80ba9480 Order Show page 2 months ago
Nisse Lommerde 7074596cb7 Work on PHPStan error fixing 2 months ago
Nisse Lommerde 61831b1940 WIP orders 2 months ago

@ -0,0 +1,5 @@
[Dolphin]
Timestamp=2024,11,17,16,40,18.95
Version=4
ViewMode=1
VisibleRoles=Icons_text,Icons_size

@ -0,0 +1,69 @@
APP_NAME=Laravel
APP_ENV=local
APP_KEY=base64:vRgghlbIdXQxXIEvgUArbI9FURhgdyqx3LDXDwHYSmA=
APP_DEBUG=true
APP_TIMEZONE=UTC
APP_URL=http://localhost
APP_LOCALE=en
APP_FALLBACK_LOCALE=en
APP_FAKER_LOCALE=en_US
APP_MAINTENANCE_DRIVER=file
# APP_MAINTENANCE_STORE=database
BCRYPT_ROUNDS=12
LOG_CHANNEL=stack
LOG_STACK=single
LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=debug
DB_CONNECTION=mysql
DB_HOST=mysql
DB_PORT=3306
DB_DATABASE=testing
DB_USERNAME=sail
DB_PASSWORD=password
SESSION_DRIVER=database
SESSION_LIFETIME=120
SESSION_ENCRYPT=false
SESSION_PATH=/
SESSION_DOMAIN=null
BROADCAST_CONNECTION=log
FILESYSTEM_DISK=local
QUEUE_CONNECTION=database
CACHE_STORE=database
CACHE_PREFIX=
MEMCACHED_HOST=127.0.0.1
REDIS_CLIENT=phpredis
REDIS_HOST=redis
REDIS_PASSWORD=null
REDIS_PORT=6379
MAIL_MAILER=smtp
MAIL_HOST=mailpit
MAIL_PORT=1025
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS="hello@example.com"
MAIL_FROM_NAME="${APP_NAME}"
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=
AWS_USE_PATH_STYLE_ENDPOINT=false
VITE_APP_NAME="${APP_NAME}"
SCOUT_DRIVER=meilisearch
MEILISEARCH_HOST=http://meilisearch:7700
MEILISEARCH_NO_ANALYTICS=false

3
.gitignore vendored

@ -103,3 +103,6 @@ fabric.properties
.idea/caches/build_file_checksums.ser
.idea
.directory
.directory
.directory

@ -0,0 +1,9 @@
const {join} = require('path');
/**
* @type {import("puppeteer").Configuration}
*/
module.exports = {
// Changes the cache location for Puppeteer.
cacheDirectory: join(__dirname, '.cache', 'puppeteer'),
};

@ -1,3 +1,7 @@
https://github.com/spatie/laravel-pdf/discussions/90
for spatie/pdf stuff
<p align="center"><a href="https://laravel.com" target="_blank"><img src="https://raw.githubusercontent.com/laravel/art/master/logo-lockup/5%20SVG/2%20CMYK/1%20Full%20Color/laravel-logolockup-cmyk-red.svg" width="400" alt="Laravel Logo"></a></p>
<p align="center">

@ -0,0 +1,37 @@
<?php
namespace App\Enums;
use Filament\Support\Contracts\HasColor;
use Filament\Support\Contracts\HasIcon;
use Filament\Support\Contracts\HasLabel;
enum InvoiceStatus: string implements HasColor, HasIcon, HasLabel
{
case UNPAID = 'Not paid';
case PAID = 'Paid';
case VOID = 'Void';
public function getLabel(): ?string
{
return $this->value;
}
public function getColor(): string|array|null
{
return match ($this) {
self::UNPAID => 'warning',
self::PAID => 'success',
self::VOID => 'gray'
};
}
public function getIcon(): ?string
{
return match ($this) {
self::UNPAID => 'lucide-circle-x',
self::PAID => 'lucide-circle-check',
self::VOID => 'lucide-circle-slash',
};
}
}

@ -0,0 +1,35 @@
<?php
namespace App\Enums;
use Filament\Support\Contracts\HasIcon;
use Filament\Support\Contracts\HasLabel;
enum OrderAttributes: string implements HasIcon, HasLabel
{
case new_art = 'New Art';
case repeat = 'Repeat';
case rush = 'Rush';
case event = 'Event';
case digitizing = 'Digitizing';
case garments = 'Garments';
case supplied_file = 'Customer Supplied File';
public function getLabel(): ?string
{
return $this->value;
}
public function getIcon(): ?string
{
return match ($this) {
self::new_art => 'lucide-brush',
self::repeat => 'lucide-files',
self::rush => 'lucide-bell-ring',
self::event => 'lucide-calendar-range',
self::digitizing => 'lucide-computer',
self::garments => 'lucide-shirt',
self::supplied_file => 'lucide-file-check',
};
}
}

@ -2,10 +2,42 @@
namespace App\Enums;
enum OrderStatus: string
use Filament\Support\Contracts\HasColor;
use Filament\Support\Contracts\HasIcon;
use Filament\Support\Contracts\HasLabel;
enum OrderStatus: string implements HasColor, HasIcon, HasLabel
{
case DRAFT = 'Draft';
case APPROVED = 'Approved';
case PRODUCTION = 'Production';
case SHIPPED = 'Shipped';
case INVOICED = 'Invoiced';
public function getLabel(): ?string
{
return $this->value;
}
public function getColor(): string|array|null
{
return match ($this) {
self::DRAFT => 'gray',
self::APPROVED => 'success',
self::PRODUCTION => 'primary',
self::SHIPPED => 'warning',
self::INVOICED => 'invoiced',
};
}
public function getIcon(): ?string
{
return match ($this) {
self::DRAFT => 'lucide-pencil',
self::APPROVED => 'lucide-check-check',
self::PRODUCTION => 'lucide-refresh-cw',
self::SHIPPED => 'lucide-send',
self::INVOICED => 'lucide-credit-card',
};
}
}

@ -2,11 +2,18 @@
namespace App\Enums;
enum OrderType: string
use Filament\Support\Contracts\HasLabel;
enum OrderType: string implements HasLabel
{
case EMBROIDERY = 'Embroidery';
case SCREEN = 'Screen printing';
case DTG = 'Direct-to-garment';
case VINYL = 'Vinyl';
case MISC = 'Misc';
case EMB = 'Embroidery';
case SCP = 'Screen printing';
case DTG = 'Direct-to-garment';
case VINYL = 'Vinyl';
case MISC = 'Misc';
public function getLabel(): ?string
{
return $this->value;
}
}

@ -2,9 +2,28 @@
namespace App\Enums;
enum ShippingType: string
use Filament\Support\Contracts\HasIcon;
use Filament\Support\Contracts\HasLabel;
enum ShippingType: string implements HasIcon, HasLabel
{
case THEY_SHIP = 'They ship';
case WE_SHIP = 'We ship';
case PICKUP = 'Pickup';
case OTHER = 'Other';
public function getLabel(): ?string
{
return $this->value;
}
public function getIcon(): ?string
{
return match ($this) {
self::THEY_SHIP => 'lucide-truck',
self::WE_SHIP => 'lucide-house',
self::PICKUP => 'lucide-handshake',
self::OTHER => 'lucide-ellipsis'
};
}
}

@ -0,0 +1,11 @@
<?php
namespace App\Filament\Resources\ContactResource\Pages;
use App\Filament\Resources\ContactResource;
use Filament\Resources\Pages\CreateRecord;
class CreateContact extends CreateRecord
{
protected static string $resource = ContactResource::class;
}

@ -0,0 +1,19 @@
<?php
namespace App\Filament\Resources\ContactResource\Pages;
use App\Filament\Resources\ContactResource;
use Filament\Actions;
use Filament\Resources\Pages\EditRecord;
class EditContact extends EditRecord
{
protected static string $resource = ContactResource::class;
protected function getHeaderActions(): array
{
return [
Actions\DeleteAction::make(),
];
}
}

@ -0,0 +1,19 @@
<?php
namespace App\Filament\Resources\ContactResource\Pages;
use App\Filament\Resources\ContactResource;
use Filament\Actions;
use Filament\Resources\Pages\ListRecords;
class ListContacts extends ListRecords
{
protected static string $resource = ContactResource::class;
protected function getHeaderActions(): array
{
return [
Actions\CreateAction::make(),
];
}
}

@ -0,0 +1,119 @@
<?php
namespace App\Filament\Resources;
use App\Filament\Resources\CustomerReportResource\Pages;
use App\Models\Customer;
use Filament\Forms\Components\DatePicker;
use Filament\Forms\Form;
use Filament\Resources\Resource;
use Filament\Tables;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Model;
class CustomerReportResource extends Resource
{
protected static ?string $model = Customer::class;
protected static ?string $navigationGroup = 'Reports';
protected static ?string $navigationIcon = 'heroicon-o-rectangle-stack';
protected static ?string $navigationLabel = 'Customer Sales';
protected static ?int $navigationSort = 2;
public static function form(Form $form): Form
{
return $form
->schema([
//
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
Tables\Columns\TextColumn::make('company_name')
->label('Customer')
->sortable()
->searchable()
->extraHeaderAttributes(['class' => 'w-full']),
Tables\Columns\TextColumn::make('subtotal')
->money('usd')
->alignRight()
->getStateUsing(function (Table $table, Model $record) {
return $record->getSubtotalAttribute(
$table->getFilter('created_at')->getState()['created_at'],
$table->getFilter('created_until')->getState()['created_until']
);
}),
Tables\Columns\TextColumn::make('gst')
->label('GST')
->money('usd')
->alignRight()
->getStateUsing(function (Table $table, Model $record) {
return $record->getGstAttribute(
$table->getFilter('created_at')->getState()['created_at'],
$table->getFilter('created_until')->getState()['created_until']
);
}),
Tables\Columns\TextColumn::make('pst')
->label('PST')
->money('usd')
->alignRight()
->getStateUsing(function (Table $table, Model $record) {
return $record->getPstAttribute(
$table->getFilter('created_at')->getState()['created_at'],
$table->getFilter('created_until')->getState()['created_until']
);
}),
Tables\Columns\TextColumn::make('total')
->money('usd')
->weight('bold')
->alignRight()
->getStateUsing(function (Table $table, Model $record) {
return $record->getTotalAttribute(
$table->getFilter('created_at')->getState()['created_at'],
$table->getFilter('created_until')->getState()['created_until']
);
}),
])
->filters([
Tables\Filters\Filter::make('created_at')
->form([
DatePicker::make('created_at')
->label('From date'),
]),
Tables\Filters\Filter::make('created_until')
->form([
DatePicker::make('created_until')
->label('Until date'),
]),
])
->actions([
])
->bulkActions([
]);
}
public static function getRelations(): array
{
return [
//
];
}
public static function getPages(): array
{
return [
'index' => Pages\ListCustomerReports::route('/'),
];
}
}

@ -0,0 +1,11 @@
<?php
namespace App\Filament\Resources\CustomerReportResource\Pages;
use App\Filament\Resources\CustomerReportResource;
use Filament\Resources\Pages\CreateRecord;
class CreateCustomerReport extends CreateRecord
{
protected static string $resource = CustomerReportResource::class;
}

@ -0,0 +1,19 @@
<?php
namespace App\Filament\Resources\CustomerReportResource\Pages;
use App\Filament\Resources\CustomerReportResource;
use Filament\Actions;
use Filament\Resources\Pages\EditRecord;
class EditCustomerReport extends EditRecord
{
protected static string $resource = CustomerReportResource::class;
protected function getHeaderActions(): array
{
return [
Actions\DeleteAction::make(),
];
}
}

@ -0,0 +1,20 @@
<?php
namespace App\Filament\Resources\CustomerReportResource\Pages;
use App\Filament\Resources\CustomerReportResource;
use Filament\Resources\Pages\ListRecords;
class ListCustomerReports extends ListRecords
{
protected static string $resource = CustomerReportResource::class;
protected static ?string $title = 'Customer Reports';
protected function getHeaderActions(): array
{
return [
// Actions\CreateAction::make(),
];
}
}

@ -0,0 +1,83 @@
<?php
namespace App\Filament\Resources;
use App\Filament\Resources\CustomerResource\Pages;
use App\Filament\Resources\CustomerResource\RelationManagers\ContactsRelationManager;
use App\Filament\Resources\CustomerResource\RelationManagers\ShippingEntriesRelationManager;
use App\Models\Customer;
use Filament\Forms\Components\Section;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Form;
use Filament\Resources\Resource;
use Filament\Tables;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
class CustomerResource extends Resource
{
protected static ?string $model = Customer::class;
protected static ?string $navigationIcon = 'heroicon-o-building-office';
protected static ?string $navigationGroup = 'Management';
protected static ?int $navigationSort = 1;
public static function form(Form $form): Form
{
return $form
->schema([
Section::make([
TextInput::make('company_name'),
TextInput::make('phone'),
TextInput::make('shipping_address_line_1'),
TextInput::make('shipping_address_line_2'),
TextInput::make('billing_address_line_1'),
TextInput::make('billing_address_line_2'),
])->columns(2),
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
TextColumn::make('company_name')
->searchable()
->sortable(),
// TextColumn::make('shipping_address'),
TextColumn::make('phone'),
])
->filters([
//
])
->actions([
Tables\Actions\EditAction::make(),
])
->bulkActions([
Tables\Actions\BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(),
]),
]);
}
public static function getRelations(): array
{
return [
// RelationGroup::make('Relations', [
ContactsRelationManager::class,
ShippingEntriesRelationManager::class,
// ]),
];
}
public static function getPages(): array
{
return [
'index' => Pages\ListCustomers::route('/'),
'create' => Pages\CreateCustomer::route('/create'),
'edit' => Pages\EditCustomer::route('/{record}/edit'),
];
}
}

@ -0,0 +1,11 @@
<?php
namespace App\Filament\Resources\CustomerResource\Pages;
use App\Filament\Resources\CustomerResource;
use Filament\Resources\Pages\CreateRecord;
class CreateCustomer extends CreateRecord
{
protected static string $resource = CustomerResource::class;
}

@ -0,0 +1,21 @@
<?php
namespace App\Filament\Resources\CustomerResource\Pages;
use App\Filament\Resources\CustomerResource;
use Filament\Actions;
use Filament\Resources\Pages\EditRecord;
class EditCustomer extends EditRecord
{
protected static string $resource = CustomerResource::class;
protected function getHeaderActions(): array
{
//todo: make report
return [
Actions\DeleteAction::make(),
];
}
}

@ -0,0 +1,19 @@
<?php
namespace App\Filament\Resources\CustomerResource\Pages;
use App\Filament\Resources\CustomerResource;
use Filament\Actions;
use Filament\Resources\Pages\ListRecords;
class ListCustomers extends ListRecords
{
protected static string $resource = CustomerResource::class;
protected function getHeaderActions(): array
{
return [
Actions\CreateAction::make(),
];
}
}

@ -0,0 +1,56 @@
<?php
namespace App\Filament\Resources\CustomerResource\RelationManagers;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Form;
use Filament\Resources\RelationManagers\RelationManager;
use Filament\Tables;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
class ContactsRelationManager extends RelationManager
{
protected static string $relationship = 'contacts';
public function form(Form $form): Form
{
return $form
->schema([
TextInput::make('first_name'),
TextInput::make('last_name'),
TextInput::make('email')
->email(),
TextInput::make('phone'),
TextInput::make('notes'),
]);
}
public function table(Table $table): Table
{
return $table
->recordTitleAttribute('id')
->columns([
TextColumn::make('full_name'),
TextColumn::make('email'),
TextColumn::make('phone'),
TextColumn::make('notes')
->extraHeaderAttributes(['class' => 'w-full']),
])
->filters([
//
])
->headerActions([
Tables\Actions\CreateAction::make(),
])
->actions([
Tables\Actions\EditAction::make(),
Tables\Actions\DeleteAction::make(),
])
->bulkActions([
// Tables\Actions\BulkActionGroup::make([
// Tables\Actions\DeleteBulkAction::make(),
// ]),
]);
}
}

@ -0,0 +1,50 @@
<?php
namespace App\Filament\Resources\CustomerResource\RelationManagers;
use Filament\Forms;
use Filament\Forms\Form;
use Filament\Resources\RelationManagers\RelationManager;
use Filament\Tables;
use Filament\Tables\Table;
class ShippingEntriesRelationManager extends RelationManager
{
protected static string $relationship = 'ShippingEntries';
public function form(Form $form): Form
{
return $form
->schema([
Forms\Components\TextInput::make('id')
->required()
->maxLength(255),
]);
}
public function table(Table $table): Table
{
return $table
->columns([
Tables\Columns\TextColumn::make('courier'),
Tables\Columns\TextColumn::make('account_title'),
Tables\Columns\TextColumn::make('account_username')
->label('Username'),
Tables\Columns\TextColumn::make('account_password')
->label('Password'),
Tables\Columns\TextColumn::make('info_needed'),
// ->extraHeaderAttributes(['class' => 'w-full']),
Tables\Columns\TextColumn::make('notes'),
])
->filters([
//
])
->headerActions([
Tables\Actions\CreateAction::make(),
])
->actions([
// Tables\Actions\EditAction::make(),
// Tables\Actions\DeleteAction::make(),
]);
}
}

@ -0,0 +1,159 @@
<?php
namespace App\Filament\Resources;
use App\Enums\InvoiceStatus;
use App\Filament\Resources\InvoiceReportResource\Pages;
use App\Models\Order;
use Filament\Forms\Components\DatePicker;
use Filament\Forms\Form;
use Filament\Resources\Resource;
use Filament\Tables;
use Filament\Tables\Grouping\Group;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
class InvoiceReportResource extends Resource
{
protected static ?string $model = Order::class;
protected static ?string $navigationIcon = 'heroicon-o-rectangle-stack';
protected static ?string $navigationGroup = 'Reports';
protected static ?string $navigationLabel = 'Invoice Reports';
protected static ?int $navigationSort = 2;
public static function form(Form $form): Form
{
return $form
->schema([
//
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
Tables\Columns\TextColumn::make('invoice.date')
->label('Date')
->date('Y-m-d'),
Tables\Columns\TextColumn::make('invoice.internal_id')
->color('primary')
->fontFamily('mono'),
Tables\Columns\TextColumn::make('customer_po')
->color('code')
->weight('bold')
->extraHeaderAttributes(['class' => 'w-full']),
Tables\Columns\TextColumn::make('total_service_price')
->label('Subtotal')
->alignRight()
->money(),
Tables\Columns\TextColumn::make('pst')
->label('PST')
->alignRight()
->getStateUsing(fn (Order $record) => $record->invoice->pst ? $record->total_service_price * 0.07 : 0.00)
->formatStateUsing(function ($state) {
return $state == 0.00 ? '-' : '$'.$state;
}),
Tables\Columns\TextColumn::make('gst')
->label('GST')
->getStateUsing(fn (Order $record) => $record->total_service_price * 0.05)
->alignRight()
->money(),
Tables\Columns\TextColumn::make('invoice.total')
->label('Total')
->getStateUsing(function (Order $record) {
$total = $record->total_service_price * 1.05;
if ($record->invoice->pst) {
$total += $record->total_service_price * 0.07;
}
return $total;
})
->alignRight()
->money(),
Tables\Columns\TextColumn::make('invoice.status')
->badge(InvoiceStatus::class),
])
->filters([
Tables\Filters\SelectFilter::make('customer')
->relationship('customer', 'company_name')
->preload()
->searchable()
->placeholder('Select a customer...')
->selectablePlaceholder(false)
->query(function (array $data, Builder $query): Builder {
return $query->where('orders.customer_id', $data['value'] ?? '-1');
}),
Tables\Filters\Filter::make('date_from')
->form([DatePicker::make('date_from')])
->query(function (Builder $query, array $data): Builder {
return $query->when($data['date_from'], function (Builder $query, $date) {
return $query->whereHas('invoice', fn ($query) => $query->whereDate('date', '>=', $date));
});
}),
Tables\Filters\Filter::make('date_until')
->form([DatePicker::make('date_until')])
->query(function (Builder $query, array $data): Builder {
return $query->when($data['date_until'], function (Builder $query, $date) {
return $query->whereHas('invoice', fn ($query) => $query->whereDate('date', '<=', $date));
});
}),
Tables\Filters\SelectFilter::make('invoice_status')
->options(InvoiceStatus::class)
->query(function (Builder $query, array $data): Builder {
return $query->when($data['value'], fn (Builder $query, $value) => $query->whereHas('invoice', fn (Builder $query) => $query->where('status', $value)));
}),
], layout: Tables\Enums\FiltersLayout::AboveContent)
->hiddenFilterIndicators()
->actions([])
->defaultGroup(
Group::make('date')
->getKeyFromRecordUsing(fn (Order $record): string => $record->invoice->date->format('Y-m-0'))
->getTitleFromRecordUsing(fn (Order $record): string => $record->invoice->date->format('F Y'))
->orderQueryUsing(function (Builder $query) {
return $query->join('invoices', 'orders.invoice_id', '=', 'invoices.id')
->orderBy('invoices.date', 'desc');
})
->titlePrefixedWithLabel(false),
);
}
public static function getEloquentQuery(): \Illuminate\Database\Eloquent\Builder
{
return Order::query()
->has('invoice');
}
public static function getRelations(): array
{
return [
//
];
}
public static function getPages(): array
{
return [
'index' => Pages\ListInvoiceReports::route('/'),
];
}
}

@ -0,0 +1,11 @@
<?php
namespace App\Filament\Resources\InvoiceReportResource\Pages;
use App\Filament\Resources\InvoiceReportResource;
use Filament\Resources\Pages\CreateRecord;
class CreateInvoiceReport extends CreateRecord
{
protected static string $resource = InvoiceReportResource::class;
}

@ -0,0 +1,20 @@
<?php
namespace App\Filament\Resources\InvoiceReportResource\Pages;
use App\Filament\Resources\InvoiceReportResource;
use Filament\Actions;
use Filament\Resources\Pages\EditRecord;
class EditInvoiceReport extends EditRecord
{
protected static string $resource = InvoiceReportResource::class;
protected function getHeaderActions(): array
{
return [
Actions\ViewAction::make(),
Actions\DeleteAction::make(),
];
}
}

@ -0,0 +1,23 @@
<?php
namespace App\Filament\Resources\InvoiceReportResource\Pages;
use App\Filament\Resources\InvoiceReportResource;
use Filament\Actions\Action;
use Filament\Resources\Pages\ListRecords;
class ListInvoiceReports extends ListRecords
{
protected static string $resource = InvoiceReportResource::class;
protected static ?string $title = 'Invoice Reports';
protected function getHeaderActions(): array
{
return [
Action::make('generateReport')
->label('Make Report')
->icon('lucide-printer'),
];
}
}

@ -0,0 +1,19 @@
<?php
namespace App\Filament\Resources\InvoiceReportResource\Pages;
use App\Filament\Resources\InvoiceReportResource;
use Filament\Resources\Pages\ViewRecord;
class ViewInvoiceReport extends ViewRecord
{
protected static string $resource = InvoiceReportResource::class;
protected static ?string $title = 'View Invoice Report';
protected function getHeaderActions(): array
{
return [
];
}
}

@ -0,0 +1,213 @@
<?php
namespace App\Filament\Resources;
use App\Enums\InvoiceStatus;
use App\Filament\Resources\InvoiceResource\Pages;
use App\Filament\Resources\InvoiceResource\RelationManagers\OrdersRelationManager;
use App\Filament\Resources\InvoiceResource\RelationManagers\ProductServicesRelationManager;
use App\Models\Customer;
use App\Models\Invoice;
use Filament\Forms\Components\DatePicker;
use Filament\Forms\Components\Grid;
use Filament\Forms\Components\Section;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\Split;
use Filament\Forms\Components\ToggleButtons;
use Filament\Forms\Form;
use Filament\Notifications\Notification;
use Filament\Resources\Resource;
use Filament\Tables;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Collection;
class InvoiceResource extends Resource
{
protected static ?string $model = Invoice::class;
protected static ?string $navigationIcon = 'lucide-receipt-text';
protected static ?string $navigationGroup = 'Production';
protected static ?int $navigationSort = 2;
public static function form(Form $form): Form
{
return $form
->schema([
Section::make([
Grid::make(2)
->schema([
Select::make('customer_id')
->required()
->label('Customer')
->options(Customer::all()->pluck('company_name', 'id'))
->reactive()
->searchable()
->disabledOn('edit')
->columnSpan(2),
Split::make([
DatePicker::make('date')
->required()
->default(today()),
DatePicker::make('due_date'),
])
->columnSpan(2),
Select::make('status')
->options(InvoiceStatus::class)
->searchable()
->required()
->default(InvoiceStatus::UNPAID),
])->columnSpan(2),
Grid::make(1)
->schema([
ToggleButtons::make('gst')
->boolean()
->default(true)
->inline()
->colors([
'true' => 'info',
'false' => 'info',
]),
ToggleButtons::make('pst')
->boolean()
->default(false)
->inline()
->colors([
'true' => 'info',
'false' => 'info',
]),
])->columnSpan(1),
])
->columns(3)
->columnSpan(3),
])->columns(3);
}
public static function table(Table $table): Table
{
return $table
->columns([
Tables\Columns\TextColumn::make('internal_id')
->label('ID')
->fontFamily('mono')
->color('primary')
->sortable()
->searchable(),
Tables\Columns\TextColumn::make('customer.company_name')
->sortable()
->extraHeaderAttributes(['class' => 'w-full'])
->searchable(),
Tables\Columns\TextColumn::make('created_at')
->label('Created')
->date()
->sortable(),
Tables\Columns\TextColumn::make('subtotal')
->money('USD')
->alignRight(),
Tables\Columns\TextColumn::make('gst_amount')
->label('GST')
->money('USD')
->alignRight(),
Tables\Columns\TextColumn::make('pst_amount')
->label('PST')
->formatStateUsing(function ($state) {
return $state == 0.00 ? '-' : '$'.$state;
})
->alignRight(),
Tables\Columns\TextColumn::make('total')
->money('USD')
->weight('bold')
->alignRight(),
Tables\Columns\TextColumn::make('status')
->badge(InvoiceStatus::class)
->sortable(),
])
->filters([
Tables\Filters\Filter::make('created_at')
->form([
DatePicker::make('created_from')
->label('From date'),
DatePicker::make('created_until')
->label('Until date'),
])
->query(function (Builder $query, array $data): Builder {
return $query
->when(
$data['created_from'],
fn (Builder $query, $date): Builder => $query->whereDate('date', '>=', $date),
)
->when(
$data['created_until'],
fn (Builder $query, $date): Builder => $query->whereDate('date', '<=', $date),
);
}),
Tables\Filters\SelectFilter::make('status')
->options(InvoiceStatus::class),
], )
->groups([
'status',
])
->defaultSort('created_at', 'desc')
->actions([
Tables\Actions\EditAction::make(),
//todo: generate report pdf
])
->bulkActions([
Tables\Actions\BulkAction::make('Mark as paid')
->action(function (Collection $records) {
$records->each->setStatus(InvoiceStatus::PAID);
Notification::make()
->title(count($records).' item(s) saved successfully')
->success()
->send();
})
->icon('lucide-circle-check')
->deselectRecordsAfterCompletion(),
Tables\Actions\BulkActionGroup::make([
Tables\Actions\BulkAction::make('Mark as unpaid')
->action(function (Collection $records) {
$records->each->setStatus(InvoiceStatus::UNPAID);
Notification::make()
->title(count($records).' item(s) saved successfully')
->success()
->send();
})
->icon('lucide-circle-x')
->deselectRecordsAfterCompletion(),
Tables\Actions\DeleteBulkAction::make(),
])
->label('Other actions'),
])
->selectCurrentPageOnly();
}
public static function getRelations(): array
{
return [
OrdersRelationManager::class,
ProductServicesRelationManager::class,
];
}
public static function getPages(): array
{
return [
'index' => Pages\ListInvoices::route('/'),
'create' => Pages\CreateInvoice::route('/create'),
'edit' => Pages\EditInvoice::route('/{record}/edit'),
];
}
}

@ -0,0 +1,21 @@
<?php
namespace App\Filament\Resources\InvoiceResource\Pages;
use App\Filament\Resources\InvoiceResource;
use App\Models\Invoice;
use Filament\Resources\Pages\CreateRecord;
use Illuminate\Database\Eloquent\Model;
class CreateInvoice extends CreateRecord
{
protected static string $resource = InvoiceResource::class;
protected function handleRecordCreation(array $data): Model
{
$invoice = Invoice::create($data);
$invoice->calculateTotals();
return $invoice;
}
}

@ -0,0 +1,26 @@
<?php
namespace App\Filament\Resources\InvoiceResource\Pages;
use App\Filament\Resources\InvoiceResource;
use App\Models\Invoice;
use Filament\Actions;
use Filament\Actions\Action;
use Filament\Resources\Pages\EditRecord;
class EditInvoice extends EditRecord
{
protected static string $resource = InvoiceResource::class;
protected function getHeaderActions(): array
{
return [
Action::make('print')
->icon('lucide-printer')
->url(fn (Invoice $record) => route('invoice.pdf', $record))
->openUrlInNewTab(),
Actions\DeleteAction::make()
->icon('lucide-trash-2'),
];
}
}

@ -0,0 +1,19 @@
<?php
namespace App\Filament\Resources\InvoiceResource\Pages;
use App\Filament\Resources\InvoiceResource;
use Filament\Actions;
use Filament\Resources\Pages\ListRecords;
class ListInvoices extends ListRecords
{
protected static string $resource = InvoiceResource::class;
protected function getHeaderActions(): array
{
return [
Actions\CreateAction::make(),
];
}
}

@ -0,0 +1,68 @@
<?php
namespace App\Filament\Resources\InvoiceResource\RelationManagers;
use Filament\Forms;
use Filament\Forms\Form;
use Filament\Resources\RelationManagers\RelationManager;
use Filament\Tables;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
class OrdersRelationManager extends RelationManager
{
protected static string $relationship = 'orders';
public function form(Form $form): Form
{
return $form
->schema([
Forms\Components\TextInput::make('customer_po')
->required()
->maxLength(100),
]);
}
public function table(Table $table): Table
{
return $table
->recordTitleAttribute('customer_po')
->columns([
Tables\Columns\TextColumn::make('customer_po')
->color('code')
->weight('bold')
->extraHeaderAttributes(['class' => 'w-full']),
Tables\Columns\TextColumn::make('total_product_quantity')
->label('Total QTY')
->alignRight(),
Tables\Columns\TextColumn::make('total_service_price')
->alignRight()
->label('Total price')
->money('usd'),
])
->filters([
//
])
->headerActions([
Tables\Actions\AssociateAction::make()
->multiple()
->preloadRecordSelect()
->recordSelectOptionsQuery(fn (Builder $query) => $query->where('customer_id', $this->ownerRecord->customer->id))
->after(function () {
$this->ownerRecord->calculateTotals();
}),
])
->actions([
Tables\Actions\DissociateAction::make()
->after(function () {
$this->ownerRecord->calculateTotals();
}),
])
->bulkActions([
Tables\Actions\BulkActionGroup::make([
Tables\Actions\DissociateBulkAction::make(),
]),
])
->inverseRelationship('invoice');
}
}

@ -0,0 +1,64 @@
<?php
namespace App\Filament\Resources\InvoiceResource\RelationManagers;
use Filament\Forms;
use Filament\Forms\Form;
use Filament\Resources\RelationManagers\RelationManager;
use Filament\Tables;
use Filament\Tables\Table;
class ProductServicesRelationManager extends RelationManager
{
protected static string $relationship = 'ProductServices';
public function form(Form $form): Form
{
return $form
->schema([
Forms\Components\TextInput::make('id')
->required()
->maxLength(255),
]);
}
public function table(Table $table): Table
{
return $table
->recordTitleAttribute('id')
->columns([
Tables\Columns\TextColumn::make('order.internal_po')
->label('WO')
->color('primary')
->fontFamily('mono')
->sortable(),
Tables\Columns\TextColumn::make('order.customer_po')
->label('PO')
->color('code')
->weight('bold')
->sortable(),
Tables\Columns\TextColumn::make('serviceType.name')
->label('Type')
->weight('bold')
->sortable(),
Tables\Columns\TextColumn::make('service_details'),
Tables\Columns\TextColumn::make('amount')
->label('QTY'),
Tables\Columns\TextColumn::make('amount_price')
->label('Rate')
->prefix('$'),
Tables\Columns\TextColumn::make('price')
->label('Amount')
->prefix('$'),
])
->filters([
])
->headerActions([
])
->actions([
])
->bulkActions([
])
->defaultPaginationPageOption('all');
}
}

@ -0,0 +1,322 @@
<?php
namespace App\Filament\Resources;
use App\Enums\OrderAttributes;
use App\Enums\OrderStatus;
use App\Enums\OrderType;
use App\Filament\Resources\OrderResource\Pages;
use App\Models\Contact;
use App\Models\Customer;
use App\Models\Order;
use App\Models\ServiceType;
use Filament\Forms\Components\DatePicker;
use Filament\Forms\Components\Grid;
use Filament\Forms\Components\Repeater;
use Filament\Forms\Components\Section;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\Split;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\ToggleButtons;
use Filament\Forms\Form;
use Filament\Forms\Get;
use Filament\Forms\Set;
use Filament\Resources\Resource;
use Filament\Tables;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Guava\FilamentClusters\Forms\Cluster;
use Icetalker\FilamentTableRepeater\Forms\Components\TableRepeater;
use Illuminate\Database\Eloquent\Builder;
class OrderResource extends Resource
{
protected static ?string $model = Order::class;
protected static ?string $navigationIcon = 'lucide-shopping-cart';
protected static ?string $navigationGroup = 'Production';
public static function form(Form $form): Form
{
return $form->schema([
Section::make([
Grid::make(1)
->schema([
Select::make('order_type')
->required()
->options(OrderType::class)
->searchable(),
Split::make([
Select::make('customer_id')
->required()
->label('Customer')
->options(Customer::all()->pluck('company_name', 'id'))
->reactive()
->searchable(),
Select::make('contact_id')
->label('Contact')
->options(fn ($get): array => Contact::where('customer_id', $get('customer_id') ?? null)
->get()
->pluck('full_name', 'id')
->toArray())
->searchable(),
]),
TextInput::make('customer_po')
->required()
->label('Customer PO'),
Split::make([
DatePicker::make('order_date')
->required()
->default(today()),
DatePicker::make('due_date')
->required()
->default(today()->add('10 days')),
]),
Textarea::make('notes')
->rows(3),
])->columnSpan(1),
Grid::make(1)
->schema([
ToggleButtons::make('status')
->required()
->default(OrderStatus::DRAFT->value)
->options(OrderStatus::class)
->inline(),
ToggleButtons::make('order_attributes')
->options(OrderAttributes::class)
->multiple()
->inline(),
ToggleButtons::make('printed')
->boolean()
->default(false)
->inline(),
ToggleButtons::make('pre_production')
->label('Pre-production')
->default(false)
->boolean()
->inline()
->colors([
'true' => 'info',
'false' => 'info',
]),
])->columnSpan(1),
])->columns(2),
TableRepeater::make('order_products')
->label('Garments')
->schema([
TextInput::make('sku'),
TextInput::make('product_name')
->required(),
TextInput::make('color'),
Cluster::make([
TextInput::make('xs')
->placeholder('xs'),
TextInput::make('s')
->placeholder('s'),
TextInput::make('m')
->placeholder('m'),
TextInput::make('l')
->placeholder('l'),
TextInput::make('xl')
->placeholder('xl'),
TextInput::make('2xl')
->placeholder('2xl'),
TextInput::make('3xl')
->placeholder('3xl'),
TextInput::make('osfa')
->placeholder('osfa'),
])
->label('Sizes'),
])
->reorderable()
->cloneable(),
Repeater::make('services')
->label('Product Services')
->schema([
Grid::make(19)
->schema([
Select::make('serviceType')
->options(ServiceType::all()->pluck('name', 'id'))
->columnSpan(2)
->placeholder('Select...')
->searchable(),
TextInput::make('placement')
->columnSpan(3),
TextInput::make('serviceFileName')
->columnSpan(3)
->label('Logo Name'),
TextInput::make('serviceFileSetupNumber')
->label('Setup')
->columnSpan(1),
Cluster::make([
TextInput::make('serviceFileWidth')
->prefix('w'),
TextInput::make('serviceFileHeight')
->prefix('h'),
])
->label('Dimensions')
->columnSpan(4),
TextInput::make('amount')
->label('Quantity')
->live()
->reactive()
->afterStateUpdated(function ($state, Get $get, Set $set) {
$set('total_price', ($get('amount_price') * $state ?? 0));
})
->afterStateHydrated(function ($state, Get $get, Set $set) {
$set('total_price', ($get('amount_price') * $state ?? 0));
})
->prefix('#')
->columnSpan(2),
TextInput::make('amount_price')
->prefix('$')
->reactive()
->afterStateUpdated(function ($state, Get $get, Set $set) {
$set('total_price', ($get('amount') * $state ?? 0));
})
->afterStateHydrated(function ($state, Get $get, Set $set) {
$set('total_price', ($get('amount') * $state ?? 0));
})
->columnSpan(2),
TextInput::make('total_price')
->prefix('$')
->readOnly()
->columnSpan(2),
]),
Grid::make(9)
->schema([
TextInput::make('serviceFileCode')
->label('Code')
->columnSpan(1)
->placeholder('A1234'),
Textarea::make('notes')
->placeholder('Thread colors...')
->columnSpan(8),
]),
])
->reorderable()
->cloneable()
->columns(4)
->columnSpan(2),
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
Tables\Columns\IconColumn::make('alert')
->getStateUsing(fn ($record) => $record->is_alert_danger || $record->is_alert_warning)
->label('')
->color(fn ($record) => $record->is_alert_danger ? 'danger' : 'warning')
->icon(function ($record) {
return $record->is_alert_danger
? 'lucide-calendar-clock' : ($record->rush
? OrderAttributes::rush->getIcon() : null);
})
->size(Tables\Columns\IconColumn\IconColumnSize::Medium),
TextColumn::make('internal_po')
->label('Internal PO')
->fontFamily('mono')
->color('info')
->searchable()
->sortable(),
TextColumn::make('customer.company_name')
->searchable()
->sortable(),
TextColumn::make('customer_po')
->label('PO')
->wrap()
->weight('bold')
->color('code')
->searchable()
->sortable()
->extraHeaderAttributes([
'class' => 'w-full',
]),
TextColumn::make('order_date')
->searchable()
->sortable(),
TextColumn::make('due_date')
->searchable()
->sortable(),
TextColumn::make('status')
->badge()
->searchable()
->sortable(),
])
->defaultSort('order_date', 'desc')
->filters([
Tables\Filters\Filter::make('order_date')
->form([
DatePicker::make('created_from'),
DatePicker::make('created_until'),
])
->query(function (Builder $query, array $data): Builder {
return $query
->when(
$data['created_from'],
fn (Builder $query, $date): Builder => $query->whereDate('order_date', '>=', $date),
)
->when(
$data['created_until'],
fn (Builder $query, $date): Builder => $query->whereDate('order_date', '<=', $date),
);
}),
], )
->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\ListOrders::route('/'),
'create' => Pages\CreateOrder::route('/create'),
'edit' => Pages\EditOrder::route('/{record}/edit'),
];
}
}

@ -0,0 +1,82 @@
<?php
namespace App\Filament\Resources\OrderResource\Pages;
use App\Enums\OrderAttributes;
use App\Filament\Resources\OrderResource;
use App\Models\Order;
use App\Models\OrderProduct;
use App\Models\ProductService;
use App\Models\ProductSize;
use App\Models\ServiceFile;
use App\Models\ServiceType;
use Filament\Resources\Pages\CreateRecord;
class CreateOrder extends CreateRecord
{
protected static string $resource = OrderResource::class;
protected function handleRecordCreation(array $data): Order
{
// Attributes
foreach (OrderAttributes::cases() as $case) {
$data[$case->name] = false;
}
$data['order_attributes'] = array_filter($data['order_attributes']);
foreach ($data['order_attributes'] as $attribute) {
$data[OrderAttributes::from($attribute)->name] = true;
}
unset($data['order_attributes']);
$order = Order::create($data);
// Create Order Products
foreach ($data['order_products'] as $product) {
$orderProduct = OrderProduct::create([
'sku' => $product['sku'],
'product_name' => $product['product_name'],
'color' => $product['color'],
'order_id' => $order->id,
]);
$sizes = ['xs', 's', 'm', 'l', 'xl', '2xl', '3xl', 'osfa'];
foreach ($sizes as $size) {
if ($product[$size] > 0) {
ProductSize::create([
'amount' => $product[$size],
'size' => $size,
'order_product_id' => $orderProduct->id,
]);
}
}
}
// ProductServices and ServiceFiles
foreach ($data['services'] as $service) {
$serviceFile = ServiceFile::create([
'name' => strtoupper($service['serviceFileName']) ?? '',
'code' => strtoupper($service['serviceFileCode']) ?? '',
'width' => $service['serviceFileWidth'] ?? null,
'height' => $service['serviceFileHeight'] ?? null,
'setup_number' => $service['serviceFileSetupNumber'] ?? null,
]);
ProductService::create([
'service_type_id' => ServiceType::findOrFail($service['serviceType'])->id ?? null,
'placement' => strtoupper($service['placement']) ?? null,
'notes' => strtoupper($service['notes']) ?? null,
'amount' => $service['amount'] ?? null,
'amount_price' => $service['amount_price'] ?? null,
'total_price' => $service['total_price'] ?? null,
'service_file_id' => $serviceFile->id,
'order_id' => $order->id,
]);
}
return $order;
}
}

@ -0,0 +1,159 @@
<?php
namespace App\Filament\Resources\OrderResource\Pages;
use App\Enums\OrderAttributes;
use App\Filament\Resources\OrderResource;
use App\Models\Order;
use App\Models\OrderProduct;
use App\Models\ProductService;
use App\Models\ProductSize;
use App\Models\ServiceFile;
use App\Models\ServiceType;
use Filament\Actions;
use Filament\Actions\Action;
use Filament\Resources\Pages\EditRecord;
use Illuminate\Database\Eloquent\Model;
class EditOrder extends EditRecord
{
protected static string $resource = OrderResource::class;
protected function mutateFormDataBeforeFill(array $data): array
{
$order = Order::findOrFail($data['id']);
// Order Products
foreach ($order->orderProducts as $key => $product) {
$data['order_products'][$key] = [
'sku' => $product->sku,
'product_name' => $product->product_name,
'color' => $product->color,
'xs' => $product->productSizes->where('size', 'xs')->first()->amount ?? null,
's' => $product->productSizes->where('size', 's')->first()->amount ?? null,
'm' => $product->productSizes->where('size', 'm')->first()->amount ?? null,
'l' => $product->productSizes->where('size', 'l')->first()->amount ?? null,
'xl' => $product->productSizes->where('size', 'xl')->first()->amount ?? null,
'2xl' => $product->productSizes->where('size', '2xl')->first()->amount ?? null,
'3xl' => $product->productSizes->where('size', '3xl')->first()->amount ?? null,
'osfa' => $product->productSizes->where('size', 'osfa')->first()->amount ?? null,
];
}
// Product Services
foreach ($order->productServices as $key => $service) {
$data['services'][$key] = [
'placement' => $service->placement ?? '',
'amount' => $service->amount ?? '',
'amount_price' => $service->amount_price ?? '',
'notes' => $service->notes ?? '',
'serviceType' => $service->serviceType->id ?? '',
'serviceFileName' => $service->serviceFile->name ?? '',
'serviceFileWidth' => $service->serviceFile->width ?? '',
'serviceFileHeight' => $service->serviceFile->height ?? '',
'serviceFileCode' => $service->serviceFile->code ?? '',
'serviceFileSetupNumber' => $service->serviceFile->setup_number ?? '',
];
}
foreach (OrderAttributes::cases() as $case) {
if ($data[$case->name]) {
$data['order_attributes'][] = $case->value ?? null;
}
}
return $data;
}
public function handleRecordUpdate(Model $record, array $data): Model
{
// Correctly set attribute booleans
foreach (OrderAttributes::cases() as $case) {
$data[$case->name] = false;
}
$data['order_attributes'] = array_filter($data['order_attributes']);
foreach ($data['order_attributes'] as $attribute) {
$data[OrderAttributes::from($attribute)->name] = true;
}
unset($data['order_attributes']);
$record->update($data);
// Delete old and create new Order Products
foreach ($record->orderProducts as $product) {
foreach ($product->productSizes as $size) {
$size->delete();
}
$product->delete();
}
foreach ($data['order_products'] as $product) {
$orderProduct = OrderProduct::create([
'sku' => $product['sku'],
'product_name' => $product['product_name'],
'color' => $product['color'],
'order_id' => $record->id,
]);
$sizes = ['xs', 's', 'm', 'l', 'xl', '2xl', '3xl', 'osfa'];
foreach ($sizes as $size) {
if ($product[$size] > 0) {
ProductSize::create([
'amount' => $product[$size],
'size' => $size,
'order_product_id' => $orderProduct->id,
]);
}
}
}
// Delete old and create new services
foreach ($record->productServices as $service) {
$service->delete();
}
foreach ($data['services'] as $service) {
$serviceFile = ServiceFile::create([
'name' => strtoupper($service['serviceFileName']) ?? '',
'code' => strtoupper($service['serviceFileCode']) ?? '',
'width' => $service['serviceFileWidth'] ?? null,
'height' => $service['serviceFileHeight'] ?? null,
'setup_number' => $service['serviceFileSetupNumber'] ?? null,
]);
ProductService::create([
'service_type_id' => ServiceType::findOrFail($service['serviceType'])->id ?? null,
'placement' => strtoupper($service['placement']) ?? null,
'notes' => strtoupper($service['notes']) ?? null,
'amount' => $service['amount'] ?? null,
'amount_price' => $service['amount_price'] ?? null,
'total_price' => $service['total_price'] ?? null,
'service_file_id' => $serviceFile->id,
'order_id' => $record->id,
]);
}
return $record;
}
protected function getHeaderActions(): array
{
return [
Action::make('save')
->label('Save changes')
->action('save')
->icon('lucide-save'),
Action::make('print')
->icon('lucide-printer')
->url(fn (Order $record) => route('orders.pdf', $record))
->openUrlInNewTab(),
Actions\DeleteAction::make()
->icon('lucide-trash-2'),
];
}
}

@ -0,0 +1,97 @@
<?php
namespace App\Filament\Resources\OrderResource\Pages;
use App\Enums\OrderAttributes;
use App\Enums\OrderStatus;
use App\Filament\Resources\OrderResource;
use App\Models\Order;
use Filament\Actions;
use Filament\Resources\Components\Tab;
use Filament\Resources\Pages\ListRecords;
class ListOrders extends ListRecords
{
protected static string $resource = OrderResource::class;
protected function getHeaderActions(): array
{
return [
Actions\CreateAction::make(),
];
}
public function getTabs(): array
{
return [
'active' => Tab::make()
->query(function ($query) {
return $query
->whereNot('status', OrderStatus::INVOICED)
->whereNot('status', ORderStatus::SHIPPED);
})
->icon(OrderStatus::PRODUCTION->getIcon())
->badge(function () {
return Order::whereNot('status', OrderStatus::SHIPPED)
->whereNot('status', OrderStatus::INVOICED)
->count();
}),
'overdue' => Tab::make()
->query(function ($query) {
return $query->whereDate('due_date', '<=', today())
->whereNot('status', OrderStatus::INVOICED)
->whereNot('status', ORderStatus::SHIPPED);
})
->icon('lucide-calendar-clock')
->badge(function () {
$count = Order::whereDate('due_date', '<=', today())
->whereNot('status', OrderStatus::INVOICED)
->whereNot('status', ORderStatus::SHIPPED)
->count();
return $count > 0 ? $count : null;
})
->badgeColor('danger'),
'rush' => Tab::make()
->query(function ($query) {
return $query->where('rush', true)
->whereNot('status', OrderStatus::INVOICED)
->whereNot('status', OrderStatus::SHIPPED);
})
->icon(OrderAttributes::rush->getIcon())
->badge(function () {
$count = Order::where('rush', true)
->whereNot('status', OrderStatus::INVOICED)
->whereNot('status', OrderStatus::SHIPPED)
->count();
return $count > 0 ? $count : null;
})
->badgeColor('warning'),
null => Tab::make('All')
->icon('lucide-layout-grid'),
// 'draft' => Tab::make()
// ->query(fn ($query) => $query->where('status', OrderStatus::DRAFT->value))
// ->icon(OrderStatus::DRAFT->getIcon()),
//
// 'approved' => Tab::make()
// ->query(fn ($query) => $query->where('status', OrderStatus::APPROVED->value))
// ->icon(OrderStatus::APPROVED->getIcon()),
//
// 'production' => Tab::make()
// ->query(fn ($query) => $query->where('status', OrderStatus::PRODUCTION->value))
// ->icon(OrderStatus::PRODUCTION->getIcon()),
//
// 'shipped' => Tab::make()
// ->query(fn ($query) => $query->where('status', OrderStatus::SHIPPED->value))
// ->icon(OrderStatus::SHIPPED->getIcon()),
//
// 'invoiced' => Tab::make()
// ->query(fn ($query) => $query->where('status', OrderStatus::INVOICED->value))
// ->icon(OrderStatus::INVOICED->getIcon()),
];
}
}

@ -0,0 +1,11 @@
<?php
namespace App\Filament\Resources\OrderResource\Pages;
use App\Filament\Resources\OrderResource;
use Filament\Resources\Pages\ViewRecord;
class ViewOrder extends ViewRecord
{
protected static string $resource = OrderResource::class;
}

@ -0,0 +1,50 @@
<?php
namespace App\Filament\Resources\OrderResource\RelationManagers;
use Filament\Forms;
use Filament\Forms\Form;
use Filament\Resources\RelationManagers\RelationManager;
use Filament\Tables;
use Filament\Tables\Table;
class OrderProductsRelationManager extends RelationManager
{
protected static string $relationship = 'orderProducts';
public function form(Form $form): Form
{
return $form
->schema([
Forms\Components\TextInput::make('id')
->required()
->maxLength(255),
]);
}
public function table(Table $table): Table
{
return $table
->recordTitleAttribute('id')
->columns([
Tables\Columns\TextColumn::make('sku'),
Tables\Columns\TextColumn::make('product_name'),
Tables\Columns\TextColumn::make('color'),
])
->filters([
//
])
->headerActions([
Tables\Actions\CreateAction::make(),
])
->actions([
Tables\Actions\EditAction::make(),
Tables\Actions\DeleteAction::make(),
])
->bulkActions([
Tables\Actions\BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(),
]),
]);
}
}

@ -0,0 +1,96 @@
<?php
namespace App\Filament\Resources;
use App\Filament\Resources\PackingSlipResource\Pages;
use App\Models\Customer;
use App\Models\Order;
use App\Models\PackingSlip;
use Filament\Forms\Components\DatePicker;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Form;
use Filament\Resources\Resource;
use Filament\Tables;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
class PackingSlipResource extends Resource
{
protected static ?string $model = PackingSlip::class;
protected static ?string $navigationIcon = 'lucide-package';
protected static ?string $navigationGroup = 'Management';
protected static ?int $navigationSort = 2;
public static function form(Form $form): Form
{
return $form
->schema([
DatePicker::make('date_received'),
TextInput::make('amount'),
Select::make('customer_id')
->options(Customer::all()->pluck('company_name', 'id'))
->reactive()
->searchable(),
Select::make('order_id')
->options(fn ($get): array => Order::where('customer_id', $get('customer_id') ?? null)
->get()
->pluck('customer_po', 'id')
->toArray())
->searchable(),
TextArea::make('contents'),
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
TextColumn::make('date_received')
->sortable()
->searchable(),
TextColumn::make('order.customer_po')
->weight('bold')
->color('code')
->sortable()
->searchable(),
TextColumn::make('contents'),
TextColumn::make('amount'),
TextColumn::make('order.customer.company_name')
->sortable()
->searchable(),
])
->defaultSort('date_received', 'desc')
->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\ListPackingSlips::route('/'),
'create' => Pages\CreatePackingSlip::route('/create'),
'edit' => Pages\EditPackingSlip::route('/{record}/edit'),
];
}
}

@ -0,0 +1,11 @@
<?php
namespace App\Filament\Resources\PackingSlipResource\Pages;
use App\Filament\Resources\PackingSlipResource;
use Filament\Resources\Pages\CreateRecord;
class CreatePackingSlip extends CreateRecord
{
protected static string $resource = PackingSlipResource::class;
}

@ -0,0 +1,19 @@
<?php
namespace App\Filament\Resources\PackingSlipResource\Pages;
use App\Filament\Resources\PackingSlipResource;
use Filament\Actions;
use Filament\Resources\Pages\EditRecord;
class EditPackingSlip extends EditRecord
{
protected static string $resource = PackingSlipResource::class;
protected function getHeaderActions(): array
{
return [
Actions\DeleteAction::make(),
];
}
}

@ -0,0 +1,19 @@
<?php
namespace App\Filament\Resources\PackingSlipResource\Pages;
use App\Filament\Resources\PackingSlipResource;
use Filament\Actions;
use Filament\Resources\Pages\ListRecords;
class ListPackingSlips extends ListRecords
{
protected static string $resource = PackingSlipResource::class;
protected function getHeaderActions(): array
{
return [
Actions\CreateAction::make(),
];
}
}

@ -0,0 +1,102 @@
<?php
namespace App\Filament\Resources;
use App\Filament\Resources\QuoteResource\Pages;
use App\Models\Customer;
use App\Models\Order;
use App\Models\Quote;
use Filament\Forms\Components\Section;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\Split;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Form;
use Filament\Resources\Resource;
use Filament\Tables;
use Filament\Tables\Table;
class QuoteResource extends Resource
{
protected static ?string $model = Quote::class;
protected static ?string $navigationIcon = 'lucide-quote';
protected static ?string $navigationGroup = 'Production';
protected static ?int $navigationSort = 1;
public static function form(Form $form): Form
{
return $form
->schema([
Section::make([
Split::make([
Select::make('customer_id')
->required()
->label('Customer')
->options(Customer::all()->pluck('company_name', 'id'))
->reactive()
->searchable(),
Select::make('order_id')
->label('Order')
->options(fn ($get): array => Order::where('customer_id', $get('customer_id') ?? null)
->get()
->pluck('customer_po', 'id')
->toArray())
->searchable(),
])->columnSpan(2),
Textarea::make('body')
->columnSpan(2)
->rows(8),
]),
])->columns(3);
}
public static function table(Table $table): Table
{
return $table
->columns([
Tables\Columns\TextColumn::make('order.customer.company_name')
->searchable(),
Tables\Columns\TextColumn::make('order.customer_po')
->searchable()
->weight('bold')
->color('code'),
Tables\Columns\TextColumn::make('body')
->searchable()
->limit(100),
Tables\Columns\TextColumn::make('created_at')
->date('Y-m-d')
->sortable(),
])
->defaultSort('created_at', 'desc')
->groups([
'order.customer.company_name',
])
->filters([
])
->actions([
Tables\Actions\EditAction::make(),
])
->bulkActions([
]);
}
public static function getRelations(): array
{
return [
//
];
}
public static function getPages(): array
{
return [
'index' => Pages\ListQuotes::route('/'),
'create' => Pages\CreateQuote::route('/create'),
'edit' => Pages\EditQuote::route('/{record}/edit'),
];
}
}

@ -0,0 +1,11 @@
<?php
namespace App\Filament\Resources\QuoteResource\Pages;
use App\Filament\Resources\QuoteResource;
use Filament\Resources\Pages\CreateRecord;
class CreateQuote extends CreateRecord
{
protected static string $resource = QuoteResource::class;
}

@ -0,0 +1,19 @@
<?php
namespace App\Filament\Resources\QuoteResource\Pages;
use App\Filament\Resources\QuoteResource;
use Filament\Actions;
use Filament\Resources\Pages\EditRecord;
class EditQuote extends EditRecord
{
protected static string $resource = QuoteResource::class;
protected function getHeaderActions(): array
{
return [
Actions\DeleteAction::make(),
];
}
}

@ -0,0 +1,19 @@
<?php
namespace App\Filament\Resources\QuoteResource\Pages;
use App\Filament\Resources\QuoteResource;
use Filament\Actions;
use Filament\Resources\Pages\ListRecords;
class ListQuotes extends ListRecords
{
protected static string $resource = QuoteResource::class;
protected function getHeaderActions(): array
{
return [
Actions\CreateAction::make(),
];
}
}

@ -0,0 +1,125 @@
<?php
namespace App\Filament\Resources;
use App\Filament\Resources\ServiceTypeResource\Pages;
use App\Filament\Resources\ServiceTypeResource\Widgets\ServiceTypeOverview;
use App\Models\ServiceType;
use Filament\Forms\Components\DatePicker;
use Filament\Forms\Form;
use Filament\Resources\Resource;
use Filament\Tables;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Model;
class ServiceTypeResource extends Resource
{
protected static ?string $model = ServiceType::class;
protected static ?string $navigationIcon = 'heroicon-o-rectangle-stack';
protected static ?string $navigationGroup = 'Reports';
protected static ?string $label = 'Product Services';
public static function getWidgets(): array
{
return [
ServiceTypeOverview::class,
];
}
public static function form(Form $form): Form
{
return $form
->schema([
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
Tables\Columns\TextColumn::make('name')
->label('Code'),
Tables\Columns\TextColumn::make('value')
->label('Long Name')
->extraHeaderAttributes(['class' => 'w-full']),
Tables\Columns\TextColumn::make('quantity')
->alignRight()
->getStateUsing(function (Table $table, Model $record) {
return $record->getQuantityAttribute(
$table->getFilter('created_at')->getState()['created_at'],
$table->getFilter('created_until')->getState()['created_until']
);
}),
Tables\Columns\TextColumn::make('amount')
->alignRight()
->getStateUsing(function (Table $table, Model $record) {
return $record->getAmountAttribute(
$table->getFilter('created_at')->getState()['created_at'],
$table->getFilter('created_until')->getState()['created_until']
);
})
->money('usd'),
Tables\Columns\TextColumn::make('salesPercentage')
->alignRight()
->getStateUsing(function (Table $table, Model $record) {
return $record->getSalesPercentageAttribute(
$table->getFilter('created_at')->getState()['created_at'],
$table->getFilter('created_until')->getState()['created_until']
);
})
->suffix('%')
->label('% sales'),
Tables\Columns\TextColumn::make('averagePrice')
->getStateUsing(function (Table $table, Model $record) {
return $record->getAveragePriceAttribute(
$table->getFilter('created_at')->getState()['created_at'],
$table->getFilter('created_until')->getState()['created_until']
);
})
->alignRight()
->label('Average')
->prefix('$'),
])
->filters([
Tables\Filters\Filter::make('created_at')
->form([
DatePicker::make('created_at')
->label('From date'),
]),
Tables\Filters\Filter::make('created_until')
->form([
DatePicker::make('created_until')
->label('Until date'),
]),
], layout: Tables\Enums\FiltersLayout::AboveContentCollapsible)
->actions([
])
->bulkActions([
]);
}
public static function getRelations(): array
{
return [
//
];
}
public static function getPages(): array
{
return [
'index' => Pages\ListServiceTypes::route('/'),
'create' => Pages\CreateServiceType::route('/create'),
];
}
}

@ -0,0 +1,11 @@
<?php
namespace App\Filament\Resources\ServiceTypeResource\Pages;
use App\Filament\Resources\ServiceTypeResource;
use Filament\Resources\Pages\CreateRecord;
class CreateServiceType extends CreateRecord
{
protected static string $resource = ServiceTypeResource::class;
}

@ -0,0 +1,19 @@
<?php
namespace App\Filament\Resources\ServiceTypeResource\Pages;
use App\Filament\Resources\ServiceTypeResource;
use Filament\Actions;
use Filament\Resources\Pages\EditRecord;
class EditServiceType extends EditRecord
{
protected static string $resource = ServiceTypeResource::class;
protected function getHeaderActions(): array
{
return [
Actions\DeleteAction::make(),
];
}
}

@ -0,0 +1,25 @@
<?php
namespace App\Filament\Resources\ServiceTypeResource\Pages;
use App\Filament\Resources\ServiceTypeResource;
use Filament\Resources\Pages\ListRecords;
class ListServiceTypes extends ListRecords
{
protected static string $resource = ServiceTypeResource::class;
protected function getHeaderWidgets(): array
{
return [
// ServiceTypeResource\Widgets\ServiceTypeOverview::class,
];
}
protected function getHeaderActions(): array
{
return [
// Actions\CreateAction::make(),
];
}
}

@ -0,0 +1,35 @@
<?php
namespace App\Filament\Resources\ServiceTypeResource\Widgets;
use Filament\Widgets\ChartWidget;
class ServiceTypeOverview extends ChartWidget
{
protected static ?string $heading = 'Services';
protected static ?string $maxHeight = '200px';
protected function getData(): array
{
return [
'datasets' => [
[
'label' => 'Test Label',
'data' => [30, 15, 25, 30],
],
],
'labels' => [
'Test 1',
'Test 2',
'Test 3',
'Test 4',
],
];
}
protected function getType(): string
{
return 'pie';
}
}

@ -0,0 +1,155 @@
<?php
namespace App\Filament\Resources;
use App\Enums\ShippingType;
use App\Filament\Resources\ShippingEntryResource\Pages;
use App\Models\ShippingEntry;
use Filament\Forms\Components\Fieldset;
use Filament\Forms\Components\Section;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\Split;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\ToggleButtons;
use Filament\Forms\Form;
use Filament\Resources\Resource;
use Filament\Support\Enums\IconPosition;
use Filament\Tables;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Grouping\Group;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
class ShippingEntryResource extends Resource
{
protected static ?string $model = ShippingEntry::class;
protected static ?string $navigationIcon = 'lucide-truck';
protected static ?string $navigationGroup = 'Management';
protected static ?int $navigationSort = 3;
public static function form(Form $form): Form
{
return $form
->schema([
Section::make([
Fieldset::make('Primary information')
->schema([
Select::make('customer')
->relationship('customer', 'company_name')
->searchable()
->required(),
ToggleButtons::make('shipping_type')
->options(ShippingType::class)
->inline()
->required(),
TextInput::make('courier')
->placeholder('UPS, Purolator...'),
]),
Split::make([
Fieldset::make('Account Details')
->schema([
TextInput::make('account_title')
->label('Title')
->prefixIcon('lucide-folder-pen')
->placeholder('What is this account used for?')
->columnSpan(2),
TextInput::make('account_url')
->label('URL')
->prefixIcon('lucide-globe')
->placeholder('Shipping website')
->url()
->columnSpan(2),
TextInput::make('account_username')
->label('Username')
->prefixIcon('lucide-circle-user')
->placeholder('...'),
TextInput::make('account_password')
->label('Password')
->prefixIcon('lucide-key-round')
->placeholder('...'),
])->columnSpan(1),
Fieldset::make('Shipping Instructions')
->schema([
TextInput::make('info_needed')
->label('Instructions')
->prefixIcon('lucide-pencil')
->placeholder('Example: put PO on box')
->columnSpan(2),
TextInput::make('notify')
->placeholder('Who to email and CC?')
->prefixIcon('lucide-users-round')
->columnSpan(2),
TextArea::make('notes')
->placeholder('Any additional information...')
->rows(2)
->columnSpan(2),
]),
])->columnSpan(2),
])->columns(2),
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
TextColumn::make('shipping_type')
->label('Type')
->sortable(),
TextColumn::make('courier')
->url(fn ($record) => $record->account_url ?? null, shouldOpenInNewTab: true)
->icon(fn ($record) => $record->account_url ? 'lucide-external-link' : null)
->iconPosition(IconPosition::After)
->searchable(query: function (Builder $query, $search) {
return $query
->where('courier', 'like', "%{$search}%")
->orWhereHas('customer', function (Builder $query) use ($search) {
return $query->where('company_name', 'like', "%{$search}%");
});
}),
TextColumn::make('account_title'),
TextColumn::make('info_needed'),
TextColumn::make('notify'),
])
->filters([
//
])
->actions([
Tables\Actions\EditAction::make(),
])
->bulkActions([
// Tables\Actions\BulkActionGroup::make([
// Tables\Actions\DeleteBulkAction::make(),
// ]),
])
->defaultGroup(
Group::make('customer.company_name')
->titlePrefixedWithLabel(false)
);
}
public static function getRelations(): array
{
return [
//
];
}
public static function getPages(): array
{
return [
'index' => Pages\ListShippingEntries::route('/'),
'create' => Pages\CreateShippingEntry::route('/create'),
'edit' => Pages\EditShippingEntry::route('/{record}/edit'),
];
}
}

@ -0,0 +1,11 @@
<?php
namespace App\Filament\Resources\ShippingEntryResource\Pages;
use App\Filament\Resources\ShippingEntryResource;
use Filament\Resources\Pages\CreateRecord;
class CreateShippingEntry extends CreateRecord
{
protected static string $resource = ShippingEntryResource::class;
}

@ -0,0 +1,19 @@
<?php
namespace App\Filament\Resources\ShippingEntryResource\Pages;
use App\Filament\Resources\ShippingEntryResource;
use Filament\Actions;
use Filament\Resources\Pages\EditRecord;
class EditShippingEntry extends EditRecord
{
protected static string $resource = ShippingEntryResource::class;
protected function getHeaderActions(): array
{
return [
Actions\DeleteAction::make(),
];
}
}

@ -0,0 +1,19 @@
<?php
namespace App\Filament\Resources\ShippingEntryResource\Pages;
use App\Filament\Resources\ShippingEntryResource;
use Filament\Actions;
use Filament\Resources\Pages\ListRecords;
class ListShippingEntries extends ListRecords
{
protected static string $resource = ShippingEntryResource::class;
protected function getHeaderActions(): array
{
return [
Actions\CreateAction::make(),
];
}
}

@ -0,0 +1,34 @@
<?php
namespace App\Filament\Widgets;
use App\Enums\OrderStatus;
use App\Models\Order;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Filament\Widgets\TableWidget as BaseWidget;
class ActiveOrdersTable extends BaseWidget
{
protected static ?int $sort = 2;
public function table(Table $table): Table
{
return $table
->query(
Order::query()
->where('status', '!=', OrderStatus::SHIPPED)
->where('status', '!=', OrderStatus::INVOICED)
)
->columns([
TextColumn::make('customer.company_name'),
TextColumn::make('customer_po')
->color('code')
->weight('bold'),
TextColumn::make('status')
// ->color(OrderStatus::class)
->badge(),
])
->defaultPaginationPageOption(5);
}
}

@ -0,0 +1,86 @@
<?php
namespace App\Filament\Widgets;
use App\Enums\OrderStatus;
use App\Models\Order;
use Filament\Widgets\StatsOverviewWidget as BaseWidget;
use Filament\Widgets\StatsOverviewWidget\Stat;
class OrderStats extends BaseWidget
{
// protected int|string|array $columnSpan = '2';
protected function getStats(): array
{
return [
Stat::make('This Month', $this->getOrdersPast30Days())
->icon('heroicon-s-calendar')
->chartColor('success')
->chart($this->getOrdersInPast30DaysChart())
->description('New orders in the past 30 days'),
Stat::make('Active Orders', $this->getActiveOrders())
->icon('heroicon-o-arrow-path')
->description('Orders that have yet to be completed'),
Stat::make('Due Today', $this->getDueOrders())
->icon('heroicon-o-clock')
->chartColor('info')
->chart($this->getDueOrdersChart())
->description('Orders that are scheduled to be due today'),
];
}
private function getActiveOrders(): string
{
return Order::all()
->where('order_status', '!=', OrderStatus::SHIPPED)
->where('order_status', '!=', OrderStatus::INVOICED)
->count();
}
private function getOrdersPast30Days(): string
{
return Order::all()
->where('order_status', '!=', OrderStatus::SHIPPED)
->where('order_status', '!=', OrderStatus::INVOICED)
->whereBetween('created_at', [now()->startOfMonth(), now()->endOfMonth()])
->count();
}
private function getOrdersInPast30DaysChart(): array
{
$chart = [];
$points = 30;
$startDate = today()->subDays(31);
for ($i = 0; $i < $points; $i++) {
$chart[$i] = Order::where('order_date', $startDate->addDay())->count();
}
return $chart;
}
private function getDueOrders(): string
{
return Order::all()
->where('order_status', '!=', OrderStatus::SHIPPED)
->where('order_status', '!=', OrderStatus::INVOICED)
->where('due_date', '<=', now())
->count();
}
private function getDueOrdersChart(): array
{
$chart = [];
$points = 30;
$startDate = today()->subDays(31);
for ($i = 0; $i < $points; $i++) {
$chart[$i] = Order::where('due_date', $startDate->addDay())->count();
}
return $chart;
}
}

@ -0,0 +1,33 @@
<?php
namespace App\Filament\Widgets;
use App\Enums\OrderStatus;
use App\Models\Order;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Filament\Widgets\TableWidget as BaseWidget;
class RushOrdersTable extends BaseWidget
{
public function table(Table $table): Table
{
return $table
->query(
Order::query()
->where('status', '!=', OrderStatus::SHIPPED)
->where('status', '!=', OrderStatus::INVOICED)
->where('rush', true)
->orderByDesc('due_date')
)
->columns([
TextColumn::make('customer.company_name'),
TextColumn::make('customer_po')
->color('code')
->weight('bold'),
TextColumn::make('status')
->badge(),
])
->defaultPaginationPageOption(5);
}
}

@ -5,13 +5,15 @@ namespace App\Http\Controllers;
use App\Http\Requests\ContactRequest;
use App\Models\Contact;
use App\Models\Customer;
use Illuminate\Contracts\View\View;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
class ContactController extends Controller
{
public function index() {}
public function index(): void {}
public function create(Request $request)
public function create(Request $request): View
{
return view('contacts.create', [
'customers' => Customer::all(),
@ -19,20 +21,20 @@ class ContactController extends Controller
]);
}
public function store(ContactRequest $request)
public function store(ContactRequest $request): RedirectResponse
{
$contact = Contact::create($request->validated());
return redirect()->route('customers.show', [$contact->customer, 'contacts'])->with('status', 'Contact created successfully');
}
public function show($id) {}
public function show(int $id): void {}
public function edit($id) {}
public function edit(int $id): void {}
public function update(Request $request, $id) {}
public function update(Request $request, int $id): void {}
public function requestDestroy(Request $request)
public function requestDestroy(Request $request): RedirectResponse
{
$contact = Contact::find($request->get('contact'));
$contact->delete();
@ -40,5 +42,5 @@ class ContactController extends Controller
return redirect()->route('customers.show', [$contact->customer->id, 'contacts'])->with('status', 'Contact deleted successfully');
}
public function destroy($id) {}
public function destroy(int $id): void {}
}

@ -5,12 +5,16 @@ namespace App\Http\Controllers;
use App\Http\Requests\CustomerRequest;
use App\Models\Customer;
use App\Models\PackingSlip;
use Illuminate\Contracts\View\Factory;
use Illuminate\Contracts\View\View;
use Illuminate\Foundation\Application;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Carbon;
class CustomerController extends Controller
{
public function index() {}
public function index(): void {}
public function store(CustomerRequest $request)
{
@ -19,12 +23,12 @@ class CustomerController extends Controller
return redirect()->route('management.index')->with('status', 'Customer created successfully.');
}
public function create()
public function create(): Factory|View|Application|\Illuminate\View\View
{
return view('customers.create');
}
public function show(Customer $customer, ?string $tab = null)
public function show(Customer $customer, ?string $tab = null): RedirectResponse|View
{
if (! $tab) {
return redirect()->route('customers.show', [$customer, 'tab' => 'details']);
@ -40,14 +44,14 @@ class CustomerController extends Controller
]);
}
public function update(CustomerRequest $request, Customer $customer)
public function update(CustomerRequest $request, Customer $customer): RedirectResponse
{
$customer->update($request->validated());
return redirect()->route('customers.show', $customer)->with('status', 'Customer updated successfully.');
}
public function requestDestroy(Request $request)
public function requestDestroy(Request $request): RedirectResponse
{
$customer = Customer::find($request->id);
$customer->delete();
@ -55,10 +59,15 @@ class CustomerController extends Controller
return redirect()->route('management.index')->with('status', 'Customer deleted successfully.');
}
public function destroy(Customer $customer)
public function destroy(Customer $customer): RedirectResponse
{
$customer->delete();
return redirect()->route('management.index')->with('status', 'Customer deleted successfully.');
}
public function pdf(Customer $customer, ?bool $paid = false, ?string $created_from = null, ?string $created_until = null): RedirectResponse
{
dd($customer, $paid, $created_from, $created_until);
}
}

@ -0,0 +1,26 @@
<?php
namespace App\Http\Controllers;
use App\Models\Invoice;
use Spatie\Browsershot\Browsershot;
use Spatie\LaravelPdf\Facades\Pdf;
class InvoiceController extends Controller
{
public function pdf(int $id)
{
$invoice = Invoice::find($id);
$url = strtolower('invoice-'.$invoice->internal_id.'.pdf');
Pdf::view('pdf.invoice', ['invoice' => $invoice])
->withBrowsershot(function (Browsershot $browsershot) {
$browsershot->noSandbox();
})
->margins(8, 8, 15, 8)
->footerView('pdf.invoice-footer', ['invoice' => $invoice])
->save($url);
return redirect($url);
}
}

@ -3,6 +3,7 @@
namespace App\Http\Controllers;
use App\Models\Customer;
use App\Models\ServiceFile;
class ManagementController extends Controller
{
@ -15,8 +16,9 @@ class ManagementController extends Controller
}
return view('management.index', [
'customers' => Customer::all(),
'tab' => $tab,
'customers' => Customer::all(),
'serviceFiles' => ServiceFile::paginate(15),
'tab' => $tab,
]);
}
}

@ -11,8 +11,13 @@ use App\Models\OrderProduct;
use App\Models\ProductService;
use App\Models\ProductSize;
use App\Models\ServiceFile;
use Illuminate\Contracts\View\Factory;
use Illuminate\Foundation\Application;
use Illuminate\Http\Request;
use Illuminate\Support\Carbon;
use Illuminate\View\View;
use Spatie\Browsershot\Browsershot;
use Spatie\LaravelPdf\Facades\Pdf;
class OrderController extends Controller
{
@ -80,30 +85,29 @@ class OrderController extends Controller
// Create productServices
for ($i = 0; $i < count($request->get('serviceInputCount')) - 1; $i++) {
$productService = ProductService::create([
'order_id' => $order->id,
'service_type' => $request->get('service_type')[$i],
'placement' => $request->get('placement')[$i],
'setup_amount' => $request->get('setup_amount')[$i],
'amount' => $request->get('amount')[$i],
'amount_price' => $request->get('amount_price')[$i],
$serviceFile = ServiceFile::create([
'code' => $request->get('service_file_name')[$i],
'name' => $request->get('logo_name')[$i],
'width' => $request->get('service_width')[$i],
'height' => $request->get('service_height')[$i],
'setup_number' => $request->get('setup_amount')[$i],
]);
ServiceFile::create([
'product_service_id' => $productService,
'code' => $request->get('service_file_name')[$i],
'name' => $request->get('logo_name')[$i],
'width' => $request->get('service_width')[$i],
'height' => $request->get('service_height')[$i],
'unit' => $request->get('service_setup_unit')[$i],
'setup_number' => $request->get('setup_number')[$i],
ProductService::create([
'order_id' => $order->id,
'service_file_id' => $serviceFile->id,
'service_type' => $request->get('service_type')[$i],
'placement' => $request->get('placement')[$i],
'amount' => $request->get('amount')[$i],
'amount_price' => $request->get('amount_price')[$i],
'notes' => $request->get('service_notes')[$i],
]);
}
return redirect()->route('order-products.create', ['order' => $order->id]);
return redirect()->route('orders.show', $order);
}
public function show($id)
public function show(int $id): Factory|\Illuminate\Contracts\View\View|Application|View
{
return view('orders.show', [
'order' => Order::find($id),
@ -111,9 +115,25 @@ class OrderController extends Controller
]);
}
public function edit($id) {}
public function edit(int $id) {}
public function update(Request $request, $id) {}
public function destroy($id) {}
public function destroy(int $id): void {}
public function pdf(int $id)
{
$order = Order::find($id);
$url = strtolower('order-'.$order->internal_po.'.pdf');
Pdf::view('pdf.order', ['order' => $order])
->withBrowsershot(function (Browsershot $browsershot) {
$browsershot->noSandbox();
})
->margins(8, 8, 15, 8)
->footerView('pdf.order-footer', ['order' => $order])
->save($url);
return redirect($url);
}
}

@ -2,24 +2,27 @@
namespace App\Http\Controllers;
use Illuminate\Contracts\View\Factory;
use Illuminate\Foundation\Application;
use Illuminate\Http\Request;
use Illuminate\View\View;
class OrderProductController extends Controller
{
public function index() {}
public function index(): void {}
public function create()
public function create(): Factory|\Illuminate\Contracts\View\View|Application|View
{
return view('order-products.create');
}
public function store(Request $request) {}
public function store(Request $request): void {}
public function show($id) {}
public function show($id): void {}
public function edit($id) {}
public function edit($id): void {}
public function update(Request $request, $id) {}
public function update(Request $request, $id): void {}
public function destroy($id) {}
public function destroy($id): void {}
}

@ -5,15 +5,16 @@ namespace App\Http\Controllers;
use App\Http\Requests\PackingSlipRequest;
use App\Models\Customer;
use App\Models\PackingSlip;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
class PackingSlipController extends Controller
{
public function index() {}
public function index(): void {}
public function create() {}
public function create(): void {}
public function store(PackingSlipRequest $request)
public function store(PackingSlipRequest $request): RedirectResponse
{
PackingSlip::create($request->validated());
@ -27,11 +28,11 @@ class PackingSlipController extends Controller
return redirect()->back(); //todo: change to packing slips page
}
public function show($id) {}
public function show($id): void {}
public function edit($id) {}
public function edit($id): void {}
public function update(Request $request, $id) {}
public function update(Request $request, $id): void {}
public function destroy($id) {}
public function destroy($id): void {}
}

@ -0,0 +1,8 @@
<?php
namespace App\Http\Controllers;
class QuoteController extends Controller
{
//
}

@ -4,26 +4,27 @@ namespace App\Http\Controllers;
use App\Http\Requests\ShippingEntryRequest;
use App\Models\ShippingEntry;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
class ShippingEntryController extends Controller
{
public function index() {}
public function index(): void {}
public function create() {}
public function create(): void {}
public function store(ShippingEntryRequest $request)
public function store(ShippingEntryRequest $request): RedirectResponse
{
$entry = ShippingEntry::create($request->validated());
return redirect()->route('customers.show', [$entry->customer, 'tab' => 'shipping']);
}
public function show($id) {}
public function show(int $id): void {}
public function edit($id) {}
public function edit(int $id): void {}
public function update(Request $request, $id) {}
public function update(Request $request, int $id): void {}
public function destroy($id) {}
public function destroy(int $id): void {}
}

@ -6,6 +6,9 @@ use Illuminate\Foundation\Http\FormRequest;
class ContactRequest extends FormRequest
{
/**
* @return string[]
*/
public function rules(): array
{
// todo: required first name if no last name and vice versa

@ -6,18 +6,21 @@ use Illuminate\Foundation\Http\FormRequest;
class CustomerRequest extends FormRequest
{
/**
* @return array<array<string>>
*/
public function rules()
{
return [
'company_name' => 'required',
'internal_name' => 'required',
'shipping_address' => 'required',
'billing_address' => 'required',
'phone' => 'required',
'company_name' => ['required'],
'internal_name' => ['required'],
'shipping_address' => ['required'],
'billing_address' => ['required'],
'phone' => ['required'],
];
}
public function authorize()
public function authorize(): bool
{
return true;
}

@ -6,6 +6,9 @@ use Illuminate\Foundation\Http\FormRequest;
class OrderProductRequest extends FormRequest
{
/**
* @return array<array<string>>
*/
public function rules(): array
{
return [

@ -6,9 +6,13 @@ use Illuminate\Foundation\Http\FormRequest;
class OrderRequest extends FormRequest
{
/**
* @return array<array<string>>
*/
public function rules(): array
{
return [
// Order
'customer_id' => ['required', 'exists:customers,id'],
'contact_id' => ['nullable', 'exists:contacts,id'],
'customer_po' => ['required', 'string'],
@ -23,6 +27,14 @@ class OrderRequest extends FormRequest
'purchased_garments' => ['nullable'],
'customer_supplied_file' => ['nullable'],
'notes' => ['nullable'],
// Order Products
// Product Sizes
// Product Services
// Service Files
];
}

@ -6,15 +6,18 @@ use Illuminate\Foundation\Http\FormRequest;
class PackingSlipRequest extends FormRequest
{
/**
* @return string<array<string>>
*/
public function rules(): array
{
return [
'date_received' => 'required|date',
'customer_id' => 'required|exists:customers,id',
'order_id' => 'string|nullable',
'amount' => 'required|string',
'contents' => 'required|string',
'from_customer' => 'required|bool',
'date_received' => ['required|date'],
'customer_id' => ['required|exists:customers,id'],
'order_id' => ['string|nullable'],
'amount' => ['required|string'],
'contents' => ['required|string'],
'from_customer' => ['required|bool'],
];
}

@ -6,6 +6,9 @@ use Illuminate\Foundation\Http\FormRequest;
class ProductServiceRequest extends FormRequest
{
/**
* @return array<array<string>>
*/
public function rules(): array
{
return [

@ -6,6 +6,9 @@ use Illuminate\Foundation\Http\FormRequest;
class ProductSizeRequest extends FormRequest
{
/**
* @return array<array<string>>
*/
public function rules(): array
{
return [

@ -6,6 +6,9 @@ use Illuminate\Foundation\Http\FormRequest;
class ServiceFileRequest extends FormRequest
{
/**
* @return array<array<string>>
*/
public function rules(): array
{
return [

@ -6,6 +6,9 @@ use Illuminate\Foundation\Http\FormRequest;
class ShippingEntryRequest extends FormRequest
{
/**
* @return array<array<string>>
*/
public function rules(): array
{
return [

@ -5,6 +5,7 @@ namespace App\Livewire;
use App\Enums\OrderStatus;
use App\Enums\OrderType;
use App\Models\Customer;
use Illuminate\Contracts\View\View;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Carbon;
use Livewire\Component;
@ -17,18 +18,18 @@ class CreateOrder extends Component
public $contacts;
public function mount(Collection $customers)
public function mount(Collection $customers): void
{
$this->customers = $customers;
$this->contacts = $customers->first()->contacts;
}
public function getContacts()
public function getContacts(): void
{
$this->contacts = Customer::find($this->selectedCustomer)->contacts;
}
public function render()
public function render(): View
{
return view('livewire.create-order', [
'contacts' => $this->contacts,

@ -3,30 +3,44 @@
namespace App\Livewire;
use Exception;
use Illuminate\Contracts\View\Factory;
use Illuminate\Foundation\Application;
use Illuminate\Support\Collection;
use Illuminate\View\View;
use Livewire\Component;
class OrderProductsCreate extends Component
{
/** @var Collection<string, int> */
public Collection $productInputs;
/** @var Collection<string, int> */
public Collection $serviceInputs;
/** @var array<int> */
public array $sizes = [];
/** @var array<int> */
public array $totals = [];
/** @var array<int> */
public array $units = [];
/**
* @var array<int>
*/
public array $prices = [];
/**
* @var array<int>
*/
public array $priceTotals = [];
public int $totalQuantity = 0;
public string $totalPrice = '$0.00';
public function updated()
public function updated(): void
{
try {
foreach ($this->sizes as $index => $size) {
@ -48,7 +62,7 @@ class OrderProductsCreate extends Component
}
public function addProductInput()
public function addProductInput(): void
{
$index = $this->productInputs->count();
$this->productInputs->push([
@ -69,28 +83,28 @@ class OrderProductsCreate extends Component
]);
}
public function determineAddProductRow($index)
public function determineAddProductRow(int $index): void
{
if ($index == $this->productInputs->count() - 1) {
$this->addProductInput();
}
}
public function determineAddServiceProductRow($index)
public function determineAddServiceProductRow(int $index): void
{
if ($index == $this->serviceInputs->count() - 1) {
$this->addServiceInput();
}
}
public function removeProductInput($key)
public function removeProductInput(int $key): void
{
if ($this->productInputs->count() > 1) {
$this->productInputs->pull($key);
}
}
public function addServiceInput()
public function addServiceInput(): void
{
$this->serviceInputs->push([
$this->serviceInputs->count() => [
@ -108,14 +122,14 @@ class OrderProductsCreate extends Component
]);
}
public function removeServiceInput($key)
public function removeServiceInput(int $key): void
{
if ($this->serviceInputs->count() > 1) {
$this->serviceInputs->pull($key);
}
}
public function mount()
public function mount(): void
{
$this->fill([
'productInputs' => collect([
@ -151,7 +165,7 @@ class OrderProductsCreate extends Component
]);
}
public function render()
public function render(): \Illuminate\Contracts\View\View|Factory|Application|View
{
return view('livewire.order-products-create');
}

@ -3,7 +3,10 @@
namespace App\Livewire;
use App\Models\Order;
use Illuminate\Contracts\View\Factory;
use Illuminate\Foundation\Application;
use Illuminate\Support\Carbon;
use Illuminate\View\View;
use Livewire\Component;
use Livewire\WithPagination;
@ -11,7 +14,7 @@ class OrdersTable extends Component
{
use WithPagination;
protected $paginationTheme = 'bootstrap';
protected string $paginationTheme = 'bootstrap';
public bool $showCustomerColumn;
@ -25,7 +28,7 @@ class OrdersTable extends Component
public Carbon $today;
public function mount(bool $showCustomerColumn, string $orderType, string $title, ?string $customer_id = null)
public function mount(bool $showCustomerColumn, string $orderType, string $title, ?string $customer_id = null): void
{
$this->today = Carbon::today();
$this->showCustomerColumn = $showCustomerColumn;
@ -34,7 +37,7 @@ class OrdersTable extends Component
$this->customer_id = $customer_id ?? '';
}
public function render()
public function render(): \Illuminate\Contracts\View\View|Factory|Application|View
{
return view('livewire.orders-table', [
'orders' => Order::with('customer')

@ -2,6 +2,7 @@
namespace App\Models;
use Database\Factories\ContactFactory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
@ -9,6 +10,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
class Contact extends Model
{
/** @use HasFactory<ContactFactory> */
use HasFactory, SoftDeletes;
protected $fillable = [
@ -25,6 +27,9 @@ class Contact extends Model
return $this->first_name.' '.$this->last_name;
}
/**
* @return BelongsTo<Customer, self>
*/
public function customer(): BelongsTo
{
return $this->belongsTo(Customer::class);

@ -2,6 +2,7 @@
namespace App\Models;
use Database\Factories\CustomerFactory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
@ -9,33 +10,93 @@ use Illuminate\Database\Eloquent\SoftDeletes;
class Customer extends Model
{
/** @use HasFactory<CustomerFactory> */
use HasFactory, SoftDeletes;
protected $fillable = [
'company_name',
'internal_name',
'shipping_address',
'billing_address',
'shipping_address_line_1',
'shipping_address_line_2',
'billing_address_line_1',
'billing_address_line_2',
'phone',
];
protected $appends = [
'subtotal',
'gst',
'pst',
'total',
];
public function getSubtotalAttribute($created_at = null, $created_until = null): float
{
return $this->invoices()
->when($created_at, fn ($query) => $query->whereDate('created_at', '>=', $created_at))
->when($created_until, fn ($query) => $query->whereDate('created_at', '<=', $created_until))
->sum('subtotal');
}
public function getGstAttribute($created_at = null, $created_until = null): float
{
return $this->invoices()
->when($created_at, fn ($query) => $query->whereDate('created_at', '>=', $created_at))
->when($created_until, fn ($query) => $query->whereDate('created_at', '<=', $created_until))
->sum('total') * 0.05;
}
public function getPstAttribute($created_at = null, $created_until = null): float
{
return $this->invoices()
->when($created_at, fn ($query) => $query->whereDate('created_at', '>=', $created_at))
->when($created_until, fn ($query) => $query->whereDate('created_at', '<=', $created_until))
->where('pst', true)
->sum('total') * 0.07;
}
public function getTotalAttribute($created_at = null, $created_until = null): float
{
return $this->invoices()
->when($created_at, fn ($query) => $query->whereDate('created_at', '>=', $created_at))
->when($created_until, fn ($query) => $query->whereDate('created_at', '<=', $created_until))
->sum('total');
}
/**
* @return HasMany<Contact>
*/
public function contacts(): HasMany
{
return $this->hasMany(Contact::class);
}
/**
* @return HasMany<PackingSlip>
*/
public function packingSlips(): HasMany
{
return $this->hasMany(PackingSlip::class);
}
/**
* @return HasMany<ShippingEntry>
*/
public function shippingEntries(): HasMany
{
return $this->hasMany(ShippingEntry::class);
}
/**
* @return HasMany<Order>
*/
public function orders(): HasMany
{
return $this->hasMany(Order::class);
}
public function invoices(): HasMany
{
return $this->hasMany(Invoice::class);
}
}

@ -0,0 +1,111 @@
<?php
namespace App\Models;
use App\Enums\InvoiceStatus;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
class Invoice extends Model
{
use HasFactory;
protected $fillable = [
'customer_id',
'internal_id',
'status',
'subtotal',
'total',
'gst',
'pst',
'date',
];
protected $appends = [
'gst_amount',
'pst_amount',
];
protected $casts = [
'date' => 'datetime',
'total' => 'decimal:2',
'subtotal' => 'decimal:2',
'status' => InvoiceStatus::class,
];
public static function boot(): void
{
parent::boot();
static::created(function ($model) {
$model->attributes['internal_id'] = $model->generateInternalId($model->id);
$model->save();
});
}
public function setStatus(InvoiceStatus $status)
{
if ($this->status !== $status) {
$this->status = $status;
$this->save();
}
}
public function generateInternalId(int $id): string
{
$po = str_pad(strval($id), 4, '0', STR_PAD_LEFT);
$year = date('y');
return 'TN-IN-'.$year.'-'.$po;
}
public function calculateTotals(): void
{
$subtotal = 0;
foreach ($this->orders as $order) {
$subtotal += $order->total_service_price;
}
$this->subtotal = $subtotal;
$this->total = $subtotal + $this->gst_amount + $this->pst_amount;
$this->save();
}
public function getGstAmountAttribute(): float
{
if ($this->gst) {
return number_format($this->subtotal * 0.05, 2);
}
return 0.00;
}
public function getPstAmountAttribute(): float
{
if ($this->pst) {
return number_format($this->subtotal * 0.07, 2);
}
return 0.00;
}
public function orders(): HasMany
{
return $this->HasMany(Order::class);
}
public function customer(): BelongsTo
{
return $this->belongsTo(Customer::class);
}
public function productServices(): HasManyThrough
{
return $this->hasManyThrough(ProductService::class, Order::class);
}
}

@ -3,15 +3,21 @@
namespace App\Models;
use App\Enums\OrderStatus;
use App\Enums\OrderType;
use Database\Factories\OrderFactory;
use DateTimeInterface;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Carbon;
class Order extends Model
{
/** @use HasFactory<OrderFactory> */
use HasFactory, SoftDeletes;
protected $fillable = [
@ -19,24 +25,36 @@ class Order extends Model
'contact_id',
'internal_po',
'customer_po',
'invoice_id',
'order_date',
'order_type',
'status',
'due_date',
'notes',
'rush',
'repeat',
'new_art',
'event',
'digitizing',
'repeat',
'purchased_garments',
'customer_supplied_file',
'notes',
'garments',
'supplied_file',
'printed',
'pre_production',
];
protected $appends = [
'active',
'total_service_price',
'total_product_quantity',
'is_alert_warning',
'is_alert_danger',
];
protected $casts = [
'status' => OrderStatus::class,
'order_type' => OrderType::class,
];
public static function boot()
public static function boot(): void
{
parent::boot();
@ -46,14 +64,60 @@ class Order extends Model
});
}
public function generateInternalPo($id): string
public function dueDatePdf(): string
{
return Carbon::createFromDate($this->due_date)->format('M d, Y');
}
public function orderDatePdf(): string
{
return Carbon::createFromDate($this->order_date)->format('M d, Y');
}
public function generateInternalPo(int $id): string
{
$po = str_pad($id, 4, '0', STR_PAD_LEFT);
$po = str_pad(strval($id), 4, '0', STR_PAD_LEFT);
$year = date('y');
return 'TN'.$year.'-'.$po;
}
public function getIsAlertWarningAttribute(): bool
{
if ($this->rush) {
return ! ($this->status === OrderStatus::INVOICED || $this->status === OrderStatus::SHIPPED);
}
return false;
}
public function getIsAlertDangerAttribute(): bool
{
return $this->due_date <= today() && $this->status !== OrderStatus::INVOICED && $this->status !== OrderStatus::SHIPPED;
}
public function getTotalProductQuantityAttribute(): int
{
$total = 0;
foreach ($this->orderProducts as $product) {
$total += $product->totalQuantity();
}
return $total;
}
public function getTotalServicePriceAttribute(): float
{
$total = 0;
foreach ($this->productServices as $service) {
$total += $service->amount * $service->amount_price;
}
return number_format($total, 2);
}
public function active(): bool
{
if ($this->status == OrderStatus::APPROVED
@ -64,49 +128,93 @@ class Order extends Model
return false;
}
public function scopeActive($query)
/**
* @param Builder<Order> $query
* @return Builder<Order>
*/
public function scopeActive(Builder $query): Builder
{
return $query->where('status', 'approved')
->orWhere('status', 'production');
}
public function scopeFinished($query)
/**
* @param Builder<Order> $query
* @return Builder<Order>
*/
public function scopeFinished(Builder $query): Builder
{
return $query->where('status', 'shipped')
->orWhere('status', 'completed');
}
public function scopeInvoiced($query)
/**
* @param Builder<Order> $query
* @return Builder<Order>
*/
public function scopeInvoiced(Builder $query): Builder
{
return $query->where('status', 'invoiced');
}
public function scopeRush($query)
/**
* @param Builder<Order> $query
* @return Builder<Order>
*/
public function scopeRush(Builder $query): Builder
{
return $query->where('rush', true);
}
/**
* @return BelongsTo<Customer, self>
*/
public function customer(): BelongsTo
{
return $this->belongsTo(Customer::class);
}
/**
* @return BelongsTo<Contact, self>
*/
public function contact(): BelongsTo
{
return $this->belongsTo(Contact::class);
}
/**
* @return HasMany<OrderProduct>
*/
public function orderProducts(): HasMany
{
return $this->hasMany(OrderProduct::class);
}
/**
* @return HasMany<ProductService>
*/
public function productServices(): HasMany
{
return $this->hasMany(ProductService::class);
}
public function packingSlips(): HasMany
{
return $this->hasMany(PackingSlip::class);
}
public function quote(): HasOne
{
return $this->hasOne(Quote::class);
}
public function invoice(): BelongsTo
{
return $this->belongsTo(Invoice::class);
}
protected function serializeDate(DateTimeInterface $date): string
{
return $date->format('Y-m-d');
}
protected $casts = [
'status' => OrderStatus::class,
];
}

@ -2,15 +2,16 @@
namespace App\Models;
use Database\Factories\OrderProductFactory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\SoftDeletes;
class OrderProduct extends Model
{
/** @use HasFactory<OrderProductFactory> */
use HasFactory, SoftDeletes;
protected $fillable = [
@ -20,17 +21,23 @@ class OrderProduct extends Model
'color',
];
public function order(): BelongsTo
public function totalQuantity(): int
{
return $this->belongsTo(Order::class);
return array_sum($this->productSizes()->pluck('amount')->toArray());
}
public function serviceFile(): HasOne
/**
* @return BelongsTo<Order, self>
*/
public function order(): BelongsTo
{
return $this->hasOne(ServiceFile::class);
return $this->belongsTo(Order::class);
}
public function productSize(): HasMany
/**
* @return HasMany<ProductSize>
*/
public function productSizes(): HasMany
{
return $this->hasMany(ProductSize::class);
}

@ -2,6 +2,7 @@
namespace App\Models;
use Database\Factories\PackingSlipFactory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
@ -9,6 +10,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
class PackingSlip extends Model
{
/** @use HasFactory<PackingSlipFactory> */
use HasFactory, SoftDeletes;
protected $fillable = [
@ -19,8 +21,19 @@ class PackingSlip extends Model
'contents',
];
/**
* @return BelongsTo<Customer, self>
*/
public function customer(): BelongsTo
{
return $this->belongsTo(Customer::class);
}
/**
* @return BelongsTo<Order, self>)
*/
public function order(): BelongsTo
{
return $this->belongsTo(Order::class);
}
}

@ -2,32 +2,62 @@
namespace App\Models;
use Database\Factories\ProductServiceFactory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\SoftDeletes;
class ProductService extends Model
{
/** @use HasFactory<ProductServiceFactory> */
use HasFactory, SoftDeletes;
protected $fillable = [
'order_id',
'service_type',
'service_file_id',
'service_type_id',
'placement',
'setup_amount',
'amount',
'amount_price',
'notes',
];
protected $appends = [
'service_details',
'price',
];
// public function getServiceType(): string
// {
// return $this->serviceType->name ?? '';
// }
public function getPriceAttribute(): float
{
return number_format($this->amount * $this->amount_price, 2);
}
public function getServiceDetailsAttribute(): string
{
$file = $this->serviceFile;
return $file->name.', '.$this->placement.', '.$file->width.' W, '.$file->height.' H';
}
public function serviceType(): BelongsTo
{
return $this->belongsTo(ServiceType::class);
}
public function order(): BelongsTo
{
return $this->belongsTo(Order::class);
}
public function serviceFile(): HasOne
public function serviceFile(): BelongsTo
{
return $this->hasOne(ServiceFile::class);
return $this->BelongsTo(ServiceFile::class);
}
}

@ -2,6 +2,7 @@
namespace App\Models;
use Database\Factories\ProductSizeFactory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
@ -9,6 +10,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
class ProductSize extends Model
{
/** @use HasFactory<ProductSizeFactory> */
use HasFactory, SoftDeletes;
protected $fillable = [
@ -17,6 +19,9 @@ class ProductSize extends Model
'amount',
];
/**
* @return BelongsTo<OrderProduct, self>
*/
public function orderProduct(): BelongsTo
{
return $this->belongsTo(OrderProduct::class);

@ -0,0 +1,22 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Quote extends Model
{
use HasFactory;
protected $fillable = [
'body',
'order_id',
];
public function order(): BelongsTo
{
return $this->belongsTo(Order::class);
}
}

@ -2,27 +2,30 @@
namespace App\Models;
use Database\Factories\ServiceFileFactory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
class ServiceFile extends Model
{
/** @use HasFactory<ServiceFileFactory> */
use HasFactory, SoftDeletes;
protected $fillable = [
'code',
'product_service_id',
'name',
'width',
'height',
'unit',
'setup_number',
];
public function productService(): BelongsTo
/**
* @return HasMany<ProductService>
*/
public function productServices(): HasMany
{
return $this->belongsTo(ProductService::class);
return $this->HasMany(ProductService::class);
}
}

@ -0,0 +1,78 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
class ServiceType extends Model
{
use HasFactory;
protected $fillable = [
'name',
'value',
];
protected $appends = [
'quantity',
'amount',
'sales_percentage',
'average_price',
];
public function getQuantityAttribute($created_at = null, $created_until = null): int
{
return $this->productServices()
->when($created_at, fn ($query) => $query->whereDate('created_at', '>=', $created_at))
->when($created_until, fn ($query) => $query->whereDate('created_at', '<=', $created_until))
->sum('amount');
}
public function getAmountAttribute($created_at = null, $created_until = null): float
{
return $this->productServices()
->when($created_at, fn ($query) => $query->whereDate('created_at', '>=', $created_at))
->when($created_until, fn ($query) => $query->whereDate('created_at', '<=', $created_until))
->sum('amount_price');
}
public function getSalesPercentageAttribute($created_at = null, $created_until = null): float
{
$query = ProductService::query()
->when($created_at, fn ($query) => $query->whereDate('created_at', '>=', $created_at))
->when($created_until, fn ($query) => $query->whereDate('created_until', '<=', $created_until));
$total = $query->count();
$part = $query->where('service_type_id', $this->id)->count();
if ($total == 0) {
return 0.0;
}
return round(($part / $total) * 100, 1);
}
public function getAveragePriceAttribute($created_at = null, $created_until = null): string
{
$quantity = $this->getQuantityAttribute($created_at, $created_until);
$amount = $this->getAmountAttribute($created_at, $created_until);
if ($quantity == 0) {
return '0.0';
}
try {
return number_format($quantity / $amount, 2);
} catch (\Exception $exception) {
dd($quantity, $amount, $exception);
}
}
public function productServices(): HasMany
{
return $this->hasMany(ProductService::class);
}
}

@ -3,6 +3,7 @@
namespace App\Models;
use App\Enums\ShippingType;
use Database\Factories\ShippingEntryFactory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
@ -10,6 +11,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
class ShippingEntry extends Model
{
/** @use HasFactory<ShippingEntryFactory> */
use HasFactory, SoftDeletes;
protected $casts = [
@ -22,6 +24,7 @@ class ShippingEntry extends Model
'courier',
'contact',
'account_title',
'account_url',
'account_username',
'account_password',
'info_needed',
@ -29,6 +32,9 @@ class ShippingEntry extends Model
'notes',
];
/**
* @return BelongsTo<Customer,self>
*/
public function customer(): BelongsTo
{
return $this->belongsTo(Customer::class);

@ -3,12 +3,14 @@
namespace App\Models;
// use Illuminate\Contracts\Auth\MustVerifyEmail;
use Database\Factories\UserFactory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
class User extends Authenticatable
{
/** @use HasFactory<UserFactory> */
use HasFactory, Notifiable;
/**

@ -0,0 +1,57 @@
<?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 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;
class AdminPanelProvider extends PanelProvider
{
public function panel(Panel $panel): Panel
{
return $panel
->default()
->id('admin')
->path('admin')
->login()
->colors([
'primary' => Color::Blue,
'code' => Color::hex('#d63384'),
'invoiced' => Color::hex('#900090'),
])
->discoverResources(in: app_path('Filament/Resources'), for: 'App\\Filament\\Resources')
->discoverPages(in: app_path('Filament/Pages'), for: 'App\\Filament\\Pages')
->pages([
Pages\Dashboard::class,
])
->discoverWidgets(in: app_path('Filament/Widgets'), for: 'App\\Filament\\Widgets')
->widgets([])
->middleware([
EncryptCookies::class,
AddQueuedCookiesToResponse::class,
StartSession::class,
AuthenticateSession::class,
ShareErrorsFromSession::class,
VerifyCsrfToken::class,
SubstituteBindings::class,
DisableBladeIconComponents::class,
DispatchServingFilamentEvent::class,
])
->authMiddleware([
Authenticate::class,
])
->sidebarWidth('13rem');
}
}

@ -2,4 +2,5 @@
return [
App\Providers\AppServiceProvider::class,
App\Providers\Filament\AdminPanelProvider::class,
];

@ -7,18 +7,26 @@
"require": {
"php": "^8.2",
"davidhsianturi/blade-bootstrap-icons": "^1.5",
"filament/filament": "^3.2",
"guava/filament-clusters": "^1.4",
"icetalker/filament-table-repeater": "^1.3",
"laravel/framework": "^11.9",
"laravel/tinker": "^2.9",
"livewire/livewire": "^3.5"
"livewire/livewire": "^3.5",
"mallardduck/blade-lucide-icons": "^1.23",
"spatie/laravel-pdf": "^1.5"
},
"require-dev": {
"fakerphp/faker": "^1.23",
"larastan/larastan": "^2.0",
"laravel/pint": "^1.17",
"laravel/sail": "^1.26",
"laravel/ui": "^4.5",
"mockery/mockery": "^1.6",
"nunomaduro/collision": "^8.0",
"phpunit/phpunit": "^11.0.1"
"pestphp/pest": "^3.5",
"pestphp/pest-plugin-laravel": "^3.0",
"pestphp/pest-plugin-livewire": "^3.0"
},
"autoload": {
"psr-4": {
@ -35,7 +43,8 @@
"scripts": {
"post-autoload-dump": [
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
"@php artisan package:discover --ansi"
"@php artisan package:discover --ansi",
"@php artisan filament:upgrade"
],
"post-update-cmd": [
"@php artisan vendor:publish --tag=laravel-assets --ansi --force"

6840
composer.lock generated

File diff suppressed because it is too large Load Diff

@ -62,6 +62,23 @@ return [
]) : [],
],
'testing' => [
'driver' => 'mysql',
'url' => env('DATABASE_URL'),
'host' => env('DB_TEST_HOST', '127.0.0.1'),
'port' => env('DB_TEST_PORT', '3306'),
'database' => env('DB_TEST_DATABASE', 'forge'),
'username' => env('DB_TEST_USERNAME', 'forge'),
'password' => env('DB_TEST_PASSWORD', ''),
'unix_socket' => env('DB_SOCKET', ''),
'charset' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci',
'prefix' => '',
'prefix_indexes' => true,
'strict' => true,
'engine' => null,
],
'mariadb' => [
'driver' => 'mariadb',
'url' => env('DB_URL'),

@ -0,0 +1,89 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Broadcasting
|--------------------------------------------------------------------------
|
| By uncommenting the Laravel Echo configuration, you may connect Filament
| to any Pusher-compatible websockets server.
|
| This will allow your users to receive real-time notifications.
|
*/
'broadcasting' => [
// 'echo' => [
// 'broadcaster' => 'pusher',
// 'key' => env('VITE_PUSHER_APP_KEY'),
// 'cluster' => env('VITE_PUSHER_APP_CLUSTER'),
// 'wsHost' => env('VITE_PUSHER_HOST'),
// 'wsPort' => env('VITE_PUSHER_PORT'),
// 'wssPort' => env('VITE_PUSHER_PORT'),
// 'authEndpoint' => '/broadcasting/auth',
// 'disableStats' => true,
// 'encrypted' => true,
// 'forceTLS' => true,
// ],
],
/*
|--------------------------------------------------------------------------
| Default Filesystem Disk
|--------------------------------------------------------------------------
|
| This is the storage disk Filament will use to store files. You may use
| any of the disks defined in the `config/filesystems.php`.
|
*/
'default_filesystem_disk' => env('FILAMENT_FILESYSTEM_DISK', 'public'),
/*
|--------------------------------------------------------------------------
| Assets Path
|--------------------------------------------------------------------------
|
| This is the directory where Filament's assets will be published to. It
| is relative to the `public` directory of your Laravel application.
|
| After changing the path, you should run `php artisan filament:assets`.
|
*/
'assets_path' => null,
/*
|--------------------------------------------------------------------------
| Cache Path
|--------------------------------------------------------------------------
|
| This is the directory that Filament will use to store cache files that
| are used to optimize the registration of components.
|
| After changing the path, you should run `php artisan filament:cache-components`.
|
*/
'cache_path' => base_path('bootstrap/cache/filament'),
/*
|--------------------------------------------------------------------------
| Livewire Loading Delay
|--------------------------------------------------------------------------
|
| This sets the delay before loading indicators appear.
|
| Setting this to 'none' makes indicators appear immediately, which can be
| desirable for high-latency connections. Setting it to 'default' applies
| Livewire's standard 200ms delay.
|
*/
'livewire_loading_delay' => 'default',
];

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save