step 1, 2 and 3 of the implementation plan

This commit is contained in:
2026-02-03 09:43:23 +01:00
parent d38001e3e2
commit 3684d9ef6b
34 changed files with 4070 additions and 18 deletions

View File

@@ -31,7 +31,7 @@ public function handle(): int
// Get all tables
$tables = DB::select('SHOW TABLES');
$tableKey = 'Tables_in_' . DB::getDatabaseName();
$tableKey = 'Tables_in_'.DB::getDatabaseName();
$tableNames = [];
foreach ($tables as $table) {
@@ -43,9 +43,9 @@ public function handle(): int
// Build markdown content
$markdown = "# Database Schema Documentation\n\n";
$markdown .= '> Generated: ' . date('Y-m-d H:i:s') . "\n";
$markdown .= '> Database: ' . DB::getDatabaseName() . "\n";
$markdown .= '> Total Tables: ' . count($tableNames) . "\n\n";
$markdown .= '> Generated: '.date('Y-m-d H:i:s')."\n";
$markdown .= '> Database: '.DB::getDatabaseName()."\n";
$markdown .= '> Total Tables: '.count($tableNames)."\n\n";
$markdown .= "## Table of Contents\n\n";
// Add table of contents
@@ -100,7 +100,7 @@ public function handle(): int
}
$markdown .= "| {$column->Field} | {$column->Type} | {$column->Null} | {$column->Key} | ";
$markdown .= ($column->Default === null ? 'NULL' : $column->Default) . ' | ';
$markdown .= ($column->Default === null ? 'NULL' : $column->Default).' | ';
$markdown .= "{$column->Extra} | {$fkInfo} |\n";
}
@@ -122,7 +122,7 @@ public function handle(): int
}
// Write to file
$filePath = $directory . '/schema.md';
$filePath = $directory.'/schema.md';
File::put($filePath, $markdown);
$this->info('✅ Schema documentation generated successfully!');

View File

@@ -0,0 +1,55 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Laravel\Socialite\Facades\Socialite;
final class SocialiteController extends Controller
{
/**
* Redirect the user to the Azure AD authentication page.
*/
public function redirect(): RedirectResponse
{
return Socialite::driver('azure')->redirect();
}
/**
* Handle the callback from Azure AD after authentication.
*/
public function callback(): RedirectResponse
{
$azureUser = Socialite::driver('azure')->user();
$user = User::query()->firstOrCreate(
['email' => $azureUser->getEmail()],
[
'name' => $azureUser->getName(),
'password' => null,
]
);
auth()->login($user);
return redirect('/');
}
/**
* Log the user out and redirect to landing page.
*/
public function logout(Request $request): RedirectResponse
{
auth()->logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return redirect('/');
}
}

View File

@@ -0,0 +1,60 @@
<?php
declare(strict_types=1);
namespace App\Http\Middleware;
use Illuminate\Http\Request;
use Illuminate\Support\Arr;
use Inertia\Middleware;
final class HandleInertiaRequests extends Middleware
{
/**
* The root template that is loaded on the first page visit.
*/
protected $rootView = 'app';
/**
* Determine the current asset version.
*/
public function version(Request $request): ?string
{
return parent::version($request);
}
/**
* Define the props that are shared by default.
*/
public function share(Request $request): array
{
return [
...parent::share($request),
'auth' => [
'user' => $this->getAuthenticatedUser(),
],
'flash' => [
'success' => fn () => Arr::get($request->session()->all(), 'success'),
'error' => fn () => Arr::get($request->session()->all(), 'error'),
],
];
}
/**
* Get authenticated user data for frontend.
*/
private function getAuthenticatedUser(): ?array
{
$user = auth()->user();
if ($user === null) {
return null;
}
return [
'id' => $user->id,
'name' => $user->name,
'email' => $user->email,
];
}
}

48
app/Models/Answer.php Normal file
View File

@@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
final class Answer extends Model
{
/**
* Fillable attributes for mass assignment.
*/
protected $fillable = [
'session_id',
'question_id',
'value',
'text_value',
];
/**
* Cast attributes to specific types.
*/
protected function casts(): array
{
return [
'session_id' => 'integer',
'question_id' => 'integer',
];
}
/**
* Get the session that owns this answer.
*/
public function session(): BelongsTo
{
return $this->belongsTo(Session::class);
}
/**
* Get the question that this answer belongs to.
*/
public function question(): BelongsTo
{
return $this->belongsTo(Question::class);
}
}

45
app/Models/Category.php Normal file
View File

@@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
final class Category extends Model
{
/**
* Fillable attributes for mass assignment.
*/
protected $fillable = [
'name',
'sort_order',
];
/**
* Cast attributes to specific types.
*/
protected function casts(): array
{
return [
'sort_order' => 'integer',
];
}
/**
* Get all question groups for this category.
*/
public function questionGroups(): HasMany
{
return $this->hasMany(QuestionGroup::class);
}
/**
* Get all sessions for this category.
*/
public function sessions(): HasMany
{
return $this->hasMany(Session::class);
}
}

78
app/Models/Log.php Normal file
View File

@@ -0,0 +1,78 @@
<?php
declare(strict_types=1);
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
final class Log extends Model
{
/**
* Disable the updated_at timestamp for append-only logs.
*/
public const UPDATED_AT = null;
/**
* Fillable attributes for mass assignment.
*/
protected $fillable = [
'user_id',
'session_id',
'category_id',
'action',
'metadata',
];
/**
* Cast attributes to specific types.
*/
protected function casts(): array
{
return [
'user_id' => 'integer',
'session_id' => 'integer',
'category_id' => 'integer',
'metadata' => 'array',
];
}
/**
* Boot the model and prevent updates and deletes.
*/
protected static function booted(): void
{
self::updating(function (): bool {
return false;
});
self::deleting(function (): bool {
return false;
});
}
/**
* Get the user that performed the logged action.
*/
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
/**
* Get the session associated with this log entry.
*/
public function session(): BelongsTo
{
return $this->belongsTo(Session::class);
}
/**
* Get the category associated with this log entry.
*/
public function category(): BelongsTo
{
return $this->belongsTo(Category::class);
}
}

48
app/Models/Question.php Normal file
View File

@@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
final class Question extends Model
{
/**
* Fillable attributes for mass assignment.
*/
protected $fillable = [
'question_group_id',
'text',
'has_yes',
'has_no',
'has_na',
'details',
'sort_order',
'is_scored',
];
/**
* Cast attributes to specific types.
*/
protected function casts(): array
{
return [
'question_group_id' => 'integer',
'has_yes' => 'boolean',
'has_no' => 'boolean',
'has_na' => 'boolean',
'sort_order' => 'integer',
'is_scored' => 'boolean',
];
}
/**
* Get the question group that owns this question.
*/
public function questionGroup(): BelongsTo
{
return $this->belongsTo(QuestionGroup::class);
}
}

View File

@@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
final class QuestionGroup extends Model
{
/**
* Fillable attributes for mass assignment.
*/
protected $fillable = [
'category_id',
'name',
'sort_order',
'description',
'scoring_instructions',
];
/**
* Cast attributes to specific types.
*/
protected function casts(): array
{
return [
'category_id' => 'integer',
'sort_order' => 'integer',
];
}
/**
* Get the category that owns this question group.
*/
public function category(): BelongsTo
{
return $this->belongsTo(Category::class);
}
/**
* Get all questions for this question group.
*/
public function questions(): HasMany
{
return $this->hasMany(Question::class);
}
}

57
app/Models/Screening.php Normal file
View File

@@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
final class Screening extends Model
{
/**
* Fillable attributes for mass assignment.
*/
protected $fillable = [
'user_id',
'score',
'passed',
];
/**
* Cast attributes to specific types.
*/
protected function casts(): array
{
return [
'user_id' => 'integer',
'score' => 'integer',
'passed' => 'boolean',
];
}
/**
* Get the user that owns this screening.
*/
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
/**
* Get all answers for this screening.
*/
public function answers(): HasMany
{
return $this->hasMany(ScreeningAnswer::class);
}
/**
* Get all sessions that reference this screening.
*/
public function sessions(): HasMany
{
return $this->hasMany(Session::class);
}
}

View File

@@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
final class ScreeningAnswer extends Model
{
/**
* Fillable attributes for mass assignment.
*/
protected $fillable = [
'screening_id',
'question_number',
'value',
];
/**
* Cast attributes to specific types.
*/
protected function casts(): array
{
return [
'screening_id' => 'integer',
'question_number' => 'integer',
];
}
/**
* Get the screening that owns this answer.
*/
public function screening(): BelongsTo
{
return $this->belongsTo(Screening::class);
}
}

84
app/Models/Session.php Normal file
View File

@@ -0,0 +1,84 @@
<?php
declare(strict_types=1);
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
final class Session extends Model
{
protected $table = 'questionnaire_sessions';
/**
* Fillable attributes for mass assignment.
*/
protected $fillable = [
'user_id',
'category_id',
'screening_id',
'status',
'score',
'result',
'basic_info',
'additional_comments',
'completed_at',
];
/**
* Cast attributes to specific types.
*/
protected function casts(): array
{
return [
'user_id' => 'integer',
'category_id' => 'integer',
'screening_id' => 'integer',
'score' => 'integer',
'basic_info' => 'array',
'completed_at' => 'datetime',
];
}
/**
* Get the user that owns this session.
*/
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
/**
* Get the category for this session.
*/
public function category(): BelongsTo
{
return $this->belongsTo(Category::class);
}
/**
* Get the screening that preceded this session.
*/
public function screening(): BelongsTo
{
return $this->belongsTo(Screening::class);
}
/**
* Get all answers for this session.
*/
public function answers(): HasMany
{
return $this->hasMany(Answer::class);
}
/**
* Get all logs for this session.
*/
public function logs(): HasMany
{
return $this->hasMany(Log::class);
}
}

View File

@@ -1,17 +1,18 @@
<?php
declare(strict_types=1);
namespace App\Models;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Fortify\TwoFactorAuthenticatable;
class User extends Authenticatable implements MustVerifyEmail
final class User extends Authenticatable
{
/** @use HasFactory<\Database\Factories\UserFactory> */
use HasFactory, Notifiable, TwoFactorAuthenticatable;
use HasFactory, Notifiable;
/**
* The attributes that are mass assignable.
@@ -46,4 +47,28 @@ protected function casts(): array
'password' => 'hashed',
];
}
/**
* Get all sessions for this user.
*/
public function sessions(): HasMany
{
return $this->hasMany(Session::class);
}
/**
* Get all screenings for this user.
*/
public function screenings(): HasMany
{
return $this->hasMany(Screening::class);
}
/**
* Get all logs for this user.
*/
public function logs(): HasMany
{
return $this->hasMany(Log::class);
}
}