Initialize repo and add session changes (2025-09-08)
This commit is contained in:
192
app/Filament/Resources/EmotionResource.php
Normal file
192
app/Filament/Resources/EmotionResource.php
Normal file
@@ -0,0 +1,192 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources;
|
||||
|
||||
use App\Filament\Resources\EmotionResource\Pages;
|
||||
use App\Models\Emotion;
|
||||
use Filament\Schemas\Schema as Schema;
|
||||
use Filament\Schemas\Components as SC;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class EmotionResource extends Resource
|
||||
{
|
||||
protected static ?string $model = Emotion::class;
|
||||
protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-face-smile';
|
||||
protected static string|\UnitEnum|null $navigationGroup = 'Library';
|
||||
protected static ?int $navigationSort = 10;
|
||||
|
||||
public static function form(Schema $schema): Schema
|
||||
{
|
||||
return $schema->components([
|
||||
SC\KeyValue::make('name')->label('Name (de/en)')->keyLabel('locale')->valueLabel('value')->default(['de' => '', 'en' => ''])->required(),
|
||||
SC\TextInput::make('icon')->label('Icon/Emoji')->maxLength(50),
|
||||
SC\TextInput::make('color')->maxLength(7)->helperText('#RRGGBB'),
|
||||
SC\KeyValue::make('description')->label('Description (de/en)')->keyLabel('locale')->valueLabel('value'),
|
||||
SC\TextInput::make('sort_order')->numeric()->default(0),
|
||||
SC\Toggle::make('is_active')->default(true),
|
||||
SC\Select::make('eventTypes')
|
||||
->label('Event Types')
|
||||
->multiple()
|
||||
->searchable()
|
||||
->preload()
|
||||
->relationship('eventTypes', 'name'),
|
||||
])->columns(2);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
Tables\Columns\TextColumn::make('id')->sortable(),
|
||||
Tables\Columns\TextColumn::make('name')->searchable(),
|
||||
Tables\Columns\TextColumn::make('icon'),
|
||||
Tables\Columns\TextColumn::make('color'),
|
||||
Tables\Columns\IconColumn::make('is_active')->boolean(),
|
||||
Tables\Columns\TextColumn::make('sort_order')->sortable(),
|
||||
])
|
||||
->filters([])
|
||||
->actions([
|
||||
Tables\Actions\EditAction::make(),
|
||||
])
|
||||
->bulkActions([
|
||||
Tables\Actions\BulkActionGroup::make([
|
||||
Tables\Actions\DeleteBulkAction::make(),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => Pages\ManageEmotions::route('/'),
|
||||
'import' => Pages\ImportEmotions::route('/import'),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
namespace App\Filament\Resources\EmotionResource\Pages;
|
||||
|
||||
use App\Filament\Resources\EmotionResource;
|
||||
use Filament\Resources\Pages\ManageRecords;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\Page;
|
||||
use Filament\Forms;
|
||||
use Filament\Notifications\Notification;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class ManageEmotions extends ManageRecords
|
||||
{
|
||||
protected static string $resource = EmotionResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\Action::make('import')
|
||||
->label('Import CSV')
|
||||
->icon('heroicon-o-arrow-up-tray')
|
||||
->url(EmotionResource::getUrl('import')),
|
||||
Actions\Action::make('template')
|
||||
->label('Download CSV Template')
|
||||
->icon('heroicon-o-document-arrow-down')
|
||||
->url(url('/super-admin/templates/emotions.csv')),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
class ImportEmotions extends Page
|
||||
{
|
||||
protected static string $resource = EmotionResource::class;
|
||||
protected string $view = 'filament.pages.blank';
|
||||
protected ?string $heading = 'Import Emotions (CSV)';
|
||||
|
||||
public ?string $file = null;
|
||||
|
||||
protected function getFormSchema(): array
|
||||
{
|
||||
return [
|
||||
Forms\Components\FileUpload::make('file')
|
||||
->label('CSV file')
|
||||
->acceptedFileTypes(['text/csv', 'text/plain'])
|
||||
->directory('imports')
|
||||
->required(),
|
||||
];
|
||||
}
|
||||
|
||||
protected function getFormActions(): array
|
||||
{
|
||||
return [
|
||||
Forms\Components\Actions\Action::make('import')
|
||||
->label('Import')
|
||||
->action('doImport')
|
||||
->color('primary')
|
||||
];
|
||||
}
|
||||
|
||||
public function doImport(): void
|
||||
{
|
||||
$state = $this->form->getState();
|
||||
$path = $state['file'] ?? null;
|
||||
if (! $path || ! Storage::disk('public')->exists($path)) {
|
||||
Notification::make()->danger()->title('File not found')->send();
|
||||
return;
|
||||
}
|
||||
$full = Storage::disk('public')->path($path);
|
||||
[$ok, $fail] = $this->importEmotionsCsv($full);
|
||||
Notification::make()->success()->title("Imported {$ok} rows")->body($fail ? "{$fail} failed" : null)->send();
|
||||
}
|
||||
|
||||
private function importEmotionsCsv(string $file): array
|
||||
{
|
||||
$h = fopen($file, 'r');
|
||||
if (! $h) return [0,0];
|
||||
$ok = 0; $fail = 0;
|
||||
// Expected headers: name_de,name_en,icon,color,description_de,description_en,sort_order,is_active,event_types
|
||||
$headers = fgetcsv($h, 0, ',');
|
||||
if (! $headers) return [0,0];
|
||||
$map = array_flip($headers);
|
||||
while (($row = fgetcsv($h, 0, ',')) !== false) {
|
||||
try {
|
||||
$nameDe = trim($row[$map['name_de']] ?? '');
|
||||
$nameEn = trim($row[$map['name_en']] ?? '');
|
||||
$name = $nameDe ?: $nameEn;
|
||||
if ($name === '') { $fail++; continue; }
|
||||
$data = [
|
||||
'name' => ['de' => $nameDe, 'en' => $nameEn],
|
||||
'icon' => $row[$map['icon']] ?? null,
|
||||
'color' => $row[$map['color']] ?? null,
|
||||
'description' => [
|
||||
'de' => $row[$map['description_de']] ?? null,
|
||||
'en' => $row[$map['description_en']] ?? null,
|
||||
],
|
||||
'sort_order' => (int)($row[$map['sort_order']] ?? 0),
|
||||
'is_active' => (int)($row[$map['is_active']] ?? 1) ? 1 : 0,
|
||||
];
|
||||
$id = DB::table('emotions')->insertGetId(array_merge($data, [
|
||||
'created_at' => now(), 'updated_at' => now(),
|
||||
]));
|
||||
// Attach event types if provided (by slug list separated by '|')
|
||||
$et = $row[$map['event_types']] ?? '';
|
||||
if ($et) {
|
||||
$slugs = array_filter(array_map('trim', explode('|', $et)));
|
||||
if ($slugs) {
|
||||
$ids = DB::table('event_types')->whereIn('slug', $slugs)->pluck('id')->all();
|
||||
foreach ($ids as $eid) {
|
||||
DB::table('emotion_event_type')->insertOrIgnore([
|
||||
'emotion_id' => $id,
|
||||
'event_type_id' => $eid,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
$ok++;
|
||||
} catch (\Throwable $e) {
|
||||
$fail++;
|
||||
}
|
||||
}
|
||||
fclose($h);
|
||||
return [$ok, $fail];
|
||||
}
|
||||
}
|
||||
89
app/Filament/Resources/EventResource.php
Normal file
89
app/Filament/Resources/EventResource.php
Normal file
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources;
|
||||
|
||||
use App\Filament\Resources\EventResource\Pages;
|
||||
use App\Models\Event;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class EventResource extends Resource
|
||||
{
|
||||
protected static ?string $model = Event::class;
|
||||
protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-calendar';
|
||||
protected static string|\UnitEnum|null $navigationGroup = 'Platform';
|
||||
protected static ?int $navigationSort = 20;
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
Tables\Columns\TextColumn::make('id')->sortable(),
|
||||
Tables\Columns\TextColumn::make('tenant_id')->label('Tenant')->sortable(),
|
||||
Tables\Columns\TextColumn::make('name')->limit(30),
|
||||
Tables\Columns\TextColumn::make('slug')->searchable(),
|
||||
Tables\Columns\TextColumn::make('date')->date(),
|
||||
Tables\Columns\IconColumn::make('is_active')->boolean(),
|
||||
Tables\Columns\TextColumn::make('default_locale'),
|
||||
Tables\Columns\TextColumn::make('join')->label('Join')
|
||||
->getStateUsing(fn($record) => url("/e/{$record->slug}"))
|
||||
->copyable()
|
||||
->copyMessage('Join link copied'),
|
||||
Tables\Columns\TextColumn::make('created_at')->since(),
|
||||
])
|
||||
->filters([])
|
||||
->actions([
|
||||
Tables\Actions\ViewAction::make(),
|
||||
Tables\Actions\EditAction::make(),
|
||||
Tables\Actions\Action::make('toggle')
|
||||
->label('Toggle Active')
|
||||
->icon('heroicon-o-power')
|
||||
->action(fn($record) => $record->update(['is_active' => ! (bool)$record->is_active])),
|
||||
Tables\Actions\Action::make('join_link')
|
||||
->label('Join Link / QR')
|
||||
->icon('heroicon-o-qr-code')
|
||||
->modalHeading('Event Join Link')
|
||||
->modalSubmitActionLabel('Close')
|
||||
->modalContent(fn($record) => view('filament.events.join-link', [
|
||||
'link' => url("/e/{$record->slug}"),
|
||||
])),
|
||||
])
|
||||
->bulkActions([
|
||||
Tables\Actions\BulkActionGroup::make([
|
||||
Tables\Actions\DeleteBulkAction::make(),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => Pages\ListEvents::route('/'),
|
||||
'view' => Pages\ViewEvent::route('/{record}'),
|
||||
'edit' => Pages\EditEvent::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
namespace App\Filament\Resources\EventResource\Pages;
|
||||
|
||||
use App\Filament\Resources\EventResource;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
use Filament\Resources\Pages\ViewRecord;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
|
||||
class ListEvents extends ListRecords
|
||||
{
|
||||
protected static string $resource = EventResource::class;
|
||||
}
|
||||
|
||||
class ViewEvent extends ViewRecord
|
||||
{
|
||||
protected static string $resource = EventResource::class;
|
||||
}
|
||||
|
||||
class EditEvent extends EditRecord
|
||||
{
|
||||
protected static string $resource = EventResource::class;
|
||||
}
|
||||
73
app/Filament/Resources/EventTypeResource.php
Normal file
73
app/Filament/Resources/EventTypeResource.php
Normal file
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources;
|
||||
|
||||
use App\Filament\Resources\EventTypeResource\Pages;
|
||||
use App\Models\EventType;
|
||||
use Filament\Schemas\Schema as Schema;
|
||||
use Filament\Schemas\Components as SC;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class EventTypeResource extends Resource
|
||||
{
|
||||
protected static ?string $model = EventType::class;
|
||||
protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-swatch';
|
||||
protected static string|\UnitEnum|null $navigationGroup = 'Library';
|
||||
protected static ?int $navigationSort = 20;
|
||||
|
||||
public static function form(Schema $schema): Schema
|
||||
{
|
||||
return $schema->components([
|
||||
SC\KeyValue::make('name')->label('Name (de/en)')->default(['de' => '', 'en' => ''])->required(),
|
||||
SC\TextInput::make('slug')->required()->unique(ignoreRecord: true),
|
||||
SC\TextInput::make('icon')->maxLength(64),
|
||||
SC\KeyValue::make('settings')->label('Settings')->keyLabel('key')->valueLabel('value'),
|
||||
SC\Select::make('emotions')
|
||||
->label('Emotions')
|
||||
->multiple()
|
||||
->searchable()
|
||||
->preload()
|
||||
->relationship('emotions', 'name'),
|
||||
])->columns(2);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
Tables\Columns\TextColumn::make('id')->sortable(),
|
||||
Tables\Columns\TextColumn::make('name')->searchable(),
|
||||
Tables\Columns\TextColumn::make('slug')->searchable(),
|
||||
Tables\Columns\TextColumn::make('icon'),
|
||||
Tables\Columns\TextColumn::make('created_at')->since(),
|
||||
])
|
||||
->filters([])
|
||||
->actions([
|
||||
Tables\Actions\EditAction::make(),
|
||||
])
|
||||
->bulkActions([
|
||||
Tables\Actions\BulkActionGroup::make([
|
||||
Tables\Actions\DeleteBulkAction::make(),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => Pages\ManageEventTypes::route('/'),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
namespace App\Filament\Resources\EventTypeResource\Pages;
|
||||
|
||||
use App\Filament\Resources\EventTypeResource;
|
||||
use Filament\Resources\Pages\ManageRecords;
|
||||
|
||||
class ManageEventTypes extends ManageRecords
|
||||
{
|
||||
protected static string $resource = EventTypeResource::class;
|
||||
}
|
||||
71
app/Filament/Resources/LegalPageResource.php
Normal file
71
app/Filament/Resources/LegalPageResource.php
Normal file
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources;
|
||||
|
||||
use App\Filament\Resources\LegalPageResource\Pages;
|
||||
use App\Models\LegalPage;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class LegalPageResource extends Resource
|
||||
{
|
||||
protected static ?string $model = LegalPage::class;
|
||||
protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-scale';
|
||||
protected static string|\UnitEnum|null $navigationGroup = 'Platform';
|
||||
protected static ?int $navigationSort = 40;
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
Tables\Columns\TextColumn::make('id')->sortable(),
|
||||
Tables\Columns\TextColumn::make('slug')->badge(),
|
||||
Tables\Columns\TextColumn::make('version')->badge(),
|
||||
Tables\Columns\IconColumn::make('is_published')->boolean(),
|
||||
Tables\Columns\TextColumn::make('effective_from')->date(),
|
||||
Tables\Columns\TextColumn::make('created_at')->since(),
|
||||
])
|
||||
->filters([])
|
||||
->actions([
|
||||
Tables\Actions\ViewAction::make(),
|
||||
Tables\Actions\EditAction::make(),
|
||||
])
|
||||
->bulkActions([
|
||||
Tables\Actions\BulkActionGroup::make([
|
||||
Tables\Actions\DeleteBulkAction::make(),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => Pages\ListLegalPages::route('/'),
|
||||
'view' => Pages\ViewLegalPage::route('/{record}'),
|
||||
'edit' => Pages\EditLegalPage::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
namespace App\Filament\Resources\LegalPageResource\Pages;
|
||||
|
||||
use App\Filament\Resources\LegalPageResource;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
use Filament\Resources\Pages\ViewRecord;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
|
||||
class ListLegalPages extends ListRecords
|
||||
{
|
||||
protected static string $resource = LegalPageResource::class;
|
||||
}
|
||||
|
||||
class ViewLegalPage extends ViewRecord
|
||||
{
|
||||
protected static string $resource = LegalPageResource::class;
|
||||
}
|
||||
|
||||
class EditLegalPage extends EditRecord
|
||||
{
|
||||
protected static string $resource = LegalPageResource::class;
|
||||
}
|
||||
90
app/Filament/Resources/PhotoResource.php
Normal file
90
app/Filament/Resources/PhotoResource.php
Normal file
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources;
|
||||
|
||||
use App\Filament\Resources\PhotoResource\Pages;
|
||||
use App\Models\Photo;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class PhotoResource extends Resource
|
||||
{
|
||||
protected static ?string $model = Photo::class;
|
||||
protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-photo';
|
||||
protected static string|\UnitEnum|null $navigationGroup = 'Content';
|
||||
protected static ?int $navigationSort = 30;
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
Tables\Columns\ImageColumn::make('thumbnail_path')->label('Thumb')->circular(),
|
||||
Tables\Columns\TextColumn::make('id')->sortable(),
|
||||
Tables\Columns\TextColumn::make('event_id')->label('Event'),
|
||||
Tables\Columns\TextColumn::make('likes_count')->label('Likes'),
|
||||
Tables\Columns\IconColumn::make('is_featured')->boolean(),
|
||||
Tables\Columns\TextColumn::make('created_at')->since(),
|
||||
])
|
||||
->filters([])
|
||||
->actions([
|
||||
Tables\Actions\ViewAction::make(),
|
||||
Tables\Actions\EditAction::make(),
|
||||
Tables\Actions\Action::make('feature')
|
||||
->label('Feature')
|
||||
->visible(fn($record) => ! (bool)$record->is_featured)
|
||||
->action(fn($record) => $record->update(['is_featured' => 1]))
|
||||
->icon('heroicon-o-star'),
|
||||
Tables\Actions\Action::make('unfeature')
|
||||
->label('Unfeature')
|
||||
->visible(fn($record) => (bool)$record->is_featured)
|
||||
->action(fn($record) => $record->update(['is_featured' => 0]))
|
||||
->icon('heroicon-o-star'),
|
||||
Tables\Actions\DeleteAction::make(),
|
||||
])
|
||||
->bulkActions([
|
||||
Tables\Actions\BulkActionGroup::make([
|
||||
Tables\Actions\BulkAction::make('feature')
|
||||
->label('Feature selected')
|
||||
->icon('heroicon-o-star')
|
||||
->action(fn($records) => $records->each->update(['is_featured' => 1])),
|
||||
Tables\Actions\BulkAction::make('unfeature')
|
||||
->label('Unfeature selected')
|
||||
->icon('heroicon-o-star')
|
||||
->action(fn($records) => $records->each->update(['is_featured' => 0])),
|
||||
Tables\Actions\DeleteBulkAction::make(),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => Pages\ListPhotos::route('/'),
|
||||
'view' => Pages\ViewPhoto::route('/{record}'),
|
||||
'edit' => Pages\EditPhoto::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
namespace App\Filament\Resources\PhotoResource\Pages;
|
||||
|
||||
use App\Filament\Resources\PhotoResource;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
use Filament\Resources\Pages\ViewRecord;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
|
||||
class ListPhotos extends ListRecords
|
||||
{
|
||||
protected static string $resource = PhotoResource::class;
|
||||
}
|
||||
|
||||
class ViewPhoto extends ViewRecord
|
||||
{
|
||||
protected static string $resource = PhotoResource::class;
|
||||
}
|
||||
|
||||
class EditPhoto extends EditRecord
|
||||
{
|
||||
protected static string $resource = PhotoResource::class;
|
||||
}
|
||||
203
app/Filament/Resources/TaskResource.php
Normal file
203
app/Filament/Resources/TaskResource.php
Normal file
@@ -0,0 +1,203 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources;
|
||||
|
||||
use App\Filament\Resources\TaskResource\Pages;
|
||||
use App\Models\Task;
|
||||
use Filament\Schemas\Schema as Schema;
|
||||
use Filament\Schemas\Components as SC;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class TaskResource extends Resource
|
||||
{
|
||||
protected static ?string $model = Task::class;
|
||||
protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-clipboard-document-check';
|
||||
protected static string|\UnitEnum|null $navigationGroup = 'Library';
|
||||
protected static ?int $navigationSort = 30;
|
||||
|
||||
public static function form(Schema $schema): Schema
|
||||
{
|
||||
return $schema->components([
|
||||
SC\Select::make('emotion_id')->relationship('emotion', 'name')->required()->searchable()->preload(),
|
||||
SC\Select::make('event_type_id')->relationship('eventType', 'name')->searchable()->preload()->label('Event Type (optional)'),
|
||||
SC\KeyValue::make('title')->label('Title (de/en)')->default(['de' => '', 'en' => ''])->required(),
|
||||
SC\KeyValue::make('description')->label('Description (de/en)'),
|
||||
SC\Select::make('difficulty')->options([
|
||||
'easy' => 'Easy',
|
||||
'medium' => 'Medium',
|
||||
'hard' => 'Hard',
|
||||
])->default('easy'),
|
||||
SC\KeyValue::make('example_text')->label('Example (de/en)'),
|
||||
SC\TextInput::make('sort_order')->numeric()->default(0),
|
||||
SC\Toggle::make('is_active')->default(true),
|
||||
])->columns(2);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
Tables\Columns\TextColumn::make('id')->sortable(),
|
||||
Tables\Columns\TextColumn::make('emotion.name')->label('Emotion')->sortable()->searchable(),
|
||||
Tables\Columns\TextColumn::make('eventType.name')->label('Event Type')->toggleable(),
|
||||
Tables\Columns\TextColumn::make('title')->searchable()->limit(40),
|
||||
Tables\Columns\TextColumn::make('difficulty')->badge(),
|
||||
Tables\Columns\IconColumn::make('is_active')->boolean(),
|
||||
Tables\Columns\TextColumn::make('sort_order')->sortable(),
|
||||
])
|
||||
->filters([])
|
||||
->actions([
|
||||
Tables\Actions\EditAction::make(),
|
||||
Tables\Actions\DeleteAction::make(),
|
||||
])
|
||||
->bulkActions([
|
||||
Tables\Actions\BulkActionGroup::make([
|
||||
Tables\Actions\DeleteBulkAction::make(),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => Pages\ManageTasks::route('/'),
|
||||
'import' => Pages\ImportTasks::route('/import'),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
namespace App\Filament\Resources\TaskResource\Pages;
|
||||
|
||||
use App\Filament\Resources\TaskResource;
|
||||
use Filament\Resources\Pages\ManageRecords;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\Page;
|
||||
use Filament\Forms;
|
||||
use Filament\Notifications\Notification;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class ManageTasks extends ManageRecords
|
||||
{
|
||||
protected static string $resource = TaskResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\Action::make('import')
|
||||
->label('Import CSV')
|
||||
->icon('heroicon-o-arrow-up-tray')
|
||||
->url(TaskResource::getUrl('import')),
|
||||
Actions\Action::make('template')
|
||||
->label('Download CSV Template')
|
||||
->icon('heroicon-o-document-arrow-down')
|
||||
->url(url('/super-admin/templates/tasks.csv')),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
class ImportTasks extends Page
|
||||
{
|
||||
protected static string $resource = TaskResource::class;
|
||||
protected string $view = 'filament.pages.blank';
|
||||
protected ?string $heading = 'Import Tasks (CSV)';
|
||||
|
||||
public ?string $file = null;
|
||||
|
||||
protected function getFormSchema(): array
|
||||
{
|
||||
return [
|
||||
Forms\Components\FileUpload::make('file')
|
||||
->label('CSV file')
|
||||
->acceptedFileTypes(['text/csv', 'text/plain'])
|
||||
->directory('imports')
|
||||
->required(),
|
||||
];
|
||||
}
|
||||
|
||||
protected function getFormActions(): array
|
||||
{
|
||||
return [
|
||||
Forms\Components\Actions\Action::make('import')
|
||||
->label('Import')
|
||||
->action('doImport')
|
||||
->color('primary')
|
||||
];
|
||||
}
|
||||
|
||||
public function doImport(): void
|
||||
{
|
||||
$state = $this->form->getState();
|
||||
$path = $state['file'] ?? null;
|
||||
if (! $path || ! Storage::disk('public')->exists($path)) {
|
||||
Notification::make()->danger()->title('File not found')->send();
|
||||
return;
|
||||
}
|
||||
$full = Storage::disk('public')->path($path);
|
||||
[$ok, $fail] = $this->importTasksCsv($full);
|
||||
Notification::make()->success()->title("Imported {$ok} rows")->body($fail ? "${fail} failed" : null)->send();
|
||||
}
|
||||
|
||||
private function importTasksCsv(string $file): array
|
||||
{
|
||||
$h = fopen($file, 'r');
|
||||
if (! $h) return [0,0];
|
||||
$ok = 0; $fail = 0;
|
||||
// Expected headers: emotion_name,emotion_name_de,emotion_name_en,event_type_slug,title_de,title_en,description_de,description_en,difficulty,example_text_de,example_text_en,sort_order,is_active
|
||||
$headers = fgetcsv($h, 0, ',');
|
||||
if (! $headers) return [0,0];
|
||||
$map = array_flip($headers);
|
||||
while (($row = fgetcsv($h, 0, ',')) !== false) {
|
||||
try {
|
||||
$emotionName = trim($row[$map['emotion_name']] ?? '');
|
||||
$emotionNameDe = trim($row[$map['emotion_name_de']] ?? '');
|
||||
$emotionNameEn = trim($row[$map['emotion_name_en']] ?? '');
|
||||
$emotionId = null;
|
||||
if ($emotionName !== '') {
|
||||
$emotionId = DB::table('emotions')->where('name', $emotionName)->value('id');
|
||||
}
|
||||
if (! $emotionId && $emotionNameDe !== '') {
|
||||
$emotionId = DB::table('emotions')->where('name', 'like', '%"de":"'.str_replace('"','""',$emotionNameDe).'"%')->value('id');
|
||||
}
|
||||
if (! $emotionId && $emotionNameEn !== '') {
|
||||
$emotionId = DB::table('emotions')->where('name', 'like', '%"en":"'.str_replace('"','""',$emotionNameEn).'"%')->value('id');
|
||||
}
|
||||
if (! $emotionId) { $fail++; continue; }
|
||||
$eventTypeSlug = trim($row[$map['event_type_slug']] ?? '');
|
||||
$eventTypeId = null;
|
||||
if ($eventTypeSlug !== '') {
|
||||
$eventTypeId = DB::table('event_types')->where('slug', $eventTypeSlug)->value('id');
|
||||
}
|
||||
$data = [
|
||||
'emotion_id' => $emotionId,
|
||||
'event_type_id' => $eventTypeId,
|
||||
'title' => [
|
||||
'de' => $row[$map['title_de']] ?? null,
|
||||
'en' => $row[$map['title_en']] ?? null,
|
||||
],
|
||||
'description' => [
|
||||
'de' => $row[$map['description_de']] ?? null,
|
||||
'en' => $row[$map['description_en']] ?? null,
|
||||
],
|
||||
'difficulty' => $row[$map['difficulty']] ?? 'easy',
|
||||
'example_text' => [
|
||||
'de' => $row[$map['example_text_de']] ?? null,
|
||||
'en' => $row[$map['example_text_en']] ?? null,
|
||||
],
|
||||
'sort_order' => (int)($row[$map['sort_order']] ?? 0),
|
||||
'is_active' => (int)($row[$map['is_active']] ?? 1) ? 1 : 0,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
];
|
||||
DB::table('tasks')->insert($data);
|
||||
$ok++;
|
||||
} catch (\Throwable $e) {
|
||||
$fail++;
|
||||
}
|
||||
}
|
||||
fclose($h);
|
||||
return [$ok, $fail];
|
||||
}
|
||||
}
|
||||
72
app/Filament/Resources/TenantResource.php
Normal file
72
app/Filament/Resources/TenantResource.php
Normal file
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources;
|
||||
|
||||
use App\Filament\Resources\TenantResource\Pages;
|
||||
use App\Models\Tenant;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class TenantResource extends Resource
|
||||
{
|
||||
protected static ?string $model = Tenant::class;
|
||||
protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-building-office';
|
||||
protected static string|\UnitEnum|null $navigationGroup = 'Platform';
|
||||
protected static ?int $navigationSort = 10;
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
Tables\Columns\TextColumn::make('id')->sortable(),
|
||||
Tables\Columns\TextColumn::make('name')->searchable()->sortable(),
|
||||
Tables\Columns\TextColumn::make('slug')->searchable(),
|
||||
Tables\Columns\TextColumn::make('contact_email'),
|
||||
Tables\Columns\TextColumn::make('event_credits_balance')->label('Credits'),
|
||||
Tables\Columns\TextColumn::make('last_activity_at')->since()->label('Last activity'),
|
||||
Tables\Columns\TextColumn::make('created_at')->dateTime()->toggleable(isToggledHiddenByDefault: true),
|
||||
])
|
||||
->filters([])
|
||||
->actions([
|
||||
Tables\Actions\ViewAction::make(),
|
||||
Tables\Actions\EditAction::make(),
|
||||
])
|
||||
->bulkActions([
|
||||
Tables\Actions\BulkActionGroup::make([
|
||||
Tables\Actions\DeleteBulkAction::make(),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => Pages\ListTenants::route('/'),
|
||||
'view' => Pages\ViewTenant::route('/{record}'),
|
||||
'edit' => Pages\EditTenant::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
namespace App\Filament\Resources\TenantResource\Pages;
|
||||
|
||||
use App\Filament\Resources\TenantResource;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
use Filament\Resources\Pages\ViewRecord;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
|
||||
class ListTenants extends ListRecords
|
||||
{
|
||||
protected static string $resource = TenantResource::class;
|
||||
}
|
||||
|
||||
class ViewTenant extends ViewRecord
|
||||
{
|
||||
protected static string $resource = TenantResource::class;
|
||||
}
|
||||
|
||||
class EditTenant extends EditRecord
|
||||
{
|
||||
protected static string $resource = TenantResource::class;
|
||||
}
|
||||
40
app/Filament/Widgets/EventsActiveToday.php
Normal file
40
app/Filament/Widgets/EventsActiveToday.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Widgets;
|
||||
|
||||
use Filament\Tables;
|
||||
use Filament\Widgets\TableWidget as BaseWidget;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Carbon;
|
||||
|
||||
class EventsActiveToday extends BaseWidget
|
||||
{
|
||||
protected static ?string $heading = 'Events active today';
|
||||
protected ?string $pollingInterval = '60s';
|
||||
|
||||
public function table(Tables\Table $table): Tables\Table
|
||||
{
|
||||
$today = Carbon::today()->toDateString();
|
||||
$query = DB::table('events as e')
|
||||
->leftJoin('photos as p', function ($join) use ($today) {
|
||||
$join->on('p.event_id', '=', 'e.id')
|
||||
->whereRaw("date(p.created_at) = ?", [$today]);
|
||||
})
|
||||
->where('e.is_active', 1)
|
||||
->whereDate('e.date', '<=', $today)
|
||||
->selectRaw('e.id, e.slug, e.name, e.date, COUNT(p.id) as uploads_today')
|
||||
->groupBy('e.id', 'e.slug', 'e.name', 'e.date')
|
||||
->orderBy('e.date', 'desc')
|
||||
->limit(10);
|
||||
|
||||
return $table
|
||||
->query($query)
|
||||
->columns([
|
||||
Tables\Columns\TextColumn::make('id')->label('#')->width('60px'),
|
||||
Tables\Columns\TextColumn::make('slug')->label('Slug')->searchable(),
|
||||
Tables\Columns\TextColumn::make('date')->date(),
|
||||
Tables\Columns\TextColumn::make('uploads_today')->label('Uploads today')->numeric(),
|
||||
])
|
||||
->paginated(false);
|
||||
}
|
||||
}
|
||||
31
app/Filament/Widgets/PlatformStatsWidget.php
Normal file
31
app/Filament/Widgets/PlatformStatsWidget.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Widgets;
|
||||
|
||||
use Filament\Widgets\StatsOverviewWidget as BaseWidget;
|
||||
use Filament\Widgets\StatsOverviewWidget\Stat;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Carbon;
|
||||
|
||||
class PlatformStatsWidget extends BaseWidget
|
||||
{
|
||||
protected ?string $pollingInterval = '30s';
|
||||
|
||||
protected function getStats(): array
|
||||
{
|
||||
$now = Carbon::now();
|
||||
$dayAgo = $now->copy()->subDay();
|
||||
|
||||
$tenants = (int) DB::table('tenants')->count();
|
||||
$events = (int) DB::table('events')->count();
|
||||
$photos = (int) DB::table('photos')->count();
|
||||
$photos24h = (int) DB::table('photos')->where('created_at', '>=', $dayAgo)->count();
|
||||
|
||||
return [
|
||||
Stat::make('Tenants', number_format($tenants)),
|
||||
Stat::make('Events', number_format($events)),
|
||||
Stat::make('Photos', number_format($photos))
|
||||
->description("+{$photos24h} in last 24h"),
|
||||
];
|
||||
}
|
||||
}
|
||||
39
app/Filament/Widgets/RecentPhotosTable.php
Normal file
39
app/Filament/Widgets/RecentPhotosTable.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Widgets;
|
||||
|
||||
use Filament\Tables;
|
||||
use Filament\Widgets\TableWidget as BaseWidget;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class RecentPhotosTable extends BaseWidget
|
||||
{
|
||||
protected static ?string $heading = 'Recent uploads';
|
||||
protected int|string|array $columnSpan = 'full';
|
||||
|
||||
public function table(Tables\Table $table): Tables\Table
|
||||
{
|
||||
$query = DB::table('photos')->orderByDesc('created_at')->limit(10);
|
||||
|
||||
return $table
|
||||
->query($query)
|
||||
->columns([
|
||||
Tables\Columns\ImageColumn::make('thumbnail_path')->label('Thumb')->circular(),
|
||||
Tables\Columns\TextColumn::make('id')->label('#'),
|
||||
Tables\Columns\TextColumn::make('event_id')->label('Event'),
|
||||
Tables\Columns\TextColumn::make('likes_count')->label('Likes'),
|
||||
Tables\Columns\TextColumn::make('created_at')->since(),
|
||||
])
|
||||
->actions([
|
||||
Tables\Actions\Action::make('feature')
|
||||
->label('Feature')
|
||||
->visible(fn($record) => ! (bool)($record->is_featured ?? 0))
|
||||
->action(fn($record) => DB::table('photos')->where('id', $record->id)->update(['is_featured' => 1, 'updated_at' => now()])),
|
||||
Tables\Actions\Action::make('unfeature')
|
||||
->label('Unfeature')
|
||||
->visible(fn($record) => (bool)($record->is_featured ?? 0))
|
||||
->action(fn($record) => DB::table('photos')->where('id', $record->id)->update(['is_featured' => 0, 'updated_at' => now()])),
|
||||
])
|
||||
->paginated(false);
|
||||
}
|
||||
}
|
||||
33
app/Filament/Widgets/TopTenantsByUploads.php
Normal file
33
app/Filament/Widgets/TopTenantsByUploads.php
Normal file
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Widgets;
|
||||
|
||||
use Filament\Tables;
|
||||
use Filament\Widgets\TableWidget as BaseWidget;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class TopTenantsByUploads extends BaseWidget
|
||||
{
|
||||
protected static ?string $heading = 'Top tenants by uploads';
|
||||
protected ?string $pollingInterval = '60s';
|
||||
|
||||
public function table(Tables\Table $table): Tables\Table
|
||||
{
|
||||
$query = DB::table('photos as p')
|
||||
->join('events as e', 'e.id', '=', 'p.event_id')
|
||||
->join('tenants as t', 't.id', '=', 'e.tenant_id')
|
||||
->selectRaw('t.id as tenant_id, t.name as tenant_name, COUNT(p.id) as uploads')
|
||||
->groupBy('t.id', 't.name')
|
||||
->orderByDesc('uploads')
|
||||
->limit(5);
|
||||
|
||||
return $table
|
||||
->query($query)
|
||||
->columns([
|
||||
Tables\Columns\TextColumn::make('tenant_id')->label('#')->width('60px'),
|
||||
Tables\Columns\TextColumn::make('tenant_name')->label('Tenant')->searchable(),
|
||||
Tables\Columns\TextColumn::make('uploads')->label('Uploads')->numeric(),
|
||||
])
|
||||
->paginated(false);
|
||||
}
|
||||
}
|
||||
52
app/Filament/Widgets/UploadsPerDayChart.php
Normal file
52
app/Filament/Widgets/UploadsPerDayChart.php
Normal file
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Widgets;
|
||||
|
||||
use Filament\Widgets\ChartWidget;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Carbon;
|
||||
|
||||
class UploadsPerDayChart extends ChartWidget
|
||||
{
|
||||
protected ?string $heading = 'Uploads (14 days)';
|
||||
protected ?string $maxHeight = '220px';
|
||||
protected ?string $pollingInterval = '60s';
|
||||
|
||||
protected function getData(): array
|
||||
{
|
||||
// Build last 14 days labels
|
||||
$labels = [];
|
||||
$start = Carbon::now()->startOfDay()->subDays(13);
|
||||
for ($i = 0; $i < 14; $i++) {
|
||||
$labels[] = $start->copy()->addDays($i)->format('Y-m-d');
|
||||
}
|
||||
|
||||
// SQLite-friendly group by date
|
||||
$rows = DB::table('photos')
|
||||
->selectRaw("strftime('%Y-%m-%d', created_at) as d, count(*) as c")
|
||||
->where('created_at', '>=', $start)
|
||||
->groupBy('d')
|
||||
->orderBy('d')
|
||||
->get();
|
||||
$map = collect($rows)->keyBy('d');
|
||||
$data = array_map(fn ($d) => (int) ($map[$d]->c ?? 0), $labels);
|
||||
|
||||
return [
|
||||
'labels' => $labels,
|
||||
'datasets' => [
|
||||
[
|
||||
'label' => 'Uploads',
|
||||
'data' => $data,
|
||||
'borderColor' => '#f59e0b',
|
||||
'backgroundColor' => 'rgba(245, 158, 11, 0.2)',
|
||||
'tension' => 0.3,
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
protected function getType(): string
|
||||
{
|
||||
return 'line';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user