completed story 1.1 with QA tests and fixes

This commit is contained in:
Naser Mansour
2025-12-26 13:46:39 +02:00
parent 028ce573b9
commit 84d9c2f66a
57 changed files with 2193 additions and 85 deletions
+42
View File
@@ -0,0 +1,42 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class AdminLog extends Model
{
use HasFactory;
public $timestamps = false;
protected $fillable = [
'admin_id',
'action',
'target_type',
'target_id',
'old_values',
'new_values',
'ip_address',
'created_at',
];
protected function casts(): array
{
return [
'old_values' => 'array',
'new_values' => 'array',
'created_at' => 'datetime',
];
}
/**
* Get the admin that created the log.
*/
public function admin(): BelongsTo
{
return $this->belongsTo(User::class, 'admin_id');
}
}
+33
View File
@@ -0,0 +1,33 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class BlockedTime extends Model
{
use HasFactory;
protected $fillable = [
'block_date',
'start_time',
'end_time',
'reason',
];
protected function casts(): array
{
return [
'block_date' => 'date',
];
}
/**
* Check if this is a full day block.
*/
public function isFullDay(): bool
{
return is_null($this->start_time) && is_null($this->end_time);
}
}
+46
View File
@@ -0,0 +1,46 @@
<?php
namespace App\Models;
use App\Enums\ConsultationStatus;
use App\Enums\ConsultationType;
use App\Enums\PaymentStatus;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Consultation extends Model
{
use HasFactory;
protected $fillable = [
'user_id',
'booking_date',
'booking_time',
'problem_summary',
'consultation_type',
'payment_amount',
'payment_status',
'status',
'admin_notes',
];
protected function casts(): array
{
return [
'booking_date' => 'date',
'consultation_type' => ConsultationType::class,
'payment_status' => PaymentStatus::class,
'status' => ConsultationStatus::class,
'payment_amount' => 'decimal:2',
];
}
/**
* Get the user that owns the consultation.
*/
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
}
+53
View File
@@ -0,0 +1,53 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Notification extends Model
{
use HasFactory;
protected $fillable = [
'user_id',
'type',
'data',
'read_at',
'sent_at',
];
protected function casts(): array
{
return [
'data' => 'array',
'read_at' => 'datetime',
'sent_at' => 'datetime',
];
}
/**
* Get the user that owns the notification.
*/
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
/**
* Mark the notification as read.
*/
public function markAsRead(): void
{
$this->update(['read_at' => now()]);
}
/**
* Scope to filter unread notifications.
*/
public function scopeUnread($query)
{
return $query->whereNull('read_at');
}
}
+65
View File
@@ -0,0 +1,65 @@
<?php
namespace App\Models;
use App\Enums\PostStatus;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Post extends Model
{
use HasFactory;
protected $fillable = [
'title',
'body',
'status',
'published_at',
];
protected function casts(): array
{
return [
'title' => 'array',
'body' => 'array',
'status' => PostStatus::class,
'published_at' => 'datetime',
];
}
/**
* Scope to filter published posts.
*/
public function scopePublished($query)
{
return $query->where('status', PostStatus::Published);
}
/**
* Scope to filter draft posts.
*/
public function scopeDraft($query)
{
return $query->where('status', PostStatus::Draft);
}
/**
* Get the title in the specified locale, with fallback to Arabic.
*/
public function getTitle(?string $locale = null): string
{
$locale ??= app()->getLocale();
return $this->title[$locale] ?? $this->title['ar'] ?? '';
}
/**
* Get the body in the specified locale, with fallback to Arabic.
*/
public function getBody(?string $locale = null): string
{
$locale ??= app()->getLocale();
return $this->body[$locale] ?? $this->body['ar'] ?? '';
}
}
+44
View File
@@ -0,0 +1,44 @@
<?php
namespace App\Models;
use App\Enums\TimelineStatus;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Timeline extends Model
{
use HasFactory;
protected $fillable = [
'user_id',
'case_name',
'case_reference',
'status',
];
protected function casts(): array
{
return [
'status' => TimelineStatus::class,
];
}
/**
* Get the user that owns the timeline.
*/
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
/**
* Get the updates for the timeline.
*/
public function updates(): HasMany
{
return $this->hasMany(TimelineUpdate::class);
}
}
+34
View File
@@ -0,0 +1,34 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class TimelineUpdate extends Model
{
use HasFactory;
protected $fillable = [
'timeline_id',
'admin_id',
'update_text',
];
/**
* Get the timeline that owns the update.
*/
public function timeline(): BelongsTo
{
return $this->belongsTo(Timeline::class);
}
/**
* Get the admin that created the update.
*/
public function admin(): BelongsTo
{
return $this->belongsTo(User::class, 'admin_id');
}
}
+115 -4
View File
@@ -2,8 +2,10 @@
namespace App\Models;
// use Illuminate\Contracts\Auth\MustVerifyEmail;
use App\Enums\UserStatus;
use App\Enums\UserType;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Illuminate\Support\Str;
@@ -20,9 +22,18 @@ class User extends Authenticatable
* @var list<string>
*/
protected $fillable = [
'name',
'user_type',
'full_name',
'national_id',
'company_name',
'company_cert_number',
'contact_person_name',
'contact_person_id',
'email',
'phone',
'password',
'status',
'preferred_language',
];
/**
@@ -32,6 +43,7 @@ class User extends Authenticatable
*/
protected $hidden = [
'password',
'national_id',
'two_factor_secret',
'two_factor_recovery_codes',
'remember_token',
@@ -45,20 +57,119 @@ class User extends Authenticatable
protected function casts(): array
{
return [
'user_type' => UserType::class,
'status' => UserStatus::class,
'email_verified_at' => 'datetime',
'two_factor_confirmed_at' => 'datetime',
'password' => 'hashed',
];
}
/**
* Get the user's initials
* Get the user's initials.
*/
public function initials(): string
{
return Str::of($this->name)
return Str::of($this->full_name)
->explode(' ')
->take(2)
->map(fn ($word) => Str::substr($word, 0, 1))
->implode('');
}
/**
* Check if user is an admin.
*/
public function isAdmin(): bool
{
return $this->user_type === UserType::Admin;
}
/**
* Check if user is an individual client.
*/
public function isIndividual(): bool
{
return $this->user_type === UserType::Individual;
}
/**
* Check if user is a company client.
*/
public function isCompany(): bool
{
return $this->user_type === UserType::Company;
}
/**
* Check if user is a client (individual or company).
*/
public function isClient(): bool
{
return $this->isIndividual() || $this->isCompany();
}
/**
* Scope to filter admin users.
*/
public function scopeAdmins($query)
{
return $query->where('user_type', UserType::Admin);
}
/**
* Scope to filter client users (individual or company).
*/
public function scopeClients($query)
{
return $query->whereIn('user_type', [UserType::Individual, UserType::Company]);
}
/**
* Scope to filter active users.
*/
public function scopeActive($query)
{
return $query->where('status', UserStatus::Active);
}
/**
* Get the consultations for the user.
*/
public function consultations(): HasMany
{
return $this->hasMany(Consultation::class);
}
/**
* Get the timelines for the user.
*/
public function timelines(): HasMany
{
return $this->hasMany(Timeline::class);
}
/**
* Get the notifications for the user.
*/
public function customNotifications(): HasMany
{
return $this->hasMany(Notification::class);
}
/**
* Get the admin logs for the user.
*/
public function adminLogs(): HasMany
{
return $this->hasMany(AdminLog::class, 'admin_id');
}
/**
* Get the timeline updates created by this admin.
*/
public function timelineUpdates(): HasMany
{
return $this->hasMany(TimelineUpdate::class, 'admin_id');
}
}
+34
View File
@@ -0,0 +1,34 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class WorkingHour extends Model
{
use HasFactory;
protected $fillable = [
'day_of_week',
'start_time',
'end_time',
'is_active',
];
protected function casts(): array
{
return [
'day_of_week' => 'integer',
'is_active' => 'boolean',
];
}
/**
* Scope to filter active working hours.
*/
public function scopeActive($query)
{
return $query->where('is_active', true);
}
}