latest
This commit is contained in:
parent
b03c1d4619
commit
b07423aef3
@ -20,7 +20,22 @@
|
||||
"Bash(php -r \"echo file_put_contents\\('C:/xampp/htdocs/scp-syria/storage/app/public/test.txt', 'ok'\\) ? 'write OK' : 'write FAIL';\")",
|
||||
"Bash(curl:*)",
|
||||
"Bash(php:*)",
|
||||
"Bash(grep -v \"^$\")"
|
||||
"Bash(grep -v \"^$\")",
|
||||
"Bash(git config *)",
|
||||
"Bash(/usr/local/bin/npm run *)",
|
||||
"Bash(node_modules/.bin/vite build *)",
|
||||
"Read(//usr/local/nvm/versions/**)",
|
||||
"Read(//root/.nvm/versions/**)",
|
||||
"Read(//opt/**)",
|
||||
"Bash(/root/.vscode-server/cli/servers/Stable-560a9dba96f961efea7b1612916f89e5d5d4d679/server/node /var/www/scp-syria/node_modules/vite/bin/vite.js build --config /var/www/scp-syria/vite.config.js)",
|
||||
"Bash(grep -o \".\\\\{0,80\\\\}inset-inline-end:0.\\\\{0,80\\\\}\" /var/www/scp-syria/public/build/assets/app-BtJpbOwL.css)",
|
||||
"Bash(grep -o \".\\\\{0,150\\\\}sidebar-toggle.\\\\{0,20\\\\}display:none.\\\\{0,200\\\\}\" /var/www/scp-syria/public/build/assets/app-BtJpbOwL.css)",
|
||||
"Bash(grep -o \".\\\\{0,60\\\\}inset-inline.\\\\{0,80\\\\}height:100%.\\\\{0,30\\\\}\" /var/www/scp-syria/public/build/assets/app-BtJpbOwL.css)",
|
||||
"Bash(grep -o \".\\\\{0,10\\\\}app-sidebar.\\\\{0,40\\\\}display:none.\\\\{0,10\\\\}\" /var/www/scp-syria/public/build/assets/app-BtJpbOwL.css)",
|
||||
"Bash(node node_modules/vite/bin/vite.js build)",
|
||||
"Bash(/root/.vscode-server/cli/servers/Stable-41dd792b5e652393e7787322889ed5fdc58bd75b/server/node /var/www/scp-syria/node_modules/vite/bin/vite.js build --root /var/www/scp-syria)",
|
||||
"Bash(/root/.vscode-server/cli/servers/Stable-41dd792b5e652393e7787322889ed5fdc58bd75b/server/node node_modules/vite/bin/vite.js build)",
|
||||
"Bash(python3 *)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
48
.env
48
.env
@ -1,44 +1,23 @@
|
||||
APP_NAME="Smart Car Park"
|
||||
APP_NAME="Damascus Parking"
|
||||
APP_ENV=local
|
||||
APP_KEY=base64:92pMpQ2HV7icz1lfUZUQfUguPfUdqYUPVU+aWdYDstY=
|
||||
APP_KEY=base64:gC4TfmQLkKdWmH/vRQ9KY//WgH0w8+RhkVuswqCbgxo=
|
||||
APP_DEBUG=true
|
||||
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
|
||||
APP_TIMEZONE=Asia/Damascus
|
||||
APP_URL=http://localhost:8000
|
||||
|
||||
LOG_CHANNEL=stack
|
||||
LOG_STACK=single
|
||||
LOG_DEPRECATIONS_CHANNEL=null
|
||||
LOG_LEVEL=debug
|
||||
|
||||
DB_CONNECTION=sqlite
|
||||
# DB_HOST=127.0.0.1
|
||||
# DB_PORT=3306
|
||||
# DB_DATABASE=laravel
|
||||
# DB_USERNAME=root
|
||||
# DB_PASSWORD=
|
||||
|
||||
SESSION_DRIVER=database
|
||||
SESSION_LIFETIME=120
|
||||
SESSION_ENCRYPT=false
|
||||
SESSION_PATH=/
|
||||
SESSION_DOMAIN=null
|
||||
DB_DATABASE=/var/www/scp-syria/database/database.sqlite
|
||||
|
||||
BROADCAST_CONNECTION=log
|
||||
FILESYSTEM_DISK=local
|
||||
QUEUE_CONNECTION=database
|
||||
|
||||
CACHE_STORE=database
|
||||
# CACHE_PREFIX=
|
||||
FILESYSTEM_DISK=local
|
||||
QUEUE_CONNECTION=sync
|
||||
SESSION_DRIVER=database
|
||||
SESSION_LIFETIME=120
|
||||
|
||||
MEMCACHED_HOST=127.0.0.1
|
||||
|
||||
@ -47,12 +26,12 @@ REDIS_HOST=127.0.0.1
|
||||
REDIS_PASSWORD=null
|
||||
REDIS_PORT=6379
|
||||
|
||||
MAIL_MAILER=log
|
||||
MAIL_SCHEME=null
|
||||
MAIL_HOST=127.0.0.1
|
||||
MAIL_PORT=2525
|
||||
MAIL_MAILER=smtp
|
||||
MAIL_HOST=mailpit
|
||||
MAIL_PORT=1025
|
||||
MAIL_USERNAME=null
|
||||
MAIL_PASSWORD=null
|
||||
MAIL_ENCRYPTION=null
|
||||
MAIL_FROM_ADDRESS="hello@example.com"
|
||||
MAIL_FROM_NAME="${APP_NAME}"
|
||||
|
||||
@ -63,3 +42,4 @@ AWS_BUCKET=
|
||||
AWS_USE_PATH_STYLE_ENDPOINT=false
|
||||
|
||||
VITE_APP_NAME="${APP_NAME}"
|
||||
|
||||
|
||||
43
CLAUDE.md
43
CLAUDE.md
@ -2,6 +2,28 @@
|
||||
|
||||
## 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.
|
||||
@ -136,12 +158,14 @@ resources/
|
||||
|
||||
| Role | Middleware | Access |
|
||||
|------------|-----------------|---------------------------------------------|
|
||||
| `admin` | `EnsureAdmin` | Full admin dashboard, parking lot CRUD, all bookings |
|
||||
| `operator` | `EnsureOperator`| Vehicle check-in/check-out, active bookings for assigned lot |
|
||||
| `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 |
|
||||
| `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
|
||||
@ -154,6 +178,15 @@ 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 |
|
||||
@ -231,7 +264,9 @@ 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 view |
|
||||
| `GET /operator/dashboard` | operator | Operator lot-picker + operations panel |
|
||||
| `GET /operator/stats` | operator | Operator statistics & charts dashboard |
|
||||
| `GET /operator/stats-data` | operator | JSON stats data for operator dashboard (AJAX) |
|
||||
| `POST /operator/check-in` | operator | Vehicle entry |
|
||||
| `POST /operator/{booking}/checkout` | operator | Vehicle exit + fee |
|
||||
| `GET /admin/stats` | admin | JSON stats (AJAX) |
|
||||
@ -361,7 +396,9 @@ 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('assignedLot')
|
||||
->with('assignedLots')
|
||||
->orderBy('name')
|
||||
->get();
|
||||
|
||||
@ -26,24 +26,26 @@ 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)],
|
||||
'parking_lot_id' => 'nullable|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)],
|
||||
'lot_ids' => 'nullable|array',
|
||||
'lot_ids.*'=> 'exists:parking_lots,id',
|
||||
], [
|
||||
'email.unique' => 'هذا البريد الإلكتروني مستخدم بالفعل.',
|
||||
]);
|
||||
|
||||
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 = User::create([
|
||||
'name' => $data['name'],
|
||||
'email' => $data['email'],
|
||||
'phone' => $data['phone'] ?? null,
|
||||
'password' => Hash::make($data['password']),
|
||||
'role' => 'operator',
|
||||
]);
|
||||
|
||||
$operator->assignedLots()->sync($data['lot_ids'] ?? []);
|
||||
|
||||
return response()->json(['success' => true, 'message' => 'تم إنشاء حساب المشغّل بنجاح.']);
|
||||
}
|
||||
|
||||
@ -54,20 +56,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)],
|
||||
'parking_lot_id' => 'nullable|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)],
|
||||
'lot_ids' => 'nullable|array',
|
||||
'lot_ids.*'=> 'exists:parking_lots,id',
|
||||
], [
|
||||
'email.unique' => 'هذا البريد الإلكتروني مستخدم بالفعل.',
|
||||
]);
|
||||
|
||||
$updates = [
|
||||
'name' => $data['name'],
|
||||
'email' => $data['email'],
|
||||
'phone' => $data['phone'] ?? null,
|
||||
'parking_lot_id' => $data['parking_lot_id'] ?? null,
|
||||
'name' => $data['name'],
|
||||
'email' => $data['email'],
|
||||
'phone' => $data['phone'] ?? null,
|
||||
];
|
||||
|
||||
if (!empty($data['password'])) {
|
||||
@ -75,6 +77,7 @@ class AdminOperatorController extends Controller
|
||||
}
|
||||
|
||||
$operator->update($updates);
|
||||
$operator->assignedLots()->sync($data['lot_ids'] ?? []);
|
||||
|
||||
return response()->json(['success' => true, 'message' => 'تم تحديث بيانات المشغّل.']);
|
||||
}
|
||||
|
||||
@ -17,10 +17,18 @@ class OperatorController extends Controller
|
||||
|
||||
public function dashboard(Request $request): \Illuminate\View\View
|
||||
{
|
||||
$user = Auth::user();
|
||||
$assignedLotId = $user->parking_lot_id; // null = no restriction (e.g. admin)
|
||||
$user = Auth::user();
|
||||
$assignedLotIds = $user->assignedLots->pluck('id')->toArray();
|
||||
|
||||
$rawLots = ParkingLot::active()->withStatus()->get();
|
||||
// Operators only see their assigned lots; no assignment = no lots visible
|
||||
$query = ParkingLot::active()->withStatus();
|
||||
if (!empty($assignedLotIds)) {
|
||||
$query->whereIn('id', $assignedLotIds);
|
||||
} else {
|
||||
$query->whereRaw('1 = 0'); // no lots assigned → show nothing
|
||||
}
|
||||
|
||||
$rawLots = $query->get();
|
||||
|
||||
$parkingLots = $rawLots->map(fn($lot) => [
|
||||
'id' => $lot->id,
|
||||
@ -34,20 +42,22 @@ class OperatorController extends Controller
|
||||
'lat' => (float) $lot->latitude,
|
||||
'lng' => (float) $lot->longitude,
|
||||
'image' => $lot->image ? Storage::url($lot->image) : null,
|
||||
'locked' => $assignedLotId !== null && $lot->id !== $assignedLotId,
|
||||
'locked' => false,
|
||||
])->values();
|
||||
|
||||
// If operator has an assigned lot, force that lot
|
||||
$selectedLotId = $request->get('lot_id');
|
||||
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;
|
||||
|
||||
// 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]]);
|
||||
}
|
||||
return redirect()->route('operator.dashboard');
|
||||
}
|
||||
|
||||
// Auto-select when only one lot is assigned
|
||||
if (!$selectedLotId && count($assignedLotIds) === 1) {
|
||||
$selectedLotId = $assignedLotIds[0];
|
||||
}
|
||||
|
||||
$selectedLot = null;
|
||||
@ -73,10 +83,177 @@ class OperatorController extends Controller
|
||||
}
|
||||
|
||||
return view('operator.dashboard', compact(
|
||||
'parkingLots', 'selectedLot', 'activeCars', 'reservations', 'selectedLotId', 'assignedLotId'
|
||||
'parkingLots', 'selectedLot', 'activeCars', 'reservations', 'selectedLotId', 'assignedLotIds'
|
||||
));
|
||||
}
|
||||
|
||||
// ── Operator stats page ─────────────────────────────────────────────────────
|
||||
|
||||
public function statsPage(): \Illuminate\View\View
|
||||
{
|
||||
$user = Auth::user();
|
||||
$assignedLotIds = $user->assignedLots->pluck('id')->toArray();
|
||||
$lots = ParkingLot::whereIn('id', $assignedLotIds)->orderBy('name')->get();
|
||||
|
||||
return view('operator.stats', compact('lots', 'assignedLotIds'));
|
||||
}
|
||||
|
||||
public function statsJson(): JsonResponse
|
||||
{
|
||||
$user = Auth::user();
|
||||
$assignedLotIds = $user->assignedLots->pluck('id')->toArray();
|
||||
|
||||
// Active vehicles right now
|
||||
$activeCars = Booking::whereIn('parking_lot_id', $assignedLotIds)
|
||||
->where('source', 'walk_in')
|
||||
->where('status', 'active')
|
||||
->count();
|
||||
|
||||
// Walk-in check-ins started today
|
||||
$checkInsToday = Booking::whereIn('parking_lot_id', $assignedLotIds)
|
||||
->where('source', 'walk_in')
|
||||
->whereDate('start_time', today())
|
||||
->count();
|
||||
|
||||
// Revenue collected today (completed + paid today)
|
||||
$revenueToday = (float) Booking::whereIn('parking_lot_id', $assignedLotIds)
|
||||
->where('status', 'completed')
|
||||
->whereDate('paid_at', today())
|
||||
->sum('total_fee');
|
||||
|
||||
// Total capacity & occupied across assigned lots
|
||||
$lots = ParkingLot::whereIn('id', $assignedLotIds)->get();
|
||||
$totalCapacity = $lots->sum('total_capacity');
|
||||
$totalOccupied = Booking::whereIn('parking_lot_id', $assignedLotIds)
|
||||
->where('status', 'active')
|
||||
->count();
|
||||
$availableSpaces = max(0, $totalCapacity - $totalOccupied);
|
||||
|
||||
// Pending reservations (not yet activated)
|
||||
$pendingReservations = Booking::whereIn('parking_lot_id', $assignedLotIds)
|
||||
->where('source', 'reservation')
|
||||
->where('status', 'active')
|
||||
->count();
|
||||
|
||||
// 7-day daily check-ins and revenue
|
||||
$dailyCheckIns = [];
|
||||
$dailyRevenue = [];
|
||||
$dailyDates = [];
|
||||
for ($i = 6; $i >= 0; $i--) {
|
||||
$date = now()->subDays($i)->toDateString();
|
||||
$dailyDates[] = $date;
|
||||
$dailyCheckIns[] = Booking::whereIn('parking_lot_id', $assignedLotIds)
|
||||
->where('source', 'walk_in')
|
||||
->whereDate('start_time', $date)
|
||||
->count();
|
||||
$dailyRevenue[] = (float) Booking::whereIn('parking_lot_id', $assignedLotIds)
|
||||
->where('status', 'completed')
|
||||
->whereDate('paid_at', $date)
|
||||
->sum('total_fee');
|
||||
}
|
||||
|
||||
// Per-lot breakdown
|
||||
$lotStats = $lots->map(function ($lot) {
|
||||
$active = Booking::where('parking_lot_id', $lot->id)->where('status', 'active')->count();
|
||||
$todayIns = Booking::where('parking_lot_id', $lot->id)
|
||||
->where('source', 'walk_in')
|
||||
->whereDate('start_time', today())
|
||||
->count();
|
||||
$todayRev = (float) Booking::where('parking_lot_id', $lot->id)
|
||||
->where('status', 'completed')
|
||||
->whereDate('paid_at', today())
|
||||
->sum('total_fee');
|
||||
return [
|
||||
'id' => $lot->id,
|
||||
'name' => $lot->name,
|
||||
'address' => $lot->address,
|
||||
'total' => $lot->total_capacity,
|
||||
'active' => $active,
|
||||
'available' => max(0, $lot->total_capacity - $active),
|
||||
'pct' => $lot->total_capacity > 0 ? round($active / $lot->total_capacity * 100) : 0,
|
||||
'today_ins' => $todayIns,
|
||||
'today_rev' => $todayRev,
|
||||
];
|
||||
})->values();
|
||||
|
||||
// Last 8 completed bookings
|
||||
$recentCompletions = Booking::whereIn('parking_lot_id', $assignedLotIds)
|
||||
->where('status', 'completed')
|
||||
->with('parkingLot:id,name')
|
||||
->latest('paid_at')
|
||||
->limit(8)
|
||||
->get(['id', 'parking_lot_id', 'vehicle_plate', 'customer_name', 'start_time', 'paid_at', 'total_fee', 'payment_method']);
|
||||
|
||||
// ── Work progress ──────────────────────────────────────────────────────
|
||||
// Checkouts completed today
|
||||
$checkoutsToday = Booking::whereIn('parking_lot_id', $assignedLotIds)
|
||||
->where('status', 'completed')
|
||||
->whereDate('paid_at', today())
|
||||
->count();
|
||||
|
||||
// Cancellations today
|
||||
$cancelledToday = Booking::whereIn('parking_lot_id', $assignedLotIds)
|
||||
->where('status', 'cancelled')
|
||||
->whereDate('updated_at', today())
|
||||
->count();
|
||||
|
||||
// Yesterday check-ins (comparison)
|
||||
$checkInsYesterday = Booking::whereIn('parking_lot_id', $assignedLotIds)
|
||||
->where('source', 'walk_in')
|
||||
->whereDate('start_time', today()->subDay())
|
||||
->count();
|
||||
|
||||
// Average stay duration in minutes (completed bookings today, start→paid_at)
|
||||
$completedToday = Booking::whereIn('parking_lot_id', $assignedLotIds)
|
||||
->where('status', 'completed')
|
||||
->whereDate('paid_at', today())
|
||||
->get(['start_time', 'paid_at']);
|
||||
|
||||
$avgDurationMin = $completedToday->isNotEmpty()
|
||||
? round($completedToday->avg(fn($b) => $b->start_time->diffInMinutes($b->paid_at)))
|
||||
: 0;
|
||||
|
||||
// Completion rate today: completed / (completed + cancelled)
|
||||
$totalClosed = $checkoutsToday + $cancelledToday;
|
||||
$completionRate = $totalClosed > 0 ? round($checkoutsToday / $totalClosed * 100) : 100;
|
||||
|
||||
// Hourly activity today (check-ins per hour 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,7 +24,6 @@ class User extends Authenticatable
|
||||
'phone',
|
||||
'password',
|
||||
'role',
|
||||
'parking_lot_id',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
@ -56,8 +55,8 @@ class User extends Authenticatable
|
||||
];
|
||||
}
|
||||
|
||||
public function assignedLot(): \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
public function assignedLots(): \Illuminate\Database\Eloquent\Relations\BelongsToMany
|
||||
{
|
||||
return $this->belongsTo(ParkingLot::class, 'parking_lot_id');
|
||||
return $this->belongsToMany(ParkingLot::class, 'operator_parking_lot');
|
||||
}
|
||||
}
|
||||
|
||||
0
bootstrap/cache/.gitignore
vendored
Normal file → Executable file
0
bootstrap/cache/.gitignore
vendored
Normal file → Executable file
@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('operator_parking_lot', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
|
||||
$table->foreignId('parking_lot_id')->constrained()->cascadeOnDelete();
|
||||
$table->unique(['user_id', 'parking_lot_id']);
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
// Migrate existing single-lot assignments from users.parking_lot_id
|
||||
$operators = DB::table('users')
|
||||
->where('role', 'operator')
|
||||
->whereNotNull('parking_lot_id')
|
||||
->get(['id', 'parking_lot_id']);
|
||||
|
||||
foreach ($operators as $op) {
|
||||
DB::table('operator_parking_lot')->insertOrIgnore([
|
||||
'user_id' => $op->id,
|
||||
'parking_lot_id' => $op->parking_lot_id,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('operator_parking_lot');
|
||||
}
|
||||
};
|
||||
File diff suppressed because one or more lines are too long
9
public/build/assets/app-D0Ms7V4S2.js
Normal file
9
public/build/assets/app-D0Ms7V4S2.js
Normal file
File diff suppressed because one or more lines are too long
1
public/build/assets/app-DarkBar01.css
Normal file
1
public/build/assets/app-DarkBar01.css
Normal file
File diff suppressed because one or more lines are too long
1
public/build/assets/app-Layout02.css
Normal file
1
public/build/assets/app-Layout02.css
Normal file
File diff suppressed because one or more lines are too long
@ -1,6 +1,6 @@
|
||||
{
|
||||
"resources/css/app.scss": {
|
||||
"file": "assets/app-BtJpbOwL.css",
|
||||
"file": "assets/app-Layout02.css",
|
||||
"src": "resources/css/app.scss",
|
||||
"isEntry": true,
|
||||
"name": "app",
|
||||
@ -9,9 +9,9 @@
|
||||
]
|
||||
},
|
||||
"resources/js/app.js": {
|
||||
"file": "assets/app-D0Ms7V4S.js",
|
||||
"file": "assets/app-D0Ms7V4S2.js",
|
||||
"name": "app",
|
||||
"src": "resources/js/app.js",
|
||||
"isEntry": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -74,11 +74,17 @@ body {
|
||||
background-color: var(--app-bg);
|
||||
}
|
||||
|
||||
// ─── App Layout (Sidebar + Main) ──────────────────────────────────────────────
|
||||
// ─── App Layout (Topbar top, then sidebar + content row below) ───────────────
|
||||
.app-layout {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
// In RTL flex-direction:row, the first child (sidebar) appears on the RIGHT
|
||||
}
|
||||
|
||||
.app-inner {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
// ─── Sidebar ──────────────────────────────────────────────────────────────────
|
||||
@ -88,9 +94,9 @@ body {
|
||||
background: var(--sidebar-bg);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
height: calc(100vh - var(--topbar-height));
|
||||
position: sticky;
|
||||
top: 0;
|
||||
top: var(--topbar-height);
|
||||
overflow-y: auto;
|
||||
scrollbar-width: none;
|
||||
transition: transform .3s ease;
|
||||
@ -222,11 +228,12 @@ body {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
// ─── Topbar ───────────────────────────────────────────────────────────────────
|
||||
// ─── Topbar (unified — used on all pages) ─────────────────────────────────────
|
||||
.app-topbar {
|
||||
height: var(--topbar-height);
|
||||
background: var(--topbar-bg);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
background: var(--sidebar-bg);
|
||||
border-bottom: 1px solid rgba(255,255,255,.06);
|
||||
box-shadow: 0 2px 12px rgba(0,0,0,.15);
|
||||
padding: 0 1.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@ -234,12 +241,12 @@ body {
|
||||
gap: 1rem;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
z-index: 200;
|
||||
|
||||
.topbar-title {
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
color: #f8fafc;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@ -255,13 +262,13 @@ body {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: .375rem .5rem;
|
||||
color: var(--text-muted);
|
||||
color: rgba(255,255,255,.65);
|
||||
border-radius: .375rem;
|
||||
cursor: pointer;
|
||||
display: none;
|
||||
font-size: 1.25rem;
|
||||
|
||||
&:hover { background: #f1f5f9; color: var(--text-primary); }
|
||||
&:hover { background: rgba(255,255,255,.08); color: #fff; }
|
||||
}
|
||||
|
||||
.sidebar-overlay {
|
||||
@ -283,13 +290,10 @@ body {
|
||||
@media (max-width: 991.98px) {
|
||||
.app-sidebar {
|
||||
position: fixed;
|
||||
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%);
|
||||
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
|
||||
|
||||
&.is-open {
|
||||
transform: translateX(0);
|
||||
@ -553,14 +557,6 @@ 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;
|
||||
@ -605,11 +601,6 @@ 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;
|
||||
@ -690,6 +681,8 @@ 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);
|
||||
@ -699,8 +692,6 @@ body {
|
||||
.app-topbar {
|
||||
height: 54px;
|
||||
padding: 0 1rem;
|
||||
backdrop-filter: none;
|
||||
background: #ffffff;
|
||||
|
||||
.topbar-title { font-size: .9rem; }
|
||||
}
|
||||
@ -714,7 +705,7 @@ body {
|
||||
}
|
||||
|
||||
/* ── Public page ────────────────────────────────────────────────────── */
|
||||
.public-header { padding: .625rem 0; }
|
||||
.app-topbar { padding: .625rem 1rem; }
|
||||
.mob-hero-compact { padding: .875rem 0 1rem !important; }
|
||||
|
||||
/* Section switching — hide inactive section */
|
||||
|
||||
@ -396,10 +396,12 @@
|
||||
{{-- ── STEP 2A : receipt + payment ────────────────────────── --}}
|
||||
<div id="endStep2Pay" style="display:none;" class="p-4">
|
||||
|
||||
<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="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>
|
||||
|
||||
{{-- Times --}}
|
||||
<div class="row g-2 mb-3">
|
||||
@ -469,10 +471,12 @@
|
||||
{{-- ── STEP 2B : force close ───────────────────────────────── --}}
|
||||
<div id="endStep2Force" style="display:none;" class="p-4">
|
||||
|
||||
<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="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>
|
||||
|
||||
<div class="text-center py-2 mb-3">
|
||||
<div style="font-size:2.8rem;margin-bottom:.75rem;">⚠️</div>
|
||||
@ -609,12 +613,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,6 +22,15 @@
|
||||
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
|
||||
|
||||
@ -61,11 +70,15 @@
|
||||
<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->assignedLot)
|
||||
<span class="lot-pill">
|
||||
<i class="bi bi-buildings"></i>
|
||||
{{ $op->assignedLot->name }}
|
||||
</span>
|
||||
@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>
|
||||
@else
|
||||
<span class="no-lot-pill">
|
||||
<i class="bi bi-dash-circle"></i>
|
||||
@ -81,7 +94,7 @@
|
||||
data-name="{{ $op->name }}"
|
||||
data-email="{{ $op->email }}"
|
||||
data-phone="{{ $op->phone ?? '' }}"
|
||||
data-lot="{{ $op->parking_lot_id ?? '' }}">
|
||||
data-lots="{{ json_encode($op->assignedLots->pluck('id')) }}">
|
||||
<i class="bi bi-pencil me-1"></i>تعديل
|
||||
</button>
|
||||
<button class="btn btn-sm fw-600 btn-delete-op"
|
||||
@ -134,13 +147,17 @@
|
||||
<input type="password" id="createPassword" class="form-control" placeholder="8 أحرف على الأقل" dir="ltr">
|
||||
</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>
|
||||
<select id="createLot" class="form-select">
|
||||
<option value="">— بدون تخصيص —</option>
|
||||
@foreach($lots as $lot)
|
||||
<option value="{{ $lot->id }}">{{ $lot->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer border-0 pt-1">
|
||||
@ -186,13 +203,17 @@
|
||||
<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>
|
||||
<select id="editLot" class="form-select">
|
||||
<option value="">— بدون تخصيص —</option>
|
||||
@foreach($lots as $lot)
|
||||
<option value="{{ $lot->id }}">{{ $lot->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer border-0 pt-1">
|
||||
@ -271,7 +292,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
// ── Open create ───────────────────────────────────────────────────────────
|
||||
document.getElementById('btnOpenCreate').addEventListener('click', function () {
|
||||
['createName','createEmail','createPhone','createPassword'].forEach(id => document.getElementById(id).value = '');
|
||||
document.getElementById('createLot').value = '';
|
||||
document.querySelectorAll('.create-lot-cb').forEach(cb => cb.checked = false);
|
||||
document.getElementById('createError').classList.add('d-none');
|
||||
modal('createModal').show();
|
||||
});
|
||||
@ -284,7 +305,10 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
document.getElementById('editEmail').value = this.dataset.email;
|
||||
document.getElementById('editPhone').value = this.dataset.phone || '';
|
||||
document.getElementById('editPassword').value = '';
|
||||
document.getElementById('editLot').value = this.dataset.lot || '';
|
||||
const assignedIds = JSON.parse(this.dataset.lots || '[]');
|
||||
document.querySelectorAll('.edit-lot-cb').forEach(cb => {
|
||||
cb.checked = assignedIds.includes(parseInt(cb.value));
|
||||
});
|
||||
document.getElementById('editError').classList.add('d-none');
|
||||
modal('editModal').show();
|
||||
});
|
||||
@ -309,15 +333,16 @@ 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,
|
||||
parking_lot_id: document.getElementById('createLot').value || null,
|
||||
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,
|
||||
}),
|
||||
});
|
||||
const json = await res.json();
|
||||
@ -343,15 +368,16 @@ 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,
|
||||
parking_lot_id: document.getElementById('editLot').value || null,
|
||||
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,
|
||||
}),
|
||||
});
|
||||
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,16 +72,5 @@
|
||||
إنشاء حساب جديد
|
||||
</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,33 +15,69 @@
|
||||
/* 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 ══════════════════════════════════════════════════════════════ --}}
|
||||
<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>
|
||||
@include('partials.topbar')
|
||||
|
||||
{{-- ══ HERO SEARCH ═════════════════════════════════════════════════════════ --}}
|
||||
<div class="mob-hero-compact" style="background:linear-gradient(135deg,#0f172a 0%,#1e3a5f 100%);padding:2.5rem 0 3rem;">
|
||||
<div class="container text-center">
|
||||
<div class="container-fluid text-center">
|
||||
<h1 class="fw-800 mb-2 d-none d-md-block" style="color:#f8fafc;font-size:clamp(1.4rem,4vw,2rem);">
|
||||
ابحث عن موقف سيارات في دمشق
|
||||
</h1>
|
||||
@ -72,7 +108,7 @@
|
||||
</div>
|
||||
|
||||
{{-- ══ MAIN CONTENT ════════════════════════════════════════════════════════ --}}
|
||||
<div class="container py-4 mob-nav-pad">
|
||||
<div id="mainContent" class="container-fluid py-4 mob-nav-pad">
|
||||
<div class="row g-3">
|
||||
|
||||
{{-- Parking List (RTL: renders on right side, but we put it second so it's on LEFT visually) --}}
|
||||
@ -81,7 +117,7 @@
|
||||
|
||||
{{-- MAP --}}
|
||||
<div class="col-lg-8 order-lg-1 mob-section-map">
|
||||
<div class="card h-100" style="min-height:520px;">
|
||||
<div class="card h-100">
|
||||
<div class="card-header d-flex align-items-center justify-content-between">
|
||||
<span class="fw-700 text-sm" style="color:#0f172a;">
|
||||
<i class="bi bi-map me-1" style="color:#6366f1;"></i>
|
||||
@ -90,7 +126,7 @@
|
||||
<span class="badge badge-soft-primary text-xs" id="map-count">--</span>
|
||||
</div>
|
||||
<div class="card-body p-0" style="border-radius:0 0 .75rem .75rem;overflow:hidden;">
|
||||
<div id="map" style="height:500px;"></div>
|
||||
<div id="map"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -105,7 +141,7 @@
|
||||
</span>
|
||||
<span class="badge badge-soft-success text-xs" id="list-count">{{ $lots->count() }} موقف</span>
|
||||
</div>
|
||||
<div style="max-height:500px;overflow-y:auto;scrollbar-width:thin;" id="parkingList">
|
||||
<div style="overflow-y:auto;scrollbar-width:thin;" id="parkingList">
|
||||
@forelse($lots as $lot)
|
||||
@php
|
||||
$avail = $lot['avail'];
|
||||
@ -130,7 +166,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>
|
||||
@ -370,7 +406,7 @@
|
||||
if (l.avail < l.total * .2) return { cls: 'avail-limited', text: `${l.avail} محدود`, pct: Math.round((l.total - l.avail) / l.total * 100) };
|
||||
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;
|
||||
@ -379,7 +415,7 @@
|
||||
if (mapReady) { map?.invalidateSize(); return; }
|
||||
mapReady = true;
|
||||
|
||||
map = L.map('map').setView([33.5138, 36.2765], 12);
|
||||
map = L.map('map', { attributionControl: false }).setView([33.5138, 36.2765], 12);
|
||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
attribution: '© OpenStreetMap'
|
||||
}).addTo(map);
|
||||
|
||||
@ -17,152 +17,103 @@
|
||||
<div class="app-layout">
|
||||
|
||||
{{-- ════════════════════════════════════════
|
||||
SIDEBAR (appears on the RIGHT in RTL
|
||||
because it is the first flex child)
|
||||
TOPBAR — full width, topmost
|
||||
════════════════════════════════════════ --}}
|
||||
<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>
|
||||
@include('partials.topbar', ['isAdminLayout' => true])
|
||||
|
||||
{{-- ════════════════════════════════════════
|
||||
MAIN BODY
|
||||
INNER ROW — sidebar + content
|
||||
════════════════════════════════════════ --}}
|
||||
<div class="app-body">
|
||||
<div class="app-inner">
|
||||
|
||||
{{-- 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>الموقع العام
|
||||
<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>
|
||||
{{-- 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 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
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{{-- 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>
|
||||
{{-- Page Content --}}
|
||||
<main class="app-content">
|
||||
@yield('content')
|
||||
</main>
|
||||
|
||||
{{-- Page Content --}}
|
||||
<main class="app-content">
|
||||
@yield('content')
|
||||
</main>
|
||||
</div>{{-- /app-body --}}
|
||||
|
||||
</div>{{-- /app-body --}}
|
||||
</div>{{-- /app-inner --}}
|
||||
|
||||
</div>{{-- /app-layout --}}
|
||||
|
||||
@ -171,6 +122,7 @@
|
||||
|
||||
{{-- ══ 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>
|
||||
@ -192,10 +144,22 @@
|
||||
<span>المشغّلون</span>
|
||||
</a>
|
||||
<a href="{{ route('operator.dashboard') }}"
|
||||
class="mob-nav-item {{ request()->routeIs('operator.*') ? 'active' : '' }}">
|
||||
class="mob-nav-item {{ request()->routeIs('operator.dashboard') ? 'active' : '' }}">
|
||||
<i class="bi bi-person-badge"></i>
|
||||
<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,30 +14,10 @@
|
||||
<body style="background:#f1f5f9;font-family:'Cairo',sans-serif;">
|
||||
|
||||
{{-- ══ HEADER ══════════════════════════════════════════════════════════════ --}}
|
||||
<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>
|
||||
@include('partials.topbar')
|
||||
|
||||
{{-- ══ CONTENT ═════════════════════════════════════════════════════════════ --}}
|
||||
<main class="container py-4" style="max-width:820px;">
|
||||
<main style="padding:1.5rem;">
|
||||
|
||||
{{-- 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(!$assignedLotId)
|
||||
@if(count($assignedLotIds) > 1)
|
||||
<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>';
|
||||
|
||||
692
resources/views/operator/stats.blade.php
Normal file
692
resources/views/operator/stats.blade.php
Normal file
@ -0,0 +1,692 @@
|
||||
@extends('layouts.admin')
|
||||
@section('title', 'إحصائيات المشغّل — دمشق باركينغ')
|
||||
@section('page-title', 'إحصائيات المشغّل')
|
||||
|
||||
@section('styles')
|
||||
<style>
|
||||
/* ── KPI Cards ───────────────────────────────────────────────────────────── */
|
||||
.kpi-card {
|
||||
border-radius: 1rem;
|
||||
padding: 1.25rem 1.375rem;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border: none;
|
||||
height: 100%;
|
||||
}
|
||||
.kpi-card::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset-inline-end: -18px;
|
||||
bottom: -18px;
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
border-radius: 50%;
|
||||
opacity: .08;
|
||||
background: currentColor;
|
||||
}
|
||||
.kpi-icon {
|
||||
width: 44px; height: 44px;
|
||||
border-radius: .75rem;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-size: 1.3rem;
|
||||
margin-bottom: .875rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.kpi-value {
|
||||
font-size: 2rem;
|
||||
font-weight: 900;
|
||||
line-height: 1.1;
|
||||
margin-bottom: .2rem;
|
||||
}
|
||||
.kpi-label {
|
||||
font-size: .8rem;
|
||||
font-weight: 600;
|
||||
opacity: .75;
|
||||
}
|
||||
.kpi-sub {
|
||||
font-size: .7rem;
|
||||
opacity: .55;
|
||||
margin-top: .125rem;
|
||||
}
|
||||
|
||||
/* ── Chart cards ─────────────────────────────────────────────────────────── */
|
||||
.chart-card {
|
||||
border-radius: 1rem;
|
||||
border: none;
|
||||
box-shadow: 0 4px 16px rgba(0,0,0,.06);
|
||||
height: 100%;
|
||||
display: flex; flex-direction: column;
|
||||
}
|
||||
.chart-card .card-header {
|
||||
background: transparent;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
padding: 1rem 1.25rem .75rem;
|
||||
border-radius: 1rem 1rem 0 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* ── Lot status cards ────────────────────────────────────────────────────── */
|
||||
.lot-stat-card {
|
||||
background: #fff;
|
||||
border-radius: 1rem;
|
||||
border: none;
|
||||
box-shadow: 0 4px 16px rgba(0,0,0,.06);
|
||||
padding: 1.125rem 1.25rem;
|
||||
transition: transform .2s, box-shadow .2s;
|
||||
}
|
||||
.lot-stat-card:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 12px 28px rgba(0,0,0,.1);
|
||||
}
|
||||
.lot-occ-bar { height: 6px; background: #e2e8f0; border-radius: 4px; overflow: hidden; margin: .625rem 0; }
|
||||
.lot-occ-fill { height: 100%; border-radius: 4px; transition: width .6s cubic-bezier(.4,0,.2,1); }
|
||||
|
||||
/* ── Recent table ────────────────────────────────────────────────────────── */
|
||||
.recent-table th { font-size: .75rem; font-weight: 700; color: #64748b; text-transform: uppercase; letter-spacing: .04em; white-space: nowrap; }
|
||||
.recent-table td { font-size: .85rem; vertical-align: middle; }
|
||||
.plate-badge {
|
||||
display: inline-block;
|
||||
font-family: monospace;
|
||||
font-weight: 800;
|
||||
letter-spacing: 2px;
|
||||
font-size: .82rem;
|
||||
direction: ltr;
|
||||
background: #f1f5f9;
|
||||
color: #1e293b;
|
||||
padding: .2em .6em;
|
||||
border-radius: .4rem;
|
||||
}
|
||||
.pay-badge {
|
||||
font-size: .7rem; font-weight: 700; padding: .25em .65em; border-radius: 20px;
|
||||
}
|
||||
.pay-cash { background: rgba(16,185,129,.1); color: #059669; }
|
||||
.pay-upload { background: rgba(99,102,241,.1); color: #4f46e5; }
|
||||
|
||||
/* ── Work progress card ──────────────────────────────────────────────────── */
|
||||
.work-card {
|
||||
background: #fff;
|
||||
border-radius: 1rem;
|
||||
border: none;
|
||||
box-shadow: 0 4px 16px rgba(0,0,0,.06);
|
||||
padding: 1.25rem 1.5rem;
|
||||
}
|
||||
.day-bar-track {
|
||||
height: 8px; background: #e2e8f0; border-radius: 4px; overflow: hidden; margin: .5rem 0 .25rem;
|
||||
}
|
||||
.day-bar-fill {
|
||||
height: 100%; border-radius: 4px;
|
||||
background: linear-gradient(90deg, #6366f1, #10b981);
|
||||
transition: width .8s cubic-bezier(.4,0,.2,1);
|
||||
}
|
||||
.work-stat {
|
||||
display: flex; flex-direction: column; align-items: center;
|
||||
padding: .75rem 1rem; border-radius: .75rem; flex: 1; min-width: 90px;
|
||||
}
|
||||
.work-stat-value { font-size: 1.5rem; font-weight: 900; line-height: 1.1; }
|
||||
.work-stat-label { font-size: .72rem; font-weight: 600; opacity: .65; margin-top: .15rem; text-align: center; }
|
||||
.trend-chip {
|
||||
display: inline-flex; align-items: center; gap: .2rem;
|
||||
font-size: .7rem; font-weight: 700; padding: .15em .55em; border-radius: 20px;
|
||||
}
|
||||
.trend-up { background: rgba(16,185,129,.1); color: #059669; }
|
||||
.trend-down { background: rgba(239,68,68,.1); color: #dc2626; }
|
||||
.trend-flat { background: rgba(100,116,139,.1); color: #64748b; }
|
||||
|
||||
/* ── No-lots empty state ─────────────────────────────────────────────────── */
|
||||
.empty-state {
|
||||
display: flex; flex-direction: column; align-items: center; justify-content: center;
|
||||
padding: 4rem 2rem; text-align: center;
|
||||
}
|
||||
.empty-icon {
|
||||
width: 80px; height: 80px;
|
||||
background: rgba(99,102,241,.08);
|
||||
border-radius: 50%;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-size: 2.5rem; color: #6366f1; margin-bottom: 1.25rem;
|
||||
}
|
||||
</style>
|
||||
@endsection
|
||||
|
||||
@section('content')
|
||||
|
||||
@if(empty($assignedLotIds))
|
||||
{{-- ── No lots assigned ──────────────────────────────────────────────────── --}}
|
||||
<div class="card border-0 shadow-sm" style="border-radius:1rem;">
|
||||
<div class="empty-state">
|
||||
<div class="empty-icon"><i class="bi bi-bar-chart"></i></div>
|
||||
<h5 class="fw-800 mb-2" style="color:#0f172a;">لا توجد مواقف مخصصة</h5>
|
||||
<p class="mb-0" style="color:#64748b;max-width:340px;">
|
||||
لم يتم تعيينك في أي موقف بعد. تواصل مع المدير لتخصيص مواقف لحسابك.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@else
|
||||
{{-- ── KPI Row ───────────────────────────────────────────────────────────────── --}}
|
||||
<div class="row g-3 mb-4">
|
||||
|
||||
<div class="col-xl col-lg-4 col-sm-6">
|
||||
<div class="kpi-card h-100" style="background:linear-gradient(135deg,#0f172a 0%,#1e293b 100%);color:#e2e8f0;">
|
||||
<div class="kpi-icon" style="background:rgba(99,102,241,.2);">
|
||||
<i class="bi bi-car-front" style="color:#818cf8;"></i>
|
||||
</div>
|
||||
<div class="kpi-value" id="kpi-active" style="color:#fff;">--</div>
|
||||
<div class="kpi-label">السيارات داخل المواقف</div>
|
||||
<div class="kpi-sub">الآن · مشغول فعلياً</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-xl col-lg-4 col-sm-6">
|
||||
<div class="kpi-card h-100" style="background:linear-gradient(135deg,#064e3b 0%,#065f46 100%);color:#d1fae5;">
|
||||
<div class="kpi-icon" style="background:rgba(16,185,129,.2);">
|
||||
<i class="bi bi-box-arrow-in-down" style="color:#34d399;"></i>
|
||||
</div>
|
||||
<div class="kpi-value" id="kpi-checkins" style="color:#fff;">--</div>
|
||||
<div class="kpi-label">دخول اليوم</div>
|
||||
<div class="kpi-sub">منذ منتصف الليل</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-xl col-lg-4 col-sm-6">
|
||||
<div class="kpi-card h-100" style="background:linear-gradient(135deg,#1e1b4b 0%,#312e81 100%);color:#e0e7ff;">
|
||||
<div class="kpi-icon" style="background:rgba(129,140,248,.2);">
|
||||
<i class="bi bi-cash-stack" style="color:#a5b4fc;"></i>
|
||||
</div>
|
||||
<div class="kpi-value" id="kpi-revenue" style="color:#fff;font-size:1.4rem;">--</div>
|
||||
<div class="kpi-label">إيرادات اليوم</div>
|
||||
<div class="kpi-sub">ليرة سورية · مدفوع</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-xl col-lg-4 col-sm-6">
|
||||
<div class="kpi-card h-100" style="background:linear-gradient(135deg,#4a044e 0%,#6b21a8 100%);color:#f3e8ff;">
|
||||
<div class="kpi-icon" style="background:rgba(192,132,252,.2);">
|
||||
<i class="bi bi-p-square" style="color:#c084fc;"></i>
|
||||
</div>
|
||||
<div class="kpi-value" id="kpi-available" style="color:#fff;">--</div>
|
||||
<div class="kpi-label">أماكن متاحة</div>
|
||||
<div class="kpi-sub">مجموع المواقف</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-xl col-lg-4 col-sm-6">
|
||||
<div class="kpi-card h-100" style="background:linear-gradient(135deg,#451a03 0%,#92400e 100%);color:#fef3c7;">
|
||||
<div class="kpi-icon" style="background:rgba(251,191,36,.2);">
|
||||
<i class="bi bi-calendar-event" style="color:#fcd34d;"></i>
|
||||
</div>
|
||||
<div class="kpi-value" id="kpi-reservations" style="color:#fff;">--</div>
|
||||
<div class="kpi-label">حجوزات منتظرة</div>
|
||||
<div class="kpi-sub">لم تُفعَّل بعد</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{{-- ── Work Progress ────────────────────────────────────────────────────────── --}}
|
||||
<div class="work-card mb-4">
|
||||
{{-- Header row --}}
|
||||
<div class="d-flex align-items-center justify-content-between flex-wrap gap-2 mb-3">
|
||||
<div>
|
||||
<h6 class="fw-800 mb-0" style="color:#0f172a;font-size:.95rem;">
|
||||
<i class="bi bi-person-check me-2" style="color:#6366f1;"></i>
|
||||
تقدم العمل اليوم
|
||||
</h6>
|
||||
<p class="mb-0 mt-1" style="font-size:.75rem;color:#94a3b8;" id="work-date">{{ now()->translatedFormat('l، j F Y') }}</p>
|
||||
</div>
|
||||
<div class="d-flex align-items-center gap-2 flex-wrap">
|
||||
<span class="badge" id="completion-badge" style="font-size:.75rem;padding:.4em .8em;"></span>
|
||||
<span style="font-size:.75rem;color:#64748b;">معدل الإنجاز</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Day progress bar --}}
|
||||
<div class="mb-1" style="font-size:.72rem;color:#94a3b8;display:flex;justify-content:space-between;">
|
||||
<span>بداية اليوم 00:00</span>
|
||||
<span id="work-now-label"></span>
|
||||
<span>نهاية اليوم 24:00</span>
|
||||
</div>
|
||||
<div class="day-bar-track">
|
||||
<div class="day-bar-fill" id="day-progress-fill" style="width:0%;"></div>
|
||||
</div>
|
||||
<p class="mb-3" style="font-size:.7rem;color:#94a3b8;text-align:center;" id="day-pct-label"></p>
|
||||
|
||||
{{-- Work stats row --}}
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
|
||||
<div class="work-stat" style="background:rgba(16,185,129,.07);color:#065f46;">
|
||||
<div class="work-stat-value" id="ws-checkins">--</div>
|
||||
<div class="work-stat-label">دخول اليوم</div>
|
||||
<div id="ws-vs-yesterday" class="mt-1"></div>
|
||||
</div>
|
||||
|
||||
<div class="work-stat" style="background:rgba(99,102,241,.07);color:#312e81;">
|
||||
<div class="work-stat-value" id="ws-checkouts">--</div>
|
||||
<div class="work-stat-label">خروج مكتمل</div>
|
||||
</div>
|
||||
|
||||
<div class="work-stat" style="background:rgba(239,68,68,.07);color:#7f1d1d;">
|
||||
<div class="work-stat-value" id="ws-cancelled">--</div>
|
||||
<div class="work-stat-label">ملغي</div>
|
||||
</div>
|
||||
|
||||
<div class="work-stat" style="background:rgba(245,158,11,.07);color:#78350f;">
|
||||
<div class="work-stat-value" id="ws-avg-stay">--</div>
|
||||
<div class="work-stat-label">متوسط المدة</div>
|
||||
</div>
|
||||
|
||||
<div class="work-stat" style="background:rgba(14,165,233,.07);color:#0c4a6e;">
|
||||
<div class="work-stat-value" id="ws-peak-hour">--</div>
|
||||
<div class="work-stat-label">الساعة الأكثر ازدحاماً</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- ── Charts ────────────────────────────────────────────────────────────────── --}}
|
||||
<div class="row g-3 mb-4">
|
||||
|
||||
{{-- Daily check-ins bar chart --}}
|
||||
<div class="col-lg-5">
|
||||
<div class="chart-card card">
|
||||
<div class="card-header d-flex align-items-center justify-content-between">
|
||||
<span class="fw-700" style="font-size:.9rem;">
|
||||
<i class="bi bi-bar-chart-line me-2" style="color:#10b981;"></i>
|
||||
الدخول — آخر 7 أيام
|
||||
</span>
|
||||
<span class="badge" style="background:rgba(16,185,129,.1);color:#059669;font-size:.72rem;">دخول</span>
|
||||
</div>
|
||||
<div class="card-body p-3 flex-grow-1" style="min-height:220px;position:relative;">
|
||||
<canvas id="checkinsChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Hourly activity today --}}
|
||||
<div class="col-lg-4">
|
||||
<div class="chart-card card">
|
||||
<div class="card-header d-flex align-items-center justify-content-between">
|
||||
<span class="fw-700" style="font-size:.9rem;">
|
||||
<i class="bi bi-clock me-2" style="color:#f59e0b;"></i>
|
||||
توزيع الدخول بالساعة
|
||||
</span>
|
||||
<span class="badge" style="background:rgba(245,158,11,.1);color:#d97706;font-size:.72rem;">اليوم</span>
|
||||
</div>
|
||||
<div class="card-body p-3 flex-grow-1" style="min-height:220px;position:relative;">
|
||||
<canvas id="hourlyChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Daily revenue line chart --}}
|
||||
<div class="col-lg-3">
|
||||
<div class="chart-card card">
|
||||
<div class="card-header d-flex align-items-center justify-content-between">
|
||||
<span class="fw-700" style="font-size:.9rem;">
|
||||
<i class="bi bi-graph-up-arrow me-2" style="color:#818cf8;"></i>
|
||||
الإيرادات
|
||||
</span>
|
||||
<span class="badge" style="background:rgba(99,102,241,.1);color:#6366f1;font-size:.72rem;">ليرة سورية</span>
|
||||
</div>
|
||||
<div class="card-body p-3 flex-grow-1" style="min-height:220px;position:relative;">
|
||||
<canvas id="revenueChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{{-- ── Per-lot status (only when operator has >1 lot) ────────────────────────── --}}
|
||||
@if(count($lots) > 1)
|
||||
<div class="mb-4">
|
||||
<h6 class="fw-700 mb-3" style="color:#0f172a;font-size:.9rem;">
|
||||
<i class="bi bi-buildings me-2" style="color:#6366f1;"></i>
|
||||
حالة المواقف المخصصة
|
||||
</h6>
|
||||
<div class="row g-3" id="lotCards">
|
||||
@foreach($lots as $lot)
|
||||
<div class="col-lg-4 col-sm-6">
|
||||
<div class="lot-stat-card">
|
||||
<div class="d-flex align-items-start justify-content-between mb-2">
|
||||
<div>
|
||||
<div class="fw-800" style="font-size:.95rem;color:#0f172a;">{{ $lot->name }}</div>
|
||||
<div style="font-size:.75rem;color:#94a3b8;">{{ $lot->address }}</div>
|
||||
</div>
|
||||
<span class="lot-status-badge-{{ $lot->id }} badge" style="font-size:.7rem;"></span>
|
||||
</div>
|
||||
<div class="lot-occ-bar">
|
||||
<div class="lot-occ-fill lot-fill-{{ $lot->id }}" style="width:0;background:#6366f1;"></div>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between" style="font-size:.78rem;color:#64748b;">
|
||||
<span><i class="bi bi-car-front me-1"></i><span class="lot-active-{{ $lot->id }}">—</span> مشغول</span>
|
||||
<span><span class="lot-avail-{{ $lot->id }}">—</span> / {{ $lot->total_capacity }} متاح</span>
|
||||
</div>
|
||||
<div class="mt-2 pt-2 border-top d-flex gap-3" style="font-size:.75rem;color:#64748b;border-color:#f1f5f9!important;">
|
||||
<span><i class="bi bi-box-arrow-in-down me-1"></i>دخول اليوم: <strong class="lot-today-{{ $lot->id }}">—</strong></span>
|
||||
<span><i class="bi bi-cash me-1"></i>إيراد: <strong class="lot-rev-{{ $lot->id }}">—</strong></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- ── Recent completions table ─────────────────────────────────────────────── --}}
|
||||
<div class="card border-0 shadow-sm" style="border-radius:1rem;overflow:hidden;">
|
||||
<div class="card-header bg-transparent d-flex align-items-center justify-content-between"
|
||||
style="padding:1rem 1.25rem .75rem;border-bottom:1px solid #f1f5f9;">
|
||||
<span class="fw-700" style="font-size:.9rem;">
|
||||
<i class="bi bi-clock-history me-2" style="color:#0ea5e9;"></i>
|
||||
آخر المدفوعات المنجزة
|
||||
</span>
|
||||
<span class="badge" id="last-refresh" style="background:#f1f5f9;color:#64748b;font-size:.7rem;"></span>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table recent-table mb-0">
|
||||
<thead style="background:#f8fafc;">
|
||||
<tr>
|
||||
<th class="px-4 py-3">اللوحة</th>
|
||||
<th class="py-3">الموقف</th>
|
||||
<th class="py-3">العميل</th>
|
||||
<th class="py-3">وقت الدفع</th>
|
||||
<th class="py-3 text-center">الرسوم</th>
|
||||
<th class="py-3 text-center">الدفع</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="recent-tbody">
|
||||
<tr>
|
||||
<td colspan="6" class="text-center py-5" style="color:#94a3b8;">
|
||||
<div class="spinner-border spinner-border-sm me-2"></div>
|
||||
جاري التحميل...
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@endif {{-- assignedLotIds not empty --}}
|
||||
|
||||
@endsection
|
||||
|
||||
@push('scripts')
|
||||
@if(!empty($assignedLotIds))
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.7/dist/chart.umd.min.js"></script>
|
||||
<script>
|
||||
let checkinsChart, revenueChart, hourlyChart;
|
||||
|
||||
const LOTS = @json($lots->map(fn($l) => ['id' => $l->id, 'name' => $l->name]));
|
||||
const MULTI_LOT = LOTS.length > 1;
|
||||
|
||||
// ── Day progress bar (runs on its own clock) ──────────────────────────────────
|
||||
function updateDayProgress() {
|
||||
const now = new Date();
|
||||
const pct = Math.round((now.getHours() * 60 + now.getMinutes()) / 1440 * 100);
|
||||
const fill = document.getElementById('day-progress-fill');
|
||||
const lbl = document.getElementById('day-pct-label');
|
||||
const now2 = document.getElementById('work-now-label');
|
||||
if (fill) fill.style.width = pct + '%';
|
||||
if (lbl) lbl.textContent = `${pct}% من اليوم مضى`;
|
||||
if (now2) now2.textContent = now.toLocaleTimeString('ar-SA', {hour:'2-digit', minute:'2-digit'}) + ' الآن';
|
||||
}
|
||||
updateDayProgress();
|
||||
setInterval(updateDayProgress, 60000);
|
||||
|
||||
// ── Duration formatter ────────────────────────────────────────────────────────
|
||||
function fmtDuration(mins) {
|
||||
if (!mins) return '—';
|
||||
const h = Math.floor(mins / 60);
|
||||
const m = mins % 60;
|
||||
return h > 0 ? `${h}س ${m}د` : `${m}د`;
|
||||
}
|
||||
|
||||
// ── Load & render ─────────────────────────────────────────────────────────────
|
||||
async function loadStats() {
|
||||
try {
|
||||
const { success, data } = await fetch('{{ route("operator.statsData") }}').then(r => r.json());
|
||||
if (!success) return;
|
||||
|
||||
// KPI cards
|
||||
document.getElementById('kpi-active').textContent = data.active_cars;
|
||||
document.getElementById('kpi-checkins').textContent = data.checkins_today;
|
||||
document.getElementById('kpi-revenue').textContent = data.revenue_today.toLocaleString('ar-SA') + ' ليرة سورية';
|
||||
document.getElementById('kpi-available').textContent = data.available_spaces;
|
||||
document.getElementById('kpi-reservations').textContent = data.pending_reservations;
|
||||
|
||||
// ── Work progress ──────────────────────────────────────────────────────
|
||||
document.getElementById('ws-checkins').textContent = data.checkins_today;
|
||||
document.getElementById('ws-checkouts').textContent = data.checkouts_today;
|
||||
document.getElementById('ws-cancelled').textContent = data.cancelled_today;
|
||||
document.getElementById('ws-avg-stay').textContent = fmtDuration(data.avg_duration_min);
|
||||
|
||||
const peak = data.peak_hour;
|
||||
const peakStr = peak !== null && peak !== undefined
|
||||
? new Date(2000,0,1,peak).toLocaleTimeString('ar-SA', {hour:'2-digit', minute:'2-digit'})
|
||||
: '—';
|
||||
document.getElementById('ws-peak-hour').textContent = peakStr;
|
||||
|
||||
// Completion rate badge
|
||||
const cr = data.completion_rate ?? 100;
|
||||
const crEl = document.getElementById('completion-badge');
|
||||
if (crEl) {
|
||||
crEl.textContent = cr + '%';
|
||||
crEl.style.cssText = cr >= 80
|
||||
? 'background:rgba(16,185,129,.12);color:#059669;font-size:.75rem;padding:.4em .8em;'
|
||||
: cr >= 50
|
||||
? 'background:rgba(245,158,11,.12);color:#d97706;font-size:.75rem;padding:.4em .8em;'
|
||||
: 'background:rgba(239,68,68,.12);color:#dc2626;font-size:.75rem;padding:.4em .8em;';
|
||||
}
|
||||
|
||||
// vs yesterday trend chip
|
||||
const vsel = document.getElementById('ws-vs-yesterday');
|
||||
if (vsel) {
|
||||
const diff = data.checkins_today - data.checkins_yesterday;
|
||||
const cls = diff > 0 ? 'trend-up' : diff < 0 ? 'trend-down' : 'trend-flat';
|
||||
const icon = diff > 0 ? 'bi-arrow-up' : diff < 0 ? 'bi-arrow-down' : 'bi-dash';
|
||||
const lbl = diff > 0 ? `+${diff} عن أمس` : diff < 0 ? `${diff} عن أمس` : 'مثل أمس';
|
||||
vsel.innerHTML = `<span class="trend-chip ${cls}"><i class="bi ${icon}"></i>${lbl}</span>`;
|
||||
}
|
||||
|
||||
// Charts
|
||||
renderCheckinsChart(data.daily_checkins, data.daily_dates);
|
||||
renderRevenueChart(data.daily_revenue, data.daily_dates);
|
||||
renderHourlyChart(data.hourly_activity ?? []);
|
||||
|
||||
// Per-lot cards
|
||||
if (MULTI_LOT && data.lot_stats) {
|
||||
data.lot_stats.forEach(lot => {
|
||||
const pct = lot.pct;
|
||||
const fill = document.querySelector(`.lot-fill-${lot.id}`);
|
||||
const badge = document.querySelector(`.lot-status-badge-${lot.id}`);
|
||||
if (fill) { fill.style.width = pct + '%'; fill.style.background = pct >= 90 ? '#ef4444' : pct >= 60 ? '#f59e0b' : '#10b981'; }
|
||||
if (badge) {
|
||||
badge.textContent = pct >= 90 ? 'ممتلئ' : pct >= 60 ? 'مشغول' : 'متاح';
|
||||
badge.style.cssText = pct >= 90
|
||||
? 'background:rgba(239,68,68,.1);color:#dc2626;'
|
||||
: pct >= 60
|
||||
? 'background:rgba(245,158,11,.1);color:#d97706;'
|
||||
: 'background:rgba(16,185,129,.1);color:#059669;';
|
||||
}
|
||||
const setText = (sel, val) => { const el = document.querySelector(sel); if (el) el.textContent = val; };
|
||||
setText(`.lot-active-${lot.id}`, lot.active);
|
||||
setText(`.lot-avail-${lot.id}`, lot.available);
|
||||
setText(`.lot-today-${lot.id}`, lot.today_ins);
|
||||
setText(`.lot-rev-${lot.id}`, lot.today_rev.toLocaleString('ar-SA'));
|
||||
});
|
||||
}
|
||||
|
||||
// Recent completions table
|
||||
renderRecentTable(data.recent_completions ?? []);
|
||||
|
||||
document.getElementById('last-refresh').textContent = 'تحديث ' + new Date().toLocaleTimeString('ar-SA', {hour:'2-digit', minute:'2-digit'});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Bar chart: check-ins ──────────────────────────────────────────────────────
|
||||
function renderCheckinsChart(counts, dates) {
|
||||
const labels = dates.map(d => {
|
||||
const dt = new Date(d + 'T00:00:00');
|
||||
return dt.toLocaleDateString('ar-SA', { weekday: 'short', day: 'numeric' });
|
||||
});
|
||||
const fullLabels = dates.map(d => {
|
||||
const dt = new Date(d + 'T00:00:00');
|
||||
return dt.toLocaleDateString('ar-SA', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' });
|
||||
});
|
||||
|
||||
if (checkinsChart) { checkinsChart.destroy(); }
|
||||
checkinsChart = new Chart(document.getElementById('checkinsChart'), {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels,
|
||||
datasets: [{
|
||||
data: counts,
|
||||
backgroundColor: counts.map((_, i) => i === counts.length - 1 ? 'rgba(16,185,129,.9)' : 'rgba(16,185,129,.25)'),
|
||||
borderColor: counts.map((_, i) => i === counts.length - 1 ? '#10b981' : 'rgba(16,185,129,.4)'),
|
||||
borderWidth: 2,
|
||||
borderRadius: 8,
|
||||
hoverBackgroundColor: 'rgba(16,185,129,.7)',
|
||||
}],
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: { display: false },
|
||||
tooltip: { callbacks: {
|
||||
title: ctx => fullLabels[ctx[0].dataIndex],
|
||||
label: ctx => ` ${ctx.parsed.y} دخول`,
|
||||
}},
|
||||
},
|
||||
scales: {
|
||||
y: { beginAtZero: true, ticks: { precision: 0 }, grid: { color: '#f8fafc' } },
|
||||
x: { grid: { display: false } },
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// ── Line chart: revenue ───────────────────────────────────────────────────────
|
||||
function renderRevenueChart(amounts, dates) {
|
||||
const labels = dates.map(d => {
|
||||
const dt = new Date(d + 'T00:00:00');
|
||||
return dt.toLocaleDateString('ar-SA', { weekday: 'short', day: 'numeric' });
|
||||
});
|
||||
|
||||
if (revenueChart) { revenueChart.destroy(); }
|
||||
revenueChart = new Chart(document.getElementById('revenueChart'), {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels,
|
||||
datasets: [{
|
||||
data: amounts,
|
||||
borderColor: '#818cf8',
|
||||
backgroundColor: 'rgba(99,102,241,.08)',
|
||||
borderWidth: 2.5,
|
||||
pointBackgroundColor: '#6366f1',
|
||||
pointRadius: 4,
|
||||
pointHoverRadius: 6,
|
||||
fill: true,
|
||||
tension: 0.4,
|
||||
}],
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: { display: false },
|
||||
tooltip: { callbacks: {
|
||||
label: ctx => ` ${ctx.parsed.y.toLocaleString('ar-SA')} ليرة سورية`,
|
||||
}},
|
||||
},
|
||||
scales: {
|
||||
y: { beginAtZero: true, grid: { color: '#f8fafc' }, ticks: { callback: v => v.toLocaleString('ar-SA') } },
|
||||
x: { grid: { display: false } },
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// ── Hourly activity chart ─────────────────────────────────────────────────────
|
||||
function renderHourlyChart(hourly) {
|
||||
// Show only hours 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
|
||||
33
resources/views/partials/topbar.blade.php
Normal file
33
resources/views/partials/topbar.blade.php
Normal file
@ -0,0 +1,33 @@
|
||||
<header class="app-topbar">
|
||||
|
||||
{{-- Left: logo + optional admin controls --}}
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
|
||||
{{-- Brand logo (always shown, links to public site) --}}
|
||||
<a href="{{ route('parking.index') }}" class="d-flex align-items-center gap-3 text-decoration-none">
|
||||
<div style="width:36px;height:36px;background:#6366f1;border-radius:.5rem;display:flex;align-items:center;justify-content:center;flex-shrink:0;">
|
||||
<i class="bi bi-p-square-fill text-white" style="font-size:1.1rem;"></i>
|
||||
</div>
|
||||
<div class="d-none d-sm-block">
|
||||
<div class="fw-800" style="color:#f8fafc;font-size:1rem;line-height:1.2;">دمشق باركينغ</div>
|
||||
<div style="color:#94a3b8;font-size:.68rem;">مواقف السيارات في دمشق</div>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
@if($isAdminLayout ?? false)
|
||||
{{-- Sidebar toggle (admin/operator pages only) --}}
|
||||
<button class="sidebar-toggle" id="sidebarToggle" aria-label="قائمة التنقل">
|
||||
<i class="bi bi-list"></i>
|
||||
</button>
|
||||
{{-- Page title --}}
|
||||
<h1 class="topbar-title">@yield('page-title', 'لوحة التحكم')</h1>
|
||||
@endif
|
||||
|
||||
</div>
|
||||
|
||||
{{-- Right: user dropdown --}}
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
@include('partials.user-dropdown')
|
||||
</div>
|
||||
|
||||
</header>
|
||||
@ -6,8 +6,7 @@
|
||||
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;">
|
||||
{{-- 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);">
|
||||
<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);">
|
||||
{{ 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;">
|
||||
@ -32,7 +31,6 @@
|
||||
<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) {
|
||||
@ -53,6 +51,16 @@
|
||||
|
||||
<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') }}"
|
||||
@ -63,7 +71,8 @@
|
||||
</a>
|
||||
</li>
|
||||
|
||||
{{-- My reservations --}}
|
||||
{{-- My reservations (regular users) --}}
|
||||
@if($role === 'user')
|
||||
<li>
|
||||
<a href="{{ route('user.dashboard') }}"
|
||||
class="dropdown-item d-flex align-items-center gap-2 rounded-2"
|
||||
@ -72,9 +81,10 @@
|
||||
حجوزاتي
|
||||
</a>
|
||||
</li>
|
||||
@endif
|
||||
|
||||
{{-- Operator panel (operators & admins) --}}
|
||||
@if(in_array(auth()->user()->role, ['operator', 'admin']))
|
||||
@if(in_array($role, ['operator', 'admin']))
|
||||
<li>
|
||||
<a href="{{ route('operator.dashboard') }}"
|
||||
class="dropdown-item d-flex align-items-center gap-2 rounded-2"
|
||||
@ -86,7 +96,7 @@
|
||||
@endif
|
||||
|
||||
{{-- Admin panel (admins only) --}}
|
||||
@if(auth()->user()->role === 'admin')
|
||||
@if($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,6 +54,8 @@ 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
Normal file → Executable file
0
storage/app/.gitignore
vendored
Normal file → Executable file
0
storage/app/private/.gitignore
vendored
Normal file → Executable file
0
storage/app/private/.gitignore
vendored
Normal file → Executable file
0
storage/app/public/.gitignore
vendored
Normal file → Executable file
0
storage/app/public/.gitignore
vendored
Normal file → Executable file
0
storage/framework/.gitignore
vendored
Normal file → Executable file
0
storage/framework/.gitignore
vendored
Normal file → Executable file
0
storage/framework/cache/.gitignore
vendored
Normal file → Executable file
0
storage/framework/cache/.gitignore
vendored
Normal file → Executable file
0
storage/framework/cache/data/.gitignore
vendored
Normal file → Executable file
0
storage/framework/cache/data/.gitignore
vendored
Normal file → Executable file
0
storage/framework/sessions/.gitignore
vendored
Normal file → Executable file
0
storage/framework/sessions/.gitignore
vendored
Normal file → Executable file
0
storage/framework/testing/.gitignore
vendored
Normal file → Executable file
0
storage/framework/testing/.gitignore
vendored
Normal file → Executable file
0
storage/framework/views/.gitignore
vendored
Normal file → Executable file
0
storage/framework/views/.gitignore
vendored
Normal file → Executable file
0
storage/logs/.gitignore
vendored
Normal file → Executable file
0
storage/logs/.gitignore
vendored
Normal file → Executable file
24
vendor/composer/autoload_classmap.php
vendored
24
vendor/composer/autoload_classmap.php
vendored
@ -6,7 +6,28 @@ $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',
|
||||
@ -132,7 +153,10 @@ 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,7 +536,28 @@ 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',
|
||||
@ -662,7 +683,10 @@ 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' => '1.0.0+no-version-set',
|
||||
'version' => '1.0.0.0',
|
||||
'reference' => null,
|
||||
'pretty_version' => 'dev-main',
|
||||
'version' => 'dev-main',
|
||||
'reference' => 'b03c1d4619de986420534fe46341f4da4d088f76',
|
||||
'type' => 'project',
|
||||
'install_path' => __DIR__ . '/../../',
|
||||
'aliases' => array(),
|
||||
@ -398,9 +398,9 @@
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'laravel/laravel' => array(
|
||||
'pretty_version' => '1.0.0+no-version-set',
|
||||
'version' => '1.0.0.0',
|
||||
'reference' => null,
|
||||
'pretty_version' => 'dev-main',
|
||||
'version' => 'dev-main',
|
||||
'reference' => 'b03c1d4619de986420534fe46341f4da4d088f76',
|
||||
'type' => 'project',
|
||||
'install_path' => __DIR__ . '/../../',
|
||||
'aliases' => array(),
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user