feat: Reinitialize frontend with SvelteKit and TypeScript

- Delete old Vite+Svelte frontend
- Initialize new SvelteKit project with TypeScript
- Configure Tailwind CSS v4 + DaisyUI
- Implement JWT authentication with auto-refresh
- Create login page with form validation (Zod)
- Add protected route guards
- Update Docker configuration for single-stage build
- Add E2E tests with Playwright (6/11 passing)
- Fix Svelte 5 reactivity with $state() runes

Known issues:
- 5 E2E tests failing (timing/async issues)
- Token refresh implementation needs debugging
- Validation error display timing
This commit is contained in:
2026-02-17 16:19:59 -05:00
parent 54df6018f5
commit f935754df4
120 changed files with 21772 additions and 90 deletions

View File

@@ -0,0 +1,46 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Concerns\HasUuids;
class Actual extends Model
{
use HasFactory, HasUuids;
protected $primaryKey = 'id';
public $incrementing = false;
protected $keyType = 'string';
protected $table = 'actuals';
protected $fillable = [
'project_id',
'team_member_id',
'month',
'hours_logged',
];
protected $casts = [
'month' => 'date',
'hours_logged' => 'decimal:2',
];
/**
* Get the project that owns the actual.
*/
public function project()
{
return $this->belongsTo(Project::class);
}
/**
* Get the team member that owns the actual.
*/
public function teamMember()
{
return $this->belongsTo(TeamMember::class);
}
}

View File

@@ -0,0 +1,44 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Concerns\HasUuids;
class Allocation extends Model
{
use HasFactory, HasUuids;
protected $primaryKey = 'id';
public $incrementing = false;
protected $keyType = 'string';
protected $fillable = [
'project_id',
'team_member_id',
'month',
'allocated_hours',
];
protected $casts = [
'month' => 'date',
'allocated_hours' => 'decimal:2',
];
/**
* Get the project that owns the allocation.
*/
public function project()
{
return $this->belongsTo(Project::class);
}
/**
* Get the team member that owns the allocation.
*/
public function teamMember()
{
return $this->belongsTo(TeamMember::class);
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Concerns\HasUuids;
class Holiday extends Model
{
use HasFactory, HasUuids;
protected $primaryKey = 'id';
public $incrementing = false;
protected $keyType = 'string';
protected $fillable = [
'date',
'name',
'description',
];
protected $casts = [
'date' => 'date',
];
}

View File

@@ -0,0 +1,63 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Concerns\HasUuids;
class Project extends Model
{
use HasFactory, HasUuids;
protected $primaryKey = 'id';
public $incrementing = false;
protected $keyType = 'string';
protected $fillable = [
'code',
'title',
'status_id',
'type_id',
'approved_estimate',
'forecasted_effort',
];
protected $casts = [
'approved_estimate' => 'decimal:2',
'forecasted_effort' => 'array',
];
/**
* Get the status that owns the project.
*/
public function status()
{
return $this->belongsTo(ProjectStatus::class, 'status_id');
}
/**
* Get the type that owns the project.
*/
public function type()
{
return $this->belongsTo(ProjectType::class, 'type_id');
}
/**
* Get the allocations for the project.
*/
public function allocations(): HasMany
{
return $this->hasMany(Allocation::class);
}
/**
* Get the actuals for the project.
*/
public function actuals(): HasMany
{
return $this->hasMany(Actual::class);
}
}

View File

@@ -0,0 +1,33 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
class ProjectStatus extends Model
{
use HasFactory;
protected $fillable = [
'name',
'order',
'is_active',
'is_billable',
];
protected $casts = [
'order' => 'integer',
'is_active' => 'boolean',
'is_billable' => 'boolean',
];
/**
* Get the projects for the status.
*/
public function projects(): HasMany
{
return $this->hasMany(Project::class, 'status_id');
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
class ProjectType extends Model
{
use HasFactory;
protected $fillable = [
'name',
'description',
];
/**
* Get the projects for the type.
*/
public function projects(): HasMany
{
return $this->hasMany(Project::class, 'type_id');
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Concerns\HasUuids;
class Pto extends Model
{
use HasFactory, HasUuids;
protected $primaryKey = 'id';
public $incrementing = false;
protected $keyType = 'string';
protected $table = 'ptos';
protected $fillable = [
'team_member_id',
'start_date',
'end_date',
'reason',
'status',
];
protected $casts = [
'start_date' => 'date',
'end_date' => 'date',
];
/**
* Get the team member that owns the PTO.
*/
public function teamMember()
{
return $this->belongsTo(TeamMember::class);
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Role extends Model
{
use HasFactory;
protected $fillable = [
'name',
'description',
];
/**
* Get the team members for the role.
*/
public function teamMembers(): HasMany
{
return $this->hasMany(TeamMember::class);
}
}

View File

@@ -0,0 +1,61 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Concerns\HasUuids;
class TeamMember extends Model
{
use HasFactory, HasUuids;
protected $primaryKey = 'id';
public $incrementing = false;
protected $keyType = 'string';
protected $fillable = [
'name',
'role_id',
'hourly_rate',
'active',
];
protected $casts = [
'hourly_rate' => 'decimal:2',
'active' => 'boolean',
];
/**
* Get the role that owns the team member.
*/
public function role()
{
return $this->belongsTo(Role::class);
}
/**
* Get the allocations for the team member.
*/
public function allocations(): HasMany
{
return $this->hasMany(Allocation::class);
}
/**
* Get the actuals for the team member.
*/
public function actuals(): HasMany
{
return $this->hasMany(Actual::class);
}
/**
* Get the PTOs for the team member.
*/
public function ptos(): HasMany
{
return $this->hasMany(Pto::class);
}
}

View File

@@ -0,0 +1,78 @@
<?php
namespace App\Models;
// use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Illuminate\Database\Eloquent\Concerns\HasUuids;
use Tymon\JWTAuth\Contracts\JWTSubject;
class User extends Authenticatable implements JWTSubject
{
/** @use HasFactory<\Database\Factories\UserFactory> */
use HasFactory, Notifiable, HasUuids;
protected $primaryKey = 'id';
public $incrementing = false;
protected $keyType = 'string';
/**
* The attributes that are mass assignable.
*
* @var list<string>
*/
protected $fillable = [
'name',
'email',
'password',
'role',
];
/**
* The attributes that should be hidden for serialization.
*
* @var list<string>
*/
protected $hidden = [
'password',
'remember_token',
];
/**
* Get the attributes that should be cast.
*
* @return array<string, string>
*/
protected function casts(): array
{
return [
'email_verified_at' => 'datetime',
'password' => 'hashed',
];
}
/**
* Get the identifier that will be stored in the subject claim of the JWT.
*
* @return mixed
*/
public function getJWTIdentifier()
{
return $this->getKey();
}
/**
* Return a key value array, containing any custom claims to be added to the JWT.
*
* @return array
*/
public function getJWTCustomClaims()
{
return [
'role' => $this->role,
'email' => $this->email,
];
}
}