Compare commits

...

28 Commits

Author SHA1 Message Date
d2c72a3f33 WIP #129 Product count sum 2025-04-11 17:31:13 -04:00
3dc4b98959 #148: Change amount label to unit price on order form 2025-04-11 16:29:25 -04:00
b6592d7945 #134: Fix Dashboard 'in the past 30 days' 2025-04-11 16:27:41 -04:00
bc63a4074b #147: Resource Lock sidebar settings 2025-04-11 16:27:41 -04:00
2d5688b66e #132: Inches / cm toggle for order page
Also shows size unit on PDFs
2025-04-11 16:27:41 -04:00
f02fb63bfd Merge branch 'main' into development 2025-04-10 18:08:57 +02:00
6fdf471463 #145: Default order sort is not right 2025-04-10 12:08:28 -04:00
6be8998e3a Pint fixes 2025-04-10 11:59:06 -04:00
671a2950c7 Squash merge fix into main 2025-04-10 11:41:46 -04:00
2461a4457a Merge pull request 'Version 20250408' (#143) from development into main
Reviewed-on: #143
2025-04-08 19:49:17 +02:00
e898f3d884 #142 Packing Slip content field should be required 2025-04-08 13:22:47 -04:00
a188c34409 #136: Add new filters to order list 2025-04-08 13:03:04 -04:00
081be2f930 #138: Add decoration type to order list 2025-04-08 12:45:48 -04:00
153014dba8 #130: Improve service colors on printed PDFs 2025-04-07 20:19:12 -04:00
51f21f94c1 #135: Add create customer modal to packing slip page 2025-04-07 20:02:31 -04:00
4eb94a736a Fix InvoiceTest and add placeholder file for Feature folder 2025-04-07 19:52:21 -04:00
07c250eaac Fix product name nullability in migration 2025-04-07 19:44:04 -04:00
4500ce0c27 Merge pull request 'development' (#139) from development into main
Reviewed-on: #139
2025-04-08 01:38:08 +02:00
f188063967 Disable workflow 2025-04-07 19:33:45 -04:00
bcd5c6cdef #133: Make product name not required 2025-04-07 11:07:14 -04:00
86e48f8e16 #127 Implement Resource Lock 2025-04-03 16:27:46 -04:00
63d5f427ad #125: 'Add Payment' on List Invoice page modal is too wide 2025-04-03 11:58:16 -04:00
5de1726ce8 #124: Product Service reorder button is on wrong side 2025-04-03 11:55:21 -04:00
01f602f8d6 #123: Typo 'Add to product Services' 2025-04-03 11:52:04 -04:00
320fc0cc98 #122: Non-admins can see payments in customer edit page 2025-04-03 11:52:04 -04:00
4e952f96c0 add local env file
Some checks failed
Deploy / deploy (push) Failing after 9s
2025-04-02 15:08:10 -04:00
43eebd9528 Merge branch 'development'
Some checks failed
Deploy / deploy (push) Failing after 8s
2025-03-11 12:52:46 -04:00
3defbd8253 Merge pull request 'Version 20250311' (#121) from development into main
Some checks failed
Deploy / deploy (push) Failing after 6s
Reviewed-on: nisse/topnotch_website#121
2025-03-11 17:33:40 +01:00
29 changed files with 492 additions and 69 deletions

View File

@ -1,9 +1,9 @@
APP_NAME=Laravel APP_NAME="Top Notch"
APP_ENV=local APP_ENV=local
APP_KEY= APP_KEY=base64:vRgghlbIdXQxXIEvgUArbI9FURhgdyqx3LDXDwHYSmA=
APP_DEBUG=true APP_DEBUG=true
APP_TIMEZONE=UTC APP_TIMEZONE=UTC
APP_URL=http://localhost APP_URL=localhost
APP_LOCALE=en APP_LOCALE=en
APP_FALLBACK_LOCALE=en APP_FALLBACK_LOCALE=en
@ -19,12 +19,12 @@ LOG_STACK=single
LOG_DEPRECATIONS_CHANNEL=null LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=debug LOG_LEVEL=debug
DB_CONNECTION=sqlite DB_CONNECTION=mysql
# DB_HOST=127.0.0.1 DB_HOST=mysql
# DB_PORT=3306 DB_PORT=3306
# DB_DATABASE=laravel DB_DATABASE=laravel
# DB_USERNAME=root DB_USERNAME=sail
# DB_PASSWORD= DB_PASSWORD=password
SESSION_DRIVER=database SESSION_DRIVER=database
SESSION_LIFETIME=120 SESSION_LIFETIME=120
@ -42,13 +42,13 @@ CACHE_PREFIX=
MEMCACHED_HOST=127.0.0.1 MEMCACHED_HOST=127.0.0.1
REDIS_CLIENT=phpredis REDIS_CLIENT=phpredis
REDIS_HOST=127.0.0.1 REDIS_HOST=redis
REDIS_PASSWORD=null REDIS_PASSWORD=null
REDIS_PORT=6379 REDIS_PORT=6379
MAIL_MAILER=log MAIL_MAILER=smtp
MAIL_HOST=127.0.0.1 MAIL_HOST=mailpit
MAIL_PORT=2525 MAIL_PORT=1025
MAIL_USERNAME=null MAIL_USERNAME=null
MAIL_PASSWORD=null MAIL_PASSWORD=null
MAIL_ENCRYPTION=null MAIL_ENCRYPTION=null
@ -62,3 +62,8 @@ AWS_BUCKET=
AWS_USE_PATH_STYLE_ENDPOINT=false AWS_USE_PATH_STYLE_ENDPOINT=false
VITE_APP_NAME="${APP_NAME}" VITE_APP_NAME="${APP_NAME}"
SCOUT_DRIVER=meilisearch
MEILISEARCH_HOST=http://meilisearch:7700
MEILISEARCH_NO_ANALYTICS=false

View File

@ -1,4 +1,18 @@
# Changelog # Changelog
**2025-04-11**
- Fixed #132 - Inches / cm toggle for order page (and show unit on pdf)
**2025-04-10**
- Fixed #145 - Default order sort is incorrect (newest will always show on top now)
- Fixed #142 - Packing Slip content field should be required
**2025-04-08**
- Added #138 - Add Decoration method to order list
- Fixed #133 - Make product name not required
- Fixed #127 - Implement resource lock for orders
- Fixed #125 - 'Add Payment' on LIst Invoice page modal is too wide
- Fixed #124 - Reorder button for product services is displayed on wrong side
- Fixed #123 - Typo 'Add to product Services'
**2025-03-11** **2025-03-11**
- Fixed #122 - Non-admins can see payments - Fixed #122 - Non-admins can see payments

View File

@ -2,9 +2,10 @@
namespace App\Enums; namespace App\Enums;
use Filament\Support\Contracts\HasColor;
use Filament\Support\Contracts\HasLabel; use Filament\Support\Contracts\HasLabel;
enum OrderType: string implements HasLabel enum OrderType: string implements HasColor, HasLabel
{ {
case EMB = 'embroidery'; case EMB = 'embroidery';
case SCP = 'screen_printing'; case SCP = 'screen_printing';
@ -22,4 +23,15 @@ public function getLabel(): string
self::MISC => 'Misc', self::MISC => 'Misc',
}; };
} }
public function getColor(): string
{
return match ($this) {
self::EMB => 'embroidery',
self::SCP => 'screen_printing',
self::DTG => 'primary',
self::VINYL => 'vinyl',
self::MISC => 'misc',
};
}
} }

View File

@ -249,6 +249,7 @@ public static function table(Table $table): Table
BulkAction::make('Create Payment') BulkAction::make('Create Payment')
->icon(IconEnum::PAYMENTS->value) ->icon(IconEnum::PAYMENTS->value)
->form(fn ($form) => PaymentResource::form($form)) ->form(fn ($form) => PaymentResource::form($form))
->modalWidth('lg')
->action(function (Collection $records, array $data) { ->action(function (Collection $records, array $data) {
if ($records->pluck('customer_id')->unique()->count() !== 1) { if ($records->pluck('customer_id')->unique()->count() !== 1) {
Notification::make() Notification::make()

View File

@ -33,12 +33,29 @@
use Filament\Tables\Actions\BulkActionGroup; use Filament\Tables\Actions\BulkActionGroup;
use Filament\Tables\Columns\IconColumn\IconColumnSize; use Filament\Tables\Columns\IconColumn\IconColumnSize;
use Filament\Tables\Columns\TextColumn; use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Filters\SelectFilter;
use Filament\Tables\Table; use Filament\Tables\Table;
use Guava\FilamentClusters\Forms\Cluster; use Guava\FilamentClusters\Forms\Cluster;
use Icetalker\FilamentTableRepeater\Forms\Components\TableRepeater; use Icetalker\FilamentTableRepeater\Forms\Components\TableRepeater;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Collection;
function recalculateRowTotal(callable $set, callable $get): void
{
$amounts = collect([
$get('xs'),
$get('s'),
$get('m'),
$get('l'),
$get('xl'),
$get('2xl'),
$get('3xl'),
$get('osfa'),
])->map(fn ($val) => floatval($val ?? 0));
$set('total', $amounts->sum());
}
class OrderResource extends Resource class OrderResource extends Resource
{ {
protected static ?string $model = Order::class; protected static ?string $model = Order::class;
@ -51,6 +68,8 @@ public static function form(Form $form): Form
{ {
return $form->schema([ return $form->schema([
Group::make() Group::make()
->columns(6)
->columnSpan(2)
->schema([ ->schema([
Section::make([ Section::make([
Grid::make(1) Grid::make(1)
@ -139,55 +158,82 @@ public static function form(Form $form): Form
->columnSpan(1) ->columnSpan(1)
->hidden(fn (?Order $record) => $record === null) ->hidden(fn (?Order $record) => $record === null)
->extraAttributes(['class' => 'h-full']), ->extraAttributes(['class' => 'h-full']),
]) ]),
->columns(6)
->columnSpan(2),
TableRepeater::make('order_products') TableRepeater::make('order_products')
->label('Garments') ->label('Garments')
->addActionLabel('Add new garment')
->reorderable()
->cloneable()
->defaultItems(1)
->schema([ ->schema([
TextInput::make('sku') TextInput::make('sku')
->datalist(OrderProduct::all()->unique('sku')->pluck('sku')->toArray()), ->datalist(OrderProduct::all()->unique('sku')->pluck('sku')->toArray()),
TextInput::make('product_name') TextInput::make('product_name')
->datalist(OrderProduct::all()->unique('product_name')->pluck('product_name')->toArray()) ->datalist(OrderProduct::all()->unique('product_name')->pluck('product_name')->toArray()),
->required(),
TextInput::make('color') TextInput::make('color')
->datalist(OrderProduct::all()->unique('color')->pluck('color')->toArray()), ->datalist(OrderProduct::all()->unique('color')->pluck('color')->toArray()),
Cluster::make([ Cluster::make([
TextInput::make('xs') TextInput::make('xs')
->placeholder('xs') ->placeholder('xs')
->rules('numeric'), ->rules('numeric')
->reactive()
->afterStateUpdated(fn ($state, callable $set, callable $get) => recalculateRowTotal($set, $get)),
TextInput::make('s') TextInput::make('s')
->placeholder('s') ->placeholder('s')
->rules('numeric'), ->rules('numeric')
->reactive()
->afterStateUpdated(fn ($state, callable $set, callable $get) => recalculateRowTotal($set, $get)),
TextInput::make('m') TextInput::make('m')
->placeholder('m') ->placeholder('m')
->rules('numeric'), ->rules('numeric')
->reactive()
->afterStateUpdated(fn ($state, callable $set, callable $get) => recalculateRowTotal($set, $get)),
TextInput::make('l') TextInput::make('l')
->placeholder('l') ->placeholder('l')
->rules('numeric'), ->rules('numeric')
->reactive()
->afterStateUpdated(fn ($state, callable $set, callable $get) => recalculateRowTotal($set, $get)),
TextInput::make('xl') TextInput::make('xl')
->placeholder('xl') ->placeholder('xl')
->rules('numeric'), ->rules('numeric')
->reactive()
->afterStateUpdated(fn ($state, callable $set, callable $get) => recalculateRowTotal($set, $get)),
TextInput::make('2xl') TextInput::make('2xl')
->placeholder('2xl') ->placeholder('2xl')
->rules('numeric'), ->rules('numeric')
->reactive()
->afterStateUpdated(fn ($state, callable $set, callable $get) => recalculateRowTotal($set, $get)),
TextInput::make('3xl') TextInput::make('3xl')
->placeholder('3xl') ->placeholder('3xl')
->rules('numeric'), ->rules('numeric')
->reactive()
->afterStateUpdated(fn ($state, callable $set, callable $get) => recalculateRowTotal($set, $get)),
TextInput::make('osfa') TextInput::make('osfa')
->placeholder('osfa') ->placeholder('osfa')
->rules('numeric'), ->rules('numeric')
]) ->reactive()
->label('Sizes'), ->afterStateUpdated(fn ($state, callable $set, callable $get) => recalculateRowTotal($set, $get)),
]) ])->label('Sizes'),
->reorderable()
->cloneable() TextInput::make('total')
->defaultItems(1), ->disabled()
->dehydrated(false)
->afterStateHydrated(fn (callable $get, callable $set) => recalculateRowTotal($set, $get)),
]),
Repeater::make('services') Repeater::make('services')
->view('filament.forms.compact-repeater') ->view('filament.forms.compact-repeater')
->label('Product Services') ->label('Product Services')
->addActionLabel('Add new product service')
->schema([ ->schema([
Grid::make(19) Grid::make(19)
->schema([ ->schema([
@ -228,34 +274,44 @@ public static function form(Form $form): Form
->prefix('h') ->prefix('h')
->rules('numeric'), ->rules('numeric'),
]) ])
->label('Dimensions (inches)') ->label('Dimensions')
->columnSpan(4), ->columnSpan(4),
TextInput::make('amount') TextInput::make('amount')
->label('Quantity') ->label('Quantity')
->live()
->prefix('#') ->prefix('#')
->columnSpan(2) ->columnSpan(2)
->rules('numeric'), ->rules('numeric'),
TextInput::make('amount_price') TextInput::make('amount_price')
->label('Amount') ->label('Unit price')
->prefix('$') ->prefix('$')
->columnSpan(2) ->columnSpan(2)
->rules('numeric'), ->rules('numeric'),
]), ]),
Grid::make(9) Grid::make(19)
->schema([ ->schema([
TextInput::make('serviceFileCode') TextInput::make('serviceFileCode')
->label('Code') ->label('Code')
->datalist(ServiceFile::all()->unique('code')->pluck('code')->toArray()) ->datalist(ServiceFile::all()->unique('code')->pluck('code')->toArray())
->columnSpan(1) ->columnSpan(2)
->placeholder('A1234'), ->placeholder('A1234'),
Select::make('serviceFileSizeUnit')
->columnSpan(2)
->selectablePlaceholder(false)
->label('Unit')
->options([
'in' => 'Inches',
'cm' => 'cm',
]),
Textarea::make('notes') Textarea::make('notes')
->placeholder('Thread colors...') ->placeholder('Thread colors... (press enter for new line)')
->columnSpan(8), ->autosize()
->rows(1)
->columnSpan(15),
]), ]),
]) ])
->reorderable() ->reorderable()
@ -298,6 +354,11 @@ public static function table(Table $table): Table
}) })
->sortable(), ->sortable(),
TextColumn::make('order_type')
->badge()
->searchable()
->sortable(),
TextColumn::make('customer.company_name') TextColumn::make('customer.company_name')
->searchable() ->searchable()
->sortable(), ->sortable(),
@ -326,8 +387,16 @@ public static function table(Table $table): Table
->searchable() ->searchable()
->sortable(), ->sortable(),
]) ])
->defaultSort('order_date', 'desc') ->defaultSort('id', 'desc')
->filters([ ->filters([
SelectFilter::make('order_type')
->options(OrderType::class)
->multiple(),
SelectFilter::make('status')
->options(OrderStatus::class)
->multiple(),
Tables\Filters\Filter::make('order_date') Tables\Filters\Filter::make('order_date')
->form([ ->form([
DatePicker::make('created_from'), DatePicker::make('created_from'),

View File

@ -63,6 +63,7 @@ protected function handleRecordCreation(array $data): Order
'width' => $service['serviceFileWidth'] ?? null, 'width' => $service['serviceFileWidth'] ?? null,
'height' => $service['serviceFileHeight'] ?? null, 'height' => $service['serviceFileHeight'] ?? null,
'setup_number' => $service['serviceFileSetupNumber'] ?? null, 'setup_number' => $service['serviceFileSetupNumber'] ?? null,
'size_unit' => $service['serviceFileSizeUnit'] ?? 'inch',
]); ]);
ProductService::create([ ProductService::create([

View File

@ -21,9 +21,12 @@
use Filament\Resources\Pages\EditRecord; use Filament\Resources\Pages\EditRecord;
use Illuminate\Contracts\Support\Htmlable; use Illuminate\Contracts\Support\Htmlable;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Kenepa\ResourceLock\Resources\Pages\Concerns\UsesResourceLock;
class EditOrder extends EditRecord class EditOrder extends EditRecord
{ {
use UsesResourceLock;
protected static string $resource = OrderResource::class; protected static string $resource = OrderResource::class;
public function getTitle(): string|Htmlable public function getTitle(): string|Htmlable
@ -65,6 +68,7 @@ protected function mutateFormDataBeforeFill(array $data): array
'serviceFileHeight' => $service->serviceFile->height ?? '', 'serviceFileHeight' => $service->serviceFile->height ?? '',
'serviceFileCode' => $service->serviceFile->code ?? '', 'serviceFileCode' => $service->serviceFile->code ?? '',
'serviceFileSetupNumber' => $service->serviceFile->setup_number ?? '', 'serviceFileSetupNumber' => $service->serviceFile->setup_number ?? '',
'serviceFileSizeUnit' => $service->serviceFile->size_unit ?? 'inch',
]; ];
} }
@ -136,6 +140,7 @@ public function handleRecordUpdate(Model $record, array $data): Model
'width' => $service['serviceFileWidth'] ?? null, 'width' => $service['serviceFileWidth'] ?? null,
'height' => $service['serviceFileHeight'] ?? null, 'height' => $service['serviceFileHeight'] ?? null,
'setup_number' => $service['serviceFileSetupNumber'] ?? null, 'setup_number' => $service['serviceFileSetupNumber'] ?? null,
'size_unit' => $service['serviceFileSizeUnit'] ?? 'inch',
]); ]);
ProductService::create([ ProductService::create([

View File

@ -10,11 +10,17 @@
use Filament\Actions; use Filament\Actions;
use Filament\Resources\Components\Tab; use Filament\Resources\Components\Tab;
use Filament\Resources\Pages\ListRecords; use Filament\Resources\Pages\ListRecords;
use Filament\Support\Enums\MaxWidth;
class ListOrders extends ListRecords class ListOrders extends ListRecords
{ {
protected static string $resource = OrderResource::class; protected static string $resource = OrderResource::class;
public function getMaxContentWidth(): string
{
return 'max-w-[1920px]';
}
private function excludeStatuses($query): mixed private function excludeStatuses($query): mixed
{ {
return $query return $query

View File

@ -3,7 +3,6 @@
namespace App\Filament\Admin\Resources; namespace App\Filament\Admin\Resources;
use App\Enums\IconEnum; use App\Enums\IconEnum;
use App\Models\Customer;
use App\Models\Order; use App\Models\Order;
use App\Models\PackingSlip; use App\Models\PackingSlip;
use Filament\Forms\Components\DatePicker; use Filament\Forms\Components\DatePicker;
@ -39,11 +38,16 @@ public static function form(Form $form): Form
DatePicker::make('date_received') DatePicker::make('date_received')
->default(today()) ->default(today())
->required(), ->required(),
Select::make('customer_id') Select::make('customer_id')
->label('Customer') ->label('Customer')
->options(Customer::all()->pluck('company_name', 'id')) ->relationship(name: 'customer', titleAttribute: 'company_name')
->preload()
->createOptionForm(fn ($form) => CustomerResource::form($form))
->createOptionAction(fn ($action) => $action->modalWidth('lg'))
->reactive() ->reactive()
->searchable(), ->searchable(),
Select::make('order_id') Select::make('order_id')
->label('Order') ->label('Order')
->options(fn ($get): array => Order::where('customer_id', $get('customer_id') ?? null) ->options(fn ($get): array => Order::where('customer_id', $get('customer_id') ?? null)
@ -57,6 +61,7 @@ public static function form(Form $form): Form
Grid::make(1) Grid::make(1)
->schema([ ->schema([
TextArea::make('contents') TextArea::make('contents')
->required()
->rows(9), ->rows(9),
]) ])
->columnSpan(1), ->columnSpan(1),
@ -73,7 +78,6 @@ public static function table(Table $table): Table
->sortable() ->sortable()
->searchable(), ->searchable(),
TextColumn::make('order.customer_po') TextColumn::make('order.customer_po')
// ->url(fn ($record) => $record->to)
->url(fn ($record) => OrderResource::getUrl('edit', ['record' => $record->id])) ->url(fn ($record) => OrderResource::getUrl('edit', ['record' => $record->id]))
->weight('bold') ->weight('bold')
->color('code') ->color('code')

View File

@ -49,7 +49,7 @@ private function getOrdersPast30Days(): string
->where('order_status', '!=', OrderStatus::READY_FOR_INVOICE) ->where('order_status', '!=', OrderStatus::READY_FOR_INVOICE)
->where('order_status', '!=', OrderStatus::SHIPPED) ->where('order_status', '!=', OrderStatus::SHIPPED)
->where('order_status', '!=', OrderStatus::INVOICED) ->where('order_status', '!=', OrderStatus::INVOICED)
->whereBetween('created_at', [now()->startOfMonth(), now()->endOfMonth()]) ->whereBetween('created_at', [now()->subDays(30), now()])
->count(); ->count();
} }

View File

@ -16,13 +16,14 @@
use Illuminate\Database\Eloquent\Relations\HasOne; use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Carbon; use Illuminate\Support\Carbon;
use Kenepa\ResourceLock\Models\Concerns\HasLocks;
#[ObservedBy(OrderObserver::class)] #[ObservedBy(OrderObserver::class)]
class Order extends Model class Order extends Model
{ {
/** @use HasFactory<OrderFactory> */ /** @use HasFactory<OrderFactory> */
use HasFactory, SoftDeletes; use HasFactory, HasLocks, SoftDeletes;
protected $fillable = [ protected $fillable = [
'customer_id', 'customer_id',

View File

@ -26,6 +26,7 @@ class ProductService extends Model
'amount', 'amount',
'amount_price', 'amount_price',
'notes', 'notes',
'size_unit',
]; ];
protected $appends = [ protected $appends = [

View File

@ -19,6 +19,7 @@ class ServiceFile extends Model
'width', 'width',
'height', 'height',
'setup_number', 'setup_number',
'size_unit',
]; ];
/** /**

View File

@ -18,6 +18,7 @@
use Illuminate\Session\Middleware\AuthenticateSession; use Illuminate\Session\Middleware\AuthenticateSession;
use Illuminate\Session\Middleware\StartSession; use Illuminate\Session\Middleware\StartSession;
use Illuminate\View\Middleware\ShareErrorsFromSession; use Illuminate\View\Middleware\ShareErrorsFromSession;
use Kenepa\ResourceLock\ResourceLockPlugin;
class AdminPanelProvider extends PanelProvider class AdminPanelProvider extends PanelProvider
{ {
@ -29,10 +30,14 @@ public function panel(Panel $panel): Panel
->path('admin') ->path('admin')
->login(UsernameLogin::class) ->login(UsernameLogin::class)
->colors([ ->colors([
'primary' => Color::Blue, 'primary' => Color::Blue,
'code' => Color::hex('#d63384'), 'code' => Color::hex('#d63384'),
'invoicing' => Color::hex('#DD00DD'), 'invoicing' => Color::hex('#DD00DD'),
'invoiced' => Color::hex('#900090'), 'invoiced' => Color::hex('#900090'),
'embroidery' => Color::hex('#FF00FF'),
'screen_printing' => Color::hex('#0088FF'),
'vinyl' => Color::hex('#22AA22'),
'misc' => Color::hex('#0000FF'),
]) ])
->discoverResources(in: app_path('Filament/Admin/Resources/'), for: 'App\\Filament\\Admin\\Resources') ->discoverResources(in: app_path('Filament/Admin/Resources/'), for: 'App\\Filament\\Admin\\Resources')
->discoverPages(in: app_path('Filament/Admin/Pages'), for: 'App\\Filament\\Admin\\Pages') ->discoverPages(in: app_path('Filament/Admin/Pages'), for: 'App\\Filament\\Admin\\Pages')
@ -62,6 +67,7 @@ public function panel(Panel $panel): Panel
NavigationGroup::make('Financial'), NavigationGroup::make('Financial'),
NavigationGroup::make('Reports'), NavigationGroup::make('Reports'),
NavigationGroup::make('Settings'), NavigationGroup::make('Settings'),
]); ])
->plugin(ResourceLockPlugin::make());
} }
} }

View File

@ -7,15 +7,17 @@
"require": { "require": {
"php": "^8.2", "php": "^8.2",
"davidhsianturi/blade-bootstrap-icons": "^1.5", "davidhsianturi/blade-bootstrap-icons": "^1.5",
"doctrine/dbal": "^4.2",
"fakerphp/faker": "^1.23",
"filament/filament": "^3.2", "filament/filament": "^3.2",
"guava/filament-clusters": "^1.4", "guava/filament-clusters": "^1.4",
"icetalker/filament-table-repeater": "^1.3", "icetalker/filament-table-repeater": "^1.3",
"kenepa/resource-lock": "^2.1",
"laravel/framework": "^11.9", "laravel/framework": "^11.9",
"laravel/tinker": "^2.9", "laravel/tinker": "^2.9",
"livewire/livewire": "^3.5", "livewire/livewire": "^3.5",
"mallardduck/blade-lucide-icons": "^1.23", "mallardduck/blade-lucide-icons": "^1.23",
"spatie/laravel-pdf": "^1.5", "spatie/laravel-pdf": "^1.5"
"fakerphp/faker": "^1.23"
}, },
"require-dev": { "require-dev": {
"barryvdh/laravel-ide-helper": "^3.5", "barryvdh/laravel-ide-helper": "^3.5",

70
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "46ac06345fc3d57a7d0a01cfcaeaac17", "content-hash": "139b4d2b0a40ace1cf1efaf4c7facf20",
"packages": [ "packages": [
{ {
"name": "anourvalar/eloquent-serialize", "name": "anourvalar/eloquent-serialize",
@ -2238,6 +2238,74 @@
], ],
"time": "2025-03-05T16:06:45+00:00" "time": "2025-03-05T16:06:45+00:00"
}, },
{
"name": "kenepa/resource-lock",
"version": "2.1.3",
"source": {
"type": "git",
"url": "https://github.com/kenepa/resource-lock.git",
"reference": "dda6e14b5c4a47817569081b11248c572ebdfd07"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/kenepa/resource-lock/zipball/dda6e14b5c4a47817569081b11248c572ebdfd07",
"reference": "dda6e14b5c4a47817569081b11248c572ebdfd07",
"shasum": ""
},
"require": {
"filament/filament": "^3.0",
"illuminate/contracts": "^9.0|^10.0|^11.0|^12.0",
"php": "^8.1",
"spatie/laravel-package-tools": "^1.15.0"
},
"require-dev": {
"laravel/pint": "^1.0",
"nunomaduro/collision": "^7.0|^8.1",
"nunomaduro/larastan": "^2.0.1",
"orchestra/testbench": "^7.0|^8.0|^9.0|^10.0",
"pestphp/pest": "^2.0|^3.7",
"pestphp/pest-plugin-laravel": "^2.0|^3.1",
"pestphp/pest-plugin-livewire": "^2.0|^3.0",
"phpstan/extension-installer": "^1.1",
"phpstan/phpstan-deprecation-rules": "^1.0|^2.0",
"phpstan/phpstan-phpunit": "^1.0|^2.0",
"spatie/laravel-ray": "^1.26",
"tightenco/duster": "^1.1|^3.1"
},
"type": "library",
"extra": {
"laravel": {
"aliases": {
"ResourceLock": "Kenepa\\ResourceLock\\Facades\\ResourceLock"
},
"providers": [
"Kenepa\\ResourceLock\\ResourceLockServiceProvider"
]
}
},
"autoload": {
"psr-4": {
"Kenepa\\ResourceLock\\": "src",
"Kenepa\\ResourceLock\\Database\\Factories\\": "database/factories"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "Filament Resource Lock is a Filament plugin that adds resource locking functionality to your site.",
"homepage": "https://github.com/kenepa/resource-lock",
"keywords": [
"Kenepa",
"laravel",
"resource-lock"
],
"support": {
"issues": "https://github.com/kenepa/resource-lock/issues",
"source": "https://github.com/kenepa/resource-lock/tree/2.1.3"
},
"time": "2025-03-14T16:31:20+00:00"
},
{ {
"name": "kirschbaum-development/eloquent-power-joins", "name": "kirschbaum-development/eloquent-power-joins",
"version": "4.2.1", "version": "4.2.1",

View File

@ -25,7 +25,7 @@
| or any other location as required by the application or its packages. | or any other location as required by the application or its packages.
*/ */
'version' => '20250311', 'version' => '20250411',
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------

130
config/resource-lock.php Normal file
View File

@ -0,0 +1,130 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Models
|--------------------------------------------------------------------------
|
| The models configuration specifies the classes that represent your application's
| data objects. This configuration is used by the framework to interact with
| the application's data models. You can even implement your own ResourceLock model.
|
*/
'models' => [
'User' => \App\Models\User::class,
// 'ResourceLock' => null,
],
/*
|--------------------------------------------------------------------------
| Filament Resource
|--------------------------------------------------------------------------
|
| The resource lock filament resource displays all the current locks in place.
| You are able to replace the resource Lock with your own resource class.
|
*/
'resource' => [
'class' => \Kenepa\ResourceLock\Resources\LockResource::class,
],
/*
|--------------------------------------------------------------------------
| Resource Unlocker Button
|--------------------------------------------------------------------------
|
| The unlocker configuration specifies whether limited access is enabled for
| the resource unlock button. If limited access is enabled, only specific
| users or roles will be able to unlock locked resources directly from
| the modal.
|
*/
'unlocker' => [
'limited_access' => false,
// 'gate' => ''
],
/*
|--------------------------------------------------------------------------
| Lock Notice
|--------------------------------------------------------------------------
|
| The lock notice contains several configuration options for the modal
| that is display when a resource is locked.
|
*/
'lock_notice' => [
'display_resource_lock_owner' => false,
],
/*
|--------------------------------------------------------------------------
| Resource Lock Manager
|--------------------------------------------------------------------------
|
| The resource lock manager provides a simple way to view all resource locks
| of your application. It provides several ways to quickly unlock all or
| specific resources within your app.
|
*/
'manager' => [
'navigation_badge' => false,
'navigation_icon' => 'heroicon-o-lock-closed',
'navigation_label' => 'Resource Locks',
'plural_label' => 'Resource Locks',
'navigation_group' => 'Settings',
'navigation_sort' => 200,
'limited_access' => false,
'should_register_navigation' => true,
// 'gate' => ''
],
/*
|--------------------------------------------------------------------------
| Lock timeout (in minutes)
|--------------------------------------------------------------------------
|
| The lock_timeout configuration specifies the time interval, in minutes,
| after which a lock on a resource will expire if it has not been manually
| unlocked or released by the user.
|
*/
'lock_timeout' => 10,
/*
|--------------------------------------------------------------------------
| Check Locks before saving
|--------------------------------------------------------------------------
|
| The check_locks_before_saving configuration specifies whether a lock of a resource will be checked
| before saving a resource if a tech-savvy user is able to bypass the locked
| resource modal and attempt to save the resource. In some cases you may want to turns this off.
| It's recommended to keep this on.
|
*/
'check_locks_before_saving' => true,
/*
|--------------------------------------------------------------------------
| Actions
|--------------------------------------------------------------------------
|
| Action classes are simple classes that execute some logic within the package.
| If you want to add your own custom logic you are able to extend your own
| class with class your overwriting.
| Learn more about action classes: https://freek.dev/2442-strategies-for-making-laravel-packages-customizable
|
*/
'actions' => [
'get_resource_lock_owner_action' => \Kenepa\ResourceLock\Actions\GetResourceLockOwnerAction::class,
],
];

View File

@ -19,7 +19,6 @@ public function definition(): array
'name' => $this->faker->word(), 'name' => $this->faker->word(),
'width' => round($this->faker->randomFloat(2, 0, 10), 1), 'width' => round($this->faker->randomFloat(2, 0, 10), 1),
'height' => round($this->faker->randomFloat(2, 0, 10), 1), 'height' => round($this->faker->randomFloat(2, 0, 10), 1),
'unit' => 'inch',
]; ];
} }
} }

View File

@ -0,0 +1,29 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('resource_locks', function (Blueprint $table) {
$table->id();
$table->timestamps();
$table->foreignId('user_id')->constrained();
$table->morphs('lockable');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('resource_locks');
}
};

View File

@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('order_products', function (Blueprint $table) {
$table->string('product_name')->nullable()->change();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('order_products', function (Blueprint $table) {
$table->string('product_name')->nullable(false)->change();
});
}
};

View File

@ -0,0 +1,22 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('service_files', function (Blueprint $table) {
$table->string('size_unit')->default('in');
});
}
public function down(): void
{
Schema::table('service_files', function (Blueprint $table) {
$table->dropColumn('size_unit');
});
}
};

2
package-lock.json generated
View File

@ -1,5 +1,5 @@
{ {
"name": "topnotch_website", "name": "order_system",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {

View File

@ -118,6 +118,7 @@ class="fi-fo-repeater-item divide-y divide-gray-100 rounded-xl bg-white shadow-
> >
@if ($reorderActionIsVisible || $moveUpActionIsVisible || $moveDownActionIsVisible) @if ($reorderActionIsVisible || $moveUpActionIsVisible || $moveDownActionIsVisible)
<ul class="flex items-center gap-x-3"> <ul class="flex items-center gap-x-3">
{{--
@if ($reorderActionIsVisible) @if ($reorderActionIsVisible)
<li <li
x-sortable-handle x-sortable-handle
@ -126,6 +127,7 @@ class="fi-fo-repeater-item divide-y divide-gray-100 rounded-xl bg-white shadow-
{{ $reorderAction }} {{ $reorderAction }}
</li> </li>
@endif @endif
--}}
@if ($moveUpActionIsVisible || $moveDownActionIsVisible) @if ($moveUpActionIsVisible || $moveDownActionIsVisible)
<li <li
@ -156,7 +158,7 @@ class="flex items-center justify-center"
</h4> </h4>
@endif @endif
@if ($cloneActionIsVisible || $deleteActionIsVisible || $isCollapsible || $visibleExtraItemActions) @if ($cloneActionIsVisible || $deleteActionIsVisible || $isCollapsible || $visibleExtraItemActions || $reorderActionIsVisible)
<ul <ul
class="ms-auto flex items-center gap-x-3" class="ms-auto flex items-center gap-x-3"
> >
@ -166,6 +168,15 @@ class="ms-auto flex items-center gap-x-3"
</li> </li>
@endforeach @endforeach
@if ($reorderActionIsVisible)
<li
x-sortable-handle
x-on:click.stop
>
{{ $reorderAction }}
</li>
@endif
@if ($cloneActionIsVisible) @if ($cloneActionIsVisible)
<li x-on:click.stop> <li x-on:click.stop>
{{ $cloneAction }} {{ $cloneAction }}

View File

@ -111,7 +111,9 @@
</div> </div>
<div class="fs-6 text-primary"> <div class="fs-6 text-primary">
{{$order->customer_po}} {{$order->customer_po}}
@if($order->pre_production)<span class="text-danger"> (pre-pro)</span>@endif() @if($order->pre_production)
<span class="text-danger"> (pre-pro)</span>
@endif()
</div> </div>
</div> </div>
@ -153,7 +155,7 @@
<tbody> <tbody>
@foreach($order->orderProducts as $product) @foreach($order->orderProducts as $product)
<tr> <tr>
<td><code>{{$product->sku}}</code></td> <td><code style="font-size: 0.9rem;">{{$product->sku}}</code></td>
<td class="text-uppercase">{{$product->product_name}}</td> <td class="text-uppercase">{{$product->product_name}}</td>
<td class="text-uppercase">{{$product->color}}</td> <td class="text-uppercase">{{$product->color}}</td>
<td style="width: 40px;">{{$product->productSizes()->get()->where('size', 'xs')->first()->amount ?? ''}}</td> <td style="width: 40px;">{{$product->productSizes()->get()->where('size', 'xs')->first()->amount ?? ''}}</td>
@ -182,12 +184,13 @@
<!-- Services Table --> <!-- Services Table -->
<div class="row mt-2"> <div class="row mt-2">
<table class="table table-striped" style="font-size: 12px;"> <table class="table table-striped" style="font-size: 13px;">
<thead class="opacity-50 fw-normal"> <thead class="opacity-50 fw-normal">
<th>Placement & Service</th> <th>Placement & Service</th>
<th class="w-50">Logo Name & Instructions</th> <th class="w-50">Logo Name & Instructions</th>
<th>Width</th> <th>Width</th>
<th>Height</th> <th>Height</th>
<th>Unit</th>
<th>QTY</th> <th>QTY</th>
</thead> </thead>
@ -200,18 +203,18 @@
</div> </div>
<br> <br>
<div class="text-uppercase"> <div class="text-uppercase">
<code style="font-size: 12px"> <code style="font-size: 14px">
{{$service->serviceFile->code}} {{$service->serviceFile->code ?? ''}}
</code> </code>
</div> </div>
</td> </td>
<td> <td>
<div class="text-uppercase"> <div class="text-uppercase">
{{$service->serviceFile->name}} {{$service->serviceFile->name ?? ''}}
</div> </div>
<br> <br>
<div class="text-uppercase"> <div class="text-uppercase" style="color: #d63384;">
{{$service->notes}} {{$service->notes ?? ''}}
</div> </div>
</td> </td>
<td> <td>
@ -220,6 +223,9 @@
<td> <td>
{{$service->serviceFile->height}} {{$service->serviceFile->height}}
</td> </td>
<td>
{{$service->serviceFile->size_unit}}
</td>
<td> <td>
{{$service->amount}} {{$service->amount}}
</td> </td>

View File

@ -6,9 +6,11 @@ export default {
"./app/Filament/**/*.php", "./app/Filament/**/*.php",
"./vendor/filament/**/*.php" "./vendor/filament/**/*.php"
], ],
safelist: [
'max-w-[1920px]'
],
theme: { theme: {
extend: {}, extend: {},
}, },
plugins: [], plugins: [],
} }

View File

View File

@ -54,7 +54,7 @@
->assertHasNoErrors(); ->assertHasNoErrors();
$this->assertDatabaseHas('invoices', [ $this->assertDatabaseHas('invoices', [
'internal_id' => 'TN40001', 'internal_id' => 'TN4001',
'customer_id' => $formData['customer_id'], 'customer_id' => $formData['customer_id'],
'status' => $formData['status'], 'status' => $formData['status'],
'has_gst' => $formData['has_gst'], 'has_gst' => $formData['has_gst'],
@ -65,7 +65,7 @@
'hst_rate' => $hst_rate, 'hst_rate' => $hst_rate,
]); ]);
$invoice = Invoice::where('internal_id', 'TN40001')->firstOrFail(); $invoice = Invoice::where('internal_id', 'TN4001')->firstOrFail();
$this->assertEquals($invoice->orders->isEmpty(), true); $this->assertEquals($invoice->orders->isEmpty(), true);
}); });