Skip to content

Commit 19d3298

Browse files
committed
add processing state for badges and implement stuck print job check
1 parent b493727 commit 19d3298

33 files changed

Lines changed: 2065 additions & 364 deletions
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
<?php
2+
3+
namespace App\Console\Commands;
4+
5+
use App\Domain\Printing\Models\PrintJob;
6+
use App\Domain\Printing\Models\Printer;
7+
use App\Enum\PrintJobStatusEnum;
8+
use App\Enum\PrinterStatusEnum;
9+
use Illuminate\Console\Command;
10+
use Illuminate\Support\Facades\Log;
11+
12+
class CheckStuckPrintJobs extends Command
13+
{
14+
/**
15+
* The name and signature of the console command.
16+
*/
17+
protected $signature = 'printing:check-stuck-jobs';
18+
19+
/**
20+
* The console command description.
21+
*/
22+
protected $description = 'Check for print jobs stuck in printing state and mark printers as needing attention';
23+
24+
/**
25+
* Execute the console command.
26+
*/
27+
public function handle()
28+
{
29+
$this->info('Checking for stuck print jobs...');
30+
31+
// Find print jobs that have been in "printing" state for longer than 5 minutes
32+
$stuckJobs = PrintJob::where('status', PrintJobStatusEnum::Printing)
33+
->where('started_at', '<', now()->subMinutes(5))
34+
->with(['printer', 'processingMachine'])
35+
->get();
36+
37+
if ($stuckJobs->isEmpty()) {
38+
$this->info('No stuck print jobs found.');
39+
return Command::SUCCESS;
40+
}
41+
42+
$this->warn("Found {$stuckJobs->count()} stuck print jobs:");
43+
44+
foreach ($stuckJobs as $job) {
45+
$this->line("- Job #{$job->id} on printer '{$job->printer->name}' (started: {$job->started_at->diffForHumans()})");
46+
47+
// Check if this printer has processed any jobs successfully since this stuck job started
48+
$recentSuccessfulJobs = PrintJob::where('printer_id', $job->printer_id)
49+
->where('status', PrintJobStatusEnum::Printed)
50+
->where('printed_at', '>', $job->started_at)
51+
->exists();
52+
53+
if ($recentSuccessfulJobs) {
54+
$this->line(" ✓ Printer has processed jobs recently - skipping");
55+
continue;
56+
}
57+
58+
// Mark printer as needing attention (paused state)
59+
$this->markPrinterAsStuck($job->printer, $job);
60+
}
61+
62+
return Command::SUCCESS;
63+
}
64+
65+
/**
66+
* Mark a printer as stuck/paused due to a stuck print job
67+
*/
68+
private function markPrinterAsStuck(Printer $printer, PrintJob $stuckJob): void
69+
{
70+
// Update printer status to paused with error message
71+
Printer::updatePrinterState(
72+
$printer->name,
73+
PrinterStatusEnum::PAUSED,
74+
$stuckJob->id,
75+
"Print job #{$stuckJob->id} has been processing for over 5 minutes. Please check printer for paper jams, low ribbons, or other issues.",
76+
$stuckJob->processingMachine->name ?? 'System'
77+
);
78+
79+
Log::warning("Printer '{$printer->name}' marked as paused due to stuck print job #{$stuckJob->id}", [
80+
'printer_id' => $printer->id,
81+
'job_id' => $stuckJob->id,
82+
'job_started_at' => $stuckJob->started_at,
83+
'stuck_duration_minutes' => $stuckJob->started_at->diffInMinutes(now())
84+
]);
85+
86+
$this->warn(" ⚠ Marked printer '{$printer->name}' as PAUSED - requires staff attention");
87+
}
88+
}

app/Filament/Resources/BadgeResource.php

Lines changed: 96 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,17 @@
88
use App\Filament\Traits\HasEventFilter;
99
use App\Jobs\Printing\PrintBadgeJob;
1010
use App\Models\Badge\Badge;
11+
use App\Models\Event;
1112
use App\Models\Badge\State_Fulfillment\BadgeFulfillmentStatusState;
12-
use App\Models\Badge\State_Fulfillment\Printed;
13+
use App\Models\Badge\State_Fulfillment\Processing;
1314
use App\Models\Badge\State_Payment\BadgePaymentStatusState;
1415
use Filament\Forms;
1516
use Filament\Forms\Form;
1617
use Filament\Resources\Resource;
1718
use Filament\Tables;
1819
use Filament\Tables\Table;
1920
use Illuminate\Support\Collection;
21+
use Illuminate\Support\Facades\Bus;
2022

2123
class 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)

app/Filament/Resources/PrintJobResource.php

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,41 @@ public static function table(Table $table): Table
163163
]),
164164
SelectFilter::make('printer')
165165
->relationship('printer', 'name'),
166+
\Filament\Tables\Filters\Filter::make('printable_id')
167+
->form([
168+
\Filament\Forms\Components\TextInput::make('value')
169+
->label('Printable ID')
170+
->numeric(),
171+
])
172+
->query(function (Builder $query, array $data): Builder {
173+
return $query->when(
174+
$data['value'],
175+
fn (Builder $query, $value): Builder => $query->where('printable_id', $value),
176+
);
177+
})
178+
->indicateUsing(function (array $data): ?string {
179+
if (! $data['value']) {
180+
return null;
181+
}
182+
return 'Printable ID: ' . $data['value'];
183+
}),
184+
\Filament\Tables\Filters\Filter::make('printable_type')
185+
->form([
186+
\Filament\Forms\Components\TextInput::make('value')
187+
->label('Printable Type'),
188+
])
189+
->query(function (Builder $query, array $data): Builder {
190+
return $query->when(
191+
$data['value'],
192+
fn (Builder $query, $value): Builder => $query->where('printable_type', $value),
193+
);
194+
})
195+
->indicateUsing(function (array $data): ?string {
196+
if (! $data['value']) {
197+
return null;
198+
}
199+
return 'Type: ' . class_basename($data['value']);
200+
}),
166201
])
167202
->actions([
168203
Tables\Actions\ViewAction::make(),
@@ -185,7 +220,8 @@ public static function table(Table $table): Table
185220
Tables\Actions\DeleteBulkAction::make(),
186221
]),
187222
])
188-
->defaultSort('created_at', 'desc');
223+
->defaultSort('id', 'desc')
224+
->poll('5s');
189225
}
190226

191227
public static function getRelations(): array

app/Http/Controllers/BadgeController.php

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,23 @@
2323

2424
class BadgeController extends Controller
2525
{
26-
public function show(Request $request) // Catch any malformed request e.g. ./badges/AnyWord
26+
public function show(Request $request, Badge $badge)
2727
{
28-
return $this->index($request);
28+
$user = $request->user();
29+
30+
// Check if user can view this badge
31+
Gate::authorize('view', $badge);
32+
33+
// Load relationships
34+
$badge->load(['fursuit.species', 'fursuit.event', 'fursuit.user']);
35+
36+
// Add edit permission
37+
$badge->canEdit = Gate::allows('update', $badge);
38+
39+
return Inertia::render('Badges/BadgeShow', [
40+
'badge' => $badge,
41+
'canEdit' => $badge->canEdit,
42+
]);
2943
}
3044

3145
public function index(Request $request)
@@ -53,7 +67,7 @@ public function index(Request $request)
5367
$query->where('id', '!=', $activeEvent->id);
5468
}
5569
})
56-
->whereIn('status_fulfillment', ['printed', 'ready_for_pickup'])
70+
->where('status_fulfillment', 'ready_for_pickup')
5771
->with(['fursuit.species', 'fursuit.event'])
5872
->get();
5973

0 commit comments

Comments
 (0)