Compare commits

..

1 Commits

Author SHA1 Message Date
root
b07423aef3 latest 2026-05-11 17:58:21 +00:00
42 changed files with 1431 additions and 394 deletions

View File

@ -20,7 +20,22 @@
"Bash(php -r \"echo file_put_contents\\('C:/xampp/htdocs/scp-syria/storage/app/public/test.txt', 'ok'\\) ? 'write OK' : 'write FAIL';\")", "Bash(php -r \"echo file_put_contents\\('C:/xampp/htdocs/scp-syria/storage/app/public/test.txt', 'ok'\\) ? 'write OK' : 'write FAIL';\")",
"Bash(curl:*)", "Bash(curl:*)",
"Bash(php:*)", "Bash(php:*)",
"Bash(grep -v \"^$\")" "Bash(grep -v \"^$\")",
"Bash(git config *)",
"Bash(/usr/local/bin/npm run *)",
"Bash(node_modules/.bin/vite build *)",
"Read(//usr/local/nvm/versions/**)",
"Read(//root/.nvm/versions/**)",
"Read(//opt/**)",
"Bash(/root/.vscode-server/cli/servers/Stable-560a9dba96f961efea7b1612916f89e5d5d4d679/server/node /var/www/scp-syria/node_modules/vite/bin/vite.js build --config /var/www/scp-syria/vite.config.js)",
"Bash(grep -o \".\\\\{0,80\\\\}inset-inline-end:0.\\\\{0,80\\\\}\" /var/www/scp-syria/public/build/assets/app-BtJpbOwL.css)",
"Bash(grep -o \".\\\\{0,150\\\\}sidebar-toggle.\\\\{0,20\\\\}display:none.\\\\{0,200\\\\}\" /var/www/scp-syria/public/build/assets/app-BtJpbOwL.css)",
"Bash(grep -o \".\\\\{0,60\\\\}inset-inline.\\\\{0,80\\\\}height:100%.\\\\{0,30\\\\}\" /var/www/scp-syria/public/build/assets/app-BtJpbOwL.css)",
"Bash(grep -o \".\\\\{0,10\\\\}app-sidebar.\\\\{0,40\\\\}display:none.\\\\{0,10\\\\}\" /var/www/scp-syria/public/build/assets/app-BtJpbOwL.css)",
"Bash(node node_modules/vite/bin/vite.js build)",
"Bash(/root/.vscode-server/cli/servers/Stable-41dd792b5e652393e7787322889ed5fdc58bd75b/server/node /var/www/scp-syria/node_modules/vite/bin/vite.js build --root /var/www/scp-syria)",
"Bash(/root/.vscode-server/cli/servers/Stable-41dd792b5e652393e7787322889ed5fdc58bd75b/server/node node_modules/vite/bin/vite.js build)",
"Bash(python3 *)"
] ]
} }
} }

48
.env
View File

@ -1,44 +1,23 @@
APP_NAME="Smart Car Park" APP_NAME="Damascus Parking"
APP_ENV=local APP_ENV=local
APP_KEY=base64:92pMpQ2HV7icz1lfUZUQfUguPfUdqYUPVU+aWdYDstY= APP_KEY=base64:gC4TfmQLkKdWmH/vRQ9KY//WgH0w8+RhkVuswqCbgxo=
APP_DEBUG=true APP_DEBUG=true
APP_URL=http://localhost APP_TIMEZONE=Asia/Damascus
APP_URL=http://localhost:8000
APP_LOCALE=en
APP_FALLBACK_LOCALE=en
APP_FAKER_LOCALE=en_US
APP_MAINTENANCE_DRIVER=file
# APP_MAINTENANCE_STORE=database
# PHP_CLI_SERVER_WORKERS=4
BCRYPT_ROUNDS=12
LOG_CHANNEL=stack LOG_CHANNEL=stack
LOG_STACK=single
LOG_DEPRECATIONS_CHANNEL=null LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=debug LOG_LEVEL=debug
DB_CONNECTION=sqlite DB_CONNECTION=sqlite
# DB_HOST=127.0.0.1 DB_DATABASE=/var/www/scp-syria/database/database.sqlite
# DB_PORT=3306
# DB_DATABASE=laravel
# DB_USERNAME=root
# DB_PASSWORD=
SESSION_DRIVER=database
SESSION_LIFETIME=120
SESSION_ENCRYPT=false
SESSION_PATH=/
SESSION_DOMAIN=null
BROADCAST_CONNECTION=log BROADCAST_CONNECTION=log
FILESYSTEM_DISK=local
QUEUE_CONNECTION=database
CACHE_STORE=database CACHE_STORE=database
# CACHE_PREFIX= FILESYSTEM_DISK=local
QUEUE_CONNECTION=sync
SESSION_DRIVER=database
SESSION_LIFETIME=120
MEMCACHED_HOST=127.0.0.1 MEMCACHED_HOST=127.0.0.1
@ -47,12 +26,12 @@ REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null REDIS_PASSWORD=null
REDIS_PORT=6379 REDIS_PORT=6379
MAIL_MAILER=log MAIL_MAILER=smtp
MAIL_SCHEME=null MAIL_HOST=mailpit
MAIL_HOST=127.0.0.1 MAIL_PORT=1025
MAIL_PORT=2525
MAIL_USERNAME=null MAIL_USERNAME=null
MAIL_PASSWORD=null MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS="hello@example.com" MAIL_FROM_ADDRESS="hello@example.com"
MAIL_FROM_NAME="${APP_NAME}" MAIL_FROM_NAME="${APP_NAME}"
@ -63,3 +42,4 @@ AWS_BUCKET=
AWS_USE_PATH_STYLE_ENDPOINT=false AWS_USE_PATH_STYLE_ENDPOINT=false
VITE_APP_NAME="${APP_NAME}" VITE_APP_NAME="${APP_NAME}"

View File

@ -2,6 +2,28 @@
## Working Rules ## Working Rules
### Back Buttons Must Be on the Far Left in RTL
In an RTL layout, "back" navigation buttons must always appear on the **far left** end of the header row — not the right. In RTL flex rows, the first HTML child lands on the right, so placing the back button first puts it on the wrong side.
**The fix:** always place the back button **last** in the flex row HTML (so it lands on the left in RTL flow), or combine it with `ms-auto` to push it to the left end.
```html
{{-- CORRECT: button last in RTL flex row → appears on the far left --}}
<div class="d-flex align-items-center gap-3">
<div>
<h1>Page Title</h1>
</div>
<a href="..." class="btn btn-sm ms-auto">
<i class="bi bi-arrow-right me-1"></i>العودة
</a>
</div>
```
**Rule: never place a back/return button as the first child of a flex row — it will appear on the right side in RTL. Always put it last.**
---
### Modal Close Button Must Be on the Far Left in RTL ### Modal Close Button Must Be on the Far Left in RTL
Bootstrap compiles `.modal-header .btn-close` with physical `margin-left: auto`, which in RTL pushes the × button to the right (next to the title text) instead of to the far left end of the header. Bootstrap compiles `.modal-header .btn-close` with physical `margin-left: auto`, which in RTL pushes the × button to the right (next to the title text) instead of to the far left end of the header.
@ -136,12 +158,14 @@ resources/
| Role | Middleware | Access | | Role | Middleware | Access |
|------------|-----------------|---------------------------------------------| |------------|-----------------|---------------------------------------------|
| `admin` | `EnsureAdmin` | Full admin dashboard, parking lot CRUD, all bookings | | `admin` | `EnsureAdmin` | Full admin dashboard, parking lot CRUD, all bookings, operator management |
| `operator` | `EnsureOperator`| Vehicle check-in/check-out, active bookings for assigned lot | | `operator` | `EnsureOperator`| Vehicle check-in/check-out, stats dashboard — restricted to assigned lots only |
| `user` | (auth only) | Public search, create bookings via API | | `user` | (auth only) | Public search, create bookings via API |
Both middleware classes return Arabic 403 messages on unauthorized access. Both middleware classes return Arabic 403 messages on unauthorized access.
**Operator lot assignment:** uses pivot table `operator_parking_lot` (many-to-many). An operator with no lots assigned sees no lots. The admin assigns lots via the operators management page. See `User::assignedLots()` BelongsToMany.
--- ---
## Database Schema ## Database Schema
@ -154,6 +178,15 @@ Both middleware classes return Arabic 403 messages on unauthorized access.
| email | string unique | | | email | string unique | |
| password | string | bcrypt | | password | string | bcrypt |
| role | string | 'admin' \| 'operator' \| 'user' (default) | | role | string | 'admin' \| 'operator' \| 'user' (default) |
| phone | string nullable | |
| parking_lot_id | bigint nullable | **Unused legacy column** — left in DB due to SQLite FK drop limitation. All operator-lot assignments now live in `operator_parking_lot` pivot. |
### `operator_parking_lot` (pivot)
| Column | Type | Notes |
|--------|------|-------|
| user_id | FK → users | cascadeOnDelete |
| parking_lot_id | FK → parking_lots | cascadeOnDelete |
| unique | (user_id, parking_lot_id) | one row per assignment |
### `parking_lots` ### `parking_lots`
| Column | Type | Notes | | Column | Type | Notes |
@ -231,7 +264,9 @@ Check-in requires: `parking_lot_id`, `vehicle_plate`, `duration_hours` (0.252
| `POST /admin/parking-lots/{id}/toggle` | admin | Toggle active status | | `POST /admin/parking-lots/{id}/toggle` | admin | Toggle active status |
| `GET /admin/bookings/active` | admin | Active bookings | | `GET /admin/bookings/active` | admin | Active bookings |
| `POST /admin/bookings/{id}/complete` | admin | Mark booking complete | | `POST /admin/bookings/{id}/complete` | admin | Mark booking complete |
| `GET /operator/dashboard` | operator | Operator view | | `GET /operator/dashboard` | operator | Operator lot-picker + operations panel |
| `GET /operator/stats` | operator | Operator statistics & charts dashboard |
| `GET /operator/stats-data` | operator | JSON stats data for operator dashboard (AJAX) |
| `POST /operator/check-in` | operator | Vehicle entry | | `POST /operator/check-in` | operator | Vehicle entry |
| `POST /operator/{booking}/checkout` | operator | Vehicle exit + fee | | `POST /operator/{booking}/checkout` | operator | Vehicle exit + fee |
| `GET /admin/stats` | admin | JSON stats (AJAX) | | `GET /admin/stats` | admin | JSON stats (AJAX) |
@ -361,7 +396,9 @@ php artisan config:clear && php artisan test
## Notes & Known Patterns ## Notes & Known Patterns
- Admin stats (`/admin/stats`, `/admin/charts`) are fetched via AJAX on page load — not cached, computed fresh each request. - Admin stats (`/admin/stats`, `/admin/charts`) are fetched via AJAX on page load — not cached, computed fresh each request.
- Operator stats (`/operator/stats-data`) follow the same pattern — AJAX on page load, auto-refreshes every 30 s, scoped to the operator's assigned lots only.
- The `CarRegistry` model tracks physical vehicle presence; `Booking` tracks reservations — they are separate but both count toward capacity. - The `CarRegistry` model tracks physical vehicle presence; `Booking` tracks reservations — they are separate but both count toward capacity.
- **Multi-lot operator assignment:** `User::assignedLots()` is a BelongsToMany through `operator_parking_lot`. Use `$user->assignedLots->pluck('id')` to get the operator's allowed lot IDs. An operator with an empty `assignedLots` collection sees **no** lots (unlike the old single-FK system where null = all lots).
- `StoreParkingLotRequest` and `StoreBookingRequest` are in `app/Http/Requests/`. - `StoreParkingLotRequest` and `StoreBookingRequest` are in `app/Http/Requests/`.
- Arabic error messages are returned by both middleware and validation responses. - Arabic error messages are returned by both middleware and validation responses.
- The `TODO.md` at the root is nearly empty — "Update with checked" is the only entry. - The `TODO.md` at the root is nearly empty — "Update with checked" is the only entry.

View File

@ -14,7 +14,7 @@ class AdminOperatorController extends Controller
public function index() public function index()
{ {
$operators = User::where('role', 'operator') $operators = User::where('role', 'operator')
->with('assignedLot') ->with('assignedLots')
->orderBy('name') ->orderBy('name')
->get(); ->get();
@ -30,20 +30,22 @@ class AdminOperatorController extends Controller
'email' => 'required|email|unique:users,email', 'email' => 'required|email|unique:users,email',
'phone' => 'nullable|string|max:20', 'phone' => 'nullable|string|max:20',
'password' => ['required', Password::min(8)], 'password' => ['required', Password::min(8)],
'parking_lot_id' => 'nullable|exists:parking_lots,id', 'lot_ids' => 'nullable|array',
'lot_ids.*'=> 'exists:parking_lots,id',
], [ ], [
'email.unique' => 'هذا البريد الإلكتروني مستخدم بالفعل.', 'email.unique' => 'هذا البريد الإلكتروني مستخدم بالفعل.',
]); ]);
User::create([ $operator = User::create([
'name' => $data['name'], 'name' => $data['name'],
'email' => $data['email'], 'email' => $data['email'],
'phone' => $data['phone'] ?? null, 'phone' => $data['phone'] ?? null,
'password' => Hash::make($data['password']), 'password' => Hash::make($data['password']),
'role' => 'operator', 'role' => 'operator',
'parking_lot_id' => $data['parking_lot_id'] ?? null,
]); ]);
$operator->assignedLots()->sync($data['lot_ids'] ?? []);
return response()->json(['success' => true, 'message' => 'تم إنشاء حساب المشغّل بنجاح.']); return response()->json(['success' => true, 'message' => 'تم إنشاء حساب المشغّل بنجاح.']);
} }
@ -58,7 +60,8 @@ class AdminOperatorController extends Controller
'email' => 'required|email|unique:users,email,' . $operator->id, 'email' => 'required|email|unique:users,email,' . $operator->id,
'phone' => 'nullable|string|max:20', 'phone' => 'nullable|string|max:20',
'password' => ['nullable', Password::min(8)], 'password' => ['nullable', Password::min(8)],
'parking_lot_id' => 'nullable|exists:parking_lots,id', 'lot_ids' => 'nullable|array',
'lot_ids.*'=> 'exists:parking_lots,id',
], [ ], [
'email.unique' => 'هذا البريد الإلكتروني مستخدم بالفعل.', 'email.unique' => 'هذا البريد الإلكتروني مستخدم بالفعل.',
]); ]);
@ -67,7 +70,6 @@ class AdminOperatorController extends Controller
'name' => $data['name'], 'name' => $data['name'],
'email' => $data['email'], 'email' => $data['email'],
'phone' => $data['phone'] ?? null, 'phone' => $data['phone'] ?? null,
'parking_lot_id' => $data['parking_lot_id'] ?? null,
]; ];
if (!empty($data['password'])) { if (!empty($data['password'])) {
@ -75,6 +77,7 @@ class AdminOperatorController extends Controller
} }
$operator->update($updates); $operator->update($updates);
$operator->assignedLots()->sync($data['lot_ids'] ?? []);
return response()->json(['success' => true, 'message' => 'تم تحديث بيانات المشغّل.']); return response()->json(['success' => true, 'message' => 'تم تحديث بيانات المشغّل.']);
} }

View File

@ -18,9 +18,17 @@ class OperatorController extends Controller
public function dashboard(Request $request): \Illuminate\View\View public function dashboard(Request $request): \Illuminate\View\View
{ {
$user = Auth::user(); $user = Auth::user();
$assignedLotId = $user->parking_lot_id; // null = no restriction (e.g. admin) $assignedLotIds = $user->assignedLots->pluck('id')->toArray();
$rawLots = ParkingLot::active()->withStatus()->get(); // Operators only see their assigned lots; no assignment = no lots visible
$query = ParkingLot::active()->withStatus();
if (!empty($assignedLotIds)) {
$query->whereIn('id', $assignedLotIds);
} else {
$query->whereRaw('1 = 0'); // no lots assigned → show nothing
}
$rawLots = $query->get();
$parkingLots = $rawLots->map(fn($lot) => [ $parkingLots = $rawLots->map(fn($lot) => [
'id' => $lot->id, 'id' => $lot->id,
@ -34,20 +42,22 @@ class OperatorController extends Controller
'lat' => (float) $lot->latitude, 'lat' => (float) $lot->latitude,
'lng' => (float) $lot->longitude, 'lng' => (float) $lot->longitude,
'image' => $lot->image ? Storage::url($lot->image) : null, 'image' => $lot->image ? Storage::url($lot->image) : null,
'locked' => $assignedLotId !== null && $lot->id !== $assignedLotId, 'locked' => false,
])->values(); ])->values();
// If operator has an assigned lot, force that lot
$selectedLotId = $request->get('lot_id'); $selectedLotId = $request->get('lot_id');
if ($assignedLotId !== null) {
// Reject any attempt to view a different lot // Reject attempts to access a lot not in the operator's assigned list
if ($selectedLotId && (int) $selectedLotId !== $assignedLotId) { if ($selectedLotId && !in_array((int) $selectedLotId, $assignedLotIds)) {
return redirect()->route('operator.dashboard', ['lot_id' => $assignedLotId]); if (!empty($assignedLotIds)) {
return redirect()->route('operator.dashboard', ['lot_id' => $assignedLotIds[0]]);
} }
// Auto-select assigned lot if nothing selected return redirect()->route('operator.dashboard');
if (!$selectedLotId) {
$selectedLotId = $assignedLotId;
} }
// Auto-select when only one lot is assigned
if (!$selectedLotId && count($assignedLotIds) === 1) {
$selectedLotId = $assignedLotIds[0];
} }
$selectedLot = null; $selectedLot = null;
@ -73,10 +83,177 @@ class OperatorController extends Controller
} }
return view('operator.dashboard', compact( return view('operator.dashboard', compact(
'parkingLots', 'selectedLot', 'activeCars', 'reservations', 'selectedLotId', 'assignedLotId' 'parkingLots', 'selectedLot', 'activeCars', 'reservations', 'selectedLotId', 'assignedLotIds'
)); ));
} }
// ── Operator stats page ─────────────────────────────────────────────────────
public function statsPage(): \Illuminate\View\View
{
$user = Auth::user();
$assignedLotIds = $user->assignedLots->pluck('id')->toArray();
$lots = ParkingLot::whereIn('id', $assignedLotIds)->orderBy('name')->get();
return view('operator.stats', compact('lots', 'assignedLotIds'));
}
public function statsJson(): JsonResponse
{
$user = Auth::user();
$assignedLotIds = $user->assignedLots->pluck('id')->toArray();
// Active vehicles right now
$activeCars = Booking::whereIn('parking_lot_id', $assignedLotIds)
->where('source', 'walk_in')
->where('status', 'active')
->count();
// Walk-in check-ins started today
$checkInsToday = Booking::whereIn('parking_lot_id', $assignedLotIds)
->where('source', 'walk_in')
->whereDate('start_time', today())
->count();
// Revenue collected today (completed + paid today)
$revenueToday = (float) Booking::whereIn('parking_lot_id', $assignedLotIds)
->where('status', 'completed')
->whereDate('paid_at', today())
->sum('total_fee');
// Total capacity & occupied across assigned lots
$lots = ParkingLot::whereIn('id', $assignedLotIds)->get();
$totalCapacity = $lots->sum('total_capacity');
$totalOccupied = Booking::whereIn('parking_lot_id', $assignedLotIds)
->where('status', 'active')
->count();
$availableSpaces = max(0, $totalCapacity - $totalOccupied);
// Pending reservations (not yet activated)
$pendingReservations = Booking::whereIn('parking_lot_id', $assignedLotIds)
->where('source', 'reservation')
->where('status', 'active')
->count();
// 7-day daily check-ins and revenue
$dailyCheckIns = [];
$dailyRevenue = [];
$dailyDates = [];
for ($i = 6; $i >= 0; $i--) {
$date = now()->subDays($i)->toDateString();
$dailyDates[] = $date;
$dailyCheckIns[] = Booking::whereIn('parking_lot_id', $assignedLotIds)
->where('source', 'walk_in')
->whereDate('start_time', $date)
->count();
$dailyRevenue[] = (float) Booking::whereIn('parking_lot_id', $assignedLotIds)
->where('status', 'completed')
->whereDate('paid_at', $date)
->sum('total_fee');
}
// Per-lot breakdown
$lotStats = $lots->map(function ($lot) {
$active = Booking::where('parking_lot_id', $lot->id)->where('status', 'active')->count();
$todayIns = Booking::where('parking_lot_id', $lot->id)
->where('source', 'walk_in')
->whereDate('start_time', today())
->count();
$todayRev = (float) Booking::where('parking_lot_id', $lot->id)
->where('status', 'completed')
->whereDate('paid_at', today())
->sum('total_fee');
return [
'id' => $lot->id,
'name' => $lot->name,
'address' => $lot->address,
'total' => $lot->total_capacity,
'active' => $active,
'available' => max(0, $lot->total_capacity - $active),
'pct' => $lot->total_capacity > 0 ? round($active / $lot->total_capacity * 100) : 0,
'today_ins' => $todayIns,
'today_rev' => $todayRev,
];
})->values();
// Last 8 completed bookings
$recentCompletions = Booking::whereIn('parking_lot_id', $assignedLotIds)
->where('status', 'completed')
->with('parkingLot:id,name')
->latest('paid_at')
->limit(8)
->get(['id', 'parking_lot_id', 'vehicle_plate', 'customer_name', 'start_time', 'paid_at', 'total_fee', 'payment_method']);
// ── Work progress ──────────────────────────────────────────────────────
// Checkouts completed today
$checkoutsToday = Booking::whereIn('parking_lot_id', $assignedLotIds)
->where('status', 'completed')
->whereDate('paid_at', today())
->count();
// Cancellations today
$cancelledToday = Booking::whereIn('parking_lot_id', $assignedLotIds)
->where('status', 'cancelled')
->whereDate('updated_at', today())
->count();
// Yesterday check-ins (comparison)
$checkInsYesterday = Booking::whereIn('parking_lot_id', $assignedLotIds)
->where('source', 'walk_in')
->whereDate('start_time', today()->subDay())
->count();
// Average stay duration in minutes (completed bookings today, start→paid_at)
$completedToday = Booking::whereIn('parking_lot_id', $assignedLotIds)
->where('status', 'completed')
->whereDate('paid_at', today())
->get(['start_time', 'paid_at']);
$avgDurationMin = $completedToday->isNotEmpty()
? round($completedToday->avg(fn($b) => $b->start_time->diffInMinutes($b->paid_at)))
: 0;
// Completion rate today: completed / (completed + cancelled)
$totalClosed = $checkoutsToday + $cancelledToday;
$completionRate = $totalClosed > 0 ? round($checkoutsToday / $totalClosed * 100) : 100;
// Hourly activity today (check-ins per hour 023)
$hourlyActivity = array_fill(0, 24, 0);
Booking::whereIn('parking_lot_id', $assignedLotIds)
->where('source', 'walk_in')
->whereDate('start_time', today())
->get(['start_time'])
->each(function ($b) use (&$hourlyActivity) {
$hourlyActivity[(int) $b->start_time->format('H')]++;
});
$peakHour = (int) array_search(max($hourlyActivity), $hourlyActivity);
return response()->json([
'success' => true,
'data' => [
'active_cars' => $activeCars,
'checkins_today' => $checkInsToday,
'revenue_today' => $revenueToday,
'available_spaces' => $availableSpaces,
'pending_reservations' => $pendingReservations,
'daily_checkins' => $dailyCheckIns,
'daily_revenue' => $dailyRevenue,
'daily_dates' => $dailyDates,
'lot_stats' => $lotStats,
'recent_completions' => $recentCompletions,
// work progress
'checkouts_today' => $checkoutsToday,
'cancelled_today' => $cancelledToday,
'checkins_yesterday' => $checkInsYesterday,
'avg_duration_min' => $avgDurationMin,
'completion_rate' => $completionRate,
'hourly_activity' => array_values($hourlyActivity),
'peak_hour' => $peakHour,
],
]);
}
// ── Walk-in check-in ──────────────────────────────────────────────────────── // ── Walk-in check-in ────────────────────────────────────────────────────────
public function checkIn(Request $request): JsonResponse public function checkIn(Request $request): JsonResponse

View File

@ -24,7 +24,6 @@ class User extends Authenticatable
'phone', 'phone',
'password', 'password',
'role', 'role',
'parking_lot_id',
]; ];
protected $casts = [ protected $casts = [
@ -56,8 +55,8 @@ class User extends Authenticatable
]; ];
} }
public function assignedLot(): \Illuminate\Database\Eloquent\Relations\BelongsTo public function assignedLots(): \Illuminate\Database\Eloquent\Relations\BelongsToMany
{ {
return $this->belongsTo(ParkingLot::class, 'parking_lot_id'); return $this->belongsToMany(ParkingLot::class, 'operator_parking_lot');
} }
} }

0
bootstrap/cache/.gitignore vendored Normal file → Executable file
View File

View File

@ -0,0 +1,40 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('operator_parking_lot', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->foreignId('parking_lot_id')->constrained()->cascadeOnDelete();
$table->unique(['user_id', 'parking_lot_id']);
$table->timestamps();
});
// Migrate existing single-lot assignments from users.parking_lot_id
$operators = DB::table('users')
->where('role', 'operator')
->whereNotNull('parking_lot_id')
->get(['id', 'parking_lot_id']);
foreach ($operators as $op) {
DB::table('operator_parking_lot')->insertOrIgnore([
'user_id' => $op->id,
'parking_lot_id' => $op->parking_lot_id,
'created_at' => now(),
'updated_at' => now(),
]);
}
}
public function down(): void
{
Schema::dropIfExists('operator_parking_lot');
}
};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,6 +1,6 @@
{ {
"resources/css/app.scss": { "resources/css/app.scss": {
"file": "assets/app-BtJpbOwL.css", "file": "assets/app-Layout02.css",
"src": "resources/css/app.scss", "src": "resources/css/app.scss",
"isEntry": true, "isEntry": true,
"name": "app", "name": "app",
@ -9,7 +9,7 @@
] ]
}, },
"resources/js/app.js": { "resources/js/app.js": {
"file": "assets/app-D0Ms7V4S.js", "file": "assets/app-D0Ms7V4S2.js",
"name": "app", "name": "app",
"src": "resources/js/app.js", "src": "resources/js/app.js",
"isEntry": true "isEntry": true

View File

@ -74,11 +74,17 @@ body {
background-color: var(--app-bg); background-color: var(--app-bg);
} }
// App Layout (Sidebar + Main) // App Layout (Topbar top, then sidebar + content row below)
.app-layout { .app-layout {
display: flex; display: flex;
flex-direction: column;
min-height: 100vh; min-height: 100vh;
// In RTL flex-direction:row, the first child (sidebar) appears on the RIGHT }
.app-inner {
display: flex;
flex: 1;
min-height: 0;
} }
// Sidebar // Sidebar
@ -88,9 +94,9 @@ body {
background: var(--sidebar-bg); background: var(--sidebar-bg);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: 100vh; height: calc(100vh - var(--topbar-height));
position: sticky; position: sticky;
top: 0; top: var(--topbar-height);
overflow-y: auto; overflow-y: auto;
scrollbar-width: none; scrollbar-width: none;
transition: transform .3s ease; transition: transform .3s ease;
@ -222,11 +228,12 @@ body {
flex-direction: column; flex-direction: column;
} }
// Topbar // Topbar (unified used on all pages)
.app-topbar { .app-topbar {
height: var(--topbar-height); height: var(--topbar-height);
background: var(--topbar-bg); background: var(--sidebar-bg);
border-bottom: 1px solid var(--border-color); border-bottom: 1px solid rgba(255,255,255,.06);
box-shadow: 0 2px 12px rgba(0,0,0,.15);
padding: 0 1.5rem; padding: 0 1.5rem;
display: flex; display: flex;
align-items: center; align-items: center;
@ -234,12 +241,12 @@ body {
gap: 1rem; gap: 1rem;
position: sticky; position: sticky;
top: 0; top: 0;
z-index: 100; z-index: 200;
.topbar-title { .topbar-title {
font-size: 1rem; font-size: 1rem;
font-weight: 700; font-weight: 700;
color: var(--text-primary); color: #f8fafc;
margin: 0; margin: 0;
} }
@ -255,13 +262,13 @@ body {
background: none; background: none;
border: none; border: none;
padding: .375rem .5rem; padding: .375rem .5rem;
color: var(--text-muted); color: rgba(255,255,255,.65);
border-radius: .375rem; border-radius: .375rem;
cursor: pointer; cursor: pointer;
display: none; display: none;
font-size: 1.25rem; font-size: 1.25rem;
&:hover { background: #f1f5f9; color: var(--text-primary); } &:hover { background: rgba(255,255,255,.08); color: #fff; }
} }
.sidebar-overlay { .sidebar-overlay {
@ -283,13 +290,10 @@ body {
@media (max-width: 991.98px) { @media (max-width: 991.98px) {
.app-sidebar { .app-sidebar {
position: fixed; position: fixed;
top: 0; top: var(--topbar-height);
inset-inline-end: 0; // right in RTL inset-inline-start: 0; // anchors right edge to viewport right in RTL
height: 100%; height: calc(100% - var(--topbar-height));
transform: translateX(calc(-1 * var(--sidebar-width))); transform: translateX(100%); // hides off-screen to the right in RTL
// In RTL, translateX negative moves LEFT (off screen)
// We need to hide it to the right side
transform: translateX(100%);
&.is-open { &.is-open {
transform: translateX(0); transform: translateX(0);
@ -553,14 +557,6 @@ body {
} }
// Public (Index) Page // Public (Index) Page
.public-header {
background: var(--sidebar-bg);
padding: 1.25rem 0;
position: sticky;
top: 0;
z-index: 200;
box-shadow: 0 2px 12px rgba(0,0,0,.15);
}
.parking-card { .parking-card {
transition: all .18s ease; transition: all .18s ease;
@ -605,11 +601,6 @@ body {
} }
@media (min-width: 768px) { @media (min-width: 768px) {
.app-topbar {
backdrop-filter: blur(8px);
background: rgba(255,255,255,.96);
}
.stat-card:hover { .stat-card:hover {
transform: translateY(-3px); transform: translateY(-3px);
box-shadow: 0 8px 28px rgba(0,0,0,.09) !important; box-shadow: 0 8px 28px rgba(0,0,0,.09) !important;
@ -690,6 +681,8 @@ body {
/* ── Admin / Operator layout ────────────────────────────────────────── */ /* ── Admin / Operator layout ────────────────────────────────────────── */
.sidebar-toggle { display: none !important; } .sidebar-toggle { display: none !important; }
.app-layout { display: block; } .app-layout { display: block; }
.app-inner { display: block; }
.app-sidebar { display: none !important; }
.app-body { .app-body {
padding-bottom: var(--mobile-nav-h); padding-bottom: var(--mobile-nav-h);
@ -699,8 +692,6 @@ body {
.app-topbar { .app-topbar {
height: 54px; height: 54px;
padding: 0 1rem; padding: 0 1rem;
backdrop-filter: none;
background: #ffffff;
.topbar-title { font-size: .9rem; } .topbar-title { font-size: .9rem; }
} }
@ -714,7 +705,7 @@ body {
} }
/* ── Public page ────────────────────────────────────────────────────── */ /* ── Public page ────────────────────────────────────────────────────── */
.public-header { padding: .625rem 0; } .app-topbar { padding: .625rem 1rem; }
.mob-hero-compact { padding: .875rem 0 1rem !important; } .mob-hero-compact { padding: .875rem 0 1rem !important; }
/* Section switching — hide inactive section */ /* Section switching — hide inactive section */

View File

@ -396,10 +396,12 @@
{{-- ── STEP 2A : receipt + payment ────────────────────────── --}} {{-- ── STEP 2A : receipt + payment ────────────────────────── --}}
<div id="endStep2Pay" style="display:none;" class="p-4"> <div id="endStep2Pay" style="display:none;" class="p-4">
<button class="btn btn-sm mb-3 fw-600" onclick="endBack()" <div class="d-flex justify-content-end mb-3">
<button class="btn btn-sm fw-600" onclick="endBack()"
style="background:#f1f5f9;color:#475569;border:none;border-radius:.5rem;font-family:'Cairo',sans-serif;"> style="background:#f1f5f9;color:#475569;border:none;border-radius:.5rem;font-family:'Cairo',sans-serif;">
<i class="bi bi-arrow-right me-1"></i>رجوع <i class="bi bi-arrow-left me-1"></i>رجوع
</button> </button>
</div>
{{-- Times --}} {{-- Times --}}
<div class="row g-2 mb-3"> <div class="row g-2 mb-3">
@ -469,10 +471,12 @@
{{-- ── STEP 2B : force close ───────────────────────────────── --}} {{-- ── STEP 2B : force close ───────────────────────────────── --}}
<div id="endStep2Force" style="display:none;" class="p-4"> <div id="endStep2Force" style="display:none;" class="p-4">
<button class="btn btn-sm mb-3 fw-600" onclick="endBack()" <div class="d-flex justify-content-end mb-3">
<button class="btn btn-sm fw-600" onclick="endBack()"
style="background:#f1f5f9;color:#475569;border:none;border-radius:.5rem;font-family:'Cairo',sans-serif;"> style="background:#f1f5f9;color:#475569;border:none;border-radius:.5rem;font-family:'Cairo',sans-serif;">
<i class="bi bi-arrow-right me-1"></i>رجوع <i class="bi bi-arrow-left me-1"></i>رجوع
</button> </button>
</div>
<div class="text-center py-2 mb-3"> <div class="text-center py-2 mb-3">
<div style="font-size:2.8rem;margin-bottom:.75rem;">⚠️</div> <div style="font-size:2.8rem;margin-bottom:.75rem;">⚠️</div>
@ -609,12 +613,12 @@ async function endChoose(type) {
const d = data.data; const d = data.data;
document.getElementById('payRcptEntry').textContent = d.entry_time; document.getElementById('payRcptEntry').textContent = d.entry_time;
document.getElementById('payRcptExit').textContent = d.exit_time; document.getElementById('payRcptExit').textContent = d.exit_time;
document.getElementById('payRcptTotal').textContent = Number(d.total_fee).toLocaleString('ar-SA') + ' ل'; document.getElementById('payRcptTotal').textContent = Number(d.total_fee).toLocaleString('ar-SA') + ' ليرة سورية';
const rows = d.fee_details.map(r => ` const rows = d.fee_details.map(r => `
<div style="display:flex;justify-content:space-between;padding:.3rem 0;border-bottom:1px dashed #f1f5f9;font-size:.82rem;"> <div style="display:flex;justify-content:space-between;padding:.3rem 0;border-bottom:1px dashed #f1f5f9;font-size:.82rem;">
<span>${r.day} <small style="color:#94a3b8;">${r.date}</small></span> <span>${r.day} <small style="color:#94a3b8;">${r.date}</small></span>
<span style="color:#64748b;">${r.hours}س × ${Number(r.rate).toLocaleString('ar-SA')}</span> <span style="color:#64748b;">${r.hours}س × ${Number(r.rate).toLocaleString('ar-SA')}</span>
<span class="fw-600" style="color:#0f172a;">${Number(r.subtotal).toLocaleString('ar-SA')} ل.س</span> <span class="fw-600" style="color:#0f172a;">${Number(r.subtotal).toLocaleString('ar-SA')} ليرة سورية</span>
</div>`).join(''); </div>`).join('');
document.getElementById('payRcptBreakdown').innerHTML = document.getElementById('payRcptBreakdown').innerHTML =
rows || '<p class="text-xs text-center" style="color:#94a3b8;">لا تفاصيل</p>'; rows || '<p class="text-xs text-center" style="color:#94a3b8;">لا تفاصيل</p>';

View File

@ -274,7 +274,7 @@ async function loadStats() {
document.getElementById('total-bookings').textContent = data.total_bookings ?? 0; document.getElementById('total-bookings').textContent = data.total_bookings ?? 0;
document.getElementById('active-bookings').textContent = data.active_bookings ?? 0; document.getElementById('active-bookings').textContent = data.active_bookings ?? 0;
document.getElementById('occupancy-rate').textContent = (data.occupancy_rate ?? 0) + '%'; document.getElementById('occupancy-rate').textContent = (data.occupancy_rate ?? 0) + '%';
document.getElementById('estimated-revenue').textContent = (data.estimated_revenue ?? 0).toLocaleString('ar-SA') + ' ر.س'; document.getElementById('estimated-revenue').textContent = (data.estimated_revenue ?? 0).toLocaleString('ar-SA') + ' ليرة سورية';
document.getElementById('available-spots').textContent = data.available_spots ?? 0; document.getElementById('available-spots').textContent = data.available_spots ?? 0;
} catch {} } catch {}
} }

View File

@ -22,6 +22,15 @@
padding:.25em .75em; border-radius:20px; font-size:.75rem; font-weight:600; padding:.25em .75em; border-radius:20px; font-size:.75rem; font-weight:600;
background:#f1f5f9; color:#94a3b8; background:#f1f5f9; color:#94a3b8;
} }
.lot-checkbox-list {
border:1px solid #e2e8f0; border-radius:.625rem; max-height:180px; overflow-y:auto; padding:.5rem;
}
.lot-checkbox-item {
display:flex; align-items:center; gap:.6rem; padding:.375rem .5rem; border-radius:.4rem; cursor:pointer;
font-size:.875rem; color:#374151; transition:background .15s;
}
.lot-checkbox-item:hover { background:#f8fafc; }
.lot-checkbox-item input[type=checkbox] { width:1rem; height:1rem; cursor:pointer; flex-shrink:0; }
</style> </style>
@endsection @endsection
@ -61,11 +70,15 @@
<td class="py-3" style="color:#475569;">{{ $op->email }}</td> <td class="py-3" style="color:#475569;">{{ $op->email }}</td>
<td class="py-3" style="color:#475569;">{{ $op->phone ?? '—' }}</td> <td class="py-3" style="color:#475569;">{{ $op->phone ?? '—' }}</td>
<td class="py-3"> <td class="py-3">
@if($op->assignedLot) @if($op->assignedLots->isNotEmpty())
<div class="d-flex flex-wrap gap-1">
@foreach($op->assignedLots as $assignedLot)
<span class="lot-pill"> <span class="lot-pill">
<i class="bi bi-buildings"></i> <i class="bi bi-buildings"></i>
{{ $op->assignedLot->name }} {{ $assignedLot->name }}
</span> </span>
@endforeach
</div>
@else @else
<span class="no-lot-pill"> <span class="no-lot-pill">
<i class="bi bi-dash-circle"></i> <i class="bi bi-dash-circle"></i>
@ -81,7 +94,7 @@
data-name="{{ $op->name }}" data-name="{{ $op->name }}"
data-email="{{ $op->email }}" data-email="{{ $op->email }}"
data-phone="{{ $op->phone ?? '' }}" data-phone="{{ $op->phone ?? '' }}"
data-lot="{{ $op->parking_lot_id ?? '' }}"> data-lots="{{ json_encode($op->assignedLots->pluck('id')) }}">
<i class="bi bi-pencil me-1"></i>تعديل <i class="bi bi-pencil me-1"></i>تعديل
</button> </button>
<button class="btn btn-sm fw-600 btn-delete-op" <button class="btn btn-sm fw-600 btn-delete-op"
@ -134,13 +147,17 @@
<input type="password" id="createPassword" class="form-control" placeholder="8 أحرف على الأقل" dir="ltr"> <input type="password" id="createPassword" class="form-control" placeholder="8 أحرف على الأقل" dir="ltr">
</div> </div>
<div class="mb-1"> <div class="mb-1">
<label class="form-label fw-600" style="font-size:.875rem;color:#374151;">الموقف المخصص <span style="color:#94a3b8;font-weight:400;">(اختياري)</span></label> <label class="form-label fw-600" style="font-size:.875rem;color:#374151;">المواقف المخصصة <span style="color:#94a3b8;font-weight:400;">(اختياري)</span></label>
<select id="createLot" class="form-select"> <div class="lot-checkbox-list" id="createLotList">
<option value=""> بدون تخصيص </option> @forelse($lots as $lot)
@foreach($lots as $lot) <label class="lot-checkbox-item">
<option value="{{ $lot->id }}">{{ $lot->name }}</option> <input type="checkbox" class="create-lot-cb" value="{{ $lot->id }}">
@endforeach <span>{{ $lot->name }}</span>
</select> </label>
@empty
<p class="text-center mb-0 py-2" style="color:#94a3b8;font-size:.8rem;">لا توجد مواقف</p>
@endforelse
</div>
</div> </div>
</div> </div>
<div class="modal-footer border-0 pt-1"> <div class="modal-footer border-0 pt-1">
@ -186,13 +203,17 @@
<input type="password" id="editPassword" class="form-control" placeholder="8 أحرف على الأقل" dir="ltr"> <input type="password" id="editPassword" class="form-control" placeholder="8 أحرف على الأقل" dir="ltr">
</div> </div>
<div class="mb-1"> <div class="mb-1">
<label class="form-label fw-600" style="font-size:.875rem;color:#374151;">الموقف المخصص</label> <label class="form-label fw-600" style="font-size:.875rem;color:#374151;">المواقف المخصصة</label>
<select id="editLot" class="form-select"> <div class="lot-checkbox-list" id="editLotList">
<option value=""> بدون تخصيص </option> @forelse($lots as $lot)
@foreach($lots as $lot) <label class="lot-checkbox-item">
<option value="{{ $lot->id }}">{{ $lot->name }}</option> <input type="checkbox" class="edit-lot-cb" value="{{ $lot->id }}">
@endforeach <span>{{ $lot->name }}</span>
</select> </label>
@empty
<p class="text-center mb-0 py-2" style="color:#94a3b8;font-size:.8rem;">لا توجد مواقف</p>
@endforelse
</div>
</div> </div>
</div> </div>
<div class="modal-footer border-0 pt-1"> <div class="modal-footer border-0 pt-1">
@ -271,7 +292,7 @@ document.addEventListener('DOMContentLoaded', function () {
// ── Open create ─────────────────────────────────────────────────────────── // ── Open create ───────────────────────────────────────────────────────────
document.getElementById('btnOpenCreate').addEventListener('click', function () { document.getElementById('btnOpenCreate').addEventListener('click', function () {
['createName','createEmail','createPhone','createPassword'].forEach(id => document.getElementById(id).value = ''); ['createName','createEmail','createPhone','createPassword'].forEach(id => document.getElementById(id).value = '');
document.getElementById('createLot').value = ''; document.querySelectorAll('.create-lot-cb').forEach(cb => cb.checked = false);
document.getElementById('createError').classList.add('d-none'); document.getElementById('createError').classList.add('d-none');
modal('createModal').show(); modal('createModal').show();
}); });
@ -284,7 +305,10 @@ document.addEventListener('DOMContentLoaded', function () {
document.getElementById('editEmail').value = this.dataset.email; document.getElementById('editEmail').value = this.dataset.email;
document.getElementById('editPhone').value = this.dataset.phone || ''; document.getElementById('editPhone').value = this.dataset.phone || '';
document.getElementById('editPassword').value = ''; document.getElementById('editPassword').value = '';
document.getElementById('editLot').value = this.dataset.lot || ''; const assignedIds = JSON.parse(this.dataset.lots || '[]');
document.querySelectorAll('.edit-lot-cb').forEach(cb => {
cb.checked = assignedIds.includes(parseInt(cb.value));
});
document.getElementById('editError').classList.add('d-none'); document.getElementById('editError').classList.add('d-none');
modal('editModal').show(); modal('editModal').show();
}); });
@ -309,6 +333,7 @@ document.addEventListener('DOMContentLoaded', function () {
spinner.classList.remove('d-none'); spinner.classList.remove('d-none');
errEl.classList.add('d-none'); errEl.classList.add('d-none');
try { try {
const createLotIds = [...document.querySelectorAll('.create-lot-cb:checked')].map(cb => parseInt(cb.value));
const res = await fetch('{{ route("admin.operators.store") }}', { const res = await fetch('{{ route("admin.operators.store") }}', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': csrf }, headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': csrf },
@ -317,7 +342,7 @@ document.addEventListener('DOMContentLoaded', function () {
email: document.getElementById('createEmail').value.trim(), email: document.getElementById('createEmail').value.trim(),
phone: document.getElementById('createPhone').value.trim() || null, phone: document.getElementById('createPhone').value.trim() || null,
password: document.getElementById('createPassword').value, password: document.getElementById('createPassword').value,
parking_lot_id: document.getElementById('createLot').value || null, lot_ids: createLotIds,
}), }),
}); });
const json = await res.json(); const json = await res.json();
@ -343,6 +368,7 @@ document.addEventListener('DOMContentLoaded', function () {
spinner.classList.remove('d-none'); spinner.classList.remove('d-none');
errEl.classList.add('d-none'); errEl.classList.add('d-none');
try { try {
const editLotIds = [...document.querySelectorAll('.edit-lot-cb:checked')].map(cb => parseInt(cb.value));
const res = await fetch(`/admin/operators/${id}`, { const res = await fetch(`/admin/operators/${id}`, {
method: 'PUT', method: 'PUT',
headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': csrf }, headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': csrf },
@ -351,7 +377,7 @@ document.addEventListener('DOMContentLoaded', function () {
email: document.getElementById('editEmail').value.trim(), email: document.getElementById('editEmail').value.trim(),
phone: document.getElementById('editPhone').value.trim() || null, phone: document.getElementById('editPhone').value.trim() || null,
password: document.getElementById('editPassword').value || null, password: document.getElementById('editPassword').value || null,
parking_lot_id: document.getElementById('editLot').value || null, lot_ids: editLotIds,
}), }),
}); });
const json = await res.json(); const json = await res.json();

View File

@ -209,7 +209,7 @@ $gradients = [
</span> </span>
<span class="lot-card-stat" style="{{ !empty($lot->pricing_rules) ? 'border-color:#fbbf24;color:#92400e;' : '' }}"> <span class="lot-card-stat" style="{{ !empty($lot->pricing_rules) ? 'border-color:#fbbf24;color:#92400e;' : '' }}">
<i class="bi bi-{{ !empty($lot->pricing_rules) ? 'tags' : 'tag' }}" style="color:{{ !empty($lot->pricing_rules) ? '#f59e0b' : '#10b981' }};"></i> <i class="bi bi-{{ !empty($lot->pricing_rules) ? 'tags' : 'tag' }}" style="color:{{ !empty($lot->pricing_rules) ? '#f59e0b' : '#10b981' }};"></i>
<strong>{{ number_format($lot->price_per_hour, 0) }}</strong> ر.س/س <strong>{{ number_format($lot->price_per_hour, 0) }}</strong> ليرة سورية/س
@if(!empty($lot->pricing_rules)) @if(!empty($lot->pricing_rules))
<span style="font-size:.6rem;color:#f59e0b;"> (مخصص)</span> <span style="font-size:.6rem;color:#f59e0b;"> (مخصص)</span>
@endif @endif
@ -385,7 +385,7 @@ $gradients = [
<input type="number" id="p_base" class="form-control" <input type="number" id="p_base" class="form-control"
step="0.01" min="0" placeholder="0.00" step="0.01" min="0" placeholder="0.00"
oninput="syncBaseHints()"> oninput="syncBaseHints()">
<span class="input-group-text" style="font-family:'Cairo',sans-serif;background:#f8fafc;color:#64748b;border-color:#e2e8f0;">ر.س / ساعة</span> <span class="input-group-text" style="font-family:'Cairo',sans-serif;background:#f8fafc;color:#64748b;border-color:#e2e8f0;">ليرة سورية / ساعة</span>
</div> </div>
<p class="text-xs mt-1 mb-0" style="color:#94a3b8;"> <p class="text-xs mt-1 mb-0" style="color:#94a3b8;">
الأيام التي لا تحمل سعراً مخصصاً ستستخدم هذا السعر تلقائياً. الأيام التي لا تحمل سعراً مخصصاً ستستخدم هذا السعر تلقائياً.
@ -425,7 +425,7 @@ $gradients = [
step="0.01" min="0" step="0.01" min="0"
placeholder="مثل السعر الأساسي" placeholder="مثل السعر الأساسي"
style="font-family:'Cairo',sans-serif;"> style="font-family:'Cairo',sans-serif;">
<span class="input-group-text" style="background:#f8fafc;color:#94a3b8;border-color:#e2e8f0;font-size:.75rem;">ر.س</span> <span class="input-group-text" style="background:#f8fafc;color:#94a3b8;border-color:#e2e8f0;font-size:.75rem;">ليرة سورية</span>
</div> </div>
</div> </div>
<div style="width:70px;text-align:center;"> <div style="width:70px;text-align:center;">

View File

@ -72,16 +72,5 @@
إنشاء حساب جديد إنشاء حساب جديد
</a> </a>
<div class="mt-4 p-3 rounded-3" style="background:#f0f9ff;border:1px solid #bae6fd;">
<p class="text-xs fw-700 mb-2" style="color:#0369a1;">
<i class="bi bi-key-fill me-1"></i>حسابات الاختبار
</p>
<p class="text-xs mb-1" style="color:#0369a1;">
<strong>مدير:</strong> admin@damascusparking.com / admin123
</p>
<p class="text-xs mb-0" style="color:#0369a1;">
<strong>مشغّل:</strong> operator@damascusparking.com / operator123
</p>
</div>
@endsection @endsection

View File

@ -15,33 +15,69 @@
/* Bootstrap resets max-width:100% on all <img> which breaks Leaflet tiles */ /* Bootstrap resets max-width:100% on all <img> which breaks Leaflet tiles */
.leaflet-container img { max-width: none !important; box-shadow: none !important; } .leaflet-container img { max-width: none !important; box-shadow: none !important; }
.leaflet-container { direction: ltr; } .leaflet-container { direction: ltr; }
/* ── Desktop: map + list fill remaining viewport height ─────────────── */
@media (min-width: 992px) {
html, body { height: 100%; overflow: hidden; }
body { display: flex; flex-direction: column; }
.app-topbar { flex-shrink: 0; }
.mob-hero-compact { flex-shrink: 0; }
#mainContent {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
padding-bottom: 0.75rem !important;
}
#mainContent > .row {
flex: 1;
min-height: 0;
}
#mainContent .col-lg-8,
#mainContent .col-lg-4 {
display: flex;
flex-direction: column;
}
/* Map card */
#mainContent .col-lg-8 > .card {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
}
#mainContent .col-lg-8 .card-body {
flex: 1;
min-height: 0;
overflow: hidden;
}
#map { height: 100% !important; }
/* List card */
#mainContent .col-lg-4 > .card {
flex: 1;
min-height: 0;
position: static !important;
display: flex;
flex-direction: column;
}
#parkingList {
flex: 1;
min-height: 0;
max-height: none !important;
overflow-y: auto;
}
}
</style> </style>
</head> </head>
<body style="background:#f1f5f9;font-family:'Cairo',sans-serif;"> <body style="background:#f1f5f9;font-family:'Cairo',sans-serif;">
{{-- ══ HEADER ══════════════════════════════════════════════════════════════ --}} {{-- ══ HEADER ══════════════════════════════════════════════════════════════ --}}
<header class="public-header"> @include('partials.topbar')
<div class="container">
<div class="d-flex align-items-center justify-content-between">
<div class="d-flex align-items-center gap-3">
<div style="width:40px;height:40px;background:#6366f1;border-radius:.625rem;display:flex;align-items:center;justify-content:center;flex-shrink:0;">
<i class="bi bi-p-square-fill text-white" style="font-size:1.25rem;"></i>
</div>
<div>
<div class="fw-800" style="color:#f8fafc;font-size:1.05rem;line-height:1.2;">دمشق باركينغ</div>
<div style="color:#94a3b8;font-size:.72rem;">مواقف السيارات في دمشق</div>
</div>
</div>
<div class="d-flex align-items-center gap-2">
@include('partials.user-dropdown')
</div>
</div>
</div>
</header>
{{-- ══ HERO SEARCH ═════════════════════════════════════════════════════════ --}} {{-- ══ HERO SEARCH ═════════════════════════════════════════════════════════ --}}
<div class="mob-hero-compact" style="background:linear-gradient(135deg,#0f172a 0%,#1e3a5f 100%);padding:2.5rem 0 3rem;"> <div class="mob-hero-compact" style="background:linear-gradient(135deg,#0f172a 0%,#1e3a5f 100%);padding:2.5rem 0 3rem;">
<div class="container text-center"> <div class="container-fluid text-center">
<h1 class="fw-800 mb-2 d-none d-md-block" style="color:#f8fafc;font-size:clamp(1.4rem,4vw,2rem);"> <h1 class="fw-800 mb-2 d-none d-md-block" style="color:#f8fafc;font-size:clamp(1.4rem,4vw,2rem);">
ابحث عن موقف سيارات في دمشق ابحث عن موقف سيارات في دمشق
</h1> </h1>
@ -72,7 +108,7 @@
</div> </div>
{{-- ══ MAIN CONTENT ════════════════════════════════════════════════════════ --}} {{-- ══ MAIN CONTENT ════════════════════════════════════════════════════════ --}}
<div class="container py-4 mob-nav-pad"> <div id="mainContent" class="container-fluid py-4 mob-nav-pad">
<div class="row g-3"> <div class="row g-3">
{{-- Parking List (RTL: renders on right side, but we put it second so it's on LEFT visually) --}} {{-- Parking List (RTL: renders on right side, but we put it second so it's on LEFT visually) --}}
@ -81,7 +117,7 @@
{{-- MAP --}} {{-- MAP --}}
<div class="col-lg-8 order-lg-1 mob-section-map"> <div class="col-lg-8 order-lg-1 mob-section-map">
<div class="card h-100" style="min-height:520px;"> <div class="card h-100">
<div class="card-header d-flex align-items-center justify-content-between"> <div class="card-header d-flex align-items-center justify-content-between">
<span class="fw-700 text-sm" style="color:#0f172a;"> <span class="fw-700 text-sm" style="color:#0f172a;">
<i class="bi bi-map me-1" style="color:#6366f1;"></i> <i class="bi bi-map me-1" style="color:#6366f1;"></i>
@ -90,7 +126,7 @@
<span class="badge badge-soft-primary text-xs" id="map-count">--</span> <span class="badge badge-soft-primary text-xs" id="map-count">--</span>
</div> </div>
<div class="card-body p-0" style="border-radius:0 0 .75rem .75rem;overflow:hidden;"> <div class="card-body p-0" style="border-radius:0 0 .75rem .75rem;overflow:hidden;">
<div id="map" style="height:500px;"></div> <div id="map"></div>
</div> </div>
</div> </div>
</div> </div>
@ -105,7 +141,7 @@
</span> </span>
<span class="badge badge-soft-success text-xs" id="list-count">{{ $lots->count() }} موقف</span> <span class="badge badge-soft-success text-xs" id="list-count">{{ $lots->count() }} موقف</span>
</div> </div>
<div style="max-height:500px;overflow-y:auto;scrollbar-width:thin;" id="parkingList"> <div style="overflow-y:auto;scrollbar-width:thin;" id="parkingList">
@forelse($lots as $lot) @forelse($lots as $lot)
@php @php
$avail = $lot['avail']; $avail = $lot['avail'];
@ -130,7 +166,7 @@
<p class="text-xs mb-2" style="color:#94a3b8;">{{ $lot['address'] }}</p> <p class="text-xs mb-2" style="color:#94a3b8;">{{ $lot['address'] }}</p>
<div class="d-flex gap-3 text-xs" style="color:#64748b;"> <div class="d-flex gap-3 text-xs" style="color:#64748b;">
<span><i class="bi bi-car-front me-1"></i>{{ $total }}</span> <span><i class="bi bi-car-front me-1"></i>{{ $total }}</span>
<span><i class="bi bi-currency-exchange me-1"></i>{{ number_format($lot['price']) }} ر.س</span> <span><i class="bi bi-currency-exchange me-1"></i>{{ number_format($lot['price']) }} ليرة سورية</span>
<span><i class="bi bi-clock me-1"></i>{{ $lot['hours'] }}</span> <span><i class="bi bi-clock me-1"></i>{{ $lot['hours'] }}</span>
</div> </div>
</div> </div>
@ -370,7 +406,7 @@
if (l.avail < l.total * .2) return { cls: 'avail-limited', text: `${l.avail} محدود`, pct: Math.round((l.total - l.avail) / l.total * 100) }; if (l.avail < l.total * .2) return { cls: 'avail-limited', text: `${l.avail} محدود`, pct: Math.round((l.total - l.avail) / l.total * 100) };
return { cls: 'avail-open', text: `${l.avail} متاح`, pct: Math.round((l.total - l.avail) / l.total * 100) }; return { cls: 'avail-open', text: `${l.avail} متاح`, pct: Math.round((l.total - l.avail) / l.total * 100) };
} }
const fmtPrice = p => new Intl.NumberFormat('ar-SY').format(p) + ' ر.س'; const fmtPrice = p => new Intl.NumberFormat('ar-SY').format(p) + ' ليرة سورية';
// ── Map (lazy-init on mobile) ───────────────────────────────────────── // ── Map (lazy-init on mobile) ─────────────────────────────────────────
let map = null, mapReady = false; let map = null, mapReady = false;
@ -379,7 +415,7 @@
if (mapReady) { map?.invalidateSize(); return; } if (mapReady) { map?.invalidateSize(); return; }
mapReady = true; mapReady = true;
map = L.map('map').setView([33.5138, 36.2765], 12); map = L.map('map', { attributionControl: false }).setView([33.5138, 36.2765], 12);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap' attribution: '© OpenStreetMap'
}).addTo(map); }).addTo(map);

View File

@ -17,25 +17,23 @@
<div class="app-layout"> <div class="app-layout">
{{-- ════════════════════════════════════════ {{-- ════════════════════════════════════════
SIDEBAR (appears on the RIGHT in RTL TOPBAR full width, topmost
because it is the first flex child)
════════════════════════════════════════ --}} ════════════════════════════════════════ --}}
@include('partials.topbar', ['isAdminLayout' => true])
{{-- ════════════════════════════════════════
INNER ROW sidebar + content
════════════════════════════════════════ --}}
<div class="app-inner">
<aside class="app-sidebar" id="appSidebar"> <aside class="app-sidebar" id="appSidebar">
{{-- Logo --}} @php $isAdmin = auth()->user()?->role === 'admin'; @endphp
<a href="{{ route('admin.dashboard') }}" class="sidebar-logo">
<div class="logo-icon">
<i class="bi bi-p-square-fill"></i>
</div>
<div>
<div class="logo-text">دمشق باركينغ</div>
<div class="logo-sub">لوحة الإدارة</div>
</div>
</a>
{{-- Navigation --}} {{-- Navigation --}}
<nav class="sidebar-nav"> <nav class="sidebar-nav">
@if($isAdmin)
<div class="sidebar-section">الرئيسية</div> <div class="sidebar-section">الرئيسية</div>
<a href="{{ route('admin.dashboard') }}" <a href="{{ route('admin.dashboard') }}"
@ -65,38 +63,22 @@
<i class="bi bi-people sidebar-icon"></i> <i class="bi bi-people sidebar-icon"></i>
<span>المشغّلون</span> <span>المشغّلون</span>
</a> </a>
@endif
<a href="{{ route('operator.stats') }}"
class="sidebar-link {{ request()->routeIs('operator.stats') ? 'active' : '' }}">
<i class="bi bi-graph-up sidebar-icon"></i>
<span>الإحصائيات</span>
</a>
<a href="{{ route('operator.dashboard') }}" <a href="{{ route('operator.dashboard') }}"
class="sidebar-link {{ request()->routeIs('operator.*') ? 'active' : '' }}"> class="sidebar-link {{ request()->routeIs('operator.dashboard') ? 'active' : '' }}">
<i class="bi bi-person-badge sidebar-icon"></i> <i class="bi bi-person-badge sidebar-icon"></i>
<span>لوحة المشغّل</span> <span>{{ $isAdmin ? 'لوحة المشغّل' : 'الموقف' }}</span>
</a> </a>
</nav> </nav>
{{-- Footer: user info + logout --}}
<div class="sidebar-footer">
<div class="d-flex align-items-center gap-2 mb-2">
<div class="user-avatar">
{{ mb_substr(auth()->user()?->name ?? 'م', 0, 1) }}
</div>
<div style="min-width:0">
<div class="user-name text-truncate">{{ auth()->user()?->name ?? 'المستخدم' }}</div>
<div class="user-role">
{{ auth()->user()?->role === 'admin' ? 'مدير النظام' : 'مشغّل' }}
</div>
</div>
</div>
<form method="POST" action="{{ route('logout') }}">
@csrf
<button type="submit"
class="btn btn-sm w-100 mt-1 text-start"
style="background:rgba(239,68,68,.12);color:#f87171;border:none;border-radius:.5rem;padding:.45rem .75rem;font-family:'Cairo',sans-serif;font-size:.82rem;">
<i class="bi bi-box-arrow-left me-2"></i>تسجيل الخروج
</button>
</form>
</div>
</aside> </aside>
{{-- ════════════════════════════════════════ {{-- ════════════════════════════════════════
@ -104,39 +86,6 @@
════════════════════════════════════════ --}} ════════════════════════════════════════ --}}
<div class="app-body"> <div class="app-body">
{{-- Topbar --}}
<header class="app-topbar">
<div class="d-flex align-items-center gap-3">
<button class="sidebar-toggle" id="sidebarToggle" aria-label="قائمة التنقل">
<i class="bi bi-list"></i>
</button>
<h1 class="topbar-title">@yield('page-title', 'لوحة التحكم')</h1>
</div>
<div class="topbar-actions d-flex align-items-center gap-2">
{{-- Desktop: link to public site --}}
<a href="{{ route('parking.index') }}"
class="btn btn-sm d-none d-md-inline-flex align-items-center"
style="background:#f1f5f9;color:#475569;border:none;border-radius:.5rem;font-family:'Cairo',sans-serif;"
>
<i class="bi bi-globe2 me-1"></i>الموقع العام
</a>
{{-- Mobile: user avatar + logout --}}
<div class="d-flex d-md-none align-items-center gap-2">
<div style="width:30px;height:30px;background:rgba(99,102,241,.12);border-radius:50%;display:flex;align-items:center;justify-content:center;color:#6366f1;font-weight:800;font-size:.82rem;flex-shrink:0;">
{{ mb_substr(auth()->user()?->name ?? 'م', 0, 1) }}
</div>
<form method="POST" action="{{ route('logout') }}" style="margin:0;">
@csrf
<button type="submit"
style="background:none;border:none;color:#94a3b8;padding:4px 6px;font-size:1.15rem;cursor:pointer;line-height:1;"
title="تسجيل الخروج">
<i class="bi bi-box-arrow-left"></i>
</button>
</form>
</div>
</div>
</header>
{{-- Flash Messages --}} {{-- Flash Messages --}}
<div class="px-4 pt-3"> <div class="px-4 pt-3">
@if(session('success')) @if(session('success'))
@ -164,6 +113,8 @@
</div>{{-- /app-body --}} </div>{{-- /app-body --}}
</div>{{-- /app-inner --}}
</div>{{-- /app-layout --}} </div>{{-- /app-layout --}}
{{-- Mobile sidebar overlay --}} {{-- Mobile sidebar overlay --}}
@ -171,6 +122,7 @@
{{-- ══ MOBILE BOTTOM NAVIGATION ═══════════════════════════════════════════════ --}} {{-- ══ MOBILE BOTTOM NAVIGATION ═══════════════════════════════════════════════ --}}
<nav class="mobile-bottom-nav" aria-label="التنقل"> <nav class="mobile-bottom-nav" aria-label="التنقل">
@if($isAdmin)
<a href="{{ route('admin.dashboard') }}" <a href="{{ route('admin.dashboard') }}"
class="mob-nav-item {{ request()->routeIs('admin.dashboard') ? 'active' : '' }}"> class="mob-nav-item {{ request()->routeIs('admin.dashboard') ? 'active' : '' }}">
<i class="bi bi-speedometer2"></i> <i class="bi bi-speedometer2"></i>
@ -192,10 +144,22 @@
<span>المشغّلون</span> <span>المشغّلون</span>
</a> </a>
<a href="{{ route('operator.dashboard') }}" <a href="{{ route('operator.dashboard') }}"
class="mob-nav-item {{ request()->routeIs('operator.*') ? 'active' : '' }}"> class="mob-nav-item {{ request()->routeIs('operator.dashboard') ? 'active' : '' }}">
<i class="bi bi-person-badge"></i> <i class="bi bi-person-badge"></i>
<span>التشغيل</span> <span>التشغيل</span>
</a> </a>
@else
<a href="{{ route('operator.stats') }}"
class="mob-nav-item {{ request()->routeIs('operator.stats') ? 'active' : '' }}">
<i class="bi bi-graph-up"></i>
<span>الإحصائيات</span>
</a>
<a href="{{ route('operator.dashboard') }}"
class="mob-nav-item {{ request()->routeIs('operator.dashboard') ? 'active' : '' }}">
<i class="bi bi-person-badge"></i>
<span>الموقف</span>
</a>
@endif
</nav> </nav>
<script> <script>

View File

@ -14,30 +14,10 @@
<body style="background:#f1f5f9;font-family:'Cairo',sans-serif;"> <body style="background:#f1f5f9;font-family:'Cairo',sans-serif;">
{{-- ══ HEADER ══════════════════════════════════════════════════════════════ --}} {{-- ══ HEADER ══════════════════════════════════════════════════════════════ --}}
<header class="public-header"> @include('partials.topbar')
<div class="container">
<div class="d-flex align-items-center justify-content-between">
{{-- Logo --}}
<a href="{{ route('parking.index') }}" class="d-flex align-items-center gap-3 text-decoration-none">
<div style="width:40px;height:40px;background:#6366f1;border-radius:.625rem;display:flex;align-items:center;justify-content:center;flex-shrink:0;">
<i class="bi bi-p-square-fill text-white" style="font-size:1.25rem;"></i>
</div>
<div>
<div class="fw-800" style="color:#f8fafc;font-size:1.05rem;line-height:1.2;">دمشق باركينغ</div>
<div style="color:#94a3b8;font-size:.72rem;">مواقف السيارات في دمشق</div>
</div>
</a>
{{-- User Dropdown --}}
@include('partials.user-dropdown')
</div>
</div>
</header>
{{-- ══ CONTENT ═════════════════════════════════════════════════════════════ --}} {{-- ══ CONTENT ═════════════════════════════════════════════════════════════ --}}
<main class="container py-4" style="max-width:820px;"> <main style="padding:1.5rem;">
{{-- Flash messages --}} {{-- Flash messages --}}
@if(session('success')) @if(session('success'))

View File

@ -241,7 +241,7 @@ $gradients = [
<div class="lot-card-stats"> <div class="lot-card-stats">
<span><i class="bi bi-car-front"></i>{{ $lot['total'] }} مكان</span> <span><i class="bi bi-car-front"></i>{{ $lot['total'] }} مكان</span>
<span><i class="bi bi-currency-exchange"></i>{{ number_format($lot['price']) }} ل.س/س</span> <span><i class="bi bi-currency-exchange"></i>{{ number_format($lot['price']) }} ليرة سورية/س</span>
<span><i class="bi bi-clock"></i>{{ $lot['hours'] }}</span> <span><i class="bi bi-clock"></i>{{ $lot['hours'] }}</span>
</div> </div>
@ -290,7 +290,7 @@ $gradients = [
<div class="d-flex align-items-center gap-3"> <div class="d-flex align-items-center gap-3">
<span class="badge badge-soft-success">{{ $selectedLot->available_spaces }} متاح</span> <span class="badge badge-soft-success">{{ $selectedLot->available_spaces }} متاح</span>
<span class="badge badge-soft-warning">{{ $selectedLot->occupied_spaces }} مشغول</span> <span class="badge badge-soft-warning">{{ $selectedLot->occupied_spaces }} مشغول</span>
@if(!$assignedLotId) @if(count($assignedLotIds) > 1)
<a href="{{ route('operator.dashboard') }}" <a href="{{ route('operator.dashboard') }}"
class="btn btn-sm fw-600" class="btn btn-sm fw-600"
style="background:#f1f5f9;color:#475569;border:none;border-radius:.5rem;font-family:'Cairo',sans-serif;"> style="background:#f1f5f9;color:#475569;border:none;border-radius:.5rem;font-family:'Cairo',sans-serif;">
@ -962,13 +962,13 @@ async function openReceipt(id) {
document.getElementById('rcpt-name').textContent = d.customer_name || 'غير محدد'; document.getElementById('rcpt-name').textContent = d.customer_name || 'غير محدد';
document.getElementById('rcpt-entry').textContent = d.entry_time; document.getElementById('rcpt-entry').textContent = d.entry_time;
document.getElementById('rcpt-exit').textContent = d.exit_time; document.getElementById('rcpt-exit').textContent = d.exit_time;
document.getElementById('rcpt-total').textContent = Number(d.total_fee).toLocaleString('ar-SA') + ' ل'; document.getElementById('rcpt-total').textContent = Number(d.total_fee).toLocaleString('ar-SA') + ' ليرة سورية';
const rows = d.fee_details.map(r => ` const rows = d.fee_details.map(r => `
<div class="fee-row"> <div class="fee-row">
<span>${r.day} <small style="color:#94a3b8;">${r.date}</small></span> <span>${r.day} <small style="color:#94a3b8;">${r.date}</small></span>
<span style="color:#64748b;">${r.hours}س × ${Number(r.rate).toLocaleString('ar-SA')}</span> <span style="color:#64748b;">${r.hours}س × ${Number(r.rate).toLocaleString('ar-SA')}</span>
<span class="fw-600" style="color:#0f172a;">${Number(r.subtotal).toLocaleString('ar-SA')} ل.س</span> <span class="fw-600" style="color:#0f172a;">${Number(r.subtotal).toLocaleString('ar-SA')} ليرة سورية</span>
</div>`).join(''); </div>`).join('');
document.getElementById('rcpt-breakdown').innerHTML = document.getElementById('rcpt-breakdown').innerHTML =
rows || '<p class="text-xs text-center" style="color:#94a3b8;">لا تفاصيل</p>'; rows || '<p class="text-xs text-center" style="color:#94a3b8;">لا تفاصيل</p>';

View File

@ -0,0 +1,692 @@
@extends('layouts.admin')
@section('title', 'إحصائيات المشغّل — دمشق باركينغ')
@section('page-title', 'إحصائيات المشغّل')
@section('styles')
<style>
/* ── KPI Cards ───────────────────────────────────────────────────────────── */
.kpi-card {
border-radius: 1rem;
padding: 1.25rem 1.375rem;
position: relative;
overflow: hidden;
border: none;
height: 100%;
}
.kpi-card::after {
content: '';
position: absolute;
inset-inline-end: -18px;
bottom: -18px;
width: 90px;
height: 90px;
border-radius: 50%;
opacity: .08;
background: currentColor;
}
.kpi-icon {
width: 44px; height: 44px;
border-radius: .75rem;
display: flex; align-items: center; justify-content: center;
font-size: 1.3rem;
margin-bottom: .875rem;
flex-shrink: 0;
}
.kpi-value {
font-size: 2rem;
font-weight: 900;
line-height: 1.1;
margin-bottom: .2rem;
}
.kpi-label {
font-size: .8rem;
font-weight: 600;
opacity: .75;
}
.kpi-sub {
font-size: .7rem;
opacity: .55;
margin-top: .125rem;
}
/* ── Chart cards ─────────────────────────────────────────────────────────── */
.chart-card {
border-radius: 1rem;
border: none;
box-shadow: 0 4px 16px rgba(0,0,0,.06);
height: 100%;
display: flex; flex-direction: column;
}
.chart-card .card-header {
background: transparent;
border-bottom: 1px solid #f1f5f9;
padding: 1rem 1.25rem .75rem;
border-radius: 1rem 1rem 0 0;
flex-shrink: 0;
}
/* ── Lot status cards ────────────────────────────────────────────────────── */
.lot-stat-card {
background: #fff;
border-radius: 1rem;
border: none;
box-shadow: 0 4px 16px rgba(0,0,0,.06);
padding: 1.125rem 1.25rem;
transition: transform .2s, box-shadow .2s;
}
.lot-stat-card:hover {
transform: translateY(-4px);
box-shadow: 0 12px 28px rgba(0,0,0,.1);
}
.lot-occ-bar { height: 6px; background: #e2e8f0; border-radius: 4px; overflow: hidden; margin: .625rem 0; }
.lot-occ-fill { height: 100%; border-radius: 4px; transition: width .6s cubic-bezier(.4,0,.2,1); }
/* ── Recent table ────────────────────────────────────────────────────────── */
.recent-table th { font-size: .75rem; font-weight: 700; color: #64748b; text-transform: uppercase; letter-spacing: .04em; white-space: nowrap; }
.recent-table td { font-size: .85rem; vertical-align: middle; }
.plate-badge {
display: inline-block;
font-family: monospace;
font-weight: 800;
letter-spacing: 2px;
font-size: .82rem;
direction: ltr;
background: #f1f5f9;
color: #1e293b;
padding: .2em .6em;
border-radius: .4rem;
}
.pay-badge {
font-size: .7rem; font-weight: 700; padding: .25em .65em; border-radius: 20px;
}
.pay-cash { background: rgba(16,185,129,.1); color: #059669; }
.pay-upload { background: rgba(99,102,241,.1); color: #4f46e5; }
/* ── Work progress card ──────────────────────────────────────────────────── */
.work-card {
background: #fff;
border-radius: 1rem;
border: none;
box-shadow: 0 4px 16px rgba(0,0,0,.06);
padding: 1.25rem 1.5rem;
}
.day-bar-track {
height: 8px; background: #e2e8f0; border-radius: 4px; overflow: hidden; margin: .5rem 0 .25rem;
}
.day-bar-fill {
height: 100%; border-radius: 4px;
background: linear-gradient(90deg, #6366f1, #10b981);
transition: width .8s cubic-bezier(.4,0,.2,1);
}
.work-stat {
display: flex; flex-direction: column; align-items: center;
padding: .75rem 1rem; border-radius: .75rem; flex: 1; min-width: 90px;
}
.work-stat-value { font-size: 1.5rem; font-weight: 900; line-height: 1.1; }
.work-stat-label { font-size: .72rem; font-weight: 600; opacity: .65; margin-top: .15rem; text-align: center; }
.trend-chip {
display: inline-flex; align-items: center; gap: .2rem;
font-size: .7rem; font-weight: 700; padding: .15em .55em; border-radius: 20px;
}
.trend-up { background: rgba(16,185,129,.1); color: #059669; }
.trend-down { background: rgba(239,68,68,.1); color: #dc2626; }
.trend-flat { background: rgba(100,116,139,.1); color: #64748b; }
/* ── No-lots empty state ─────────────────────────────────────────────────── */
.empty-state {
display: flex; flex-direction: column; align-items: center; justify-content: center;
padding: 4rem 2rem; text-align: center;
}
.empty-icon {
width: 80px; height: 80px;
background: rgba(99,102,241,.08);
border-radius: 50%;
display: flex; align-items: center; justify-content: center;
font-size: 2.5rem; color: #6366f1; margin-bottom: 1.25rem;
}
</style>
@endsection
@section('content')
@if(empty($assignedLotIds))
{{-- ── No lots assigned ──────────────────────────────────────────────────── --}}
<div class="card border-0 shadow-sm" style="border-radius:1rem;">
<div class="empty-state">
<div class="empty-icon"><i class="bi bi-bar-chart"></i></div>
<h5 class="fw-800 mb-2" style="color:#0f172a;">لا توجد مواقف مخصصة</h5>
<p class="mb-0" style="color:#64748b;max-width:340px;">
لم يتم تعيينك في أي موقف بعد. تواصل مع المدير لتخصيص مواقف لحسابك.
</p>
</div>
</div>
@else
{{-- ── KPI Row ───────────────────────────────────────────────────────────────── --}}
<div class="row g-3 mb-4">
<div class="col-xl col-lg-4 col-sm-6">
<div class="kpi-card h-100" style="background:linear-gradient(135deg,#0f172a 0%,#1e293b 100%);color:#e2e8f0;">
<div class="kpi-icon" style="background:rgba(99,102,241,.2);">
<i class="bi bi-car-front" style="color:#818cf8;"></i>
</div>
<div class="kpi-value" id="kpi-active" style="color:#fff;">--</div>
<div class="kpi-label">السيارات داخل المواقف</div>
<div class="kpi-sub">الآن · مشغول فعلياً</div>
</div>
</div>
<div class="col-xl col-lg-4 col-sm-6">
<div class="kpi-card h-100" style="background:linear-gradient(135deg,#064e3b 0%,#065f46 100%);color:#d1fae5;">
<div class="kpi-icon" style="background:rgba(16,185,129,.2);">
<i class="bi bi-box-arrow-in-down" style="color:#34d399;"></i>
</div>
<div class="kpi-value" id="kpi-checkins" style="color:#fff;">--</div>
<div class="kpi-label">دخول اليوم</div>
<div class="kpi-sub">منذ منتصف الليل</div>
</div>
</div>
<div class="col-xl col-lg-4 col-sm-6">
<div class="kpi-card h-100" style="background:linear-gradient(135deg,#1e1b4b 0%,#312e81 100%);color:#e0e7ff;">
<div class="kpi-icon" style="background:rgba(129,140,248,.2);">
<i class="bi bi-cash-stack" style="color:#a5b4fc;"></i>
</div>
<div class="kpi-value" id="kpi-revenue" style="color:#fff;font-size:1.4rem;">--</div>
<div class="kpi-label">إيرادات اليوم</div>
<div class="kpi-sub">ليرة سورية · مدفوع</div>
</div>
</div>
<div class="col-xl col-lg-4 col-sm-6">
<div class="kpi-card h-100" style="background:linear-gradient(135deg,#4a044e 0%,#6b21a8 100%);color:#f3e8ff;">
<div class="kpi-icon" style="background:rgba(192,132,252,.2);">
<i class="bi bi-p-square" style="color:#c084fc;"></i>
</div>
<div class="kpi-value" id="kpi-available" style="color:#fff;">--</div>
<div class="kpi-label">أماكن متاحة</div>
<div class="kpi-sub">مجموع المواقف</div>
</div>
</div>
<div class="col-xl col-lg-4 col-sm-6">
<div class="kpi-card h-100" style="background:linear-gradient(135deg,#451a03 0%,#92400e 100%);color:#fef3c7;">
<div class="kpi-icon" style="background:rgba(251,191,36,.2);">
<i class="bi bi-calendar-event" style="color:#fcd34d;"></i>
</div>
<div class="kpi-value" id="kpi-reservations" style="color:#fff;">--</div>
<div class="kpi-label">حجوزات منتظرة</div>
<div class="kpi-sub">لم تُفعَّل بعد</div>
</div>
</div>
</div>
{{-- ── Work Progress ────────────────────────────────────────────────────────── --}}
<div class="work-card mb-4">
{{-- Header row --}}
<div class="d-flex align-items-center justify-content-between flex-wrap gap-2 mb-3">
<div>
<h6 class="fw-800 mb-0" style="color:#0f172a;font-size:.95rem;">
<i class="bi bi-person-check me-2" style="color:#6366f1;"></i>
تقدم العمل اليوم
</h6>
<p class="mb-0 mt-1" style="font-size:.75rem;color:#94a3b8;" id="work-date">{{ now()->translatedFormat('l، j F Y') }}</p>
</div>
<div class="d-flex align-items-center gap-2 flex-wrap">
<span class="badge" id="completion-badge" style="font-size:.75rem;padding:.4em .8em;"></span>
<span style="font-size:.75rem;color:#64748b;">معدل الإنجاز</span>
</div>
</div>
{{-- Day progress bar --}}
<div class="mb-1" style="font-size:.72rem;color:#94a3b8;display:flex;justify-content:space-between;">
<span>بداية اليوم 00:00</span>
<span id="work-now-label"></span>
<span>نهاية اليوم 24:00</span>
</div>
<div class="day-bar-track">
<div class="day-bar-fill" id="day-progress-fill" style="width:0%;"></div>
</div>
<p class="mb-3" style="font-size:.7rem;color:#94a3b8;text-align:center;" id="day-pct-label"></p>
{{-- Work stats row --}}
<div class="d-flex flex-wrap gap-2">
<div class="work-stat" style="background:rgba(16,185,129,.07);color:#065f46;">
<div class="work-stat-value" id="ws-checkins">--</div>
<div class="work-stat-label">دخول اليوم</div>
<div id="ws-vs-yesterday" class="mt-1"></div>
</div>
<div class="work-stat" style="background:rgba(99,102,241,.07);color:#312e81;">
<div class="work-stat-value" id="ws-checkouts">--</div>
<div class="work-stat-label">خروج مكتمل</div>
</div>
<div class="work-stat" style="background:rgba(239,68,68,.07);color:#7f1d1d;">
<div class="work-stat-value" id="ws-cancelled">--</div>
<div class="work-stat-label">ملغي</div>
</div>
<div class="work-stat" style="background:rgba(245,158,11,.07);color:#78350f;">
<div class="work-stat-value" id="ws-avg-stay">--</div>
<div class="work-stat-label">متوسط المدة</div>
</div>
<div class="work-stat" style="background:rgba(14,165,233,.07);color:#0c4a6e;">
<div class="work-stat-value" id="ws-peak-hour">--</div>
<div class="work-stat-label">الساعة الأكثر ازدحاماً</div>
</div>
</div>
</div>
{{-- ── Charts ────────────────────────────────────────────────────────────────── --}}
<div class="row g-3 mb-4">
{{-- Daily check-ins bar chart --}}
<div class="col-lg-5">
<div class="chart-card card">
<div class="card-header d-flex align-items-center justify-content-between">
<span class="fw-700" style="font-size:.9rem;">
<i class="bi bi-bar-chart-line me-2" style="color:#10b981;"></i>
الدخول آخر 7 أيام
</span>
<span class="badge" style="background:rgba(16,185,129,.1);color:#059669;font-size:.72rem;">دخول</span>
</div>
<div class="card-body p-3 flex-grow-1" style="min-height:220px;position:relative;">
<canvas id="checkinsChart"></canvas>
</div>
</div>
</div>
{{-- Hourly activity today --}}
<div class="col-lg-4">
<div class="chart-card card">
<div class="card-header d-flex align-items-center justify-content-between">
<span class="fw-700" style="font-size:.9rem;">
<i class="bi bi-clock me-2" style="color:#f59e0b;"></i>
توزيع الدخول بالساعة
</span>
<span class="badge" style="background:rgba(245,158,11,.1);color:#d97706;font-size:.72rem;">اليوم</span>
</div>
<div class="card-body p-3 flex-grow-1" style="min-height:220px;position:relative;">
<canvas id="hourlyChart"></canvas>
</div>
</div>
</div>
{{-- Daily revenue line chart --}}
<div class="col-lg-3">
<div class="chart-card card">
<div class="card-header d-flex align-items-center justify-content-between">
<span class="fw-700" style="font-size:.9rem;">
<i class="bi bi-graph-up-arrow me-2" style="color:#818cf8;"></i>
الإيرادات
</span>
<span class="badge" style="background:rgba(99,102,241,.1);color:#6366f1;font-size:.72rem;">ليرة سورية</span>
</div>
<div class="card-body p-3 flex-grow-1" style="min-height:220px;position:relative;">
<canvas id="revenueChart"></canvas>
</div>
</div>
</div>
</div>
{{-- ── Per-lot status (only when operator has >1 lot) ────────────────────────── --}}
@if(count($lots) > 1)
<div class="mb-4">
<h6 class="fw-700 mb-3" style="color:#0f172a;font-size:.9rem;">
<i class="bi bi-buildings me-2" style="color:#6366f1;"></i>
حالة المواقف المخصصة
</h6>
<div class="row g-3" id="lotCards">
@foreach($lots as $lot)
<div class="col-lg-4 col-sm-6">
<div class="lot-stat-card">
<div class="d-flex align-items-start justify-content-between mb-2">
<div>
<div class="fw-800" style="font-size:.95rem;color:#0f172a;">{{ $lot->name }}</div>
<div style="font-size:.75rem;color:#94a3b8;">{{ $lot->address }}</div>
</div>
<span class="lot-status-badge-{{ $lot->id }} badge" style="font-size:.7rem;"></span>
</div>
<div class="lot-occ-bar">
<div class="lot-occ-fill lot-fill-{{ $lot->id }}" style="width:0;background:#6366f1;"></div>
</div>
<div class="d-flex justify-content-between" style="font-size:.78rem;color:#64748b;">
<span><i class="bi bi-car-front me-1"></i><span class="lot-active-{{ $lot->id }}"></span> مشغول</span>
<span><span class="lot-avail-{{ $lot->id }}"></span> / {{ $lot->total_capacity }} متاح</span>
</div>
<div class="mt-2 pt-2 border-top d-flex gap-3" style="font-size:.75rem;color:#64748b;border-color:#f1f5f9!important;">
<span><i class="bi bi-box-arrow-in-down me-1"></i>دخول اليوم: <strong class="lot-today-{{ $lot->id }}"></strong></span>
<span><i class="bi bi-cash me-1"></i>إيراد: <strong class="lot-rev-{{ $lot->id }}"></strong></span>
</div>
</div>
</div>
@endforeach
</div>
</div>
@endif
{{-- ── Recent completions table ─────────────────────────────────────────────── --}}
<div class="card border-0 shadow-sm" style="border-radius:1rem;overflow:hidden;">
<div class="card-header bg-transparent d-flex align-items-center justify-content-between"
style="padding:1rem 1.25rem .75rem;border-bottom:1px solid #f1f5f9;">
<span class="fw-700" style="font-size:.9rem;">
<i class="bi bi-clock-history me-2" style="color:#0ea5e9;"></i>
آخر المدفوعات المنجزة
</span>
<span class="badge" id="last-refresh" style="background:#f1f5f9;color:#64748b;font-size:.7rem;"></span>
</div>
<div class="table-responsive">
<table class="table recent-table mb-0">
<thead style="background:#f8fafc;">
<tr>
<th class="px-4 py-3">اللوحة</th>
<th class="py-3">الموقف</th>
<th class="py-3">العميل</th>
<th class="py-3">وقت الدفع</th>
<th class="py-3 text-center">الرسوم</th>
<th class="py-3 text-center">الدفع</th>
</tr>
</thead>
<tbody id="recent-tbody">
<tr>
<td colspan="6" class="text-center py-5" style="color:#94a3b8;">
<div class="spinner-border spinner-border-sm me-2"></div>
جاري التحميل...
</td>
</tr>
</tbody>
</table>
</div>
</div>
@endif {{-- assignedLotIds not empty --}}
@endsection
@push('scripts')
@if(!empty($assignedLotIds))
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.7/dist/chart.umd.min.js"></script>
<script>
let checkinsChart, revenueChart, hourlyChart;
const LOTS = @json($lots->map(fn($l) => ['id' => $l->id, 'name' => $l->name]));
const MULTI_LOT = LOTS.length > 1;
// ── Day progress bar (runs on its own clock) ──────────────────────────────────
function updateDayProgress() {
const now = new Date();
const pct = Math.round((now.getHours() * 60 + now.getMinutes()) / 1440 * 100);
const fill = document.getElementById('day-progress-fill');
const lbl = document.getElementById('day-pct-label');
const now2 = document.getElementById('work-now-label');
if (fill) fill.style.width = pct + '%';
if (lbl) lbl.textContent = `${pct}% من اليوم مضى`;
if (now2) now2.textContent = now.toLocaleTimeString('ar-SA', {hour:'2-digit', minute:'2-digit'}) + ' الآن';
}
updateDayProgress();
setInterval(updateDayProgress, 60000);
// ── Duration formatter ────────────────────────────────────────────────────────
function fmtDuration(mins) {
if (!mins) return '—';
const h = Math.floor(mins / 60);
const m = mins % 60;
return h > 0 ? `${h}س ${m}د` : `${m}د`;
}
// ── Load & render ─────────────────────────────────────────────────────────────
async function loadStats() {
try {
const { success, data } = await fetch('{{ route("operator.statsData") }}').then(r => r.json());
if (!success) return;
// KPI cards
document.getElementById('kpi-active').textContent = data.active_cars;
document.getElementById('kpi-checkins').textContent = data.checkins_today;
document.getElementById('kpi-revenue').textContent = data.revenue_today.toLocaleString('ar-SA') + ' ليرة سورية';
document.getElementById('kpi-available').textContent = data.available_spaces;
document.getElementById('kpi-reservations').textContent = data.pending_reservations;
// ── Work progress ──────────────────────────────────────────────────────
document.getElementById('ws-checkins').textContent = data.checkins_today;
document.getElementById('ws-checkouts').textContent = data.checkouts_today;
document.getElementById('ws-cancelled').textContent = data.cancelled_today;
document.getElementById('ws-avg-stay').textContent = fmtDuration(data.avg_duration_min);
const peak = data.peak_hour;
const peakStr = peak !== null && peak !== undefined
? new Date(2000,0,1,peak).toLocaleTimeString('ar-SA', {hour:'2-digit', minute:'2-digit'})
: '—';
document.getElementById('ws-peak-hour').textContent = peakStr;
// Completion rate badge
const cr = data.completion_rate ?? 100;
const crEl = document.getElementById('completion-badge');
if (crEl) {
crEl.textContent = cr + '%';
crEl.style.cssText = cr >= 80
? 'background:rgba(16,185,129,.12);color:#059669;font-size:.75rem;padding:.4em .8em;'
: cr >= 50
? 'background:rgba(245,158,11,.12);color:#d97706;font-size:.75rem;padding:.4em .8em;'
: 'background:rgba(239,68,68,.12);color:#dc2626;font-size:.75rem;padding:.4em .8em;';
}
// vs yesterday trend chip
const vsel = document.getElementById('ws-vs-yesterday');
if (vsel) {
const diff = data.checkins_today - data.checkins_yesterday;
const cls = diff > 0 ? 'trend-up' : diff < 0 ? 'trend-down' : 'trend-flat';
const icon = diff > 0 ? 'bi-arrow-up' : diff < 0 ? 'bi-arrow-down' : 'bi-dash';
const lbl = diff > 0 ? `+${diff} عن أمس` : diff < 0 ? `${diff} عن أمس` : 'مثل أمس';
vsel.innerHTML = `<span class="trend-chip ${cls}"><i class="bi ${icon}"></i>${lbl}</span>`;
}
// Charts
renderCheckinsChart(data.daily_checkins, data.daily_dates);
renderRevenueChart(data.daily_revenue, data.daily_dates);
renderHourlyChart(data.hourly_activity ?? []);
// Per-lot cards
if (MULTI_LOT && data.lot_stats) {
data.lot_stats.forEach(lot => {
const pct = lot.pct;
const fill = document.querySelector(`.lot-fill-${lot.id}`);
const badge = document.querySelector(`.lot-status-badge-${lot.id}`);
if (fill) { fill.style.width = pct + '%'; fill.style.background = pct >= 90 ? '#ef4444' : pct >= 60 ? '#f59e0b' : '#10b981'; }
if (badge) {
badge.textContent = pct >= 90 ? 'ممتلئ' : pct >= 60 ? 'مشغول' : 'متاح';
badge.style.cssText = pct >= 90
? 'background:rgba(239,68,68,.1);color:#dc2626;'
: pct >= 60
? 'background:rgba(245,158,11,.1);color:#d97706;'
: 'background:rgba(16,185,129,.1);color:#059669;';
}
const setText = (sel, val) => { const el = document.querySelector(sel); if (el) el.textContent = val; };
setText(`.lot-active-${lot.id}`, lot.active);
setText(`.lot-avail-${lot.id}`, lot.available);
setText(`.lot-today-${lot.id}`, lot.today_ins);
setText(`.lot-rev-${lot.id}`, lot.today_rev.toLocaleString('ar-SA'));
});
}
// Recent completions table
renderRecentTable(data.recent_completions ?? []);
document.getElementById('last-refresh').textContent = 'تحديث ' + new Date().toLocaleTimeString('ar-SA', {hour:'2-digit', minute:'2-digit'});
} catch (e) {
console.error(e);
}
}
// ── Bar chart: check-ins ──────────────────────────────────────────────────────
function renderCheckinsChart(counts, dates) {
const labels = dates.map(d => {
const dt = new Date(d + 'T00:00:00');
return dt.toLocaleDateString('ar-SA', { weekday: 'short', day: 'numeric' });
});
const fullLabels = dates.map(d => {
const dt = new Date(d + 'T00:00:00');
return dt.toLocaleDateString('ar-SA', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' });
});
if (checkinsChart) { checkinsChart.destroy(); }
checkinsChart = new Chart(document.getElementById('checkinsChart'), {
type: 'bar',
data: {
labels,
datasets: [{
data: counts,
backgroundColor: counts.map((_, i) => i === counts.length - 1 ? 'rgba(16,185,129,.9)' : 'rgba(16,185,129,.25)'),
borderColor: counts.map((_, i) => i === counts.length - 1 ? '#10b981' : 'rgba(16,185,129,.4)'),
borderWidth: 2,
borderRadius: 8,
hoverBackgroundColor: 'rgba(16,185,129,.7)',
}],
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { display: false },
tooltip: { callbacks: {
title: ctx => fullLabels[ctx[0].dataIndex],
label: ctx => ` ${ctx.parsed.y} دخول`,
}},
},
scales: {
y: { beginAtZero: true, ticks: { precision: 0 }, grid: { color: '#f8fafc' } },
x: { grid: { display: false } },
},
},
});
}
// ── Line chart: revenue ───────────────────────────────────────────────────────
function renderRevenueChart(amounts, dates) {
const labels = dates.map(d => {
const dt = new Date(d + 'T00:00:00');
return dt.toLocaleDateString('ar-SA', { weekday: 'short', day: 'numeric' });
});
if (revenueChart) { revenueChart.destroy(); }
revenueChart = new Chart(document.getElementById('revenueChart'), {
type: 'line',
data: {
labels,
datasets: [{
data: amounts,
borderColor: '#818cf8',
backgroundColor: 'rgba(99,102,241,.08)',
borderWidth: 2.5,
pointBackgroundColor: '#6366f1',
pointRadius: 4,
pointHoverRadius: 6,
fill: true,
tension: 0.4,
}],
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { display: false },
tooltip: { callbacks: {
label: ctx => ` ${ctx.parsed.y.toLocaleString('ar-SA')} ليرة سورية`,
}},
},
scales: {
y: { beginAtZero: true, grid: { color: '#f8fafc' }, ticks: { callback: v => v.toLocaleString('ar-SA') } },
x: { grid: { display: false } },
},
},
});
}
// ── Hourly activity chart ─────────────────────────────────────────────────────
function renderHourlyChart(hourly) {
// Show only hours 623 to keep the chart readable
const hours = Array.from({length: 18}, (_, i) => i + 6);
const counts = hours.map(h => hourly[h] ?? 0);
const labels = hours.map(h => h + ':00');
const nowH = new Date().getHours();
const colors = counts.map((_, i) => {
const h = hours[i];
if (h === nowH) return 'rgba(245,158,11,.9)';
return counts[i] === Math.max(...counts) && counts[i] > 0 ? 'rgba(99,102,241,.8)' : 'rgba(99,102,241,.2)';
});
if (hourlyChart) { hourlyChart.destroy(); }
hourlyChart = new Chart(document.getElementById('hourlyChart'), {
type: 'bar',
data: {
labels,
datasets: [{
data: counts,
backgroundColor: colors,
borderColor: colors.map(c => c.replace(/[\d.]+\)$/, '1)')),
borderWidth: 1,
borderRadius: 4,
}],
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { display: false },
tooltip: { callbacks: {
title: ctx => `${hours[ctx[0].dataIndex]}:00`,
label: ctx => ` ${ctx.parsed.y} دخول`,
}},
},
scales: {
y: { beginAtZero: true, ticks: { precision: 0, font: { size: 10 } }, grid: { color: '#f8fafc' } },
x: { grid: { display: false }, ticks: { font: { size: 9 }, maxRotation: 0 } },
},
},
});
}
// ── Recent completions ────────────────────────────────────────────────────────
function renderRecentTable(rows) {
const tbody = document.getElementById('recent-tbody');
if (!rows.length) {
tbody.innerHTML = `<tr><td colspan="6" class="text-center py-5" style="color:#94a3b8;">
<i class="bi bi-inbox d-block mb-2" style="font-size:2rem;opacity:.3;"></i>
لا توجد مدفوعات منجزة بعد
</td></tr>`;
return;
}
const fmt = iso => {
if (!iso) return '—';
return new Date(iso).toLocaleString('ar-SA', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' });
};
tbody.innerHTML = rows.map(r => {
const payBadge = r.payment_method === 'cash'
? '<span class="pay-badge pay-cash"><i class="bi bi-cash me-1"></i>نقدي</span>'
: '<span class="pay-badge pay-upload"><i class="bi bi-upload me-1"></i>تحويل</span>';
const fee = r.total_fee ? r.total_fee.toLocaleString('ar-SA') + ' ليرة سورية' : '—';
return `<tr>
<td class="px-4 py-3"><span class="plate-badge">${r.vehicle_plate ?? '—'}</span></td>
<td class="py-3" style="color:#475569;">${r.parking_lot?.name ?? '—'}</td>
<td class="py-3" style="color:#475569;">${r.customer_name ?? '—'}</td>
<td class="py-3" style="color:#64748b;font-size:.8rem;">${fmt(r.paid_at)}</td>
<td class="py-3 text-center fw-700" style="color:#0f172a;">${fee}</td>
<td class="py-3 text-center">${payBadge}</td>
</tr>`;
}).join('');
}
// ── Boot ──────────────────────────────────────────────────────────────────────
loadStats();
setInterval(loadStats, 30000);
</script>
@endif
@endpush

View File

@ -0,0 +1,33 @@
<header class="app-topbar">
{{-- Left: logo + optional admin controls --}}
<div class="d-flex align-items-center gap-3">
{{-- Brand logo (always shown, links to public site) --}}
<a href="{{ route('parking.index') }}" class="d-flex align-items-center gap-3 text-decoration-none">
<div style="width:36px;height:36px;background:#6366f1;border-radius:.5rem;display:flex;align-items:center;justify-content:center;flex-shrink:0;">
<i class="bi bi-p-square-fill text-white" style="font-size:1.1rem;"></i>
</div>
<div class="d-none d-sm-block">
<div class="fw-800" style="color:#f8fafc;font-size:1rem;line-height:1.2;">دمشق باركينغ</div>
<div style="color:#94a3b8;font-size:.68rem;">مواقف السيارات في دمشق</div>
</div>
</a>
@if($isAdminLayout ?? false)
{{-- Sidebar toggle (admin/operator pages only) --}}
<button class="sidebar-toggle" id="sidebarToggle" aria-label="قائمة التنقل">
<i class="bi bi-list"></i>
</button>
{{-- Page title --}}
<h1 class="topbar-title">@yield('page-title', 'لوحة التحكم')</h1>
@endif
</div>
{{-- Right: user dropdown --}}
<div class="d-flex align-items-center gap-2">
@include('partials.user-dropdown')
</div>
</header>

View File

@ -6,8 +6,7 @@
data-bs-toggle="dropdown" data-bs-toggle="dropdown"
aria-expanded="false" aria-expanded="false"
style="background:rgba(255,255,255,.1);color:#f8fafc;border:1px solid rgba(255,255,255,.18);border-radius:.625rem;padding:.35rem .75rem;font-family:'Cairo',sans-serif;"> style="background:rgba(255,255,255,.1);color:#f8fafc;border:1px solid rgba(255,255,255,.18);border-radius:.625rem;padding:.35rem .75rem;font-family:'Cairo',sans-serif;">
{{-- Avatar circle --}} <div style="width:32px;height:32px;background:linear-gradient(135deg,#6366f1,#8b5cf6);border-radius:50%;display:flex;align-items:center;justify-content:center;font-weight:800;font-size:.9rem;flex-shrink:0;color:#fff;border:2px solid rgba(255,255,255,.25);">
<div style="width:32px;height:32px;background:#6366f1;border-radius:50%;display:flex;align-items:center;justify-content:center;font-weight:800;font-size:.9rem;flex-shrink:0;color:#fff;border:2px solid rgba(255,255,255,.25);">
{{ mb_substr(auth()->user()->name, 0, 1) }} {{ mb_substr(auth()->user()->name, 0, 1) }}
</div> </div>
<span class="d-none d-sm-inline fw-600" style="font-size:.875rem;max-width:120px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;"> <span class="d-none d-sm-inline fw-600" style="font-size:.875rem;max-width:120px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">
@ -32,7 +31,6 @@
<div style="color:#64748b;font-size:.75rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;"> <div style="color:#64748b;font-size:.75rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">
{{ auth()->user()->email }} {{ auth()->user()->email }}
</div> </div>
{{-- Role badge --}}
@php @php
$role = auth()->user()->role; $role = auth()->user()->role;
$roleLabel = match($role) { $roleLabel = match($role) {
@ -53,6 +51,16 @@
<li><hr class="dropdown-divider my-1" style="border-color:#f1f5f9;"></li> <li><hr class="dropdown-divider my-1" style="border-color:#f1f5f9;"></li>
{{-- Public site --}}
<li>
<a href="{{ route('parking.index') }}"
class="dropdown-item d-flex align-items-center gap-2 rounded-2"
style="color:#374151;font-size:.875rem;padding:.5rem .75rem;">
<i class="bi bi-globe2" style="color:#6366f1;font-size:1rem;width:18px;text-align:center;"></i>
الموقع العام
</a>
</li>
{{-- My profile --}} {{-- My profile --}}
<li> <li>
<a href="{{ route('profile.show') }}" <a href="{{ route('profile.show') }}"
@ -63,7 +71,8 @@
</a> </a>
</li> </li>
{{-- My reservations --}} {{-- My reservations (regular users) --}}
@if($role === 'user')
<li> <li>
<a href="{{ route('user.dashboard') }}" <a href="{{ route('user.dashboard') }}"
class="dropdown-item d-flex align-items-center gap-2 rounded-2" class="dropdown-item d-flex align-items-center gap-2 rounded-2"
@ -72,9 +81,10 @@
حجوزاتي حجوزاتي
</a> </a>
</li> </li>
@endif
{{-- Operator panel (operators & admins) --}} {{-- Operator panel (operators & admins) --}}
@if(in_array(auth()->user()->role, ['operator', 'admin'])) @if(in_array($role, ['operator', 'admin']))
<li> <li>
<a href="{{ route('operator.dashboard') }}" <a href="{{ route('operator.dashboard') }}"
class="dropdown-item d-flex align-items-center gap-2 rounded-2" class="dropdown-item d-flex align-items-center gap-2 rounded-2"
@ -86,7 +96,7 @@
@endif @endif
{{-- Admin panel (admins only) --}} {{-- Admin panel (admins only) --}}
@if(auth()->user()->role === 'admin') @if($role === 'admin')
<li> <li>
<a href="{{ route('admin.dashboard') }}" <a href="{{ route('admin.dashboard') }}"
class="dropdown-item d-flex align-items-center gap-2 rounded-2" class="dropdown-item d-flex align-items-center gap-2 rounded-2"

View File

@ -5,15 +5,15 @@
{{-- Page header --}} {{-- Page header --}}
<div class="d-flex align-items-center gap-3 mb-4"> <div class="d-flex align-items-center gap-3 mb-4">
<a href="{{ route('parking.index') }}"
class="btn btn-sm"
style="background:#f1f5f9;color:#475569;border:none;border-radius:.5rem;font-family:'Cairo',sans-serif;">
<i class="bi bi-arrow-right me-1"></i>العودة
</a>
<div> <div>
<h1 class="fw-800 mb-0" style="font-size:1.25rem;color:#0f172a;">حجوزاتي</h1> <h1 class="fw-800 mb-0" style="font-size:1.25rem;color:#0f172a;">حجوزاتي</h1>
<p class="text-sm mb-0" style="color:#64748b;">سجل حجوزاتك في مواقف السيارات</p> <p class="text-sm mb-0" style="color:#64748b;">سجل حجوزاتك في مواقف السيارات</p>
</div> </div>
<a href="{{ route('parking.index') }}"
class="btn btn-sm ms-auto"
style="background:#f1f5f9;color:#475569;border:none;border-radius:.5rem;font-family:'Cairo',sans-serif;">
<i class="bi bi-arrow-left me-1"></i>العودة
</a>
</div> </div>
{{-- ── Debt banner ─────────────────────────────────────────────────────────── --}} {{-- ── Debt banner ─────────────────────────────────────────────────────────── --}}
@ -27,12 +27,12 @@
<div class="fw-700" style="color:#92400e;font-size:.9rem;">رصيد مستحق غير مدفوع</div> <div class="fw-700" style="color:#92400e;font-size:.9rem;">رصيد مستحق غير مدفوع</div>
<div class="text-sm" style="color:#b45309;"> <div class="text-sm" style="color:#b45309;">
لديك رسوم إلغاء متراكمة بقيمة لديك رسوم إلغاء متراكمة بقيمة
<strong>{{ number_format($pendingDebt) }} ل.س</strong> <strong>{{ number_format($pendingDebt) }} ليرة سورية</strong>
ستُضاف تلقائياً إلى حجزك القادم. ستُضاف تلقائياً إلى حجزك القادم.
</div> </div>
</div> </div>
<div class="fw-800" style="color:#92400e;font-size:1.25rem;white-space:nowrap;"> <div class="fw-800" style="color:#92400e;font-size:1.25rem;white-space:nowrap;">
{{ number_format($pendingDebt) }} ل.س {{ number_format($pendingDebt) }} ليرة سورية
</div> </div>
</div> </div>
@endif @endif
@ -130,13 +130,13 @@
{{-- Fee column --}} {{-- Fee column --}}
<td> <td>
@if($hasDebt) @if($hasDebt)
<span class="fw-700" style="color:#ef4444;">{{ number_format($booking->total_fee) }} ل.س</span> <span class="fw-700" style="color:#ef4444;">{{ number_format($booking->total_fee) }} ليرة سورية</span>
<div class="text-xs fw-600" style="color:#ef4444;">مستحق</div> <div class="text-xs fw-600" style="color:#ef4444;">مستحق</div>
@elseif($booking->total_fee > 0 && !is_null($booking->paid_at)) @elseif($booking->total_fee > 0 && !is_null($booking->paid_at))
<span class="fw-600 text-sm" style="color:#10b981;">{{ number_format($booking->total_fee) }} ل.س</span> <span class="fw-600 text-sm" style="color:#10b981;">{{ number_format($booking->total_fee) }} ليرة سورية</span>
<div class="text-xs" style="color:#10b981;">مدفوع</div> <div class="text-xs" style="color:#10b981;">مدفوع</div>
@elseif($booking->total_fee > 0) @elseif($booking->total_fee > 0)
<span class="fw-600 text-sm" style="color:#475569;">{{ number_format($booking->total_fee) }} ل.س</span> <span class="fw-600 text-sm" style="color:#475569;">{{ number_format($booking->total_fee) }} ليرة سورية</span>
@elseif($isFree && $booking->status === 'cancelled') @elseif($isFree && $booking->status === 'cancelled')
<span class="text-xs" style="color:#94a3b8;">مجاني</span> <span class="text-xs" style="color:#94a3b8;">مجاني</span>
@else @else
@ -304,16 +304,16 @@
{{-- After pay-now chosen: back + confirm --}} {{-- After pay-now chosen: back + confirm --}}
<div id="cancelPayNowActions" style="display:none;" class="d-flex gap-2"> <div id="cancelPayNowActions" style="display:none;" class="d-flex gap-2">
<button type="button" class="btn btn-light fw-600"
style="font-family:'Cairo',sans-serif;border-radius:.5rem;"
onclick="cancelBackToActions()">
<i class="bi bi-arrow-right me-1"></i>رجوع
</button>
<button type="button" id="cancelConfirmPayBtn" <button type="button" id="cancelConfirmPayBtn"
class="btn btn-success fw-bold flex-grow-1" class="btn btn-success fw-bold flex-grow-1"
style="font-family:'Cairo',sans-serif;border-radius:.5rem;"> style="font-family:'Cairo',sans-serif;border-radius:.5rem;">
<i class="bi bi-check2-circle me-1"></i>تأكيد الدفع والإلغاء <i class="bi bi-check2-circle me-1"></i>تأكيد الدفع والإلغاء
</button> </button>
<button type="button" class="btn btn-light fw-600"
style="font-family:'Cairo',sans-serif;border-radius:.5rem;"
onclick="cancelBackToActions()">
<i class="bi bi-arrow-left me-1"></i>رجوع
</button>
</div> </div>
</div> </div>
@ -382,12 +382,12 @@ async function openCancelModal(id, lotName, startIso) {
<div style="display:flex;justify-content:space-between;padding:.25rem 0;border-bottom:1px dashed #f1f5f9;"> <div style="display:flex;justify-content:space-between;padding:.25rem 0;border-bottom:1px dashed #f1f5f9;">
<span>${r.day} <small style="color:#94a3b8;">${r.date}</small></span> <span>${r.day} <small style="color:#94a3b8;">${r.date}</small></span>
<span style="color:#64748b;">${r.hours}س × ${Number(r.rate).toLocaleString('ar-SA')}</span> <span style="color:#64748b;">${r.hours}س × ${Number(r.rate).toLocaleString('ar-SA')}</span>
<span class="fw-600" style="color:#0f172a;">${Number(r.subtotal).toLocaleString('ar-SA')} ل.س</span> <span class="fw-600" style="color:#0f172a;">${Number(r.subtotal).toLocaleString('ar-SA')} ليرة سورية</span>
</div>`).join(''); </div>`).join('');
document.getElementById('cancelFeeBreakdown').innerHTML = document.getElementById('cancelFeeBreakdown').innerHTML =
rows || '<span style="color:#94a3b8;font-size:.8rem;">— مدة قصيرة جداً —</span>'; rows || '<span style="color:#94a3b8;font-size:.8rem;">— مدة قصيرة جداً —</span>';
document.getElementById('cancelFeeTotal').textContent = document.getElementById('cancelFeeTotal').textContent =
Number(d.fee).toLocaleString('ar-SA') + ' ل'; Number(d.fee).toLocaleString('ar-SA') + ' ليرة سورية';
// Reset UI // Reset UI
document.getElementById('cancelPayMethodWrap').style.display = 'none'; document.getElementById('cancelPayMethodWrap').style.display = 'none';

View File

@ -5,15 +5,15 @@
{{-- Page header --}} {{-- Page header --}}
<div class="d-flex align-items-center gap-3 mb-4"> <div class="d-flex align-items-center gap-3 mb-4">
<a href="{{ route('parking.index') }}"
class="btn btn-sm"
style="background:#f1f5f9;color:#475569;border:none;border-radius:.5rem;font-family:'Cairo',sans-serif;">
<i class="bi bi-arrow-right me-1"></i>العودة
</a>
<div> <div>
<h1 class="fw-800 mb-0" style="font-size:1.25rem;color:#0f172a;">معلوماتي</h1> <h1 class="fw-800 mb-0" style="font-size:1.25rem;color:#0f172a;">معلوماتي</h1>
<p class="text-sm mb-0" style="color:#64748b;">إدارة بيانات حسابك الشخصي</p> <p class="text-sm mb-0" style="color:#64748b;">إدارة بيانات حسابك الشخصي</p>
</div> </div>
<a href="{{ route('parking.index') }}"
class="btn btn-sm ms-auto"
style="background:#f1f5f9;color:#475569;border:none;border-radius:.5rem;font-family:'Cairo',sans-serif;">
<i class="bi bi-arrow-left me-1"></i>العودة
</a>
</div> </div>
<div class="row g-4"> <div class="row g-4">

View File

@ -54,6 +54,8 @@ Route::prefix('admin')->middleware('admin')->name('admin.')->group(function () {
// Operator Dashboard // Operator Dashboard
Route::prefix('operator')->middleware('operator')->name('operator.')->group(function () { Route::prefix('operator')->middleware('operator')->name('operator.')->group(function () {
Route::get('/dashboard', [\App\Http\Controllers\Operator\OperatorController::class, 'dashboard'])->name('dashboard'); Route::get('/dashboard', [\App\Http\Controllers\Operator\OperatorController::class, 'dashboard'])->name('dashboard');
Route::get('/stats', [\App\Http\Controllers\Operator\OperatorController::class, 'statsPage'])->name('stats');
Route::get('/stats-data', [\App\Http\Controllers\Operator\OperatorController::class, 'statsJson'])->name('statsData');
Route::post('/check-in', [\App\Http\Controllers\Operator\OperatorController::class, 'checkIn'])->name('checkIn'); Route::post('/check-in', [\App\Http\Controllers\Operator\OperatorController::class, 'checkIn'])->name('checkIn');
Route::post('/{booking}/activate', [\App\Http\Controllers\Operator\OperatorController::class, 'activateReservation'])->name('activate'); Route::post('/{booking}/activate', [\App\Http\Controllers\Operator\OperatorController::class, 'activateReservation'])->name('activate');
Route::get('/{booking}/checkout-preview', [\App\Http\Controllers\Operator\OperatorController::class, 'checkoutPreview'])->name('checkoutPreview'); Route::get('/{booking}/checkout-preview', [\App\Http\Controllers\Operator\OperatorController::class, 'checkoutPreview'])->name('checkoutPreview');

0
storage/app/.gitignore vendored Normal file → Executable file
View File

0
storage/app/private/.gitignore vendored Normal file → Executable file
View File

0
storage/app/public/.gitignore vendored Normal file → Executable file
View File

0
storage/framework/.gitignore vendored Normal file → Executable file
View File

0
storage/framework/cache/.gitignore vendored Normal file → Executable file
View File

0
storage/framework/cache/data/.gitignore vendored Normal file → Executable file
View File

0
storage/framework/sessions/.gitignore vendored Normal file → Executable file
View File

0
storage/framework/testing/.gitignore vendored Normal file → Executable file
View File

0
storage/framework/views/.gitignore vendored Normal file → Executable file
View File

0
storage/logs/.gitignore vendored Normal file → Executable file
View File

View File

@ -6,7 +6,28 @@ $vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir); $baseDir = dirname($vendorDir);
return array( return array(
'App\\Http\\Controllers\\Admin\\AdminOperatorController' => $baseDir . '/app/Http/Controllers/Admin/AdminOperatorController.php',
'App\\Http\\Controllers\\Admin\\BookingController' => $baseDir . '/app/Http/Controllers/Admin/BookingController.php',
'App\\Http\\Controllers\\Admin\\DashboardController' => $baseDir . '/app/Http/Controllers/Admin/DashboardController.php',
'App\\Http\\Controllers\\Admin\\ParkingLotController' => $baseDir . '/app/Http/Controllers/Admin/ParkingLotController.php',
'App\\Http\\Controllers\\Api\\BookingController' => $baseDir . '/app/Http/Controllers/Api/BookingController.php',
'App\\Http\\Controllers\\Api\\CarRegistryController' => $baseDir . '/app/Http/Controllers/Api/CarRegistryController.php',
'App\\Http\\Controllers\\Api\\ParkingLotController' => $baseDir . '/app/Http/Controllers/Api/ParkingLotController.php',
'App\\Http\\Controllers\\Controller' => $baseDir . '/app/Http/Controllers/Controller.php', 'App\\Http\\Controllers\\Controller' => $baseDir . '/app/Http/Controllers/Controller.php',
'App\\Http\\Controllers\\Operator\\OperatorController' => $baseDir . '/app/Http/Controllers/Operator/OperatorController.php',
'App\\Http\\Controllers\\ParkingController' => $baseDir . '/app/Http/Controllers/ParkingController.php',
'App\\Http\\Controllers\\ProfileController' => $baseDir . '/app/Http/Controllers/ProfileController.php',
'App\\Http\\Middleware\\EnsureAdmin' => $baseDir . '/app/Http/Middleware/EnsureAdmin.php',
'App\\Http\\Middleware\\EnsureOperator' => $baseDir . '/app/Http/Middleware/EnsureOperator.php',
'App\\Http\\Requests\\StoreBookingRequest' => $baseDir . '/app/Http/Requests/StoreBookingRequest.php',
'App\\Http\\Requests\\StoreOperatorCheckInRequest' => $baseDir . '/app/Http/Requests/StoreOperatorCheckInRequest.php',
'App\\Http\\Requests\\StoreParkingLotRequest' => $baseDir . '/app/Http/Requests/StoreParkingLotRequest.php',
'App\\Http\\Resources\\BookingResource' => $baseDir . '/app/Http/Resources/BookingResource.php',
'App\\Http\\Resources\\CarRegistryResource' => $baseDir . '/app/Http/Resources/CarRegistryResource.php',
'App\\Http\\Resources\\ParkingLotResource' => $baseDir . '/app/Http/Resources/ParkingLotResource.php',
'App\\Models\\Booking' => $baseDir . '/app/Models/Booking.php',
'App\\Models\\CarRegistry' => $baseDir . '/app/Models/CarRegistry.php',
'App\\Models\\ParkingLot' => $baseDir . '/app/Models/ParkingLot.php',
'App\\Models\\User' => $baseDir . '/app/Models/User.php', 'App\\Models\\User' => $baseDir . '/app/Models/User.php',
'App\\Providers\\AppServiceProvider' => $baseDir . '/app/Providers/AppServiceProvider.php', 'App\\Providers\\AppServiceProvider' => $baseDir . '/app/Providers/AppServiceProvider.php',
'Attribute' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/Attribute.php', 'Attribute' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/Attribute.php',
@ -132,7 +153,10 @@ return array(
'Cron\\MinutesField' => $vendorDir . '/dragonmantank/cron-expression/src/Cron/MinutesField.php', 'Cron\\MinutesField' => $vendorDir . '/dragonmantank/cron-expression/src/Cron/MinutesField.php',
'Cron\\MonthField' => $vendorDir . '/dragonmantank/cron-expression/src/Cron/MonthField.php', 'Cron\\MonthField' => $vendorDir . '/dragonmantank/cron-expression/src/Cron/MonthField.php',
'Database\\Factories\\UserFactory' => $baseDir . '/database/factories/UserFactory.php', 'Database\\Factories\\UserFactory' => $baseDir . '/database/factories/UserFactory.php',
'Database\\Seeders\\AdminUserSeeder' => $baseDir . '/database/seeders/AdminUserSeeder.php',
'Database\\Seeders\\BookingSeeder' => $baseDir . '/database/seeders/BookingSeeder.php',
'Database\\Seeders\\DatabaseSeeder' => $baseDir . '/database/seeders/DatabaseSeeder.php', 'Database\\Seeders\\DatabaseSeeder' => $baseDir . '/database/seeders/DatabaseSeeder.php',
'Database\\Seeders\\ParkingLotSeeder' => $baseDir . '/database/seeders/ParkingLotSeeder.php',
'DateError' => $vendorDir . '/symfony/polyfill-php83/Resources/stubs/DateError.php', 'DateError' => $vendorDir . '/symfony/polyfill-php83/Resources/stubs/DateError.php',
'DateException' => $vendorDir . '/symfony/polyfill-php83/Resources/stubs/DateException.php', 'DateException' => $vendorDir . '/symfony/polyfill-php83/Resources/stubs/DateException.php',
'DateInvalidOperationException' => $vendorDir . '/symfony/polyfill-php83/Resources/stubs/DateInvalidOperationException.php', 'DateInvalidOperationException' => $vendorDir . '/symfony/polyfill-php83/Resources/stubs/DateInvalidOperationException.php',

View File

@ -536,7 +536,28 @@ class ComposerStaticInit53b5d56b3b7e3cbac1713e68c8850f6c
); );
public static $classMap = array ( public static $classMap = array (
'App\\Http\\Controllers\\Admin\\AdminOperatorController' => __DIR__ . '/../..' . '/app/Http/Controllers/Admin/AdminOperatorController.php',
'App\\Http\\Controllers\\Admin\\BookingController' => __DIR__ . '/../..' . '/app/Http/Controllers/Admin/BookingController.php',
'App\\Http\\Controllers\\Admin\\DashboardController' => __DIR__ . '/../..' . '/app/Http/Controllers/Admin/DashboardController.php',
'App\\Http\\Controllers\\Admin\\ParkingLotController' => __DIR__ . '/../..' . '/app/Http/Controllers/Admin/ParkingLotController.php',
'App\\Http\\Controllers\\Api\\BookingController' => __DIR__ . '/../..' . '/app/Http/Controllers/Api/BookingController.php',
'App\\Http\\Controllers\\Api\\CarRegistryController' => __DIR__ . '/../..' . '/app/Http/Controllers/Api/CarRegistryController.php',
'App\\Http\\Controllers\\Api\\ParkingLotController' => __DIR__ . '/../..' . '/app/Http/Controllers/Api/ParkingLotController.php',
'App\\Http\\Controllers\\Controller' => __DIR__ . '/../..' . '/app/Http/Controllers/Controller.php', 'App\\Http\\Controllers\\Controller' => __DIR__ . '/../..' . '/app/Http/Controllers/Controller.php',
'App\\Http\\Controllers\\Operator\\OperatorController' => __DIR__ . '/../..' . '/app/Http/Controllers/Operator/OperatorController.php',
'App\\Http\\Controllers\\ParkingController' => __DIR__ . '/../..' . '/app/Http/Controllers/ParkingController.php',
'App\\Http\\Controllers\\ProfileController' => __DIR__ . '/../..' . '/app/Http/Controllers/ProfileController.php',
'App\\Http\\Middleware\\EnsureAdmin' => __DIR__ . '/../..' . '/app/Http/Middleware/EnsureAdmin.php',
'App\\Http\\Middleware\\EnsureOperator' => __DIR__ . '/../..' . '/app/Http/Middleware/EnsureOperator.php',
'App\\Http\\Requests\\StoreBookingRequest' => __DIR__ . '/../..' . '/app/Http/Requests/StoreBookingRequest.php',
'App\\Http\\Requests\\StoreOperatorCheckInRequest' => __DIR__ . '/../..' . '/app/Http/Requests/StoreOperatorCheckInRequest.php',
'App\\Http\\Requests\\StoreParkingLotRequest' => __DIR__ . '/../..' . '/app/Http/Requests/StoreParkingLotRequest.php',
'App\\Http\\Resources\\BookingResource' => __DIR__ . '/../..' . '/app/Http/Resources/BookingResource.php',
'App\\Http\\Resources\\CarRegistryResource' => __DIR__ . '/../..' . '/app/Http/Resources/CarRegistryResource.php',
'App\\Http\\Resources\\ParkingLotResource' => __DIR__ . '/../..' . '/app/Http/Resources/ParkingLotResource.php',
'App\\Models\\Booking' => __DIR__ . '/../..' . '/app/Models/Booking.php',
'App\\Models\\CarRegistry' => __DIR__ . '/../..' . '/app/Models/CarRegistry.php',
'App\\Models\\ParkingLot' => __DIR__ . '/../..' . '/app/Models/ParkingLot.php',
'App\\Models\\User' => __DIR__ . '/../..' . '/app/Models/User.php', 'App\\Models\\User' => __DIR__ . '/../..' . '/app/Models/User.php',
'App\\Providers\\AppServiceProvider' => __DIR__ . '/../..' . '/app/Providers/AppServiceProvider.php', 'App\\Providers\\AppServiceProvider' => __DIR__ . '/../..' . '/app/Providers/AppServiceProvider.php',
'Attribute' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/Attribute.php', 'Attribute' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/Attribute.php',
@ -662,7 +683,10 @@ class ComposerStaticInit53b5d56b3b7e3cbac1713e68c8850f6c
'Cron\\MinutesField' => __DIR__ . '/..' . '/dragonmantank/cron-expression/src/Cron/MinutesField.php', 'Cron\\MinutesField' => __DIR__ . '/..' . '/dragonmantank/cron-expression/src/Cron/MinutesField.php',
'Cron\\MonthField' => __DIR__ . '/..' . '/dragonmantank/cron-expression/src/Cron/MonthField.php', 'Cron\\MonthField' => __DIR__ . '/..' . '/dragonmantank/cron-expression/src/Cron/MonthField.php',
'Database\\Factories\\UserFactory' => __DIR__ . '/../..' . '/database/factories/UserFactory.php', 'Database\\Factories\\UserFactory' => __DIR__ . '/../..' . '/database/factories/UserFactory.php',
'Database\\Seeders\\AdminUserSeeder' => __DIR__ . '/../..' . '/database/seeders/AdminUserSeeder.php',
'Database\\Seeders\\BookingSeeder' => __DIR__ . '/../..' . '/database/seeders/BookingSeeder.php',
'Database\\Seeders\\DatabaseSeeder' => __DIR__ . '/../..' . '/database/seeders/DatabaseSeeder.php', 'Database\\Seeders\\DatabaseSeeder' => __DIR__ . '/../..' . '/database/seeders/DatabaseSeeder.php',
'Database\\Seeders\\ParkingLotSeeder' => __DIR__ . '/../..' . '/database/seeders/ParkingLotSeeder.php',
'DateError' => __DIR__ . '/..' . '/symfony/polyfill-php83/Resources/stubs/DateError.php', 'DateError' => __DIR__ . '/..' . '/symfony/polyfill-php83/Resources/stubs/DateError.php',
'DateException' => __DIR__ . '/..' . '/symfony/polyfill-php83/Resources/stubs/DateException.php', 'DateException' => __DIR__ . '/..' . '/symfony/polyfill-php83/Resources/stubs/DateException.php',
'DateInvalidOperationException' => __DIR__ . '/..' . '/symfony/polyfill-php83/Resources/stubs/DateInvalidOperationException.php', 'DateInvalidOperationException' => __DIR__ . '/..' . '/symfony/polyfill-php83/Resources/stubs/DateInvalidOperationException.php',

View File

@ -1,9 +1,9 @@
<?php return array( <?php return array(
'root' => array( 'root' => array(
'name' => 'laravel/laravel', 'name' => 'laravel/laravel',
'pretty_version' => '1.0.0+no-version-set', 'pretty_version' => 'dev-main',
'version' => '1.0.0.0', 'version' => 'dev-main',
'reference' => null, 'reference' => 'b03c1d4619de986420534fe46341f4da4d088f76',
'type' => 'project', 'type' => 'project',
'install_path' => __DIR__ . '/../../', 'install_path' => __DIR__ . '/../../',
'aliases' => array(), 'aliases' => array(),
@ -398,9 +398,9 @@
'dev_requirement' => false, 'dev_requirement' => false,
), ),
'laravel/laravel' => array( 'laravel/laravel' => array(
'pretty_version' => '1.0.0+no-version-set', 'pretty_version' => 'dev-main',
'version' => '1.0.0.0', 'version' => 'dev-main',
'reference' => null, 'reference' => 'b03c1d4619de986420534fe46341f4da4d088f76',
'type' => 'project', 'type' => 'project',
'install_path' => __DIR__ . '/../../', 'install_path' => __DIR__ . '/../../',
'aliases' => array(), 'aliases' => array(),