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
+4 -2
View File
@@ -19,7 +19,7 @@ class CreateNewUser implements CreatesNewUsers
public function create(array $input): User
{
Validator::make($input, [
'name' => ['required', 'string', 'max:255'],
'full_name' => ['required', 'string', 'max:255'],
'email' => [
'required',
'string',
@@ -27,12 +27,14 @@ class CreateNewUser implements CreatesNewUsers
'max:255',
Rule::unique(User::class),
],
'phone' => ['required', 'string', 'max:20'],
'password' => $this->passwordRules(),
])->validate();
return User::create([
'name' => $input['name'],
'full_name' => $input['full_name'],
'email' => $input['email'],
'phone' => $input['phone'],
'password' => $input['password'],
]);
}
+13
View File
@@ -0,0 +1,13 @@
<?php
namespace App\Enums;
enum ConsultationStatus: string
{
case Pending = 'pending';
case Approved = 'approved';
case Rejected = 'rejected';
case Completed = 'completed';
case NoShow = 'no_show';
case Cancelled = 'cancelled';
}
+9
View File
@@ -0,0 +1,9 @@
<?php
namespace App\Enums;
enum ConsultationType: string
{
case Free = 'free';
case Paid = 'paid';
}
+10
View File
@@ -0,0 +1,10 @@
<?php
namespace App\Enums;
enum PaymentStatus: string
{
case Pending = 'pending';
case Received = 'received';
case NotApplicable = 'na';
}
+9
View File
@@ -0,0 +1,9 @@
<?php
namespace App\Enums;
enum PostStatus: string
{
case Draft = 'draft';
case Published = 'published';
}
+9
View File
@@ -0,0 +1,9 @@
<?php
namespace App\Enums;
enum TimelineStatus: string
{
case Active = 'active';
case Archived = 'archived';
}
+9
View File
@@ -0,0 +1,9 @@
<?php
namespace App\Enums;
enum UserStatus: string
{
case Active = 'active';
case Deactivated = 'deactivated';
}
+10
View File
@@ -0,0 +1,10 @@
<?php
namespace App\Enums;
enum UserType: string
{
case Admin = 'admin';
case Individual = 'individual';
case Company = 'company';
}
+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);
}
}