Compare commits
No commits in common. "feature/multi-lot-operator-assignment" and "main" have entirely different histories.
feature/mu
...
main
@ -20,22 +20,7 @@
|
||||
"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(php:*)",
|
||||
"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 *)"
|
||||
"Bash(grep -v \"^$\")"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
48
.env
48
.env
@ -1,23 +1,44 @@
|
||||
APP_NAME="Damascus Parking"
|
||||
APP_NAME="Smart Car Park"
|
||||
APP_ENV=local
|
||||
APP_KEY=base64:gC4TfmQLkKdWmH/vRQ9KY//WgH0w8+RhkVuswqCbgxo=
|
||||
APP_KEY=base64:92pMpQ2HV7icz1lfUZUQfUguPfUdqYUPVU+aWdYDstY=
|
||||
APP_DEBUG=true
|
||||
APP_TIMEZONE=Asia/Damascus
|
||||
APP_URL=http://localhost:8000
|
||||
APP_URL=http://localhost
|
||||
|
||||
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_STACK=single
|
||||
LOG_DEPRECATIONS_CHANNEL=null
|
||||
LOG_LEVEL=debug
|
||||
|
||||
DB_CONNECTION=sqlite
|
||||
DB_DATABASE=/var/www/scp-syria/database/database.sqlite
|
||||
# DB_HOST=127.0.0.1
|
||||
# DB_PORT=3306
|
||||
# DB_DATABASE=laravel
|
||||
# DB_USERNAME=root
|
||||
# DB_PASSWORD=
|
||||
|
||||
BROADCAST_CONNECTION=log
|
||||
CACHE_STORE=database
|
||||
FILESYSTEM_DISK=local
|
||||
QUEUE_CONNECTION=sync
|
||||
SESSION_DRIVER=database
|
||||
SESSION_LIFETIME=120
|
||||
SESSION_ENCRYPT=false
|
||||
SESSION_PATH=/
|
||||
SESSION_DOMAIN=null
|
||||
|
||||
BROADCAST_CONNECTION=log
|
||||
FILESYSTEM_DISK=local
|
||||
QUEUE_CONNECTION=database
|
||||
|
||||
CACHE_STORE=database
|
||||
# CACHE_PREFIX=
|
||||
|
||||
MEMCACHED_HOST=127.0.0.1
|
||||
|
||||
@ -26,12 +47,12 @@ REDIS_HOST=127.0.0.1
|
||||
REDIS_PASSWORD=null
|
||||
REDIS_PORT=6379
|
||||
|
||||
MAIL_MAILER=smtp
|
||||
MAIL_HOST=mailpit
|
||||
MAIL_PORT=1025
|
||||
MAIL_MAILER=log
|
||||
MAIL_SCHEME=null
|
||||
MAIL_HOST=127.0.0.1
|
||||
MAIL_PORT=2525
|
||||
MAIL_USERNAME=null
|
||||
MAIL_PASSWORD=null
|
||||
MAIL_ENCRYPTION=null
|
||||
MAIL_FROM_ADDRESS="hello@example.com"
|
||||
MAIL_FROM_NAME="${APP_NAME}"
|
||||
|
||||
@ -42,4 +63,3 @@ AWS_BUCKET=
|
||||
AWS_USE_PATH_STYLE_ENDPOINT=false
|
||||
|
||||
VITE_APP_NAME="${APP_NAME}"
|
||||
|
||||
|
||||
43
CLAUDE.md
43
CLAUDE.md
@ -2,28 +2,6 @@
|
||||
|
||||
## 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
|
||||
|
||||
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.
|
||||
@ -158,14 +136,12 @@ resources/
|
||||
|
||||
| Role | Middleware | Access |
|
||||
|------------|-----------------|---------------------------------------------|
|
||||
| `admin` | `EnsureAdmin` | Full admin dashboard, parking lot CRUD, all bookings, operator management |
|
||||
| `operator` | `EnsureOperator`| Vehicle check-in/check-out, stats dashboard — restricted to assigned lots only |
|
||||
| `admin` | `EnsureAdmin` | Full admin dashboard, parking lot CRUD, all bookings |
|
||||
| `operator` | `EnsureOperator`| Vehicle check-in/check-out, active bookings for assigned lot |
|
||||
| `user` | (auth only) | Public search, create bookings via API |
|
||||
|
||||
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
|
||||
@ -178,15 +154,6 @@ Both middleware classes return Arabic 403 messages on unauthorized access.
|
||||
| email | string unique | |
|
||||
| password | string | bcrypt |
|
||||
| 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`
|
||||
| Column | Type | Notes |
|
||||
@ -264,9 +231,7 @@ Check-in requires: `parking_lot_id`, `vehicle_plate`, `duration_hours` (0.25–2
|
||||
| `POST /admin/parking-lots/{id}/toggle` | admin | Toggle active status |
|
||||
| `GET /admin/bookings/active` | admin | Active bookings |
|
||||
| `POST /admin/bookings/{id}/complete` | admin | Mark booking complete |
|
||||
| `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) |
|
||||
| `GET /operator/dashboard` | operator | Operator view |
|
||||
| `POST /operator/check-in` | operator | Vehicle entry |
|
||||
| `POST /operator/{booking}/checkout` | operator | Vehicle exit + fee |
|
||||
| `GET /admin/stats` | admin | JSON stats (AJAX) |
|
||||
@ -396,9 +361,7 @@ php artisan config:clear && php artisan test
|
||||
## Notes & Known Patterns
|
||||
|
||||
- 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.
|
||||
- **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/`.
|
||||
- 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.
|
||||
|
||||
@ -14,7 +14,7 @@ class AdminOperatorController extends Controller
|
||||
public function index()
|
||||
{
|
||||
$operators = User::where('role', 'operator')
|
||||
->with('assignedLots')
|
||||
->with('assignedLot')
|
||||
->orderBy('name')
|
||||
->get();
|
||||
|
||||
@ -26,26 +26,24 @@ class AdminOperatorController extends Controller
|
||||
public function store(Request $request)
|
||||
{
|
||||
$data = $request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'email' => 'required|email|unique:users,email',
|
||||
'phone' => 'nullable|string|max:20',
|
||||
'password' => ['required', Password::min(8)],
|
||||
'lot_ids' => 'nullable|array',
|
||||
'lot_ids.*'=> 'exists:parking_lots,id',
|
||||
'name' => 'required|string|max:255',
|
||||
'email' => 'required|email|unique:users,email',
|
||||
'phone' => 'nullable|string|max:20',
|
||||
'password' => ['required', Password::min(8)],
|
||||
'parking_lot_id' => 'nullable|exists:parking_lots,id',
|
||||
], [
|
||||
'email.unique' => 'هذا البريد الإلكتروني مستخدم بالفعل.',
|
||||
]);
|
||||
|
||||
$operator = User::create([
|
||||
'name' => $data['name'],
|
||||
'email' => $data['email'],
|
||||
'phone' => $data['phone'] ?? null,
|
||||
'password' => Hash::make($data['password']),
|
||||
'role' => 'operator',
|
||||
User::create([
|
||||
'name' => $data['name'],
|
||||
'email' => $data['email'],
|
||||
'phone' => $data['phone'] ?? null,
|
||||
'password' => Hash::make($data['password']),
|
||||
'role' => 'operator',
|
||||
'parking_lot_id' => $data['parking_lot_id'] ?? null,
|
||||
]);
|
||||
|
||||
$operator->assignedLots()->sync($data['lot_ids'] ?? []);
|
||||
|
||||
return response()->json(['success' => true, 'message' => 'تم إنشاء حساب المشغّل بنجاح.']);
|
||||
}
|
||||
|
||||
@ -56,20 +54,20 @@ class AdminOperatorController extends Controller
|
||||
}
|
||||
|
||||
$data = $request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'email' => 'required|email|unique:users,email,' . $operator->id,
|
||||
'phone' => 'nullable|string|max:20',
|
||||
'password' => ['nullable', Password::min(8)],
|
||||
'lot_ids' => 'nullable|array',
|
||||
'lot_ids.*'=> 'exists:parking_lots,id',
|
||||
'name' => 'required|string|max:255',
|
||||
'email' => 'required|email|unique:users,email,' . $operator->id,
|
||||
'phone' => 'nullable|string|max:20',
|
||||
'password' => ['nullable', Password::min(8)],
|
||||
'parking_lot_id' => 'nullable|exists:parking_lots,id',
|
||||
], [
|
||||
'email.unique' => 'هذا البريد الإلكتروني مستخدم بالفعل.',
|
||||
]);
|
||||
|
||||
$updates = [
|
||||
'name' => $data['name'],
|
||||
'email' => $data['email'],
|
||||
'phone' => $data['phone'] ?? null,
|
||||
'name' => $data['name'],
|
||||
'email' => $data['email'],
|
||||
'phone' => $data['phone'] ?? null,
|
||||
'parking_lot_id' => $data['parking_lot_id'] ?? null,
|
||||
];
|
||||
|
||||
if (!empty($data['password'])) {
|
||||
@ -77,7 +75,6 @@ class AdminOperatorController extends Controller
|
||||
}
|
||||
|
||||
$operator->update($updates);
|
||||
$operator->assignedLots()->sync($data['lot_ids'] ?? []);
|
||||
|
||||
return response()->json(['success' => true, 'message' => 'تم تحديث بيانات المشغّل.']);
|
||||
}
|
||||
|
||||
@ -17,18 +17,10 @@ class OperatorController extends Controller
|
||||
|
||||
public function dashboard(Request $request): \Illuminate\View\View
|
||||
{
|
||||
$user = Auth::user();
|
||||
$assignedLotIds = $user->assignedLots->pluck('id')->toArray();
|
||||
$user = Auth::user();
|
||||
$assignedLotId = $user->parking_lot_id; // null = no restriction (e.g. admin)
|
||||
|
||||
// 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();
|
||||
$rawLots = ParkingLot::active()->withStatus()->get();
|
||||
|
||||
$parkingLots = $rawLots->map(fn($lot) => [
|
||||
'id' => $lot->id,
|
||||
@ -42,22 +34,20 @@ class OperatorController extends Controller
|
||||
'lat' => (float) $lot->latitude,
|
||||
'lng' => (float) $lot->longitude,
|
||||
'image' => $lot->image ? Storage::url($lot->image) : null,
|
||||
'locked' => false,
|
||||
'locked' => $assignedLotId !== null && $lot->id !== $assignedLotId,
|
||||
])->values();
|
||||
|
||||
// If operator has an assigned lot, force that lot
|
||||
$selectedLotId = $request->get('lot_id');
|
||||
|
||||
// Reject attempts to access a lot not in the operator's assigned list
|
||||
if ($selectedLotId && !in_array((int) $selectedLotId, $assignedLotIds)) {
|
||||
if (!empty($assignedLotIds)) {
|
||||
return redirect()->route('operator.dashboard', ['lot_id' => $assignedLotIds[0]]);
|
||||
if ($assignedLotId !== null) {
|
||||
// Reject any attempt to view a different lot
|
||||
if ($selectedLotId && (int) $selectedLotId !== $assignedLotId) {
|
||||
return redirect()->route('operator.dashboard', ['lot_id' => $assignedLotId]);
|
||||
}
|
||||
// Auto-select assigned lot if nothing selected
|
||||
if (!$selectedLotId) {
|
||||
$selectedLotId = $assignedLotId;
|
||||
}
|
||||
return redirect()->route('operator.dashboard');
|
||||
}
|
||||
|
||||
// Auto-select when only one lot is assigned
|
||||
if (!$selectedLotId && count($assignedLotIds) === 1) {
|
||||
$selectedLotId = $assignedLotIds[0];
|
||||
}
|
||||
|
||||
$selectedLot = null;
|
||||
@ -83,177 +73,10 @@ class OperatorController extends Controller
|
||||
}
|
||||
|
||||
return view('operator.dashboard', compact(
|
||||
'parkingLots', 'selectedLot', 'activeCars', 'reservations', 'selectedLotId', 'assignedLotIds'
|
||||
'parkingLots', 'selectedLot', 'activeCars', 'reservations', 'selectedLotId', 'assignedLotId'
|
||||
));
|
||||
}
|
||||
|
||||
// ── 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 0–23)
|
||||
$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 ────────────────────────────────────────────────────────
|
||||
|
||||
public function checkIn(Request $request): JsonResponse
|
||||
|
||||
@ -24,6 +24,7 @@ class User extends Authenticatable
|
||||
'phone',
|
||||
'password',
|
||||
'role',
|
||||
'parking_lot_id',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
@ -55,8 +56,8 @@ class User extends Authenticatable
|
||||
];
|
||||
}
|
||||
|
||||
public function assignedLots(): \Illuminate\Database\Eloquent\Relations\BelongsToMany
|
||||
public function assignedLot(): \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
{
|
||||
return $this->belongsToMany(ParkingLot::class, 'operator_parking_lot');
|
||||
return $this->belongsTo(ParkingLot::class, 'parking_lot_id');
|
||||
}
|
||||
}
|
||||
|
||||
0
bootstrap/cache/.gitignore
vendored
Executable file → Normal file
0
bootstrap/cache/.gitignore
vendored
Executable file → Normal file
@ -1,40 +0,0 @@
|
||||
<?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
@ -1,6 +1,6 @@
|
||||
{
|
||||
"resources/css/app.scss": {
|
||||
"file": "assets/app-Layout02.css",
|
||||
"file": "assets/app-BtJpbOwL.css",
|
||||
"src": "resources/css/app.scss",
|
||||
"isEntry": true,
|
||||
"name": "app",
|
||||
@ -9,7 +9,7 @@
|
||||
]
|
||||
},
|
||||
"resources/js/app.js": {
|
||||
"file": "assets/app-D0Ms7V4S2.js",
|
||||
"file": "assets/app-D0Ms7V4S.js",
|
||||
"name": "app",
|
||||
"src": "resources/js/app.js",
|
||||
"isEntry": true
|
||||
|
||||
@ -74,17 +74,11 @@ body {
|
||||
background-color: var(--app-bg);
|
||||
}
|
||||
|
||||
// ─── App Layout (Topbar top, then sidebar + content row below) ───────────────
|
||||
// ─── App Layout (Sidebar + Main) ──────────────────────────────────────────────
|
||||
.app-layout {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.app-inner {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
// In RTL flex-direction:row, the first child (sidebar) appears on the RIGHT
|
||||
}
|
||||
|
||||
// ─── Sidebar ──────────────────────────────────────────────────────────────────
|
||||
@ -94,9 +88,9 @@ body {
|
||||
background: var(--sidebar-bg);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: calc(100vh - var(--topbar-height));
|
||||
height: 100vh;
|
||||
position: sticky;
|
||||
top: var(--topbar-height);
|
||||
top: 0;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: none;
|
||||
transition: transform .3s ease;
|
||||
@ -228,12 +222,11 @@ body {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
// ─── Topbar (unified — used on all pages) ─────────────────────────────────────
|
||||
// ─── Topbar ───────────────────────────────────────────────────────────────────
|
||||
.app-topbar {
|
||||
height: var(--topbar-height);
|
||||
background: var(--sidebar-bg);
|
||||
border-bottom: 1px solid rgba(255,255,255,.06);
|
||||
box-shadow: 0 2px 12px rgba(0,0,0,.15);
|
||||
background: var(--topbar-bg);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
padding: 0 1.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@ -241,12 +234,12 @@ body {
|
||||
gap: 1rem;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 200;
|
||||
z-index: 100;
|
||||
|
||||
.topbar-title {
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
color: #f8fafc;
|
||||
color: var(--text-primary);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@ -262,13 +255,13 @@ body {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: .375rem .5rem;
|
||||
color: rgba(255,255,255,.65);
|
||||
color: var(--text-muted);
|
||||
border-radius: .375rem;
|
||||
cursor: pointer;
|
||||
display: none;
|
||||
font-size: 1.25rem;
|
||||
|
||||
&:hover { background: rgba(255,255,255,.08); color: #fff; }
|
||||
&:hover { background: #f1f5f9; color: var(--text-primary); }
|
||||
}
|
||||
|
||||
.sidebar-overlay {
|
||||
@ -290,10 +283,13 @@ body {
|
||||
@media (max-width: 991.98px) {
|
||||
.app-sidebar {
|
||||
position: fixed;
|
||||
top: var(--topbar-height);
|
||||
inset-inline-start: 0; // anchors right edge to viewport right in RTL
|
||||
height: calc(100% - var(--topbar-height));
|
||||
transform: translateX(100%); // hides off-screen to the right in RTL
|
||||
top: 0;
|
||||
inset-inline-end: 0; // right in RTL
|
||||
height: 100%;
|
||||
transform: translateX(calc(-1 * var(--sidebar-width)));
|
||||
// In RTL, translateX negative moves LEFT (off screen)
|
||||
// We need to hide it to the right side
|
||||
transform: translateX(100%);
|
||||
|
||||
&.is-open {
|
||||
transform: translateX(0);
|
||||
@ -557,6 +553,14 @@ body {
|
||||
}
|
||||
|
||||
// ─── 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 {
|
||||
transition: all .18s ease;
|
||||
@ -601,6 +605,11 @@ body {
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.app-topbar {
|
||||
backdrop-filter: blur(8px);
|
||||
background: rgba(255,255,255,.96);
|
||||
}
|
||||
|
||||
.stat-card:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 8px 28px rgba(0,0,0,.09) !important;
|
||||
@ -681,8 +690,6 @@ body {
|
||||
/* ── Admin / Operator layout ────────────────────────────────────────── */
|
||||
.sidebar-toggle { display: none !important; }
|
||||
.app-layout { display: block; }
|
||||
.app-inner { display: block; }
|
||||
.app-sidebar { display: none !important; }
|
||||
|
||||
.app-body {
|
||||
padding-bottom: var(--mobile-nav-h);
|
||||
@ -692,6 +699,8 @@ body {
|
||||
.app-topbar {
|
||||
height: 54px;
|
||||
padding: 0 1rem;
|
||||
backdrop-filter: none;
|
||||
background: #ffffff;
|
||||
|
||||
.topbar-title { font-size: .9rem; }
|
||||
}
|
||||
@ -705,7 +714,7 @@ body {
|
||||
}
|
||||
|
||||
/* ── Public page ────────────────────────────────────────────────────── */
|
||||
.app-topbar { padding: .625rem 1rem; }
|
||||
.public-header { padding: .625rem 0; }
|
||||
.mob-hero-compact { padding: .875rem 0 1rem !important; }
|
||||
|
||||
/* Section switching — hide inactive section */
|
||||
|
||||
@ -396,12 +396,10 @@
|
||||
{{-- ── STEP 2A : receipt + payment ────────────────────────── --}}
|
||||
<div id="endStep2Pay" style="display:none;" class="p-4">
|
||||
|
||||
<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;">
|
||||
<i class="bi bi-arrow-left me-1"></i>رجوع
|
||||
</button>
|
||||
</div>
|
||||
<button class="btn btn-sm mb-3 fw-600" onclick="endBack()"
|
||||
style="background:#f1f5f9;color:#475569;border:none;border-radius:.5rem;font-family:'Cairo',sans-serif;">
|
||||
<i class="bi bi-arrow-right me-1"></i>رجوع
|
||||
</button>
|
||||
|
||||
{{-- Times --}}
|
||||
<div class="row g-2 mb-3">
|
||||
@ -471,12 +469,10 @@
|
||||
{{-- ── STEP 2B : force close ───────────────────────────────── --}}
|
||||
<div id="endStep2Force" style="display:none;" class="p-4">
|
||||
|
||||
<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;">
|
||||
<i class="bi bi-arrow-left me-1"></i>رجوع
|
||||
</button>
|
||||
</div>
|
||||
<button class="btn btn-sm mb-3 fw-600" onclick="endBack()"
|
||||
style="background:#f1f5f9;color:#475569;border:none;border-radius:.5rem;font-family:'Cairo',sans-serif;">
|
||||
<i class="bi bi-arrow-right me-1"></i>رجوع
|
||||
</button>
|
||||
|
||||
<div class="text-center py-2 mb-3">
|
||||
<div style="font-size:2.8rem;margin-bottom:.75rem;">⚠️</div>
|
||||
@ -613,12 +609,12 @@ async function endChoose(type) {
|
||||
const d = data.data;
|
||||
document.getElementById('payRcptEntry').textContent = d.entry_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 => `
|
||||
<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 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('');
|
||||
document.getElementById('payRcptBreakdown').innerHTML =
|
||||
rows || '<p class="text-xs text-center" style="color:#94a3b8;">لا تفاصيل</p>';
|
||||
|
||||
@ -274,7 +274,7 @@ async function loadStats() {
|
||||
document.getElementById('total-bookings').textContent = data.total_bookings ?? 0;
|
||||
document.getElementById('active-bookings').textContent = data.active_bookings ?? 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;
|
||||
} catch {}
|
||||
}
|
||||
|
||||
@ -22,15 +22,6 @@
|
||||
padding:.25em .75em; border-radius:20px; font-size:.75rem; font-weight:600;
|
||||
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>
|
||||
@endsection
|
||||
|
||||
@ -70,15 +61,11 @@
|
||||
<td class="py-3" style="color:#475569;">{{ $op->email }}</td>
|
||||
<td class="py-3" style="color:#475569;">{{ $op->phone ?? '—' }}</td>
|
||||
<td class="py-3">
|
||||
@if($op->assignedLots->isNotEmpty())
|
||||
<div class="d-flex flex-wrap gap-1">
|
||||
@foreach($op->assignedLots as $assignedLot)
|
||||
<span class="lot-pill">
|
||||
<i class="bi bi-buildings"></i>
|
||||
{{ $assignedLot->name }}
|
||||
</span>
|
||||
@endforeach
|
||||
</div>
|
||||
@if($op->assignedLot)
|
||||
<span class="lot-pill">
|
||||
<i class="bi bi-buildings"></i>
|
||||
{{ $op->assignedLot->name }}
|
||||
</span>
|
||||
@else
|
||||
<span class="no-lot-pill">
|
||||
<i class="bi bi-dash-circle"></i>
|
||||
@ -94,7 +81,7 @@
|
||||
data-name="{{ $op->name }}"
|
||||
data-email="{{ $op->email }}"
|
||||
data-phone="{{ $op->phone ?? '' }}"
|
||||
data-lots="{{ json_encode($op->assignedLots->pluck('id')) }}">
|
||||
data-lot="{{ $op->parking_lot_id ?? '' }}">
|
||||
<i class="bi bi-pencil me-1"></i>تعديل
|
||||
</button>
|
||||
<button class="btn btn-sm fw-600 btn-delete-op"
|
||||
@ -147,17 +134,13 @@
|
||||
<input type="password" id="createPassword" class="form-control" placeholder="8 أحرف على الأقل" dir="ltr">
|
||||
</div>
|
||||
<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>
|
||||
<div class="lot-checkbox-list" id="createLotList">
|
||||
@forelse($lots as $lot)
|
||||
<label class="lot-checkbox-item">
|
||||
<input type="checkbox" class="create-lot-cb" value="{{ $lot->id }}">
|
||||
<span>{{ $lot->name }}</span>
|
||||
</label>
|
||||
@empty
|
||||
<p class="text-center mb-0 py-2" style="color:#94a3b8;font-size:.8rem;">لا توجد مواقف</p>
|
||||
@endforelse
|
||||
</div>
|
||||
<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">
|
||||
<option value="">— بدون تخصيص —</option>
|
||||
@foreach($lots as $lot)
|
||||
<option value="{{ $lot->id }}">{{ $lot->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer border-0 pt-1">
|
||||
@ -203,17 +186,13 @@
|
||||
<input type="password" id="editPassword" class="form-control" placeholder="8 أحرف على الأقل" dir="ltr">
|
||||
</div>
|
||||
<div class="mb-1">
|
||||
<label class="form-label fw-600" style="font-size:.875rem;color:#374151;">المواقف المخصصة</label>
|
||||
<div class="lot-checkbox-list" id="editLotList">
|
||||
@forelse($lots as $lot)
|
||||
<label class="lot-checkbox-item">
|
||||
<input type="checkbox" class="edit-lot-cb" value="{{ $lot->id }}">
|
||||
<span>{{ $lot->name }}</span>
|
||||
</label>
|
||||
@empty
|
||||
<p class="text-center mb-0 py-2" style="color:#94a3b8;font-size:.8rem;">لا توجد مواقف</p>
|
||||
@endforelse
|
||||
</div>
|
||||
<label class="form-label fw-600" style="font-size:.875rem;color:#374151;">الموقف المخصص</label>
|
||||
<select id="editLot" class="form-select">
|
||||
<option value="">— بدون تخصيص —</option>
|
||||
@foreach($lots as $lot)
|
||||
<option value="{{ $lot->id }}">{{ $lot->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer border-0 pt-1">
|
||||
@ -292,7 +271,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
// ── Open create ───────────────────────────────────────────────────────────
|
||||
document.getElementById('btnOpenCreate').addEventListener('click', function () {
|
||||
['createName','createEmail','createPhone','createPassword'].forEach(id => document.getElementById(id).value = '');
|
||||
document.querySelectorAll('.create-lot-cb').forEach(cb => cb.checked = false);
|
||||
document.getElementById('createLot').value = '';
|
||||
document.getElementById('createError').classList.add('d-none');
|
||||
modal('createModal').show();
|
||||
});
|
||||
@ -305,10 +284,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
document.getElementById('editEmail').value = this.dataset.email;
|
||||
document.getElementById('editPhone').value = this.dataset.phone || '';
|
||||
document.getElementById('editPassword').value = '';
|
||||
const assignedIds = JSON.parse(this.dataset.lots || '[]');
|
||||
document.querySelectorAll('.edit-lot-cb').forEach(cb => {
|
||||
cb.checked = assignedIds.includes(parseInt(cb.value));
|
||||
});
|
||||
document.getElementById('editLot').value = this.dataset.lot || '';
|
||||
document.getElementById('editError').classList.add('d-none');
|
||||
modal('editModal').show();
|
||||
});
|
||||
@ -333,16 +309,15 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
spinner.classList.remove('d-none');
|
||||
errEl.classList.add('d-none');
|
||||
try {
|
||||
const createLotIds = [...document.querySelectorAll('.create-lot-cb:checked')].map(cb => parseInt(cb.value));
|
||||
const res = await fetch('{{ route("admin.operators.store") }}', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': csrf },
|
||||
body: JSON.stringify({
|
||||
name: document.getElementById('createName').value.trim(),
|
||||
email: document.getElementById('createEmail').value.trim(),
|
||||
phone: document.getElementById('createPhone').value.trim() || null,
|
||||
password: document.getElementById('createPassword').value,
|
||||
lot_ids: createLotIds,
|
||||
name: document.getElementById('createName').value.trim(),
|
||||
email: document.getElementById('createEmail').value.trim(),
|
||||
phone: document.getElementById('createPhone').value.trim() || null,
|
||||
password: document.getElementById('createPassword').value,
|
||||
parking_lot_id: document.getElementById('createLot').value || null,
|
||||
}),
|
||||
});
|
||||
const json = await res.json();
|
||||
@ -368,16 +343,15 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
spinner.classList.remove('d-none');
|
||||
errEl.classList.add('d-none');
|
||||
try {
|
||||
const editLotIds = [...document.querySelectorAll('.edit-lot-cb:checked')].map(cb => parseInt(cb.value));
|
||||
const res = await fetch(`/admin/operators/${id}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': csrf },
|
||||
body: JSON.stringify({
|
||||
name: document.getElementById('editName').value.trim(),
|
||||
email: document.getElementById('editEmail').value.trim(),
|
||||
phone: document.getElementById('editPhone').value.trim() || null,
|
||||
password: document.getElementById('editPassword').value || null,
|
||||
lot_ids: editLotIds,
|
||||
name: document.getElementById('editName').value.trim(),
|
||||
email: document.getElementById('editEmail').value.trim(),
|
||||
phone: document.getElementById('editPhone').value.trim() || null,
|
||||
password: document.getElementById('editPassword').value || null,
|
||||
parking_lot_id: document.getElementById('editLot').value || null,
|
||||
}),
|
||||
});
|
||||
const json = await res.json();
|
||||
|
||||
@ -209,7 +209,7 @@ $gradients = [
|
||||
</span>
|
||||
<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>
|
||||
<strong>{{ number_format($lot->price_per_hour, 0) }}</strong> ليرة سورية/س
|
||||
<strong>{{ number_format($lot->price_per_hour, 0) }}</strong> ر.س/س
|
||||
@if(!empty($lot->pricing_rules))
|
||||
<span style="font-size:.6rem;color:#f59e0b;"> (مخصص)</span>
|
||||
@endif
|
||||
@ -385,7 +385,7 @@ $gradients = [
|
||||
<input type="number" id="p_base" class="form-control"
|
||||
step="0.01" min="0" placeholder="0.00"
|
||||
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>
|
||||
<p class="text-xs mt-1 mb-0" style="color:#94a3b8;">
|
||||
الأيام التي لا تحمل سعراً مخصصاً ستستخدم هذا السعر تلقائياً.
|
||||
@ -425,7 +425,7 @@ $gradients = [
|
||||
step="0.01" min="0"
|
||||
placeholder="مثل السعر الأساسي"
|
||||
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 style="width:70px;text-align:center;">
|
||||
|
||||
@ -72,5 +72,16 @@
|
||||
إنشاء حساب جديد
|
||||
</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
|
||||
|
||||
@ -15,69 +15,33 @@
|
||||
/* 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 { 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>
|
||||
</head>
|
||||
<body style="background:#f1f5f9;font-family:'Cairo',sans-serif;">
|
||||
|
||||
{{-- ══ HEADER ══════════════════════════════════════════════════════════════ --}}
|
||||
@include('partials.topbar')
|
||||
<header class="public-header">
|
||||
<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 ═════════════════════════════════════════════════════════ --}}
|
||||
<div class="mob-hero-compact" style="background:linear-gradient(135deg,#0f172a 0%,#1e3a5f 100%);padding:2.5rem 0 3rem;">
|
||||
<div class="container-fluid text-center">
|
||||
<div class="container text-center">
|
||||
<h1 class="fw-800 mb-2 d-none d-md-block" style="color:#f8fafc;font-size:clamp(1.4rem,4vw,2rem);">
|
||||
ابحث عن موقف سيارات في دمشق
|
||||
</h1>
|
||||
@ -108,7 +72,7 @@
|
||||
</div>
|
||||
|
||||
{{-- ══ MAIN CONTENT ════════════════════════════════════════════════════════ --}}
|
||||
<div id="mainContent" class="container-fluid py-4 mob-nav-pad">
|
||||
<div class="container py-4 mob-nav-pad">
|
||||
<div class="row g-3">
|
||||
|
||||
{{-- Parking List (RTL: renders on right side, but we put it second so it's on LEFT visually) --}}
|
||||
@ -117,7 +81,7 @@
|
||||
|
||||
{{-- MAP --}}
|
||||
<div class="col-lg-8 order-lg-1 mob-section-map">
|
||||
<div class="card h-100">
|
||||
<div class="card h-100" style="min-height:520px;">
|
||||
<div class="card-header d-flex align-items-center justify-content-between">
|
||||
<span class="fw-700 text-sm" style="color:#0f172a;">
|
||||
<i class="bi bi-map me-1" style="color:#6366f1;"></i>
|
||||
@ -126,7 +90,7 @@
|
||||
<span class="badge badge-soft-primary text-xs" id="map-count">--</span>
|
||||
</div>
|
||||
<div class="card-body p-0" style="border-radius:0 0 .75rem .75rem;overflow:hidden;">
|
||||
<div id="map"></div>
|
||||
<div id="map" style="height:500px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -141,7 +105,7 @@
|
||||
</span>
|
||||
<span class="badge badge-soft-success text-xs" id="list-count">{{ $lots->count() }} موقف</span>
|
||||
</div>
|
||||
<div style="overflow-y:auto;scrollbar-width:thin;" id="parkingList">
|
||||
<div style="max-height:500px;overflow-y:auto;scrollbar-width:thin;" id="parkingList">
|
||||
@forelse($lots as $lot)
|
||||
@php
|
||||
$avail = $lot['avail'];
|
||||
@ -166,7 +130,7 @@
|
||||
<p class="text-xs mb-2" style="color:#94a3b8;">{{ $lot['address'] }}</p>
|
||||
<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-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>
|
||||
</div>
|
||||
</div>
|
||||
@ -406,7 +370,7 @@
|
||||
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) };
|
||||
}
|
||||
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) ─────────────────────────────────────────
|
||||
let map = null, mapReady = false;
|
||||
@ -415,7 +379,7 @@
|
||||
if (mapReady) { map?.invalidateSize(); return; }
|
||||
mapReady = true;
|
||||
|
||||
map = L.map('map', { attributionControl: false }).setView([33.5138, 36.2765], 12);
|
||||
map = L.map('map').setView([33.5138, 36.2765], 12);
|
||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
attribution: '© OpenStreetMap'
|
||||
}).addTo(map);
|
||||
|
||||
@ -17,103 +17,152 @@
|
||||
<div class="app-layout">
|
||||
|
||||
{{-- ════════════════════════════════════════
|
||||
TOPBAR — full width, topmost
|
||||
SIDEBAR (appears on the RIGHT in RTL
|
||||
because it is the first flex child)
|
||||
════════════════════════════════════════ --}}
|
||||
@include('partials.topbar', ['isAdminLayout' => true])
|
||||
<aside class="app-sidebar" id="appSidebar">
|
||||
|
||||
{{-- Logo --}}
|
||||
<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 --}}
|
||||
<nav class="sidebar-nav">
|
||||
|
||||
<div class="sidebar-section">الرئيسية</div>
|
||||
|
||||
<a href="{{ route('admin.dashboard') }}"
|
||||
class="sidebar-link {{ request()->routeIs('admin.dashboard') ? 'active' : '' }}">
|
||||
<i class="bi bi-speedometer2 sidebar-icon"></i>
|
||||
<span>لوحة التحكم</span>
|
||||
</a>
|
||||
|
||||
<div class="sidebar-section">المواقف والحجوزات</div>
|
||||
|
||||
<a href="{{ route('admin.parking-lots.index') }}"
|
||||
class="sidebar-link {{ request()->routeIs('admin.parking-lots.*') ? 'active' : '' }}">
|
||||
<i class="bi bi-buildings sidebar-icon"></i>
|
||||
<span>المواقف</span>
|
||||
</a>
|
||||
|
||||
<a href="{{ route('admin.bookings.active') }}"
|
||||
class="sidebar-link {{ request()->routeIs('admin.bookings.*') ? 'active' : '' }}">
|
||||
<i class="bi bi-calendar-check sidebar-icon"></i>
|
||||
<span>الحجوزات النشطة</span>
|
||||
</a>
|
||||
|
||||
<div class="sidebar-section">التشغيل</div>
|
||||
|
||||
<a href="{{ route('admin.operators.index') }}"
|
||||
class="sidebar-link {{ request()->routeIs('admin.operators.*') ? 'active' : '' }}">
|
||||
<i class="bi bi-people sidebar-icon"></i>
|
||||
<span>المشغّلون</span>
|
||||
</a>
|
||||
|
||||
<a href="{{ route('operator.dashboard') }}"
|
||||
class="sidebar-link {{ request()->routeIs('operator.*') ? 'active' : '' }}">
|
||||
<i class="bi bi-person-badge sidebar-icon"></i>
|
||||
<span>لوحة المشغّل</span>
|
||||
</a>
|
||||
|
||||
</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>
|
||||
|
||||
{{-- ════════════════════════════════════════
|
||||
INNER ROW — sidebar + content
|
||||
MAIN BODY
|
||||
════════════════════════════════════════ --}}
|
||||
<div class="app-inner">
|
||||
<div class="app-body">
|
||||
|
||||
<aside class="app-sidebar" id="appSidebar">
|
||||
|
||||
@php $isAdmin = auth()->user()?->role === 'admin'; @endphp
|
||||
|
||||
{{-- Navigation --}}
|
||||
<nav class="sidebar-nav">
|
||||
|
||||
@if($isAdmin)
|
||||
<div class="sidebar-section">الرئيسية</div>
|
||||
|
||||
<a href="{{ route('admin.dashboard') }}"
|
||||
class="sidebar-link {{ request()->routeIs('admin.dashboard') ? 'active' : '' }}">
|
||||
<i class="bi bi-speedometer2 sidebar-icon"></i>
|
||||
<span>لوحة التحكم</span>
|
||||
</a>
|
||||
|
||||
<div class="sidebar-section">المواقف والحجوزات</div>
|
||||
|
||||
<a href="{{ route('admin.parking-lots.index') }}"
|
||||
class="sidebar-link {{ request()->routeIs('admin.parking-lots.*') ? 'active' : '' }}">
|
||||
<i class="bi bi-buildings sidebar-icon"></i>
|
||||
<span>المواقف</span>
|
||||
</a>
|
||||
|
||||
<a href="{{ route('admin.bookings.active') }}"
|
||||
class="sidebar-link {{ request()->routeIs('admin.bookings.*') ? 'active' : '' }}">
|
||||
<i class="bi bi-calendar-check sidebar-icon"></i>
|
||||
<span>الحجوزات النشطة</span>
|
||||
</a>
|
||||
|
||||
<div class="sidebar-section">التشغيل</div>
|
||||
|
||||
<a href="{{ route('admin.operators.index') }}"
|
||||
class="sidebar-link {{ request()->routeIs('admin.operators.*') ? 'active' : '' }}">
|
||||
<i class="bi bi-people sidebar-icon"></i>
|
||||
<span>المشغّلون</span>
|
||||
</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') }}"
|
||||
class="sidebar-link {{ request()->routeIs('operator.dashboard') ? 'active' : '' }}">
|
||||
<i class="bi bi-person-badge sidebar-icon"></i>
|
||||
<span>{{ $isAdmin ? 'لوحة المشغّل' : 'الموقف' }}</span>
|
||||
</a>
|
||||
|
||||
</nav>
|
||||
|
||||
</aside>
|
||||
|
||||
{{-- ════════════════════════════════════════
|
||||
MAIN BODY
|
||||
════════════════════════════════════════ --}}
|
||||
<div class="app-body">
|
||||
|
||||
{{-- Flash Messages --}}
|
||||
<div class="px-4 pt-3">
|
||||
@if(session('success'))
|
||||
<div class="alert alert-success d-flex align-items-center gap-2 border-0 rounded-3 py-2 mb-0"
|
||||
role="alert">
|
||||
<i class="bi bi-check-circle-fill flex-shrink-0"></i>
|
||||
<span>{{ session('success') }}</span>
|
||||
<button type="button" class="btn-close ms-auto" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
@endif
|
||||
@if(session('error'))
|
||||
<div class="alert alert-danger d-flex align-items-center gap-2 border-0 rounded-3 py-2 mb-0"
|
||||
role="alert">
|
||||
<i class="bi bi-exclamation-triangle-fill flex-shrink-0"></i>
|
||||
<span>{{ session('error') }}</span>
|
||||
<button type="button" class="btn-close ms-auto" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
@endif
|
||||
{{-- 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>
|
||||
|
||||
{{-- Page Content --}}
|
||||
<main class="app-content">
|
||||
@yield('content')
|
||||
</main>
|
||||
{{-- Flash Messages --}}
|
||||
<div class="px-4 pt-3">
|
||||
@if(session('success'))
|
||||
<div class="alert alert-success d-flex align-items-center gap-2 border-0 rounded-3 py-2 mb-0"
|
||||
role="alert">
|
||||
<i class="bi bi-check-circle-fill flex-shrink-0"></i>
|
||||
<span>{{ session('success') }}</span>
|
||||
<button type="button" class="btn-close ms-auto" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
@endif
|
||||
@if(session('error'))
|
||||
<div class="alert alert-danger d-flex align-items-center gap-2 border-0 rounded-3 py-2 mb-0"
|
||||
role="alert">
|
||||
<i class="bi bi-exclamation-triangle-fill flex-shrink-0"></i>
|
||||
<span>{{ session('error') }}</span>
|
||||
<button type="button" class="btn-close ms-auto" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
</div>{{-- /app-body --}}
|
||||
{{-- Page Content --}}
|
||||
<main class="app-content">
|
||||
@yield('content')
|
||||
</main>
|
||||
|
||||
</div>{{-- /app-inner --}}
|
||||
</div>{{-- /app-body --}}
|
||||
|
||||
</div>{{-- /app-layout --}}
|
||||
|
||||
@ -122,7 +171,6 @@
|
||||
|
||||
{{-- ══ MOBILE BOTTOM NAVIGATION ═══════════════════════════════════════════════ --}}
|
||||
<nav class="mobile-bottom-nav" aria-label="التنقل">
|
||||
@if($isAdmin)
|
||||
<a href="{{ route('admin.dashboard') }}"
|
||||
class="mob-nav-item {{ request()->routeIs('admin.dashboard') ? 'active' : '' }}">
|
||||
<i class="bi bi-speedometer2"></i>
|
||||
@ -144,22 +192,10 @@
|
||||
<span>المشغّلون</span>
|
||||
</a>
|
||||
<a href="{{ route('operator.dashboard') }}"
|
||||
class="mob-nav-item {{ request()->routeIs('operator.dashboard') ? 'active' : '' }}">
|
||||
class="mob-nav-item {{ request()->routeIs('operator.*') ? 'active' : '' }}">
|
||||
<i class="bi bi-person-badge"></i>
|
||||
<span>التشغيل</span>
|
||||
</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>
|
||||
|
||||
<script>
|
||||
|
||||
@ -14,10 +14,30 @@
|
||||
<body style="background:#f1f5f9;font-family:'Cairo',sans-serif;">
|
||||
|
||||
{{-- ══ HEADER ══════════════════════════════════════════════════════════════ --}}
|
||||
@include('partials.topbar')
|
||||
<header class="public-header">
|
||||
<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 ═════════════════════════════════════════════════════════════ --}}
|
||||
<main style="padding:1.5rem;">
|
||||
<main class="container py-4" style="max-width:820px;">
|
||||
|
||||
{{-- Flash messages --}}
|
||||
@if(session('success'))
|
||||
|
||||
@ -241,7 +241,7 @@ $gradients = [
|
||||
|
||||
<div class="lot-card-stats">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
@ -290,7 +290,7 @@ $gradients = [
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
<span class="badge badge-soft-success">{{ $selectedLot->available_spaces }} متاح</span>
|
||||
<span class="badge badge-soft-warning">{{ $selectedLot->occupied_spaces }} مشغول</span>
|
||||
@if(count($assignedLotIds) > 1)
|
||||
@if(!$assignedLotId)
|
||||
<a href="{{ route('operator.dashboard') }}"
|
||||
class="btn btn-sm fw-600"
|
||||
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-entry').textContent = d.entry_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 => `
|
||||
<div class="fee-row">
|
||||
<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 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('');
|
||||
document.getElementById('rcpt-breakdown').innerHTML =
|
||||
rows || '<p class="text-xs text-center" style="color:#94a3b8;">لا تفاصيل</p>';
|
||||
|
||||
@ -1,692 +0,0 @@
|
||||
@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 6–23 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
|
||||
@ -1,33 +0,0 @@
|
||||
<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>
|
||||
@ -6,7 +6,8 @@
|
||||
data-bs-toggle="dropdown"
|
||||
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;">
|
||||
<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);">
|
||||
{{-- Avatar circle --}}
|
||||
<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) }}
|
||||
</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;">
|
||||
@ -31,6 +32,7 @@
|
||||
<div style="color:#64748b;font-size:.75rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">
|
||||
{{ auth()->user()->email }}
|
||||
</div>
|
||||
{{-- Role badge --}}
|
||||
@php
|
||||
$role = auth()->user()->role;
|
||||
$roleLabel = match($role) {
|
||||
@ -51,16 +53,6 @@
|
||||
|
||||
<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 --}}
|
||||
<li>
|
||||
<a href="{{ route('profile.show') }}"
|
||||
@ -71,8 +63,7 @@
|
||||
</a>
|
||||
</li>
|
||||
|
||||
{{-- My reservations (regular users) --}}
|
||||
@if($role === 'user')
|
||||
{{-- My reservations --}}
|
||||
<li>
|
||||
<a href="{{ route('user.dashboard') }}"
|
||||
class="dropdown-item d-flex align-items-center gap-2 rounded-2"
|
||||
@ -81,10 +72,9 @@
|
||||
حجوزاتي
|
||||
</a>
|
||||
</li>
|
||||
@endif
|
||||
|
||||
{{-- Operator panel (operators & admins) --}}
|
||||
@if(in_array($role, ['operator', 'admin']))
|
||||
@if(in_array(auth()->user()->role, ['operator', 'admin']))
|
||||
<li>
|
||||
<a href="{{ route('operator.dashboard') }}"
|
||||
class="dropdown-item d-flex align-items-center gap-2 rounded-2"
|
||||
@ -96,7 +86,7 @@
|
||||
@endif
|
||||
|
||||
{{-- Admin panel (admins only) --}}
|
||||
@if($role === 'admin')
|
||||
@if(auth()->user()->role === 'admin')
|
||||
<li>
|
||||
<a href="{{ route('admin.dashboard') }}"
|
||||
class="dropdown-item d-flex align-items-center gap-2 rounded-2"
|
||||
|
||||
@ -5,15 +5,15 @@
|
||||
|
||||
{{-- Page header --}}
|
||||
<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>
|
||||
<h1 class="fw-800 mb-0" style="font-size:1.25rem;color:#0f172a;">حجوزاتي</h1>
|
||||
<p class="text-sm mb-0" style="color:#64748b;">سجل حجوزاتك في مواقف السيارات</p>
|
||||
</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>
|
||||
|
||||
{{-- ── Debt banner ─────────────────────────────────────────────────────────── --}}
|
||||
@ -27,12 +27,12 @@
|
||||
<div class="fw-700" style="color:#92400e;font-size:.9rem;">رصيد مستحق غير مدفوع</div>
|
||||
<div class="text-sm" style="color:#b45309;">
|
||||
لديك رسوم إلغاء متراكمة بقيمة
|
||||
<strong>{{ number_format($pendingDebt) }} ليرة سورية</strong>
|
||||
<strong>{{ number_format($pendingDebt) }} ل.س</strong>
|
||||
— ستُضاف تلقائياً إلى حجزك القادم.
|
||||
</div>
|
||||
</div>
|
||||
<div class="fw-800" style="color:#92400e;font-size:1.25rem;white-space:nowrap;">
|
||||
{{ number_format($pendingDebt) }} ليرة سورية
|
||||
{{ number_format($pendingDebt) }} ل.س
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
@ -130,13 +130,13 @@
|
||||
{{-- Fee column --}}
|
||||
<td>
|
||||
@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>
|
||||
@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>
|
||||
@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')
|
||||
<span class="text-xs" style="color:#94a3b8;">مجاني</span>
|
||||
@else
|
||||
@ -304,16 +304,16 @@
|
||||
|
||||
{{-- After pay-now chosen: back + confirm --}}
|
||||
<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"
|
||||
class="btn btn-success fw-bold flex-grow-1"
|
||||
style="font-family:'Cairo',sans-serif;border-radius:.5rem;">
|
||||
<i class="bi bi-check2-circle me-1"></i>تأكيد الدفع والإلغاء
|
||||
</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>
|
||||
@ -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;">
|
||||
<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 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('');
|
||||
document.getElementById('cancelFeeBreakdown').innerHTML =
|
||||
rows || '<span style="color:#94a3b8;font-size:.8rem;">— مدة قصيرة جداً —</span>';
|
||||
document.getElementById('cancelFeeTotal').textContent =
|
||||
Number(d.fee).toLocaleString('ar-SA') + ' ليرة سورية';
|
||||
Number(d.fee).toLocaleString('ar-SA') + ' ل.س';
|
||||
|
||||
// Reset UI
|
||||
document.getElementById('cancelPayMethodWrap').style.display = 'none';
|
||||
|
||||
@ -5,15 +5,15 @@
|
||||
|
||||
{{-- Page header --}}
|
||||
<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>
|
||||
<h1 class="fw-800 mb-0" style="font-size:1.25rem;color:#0f172a;">معلوماتي</h1>
|
||||
<p class="text-sm mb-0" style="color:#64748b;">إدارة بيانات حسابك الشخصي</p>
|
||||
</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 class="row g-4">
|
||||
|
||||
@ -54,8 +54,6 @@ Route::prefix('admin')->middleware('admin')->name('admin.')->group(function () {
|
||||
// Operator Dashboard
|
||||
Route::prefix('operator')->middleware('operator')->name('operator.')->group(function () {
|
||||
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('/{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');
|
||||
|
||||
0
storage/app/.gitignore
vendored
Executable file → Normal file
0
storage/app/.gitignore
vendored
Executable file → Normal file
0
storage/app/private/.gitignore
vendored
Executable file → Normal file
0
storage/app/private/.gitignore
vendored
Executable file → Normal file
0
storage/app/public/.gitignore
vendored
Executable file → Normal file
0
storage/app/public/.gitignore
vendored
Executable file → Normal file
0
storage/framework/.gitignore
vendored
Executable file → Normal file
0
storage/framework/.gitignore
vendored
Executable file → Normal file
0
storage/framework/cache/.gitignore
vendored
Executable file → Normal file
0
storage/framework/cache/.gitignore
vendored
Executable file → Normal file
0
storage/framework/cache/data/.gitignore
vendored
Executable file → Normal file
0
storage/framework/cache/data/.gitignore
vendored
Executable file → Normal file
0
storage/framework/sessions/.gitignore
vendored
Executable file → Normal file
0
storage/framework/sessions/.gitignore
vendored
Executable file → Normal file
0
storage/framework/testing/.gitignore
vendored
Executable file → Normal file
0
storage/framework/testing/.gitignore
vendored
Executable file → Normal file
0
storage/framework/views/.gitignore
vendored
Executable file → Normal file
0
storage/framework/views/.gitignore
vendored
Executable file → Normal file
0
storage/logs/.gitignore
vendored
Executable file → Normal file
0
storage/logs/.gitignore
vendored
Executable file → Normal file
24
vendor/composer/autoload_classmap.php
vendored
24
vendor/composer/autoload_classmap.php
vendored
@ -6,28 +6,7 @@ $vendorDir = dirname(__DIR__);
|
||||
$baseDir = dirname($vendorDir);
|
||||
|
||||
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\\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\\Providers\\AppServiceProvider' => $baseDir . '/app/Providers/AppServiceProvider.php',
|
||||
'Attribute' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/Attribute.php',
|
||||
@ -153,10 +132,7 @@ return array(
|
||||
'Cron\\MinutesField' => $vendorDir . '/dragonmantank/cron-expression/src/Cron/MinutesField.php',
|
||||
'Cron\\MonthField' => $vendorDir . '/dragonmantank/cron-expression/src/Cron/MonthField.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\\ParkingLotSeeder' => $baseDir . '/database/seeders/ParkingLotSeeder.php',
|
||||
'DateError' => $vendorDir . '/symfony/polyfill-php83/Resources/stubs/DateError.php',
|
||||
'DateException' => $vendorDir . '/symfony/polyfill-php83/Resources/stubs/DateException.php',
|
||||
'DateInvalidOperationException' => $vendorDir . '/symfony/polyfill-php83/Resources/stubs/DateInvalidOperationException.php',
|
||||
|
||||
24
vendor/composer/autoload_static.php
vendored
24
vendor/composer/autoload_static.php
vendored
@ -536,28 +536,7 @@ class ComposerStaticInit53b5d56b3b7e3cbac1713e68c8850f6c
|
||||
);
|
||||
|
||||
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\\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\\Providers\\AppServiceProvider' => __DIR__ . '/../..' . '/app/Providers/AppServiceProvider.php',
|
||||
'Attribute' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/Attribute.php',
|
||||
@ -683,10 +662,7 @@ class ComposerStaticInit53b5d56b3b7e3cbac1713e68c8850f6c
|
||||
'Cron\\MinutesField' => __DIR__ . '/..' . '/dragonmantank/cron-expression/src/Cron/MinutesField.php',
|
||||
'Cron\\MonthField' => __DIR__ . '/..' . '/dragonmantank/cron-expression/src/Cron/MonthField.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\\ParkingLotSeeder' => __DIR__ . '/../..' . '/database/seeders/ParkingLotSeeder.php',
|
||||
'DateError' => __DIR__ . '/..' . '/symfony/polyfill-php83/Resources/stubs/DateError.php',
|
||||
'DateException' => __DIR__ . '/..' . '/symfony/polyfill-php83/Resources/stubs/DateException.php',
|
||||
'DateInvalidOperationException' => __DIR__ . '/..' . '/symfony/polyfill-php83/Resources/stubs/DateInvalidOperationException.php',
|
||||
|
||||
12
vendor/composer/installed.php
vendored
12
vendor/composer/installed.php
vendored
@ -1,9 +1,9 @@
|
||||
<?php return array(
|
||||
'root' => array(
|
||||
'name' => 'laravel/laravel',
|
||||
'pretty_version' => 'dev-main',
|
||||
'version' => 'dev-main',
|
||||
'reference' => 'b03c1d4619de986420534fe46341f4da4d088f76',
|
||||
'pretty_version' => '1.0.0+no-version-set',
|
||||
'version' => '1.0.0.0',
|
||||
'reference' => null,
|
||||
'type' => 'project',
|
||||
'install_path' => __DIR__ . '/../../',
|
||||
'aliases' => array(),
|
||||
@ -398,9 +398,9 @@
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'laravel/laravel' => array(
|
||||
'pretty_version' => 'dev-main',
|
||||
'version' => 'dev-main',
|
||||
'reference' => 'b03c1d4619de986420534fe46341f4da4d088f76',
|
||||
'pretty_version' => '1.0.0+no-version-set',
|
||||
'version' => '1.0.0.0',
|
||||
'reference' => null,
|
||||
'type' => 'project',
|
||||
'install_path' => __DIR__ . '/../../',
|
||||
'aliases' => array(),
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user