Skip to content

Commit c218dcb

Browse files
authored
feat(admin): badge printing (#189)
* fix(dev): use `mc alias` instead of `mc config` `mc config` has been deprecated with `mc alias` providing almost the same functionality, albeit different order of params * feat(admin): add support for generating badges from applications Also added a custom badge generator, for one-off prints * fix(admin): show table name in badge list * feat(admin): add settings for handling font and background uploads * fix(admin): Drop unused imports * feat(admin): use storage for resolving uploaded resources * fix(admin): leding -> leading * feat(admin): updated badge layout for BadgeService This probably should be evicted to a JSON blob, so updating the badge does not require updating the backend. A built-in editor is a bit harder due to how shit tc-lib-pdf is with it's bounding box calculation: either TextCell does some wildly imaginary things or the numbers given are ~40% too large. * fix(admin): use more sane logic for table numbers from Sharky * fix(admin): make display name resolving more readable Also documented order of preference * fix(admin): handle unused values cleanly This includes not overriding badge type if it isn't set to custom and skipping table number if has_table is not true.
1 parent d041366 commit c218dcb

File tree

9 files changed

+1565
-6
lines changed

9 files changed

+1565
-6
lines changed
Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
<?php
2+
3+
namespace App\Filament\Resources;
4+
5+
use App\Enums\ApplicationStatus;
6+
use App\Enums\ApplicationType;
7+
use App\Filament\Resources\BadgeResource\Pages;
8+
use App\Models\Application;
9+
use App\Services\BadgeService;
10+
use Filament\Forms;
11+
use Filament\Forms\Form;
12+
use Filament\Resources\Resource;
13+
use Filament\Tables;
14+
use Filament\Tables\Table;
15+
use Illuminate\Database\Eloquent\Builder;
16+
use Illuminate\Database\Eloquent\Collection;
17+
18+
class BadgeResource extends Resource
19+
{
20+
protected static ?string $model = Application::class;
21+
22+
protected static ?string $navigationIcon = 'heroicon-o-identification';
23+
24+
protected static ?string $label = 'Badge';
25+
protected static ?string $navigationLabel = 'Badges';
26+
public static ?string $slug = 'badges';
27+
protected static ?int $navigationSort = 15;
28+
29+
public static function form(Form $form): Form
30+
{
31+
return $form
32+
->columns(1)
33+
->schema([
34+
Forms\Components\Group::make()->columns(2)->schema([
35+
Forms\Components\Select::make('badge_type')
36+
->required()
37+
->live()
38+
->label(__("Badge Type"))
39+
->helperText(
40+
str('Select the type to display on the badge, use custom for free text input.')->inlineMarkdown()->toHtmlString()
41+
)
42+
->options([
43+
ApplicationType::Assistant->value => 'Assistant',
44+
ApplicationType::Dealer->value => 'Dealer',
45+
// ApplicationType::Share->value => 'Share',
46+
'goh' => 'Guest of Honor',
47+
'staff' => 'Staff',
48+
'custom' => 'Custom'
49+
])
50+
->dehydrated(fn($state) => $state !== 'custom')
51+
->native(false),
52+
Forms\Components\TextInput::make('badge_type_custom')
53+
->label(__("Custom Badge Type"))
54+
->helperText(
55+
str('Custom type to display on the badge, be mindful of the width in the template!')->inlineMarkdown()->toHtmlString()
56+
)
57+
->visible(fn(Forms\Get $get) => $get('badge_type') === 'custom')
58+
->requiredIf('badge_type', 'custom'),
59+
]),
60+
Forms\Components\Group::make()->columns(2)->schema([
61+
Forms\Components\Toggle::make('has_table')
62+
->live()
63+
->label(__("Show table number"))
64+
->helperText(
65+
str('Should the badge have a table number visible?')->inlineMarkdown()->toHtmlString()
66+
),
67+
Forms\Components\TextInput::make('table_number')
68+
->visible(fn(Forms\Get $get) => $get('has_table') === true)
69+
->requiredIf('has_table', true),
70+
]),
71+
Forms\Components\Group::make()->columns(2)->schema([
72+
Forms\Components\TextInput::make('reg_id')
73+
->required()
74+
->label(__("Reg ID"))
75+
->numeric()
76+
->helperText(
77+
str('Use only numbers.')->inlineMarkdown()->toHtmlString()
78+
),
79+
Forms\Components\TextInput::make('display_name')
80+
->required()
81+
->label(__("Display Name"))
82+
->helperText(
83+
str('Limit to 40 characters or less.')->inlineMarkdown()->toHtmlString()
84+
),
85+
]),
86+
Forms\Components\Toggle::make('has_share')
87+
->label(__("Show share indicator"))
88+
->helperText(
89+
str('Adds an `S` next to the Reg ID.')->inlineMarkdown()->toHtmlString()
90+
),
91+
Forms\Components\Toggle::make('double_sided')
92+
->label(__("Double sided badges"))
93+
->helperText(str('Should the print go on both sides of the card?')->inlineMarkdown()->toHtmlString()),
94+
]);
95+
}
96+
97+
public static function table(Table $table): Table
98+
{
99+
return $table
100+
->columns([
101+
Tables\Columns\TextColumn::make('user.name')
102+
->searchable(),
103+
Tables\Columns\IconColumn::make('is_ready')
104+
->label(__('Ready'))
105+
->getStateUsing(function (Application $record) {
106+
return $record->isReady();
107+
})
108+
->boolean(),
109+
Tables\Columns\TextColumn::make('type')
110+
->formatStateUsing(function (ApplicationType $state) {
111+
return ucfirst($state->value);
112+
})
113+
->sortable(),
114+
Tables\Columns\TextColumn::make('table_number')
115+
->sortable()
116+
->searchable()
117+
->disabled(fn($record) => $record->type === ApplicationType::Assistant),
118+
Tables\Columns\TextColumn::make('display_name')
119+
->searchable(),
120+
])
121+
->filters([
122+
Tables\Filters\Filter::make('dealers')
123+
->query(fn(Builder $query): Builder => $query->where('type', 'dealer'))
124+
->label(__('Only Dealerships')),
125+
Tables\Filters\SelectFilter::make('status')
126+
->options(array_combine(array_column(ApplicationStatus::cases(), 'value'), array_column(ApplicationStatus::cases(), 'name')))
127+
->query(function (Builder $query, array $data) {
128+
if (!key_exists('values', $data)) {
129+
return;
130+
}
131+
$query->where(function (Builder $query) use ($data) {
132+
foreach ($data['values'] as $value) {
133+
ApplicationStatus::tryFrom($value)?->orWhere($query);
134+
}
135+
});
136+
})
137+
->multiple(),
138+
])
139+
->actions([])
140+
->bulkActions([
141+
Tables\Actions\BulkAction::make('print')
142+
->form([
143+
Forms\Components\Toggle::make('double_sided')
144+
->label(__("Double sided badges"))
145+
->helperText(str('Should the print go on both sides of the card?')->inlineMarkdown()->toHtmlString())
146+
147+
])
148+
->action(function (Collection $records, array $data, Tables\Actions\BulkAction $action) {
149+
$badgeService = new BadgeService();
150+
$doubleSided = $data['double_sided'];
151+
foreach ($records as $record) {
152+
$badgeService->generateBadge(
153+
$record,
154+
$doubleSided
155+
);
156+
}
157+
158+
$badgeFile = tmpfile();
159+
$badgeFileUri = stream_get_meta_data($badgeFile)['uri'];
160+
$badgeService->save($badgeFile);
161+
fflush($badgeFile);
162+
163+
$action->success();
164+
165+
// Note, we need to pass the $badgeFile to the closure so PHP does not remove it prematurely
166+
return response()->streamDownload(function () use ($badgeFile, $badgeFileUri) {
167+
echo file_get_contents($badgeFileUri);
168+
}, "DD Badges - " . date("Y-m-d\TH-i-sO") . ".pdf");
169+
})
170+
->successNotificationTitle("Bogos binted")
171+
->icon('heroicon-o-printer'),
172+
]);
173+
}
174+
175+
public static function getRelations(): array
176+
{
177+
return [
178+
//
179+
];
180+
}
181+
182+
public static function getPages(): array
183+
{
184+
return [
185+
'index' => Pages\ListBadges::route('/'),
186+
'create' => Pages\PrintBadge::route('/create'),
187+
'settings' => Pages\BadgeSettings::route('/settings'),
188+
];
189+
}
190+
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
<?php
2+
3+
namespace App\Filament\Resources\BadgeResource\Pages;
4+
5+
use App\Filament\Resources\BadgeResource;
6+
use Filament\Actions\Action;
7+
use Filament\Actions\Contracts\HasActions;
8+
use Filament\Forms\Contracts\HasForms;
9+
use Filament\Forms\Concerns\InteractsWithForms;
10+
use Filament\Forms\Components\FileUpload;
11+
use Filament\Forms\Form;
12+
use Filament\Notifications\Notification;
13+
use Filament\Pages\Concerns\HasUnsavedDataChangesAlert;
14+
use Filament\Pages\Concerns\InteractsWithFormActions;
15+
use Filament\Resources\Pages\Page;
16+
use Filament\Support\Facades\FilamentIcon;
17+
use Illuminate\Contracts\Support\Htmlable;
18+
use Illuminate\Support\Facades\Storage;
19+
use Illuminate\Support\HtmlString;
20+
use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
21+
22+
class BadgeSettings extends Page implements HasForms, HasActions
23+
{
24+
use InteractsWithForms, InteractsWithFormActions, HasUnsavedDataChangesAlert;
25+
26+
protected static string $resource = BadgeResource::class;
27+
28+
protected static string $view = 'filament.resources.badge-resource.pages.badge-settings';
29+
30+
public ?array $data = [];
31+
32+
public function mount(): void
33+
{
34+
abort_unless(static::getResource()::canCreate(), 403);
35+
36+
$this->form->fill();
37+
}
38+
39+
protected function getFormActions(): array
40+
{
41+
return [
42+
Action::make('save')
43+
->label(__('Save'))
44+
->submit('save')
45+
->keyBindings(['mod+s'])
46+
];
47+
}
48+
49+
public function form(Form $form): Form
50+
{
51+
$badge_font = Storage::disk('local')->exists('badges/badge-font');
52+
$badge_background = Storage::disk('local')->exists('badges/badge-background');
53+
return $form
54+
->columns(1)
55+
->schema(
56+
[
57+
FileUpload::make('badge_font')
58+
->label(__("Badge Font"))
59+
->helperText("Present on disk: " . ($badge_font ? 'Yes' : 'No'))
60+
->required()
61+
->getUploadedFileNameForStorageUsing(fn(TemporaryUploadedFile $file): string => "badge-font")
62+
->disk("local")
63+
->directory("badges")
64+
->default('badge-font'),
65+
FileUpload::make('badge_background')
66+
->label(__("Badge Background"))
67+
->helperText("Present on disk: " . ($badge_background ? 'Yes' : 'No'))
68+
->required()
69+
->getUploadedFileNameForStorageUsing(fn(TemporaryUploadedFile $file): string => "badge-background")
70+
->disk("local")
71+
->directory("badges")
72+
->default('badge-background')
73+
->image(),
74+
]
75+
)
76+
->statePath('data');
77+
}
78+
79+
public function create(bool $another = false): void
80+
{
81+
// Force processing of the uploaded files
82+
$this->form->getState();
83+
84+
Notification::make()
85+
->success()
86+
->title("Bogos Binted")
87+
->send();
88+
}
89+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?php
2+
3+
namespace App\Filament\Resources\BadgeResource\Pages;
4+
5+
use App\Filament\Resources\BadgeResource;
6+
use Filament\Actions;
7+
use Filament\Resources\Pages\ListRecords;
8+
use Illuminate\Support\Facades\Storage;
9+
10+
class ListBadges extends ListRecords
11+
{
12+
protected static string $resource = BadgeResource::class;
13+
14+
protected function getHeaderActions(): array
15+
{
16+
$actions = [];
17+
18+
$settingsAction = Actions\Action::make("settings")
19+
->label(__("Settings"))
20+
->icon('heroicon-o-cog-6-tooth')
21+
->url(fn () => BadgeResource::getUrl('settings'));
22+
23+
24+
// Check if everything looks good
25+
$badge_font = Storage::disk('local')->exists('badges/badge-font');
26+
$badge_background = Storage::disk('local')->exists('badges/badge-background');
27+
if (!$badge_font || !$badge_background) {
28+
$settingsAction = $settingsAction
29+
->color("danger")
30+
->icon('heroicon-o-exclamation-triangle');
31+
}
32+
33+
// Yeet
34+
$actions[] = $settingsAction;
35+
36+
$actions[] = Actions\CreateAction::make()
37+
->icon('heroicon-o-paint-brush')
38+
->label(__("Custom Badge"));
39+
return $actions;
40+
}
41+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
<?php
2+
3+
namespace App\Filament\Resources\BadgeResource\Pages;
4+
5+
use App\Filament\Resources\BadgeResource;
6+
use App\Services\BadgeService;
7+
use Filament\Actions\Action;
8+
use Filament\Resources\Pages\CreateRecord;
9+
use Livewire\Component;
10+
11+
class PrintBadge extends CreateRecord
12+
{
13+
protected static string $resource = BadgeResource::class;
14+
protected static bool $canCreateAnother = false;
15+
16+
protected function getFormActions(): array
17+
{
18+
return [
19+
Action::make('create')->action(function (Component $livewire) {
20+
$data = $livewire->data;
21+
$type = 'custom';
22+
if (array_key_exists('badge_type', $data) && !is_null($data['badge_type'])) {
23+
$type = $data['badge_type'];
24+
}
25+
if ($type == 'custom' && array_key_exists('badge_type_custom', $data) && !is_null($data['badge_type_custom'])) {
26+
$type = $data['badge_type_custom'];
27+
}
28+
$regId = $data['reg_id'];
29+
$displayName = $data['display_name'];
30+
$tableNumber = null;
31+
if ($data['has_table'] && array_key_exists('table_number', $data) && !is_null($data['table_number'])) {
32+
$tableNumber = $data['table_number'];
33+
}
34+
$shareIndicator = $data['has_share'];
35+
$doubleSided = $data['double_sided'];
36+
37+
$badgeService = new BadgeService();
38+
$badgeService->generateCustomBadge(
39+
$type,
40+
$regId,
41+
$displayName,
42+
$tableNumber,
43+
$shareIndicator,
44+
$doubleSided
45+
);
46+
47+
$badgeFile = tmpfile();
48+
$badgeFileUri = stream_get_meta_data($badgeFile)['uri'];
49+
$badgeService->save($badgeFile);
50+
fflush($badgeFile);
51+
52+
// Note, we need to pass the $badgeFile to the closure so PHP does not remove it prematurely
53+
return response()->streamDownload(function () use ($badgeFile, $badgeFileUri) {
54+
echo file_get_contents($badgeFileUri);
55+
}, "DD Badge - " . date("Y-m-d\TH-i-sO") . " - $regId $displayName.pdf");
56+
}),
57+
];
58+
}
59+
60+
public function create(bool $another = false): void
61+
{
62+
// KLUDGE: A dummy since we use the form action
63+
$this->getCreatedNotification()?->send();
64+
}
65+
}

0 commit comments

Comments
 (0)