initial: project state before operator dashboard card redesign
This commit is contained in:
commit
e0aed8eed7
113 files changed
+20074
No files matched your search
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use App\Models\User;
|
||||
|
||||
class Booking extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'parking_lot_id',
|
||||
'customer_name',
|
||||
'phone',
|
||||
'vehicle_plate',
|
||||
'source',
|
||||
'start_time',
|
||||
'end_time',
|
||||
'status',
|
||||
'total_fee',
|
||||
'payment_method',
|
||||
'payment_proof',
|
||||
'paid_at',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'start_time' => 'datetime',
|
||||
'end_time' => 'datetime',
|
||||
'paid_at' => 'datetime',
|
||||
'total_fee' => 'decimal:2',
|
||||
];
|
||||
|
||||
public function parkingLot(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(ParkingLot::class);
|
||||
}
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Carbon\Carbon;
|
||||
|
||||
class CarRegistry extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'parking_lot_id',
|
||||
'plate_number',
|
||||
'entry_time',
|
||||
'exit_time',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'entry_time' => 'datetime',
|
||||
'exit_time' => 'datetime',
|
||||
];
|
||||
|
||||
public function parkingLot(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(ParkingLot::class);
|
||||
}
|
||||
|
||||
public function scopeActive($query)
|
||||
{
|
||||
return $query->whereNull('exit_time')
|
||||
->orWhere('exit_time', '>', Carbon::now());
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Carbon\Carbon;
|
||||
|
||||
class ParkingLot extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'address',
|
||||
'total_capacity',
|
||||
'price_per_hour',
|
||||
'pricing_rules',
|
||||
'latitude',
|
||||
'longitude',
|
||||
'working_hours',
|
||||
'is_active',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'latitude' => 'decimal:8',
|
||||
'longitude' => 'decimal:8',
|
||||
'price_per_hour' => 'decimal:2',
|
||||
'total_capacity' => 'integer',
|
||||
'is_active' => 'boolean',
|
||||
'pricing_rules' => 'array',
|
||||
];
|
||||
|
||||
public function scopeActive($query)
|
||||
{
|
||||
return $query->where('is_active', true);
|
||||
}
|
||||
|
||||
public function scopeWithStatus($query)
|
||||
{
|
||||
return $query->withCount([
|
||||
'carRegistries as active_registries_count' => function ($q) {
|
||||
$q->active();
|
||||
},
|
||||
'bookings as active_bookings_count' => function ($q) {
|
||||
$q->where('status', 'active');
|
||||
}
|
||||
]);
|
||||
}
|
||||
|
||||
public function bookings(): HasMany
|
||||
{
|
||||
return $this->hasMany(Booking::class);
|
||||
}
|
||||
|
||||
public function carRegistries(): HasMany
|
||||
{
|
||||
return $this->hasMany(CarRegistry::class);
|
||||
}
|
||||
|
||||
public function scopeSearch($query, $search)
|
||||
{
|
||||
return $query->where('name', 'like', "%{$search}%")
|
||||
->orWhere('address', 'like', "%{$search}%");
|
||||
}
|
||||
|
||||
public function getAvailableSpacesAttribute()
|
||||
{
|
||||
$activeBookings = $this->bookings()->where('status', 'active')->count();
|
||||
$activeRegistries = $this->carRegistries()->active()->count();
|
||||
$occupied = $activeBookings + $activeRegistries;
|
||||
return max(0, $this->total_capacity - $occupied);
|
||||
}
|
||||
|
||||
public function getOccupiedSpacesAttribute()
|
||||
{
|
||||
$activeBookings = $this->bookings()->where('status', 'active')->count();
|
||||
$activeRegistries = $this->carRegistries()->active()->count();
|
||||
return $activeBookings + $activeRegistries;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate fee and per-day breakdown between two timestamps.
|
||||
* Uses pricing_rules (ISO weekday keys 1–7) if set, otherwise price_per_hour.
|
||||
*
|
||||
* Returns ['total' => float, 'details' => [['day','date','hours','rate','subtotal'], ...]]
|
||||
*/
|
||||
public function calculateFee(Carbon $start, Carbon $end): array
|
||||
{
|
||||
$rules = $this->pricing_rules ?? [];
|
||||
$details = [];
|
||||
$total = 0.0;
|
||||
$cursor = $start->copy()->seconds(0);
|
||||
|
||||
while ($cursor < $end) {
|
||||
$dayEnd = $cursor->copy()->endOfDay()->addSecond();
|
||||
$segEnd = ($dayEnd < $end) ? $dayEnd : $end;
|
||||
|
||||
$dow = (int) $cursor->format('N'); // 1=Mon … 7=Sun
|
||||
$rate = isset($rules[$dow]) ? (float) $rules[$dow] : (float) $this->price_per_hour;
|
||||
$hours = round($cursor->diffInMinutes($segEnd) / 60, 4);
|
||||
$subtotal = $hours * $rate;
|
||||
|
||||
$details[] = [
|
||||
'day' => $this->arabicDayName($dow),
|
||||
'date' => $cursor->format('Y/m/d'),
|
||||
'hours' => round($hours, 2),
|
||||
'rate' => $rate,
|
||||
'subtotal' => round($subtotal, 2),
|
||||
];
|
||||
|
||||
$total += $subtotal;
|
||||
$cursor = $segEnd;
|
||||
}
|
||||
|
||||
return ['total' => (float) number_format(ceil($total), 2, '.', ''), 'details' => $details];
|
||||
}
|
||||
|
||||
private function arabicDayName(int $iso): string
|
||||
{
|
||||
return ['الاثنين', 'الثلاثاء', 'الأربعاء', 'الخميس', 'الجمعة', 'السبت', 'الأحد'][$iso - 1] ?? '';
|
||||
}
|
||||
|
||||
public function getUsagePercentageAttribute()
|
||||
{
|
||||
$occupied = $this->occupied_spaces;
|
||||
return $this->total_capacity > 0 ? round(($occupied / $this->total_capacity) * 100, 2) : 0;
|
||||
}
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
// use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
use Database\Factories\UserFactory;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
|
||||
class User extends Authenticatable
|
||||
{
|
||||
/** @use HasFactory<UserFactory> */
|
||||
use HasFactory, Notifiable;
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
* @var list<string>
|
||||
*/
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'email',
|
||||
'password',
|
||||
'role',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'email_verified_at' => 'datetime',
|
||||
'password' => 'hashed',
|
||||
'role' => 'string',
|
||||
];
|
||||
|
||||
/**
|
||||
* 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',
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user