adds roles

This commit is contained in:
2026-02-16 11:19:06 +01:00
parent ebaeb1722d
commit 4dc64c22cb
29 changed files with 495 additions and 89 deletions

View File

@@ -0,0 +1,86 @@
<?php
declare(strict_types=1);
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Symfony\Component\Process\Process;
final class DevMenuCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'menu';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Developer tools menu';
public function handle(): int
{
if (! in_array(app()->environment(), ['local', 'testing'])) {
$this->error('This command can only be run in local or testing environments.');
return Command::FAILURE;
}
$this->info('');
$this->info(' ╔═══════════════════════════════╗');
$this->info(' ║ Go No Go — Dev Tools ║');
$this->info(' ╚═══════════════════════════════╝');
$this->info('');
$choice = $this->choice('Select an action', [
0 => 'Exit',
1 => 'Fresh migrate, seed & build',
]);
if ($choice === 'Exit') {
$this->info('Bye!');
return Command::SUCCESS;
}
if ($choice === 'Fresh migrate, seed & build') {
$this->freshMigrateAndBuild();
}
return Command::SUCCESS;
}
/**
* Runs migrate:fresh with seeding, then runs npm build.
*
* Displays output from both processes and confirms success or failure.
*/
private function freshMigrateAndBuild(): void
{
$this->info('');
$this->comment('Running migrate:fresh --seed...');
$this->call('migrate:fresh', ['--seed' => true]);
$this->info('');
$this->comment('Running npm run build...');
$process = new Process(['npm', 'run', 'build']);
$process->setWorkingDirectory(base_path());
$process->setTimeout(120);
$process->run(function (string $type, string $output): void {
$this->output->write($output);
});
if ($process->isSuccessful()) {
$this->info('');
$this->info('Environment rebuilt successfully.');
} else {
$this->error('Build failed.');
}
}
}

View File

@@ -5,6 +5,7 @@
namespace App\Http\Controllers\Auth; namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Models\Role;
use App\Models\User; use App\Models\User;
use App\Services\ActivityLogger; use App\Services\ActivityLogger;
use Illuminate\Http\RedirectResponse; use Illuminate\Http\RedirectResponse;
@@ -29,17 +30,25 @@ public function callback(): RedirectResponse
{ {
$azureUser = Socialite::driver('azure')->user(); $azureUser = Socialite::driver('azure')->user();
$user = User::query()->firstOrCreate( $user = User::query()->updateOrCreate(
['email' => $azureUser->getEmail()], ['email' => $azureUser->getEmail()],
[ [
'name' => $azureUser->getName(), 'name' => $azureUser->getName(),
'password' => null, 'azure_id' => $azureUser->getId(),
'photo' => $azureUser->getAvatar(),
'job_title' => Arr::get($azureUser->user, 'jobTitle'),
'department' => Arr::get($azureUser->user, 'department'),
'phone' => Arr::get($azureUser->user, 'mobilePhone', Arr::get($azureUser->user, 'businessPhones.0')),
] ]
); );
if ($user->role_id === null) {
$user->update(['role_id' => Role::where('name', 'user')->first()->id]);
}
auth()->login($user); auth()->login($user);
ActivityLogger::log('login', $user->id, metadata: ['email' => $user->email, 'firm_name' => Arr::get($azureUser, 'companyName')]); ActivityLogger::log('login', $user->id, metadata: ['email' => $user->email, 'firm_name' => Arr::get($azureUser->user, 'companyName')]);
return redirect('/'); return redirect('/');
} }

View File

@@ -68,10 +68,6 @@ public function update(UpdateSessionRequest $request, Session $session): Redirec
{ {
$validated = $request->validated(); $validated = $request->validated();
if (Arr::has($validated, 'basic_info')) {
$session->update(['basic_info' => Arr::get($validated, 'basic_info')]);
}
if (Arr::has($validated, 'answers')) { if (Arr::has($validated, 'answers')) {
$this->saveAnswers($session, Arr::get($validated, 'answers')); $this->saveAnswers($session, Arr::get($validated, 'answers'));
} }

View File

@@ -6,7 +6,9 @@
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Arr; use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Gate;
use Inertia\Middleware; use Inertia\Middleware;
use Laravel\Nova\Nova;
final class HandleInertiaRequests extends Middleware final class HandleInertiaRequests extends Middleware
{ {
@@ -32,6 +34,7 @@ public function share(Request $request): array
...parent::share($request), ...parent::share($request),
'auth' => [ 'auth' => [
'user' => $this->getAuthenticatedUser(), 'user' => $this->getAuthenticatedUser(),
'logo_href' => $this->getLogoHref(),
], ],
'flash' => [ 'flash' => [
'success' => fn () => Arr::get($request->session()->all(), 'success'), 'success' => fn () => Arr::get($request->session()->all(), 'success'),
@@ -57,4 +60,18 @@ private function getAuthenticatedUser(): ?array
'email' => $user->email, 'email' => $user->email,
]; ];
} }
/**
* Determine logo href based on user Nova access.
*/
private function getLogoHref(): string
{
$user = auth()->user();
if ($user !== null && Gate::allows('viewNova', $user)) {
return Nova::path();
}
return '/';
}
} }

View File

@@ -22,11 +22,6 @@ public function authorize(): bool
public function rules(): array public function rules(): array
{ {
return [ return [
'basic_info' => ['sometimes', 'required', 'array'],
'basic_info.client_name' => ['required_with:basic_info', 'string', 'max:255'],
'basic_info.client_contact' => ['required_with:basic_info', 'string', 'max:255'],
'basic_info.lead_firm_name' => ['required_with:basic_info', 'string', 'max:255'],
'basic_info.lead_firm_contact' => ['required_with:basic_info', 'string', 'max:255'],
'answers' => ['sometimes', 'array'], 'answers' => ['sometimes', 'array'],
'answers.*.value' => ['nullable', 'string', 'in:yes,no,not_applicable'], 'answers.*.value' => ['nullable', 'string', 'in:yes,no,not_applicable'],
'answers.*.text_value' => ['nullable', 'string', 'max:10000'], 'answers.*.text_value' => ['nullable', 'string', 'max:10000'],
@@ -41,20 +36,6 @@ public function rules(): array
public function messages(): array public function messages(): array
{ {
return [ return [
'basic_info.required' => 'Basic information is required.',
'basic_info.array' => 'Basic information must be a valid data structure.',
'basic_info.client_name.required_with' => 'The client name is required.',
'basic_info.client_name.string' => 'The client name must be text.',
'basic_info.client_name.max' => 'The client name cannot exceed 255 characters.',
'basic_info.client_contact.required_with' => 'The client contact is required.',
'basic_info.client_contact.string' => 'The client contact must be text.',
'basic_info.client_contact.max' => 'The client contact cannot exceed 255 characters.',
'basic_info.lead_firm_name.required_with' => 'The lead firm name is required.',
'basic_info.lead_firm_name.string' => 'The lead firm name must be text.',
'basic_info.lead_firm_name.max' => 'The lead firm name cannot exceed 255 characters.',
'basic_info.lead_firm_contact.required_with' => 'The lead firm contact is required.',
'basic_info.lead_firm_contact.string' => 'The lead firm contact must be text.',
'basic_info.lead_firm_contact.max' => 'The lead firm contact cannot exceed 255 characters.',
'answers.array' => 'Answers must be a valid data structure.', 'answers.array' => 'Answers must be a valid data structure.',
'answers.*.value.in' => 'Answer value must be yes, no, or not_applicable.', 'answers.*.value.in' => 'Answer value must be yes, no, or not_applicable.',
'answers.*.text_value.string' => 'Answer text must be text.', 'answers.*.text_value.string' => 'Answer text must be text.',

View File

@@ -7,6 +7,7 @@
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
final class Question extends Model final class Question extends Model
{ {
@@ -48,4 +49,12 @@ public function questionGroup(): BelongsTo
{ {
return $this->belongsTo(QuestionGroup::class); return $this->belongsTo(QuestionGroup::class);
} }
/**
* Get all answers for this question.
*/
public function answers(): HasMany
{
return $this->hasMany(Answer::class);
}
} }

23
app/Models/Role.php Normal file
View File

@@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
final class Role extends Model
{
protected $fillable = [
'name',
];
/**
* Get all users with this role.
*/
public function users(): HasMany
{
return $this->hasMany(User::class);
}
}

View File

@@ -25,7 +25,6 @@ final class Session extends Model
'status', 'status',
'score', 'score',
'result', 'result',
'basic_info',
'additional_comments', 'additional_comments',
'completed_at', 'completed_at',
]; ];
@@ -40,7 +39,6 @@ protected function casts(): array
'category_id' => 'integer', 'category_id' => 'integer',
'screening_id' => 'integer', 'screening_id' => 'integer',
'score' => 'integer', 'score' => 'integer',
'basic_info' => 'array',
'completed_at' => 'datetime', 'completed_at' => 'datetime',
]; ];
} }

View File

@@ -5,6 +5,7 @@
namespace App\Models; namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable; use Illuminate\Notifications\Notifiable;
@@ -23,6 +24,12 @@ final class User extends Authenticatable
'name', 'name',
'email', 'email',
'password', 'password',
'azure_id',
'photo',
'job_title',
'department',
'phone',
'role_id',
]; ];
/** /**
@@ -33,6 +40,7 @@ final class User extends Authenticatable
protected $hidden = [ protected $hidden = [
'password', 'password',
'remember_token', 'remember_token',
'azure_id',
]; ];
/** /**
@@ -45,9 +53,18 @@ protected function casts(): array
return [ return [
'email_verified_at' => 'datetime', 'email_verified_at' => 'datetime',
'password' => 'hashed', 'password' => 'hashed',
'role_id' => 'integer',
]; ];
} }
/**
* Get the role assigned to this user.
*/
public function role(): BelongsTo
{
return $this->belongsTo(Role::class);
}
/** /**
* Get all sessions for this user. * Get all sessions for this user.
*/ */

View File

@@ -49,6 +49,22 @@ final class AnswerResource extends Resource
*/ */
public static $with = ['session', 'question']; public static $with = ['session', 'question'];
/**
* Get the displayable label of the resource.
*/
public static function label(): string
{
return 'Answers';
}
/**
* Get the displayable singular label of the resource.
*/
public static function singularLabel(): string
{
return 'Answer';
}
/** /**
* Get the fields displayed by the resource. * Get the fields displayed by the resource.
* *
@@ -76,6 +92,7 @@ public function fields(NovaRequest $request): array
->rules('nullable', 'max:255'), ->rules('nullable', 'max:255'),
Textarea::make('Text Value') Textarea::make('Text Value')
->alwaysShow()
->rules('nullable'), ->rules('nullable'),
DateTime::make('Created At') DateTime::make('Created At')

View File

@@ -42,6 +42,22 @@ final class CategoryResource extends Resource
*/ */
public static $displayInNavigation = false; public static $displayInNavigation = false;
/**
* Get the displayable label of the resource.
*/
public static function label(): string
{
return 'Categories';
}
/**
* Get the displayable singular label of the resource.
*/
public static function singularLabel(): string
{
return 'Category';
}
/** /**
* Get the fields displayed by the resource. * Get the fields displayed by the resource.
* *

View File

@@ -56,6 +56,22 @@ final class LogResource extends Resource
*/ */
public static $with = ['user', 'session', 'category']; public static $with = ['user', 'session', 'category'];
/**
* Get the displayable label of the resource.
*/
public static function label(): string
{
return 'Logs';
}
/**
* Get the displayable singular label of the resource.
*/
public static function singularLabel(): string
{
return 'Log';
}
/** /**
* Get the fields displayed by the resource. * Get the fields displayed by the resource.
* *

View File

@@ -44,6 +44,22 @@ final class QuestionGroupResource extends Resource
*/ */
public static $displayInNavigation = false; public static $displayInNavigation = false;
/**
* Get the displayable label of the resource.
*/
public static function label(): string
{
return 'Question Groups';
}
/**
* Get the displayable singular label of the resource.
*/
public static function singularLabel(): string
{
return 'Question Group';
}
/** /**
* Get the fields displayed by the resource. * Get the fields displayed by the resource.
* *

View File

@@ -7,6 +7,7 @@
use Laravel\Nova\Fields\BelongsTo; use Laravel\Nova\Fields\BelongsTo;
use Laravel\Nova\Fields\Boolean; use Laravel\Nova\Fields\Boolean;
use Laravel\Nova\Fields\DateTime; use Laravel\Nova\Fields\DateTime;
use Laravel\Nova\Fields\HasMany;
use Laravel\Nova\Fields\ID; use Laravel\Nova\Fields\ID;
use Laravel\Nova\Fields\Number; use Laravel\Nova\Fields\Number;
use Laravel\Nova\Fields\Text; use Laravel\Nova\Fields\Text;
@@ -51,6 +52,22 @@ final class QuestionResource extends Resource
*/ */
public static $group = 'Questionnaire'; public static $group = 'Questionnaire';
/**
* Get the displayable label of the resource.
*/
public static function label(): string
{
return 'Questions';
}
/**
* Get the displayable singular label of the resource.
*/
public static function singularLabel(): string
{
return 'Question';
}
/** /**
* Get the fields displayed by the resource. * Get the fields displayed by the resource.
* *
@@ -111,6 +128,8 @@ public function fields(NovaRequest $request): array
->exceptOnForms() ->exceptOnForms()
->sortable() ->sortable()
->filterable(), ->filterable(),
HasMany::make('Answers', 'answers', AnswerResource::class),
]; ];
} }

75
app/Nova/RoleResource.php Normal file
View File

@@ -0,0 +1,75 @@
<?php
declare(strict_types=1);
namespace App\Nova;
use Laravel\Nova\Fields\DateTime;
use Laravel\Nova\Fields\HasMany;
use Laravel\Nova\Fields\ID;
use Laravel\Nova\Fields\Text;
use Laravel\Nova\Http\Requests\NovaRequest;
final class RoleResource extends Resource
{
public static string $model = \App\Models\Role::class;
public static $title = 'name';
public static $search = ['id', 'name'];
public static $displayInNavigation = false;
public static function label(): string
{
return 'Roles';
}
public static function singularLabel(): string
{
return 'Role';
}
public function fields(NovaRequest $request): array
{
return [
ID::make()->sortable(),
Text::make('Name')
->sortable()
->filterable()
->copyable()
->rules('required', 'max:255'),
DateTime::make('Created At')
->exceptOnForms()
->sortable(),
DateTime::make('Updated At')
->exceptOnForms()
->sortable(),
HasMany::make('Users', 'users', User::class),
];
}
public function cards(NovaRequest $request): array
{
return [];
}
public function filters(NovaRequest $request): array
{
return [];
}
public function lenses(NovaRequest $request): array
{
return [];
}
public function actions(NovaRequest $request): array
{
return [];
}
}

View File

@@ -57,6 +57,22 @@ final class ScreeningResource extends Resource
*/ */
public static $with = ['user']; public static $with = ['user'];
/**
* Get the displayable label of the resource.
*/
public static function label(): string
{
return 'Screenings';
}
/**
* Get the displayable singular label of the resource.
*/
public static function singularLabel(): string
{
return 'Screening';
}
/** /**
* Get the fields displayed by the resource. * Get the fields displayed by the resource.
* *

View File

@@ -5,12 +5,11 @@
namespace App\Nova; namespace App\Nova;
use Laravel\Nova\Fields\BelongsTo; use Laravel\Nova\Fields\BelongsTo;
use Laravel\Nova\Fields\Code;
use Laravel\Nova\Fields\DateTime; use Laravel\Nova\Fields\DateTime;
use Laravel\Nova\Fields\HasMany; use Laravel\Nova\Fields\HasMany;
use Laravel\Nova\Fields\ID; use Laravel\Nova\Fields\ID;
use Laravel\Nova\Fields\Number; use Laravel\Nova\Fields\Number;
use Laravel\Nova\Fields\Text; use Laravel\Nova\Fields\Select;
use Laravel\Nova\Fields\Textarea; use Laravel\Nova\Fields\Textarea;
use Laravel\Nova\Http\Requests\NovaRequest; use Laravel\Nova\Http\Requests\NovaRequest;
use Maatwebsite\LaravelNovaExcel\Actions\DownloadExcel; use Maatwebsite\LaravelNovaExcel\Actions\DownloadExcel;
@@ -59,6 +58,22 @@ final class SessionResource extends Resource
*/ */
public static $with = ['user', 'category', 'screening']; public static $with = ['user', 'category', 'screening'];
/**
* Get the displayable label of the resource.
*/
public static function label(): string
{
return 'Sessions';
}
/**
* Get the displayable singular label of the resource.
*/
public static function singularLabel(): string
{
return 'Session';
}
/** /**
* Get the fields displayed by the resource. * Get the fields displayed by the resource.
* *
@@ -85,11 +100,16 @@ public function fields(NovaRequest $request): array
->filterable() ->filterable()
->rules('nullable'), ->rules('nullable'),
Text::make('Status') Select::make('Status')
->options([
'in_progress' => 'In Progress',
'completed' => 'Completed',
'abandoned' => 'Abandoned',
])
->displayUsingLabels()
->sortable() ->sortable()
->filterable() ->filterable()
->copyable() ->readonly(),
->rules('required', 'max:255'),
Number::make('Score') Number::make('Score')
->sortable() ->sortable()
@@ -97,15 +117,16 @@ public function fields(NovaRequest $request): array
->copyable() ->copyable()
->rules('nullable', 'integer'), ->rules('nullable', 'integer'),
Text::make('Result') Select::make('Result')
->options([
'go' => 'Go',
'no_go' => 'No Go',
'consult_leadership' => 'Consult Leadership',
])
->displayUsingLabels()
->sortable() ->sortable()
->filterable() ->filterable()
->copyable() ->readonly(),
->rules('nullable', 'max:255'),
Code::make('Basic Info', 'basic_info')
->json()
->rules('nullable'),
Textarea::make('Additional Comments') Textarea::make('Additional Comments')
->rules('nullable'), ->rules('nullable'),

View File

@@ -6,6 +6,7 @@
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Laravel\Nova\Auth\PasswordValidationRules; use Laravel\Nova\Auth\PasswordValidationRules;
use Laravel\Nova\Fields\BelongsTo;
use Laravel\Nova\Fields\ID; use Laravel\Nova\Fields\ID;
use Laravel\Nova\Fields\Password; use Laravel\Nova\Fields\Password;
use Laravel\Nova\Fields\Text; use Laravel\Nova\Fields\Text;
@@ -35,7 +36,7 @@ final class User extends Resource
* @var array * @var array
*/ */
public static $search = [ public static $search = [
'id', 'name', 'email', 'id', 'name', 'email', 'department', 'job_title',
]; ];
/** /**
@@ -48,6 +49,10 @@ public function fields(NovaRequest $request): array
return [ return [
ID::make()->sortable(), ID::make()->sortable(),
BelongsTo::make('Role', 'role', RoleResource::class)
->sortable()
->filterable(),
Text::make('Name') Text::make('Name')
->sortable() ->sortable()
->rules('required', 'max:255'), ->rules('required', 'max:255'),
@@ -58,6 +63,31 @@ public function fields(NovaRequest $request): array
->creationRules('unique:users,email') ->creationRules('unique:users,email')
->updateRules('unique:users,email,{{resourceId}}'), ->updateRules('unique:users,email,{{resourceId}}'),
Text::make('Azure ID', 'azure_id')
->onlyOnDetail()
->copyable(),
Text::make('Photo', 'photo')
->onlyOnDetail()
->copyable(),
Text::make('Job Title', 'job_title')
->sortable()
->filterable()
->copyable()
->readonly(),
Text::make('Department')
->sortable()
->filterable()
->copyable()
->readonly(),
Text::make('Phone')
->sortable()
->copyable()
->readonly(),
Password::make('Password') Password::make('Password')
->onlyOnForms() ->onlyOnForms()
->creationRules($this->passwordRules()) ->creationRules($this->passwordRules())

View File

@@ -3,8 +3,16 @@
namespace App\Providers; namespace App\Providers;
use App\Models\User; use App\Models\User;
use App\Nova\Dashboards\Main;
use App\Nova\LogResource;
use App\Nova\QuestionResource;
use App\Nova\ScreeningResource;
use App\Nova\SessionResource;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Gate; use Illuminate\Support\Facades\Gate;
use Laravel\Fortify\Features; use Laravel\Fortify\Features;
use Laravel\Nova\Menu\MenuItem;
use Laravel\Nova\Menu\MenuSection;
use Laravel\Nova\Nova; use Laravel\Nova\Nova;
use Laravel\Nova\NovaApplicationServiceProvider; use Laravel\Nova\NovaApplicationServiceProvider;
@@ -17,7 +25,25 @@ public function boot(): void
{ {
parent::boot(); parent::boot();
// Nova::mainMenu(function (Request $request) {
return [
MenuSection::dashboard(Main::class)->icon('home'),
MenuSection::make('Questionnaire', [
MenuItem::resource(QuestionResource::class),
MenuItem::resource(ScreeningResource::class),
MenuItem::resource(SessionResource::class),
])->icon('clipboard-document-list')->collapsible(),
MenuSection::make('Logs', [
MenuItem::resource(LogResource::class),
])->icon('chart-bar')->collapsible(),
MenuSection::make('Users', [
MenuItem::resource(\App\Nova\User::class),
])->icon('users')->collapsible(),
];
});
} }
/** /**

View File

@@ -30,7 +30,6 @@ public function definition(): array
'status' => 'in_progress', 'status' => 'in_progress',
'score' => null, 'score' => null,
'result' => null, 'result' => null,
'basic_info' => null,
'additional_comments' => null, 'additional_comments' => null,
'completed_at' => null, 'completed_at' => null,
]; ];

View File

@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('roles', function (Blueprint $table) {
$table->id();
$table->string('name')->unique();
$table->timestamps();
});
$now = now();
DB::table('roles')->insert([
['name' => 'user', 'created_at' => $now, 'updated_at' => $now],
['name' => 'admin', 'created_at' => $now, 'updated_at' => $now],
]);
}
public function down(): void
{
Schema::dropIfExists('roles');
}
};

View File

@@ -13,10 +13,16 @@ public function up(): void
{ {
Schema::create('users', function (Blueprint $table) { Schema::create('users', function (Blueprint $table) {
$table->id(); $table->id();
$table->foreignId('role_id')->default(1)->constrained();
$table->string('name'); $table->string('name');
$table->string('email')->unique(); $table->string('email')->unique();
$table->string('azure_id')->nullable()->unique();
$table->string('photo')->nullable();
$table->string('job_title')->nullable();
$table->string('department')->nullable();
$table->string('phone')->nullable();
$table->timestamp('email_verified_at')->nullable(); $table->timestamp('email_verified_at')->nullable();
$table->string('password'); $table->string('password')->nullable();
$table->rememberToken(); $table->rememberToken();
$table->timestamps(); $table->timestamps();
}); });

View File

@@ -21,7 +21,6 @@ public function up(): void
$table->string('status', 50)->default('in_progress'); $table->string('status', 50)->default('in_progress');
$table->integer('score')->nullable(); $table->integer('score')->nullable();
$table->string('result', 50)->nullable(); $table->string('result', 50)->nullable();
$table->json('basic_info')->nullable();
$table->text('additional_comments')->nullable(); $table->text('additional_comments')->nullable();
$table->timestamp('completed_at')->nullable(); $table->timestamp('completed_at')->nullable();
$table->timestamps(); $table->timestamps();

View File

@@ -1,22 +1,28 @@
<?php <?php
declare(strict_types=1);
namespace Database\Seeders; namespace Database\Seeders;
use App\Models\Role;
use App\Models\User; use App\Models\User;
use Illuminate\Database\Seeder; use Illuminate\Database\Seeder;
class JonathanSeeder extends Seeder final class JonathanSeeder extends Seeder
{ {
/** /**
* Seed the application's database. * Seed the application's database with admin user Jonathan.
*/ */
public function run(): void public function run(): void
{ {
$adminRole = Role::where('name', 'admin')->first();
User::factory()->create([ User::factory()->create([
'name' => 'Jonathan', 'name' => 'Jonathan',
'email' => 'jonathan@blijnder.nl', 'email' => 'jonathan@blijnder.nl',
'password' => bcrypt('secret'), 'password' => bcrypt('secret'),
'email_verified_at' => now(), 'email_verified_at' => now(),
'role_id' => $adminRole->id,
]); ]);
} }
} }

View File

@@ -44,11 +44,11 @@ const buttonClasses = computed(() => {
// Size classes // Size classes
if (props.size === 'sm') { if (props.size === 'sm') {
classes.push('px-3 py-1.5 text-sm') classes.push('px-4 py-2 text-sm')
} else if (props.size === 'md') { } else if (props.size === 'md') {
classes.push('px-5 py-2.5 text-base') classes.push('px-6 py-3 text-base')
} else if (props.size === 'lg') { } else if (props.size === 'lg') {
classes.push('px-7 py-3 text-lg') classes.push('px-8 py-3.5 text-lg')
} }
// Variant classes // Variant classes

View File

@@ -1,4 +1,6 @@
<script setup> <script setup>
import { computed } from 'vue'
import { usePage } from '@inertiajs/vue3'
import AppLogo from '@/Components/AppLogo.vue' import AppLogo from '@/Components/AppLogo.vue'
defineProps({ defineProps({
@@ -7,12 +9,17 @@ defineProps({
default: '', default: '',
}, },
}) })
const page = usePage()
const logoHref = computed(() => page.props.auth?.logo_href ?? '/')
</script> </script>
<template> <template>
<header class="sticky top-0 z-50 bg-surface border-b border-gray-700"> <header class="sticky top-0 z-50 bg-surface border-b border-gray-700">
<div class="px-6 py-4 flex items-center gap-6"> <div class="px-6 py-4 flex items-center gap-6">
<AppLogo class="text-2xl" /> <a :href="logoHref" class="flex items-center">
<AppLogo class="text-2xl" />
</a>
<h1 v-if="title" class="text-xl font-semibold text-white"> <h1 v-if="title" class="text-xl font-semibold text-white">
{{ title }} {{ title }}
</h1> </h1>

View File

@@ -28,9 +28,12 @@ const handleChange = (value) => {
const getSegmentClasses = (index) => { const getSegmentClasses = (index) => {
const classes = [ const classes = [
'px-6', 'inline-flex',
'py-2.5', 'items-center',
'text-sm', 'justify-center',
'px-8',
'py-3',
'text-base',
'font-medium', 'font-medium',
'select-none', 'select-none',
'transition-all', 'transition-all',
@@ -64,7 +67,7 @@ const getSegmentClasses = (index) => {
</script> </script>
<template> <template>
<div role="radiogroup" :aria-label="name" class="inline-flex rounded-lg overflow-hidden border border-white/10"> <div role="radiogroup" :aria-label="name" class="inline-flex rounded-lg overflow-hidden border border-white/10 min-h-[44px]">
<label <label
v-for="(option, index) in options" v-for="(option, index) in options"
:key="option.value" :key="option.value"

View File

@@ -91,14 +91,6 @@ const resultDisplay = computed(() => {
<dt class="text-gray-400">Category</dt> <dt class="text-gray-400">Category</dt>
<dd class="text-white font-medium">{{ categoryName }}</dd> <dd class="text-white font-medium">{{ categoryName }}</dd>
</div> </div>
<div>
<dt class="text-gray-400">Client</dt>
<dd class="text-white font-medium">{{ session.basic_info?.client_name ?? 'N/A' }}</dd>
</div>
<div>
<dt class="text-gray-400">Lead Firm</dt>
<dd class="text-white font-medium">{{ session.basic_info?.lead_firm_name ?? 'N/A' }}</dd>
</div>
<div> <div>
<dt class="text-gray-400">Completed</dt> <dt class="text-gray-400">Completed</dt>
<dd class="text-white font-medium">{{ new Date(session.completed_at).toLocaleDateString() }}</dd> <dd class="text-white font-medium">{{ new Date(session.completed_at).toLocaleDateString() }}</dd>

View File

@@ -78,32 +78,6 @@ public function test_show_returns_inertia_props(): void
); );
} }
public function test_can_save_basic_info(): void
{
$user = $this->createAuthenticatedUser();
$category = Category::factory()->create();
$session = Session::factory()->create([
'user_id' => $user->id,
'category_id' => $category->id,
]);
$basicInfo = [
'client_name' => 'Test Client',
'client_contact' => 'client@example.com',
'lead_firm_name' => 'Test Firm',
'lead_firm_contact' => 'firm@example.com',
];
$this->put("/sessions/{$session->id}", [
'basic_info' => $basicInfo,
])->assertRedirect();
$session->refresh();
$this->assertEquals($basicInfo, $session->basic_info);
}
public function test_can_save_answers(): void public function test_can_save_answers(): void
{ {
$user = $this->createAuthenticatedUser(); $user = $this->createAuthenticatedUser();