StreakFit — API Laravel 13 OSDD + Octane/FrankenPHP
Build & Deploy / build (push) Successful in 18s

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-19 03:00:17 +02:00
co-authored by Claude Opus 4.8
commit af32669931
300 changed files with 21235 additions and 0 deletions
@@ -0,0 +1,48 @@
<?php
namespace App\Filament\Resources\Achievements;
use App\Filament\Resources\Achievements\Pages\CreateAchievement;
use App\Filament\Resources\Achievements\Pages\EditAchievement;
use App\Filament\Resources\Achievements\Pages\ListAchievements;
use App\Filament\Resources\Achievements\Schemas\AchievementForm;
use App\Filament\Resources\Achievements\Tables\AchievementsTable;
use BackedEnum;
use Filament\Resources\Resource;
use Filament\Schemas\Schema;
use Filament\Support\Icons\Heroicon;
use Filament\Tables\Table;
use Functional\Streaks\Models\Achievement;
class AchievementResource extends Resource
{
protected static ?string $model = Achievement::class;
protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedRectangleStack;
public static function form(Schema $schema): Schema
{
return AchievementForm::configure($schema);
}
public static function table(Table $table): Table
{
return AchievementsTable::configure($table);
}
public static function getRelations(): array
{
return [
//
];
}
public static function getPages(): array
{
return [
'index' => ListAchievements::route('/'),
'create' => CreateAchievement::route('/create'),
'edit' => EditAchievement::route('/{record}/edit'),
];
}
}
@@ -0,0 +1,11 @@
<?php
namespace App\Filament\Resources\Achievements\Pages;
use App\Filament\Resources\Achievements\AchievementResource;
use Filament\Resources\Pages\CreateRecord;
class CreateAchievement extends CreateRecord
{
protected static string $resource = AchievementResource::class;
}
@@ -0,0 +1,19 @@
<?php
namespace App\Filament\Resources\Achievements\Pages;
use App\Filament\Resources\Achievements\AchievementResource;
use Filament\Actions\DeleteAction;
use Filament\Resources\Pages\EditRecord;
class EditAchievement extends EditRecord
{
protected static string $resource = AchievementResource::class;
protected function getHeaderActions(): array
{
return [
DeleteAction::make(),
];
}
}
@@ -0,0 +1,19 @@
<?php
namespace App\Filament\Resources\Achievements\Pages;
use App\Filament\Resources\Achievements\AchievementResource;
use Filament\Actions\CreateAction;
use Filament\Resources\Pages\ListRecords;
class ListAchievements extends ListRecords
{
protected static string $resource = AchievementResource::class;
protected function getHeaderActions(): array
{
return [
CreateAction::make(),
];
}
}
@@ -0,0 +1,26 @@
<?php
namespace App\Filament\Resources\Achievements\Schemas;
use Filament\Forms\Components\DateTimePicker;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Schemas\Schema;
class AchievementForm
{
public static function configure(Schema $schema): Schema
{
return $schema
->components([
Select::make('user_id')
->relationship('user', 'name')
->required(),
TextInput::make('key')
->required(),
DateTimePicker::make('earned_at')
->required(),
TextInput::make('meta'),
]);
}
}
@@ -0,0 +1,43 @@
<?php
namespace App\Filament\Resources\Achievements\Tables;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\DeleteBulkAction;
use Filament\Actions\EditAction;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
/**
* Table des badges débloqués : utilisateur, clé du badge, date d'obtention.
*/
class AchievementsTable
{
public static function configure(Table $table): Table
{
return $table
->columns([
TextColumn::make('user.name')
->label('Utilisateur')
->searchable()
->sortable(),
TextColumn::make('key')
->label('Badge')
->badge()
->searchable(),
TextColumn::make('earned_at')
->label('Obtenu le')
->dateTime()
->sortable(),
])
->recordActions([
EditAction::make(),
])
->toolbarActions([
BulkActionGroup::make([
DeleteBulkAction::make(),
]),
])
->defaultSort('earned_at', 'desc');
}
}
@@ -0,0 +1,56 @@
<?php
namespace App\Filament\Resources\Exercises;
use App\Filament\Resources\Exercises\Pages\ListExercises;
use App\Filament\Resources\Exercises\Schemas\ExerciseInfolist;
use App\Filament\Resources\Exercises\Tables\ExercisesTable;
use BackedEnum;
use Filament\Resources\Resource;
use Filament\Schemas\Schema;
use Filament\Support\Icons\Heroicon;
use Filament\Tables\Table;
use Functional\Exercises\Models\Exercise;
use UnitEnum;
/**
* Catalogue d'exercices consultation en lecture seule (1324 lignes).
* Filtrable par body_part / equipment / target, recherche sur le nom.
*/
class ExerciseResource extends Resource
{
protected static ?string $model = Exercise::class;
protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedRectangleStack;
protected static string|UnitEnum|null $navigationGroup = 'Catalogue';
protected static ?string $modelLabel = 'exercice';
protected static ?string $pluralModelLabel = 'exercices';
protected static ?string $recordTitleAttribute = 'name';
public static function infolist(Schema $schema): Schema
{
return ExerciseInfolist::configure($schema);
}
public static function table(Table $table): Table
{
return ExercisesTable::configure($table);
}
// Catalogue en lecture seule : pas de création / édition / suppression.
public static function canCreate(): bool
{
return false;
}
public static function getPages(): array
{
return [
'index' => ListExercises::route('/'),
];
}
}
@@ -0,0 +1,17 @@
<?php
namespace App\Filament\Resources\Exercises\Pages;
use App\Filament\Resources\Exercises\ExerciseResource;
use Filament\Resources\Pages\ListRecords;
class ListExercises extends ListRecords
{
protected static string $resource = ExerciseResource::class;
// Catalogue en lecture seule : aucune action de création.
protected function getHeaderActions(): array
{
return [];
}
}
@@ -0,0 +1,46 @@
<?php
namespace App\Filament\Resources\Exercises\Schemas;
use Filament\Infolists\Components\ImageEntry;
use Filament\Infolists\Components\TextEntry;
use Filament\Schemas\Schema;
/**
* Fiche de consultation d'un exercice (modal "Voir").
* Affiche les métadonnées et les instructions du dataset (lecture seule).
*/
class ExerciseInfolist
{
public static function configure(Schema $schema): Schema
{
return $schema
->components([
ImageEntry::make('image_url')
->label('Aperçu')
->height(160)
->square(),
TextEntry::make('name')
->label('Nom'),
TextEntry::make('category')
->label('Catégorie')
->badge(),
TextEntry::make('body_part')
->label('Partie du corps')
->badge(),
TextEntry::make('equipment')
->label('Équipement')
->badge(),
TextEntry::make('target')
->label('Muscle cible')
->badge(),
TextEntry::make('muscle_group')
->label('Groupe musculaire'),
TextEntry::make('external_id')
->label('Identifiant externe'),
TextEntry::make('attribution')
->label('Attribution')
->placeholder('—'),
]);
}
}
@@ -0,0 +1,70 @@
<?php
namespace App\Filament\Resources\Exercises\Tables;
use Filament\Actions\ViewAction;
use Filament\Tables\Columns\ImageColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Filters\SelectFilter;
use Filament\Tables\Table;
use Functional\Exercises\Models\Exercise;
/**
* Table du catalogue d'exercices : thumbnail + métadonnées, filtrable et consultable.
*/
class ExercisesTable
{
public static function configure(Table $table): Table
{
return $table
->columns([
ImageColumn::make('image_url')
->label('Aperçu')
->square(),
TextColumn::make('name')
->label('Nom')
->searchable()
->sortable(),
TextColumn::make('body_part')
->label('Partie du corps')
->badge()
->sortable(),
TextColumn::make('equipment')
->label('Équipement')
->badge(),
TextColumn::make('target')
->label('Muscle cible')
->badge(),
])
->filters([
SelectFilter::make('body_part')
->label('Partie du corps')
->options(fn (): array => self::distinctOptions('body_part')),
SelectFilter::make('equipment')
->label('Équipement')
->options(fn (): array => self::distinctOptions('equipment')),
SelectFilter::make('target')
->label('Muscle cible')
->options(fn (): array => self::distinctOptions('target')),
])
->recordActions([
ViewAction::make(),
])
->defaultSort('name');
}
/**
* Valeurs distinctes d'une colonne pour alimenter un filtre (clé = valeur = libellé brut).
*
* @return array<string, string>
*/
private static function distinctOptions(string $column): array
{
return Exercise::query()
->whereNotNull($column)
->distinct()
->orderBy($column)
->pluck($column, $column)
->all();
}
}
@@ -0,0 +1,48 @@
<?php
namespace App\Filament\Resources\Integrations;
use App\Filament\Resources\Integrations\Pages\CreateIntegration;
use App\Filament\Resources\Integrations\Pages\EditIntegration;
use App\Filament\Resources\Integrations\Pages\ListIntegrations;
use App\Filament\Resources\Integrations\Schemas\IntegrationForm;
use App\Filament\Resources\Integrations\Tables\IntegrationsTable;
use BackedEnum;
use Filament\Resources\Resource;
use Filament\Schemas\Schema;
use Filament\Support\Icons\Heroicon;
use Filament\Tables\Table;
use Technical\Integrations\Models\Integration;
class IntegrationResource extends Resource
{
protected static ?string $model = Integration::class;
protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedRectangleStack;
public static function form(Schema $schema): Schema
{
return IntegrationForm::configure($schema);
}
public static function table(Table $table): Table
{
return IntegrationsTable::configure($table);
}
public static function getRelations(): array
{
return [
//
];
}
public static function getPages(): array
{
return [
'index' => ListIntegrations::route('/'),
'create' => CreateIntegration::route('/create'),
'edit' => EditIntegration::route('/{record}/edit'),
];
}
}
@@ -0,0 +1,11 @@
<?php
namespace App\Filament\Resources\Integrations\Pages;
use App\Filament\Resources\Integrations\IntegrationResource;
use Filament\Resources\Pages\CreateRecord;
class CreateIntegration extends CreateRecord
{
protected static string $resource = IntegrationResource::class;
}
@@ -0,0 +1,19 @@
<?php
namespace App\Filament\Resources\Integrations\Pages;
use App\Filament\Resources\Integrations\IntegrationResource;
use Filament\Actions\DeleteAction;
use Filament\Resources\Pages\EditRecord;
class EditIntegration extends EditRecord
{
protected static string $resource = IntegrationResource::class;
protected function getHeaderActions(): array
{
return [
DeleteAction::make(),
];
}
}
@@ -0,0 +1,19 @@
<?php
namespace App\Filament\Resources\Integrations\Pages;
use App\Filament\Resources\Integrations\IntegrationResource;
use Filament\Actions\CreateAction;
use Filament\Resources\Pages\ListRecords;
class ListIntegrations extends ListRecords
{
protected static string $resource = IntegrationResource::class;
protected function getHeaderActions(): array
{
return [
CreateAction::make(),
];
}
}
@@ -0,0 +1,35 @@
<?php
namespace App\Filament\Resources\Integrations\Schemas;
use Filament\Forms\Components\DateTimePicker;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Schemas\Schema;
use Technical\Integrations\Enums\IntegrationStatus;
class IntegrationForm
{
public static function configure(Schema $schema): Schema
{
return $schema
->components([
Select::make('user_id')
->relationship('user', 'name')
->required(),
TextInput::make('provider')
->required(),
TextInput::make('external_user_id'),
DateTimePicker::make('token_expires_at'),
TextInput::make('scopes'),
Select::make('status')
->options(IntegrationStatus::class)
->default('connected')
->required(),
DateTimePicker::make('connected_at')
->required(),
DateTimePicker::make('last_synced_at'),
TextInput::make('meta'),
]);
}
}
@@ -0,0 +1,60 @@
<?php
namespace App\Filament\Resources\Integrations\Tables;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\DeleteBulkAction;
use Filament\Actions\EditAction;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Filters\SelectFilter;
use Filament\Tables\Table;
use Technical\Integrations\Enums\IntegrationStatus;
/**
* Table des intégrations externes : utilisateur, fournisseur, état, dernière synchro.
* Les tokens (access/refresh) ne sont JAMAIS affichés.
*/
class IntegrationsTable
{
public static function configure(Table $table): Table
{
return $table
->columns([
TextColumn::make('user.name')
->label('Utilisateur')
->searchable()
->sortable(),
TextColumn::make('provider')
->label('Fournisseur')
->badge()
->searchable(),
TextColumn::make('status')
->label('État')
->badge()
->color(fn (IntegrationStatus $state): string => match ($state) {
IntegrationStatus::Connected => 'success',
IntegrationStatus::Revoked => 'gray',
IntegrationStatus::Error => 'danger',
}),
TextColumn::make('last_synced_at')
->label('Dernière synchro')
->dateTime()
->sortable()
->placeholder('Jamais'),
])
->filters([
SelectFilter::make('status')
->label('État')
->options(IntegrationStatus::class),
])
->recordActions([
EditAction::make(),
])
->toolbarActions([
BulkActionGroup::make([
DeleteBulkAction::make(),
]),
])
->defaultSort('last_synced_at', 'desc');
}
}
@@ -0,0 +1,11 @@
<?php
namespace App\Filament\Resources\Programs\Pages;
use App\Filament\Resources\Programs\ProgramResource;
use Filament\Resources\Pages\CreateRecord;
class CreateProgram extends CreateRecord
{
protected static string $resource = ProgramResource::class;
}
@@ -0,0 +1,19 @@
<?php
namespace App\Filament\Resources\Programs\Pages;
use App\Filament\Resources\Programs\ProgramResource;
use Filament\Actions\DeleteAction;
use Filament\Resources\Pages\EditRecord;
class EditProgram extends EditRecord
{
protected static string $resource = ProgramResource::class;
protected function getHeaderActions(): array
{
return [
DeleteAction::make(),
];
}
}
@@ -0,0 +1,19 @@
<?php
namespace App\Filament\Resources\Programs\Pages;
use App\Filament\Resources\Programs\ProgramResource;
use Filament\Actions\CreateAction;
use Filament\Resources\Pages\ListRecords;
class ListPrograms extends ListRecords
{
protected static string $resource = ProgramResource::class;
protected function getHeaderActions(): array
{
return [
CreateAction::make(),
];
}
}
@@ -0,0 +1,48 @@
<?php
namespace App\Filament\Resources\Programs;
use App\Filament\Resources\Programs\Pages\CreateProgram;
use App\Filament\Resources\Programs\Pages\EditProgram;
use App\Filament\Resources\Programs\Pages\ListPrograms;
use App\Filament\Resources\Programs\Schemas\ProgramForm;
use App\Filament\Resources\Programs\Tables\ProgramsTable;
use BackedEnum;
use Filament\Resources\Resource;
use Filament\Schemas\Schema;
use Filament\Support\Icons\Heroicon;
use Filament\Tables\Table;
use Functional\Programs\Models\Program;
class ProgramResource extends Resource
{
protected static ?string $model = Program::class;
protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedRectangleStack;
public static function form(Schema $schema): Schema
{
return ProgramForm::configure($schema);
}
public static function table(Table $table): Table
{
return ProgramsTable::configure($table);
}
public static function getRelations(): array
{
return [
//
];
}
public static function getPages(): array
{
return [
'index' => ListPrograms::route('/'),
'create' => CreateProgram::route('/create'),
'edit' => EditProgram::route('/{record}/edit'),
];
}
}
@@ -0,0 +1,35 @@
<?php
namespace App\Filament\Resources\Programs\Schemas;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\Toggle;
use Filament\Schemas\Schema;
class ProgramForm
{
public static function configure(Schema $schema): Schema
{
return $schema
->components([
Select::make('user_id')
->relationship('user', 'name')
->required(),
TextInput::make('name')
->required(),
TextInput::make('slug')
->required(),
Textarea::make('description')
->columnSpanFull(),
TextInput::make('duration_weeks')
->required()
->numeric(),
TextInput::make('days_per_week')
->numeric(),
Toggle::make('is_public')
->required(),
]);
}
}
@@ -0,0 +1,51 @@
<?php
namespace App\Filament\Resources\Programs\Tables;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\DeleteBulkAction;
use Filament\Actions\EditAction;
use Filament\Tables\Columns\IconColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
/**
* Table des programmes : nom, propriétaire, durée, visibilité, date de création.
*/
class ProgramsTable
{
public static function configure(Table $table): Table
{
return $table
->columns([
TextColumn::make('name')
->label('Nom')
->searchable()
->sortable(),
TextColumn::make('user.name')
->label('Utilisateur')
->searchable()
->sortable(),
TextColumn::make('duration_weeks')
->label('Semaines')
->numeric()
->sortable(),
IconColumn::make('is_public')
->label('Public')
->boolean(),
TextColumn::make('created_at')
->label('Créé le')
->dateTime()
->sortable(),
])
->recordActions([
EditAction::make(),
])
->toolbarActions([
BulkActionGroup::make([
DeleteBulkAction::make(),
]),
])
->defaultSort('created_at', 'desc');
}
}
@@ -0,0 +1,11 @@
<?php
namespace App\Filament\Resources\Routines\Pages;
use App\Filament\Resources\Routines\RoutineResource;
use Filament\Resources\Pages\CreateRecord;
class CreateRoutine extends CreateRecord
{
protected static string $resource = RoutineResource::class;
}
@@ -0,0 +1,19 @@
<?php
namespace App\Filament\Resources\Routines\Pages;
use App\Filament\Resources\Routines\RoutineResource;
use Filament\Actions\DeleteAction;
use Filament\Resources\Pages\EditRecord;
class EditRoutine extends EditRecord
{
protected static string $resource = RoutineResource::class;
protected function getHeaderActions(): array
{
return [
DeleteAction::make(),
];
}
}
@@ -0,0 +1,19 @@
<?php
namespace App\Filament\Resources\Routines\Pages;
use App\Filament\Resources\Routines\RoutineResource;
use Filament\Actions\CreateAction;
use Filament\Resources\Pages\ListRecords;
class ListRoutines extends ListRecords
{
protected static string $resource = RoutineResource::class;
protected function getHeaderActions(): array
{
return [
CreateAction::make(),
];
}
}
@@ -0,0 +1,48 @@
<?php
namespace App\Filament\Resources\Routines;
use App\Filament\Resources\Routines\Pages\CreateRoutine;
use App\Filament\Resources\Routines\Pages\EditRoutine;
use App\Filament\Resources\Routines\Pages\ListRoutines;
use App\Filament\Resources\Routines\Schemas\RoutineForm;
use App\Filament\Resources\Routines\Tables\RoutinesTable;
use BackedEnum;
use Filament\Resources\Resource;
use Filament\Schemas\Schema;
use Filament\Support\Icons\Heroicon;
use Filament\Tables\Table;
use Functional\Routines\Models\Routine;
class RoutineResource extends Resource
{
protected static ?string $model = Routine::class;
protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedRectangleStack;
public static function form(Schema $schema): Schema
{
return RoutineForm::configure($schema);
}
public static function table(Table $table): Table
{
return RoutinesTable::configure($table);
}
public static function getRelations(): array
{
return [
//
];
}
public static function getPages(): array
{
return [
'index' => ListRoutines::route('/'),
'create' => CreateRoutine::route('/create'),
'edit' => EditRoutine::route('/{record}/edit'),
];
}
}
@@ -0,0 +1,32 @@
<?php
namespace App\Filament\Resources\Routines\Schemas;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\Toggle;
use Filament\Schemas\Schema;
class RoutineForm
{
public static function configure(Schema $schema): Schema
{
return $schema
->components([
Select::make('user_id')
->relationship('user', 'name')
->required(),
TextInput::make('name')
->required(),
TextInput::make('slug')
->required(),
Textarea::make('description')
->columnSpanFull(),
Toggle::make('is_public')
->required(),
TextInput::make('estimated_minutes')
->numeric(),
]);
}
}
@@ -0,0 +1,47 @@
<?php
namespace App\Filament\Resources\Routines\Tables;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\DeleteBulkAction;
use Filament\Actions\EditAction;
use Filament\Tables\Columns\IconColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
/**
* Table des routines : nom, propriétaire, visibilité, date de création.
*/
class RoutinesTable
{
public static function configure(Table $table): Table
{
return $table
->columns([
TextColumn::make('name')
->label('Nom')
->searchable()
->sortable(),
TextColumn::make('user.name')
->label('Utilisateur')
->searchable()
->sortable(),
IconColumn::make('is_public')
->label('Publique')
->boolean(),
TextColumn::make('created_at')
->label('Créée le')
->dateTime()
->sortable(),
])
->recordActions([
EditAction::make(),
])
->toolbarActions([
BulkActionGroup::make([
DeleteBulkAction::make(),
]),
])
->defaultSort('created_at', 'desc');
}
}
@@ -0,0 +1,11 @@
<?php
namespace App\Filament\Resources\Streaks\Pages;
use App\Filament\Resources\Streaks\StreakResource;
use Filament\Resources\Pages\CreateRecord;
class CreateStreak extends CreateRecord
{
protected static string $resource = StreakResource::class;
}
@@ -0,0 +1,19 @@
<?php
namespace App\Filament\Resources\Streaks\Pages;
use App\Filament\Resources\Streaks\StreakResource;
use Filament\Actions\DeleteAction;
use Filament\Resources\Pages\EditRecord;
class EditStreak extends EditRecord
{
protected static string $resource = StreakResource::class;
protected function getHeaderActions(): array
{
return [
DeleteAction::make(),
];
}
}
@@ -0,0 +1,19 @@
<?php
namespace App\Filament\Resources\Streaks\Pages;
use App\Filament\Resources\Streaks\StreakResource;
use Filament\Actions\CreateAction;
use Filament\Resources\Pages\ListRecords;
class ListStreaks extends ListRecords
{
protected static string $resource = StreakResource::class;
protected function getHeaderActions(): array
{
return [
CreateAction::make(),
];
}
}
@@ -0,0 +1,35 @@
<?php
namespace App\Filament\Resources\Streaks\Schemas;
use Filament\Forms\Components\DatePicker;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Schemas\Schema;
class StreakForm
{
public static function configure(Schema $schema): Schema
{
return $schema
->components([
Select::make('user_id')
->relationship('user', 'name')
->required(),
TextInput::make('current_count')
->required()
->numeric()
->default(0),
TextInput::make('longest_count')
->required()
->numeric()
->default(0),
DatePicker::make('last_active_date'),
TextInput::make('freezes_available')
->required()
->numeric()
->default(0),
DatePicker::make('freeze_last_granted_on'),
]);
}
}
@@ -0,0 +1,48 @@
<?php
namespace App\Filament\Resources\Streaks;
use App\Filament\Resources\Streaks\Pages\CreateStreak;
use App\Filament\Resources\Streaks\Pages\EditStreak;
use App\Filament\Resources\Streaks\Pages\ListStreaks;
use App\Filament\Resources\Streaks\Schemas\StreakForm;
use App\Filament\Resources\Streaks\Tables\StreaksTable;
use BackedEnum;
use Filament\Resources\Resource;
use Filament\Schemas\Schema;
use Filament\Support\Icons\Heroicon;
use Filament\Tables\Table;
use Functional\Streaks\Models\Streak;
class StreakResource extends Resource
{
protected static ?string $model = Streak::class;
protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedRectangleStack;
public static function form(Schema $schema): Schema
{
return StreakForm::configure($schema);
}
public static function table(Table $table): Table
{
return StreaksTable::configure($table);
}
public static function getRelations(): array
{
return [
//
];
}
public static function getPages(): array
{
return [
'index' => ListStreaks::route('/'),
'create' => CreateStreak::route('/create'),
'edit' => EditStreak::route('/{record}/edit'),
];
}
}
@@ -0,0 +1,52 @@
<?php
namespace App\Filament\Resources\Streaks\Tables;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\DeleteBulkAction;
use Filament\Actions\EditAction;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
/**
* Table des streaks : régularité par utilisateur (courant, record, freezes, dernière activité).
*/
class StreaksTable
{
public static function configure(Table $table): Table
{
return $table
->columns([
TextColumn::make('user.name')
->label('Utilisateur')
->searchable()
->sortable(),
TextColumn::make('current_count')
->label('Streak courant')
->badge()
->color('warning')
->sortable(),
TextColumn::make('longest_count')
->label('Record')
->numeric()
->sortable(),
TextColumn::make('freezes_available')
->label('Freezes dispo')
->numeric()
->sortable(),
TextColumn::make('last_active_date')
->label('Dernière activité')
->date()
->sortable(),
])
->recordActions([
EditAction::make(),
])
->toolbarActions([
BulkActionGroup::make([
DeleteBulkAction::make(),
]),
])
->defaultSort('current_count', 'desc');
}
}
@@ -0,0 +1,11 @@
<?php
namespace App\Filament\Resources\Users\Pages;
use App\Filament\Resources\Users\UserResource;
use Filament\Resources\Pages\CreateRecord;
class CreateUser extends CreateRecord
{
protected static string $resource = UserResource::class;
}
@@ -0,0 +1,19 @@
<?php
namespace App\Filament\Resources\Users\Pages;
use App\Filament\Resources\Users\UserResource;
use Filament\Actions\DeleteAction;
use Filament\Resources\Pages\EditRecord;
class EditUser extends EditRecord
{
protected static string $resource = UserResource::class;
protected function getHeaderActions(): array
{
return [
DeleteAction::make(),
];
}
}
@@ -0,0 +1,19 @@
<?php
namespace App\Filament\Resources\Users\Pages;
use App\Filament\Resources\Users\UserResource;
use Filament\Actions\CreateAction;
use Filament\Resources\Pages\ListRecords;
class ListUsers extends ListRecords
{
protected static string $resource = UserResource::class;
protected function getHeaderActions(): array
{
return [
CreateAction::make(),
];
}
}
@@ -0,0 +1,39 @@
<?php
namespace App\Filament\Resources\Users\Schemas;
use Filament\Forms\Components\DateTimePicker;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Schemas\Schema;
use Functional\Users\Enums\Units;
class UserForm
{
public static function configure(Schema $schema): Schema
{
return $schema
->components([
TextInput::make('name')
->required(),
TextInput::make('email')
->label('Email address')
->email()
->required(),
DateTimePicker::make('email_verified_at'),
TextInput::make('password')
->password()
->required(),
TextInput::make('locale')
->required()
->default('fr'),
Select::make('units')
->options(Units::class)
->default('metric')
->required(),
TextInput::make('timezone'),
TextInput::make('avatar_path'),
TextInput::make('oidc_sub'),
]);
}
}
@@ -0,0 +1,49 @@
<?php
namespace App\Filament\Resources\Users\Tables;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\DeleteBulkAction;
use Filament\Actions\EditAction;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
/**
* Table des utilisateurs : identité, préférences d'affichage, date d'inscription.
*/
class UsersTable
{
public static function configure(Table $table): Table
{
return $table
->columns([
TextColumn::make('name')
->label('Nom')
->searchable()
->sortable(),
TextColumn::make('email')
->label('Email')
->searchable()
->sortable(),
TextColumn::make('locale')
->label('Langue')
->badge(),
TextColumn::make('units')
->label('Unités')
->badge(),
TextColumn::make('created_at')
->label('Inscrit le')
->dateTime()
->sortable(),
])
->recordActions([
EditAction::make(),
])
->toolbarActions([
BulkActionGroup::make([
DeleteBulkAction::make(),
]),
])
->defaultSort('created_at', 'desc');
}
}
@@ -0,0 +1,48 @@
<?php
namespace App\Filament\Resources\Users;
use App\Filament\Resources\Users\Pages\CreateUser;
use App\Filament\Resources\Users\Pages\EditUser;
use App\Filament\Resources\Users\Pages\ListUsers;
use App\Filament\Resources\Users\Schemas\UserForm;
use App\Filament\Resources\Users\Tables\UsersTable;
use BackedEnum;
use Filament\Resources\Resource;
use Filament\Schemas\Schema;
use Filament\Support\Icons\Heroicon;
use Filament\Tables\Table;
use Functional\Users\Models\User;
class UserResource extends Resource
{
protected static ?string $model = User::class;
protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedRectangleStack;
public static function form(Schema $schema): Schema
{
return UserForm::configure($schema);
}
public static function table(Table $table): Table
{
return UsersTable::configure($table);
}
public static function getRelations(): array
{
return [
//
];
}
public static function getPages(): array
{
return [
'index' => ListUsers::route('/'),
'create' => CreateUser::route('/create'),
'edit' => EditUser::route('/{record}/edit'),
];
}
}
@@ -0,0 +1,11 @@
<?php
namespace App\Filament\Resources\WorkoutSessions\Pages;
use App\Filament\Resources\WorkoutSessions\WorkoutSessionResource;
use Filament\Resources\Pages\CreateRecord;
class CreateWorkoutSession extends CreateRecord
{
protected static string $resource = WorkoutSessionResource::class;
}
@@ -0,0 +1,19 @@
<?php
namespace App\Filament\Resources\WorkoutSessions\Pages;
use App\Filament\Resources\WorkoutSessions\WorkoutSessionResource;
use Filament\Actions\DeleteAction;
use Filament\Resources\Pages\EditRecord;
class EditWorkoutSession extends EditRecord
{
protected static string $resource = WorkoutSessionResource::class;
protected function getHeaderActions(): array
{
return [
DeleteAction::make(),
];
}
}
@@ -0,0 +1,19 @@
<?php
namespace App\Filament\Resources\WorkoutSessions\Pages;
use App\Filament\Resources\WorkoutSessions\WorkoutSessionResource;
use Filament\Actions\CreateAction;
use Filament\Resources\Pages\ListRecords;
class ListWorkoutSessions extends ListRecords
{
protected static string $resource = WorkoutSessionResource::class;
protected function getHeaderActions(): array
{
return [
CreateAction::make(),
];
}
}
@@ -0,0 +1,39 @@
<?php
namespace App\Filament\Resources\WorkoutSessions\Schemas;
use Filament\Forms\Components\DateTimePicker;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Textarea;
use Filament\Schemas\Schema;
use Functional\Tracking\Enums\WorkoutSessionStatus;
class WorkoutSessionForm
{
public static function configure(Schema $schema): Schema
{
return $schema
->components([
Select::make('user_id')
->relationship('user', 'name')
->required(),
Select::make('routine_id')
->relationship('routine', 'name'),
Select::make('program_id')
->relationship('program', 'name'),
TextInput::make('title'),
DateTimePicker::make('started_at')
->required(),
DateTimePicker::make('ended_at'),
Select::make('status')
->options(WorkoutSessionStatus::class)
->default('active')
->required(),
Textarea::make('notes')
->columnSpanFull(),
TextInput::make('total_volume')
->numeric(),
]);
}
}
@@ -0,0 +1,58 @@
<?php
namespace App\Filament\Resources\WorkoutSessions\Tables;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\DeleteBulkAction;
use Filament\Actions\EditAction;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Filters\SelectFilter;
use Filament\Tables\Table;
use Functional\Tracking\Enums\WorkoutSessionStatus;
/**
* Table des séances : utilisateur, statut, début, volume total.
*/
class WorkoutSessionsTable
{
public static function configure(Table $table): Table
{
return $table
->columns([
TextColumn::make('user.name')
->label('Utilisateur')
->searchable()
->sortable(),
TextColumn::make('status')
->label('Statut')
->badge()
->color(fn (WorkoutSessionStatus $state): string => match ($state) {
WorkoutSessionStatus::Active => 'warning',
WorkoutSessionStatus::Completed => 'success',
WorkoutSessionStatus::Abandoned => 'danger',
}),
TextColumn::make('started_at')
->label('Débutée le')
->dateTime()
->sortable(),
TextColumn::make('total_volume')
->label('Volume total')
->numeric()
->sortable(),
])
->filters([
SelectFilter::make('status')
->label('Statut')
->options(WorkoutSessionStatus::class),
])
->recordActions([
EditAction::make(),
])
->toolbarActions([
BulkActionGroup::make([
DeleteBulkAction::make(),
]),
])
->defaultSort('started_at', 'desc');
}
}
@@ -0,0 +1,48 @@
<?php
namespace App\Filament\Resources\WorkoutSessions;
use App\Filament\Resources\WorkoutSessions\Pages\CreateWorkoutSession;
use App\Filament\Resources\WorkoutSessions\Pages\EditWorkoutSession;
use App\Filament\Resources\WorkoutSessions\Pages\ListWorkoutSessions;
use App\Filament\Resources\WorkoutSessions\Schemas\WorkoutSessionForm;
use App\Filament\Resources\WorkoutSessions\Tables\WorkoutSessionsTable;
use BackedEnum;
use Filament\Resources\Resource;
use Filament\Schemas\Schema;
use Filament\Support\Icons\Heroicon;
use Filament\Tables\Table;
use Functional\Tracking\Models\WorkoutSession;
class WorkoutSessionResource extends Resource
{
protected static ?string $model = WorkoutSession::class;
protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedRectangleStack;
public static function form(Schema $schema): Schema
{
return WorkoutSessionForm::configure($schema);
}
public static function table(Table $table): Table
{
return WorkoutSessionsTable::configure($table);
}
public static function getRelations(): array
{
return [
//
];
}
public static function getPages(): array
{
return [
'index' => ListWorkoutSessions::route('/'),
'create' => CreateWorkoutSession::route('/create'),
'edit' => EditWorkoutSession::route('/{record}/edit'),
];
}
}
@@ -0,0 +1,47 @@
<?php
namespace App\Filament\Widgets;
use Filament\Widgets\StatsOverviewWidget;
use Filament\Widgets\StatsOverviewWidget\Stat;
use Functional\Exercises\Models\Exercise;
use Functional\Streaks\Models\Streak;
use Functional\Tracking\Models\WorkoutSession;
use Functional\Users\Models\User;
use Illuminate\Support\Carbon;
/**
* Vue d'ensemble du dashboard : chiffres clés de StreakFit.
* Nb d'utilisateurs, séances (total + cette semaine), plus long streak en cours, catalogue d'exos.
*/
class StreakFitStatsOverview extends StatsOverviewWidget
{
protected function getStats(): array
{
$sessionsThisWeek = WorkoutSession::query()
->where('started_at', '>=', Carbon::now()->startOfWeek())
->count();
return [
Stat::make('Utilisateurs', (string) User::query()->count())
->description('Comptes inscrits')
->descriptionIcon('heroicon-m-users')
->color('primary'),
Stat::make('Séances', (string) WorkoutSession::query()->count())
->description($sessionsThisWeek . ' cette semaine')
->descriptionIcon('heroicon-m-fire')
->color('warning'),
Stat::make('Meilleur streak en cours', (string) (Streak::query()->max('current_count') ?? 0))
->description('Jours consécutifs')
->descriptionIcon('heroicon-m-bolt')
->color('success'),
Stat::make('Exercices', (string) Exercise::query()->count())
->description('Catalogue disponible')
->descriptionIcon('heroicon-m-rectangle-stack')
->color('primary'),
];
}
}
@@ -0,0 +1,73 @@
<?php
namespace App\Providers\Filament;
use App\Filament\Widgets\StreakFitStatsOverview;
use Filament\Http\Middleware\Authenticate;
use Filament\Http\Middleware\AuthenticateSession;
use Filament\Http\Middleware\DisableBladeIconComponents;
use Filament\Http\Middleware\DispatchServingFilamentEvent;
use Filament\Pages\Dashboard;
use Filament\Panel;
use Filament\PanelProvider;
use Filament\Support\Colors\Color;
use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse;
use Illuminate\Cookie\Middleware\EncryptCookies;
use Illuminate\Foundation\Http\Middleware\PreventRequestForgery;
use Illuminate\Routing\Middleware\SubstituteBindings;
use Illuminate\Session\Middleware\StartSession;
use Illuminate\View\Middleware\ShareErrorsFromSession;
/**
* Back-office StreakFit (Filament).
*
* Thème "flamme" : accent orange/amber sur base claire + dark mode.
* Auth via le guard 'web' standard (compatible avec le flow Sanctum SPA du front) ;
* l'accès effectif au panel sera verrouillé quand le model User implémentera
* Filament\Models\Contracts\FilamentUser::canAccessPanel().
*/
class AdminPanelProvider extends PanelProvider
{
public function panel(Panel $panel): Panel
{
return $panel
->default()
->id('admin')
->path('admin')
->brandName('StreakFit 🔥')
->login()
// Guard web standard : n'interfère pas avec l'auth Sanctum du front.
->authGuard('web')
// Accent "flamme" : orange en primaire, amber en secondaire (warning).
->colors([
'primary' => Color::Orange,
'warning' => Color::Amber,
'danger' => Color::Red,
'success' => Color::Emerald,
])
// Dark mode disponible via le sélecteur clair/sombre (activé par défaut).
->discoverResources(in: app_path('Filament/Resources'), for: 'App\Filament\Resources')
->discoverPages(in: app_path('Filament/Pages'), for: 'App\Filament\Pages')
->pages([
Dashboard::class,
])
->discoverWidgets(in: app_path('Filament/Widgets'), for: 'App\Filament\Widgets')
->widgets([
StreakFitStatsOverview::class,
])
->middleware([
EncryptCookies::class,
AddQueuedCookiesToResponse::class,
StartSession::class,
AuthenticateSession::class,
ShareErrorsFromSession::class,
PreventRequestForgery::class,
SubstituteBindings::class,
DisableBladeIconComponents::class,
DispatchServingFilamentEvent::class,
])
->authMiddleware([
Authenticate::class,
]);
}
}