88use App \Filament \Traits \HasEventFilter ;
99use App \Jobs \Printing \PrintBadgeJob ;
1010use App \Models \Badge \Badge ;
11+ use App \Models \Event ;
1112use App \Models \Badge \State_Fulfillment \BadgeFulfillmentStatusState ;
12- use App \Models \Badge \State_Fulfillment \Printed ;
13+ use App \Models \Badge \State_Fulfillment \Processing ;
1314use App \Models \Badge \State_Payment \BadgePaymentStatusState ;
1415use Filament \Forms ;
1516use Filament \Forms \Form ;
1617use Filament \Resources \Resource ;
1718use Filament \Tables ;
1819use Filament \Tables \Table ;
1920use Illuminate \Support \Collection ;
21+ use Illuminate \Support \Facades \Bus ;
2022
2123class BadgeResource extends Resource
2224{
@@ -96,7 +98,7 @@ public static function form(Form $form): Form
9698 ->options (BadgeFulfillmentStatusState::getStateMapping ()->keys ()->mapWithKeys (fn ($ key
9799 ) => [$ key => match ($ key ) {
98100 'pending ' => 'Pending ' ,
99- 'printed ' => 'Printed ' ,
101+ 'processing ' => 'Processing ' ,
100102 'ready_for_pickup ' => 'Ready for Pickup ' ,
101103 'picked_up ' => 'Picked Up ' ,
102104 default => ucfirst ($ key )
@@ -217,7 +219,18 @@ public static function form(Form $form): Form
217219 public static function table (Table $ table ): Table
218220 {
219221 return $ table
220- ->modifyQueryUsing (fn ($ query ) => static ::applyEventFilter ($ query , 'fursuit ' ))
222+ ->modifyQueryUsing (function ($ query ) {
223+ $ query = static ::applyEventFilter ($ query , 'fursuit ' );
224+
225+ // Add joins for attendee_id sorting but select only badges columns to avoid conflicts
226+ return $ query ->leftJoin ('fursuits ' , 'badges.fursuit_id ' , '= ' , 'fursuits.id ' )
227+ ->leftJoin ('event_users ' , function ($ join ) {
228+ $ join ->on ('fursuits.user_id ' , '= ' , 'event_users.user_id ' )
229+ ->on ('fursuits.event_id ' , '= ' , 'event_users.event_id ' );
230+ })
231+ ->select ('badges.* ' )
232+ ->addSelect ('event_users.attendee_id as sort_attendee_id ' );
233+ })
221234 ->columns ([
222235 // Fursuit Image as first column
223236 Tables \Columns \ImageColumn::make ('fursuit.image ' )
@@ -257,27 +270,54 @@ public static function table(Table $table): Table
257270 ->toggleable (isToggledHiddenByDefault: false ),
258271
259272 // Attendee ID
260- Tables \Columns \TextColumn::make ('attendee_id ' )
273+ Tables \Columns \TextColumn::make ('sort_attendee_id ' )
261274 ->label ('Attendee ID ' )
262- ->getStateUsing (function (Badge $ record ) {
263- $ eventUser = $ record ->fursuit ?->user?->eventUsers?->where('event_id ' , $ record ->fursuit ->event_id )->first ();
275+ ->formatStateUsing (fn ($ state ) => $ state ?? 'N/A ' )
276+ ->sortable (query: function ($ query , string $ direction ) {
277+ return $ query ->orderByRaw ("CAST(sort_attendee_id AS UNSIGNED) $ direction " );
278+ })
279+ ->toggleable (isToggledHiddenByDefault: false ),
264280
265- return $ eventUser ?->attendee_id ?? 'N/A ' ;
281+ // Print Jobs Column
282+ Tables \Columns \TextColumn::make ('print_jobs_count ' )
283+ ->label ('Print Jobs ' )
284+ ->badge ()
285+ ->url (fn (Badge $ record ): string => route ('filament.admin.resources.print-jobs.index ' , [
286+ 'tableFilters[printable_id][value] ' => $ record ->id ,
287+ 'tableFilters[printable_type][value] ' => get_class ($ record ),
288+ ]))
289+ ->getStateUsing (function (Badge $ record ): string {
290+ $ jobs = $ record ->printJobs ()->get ();
291+ $ total = $ jobs ->count ();
292+ $ pending = $ jobs ->whereIn ('status ' , ['pending ' , 'queued ' , 'printing ' , 'retrying ' ])->count ();
293+ $ failed = $ jobs ->where ('status ' , 'failed ' )->count ();
294+ $ printed = $ jobs ->where ('status ' , 'printed ' )->count ();
295+
296+ if ($ total === 0 ) return '0 ' ;
297+ if ($ failed > 0 ) return "{$ total } ( {$ failed } failed) " ;
298+ if ($ pending > 0 ) return "{$ total } ( {$ pending } pending) " ;
299+ return "{$ total }" ;
266300 })
267- ->searchable (query: function ($ query , $ search ) {
268- return $ query ->whereHas ('fursuit.user.eventUsers ' , function ($ q ) use ($ search ) {
269- $ q ->where ('attendee_id ' , 'like ' , "% {$ search }% " );
270- });
301+ ->color (function (Badge $ record ): string {
302+ $ jobs = $ record ->printJobs ()->get ();
303+ if ($ jobs ->count () === 0 ) return 'gray ' ;
304+
305+ $ hasFailed = $ jobs ->where ('status ' , 'failed ' )->count () > 0 ;
306+ $ hasPending = $ jobs ->whereIn ('status ' , ['pending ' , 'queued ' , 'printing ' , 'retrying ' ])->count () > 0 ;
307+
308+ if ($ hasFailed ) return 'warning ' ;
309+ if ($ hasPending ) return 'info ' ;
310+ return 'success ' ;
271311 })
272- ->toggleable (isToggledHiddenByDefault: true ),
312+ ->alignCenter ( ),
273313
274314 // Status Badges
275315 Tables \Columns \TextColumn::make ('status_fulfillment ' )
276316 ->label ('Fulfillment ' )
277317 ->badge ()
278318 ->formatStateUsing (fn (string $ state ): string => match ($ state ) {
279319 'pending ' => 'Pending ' ,
280- 'printed ' => 'Printed ' ,
320+ 'processing ' => 'Processing ' ,
281321 'ready_for_pickup ' => 'Ready for Pickup ' ,
282322 'picked_up ' => 'Picked Up ' ,
283323 default => ucfirst ($ state )
@@ -325,7 +365,7 @@ public static function table(Table $table): Table
325365 Tables \Filters \SelectFilter::make ('status_fulfillment ' )
326366 ->options ([
327367 'pending ' => 'Pending ' ,
328- 'printed ' => 'Printed ' ,
368+ 'processing ' => 'Processing ' ,
329369 'ready_for_pickup ' => 'Ready for Pickup ' ,
330370 'picked_up ' => 'Picked Up ' ,
331371 ])
@@ -398,9 +438,6 @@ public static function table(Table $table): Table
398438 }),
399439 ])
400440 ->bulkActions ([
401- Tables \Actions \BulkActionGroup::make ([
402- Tables \Actions \DeleteBulkAction::make (),
403- ]),
404441 Tables \Actions \BulkAction::make ('printBadgeBulk ' )
405442 ->label ('Print Badges ' )
406443 ->icon ('heroicon-o-printer ' )
@@ -421,30 +458,61 @@ public static function table(Table $table): Table
421458 ->modalDescription ('This will print all selected badges to the specified printer. ' )
422459 ->action (function (Collection $ records , array $ data ) {
423460 $ printerId = $ data ['printer_id ' ];
461+ // sort by attendee id numerically
462+ $ sortedRecords = $ records ->sortBy (fn (Badge $ badge ) => (int ) $ badge ->sort_attendee_id );
424463
425- $ records ->each (function (Badge $ record , $ index ) use ($ printerId ) {
426- if ($ record ->status_fulfillment ->canTransitionTo (Printed::class)) {
427- $ record ->status_fulfillment ->transitionTo (Printed::class);
464+ // Update badge states to mark them as sent for printing
465+ $ sortedRecords ->each (function (Badge $ record ) {
466+ if ($ record ->status_fulfillment ->canTransitionTo (Processing::class)) {
467+ $ record ->status_fulfillment ->transitionTo (Processing::class);
428468 }
429-
430- // Dispatch with staggered delay to prevent overwhelming the queue
431- PrintBadgeJob::dispatch ($ record , $ printerId )->delay (now ()->addSeconds ($ index * 2 ));
432469 });
433470
471+ // Create individual print jobs for batching in the correct order
472+ $ printJobs = $ sortedRecords ->map (function (Badge $ badge ) use ($ printerId ) {
473+ return new PrintBadgeJob ($ badge , $ printerId );
474+ })->toArray ();
475+
476+ // Create a Laravel batch with proper chaining
477+ Bus::batch ([
478+ // wrap in array to chain!
479+ $ printJobs
480+ ])
481+ ->name ("Badge Bulk Print - {$ records ->count ()} badges " )
482+ ->onQueue ('batch-print ' )
483+ ->allowFailures ()
484+ ->dispatch ();
485+
434486 return true ;
435487 }),
436488 ])
437489 ->selectCurrentPageOnly ()
438490 ->paginationPageOptions ([10 , 25 , 50 , 100 ])
439- ->defaultSort ('custom_id ' , 'asc ' );
491+ ->defaultSort ('sort_attendee_id ' , 'asc ' )
492+ ->poll ('5s ' );
440493 }
441494
442495 public static function printBadge (Badge $ badge , $ mass = 0 , ?int $ printerId = null ): Badge
443496 {
444- if ($ badge ->status_fulfillment ->canTransitionTo (Printed::class)) {
445- $ badge ->status_fulfillment ->transitionTo (Printed::class);
497+ \Log::info ('printBadge called ' , [
498+ 'badge_id ' => $ badge ->id ,
499+ 'before_fulfillment ' => $ badge ->status_fulfillment ->getValue (),
500+ 'before_payment ' => $ badge ->status_payment ->getValue (),
501+ 'can_transition ' => $ badge ->status_fulfillment ->canTransitionTo (Processing::class),
502+ ]);
503+
504+ if ($ badge ->status_fulfillment ->canTransitionTo (Processing::class)) {
505+ $ badge ->status_fulfillment ->transitionTo (Processing::class);
446506 }
447507
508+ $ badge ->refresh ();
509+
510+ \Log::info ('printBadge after transition ' , [
511+ 'badge_id ' => $ badge ->id ,
512+ 'after_fulfillment ' => $ badge ->status_fulfillment ->getValue (),
513+ 'after_payment ' => $ badge ->status_payment ->getValue (),
514+ ]);
515+
448516 // Always use PrintBadgeJob for consistency - it handles PDF generation and file storage
449517 PrintBadgeJob::dispatch ($ badge )->delay (now ()->addSeconds ($ mass * 15 ));
450518
@@ -453,8 +521,8 @@ public static function printBadge(Badge $badge, $mass = 0, ?int $printerId = nul
453521
454522 public static function printBadgeWithPrinter (Badge $ badge , int $ printerId , int $ delaySeconds = 0 ): Badge
455523 {
456- if ($ badge ->status_fulfillment ->canTransitionTo (Printed ::class)) {
457- $ badge ->status_fulfillment ->transitionTo (Printed ::class);
524+ if ($ badge ->status_fulfillment ->canTransitionTo (Processing ::class)) {
525+ $ badge ->status_fulfillment ->transitionTo (Processing ::class);
458526 }
459527
460528 // Generate PDF content synchronously (like PrintBadgeJob does)
0 commit comments