Compare commits
No commits in common. "feature/multi-lot-operator-assignment" and "main" have entirely different histories.
feature/mu
...
main
@ -20,22 +20,7 @@
|
|||||||
"Bash(php -r \"echo file_put_contents\\('C:/xampp/htdocs/scp-syria/storage/app/public/test.txt', 'ok'\\) ? 'write OK' : 'write FAIL';\")",
|
"Bash(php -r \"echo file_put_contents\\('C:/xampp/htdocs/scp-syria/storage/app/public/test.txt', 'ok'\\) ? 'write OK' : 'write FAIL';\")",
|
||||||
"Bash(curl:*)",
|
"Bash(curl:*)",
|
||||||
"Bash(php:*)",
|
"Bash(php:*)",
|
||||||
"Bash(grep -v \"^$\")",
|
"Bash(grep -v \"^$\")"
|
||||||
"Bash(git config *)",
|
|
||||||
"Bash(/usr/local/bin/npm run *)",
|
|
||||||
"Bash(node_modules/.bin/vite build *)",
|
|
||||||
"Read(//usr/local/nvm/versions/**)",
|
|
||||||
"Read(//root/.nvm/versions/**)",
|
|
||||||
"Read(//opt/**)",
|
|
||||||
"Bash(/root/.vscode-server/cli/servers/Stable-560a9dba96f961efea7b1612916f89e5d5d4d679/server/node /var/www/scp-syria/node_modules/vite/bin/vite.js build --config /var/www/scp-syria/vite.config.js)",
|
|
||||||
"Bash(grep -o \".\\\\{0,80\\\\}inset-inline-end:0.\\\\{0,80\\\\}\" /var/www/scp-syria/public/build/assets/app-BtJpbOwL.css)",
|
|
||||||
"Bash(grep -o \".\\\\{0,150\\\\}sidebar-toggle.\\\\{0,20\\\\}display:none.\\\\{0,200\\\\}\" /var/www/scp-syria/public/build/assets/app-BtJpbOwL.css)",
|
|
||||||
"Bash(grep -o \".\\\\{0,60\\\\}inset-inline.\\\\{0,80\\\\}height:100%.\\\\{0,30\\\\}\" /var/www/scp-syria/public/build/assets/app-BtJpbOwL.css)",
|
|
||||||
"Bash(grep -o \".\\\\{0,10\\\\}app-sidebar.\\\\{0,40\\\\}display:none.\\\\{0,10\\\\}\" /var/www/scp-syria/public/build/assets/app-BtJpbOwL.css)",
|
|
||||||
"Bash(node node_modules/vite/bin/vite.js build)",
|
|
||||||
"Bash(/root/.vscode-server/cli/servers/Stable-41dd792b5e652393e7787322889ed5fdc58bd75b/server/node /var/www/scp-syria/node_modules/vite/bin/vite.js build --root /var/www/scp-syria)",
|
|
||||||
"Bash(/root/.vscode-server/cli/servers/Stable-41dd792b5e652393e7787322889ed5fdc58bd75b/server/node node_modules/vite/bin/vite.js build)",
|
|
||||||
"Bash(python3 *)"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
48
.env
48
.env
@ -1,23 +1,44 @@
|
|||||||
APP_NAME="Damascus Parking"
|
APP_NAME="Smart Car Park"
|
||||||
APP_ENV=local
|
APP_ENV=local
|
||||||
APP_KEY=base64:gC4TfmQLkKdWmH/vRQ9KY//WgH0w8+RhkVuswqCbgxo=
|
APP_KEY=base64:92pMpQ2HV7icz1lfUZUQfUguPfUdqYUPVU+aWdYDstY=
|
||||||
APP_DEBUG=true
|
APP_DEBUG=true
|
||||||
APP_TIMEZONE=Asia/Damascus
|
APP_URL=http://localhost
|
||||||
APP_URL=http://localhost:8000
|
|
||||||
|
APP_LOCALE=en
|
||||||
|
APP_FALLBACK_LOCALE=en
|
||||||
|
APP_FAKER_LOCALE=en_US
|
||||||
|
|
||||||
|
APP_MAINTENANCE_DRIVER=file
|
||||||
|
# APP_MAINTENANCE_STORE=database
|
||||||
|
|
||||||
|
# PHP_CLI_SERVER_WORKERS=4
|
||||||
|
|
||||||
|
BCRYPT_ROUNDS=12
|
||||||
|
|
||||||
LOG_CHANNEL=stack
|
LOG_CHANNEL=stack
|
||||||
|
LOG_STACK=single
|
||||||
LOG_DEPRECATIONS_CHANNEL=null
|
LOG_DEPRECATIONS_CHANNEL=null
|
||||||
LOG_LEVEL=debug
|
LOG_LEVEL=debug
|
||||||
|
|
||||||
DB_CONNECTION=sqlite
|
DB_CONNECTION=sqlite
|
||||||
DB_DATABASE=/var/www/scp-syria/database/database.sqlite
|
# DB_HOST=127.0.0.1
|
||||||
|
# DB_PORT=3306
|
||||||
|
# DB_DATABASE=laravel
|
||||||
|
# DB_USERNAME=root
|
||||||
|
# DB_PASSWORD=
|
||||||
|
|
||||||
BROADCAST_CONNECTION=log
|
|
||||||
CACHE_STORE=database
|
|
||||||
FILESYSTEM_DISK=local
|
|
||||||
QUEUE_CONNECTION=sync
|
|
||||||
SESSION_DRIVER=database
|
SESSION_DRIVER=database
|
||||||
SESSION_LIFETIME=120
|
SESSION_LIFETIME=120
|
||||||
|
SESSION_ENCRYPT=false
|
||||||
|
SESSION_PATH=/
|
||||||
|
SESSION_DOMAIN=null
|
||||||
|
|
||||||
|
BROADCAST_CONNECTION=log
|
||||||
|
FILESYSTEM_DISK=local
|
||||||
|
QUEUE_CONNECTION=database
|
||||||
|
|
||||||
|
CACHE_STORE=database
|
||||||
|
# CACHE_PREFIX=
|
||||||
|
|
||||||
MEMCACHED_HOST=127.0.0.1
|
MEMCACHED_HOST=127.0.0.1
|
||||||
|
|
||||||
@ -26,12 +47,12 @@ REDIS_HOST=127.0.0.1
|
|||||||
REDIS_PASSWORD=null
|
REDIS_PASSWORD=null
|
||||||
REDIS_PORT=6379
|
REDIS_PORT=6379
|
||||||
|
|
||||||
MAIL_MAILER=smtp
|
MAIL_MAILER=log
|
||||||
MAIL_HOST=mailpit
|
MAIL_SCHEME=null
|
||||||
MAIL_PORT=1025
|
MAIL_HOST=127.0.0.1
|
||||||
|
MAIL_PORT=2525
|
||||||
MAIL_USERNAME=null
|
MAIL_USERNAME=null
|
||||||
MAIL_PASSWORD=null
|
MAIL_PASSWORD=null
|
||||||
MAIL_ENCRYPTION=null
|
|
||||||
MAIL_FROM_ADDRESS="hello@example.com"
|
MAIL_FROM_ADDRESS="hello@example.com"
|
||||||
MAIL_FROM_NAME="${APP_NAME}"
|
MAIL_FROM_NAME="${APP_NAME}"
|
||||||
|
|
||||||
@ -42,4 +63,3 @@ AWS_BUCKET=
|
|||||||
AWS_USE_PATH_STYLE_ENDPOINT=false
|
AWS_USE_PATH_STYLE_ENDPOINT=false
|
||||||
|
|
||||||
VITE_APP_NAME="${APP_NAME}"
|
VITE_APP_NAME="${APP_NAME}"
|
||||||
|
|
||||||
|
|||||||
43
CLAUDE.md
43
CLAUDE.md
@ -2,28 +2,6 @@
|
|||||||
|
|
||||||
## Working Rules
|
## Working Rules
|
||||||
|
|
||||||
### Back Buttons Must Be on the Far Left in RTL
|
|
||||||
|
|
||||||
In an RTL layout, "back" navigation buttons must always appear on the **far left** end of the header row — not the right. In RTL flex rows, the first HTML child lands on the right, so placing the back button first puts it on the wrong side.
|
|
||||||
|
|
||||||
**The fix:** always place the back button **last** in the flex row HTML (so it lands on the left in RTL flow), or combine it with `ms-auto` to push it to the left end.
|
|
||||||
|
|
||||||
```html
|
|
||||||
{{-- CORRECT: button last in RTL flex row → appears on the far left --}}
|
|
||||||
<div class="d-flex align-items-center gap-3">
|
|
||||||
<div>
|
|
||||||
<h1>Page Title</h1>
|
|
||||||
</div>
|
|
||||||
<a href="..." class="btn btn-sm ms-auto">
|
|
||||||
<i class="bi bi-arrow-right me-1"></i>العودة
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
```
|
|
||||||
|
|
||||||
**Rule: never place a back/return button as the first child of a flex row — it will appear on the right side in RTL. Always put it last.**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Modal Close Button Must Be on the Far Left in RTL
|
### Modal Close Button Must Be on the Far Left in RTL
|
||||||
|
|
||||||
Bootstrap compiles `.modal-header .btn-close` with physical `margin-left: auto`, which in RTL pushes the × button to the right (next to the title text) instead of to the far left end of the header.
|
Bootstrap compiles `.modal-header .btn-close` with physical `margin-left: auto`, which in RTL pushes the × button to the right (next to the title text) instead of to the far left end of the header.
|
||||||
@ -158,14 +136,12 @@ resources/
|
|||||||
|
|
||||||
| Role | Middleware | Access |
|
| Role | Middleware | Access |
|
||||||
|------------|-----------------|---------------------------------------------|
|
|------------|-----------------|---------------------------------------------|
|
||||||
| `admin` | `EnsureAdmin` | Full admin dashboard, parking lot CRUD, all bookings, operator management |
|
| `admin` | `EnsureAdmin` | Full admin dashboard, parking lot CRUD, all bookings |
|
||||||
| `operator` | `EnsureOperator`| Vehicle check-in/check-out, stats dashboard — restricted to assigned lots only |
|
| `operator` | `EnsureOperator`| Vehicle check-in/check-out, active bookings for assigned lot |
|
||||||
| `user` | (auth only) | Public search, create bookings via API |
|
| `user` | (auth only) | Public search, create bookings via API |
|
||||||
|
|
||||||
Both middleware classes return Arabic 403 messages on unauthorized access.
|
Both middleware classes return Arabic 403 messages on unauthorized access.
|
||||||
|
|
||||||
**Operator lot assignment:** uses pivot table `operator_parking_lot` (many-to-many). An operator with no lots assigned sees no lots. The admin assigns lots via the operators management page. See `User::assignedLots()` BelongsToMany.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Database Schema
|
## Database Schema
|
||||||
@ -178,15 +154,6 @@ Both middleware classes return Arabic 403 messages on unauthorized access.
|
|||||||
| email | string unique | |
|
| email | string unique | |
|
||||||
| password | string | bcrypt |
|
| password | string | bcrypt |
|
||||||
| role | string | 'admin' \| 'operator' \| 'user' (default) |
|
| role | string | 'admin' \| 'operator' \| 'user' (default) |
|
||||||
| phone | string nullable | |
|
|
||||||
| parking_lot_id | bigint nullable | **Unused legacy column** — left in DB due to SQLite FK drop limitation. All operator-lot assignments now live in `operator_parking_lot` pivot. |
|
|
||||||
|
|
||||||
### `operator_parking_lot` (pivot)
|
|
||||||
| Column | Type | Notes |
|
|
||||||
|--------|------|-------|
|
|
||||||
| user_id | FK → users | cascadeOnDelete |
|
|
||||||
| parking_lot_id | FK → parking_lots | cascadeOnDelete |
|
|
||||||
| unique | (user_id, parking_lot_id) | one row per assignment |
|
|
||||||
|
|
||||||
### `parking_lots`
|
### `parking_lots`
|
||||||
| Column | Type | Notes |
|
| Column | Type | Notes |
|
||||||
@ -264,9 +231,7 @@ Check-in requires: `parking_lot_id`, `vehicle_plate`, `duration_hours` (0.25–2
|
|||||||
| `POST /admin/parking-lots/{id}/toggle` | admin | Toggle active status |
|
| `POST /admin/parking-lots/{id}/toggle` | admin | Toggle active status |
|
||||||
| `GET /admin/bookings/active` | admin | Active bookings |
|
| `GET /admin/bookings/active` | admin | Active bookings |
|
||||||
| `POST /admin/bookings/{id}/complete` | admin | Mark booking complete |
|
| `POST /admin/bookings/{id}/complete` | admin | Mark booking complete |
|
||||||
| `GET /operator/dashboard` | operator | Operator lot-picker + operations panel |
|
| `GET /operator/dashboard` | operator | Operator view |
|
||||||
| `GET /operator/stats` | operator | Operator statistics & charts dashboard |
|
|
||||||
| `GET /operator/stats-data` | operator | JSON stats data for operator dashboard (AJAX) |
|
|
||||||
| `POST /operator/check-in` | operator | Vehicle entry |
|
| `POST /operator/check-in` | operator | Vehicle entry |
|
||||||
| `POST /operator/{booking}/checkout` | operator | Vehicle exit + fee |
|
| `POST /operator/{booking}/checkout` | operator | Vehicle exit + fee |
|
||||||
| `GET /admin/stats` | admin | JSON stats (AJAX) |
|
| `GET /admin/stats` | admin | JSON stats (AJAX) |
|
||||||
@ -396,9 +361,7 @@ php artisan config:clear && php artisan test
|
|||||||
## Notes & Known Patterns
|
## Notes & Known Patterns
|
||||||
|
|
||||||
- Admin stats (`/admin/stats`, `/admin/charts`) are fetched via AJAX on page load — not cached, computed fresh each request.
|
- Admin stats (`/admin/stats`, `/admin/charts`) are fetched via AJAX on page load — not cached, computed fresh each request.
|
||||||
- Operator stats (`/operator/stats-data`) follow the same pattern — AJAX on page load, auto-refreshes every 30 s, scoped to the operator's assigned lots only.
|
|
||||||
- The `CarRegistry` model tracks physical vehicle presence; `Booking` tracks reservations — they are separate but both count toward capacity.
|
- The `CarRegistry` model tracks physical vehicle presence; `Booking` tracks reservations — they are separate but both count toward capacity.
|
||||||
- **Multi-lot operator assignment:** `User::assignedLots()` is a BelongsToMany through `operator_parking_lot`. Use `$user->assignedLots->pluck('id')` to get the operator's allowed lot IDs. An operator with an empty `assignedLots` collection sees **no** lots (unlike the old single-FK system where null = all lots).
|
|
||||||
- `StoreParkingLotRequest` and `StoreBookingRequest` are in `app/Http/Requests/`.
|
- `StoreParkingLotRequest` and `StoreBookingRequest` are in `app/Http/Requests/`.
|
||||||
- Arabic error messages are returned by both middleware and validation responses.
|
- Arabic error messages are returned by both middleware and validation responses.
|
||||||
- The `TODO.md` at the root is nearly empty — "Update with checked" is the only entry.
|
- The `TODO.md` at the root is nearly empty — "Update with checked" is the only entry.
|
||||||
|
|||||||
@ -14,7 +14,7 @@ class AdminOperatorController extends Controller
|
|||||||
public function index()
|
public function index()
|
||||||
{
|
{
|
||||||
$operators = User::where('role', 'operator')
|
$operators = User::where('role', 'operator')
|
||||||
->with('assignedLots')
|
->with('assignedLot')
|
||||||
->orderBy('name')
|
->orderBy('name')
|
||||||
->get();
|
->get();
|
||||||
|
|
||||||
@ -26,26 +26,24 @@ class AdminOperatorController extends Controller
|
|||||||
public function store(Request $request)
|
public function store(Request $request)
|
||||||
{
|
{
|
||||||
$data = $request->validate([
|
$data = $request->validate([
|
||||||
'name' => 'required|string|max:255',
|
'name' => 'required|string|max:255',
|
||||||
'email' => 'required|email|unique:users,email',
|
'email' => 'required|email|unique:users,email',
|
||||||
'phone' => 'nullable|string|max:20',
|
'phone' => 'nullable|string|max:20',
|
||||||
'password' => ['required', Password::min(8)],
|
'password' => ['required', Password::min(8)],
|
||||||
'lot_ids' => 'nullable|array',
|
'parking_lot_id' => 'nullable|exists:parking_lots,id',
|
||||||
'lot_ids.*'=> 'exists:parking_lots,id',
|
|
||||||
], [
|
], [
|
||||||
'email.unique' => 'هذا البريد الإلكتروني مستخدم بالفعل.',
|
'email.unique' => 'هذا البريد الإلكتروني مستخدم بالفعل.',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$operator = User::create([
|
User::create([
|
||||||
'name' => $data['name'],
|
'name' => $data['name'],
|
||||||
'email' => $data['email'],
|
'email' => $data['email'],
|
||||||
'phone' => $data['phone'] ?? null,
|
'phone' => $data['phone'] ?? null,
|
||||||
'password' => Hash::make($data['password']),
|
'password' => Hash::make($data['password']),
|
||||||
'role' => 'operator',
|
'role' => 'operator',
|
||||||
|
'parking_lot_id' => $data['parking_lot_id'] ?? null,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$operator->assignedLots()->sync($data['lot_ids'] ?? []);
|
|
||||||
|
|
||||||
return response()->json(['success' => true, 'message' => 'تم إنشاء حساب المشغّل بنجاح.']);
|
return response()->json(['success' => true, 'message' => 'تم إنشاء حساب المشغّل بنجاح.']);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -56,20 +54,20 @@ class AdminOperatorController extends Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
$data = $request->validate([
|
$data = $request->validate([
|
||||||
'name' => 'required|string|max:255',
|
'name' => 'required|string|max:255',
|
||||||
'email' => 'required|email|unique:users,email,' . $operator->id,
|
'email' => 'required|email|unique:users,email,' . $operator->id,
|
||||||
'phone' => 'nullable|string|max:20',
|
'phone' => 'nullable|string|max:20',
|
||||||
'password' => ['nullable', Password::min(8)],
|
'password' => ['nullable', Password::min(8)],
|
||||||
'lot_ids' => 'nullable|array',
|
'parking_lot_id' => 'nullable|exists:parking_lots,id',
|
||||||
'lot_ids.*'=> 'exists:parking_lots,id',
|
|
||||||
], [
|
], [
|
||||||
'email.unique' => 'هذا البريد الإلكتروني مستخدم بالفعل.',
|
'email.unique' => 'هذا البريد الإلكتروني مستخدم بالفعل.',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$updates = [
|
$updates = [
|
||||||
'name' => $data['name'],
|
'name' => $data['name'],
|
||||||
'email' => $data['email'],
|
'email' => $data['email'],
|
||||||
'phone' => $data['phone'] ?? null,
|
'phone' => $data['phone'] ?? null,
|
||||||
|
'parking_lot_id' => $data['parking_lot_id'] ?? null,
|
||||||
];
|
];
|
||||||
|
|
||||||
if (!empty($data['password'])) {
|
if (!empty($data['password'])) {
|
||||||
@ -77,7 +75,6 @@ class AdminOperatorController extends Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
$operator->update($updates);
|
$operator->update($updates);
|
||||||
$operator->assignedLots()->sync($data['lot_ids'] ?? []);
|
|
||||||
|
|
||||||
return response()->json(['success' => true, 'message' => 'تم تحديث بيانات المشغّل.']);
|
return response()->json(['success' => true, 'message' => 'تم تحديث بيانات المشغّل.']);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,18 +17,10 @@ class OperatorController extends Controller
|
|||||||
|
|
||||||
public function dashboard(Request $request): \Illuminate\View\View
|
public function dashboard(Request $request): \Illuminate\View\View
|
||||||
{
|
{
|
||||||
$user = Auth::user();
|
$user = Auth::user();
|
||||||
$assignedLotIds = $user->assignedLots->pluck('id')->toArray();
|
$assignedLotId = $user->parking_lot_id; // null = no restriction (e.g. admin)
|
||||||
|
|
||||||
// Operators only see their assigned lots; no assignment = no lots visible
|
$rawLots = ParkingLot::active()->withStatus()->get();
|
||||||
$query = ParkingLot::active()->withStatus();
|
|
||||||
if (!empty($assignedLotIds)) {
|
|
||||||
$query->whereIn('id', $assignedLotIds);
|
|
||||||
} else {
|
|
||||||
$query->whereRaw('1 = 0'); // no lots assigned → show nothing
|
|
||||||
}
|
|
||||||
|
|
||||||
$rawLots = $query->get();
|
|
||||||
|
|
||||||
$parkingLots = $rawLots->map(fn($lot) => [
|
$parkingLots = $rawLots->map(fn($lot) => [
|
||||||
'id' => $lot->id,
|
'id' => $lot->id,
|
||||||
@ -42,22 +34,20 @@ class OperatorController extends Controller
|
|||||||
'lat' => (float) $lot->latitude,
|
'lat' => (float) $lot->latitude,
|
||||||
'lng' => (float) $lot->longitude,
|
'lng' => (float) $lot->longitude,
|
||||||
'image' => $lot->image ? Storage::url($lot->image) : null,
|
'image' => $lot->image ? Storage::url($lot->image) : null,
|
||||||
'locked' => false,
|
'locked' => $assignedLotId !== null && $lot->id !== $assignedLotId,
|
||||||
])->values();
|
])->values();
|
||||||
|
|
||||||
|
// If operator has an assigned lot, force that lot
|
||||||
$selectedLotId = $request->get('lot_id');
|
$selectedLotId = $request->get('lot_id');
|
||||||
|
if ($assignedLotId !== null) {
|
||||||
// Reject attempts to access a lot not in the operator's assigned list
|
// Reject any attempt to view a different lot
|
||||||
if ($selectedLotId && !in_array((int) $selectedLotId, $assignedLotIds)) {
|
if ($selectedLotId && (int) $selectedLotId !== $assignedLotId) {
|
||||||
if (!empty($assignedLotIds)) {
|
return redirect()->route('operator.dashboard', ['lot_id' => $assignedLotId]);
|
||||||
return redirect()->route('operator.dashboard', ['lot_id' => $assignedLotIds[0]]);
|
}
|
||||||
|
// Auto-select assigned lot if nothing selected
|
||||||
|
if (!$selectedLotId) {
|
||||||
|
$selectedLotId = $assignedLotId;
|
||||||
}
|
}
|
||||||
return redirect()->route('operator.dashboard');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Auto-select when only one lot is assigned
|
|
||||||
if (!$selectedLotId && count($assignedLotIds) === 1) {
|
|
||||||
$selectedLotId = $assignedLotIds[0];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$selectedLot = null;
|
$selectedLot = null;
|
||||||
@ -83,177 +73,10 @@ class OperatorController extends Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
return view('operator.dashboard', compact(
|
return view('operator.dashboard', compact(
|
||||||
'parkingLots', 'selectedLot', 'activeCars', 'reservations', 'selectedLotId', 'assignedLotIds'
|
'parkingLots', 'selectedLot', 'activeCars', 'reservations', 'selectedLotId', 'assignedLotId'
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Operator stats page ─────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
public function statsPage(): \Illuminate\View\View
|
|
||||||
{
|
|
||||||
$user = Auth::user();
|
|
||||||
$assignedLotIds = $user->assignedLots->pluck('id')->toArray();
|
|
||||||
$lots = ParkingLot::whereIn('id', $assignedLotIds)->orderBy('name')->get();
|
|
||||||
|
|
||||||
return view('operator.stats', compact('lots', 'assignedLotIds'));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function statsJson(): JsonResponse
|
|
||||||
{
|
|
||||||
$user = Auth::user();
|
|
||||||
$assignedLotIds = $user->assignedLots->pluck('id')->toArray();
|
|
||||||
|
|
||||||
// Active vehicles right now
|
|
||||||
$activeCars = Booking::whereIn('parking_lot_id', $assignedLotIds)
|
|
||||||
->where('source', 'walk_in')
|
|
||||||
->where('status', 'active')
|
|
||||||
->count();
|
|
||||||
|
|
||||||
// Walk-in check-ins started today
|
|
||||||
$checkInsToday = Booking::whereIn('parking_lot_id', $assignedLotIds)
|
|
||||||
->where('source', 'walk_in')
|
|
||||||
->whereDate('start_time', today())
|
|
||||||
->count();
|
|
||||||
|
|
||||||
// Revenue collected today (completed + paid today)
|
|
||||||
$revenueToday = (float) Booking::whereIn('parking_lot_id', $assignedLotIds)
|
|
||||||
->where('status', 'completed')
|
|
||||||
->whereDate('paid_at', today())
|
|
||||||
->sum('total_fee');
|
|
||||||
|
|
||||||
// Total capacity & occupied across assigned lots
|
|
||||||
$lots = ParkingLot::whereIn('id', $assignedLotIds)->get();
|
|
||||||
$totalCapacity = $lots->sum('total_capacity');
|
|
||||||
$totalOccupied = Booking::whereIn('parking_lot_id', $assignedLotIds)
|
|
||||||
->where('status', 'active')
|
|
||||||
->count();
|
|
||||||
$availableSpaces = max(0, $totalCapacity - $totalOccupied);
|
|
||||||
|
|
||||||
// Pending reservations (not yet activated)
|
|
||||||
$pendingReservations = Booking::whereIn('parking_lot_id', $assignedLotIds)
|
|
||||||
->where('source', 'reservation')
|
|
||||||
->where('status', 'active')
|
|
||||||
->count();
|
|
||||||
|
|
||||||
// 7-day daily check-ins and revenue
|
|
||||||
$dailyCheckIns = [];
|
|
||||||
$dailyRevenue = [];
|
|
||||||
$dailyDates = [];
|
|
||||||
for ($i = 6; $i >= 0; $i--) {
|
|
||||||
$date = now()->subDays($i)->toDateString();
|
|
||||||
$dailyDates[] = $date;
|
|
||||||
$dailyCheckIns[] = Booking::whereIn('parking_lot_id', $assignedLotIds)
|
|
||||||
->where('source', 'walk_in')
|
|
||||||
->whereDate('start_time', $date)
|
|
||||||
->count();
|
|
||||||
$dailyRevenue[] = (float) Booking::whereIn('parking_lot_id', $assignedLotIds)
|
|
||||||
->where('status', 'completed')
|
|
||||||
->whereDate('paid_at', $date)
|
|
||||||
->sum('total_fee');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Per-lot breakdown
|
|
||||||
$lotStats = $lots->map(function ($lot) {
|
|
||||||
$active = Booking::where('parking_lot_id', $lot->id)->where('status', 'active')->count();
|
|
||||||
$todayIns = Booking::where('parking_lot_id', $lot->id)
|
|
||||||
->where('source', 'walk_in')
|
|
||||||
->whereDate('start_time', today())
|
|
||||||
->count();
|
|
||||||
$todayRev = (float) Booking::where('parking_lot_id', $lot->id)
|
|
||||||
->where('status', 'completed')
|
|
||||||
->whereDate('paid_at', today())
|
|
||||||
->sum('total_fee');
|
|
||||||
return [
|
|
||||||
'id' => $lot->id,
|
|
||||||
'name' => $lot->name,
|
|
||||||
'address' => $lot->address,
|
|
||||||
'total' => $lot->total_capacity,
|
|
||||||
'active' => $active,
|
|
||||||
'available' => max(0, $lot->total_capacity - $active),
|
|
||||||
'pct' => $lot->total_capacity > 0 ? round($active / $lot->total_capacity * 100) : 0,
|
|
||||||
'today_ins' => $todayIns,
|
|
||||||
'today_rev' => $todayRev,
|
|
||||||
];
|
|
||||||
})->values();
|
|
||||||
|
|
||||||
// Last 8 completed bookings
|
|
||||||
$recentCompletions = Booking::whereIn('parking_lot_id', $assignedLotIds)
|
|
||||||
->where('status', 'completed')
|
|
||||||
->with('parkingLot:id,name')
|
|
||||||
->latest('paid_at')
|
|
||||||
->limit(8)
|
|
||||||
->get(['id', 'parking_lot_id', 'vehicle_plate', 'customer_name', 'start_time', 'paid_at', 'total_fee', 'payment_method']);
|
|
||||||
|
|
||||||
// ── Work progress ──────────────────────────────────────────────────────
|
|
||||||
// Checkouts completed today
|
|
||||||
$checkoutsToday = Booking::whereIn('parking_lot_id', $assignedLotIds)
|
|
||||||
->where('status', 'completed')
|
|
||||||
->whereDate('paid_at', today())
|
|
||||||
->count();
|
|
||||||
|
|
||||||
// Cancellations today
|
|
||||||
$cancelledToday = Booking::whereIn('parking_lot_id', $assignedLotIds)
|
|
||||||
->where('status', 'cancelled')
|
|
||||||
->whereDate('updated_at', today())
|
|
||||||
->count();
|
|
||||||
|
|
||||||
// Yesterday check-ins (comparison)
|
|
||||||
$checkInsYesterday = Booking::whereIn('parking_lot_id', $assignedLotIds)
|
|
||||||
->where('source', 'walk_in')
|
|
||||||
->whereDate('start_time', today()->subDay())
|
|
||||||
->count();
|
|
||||||
|
|
||||||
// Average stay duration in minutes (completed bookings today, start→paid_at)
|
|
||||||
$completedToday = Booking::whereIn('parking_lot_id', $assignedLotIds)
|
|
||||||
->where('status', 'completed')
|
|
||||||
->whereDate('paid_at', today())
|
|
||||||
->get(['start_time', 'paid_at']);
|
|
||||||
|
|
||||||
$avgDurationMin = $completedToday->isNotEmpty()
|
|
||||||
? round($completedToday->avg(fn($b) => $b->start_time->diffInMinutes($b->paid_at)))
|
|
||||||
: 0;
|
|
||||||
|
|
||||||
// Completion rate today: completed / (completed + cancelled)
|
|
||||||
$totalClosed = $checkoutsToday + $cancelledToday;
|
|
||||||
$completionRate = $totalClosed > 0 ? round($checkoutsToday / $totalClosed * 100) : 100;
|
|
||||||
|
|
||||||
// Hourly activity today (check-ins per hour 0–23)
|
|
||||||
$hourlyActivity = array_fill(0, 24, 0);
|
|
||||||
Booking::whereIn('parking_lot_id', $assignedLotIds)
|
|
||||||
->where('source', 'walk_in')
|
|
||||||
->whereDate('start_time', today())
|
|
||||||
->get(['start_time'])
|
|
||||||
->each(function ($b) use (&$hourlyActivity) {
|
|
||||||
$hourlyActivity[(int) $b->start_time->format('H')]++;
|
|
||||||
});
|
|
||||||
|
|
||||||
$peakHour = (int) array_search(max($hourlyActivity), $hourlyActivity);
|
|
||||||
|
|
||||||
return response()->json([
|
|
||||||
'success' => true,
|
|
||||||
'data' => [
|
|
||||||
'active_cars' => $activeCars,
|
|
||||||
'checkins_today' => $checkInsToday,
|
|
||||||
'revenue_today' => $revenueToday,
|
|
||||||
'available_spaces' => $availableSpaces,
|
|
||||||
'pending_reservations' => $pendingReservations,
|
|
||||||
'daily_checkins' => $dailyCheckIns,
|
|
||||||
'daily_revenue' => $dailyRevenue,
|
|
||||||
'daily_dates' => $dailyDates,
|
|
||||||
'lot_stats' => $lotStats,
|
|
||||||
'recent_completions' => $recentCompletions,
|
|
||||||
// work progress
|
|
||||||
'checkouts_today' => $checkoutsToday,
|
|
||||||
'cancelled_today' => $cancelledToday,
|
|
||||||
'checkins_yesterday' => $checkInsYesterday,
|
|
||||||
'avg_duration_min' => $avgDurationMin,
|
|
||||||
'completion_rate' => $completionRate,
|
|
||||||
'hourly_activity' => array_values($hourlyActivity),
|
|
||||||
'peak_hour' => $peakHour,
|
|
||||||
],
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Walk-in check-in ────────────────────────────────────────────────────────
|
// ── Walk-in check-in ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
public function checkIn(Request $request): JsonResponse
|
public function checkIn(Request $request): JsonResponse
|
||||||
|
|||||||
@ -24,6 +24,7 @@ class User extends Authenticatable
|
|||||||
'phone',
|
'phone',
|
||||||
'password',
|
'password',
|
||||||
'role',
|
'role',
|
||||||
|
'parking_lot_id',
|
||||||
];
|
];
|
||||||
|
|
||||||
protected $casts = [
|
protected $casts = [
|
||||||
@ -55,8 +56,8 @@ class User extends Authenticatable
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function assignedLots(): \Illuminate\Database\Eloquent\Relations\BelongsToMany
|
public function assignedLot(): \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||||
{
|
{
|
||||||
return $this->belongsToMany(ParkingLot::class, 'operator_parking_lot');
|
return $this->belongsTo(ParkingLot::class, 'parking_lot_id');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
0
bootstrap/cache/.gitignore
vendored
Executable file → Normal file
0
bootstrap/cache/.gitignore
vendored
Executable file → Normal file
@ -1,40 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
|
||||||
use Illuminate\Support\Facades\DB;
|
|
||||||
use Illuminate\Support\Facades\Schema;
|
|
||||||
|
|
||||||
return new class extends Migration
|
|
||||||
{
|
|
||||||
public function up(): void
|
|
||||||
{
|
|
||||||
Schema::create('operator_parking_lot', function (Blueprint $table) {
|
|
||||||
$table->id();
|
|
||||||
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
|
|
||||||
$table->foreignId('parking_lot_id')->constrained()->cascadeOnDelete();
|
|
||||||
$table->unique(['user_id', 'parking_lot_id']);
|
|
||||||
$table->timestamps();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Migrate existing single-lot assignments from users.parking_lot_id
|
|
||||||
$operators = DB::table('users')
|
|
||||||
->where('role', 'operator')
|
|
||||||
->whereNotNull('parking_lot_id')
|
|
||||||
->get(['id', 'parking_lot_id']);
|
|
||||||
|
|
||||||
foreach ($operators as $op) {
|
|
||||||
DB::table('operator_parking_lot')->insertOrIgnore([
|
|
||||||
'user_id' => $op->id,
|
|
||||||
'parking_lot_id' => $op->parking_lot_id,
|
|
||||||
'created_at' => now(),
|
|
||||||
'updated_at' => now(),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function down(): void
|
|
||||||
{
|
|
||||||
Schema::dropIfExists('operator_parking_lot');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"resources/css/app.scss": {
|
"resources/css/app.scss": {
|
||||||
"file": "assets/app-Layout02.css",
|
"file": "assets/app-BtJpbOwL.css",
|
||||||
"src": "resources/css/app.scss",
|
"src": "resources/css/app.scss",
|
||||||
"isEntry": true,
|
"isEntry": true,
|
||||||
"name": "app",
|
"name": "app",
|
||||||
@ -9,7 +9,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"resources/js/app.js": {
|
"resources/js/app.js": {
|
||||||
"file": "assets/app-D0Ms7V4S2.js",
|
"file": "assets/app-D0Ms7V4S.js",
|
||||||
"name": "app",
|
"name": "app",
|
||||||
"src": "resources/js/app.js",
|
"src": "resources/js/app.js",
|
||||||
"isEntry": true
|
"isEntry": true
|
||||||
|
|||||||
@ -74,17 +74,11 @@ body {
|
|||||||
background-color: var(--app-bg);
|
background-color: var(--app-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── App Layout (Topbar top, then sidebar + content row below) ───────────────
|
// ─── App Layout (Sidebar + Main) ──────────────────────────────────────────────
|
||||||
.app-layout {
|
.app-layout {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
}
|
// In RTL flex-direction:row, the first child (sidebar) appears on the RIGHT
|
||||||
|
|
||||||
.app-inner {
|
|
||||||
display: flex;
|
|
||||||
flex: 1;
|
|
||||||
min-height: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Sidebar ──────────────────────────────────────────────────────────────────
|
// ─── Sidebar ──────────────────────────────────────────────────────────────────
|
||||||
@ -94,9 +88,9 @@ body {
|
|||||||
background: var(--sidebar-bg);
|
background: var(--sidebar-bg);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
height: calc(100vh - var(--topbar-height));
|
height: 100vh;
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: var(--topbar-height);
|
top: 0;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
scrollbar-width: none;
|
scrollbar-width: none;
|
||||||
transition: transform .3s ease;
|
transition: transform .3s ease;
|
||||||
@ -228,12 +222,11 @@ body {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Topbar (unified — used on all pages) ─────────────────────────────────────
|
// ─── Topbar ───────────────────────────────────────────────────────────────────
|
||||||
.app-topbar {
|
.app-topbar {
|
||||||
height: var(--topbar-height);
|
height: var(--topbar-height);
|
||||||
background: var(--sidebar-bg);
|
background: var(--topbar-bg);
|
||||||
border-bottom: 1px solid rgba(255,255,255,.06);
|
border-bottom: 1px solid var(--border-color);
|
||||||
box-shadow: 0 2px 12px rgba(0,0,0,.15);
|
|
||||||
padding: 0 1.5rem;
|
padding: 0 1.5rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -241,12 +234,12 @@ body {
|
|||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0;
|
top: 0;
|
||||||
z-index: 200;
|
z-index: 100;
|
||||||
|
|
||||||
.topbar-title {
|
.topbar-title {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: #f8fafc;
|
color: var(--text-primary);
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -262,13 +255,13 @@ body {
|
|||||||
background: none;
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
padding: .375rem .5rem;
|
padding: .375rem .5rem;
|
||||||
color: rgba(255,255,255,.65);
|
color: var(--text-muted);
|
||||||
border-radius: .375rem;
|
border-radius: .375rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: none;
|
display: none;
|
||||||
font-size: 1.25rem;
|
font-size: 1.25rem;
|
||||||
|
|
||||||
&:hover { background: rgba(255,255,255,.08); color: #fff; }
|
&:hover { background: #f1f5f9; color: var(--text-primary); }
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-overlay {
|
.sidebar-overlay {
|
||||||
@ -290,10 +283,13 @@ body {
|
|||||||
@media (max-width: 991.98px) {
|
@media (max-width: 991.98px) {
|
||||||
.app-sidebar {
|
.app-sidebar {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: var(--topbar-height);
|
top: 0;
|
||||||
inset-inline-start: 0; // anchors right edge to viewport right in RTL
|
inset-inline-end: 0; // right in RTL
|
||||||
height: calc(100% - var(--topbar-height));
|
height: 100%;
|
||||||
transform: translateX(100%); // hides off-screen to the right in RTL
|
transform: translateX(calc(-1 * var(--sidebar-width)));
|
||||||
|
// In RTL, translateX negative moves LEFT (off screen)
|
||||||
|
// We need to hide it to the right side
|
||||||
|
transform: translateX(100%);
|
||||||
|
|
||||||
&.is-open {
|
&.is-open {
|
||||||
transform: translateX(0);
|
transform: translateX(0);
|
||||||
@ -557,6 +553,14 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ─── Public (Index) Page ──────────────────────────────────────────────────────
|
// ─── Public (Index) Page ──────────────────────────────────────────────────────
|
||||||
|
.public-header {
|
||||||
|
background: var(--sidebar-bg);
|
||||||
|
padding: 1.25rem 0;
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 200;
|
||||||
|
box-shadow: 0 2px 12px rgba(0,0,0,.15);
|
||||||
|
}
|
||||||
|
|
||||||
.parking-card {
|
.parking-card {
|
||||||
transition: all .18s ease;
|
transition: all .18s ease;
|
||||||
@ -601,6 +605,11 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
@media (min-width: 768px) {
|
||||||
|
.app-topbar {
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
background: rgba(255,255,255,.96);
|
||||||
|
}
|
||||||
|
|
||||||
.stat-card:hover {
|
.stat-card:hover {
|
||||||
transform: translateY(-3px);
|
transform: translateY(-3px);
|
||||||
box-shadow: 0 8px 28px rgba(0,0,0,.09) !important;
|
box-shadow: 0 8px 28px rgba(0,0,0,.09) !important;
|
||||||
@ -681,8 +690,6 @@ body {
|
|||||||
/* ── Admin / Operator layout ────────────────────────────────────────── */
|
/* ── Admin / Operator layout ────────────────────────────────────────── */
|
||||||
.sidebar-toggle { display: none !important; }
|
.sidebar-toggle { display: none !important; }
|
||||||
.app-layout { display: block; }
|
.app-layout { display: block; }
|
||||||
.app-inner { display: block; }
|
|
||||||
.app-sidebar { display: none !important; }
|
|
||||||
|
|
||||||
.app-body {
|
.app-body {
|
||||||
padding-bottom: var(--mobile-nav-h);
|
padding-bottom: var(--mobile-nav-h);
|
||||||
@ -692,6 +699,8 @@ body {
|
|||||||
.app-topbar {
|
.app-topbar {
|
||||||
height: 54px;
|
height: 54px;
|
||||||
padding: 0 1rem;
|
padding: 0 1rem;
|
||||||
|
backdrop-filter: none;
|
||||||
|
background: #ffffff;
|
||||||
|
|
||||||
.topbar-title { font-size: .9rem; }
|
.topbar-title { font-size: .9rem; }
|
||||||
}
|
}
|
||||||
@ -705,7 +714,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* ── Public page ────────────────────────────────────────────────────── */
|
/* ── Public page ────────────────────────────────────────────────────── */
|
||||||
.app-topbar { padding: .625rem 1rem; }
|
.public-header { padding: .625rem 0; }
|
||||||
.mob-hero-compact { padding: .875rem 0 1rem !important; }
|
.mob-hero-compact { padding: .875rem 0 1rem !important; }
|
||||||
|
|
||||||
/* Section switching — hide inactive section */
|
/* Section switching — hide inactive section */
|
||||||
|
|||||||
@ -396,12 +396,10 @@
|
|||||||
{{-- ── STEP 2A : receipt + payment ────────────────────────── --}}
|
{{-- ── STEP 2A : receipt + payment ────────────────────────── --}}
|
||||||
<div id="endStep2Pay" style="display:none;" class="p-4">
|
<div id="endStep2Pay" style="display:none;" class="p-4">
|
||||||
|
|
||||||
<div class="d-flex justify-content-end mb-3">
|
<button class="btn btn-sm mb-3 fw-600" onclick="endBack()"
|
||||||
<button class="btn btn-sm fw-600" onclick="endBack()"
|
style="background:#f1f5f9;color:#475569;border:none;border-radius:.5rem;font-family:'Cairo',sans-serif;">
|
||||||
style="background:#f1f5f9;color:#475569;border:none;border-radius:.5rem;font-family:'Cairo',sans-serif;">
|
<i class="bi bi-arrow-right me-1"></i>رجوع
|
||||||
<i class="bi bi-arrow-left me-1"></i>رجوع
|
</button>
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{-- Times --}}
|
{{-- Times --}}
|
||||||
<div class="row g-2 mb-3">
|
<div class="row g-2 mb-3">
|
||||||
@ -471,12 +469,10 @@
|
|||||||
{{-- ── STEP 2B : force close ───────────────────────────────── --}}
|
{{-- ── STEP 2B : force close ───────────────────────────────── --}}
|
||||||
<div id="endStep2Force" style="display:none;" class="p-4">
|
<div id="endStep2Force" style="display:none;" class="p-4">
|
||||||
|
|
||||||
<div class="d-flex justify-content-end mb-3">
|
<button class="btn btn-sm mb-3 fw-600" onclick="endBack()"
|
||||||
<button class="btn btn-sm fw-600" onclick="endBack()"
|
style="background:#f1f5f9;color:#475569;border:none;border-radius:.5rem;font-family:'Cairo',sans-serif;">
|
||||||
style="background:#f1f5f9;color:#475569;border:none;border-radius:.5rem;font-family:'Cairo',sans-serif;">
|
<i class="bi bi-arrow-right me-1"></i>رجوع
|
||||||
<i class="bi bi-arrow-left me-1"></i>رجوع
|
</button>
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="text-center py-2 mb-3">
|
<div class="text-center py-2 mb-3">
|
||||||
<div style="font-size:2.8rem;margin-bottom:.75rem;">⚠️</div>
|
<div style="font-size:2.8rem;margin-bottom:.75rem;">⚠️</div>
|
||||||
@ -613,12 +609,12 @@ async function endChoose(type) {
|
|||||||
const d = data.data;
|
const d = data.data;
|
||||||
document.getElementById('payRcptEntry').textContent = d.entry_time;
|
document.getElementById('payRcptEntry').textContent = d.entry_time;
|
||||||
document.getElementById('payRcptExit').textContent = d.exit_time;
|
document.getElementById('payRcptExit').textContent = d.exit_time;
|
||||||
document.getElementById('payRcptTotal').textContent = Number(d.total_fee).toLocaleString('ar-SA') + ' ليرة سورية';
|
document.getElementById('payRcptTotal').textContent = Number(d.total_fee).toLocaleString('ar-SA') + ' ل.س';
|
||||||
const rows = d.fee_details.map(r => `
|
const rows = d.fee_details.map(r => `
|
||||||
<div style="display:flex;justify-content:space-between;padding:.3rem 0;border-bottom:1px dashed #f1f5f9;font-size:.82rem;">
|
<div style="display:flex;justify-content:space-between;padding:.3rem 0;border-bottom:1px dashed #f1f5f9;font-size:.82rem;">
|
||||||
<span>${r.day} <small style="color:#94a3b8;">${r.date}</small></span>
|
<span>${r.day} <small style="color:#94a3b8;">${r.date}</small></span>
|
||||||
<span style="color:#64748b;">${r.hours}س × ${Number(r.rate).toLocaleString('ar-SA')}</span>
|
<span style="color:#64748b;">${r.hours}س × ${Number(r.rate).toLocaleString('ar-SA')}</span>
|
||||||
<span class="fw-600" style="color:#0f172a;">${Number(r.subtotal).toLocaleString('ar-SA')} ليرة سورية</span>
|
<span class="fw-600" style="color:#0f172a;">${Number(r.subtotal).toLocaleString('ar-SA')} ل.س</span>
|
||||||
</div>`).join('');
|
</div>`).join('');
|
||||||
document.getElementById('payRcptBreakdown').innerHTML =
|
document.getElementById('payRcptBreakdown').innerHTML =
|
||||||
rows || '<p class="text-xs text-center" style="color:#94a3b8;">لا تفاصيل</p>';
|
rows || '<p class="text-xs text-center" style="color:#94a3b8;">لا تفاصيل</p>';
|
||||||
|
|||||||
@ -274,7 +274,7 @@ async function loadStats() {
|
|||||||
document.getElementById('total-bookings').textContent = data.total_bookings ?? 0;
|
document.getElementById('total-bookings').textContent = data.total_bookings ?? 0;
|
||||||
document.getElementById('active-bookings').textContent = data.active_bookings ?? 0;
|
document.getElementById('active-bookings').textContent = data.active_bookings ?? 0;
|
||||||
document.getElementById('occupancy-rate').textContent = (data.occupancy_rate ?? 0) + '%';
|
document.getElementById('occupancy-rate').textContent = (data.occupancy_rate ?? 0) + '%';
|
||||||
document.getElementById('estimated-revenue').textContent = (data.estimated_revenue ?? 0).toLocaleString('ar-SA') + ' ليرة سورية';
|
document.getElementById('estimated-revenue').textContent = (data.estimated_revenue ?? 0).toLocaleString('ar-SA') + ' ر.س';
|
||||||
document.getElementById('available-spots').textContent = data.available_spots ?? 0;
|
document.getElementById('available-spots').textContent = data.available_spots ?? 0;
|
||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,15 +22,6 @@
|
|||||||
padding:.25em .75em; border-radius:20px; font-size:.75rem; font-weight:600;
|
padding:.25em .75em; border-radius:20px; font-size:.75rem; font-weight:600;
|
||||||
background:#f1f5f9; color:#94a3b8;
|
background:#f1f5f9; color:#94a3b8;
|
||||||
}
|
}
|
||||||
.lot-checkbox-list {
|
|
||||||
border:1px solid #e2e8f0; border-radius:.625rem; max-height:180px; overflow-y:auto; padding:.5rem;
|
|
||||||
}
|
|
||||||
.lot-checkbox-item {
|
|
||||||
display:flex; align-items:center; gap:.6rem; padding:.375rem .5rem; border-radius:.4rem; cursor:pointer;
|
|
||||||
font-size:.875rem; color:#374151; transition:background .15s;
|
|
||||||
}
|
|
||||||
.lot-checkbox-item:hover { background:#f8fafc; }
|
|
||||||
.lot-checkbox-item input[type=checkbox] { width:1rem; height:1rem; cursor:pointer; flex-shrink:0; }
|
|
||||||
</style>
|
</style>
|
||||||
@endsection
|
@endsection
|
||||||
|
|
||||||
@ -70,15 +61,11 @@
|
|||||||
<td class="py-3" style="color:#475569;">{{ $op->email }}</td>
|
<td class="py-3" style="color:#475569;">{{ $op->email }}</td>
|
||||||
<td class="py-3" style="color:#475569;">{{ $op->phone ?? '—' }}</td>
|
<td class="py-3" style="color:#475569;">{{ $op->phone ?? '—' }}</td>
|
||||||
<td class="py-3">
|
<td class="py-3">
|
||||||
@if($op->assignedLots->isNotEmpty())
|
@if($op->assignedLot)
|
||||||
<div class="d-flex flex-wrap gap-1">
|
<span class="lot-pill">
|
||||||
@foreach($op->assignedLots as $assignedLot)
|
<i class="bi bi-buildings"></i>
|
||||||
<span class="lot-pill">
|
{{ $op->assignedLot->name }}
|
||||||
<i class="bi bi-buildings"></i>
|
</span>
|
||||||
{{ $assignedLot->name }}
|
|
||||||
</span>
|
|
||||||
@endforeach
|
|
||||||
</div>
|
|
||||||
@else
|
@else
|
||||||
<span class="no-lot-pill">
|
<span class="no-lot-pill">
|
||||||
<i class="bi bi-dash-circle"></i>
|
<i class="bi bi-dash-circle"></i>
|
||||||
@ -94,7 +81,7 @@
|
|||||||
data-name="{{ $op->name }}"
|
data-name="{{ $op->name }}"
|
||||||
data-email="{{ $op->email }}"
|
data-email="{{ $op->email }}"
|
||||||
data-phone="{{ $op->phone ?? '' }}"
|
data-phone="{{ $op->phone ?? '' }}"
|
||||||
data-lots="{{ json_encode($op->assignedLots->pluck('id')) }}">
|
data-lot="{{ $op->parking_lot_id ?? '' }}">
|
||||||
<i class="bi bi-pencil me-1"></i>تعديل
|
<i class="bi bi-pencil me-1"></i>تعديل
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-sm fw-600 btn-delete-op"
|
<button class="btn btn-sm fw-600 btn-delete-op"
|
||||||
@ -147,17 +134,13 @@
|
|||||||
<input type="password" id="createPassword" class="form-control" placeholder="8 أحرف على الأقل" dir="ltr">
|
<input type="password" id="createPassword" class="form-control" placeholder="8 أحرف على الأقل" dir="ltr">
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-1">
|
<div class="mb-1">
|
||||||
<label class="form-label fw-600" style="font-size:.875rem;color:#374151;">المواقف المخصصة <span style="color:#94a3b8;font-weight:400;">(اختياري)</span></label>
|
<label class="form-label fw-600" style="font-size:.875rem;color:#374151;">الموقف المخصص <span style="color:#94a3b8;font-weight:400;">(اختياري)</span></label>
|
||||||
<div class="lot-checkbox-list" id="createLotList">
|
<select id="createLot" class="form-select">
|
||||||
@forelse($lots as $lot)
|
<option value="">— بدون تخصيص —</option>
|
||||||
<label class="lot-checkbox-item">
|
@foreach($lots as $lot)
|
||||||
<input type="checkbox" class="create-lot-cb" value="{{ $lot->id }}">
|
<option value="{{ $lot->id }}">{{ $lot->name }}</option>
|
||||||
<span>{{ $lot->name }}</span>
|
@endforeach
|
||||||
</label>
|
</select>
|
||||||
@empty
|
|
||||||
<p class="text-center mb-0 py-2" style="color:#94a3b8;font-size:.8rem;">لا توجد مواقف</p>
|
|
||||||
@endforelse
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer border-0 pt-1">
|
<div class="modal-footer border-0 pt-1">
|
||||||
@ -203,17 +186,13 @@
|
|||||||
<input type="password" id="editPassword" class="form-control" placeholder="8 أحرف على الأقل" dir="ltr">
|
<input type="password" id="editPassword" class="form-control" placeholder="8 أحرف على الأقل" dir="ltr">
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-1">
|
<div class="mb-1">
|
||||||
<label class="form-label fw-600" style="font-size:.875rem;color:#374151;">المواقف المخصصة</label>
|
<label class="form-label fw-600" style="font-size:.875rem;color:#374151;">الموقف المخصص</label>
|
||||||
<div class="lot-checkbox-list" id="editLotList">
|
<select id="editLot" class="form-select">
|
||||||
@forelse($lots as $lot)
|
<option value="">— بدون تخصيص —</option>
|
||||||
<label class="lot-checkbox-item">
|
@foreach($lots as $lot)
|
||||||
<input type="checkbox" class="edit-lot-cb" value="{{ $lot->id }}">
|
<option value="{{ $lot->id }}">{{ $lot->name }}</option>
|
||||||
<span>{{ $lot->name }}</span>
|
@endforeach
|
||||||
</label>
|
</select>
|
||||||
@empty
|
|
||||||
<p class="text-center mb-0 py-2" style="color:#94a3b8;font-size:.8rem;">لا توجد مواقف</p>
|
|
||||||
@endforelse
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer border-0 pt-1">
|
<div class="modal-footer border-0 pt-1">
|
||||||
@ -292,7 +271,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
// ── Open create ───────────────────────────────────────────────────────────
|
// ── Open create ───────────────────────────────────────────────────────────
|
||||||
document.getElementById('btnOpenCreate').addEventListener('click', function () {
|
document.getElementById('btnOpenCreate').addEventListener('click', function () {
|
||||||
['createName','createEmail','createPhone','createPassword'].forEach(id => document.getElementById(id).value = '');
|
['createName','createEmail','createPhone','createPassword'].forEach(id => document.getElementById(id).value = '');
|
||||||
document.querySelectorAll('.create-lot-cb').forEach(cb => cb.checked = false);
|
document.getElementById('createLot').value = '';
|
||||||
document.getElementById('createError').classList.add('d-none');
|
document.getElementById('createError').classList.add('d-none');
|
||||||
modal('createModal').show();
|
modal('createModal').show();
|
||||||
});
|
});
|
||||||
@ -305,10 +284,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
document.getElementById('editEmail').value = this.dataset.email;
|
document.getElementById('editEmail').value = this.dataset.email;
|
||||||
document.getElementById('editPhone').value = this.dataset.phone || '';
|
document.getElementById('editPhone').value = this.dataset.phone || '';
|
||||||
document.getElementById('editPassword').value = '';
|
document.getElementById('editPassword').value = '';
|
||||||
const assignedIds = JSON.parse(this.dataset.lots || '[]');
|
document.getElementById('editLot').value = this.dataset.lot || '';
|
||||||
document.querySelectorAll('.edit-lot-cb').forEach(cb => {
|
|
||||||
cb.checked = assignedIds.includes(parseInt(cb.value));
|
|
||||||
});
|
|
||||||
document.getElementById('editError').classList.add('d-none');
|
document.getElementById('editError').classList.add('d-none');
|
||||||
modal('editModal').show();
|
modal('editModal').show();
|
||||||
});
|
});
|
||||||
@ -333,16 +309,15 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
spinner.classList.remove('d-none');
|
spinner.classList.remove('d-none');
|
||||||
errEl.classList.add('d-none');
|
errEl.classList.add('d-none');
|
||||||
try {
|
try {
|
||||||
const createLotIds = [...document.querySelectorAll('.create-lot-cb:checked')].map(cb => parseInt(cb.value));
|
|
||||||
const res = await fetch('{{ route("admin.operators.store") }}', {
|
const res = await fetch('{{ route("admin.operators.store") }}', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': csrf },
|
headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': csrf },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
name: document.getElementById('createName').value.trim(),
|
name: document.getElementById('createName').value.trim(),
|
||||||
email: document.getElementById('createEmail').value.trim(),
|
email: document.getElementById('createEmail').value.trim(),
|
||||||
phone: document.getElementById('createPhone').value.trim() || null,
|
phone: document.getElementById('createPhone').value.trim() || null,
|
||||||
password: document.getElementById('createPassword').value,
|
password: document.getElementById('createPassword').value,
|
||||||
lot_ids: createLotIds,
|
parking_lot_id: document.getElementById('createLot').value || null,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
const json = await res.json();
|
const json = await res.json();
|
||||||
@ -368,16 +343,15 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
spinner.classList.remove('d-none');
|
spinner.classList.remove('d-none');
|
||||||
errEl.classList.add('d-none');
|
errEl.classList.add('d-none');
|
||||||
try {
|
try {
|
||||||
const editLotIds = [...document.querySelectorAll('.edit-lot-cb:checked')].map(cb => parseInt(cb.value));
|
|
||||||
const res = await fetch(`/admin/operators/${id}`, {
|
const res = await fetch(`/admin/operators/${id}`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': csrf },
|
headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': csrf },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
name: document.getElementById('editName').value.trim(),
|
name: document.getElementById('editName').value.trim(),
|
||||||
email: document.getElementById('editEmail').value.trim(),
|
email: document.getElementById('editEmail').value.trim(),
|
||||||
phone: document.getElementById('editPhone').value.trim() || null,
|
phone: document.getElementById('editPhone').value.trim() || null,
|
||||||
password: document.getElementById('editPassword').value || null,
|
password: document.getElementById('editPassword').value || null,
|
||||||
lot_ids: editLotIds,
|
parking_lot_id: document.getElementById('editLot').value || null,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
const json = await res.json();
|
const json = await res.json();
|
||||||
|
|||||||
@ -209,7 +209,7 @@ $gradients = [
|
|||||||
</span>
|
</span>
|
||||||
<span class="lot-card-stat" style="{{ !empty($lot->pricing_rules) ? 'border-color:#fbbf24;color:#92400e;' : '' }}">
|
<span class="lot-card-stat" style="{{ !empty($lot->pricing_rules) ? 'border-color:#fbbf24;color:#92400e;' : '' }}">
|
||||||
<i class="bi bi-{{ !empty($lot->pricing_rules) ? 'tags' : 'tag' }}" style="color:{{ !empty($lot->pricing_rules) ? '#f59e0b' : '#10b981' }};"></i>
|
<i class="bi bi-{{ !empty($lot->pricing_rules) ? 'tags' : 'tag' }}" style="color:{{ !empty($lot->pricing_rules) ? '#f59e0b' : '#10b981' }};"></i>
|
||||||
<strong>{{ number_format($lot->price_per_hour, 0) }}</strong> ليرة سورية/س
|
<strong>{{ number_format($lot->price_per_hour, 0) }}</strong> ر.س/س
|
||||||
@if(!empty($lot->pricing_rules))
|
@if(!empty($lot->pricing_rules))
|
||||||
<span style="font-size:.6rem;color:#f59e0b;"> (مخصص)</span>
|
<span style="font-size:.6rem;color:#f59e0b;"> (مخصص)</span>
|
||||||
@endif
|
@endif
|
||||||
@ -385,7 +385,7 @@ $gradients = [
|
|||||||
<input type="number" id="p_base" class="form-control"
|
<input type="number" id="p_base" class="form-control"
|
||||||
step="0.01" min="0" placeholder="0.00"
|
step="0.01" min="0" placeholder="0.00"
|
||||||
oninput="syncBaseHints()">
|
oninput="syncBaseHints()">
|
||||||
<span class="input-group-text" style="font-family:'Cairo',sans-serif;background:#f8fafc;color:#64748b;border-color:#e2e8f0;">ليرة سورية / ساعة</span>
|
<span class="input-group-text" style="font-family:'Cairo',sans-serif;background:#f8fafc;color:#64748b;border-color:#e2e8f0;">ر.س / ساعة</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-xs mt-1 mb-0" style="color:#94a3b8;">
|
<p class="text-xs mt-1 mb-0" style="color:#94a3b8;">
|
||||||
الأيام التي لا تحمل سعراً مخصصاً ستستخدم هذا السعر تلقائياً.
|
الأيام التي لا تحمل سعراً مخصصاً ستستخدم هذا السعر تلقائياً.
|
||||||
@ -425,7 +425,7 @@ $gradients = [
|
|||||||
step="0.01" min="0"
|
step="0.01" min="0"
|
||||||
placeholder="مثل السعر الأساسي"
|
placeholder="مثل السعر الأساسي"
|
||||||
style="font-family:'Cairo',sans-serif;">
|
style="font-family:'Cairo',sans-serif;">
|
||||||
<span class="input-group-text" style="background:#f8fafc;color:#94a3b8;border-color:#e2e8f0;font-size:.75rem;">ليرة سورية</span>
|
<span class="input-group-text" style="background:#f8fafc;color:#94a3b8;border-color:#e2e8f0;font-size:.75rem;">ر.س</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="width:70px;text-align:center;">
|
<div style="width:70px;text-align:center;">
|
||||||
|
|||||||
@ -72,5 +72,16 @@
|
|||||||
إنشاء حساب جديد
|
إنشاء حساب جديد
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
<div class="mt-4 p-3 rounded-3" style="background:#f0f9ff;border:1px solid #bae6fd;">
|
||||||
|
<p class="text-xs fw-700 mb-2" style="color:#0369a1;">
|
||||||
|
<i class="bi bi-key-fill me-1"></i>حسابات الاختبار
|
||||||
|
</p>
|
||||||
|
<p class="text-xs mb-1" style="color:#0369a1;">
|
||||||
|
<strong>مدير:</strong> admin@damascusparking.com / admin123
|
||||||
|
</p>
|
||||||
|
<p class="text-xs mb-0" style="color:#0369a1;">
|
||||||
|
<strong>مشغّل:</strong> operator@damascusparking.com / operator123
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
@endsection
|
@endsection
|
||||||
|
|||||||
@ -15,69 +15,33 @@
|
|||||||
/* Bootstrap resets max-width:100% on all <img> which breaks Leaflet tiles */
|
/* Bootstrap resets max-width:100% on all <img> which breaks Leaflet tiles */
|
||||||
.leaflet-container img { max-width: none !important; box-shadow: none !important; }
|
.leaflet-container img { max-width: none !important; box-shadow: none !important; }
|
||||||
.leaflet-container { direction: ltr; }
|
.leaflet-container { direction: ltr; }
|
||||||
|
|
||||||
/* ── Desktop: map + list fill remaining viewport height ─────────────── */
|
|
||||||
@media (min-width: 992px) {
|
|
||||||
html, body { height: 100%; overflow: hidden; }
|
|
||||||
body { display: flex; flex-direction: column; }
|
|
||||||
.app-topbar { flex-shrink: 0; }
|
|
||||||
.mob-hero-compact { flex-shrink: 0; }
|
|
||||||
|
|
||||||
#mainContent {
|
|
||||||
flex: 1;
|
|
||||||
min-height: 0;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
padding-bottom: 0.75rem !important;
|
|
||||||
}
|
|
||||||
#mainContent > .row {
|
|
||||||
flex: 1;
|
|
||||||
min-height: 0;
|
|
||||||
}
|
|
||||||
#mainContent .col-lg-8,
|
|
||||||
#mainContent .col-lg-4 {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
/* Map card */
|
|
||||||
#mainContent .col-lg-8 > .card {
|
|
||||||
flex: 1;
|
|
||||||
min-height: 0;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
#mainContent .col-lg-8 .card-body {
|
|
||||||
flex: 1;
|
|
||||||
min-height: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
#map { height: 100% !important; }
|
|
||||||
|
|
||||||
/* List card */
|
|
||||||
#mainContent .col-lg-4 > .card {
|
|
||||||
flex: 1;
|
|
||||||
min-height: 0;
|
|
||||||
position: static !important;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
#parkingList {
|
|
||||||
flex: 1;
|
|
||||||
min-height: 0;
|
|
||||||
max-height: none !important;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body style="background:#f1f5f9;font-family:'Cairo',sans-serif;">
|
<body style="background:#f1f5f9;font-family:'Cairo',sans-serif;">
|
||||||
|
|
||||||
{{-- ══ HEADER ══════════════════════════════════════════════════════════════ --}}
|
{{-- ══ HEADER ══════════════════════════════════════════════════════════════ --}}
|
||||||
@include('partials.topbar')
|
<header class="public-header">
|
||||||
|
<div class="container">
|
||||||
|
<div class="d-flex align-items-center justify-content-between">
|
||||||
|
<div class="d-flex align-items-center gap-3">
|
||||||
|
<div style="width:40px;height:40px;background:#6366f1;border-radius:.625rem;display:flex;align-items:center;justify-content:center;flex-shrink:0;">
|
||||||
|
<i class="bi bi-p-square-fill text-white" style="font-size:1.25rem;"></i>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="fw-800" style="color:#f8fafc;font-size:1.05rem;line-height:1.2;">دمشق باركينغ</div>
|
||||||
|
<div style="color:#94a3b8;font-size:.72rem;">مواقف السيارات في دمشق</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex align-items-center gap-2">
|
||||||
|
@include('partials.user-dropdown')
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
{{-- ══ HERO SEARCH ═════════════════════════════════════════════════════════ --}}
|
{{-- ══ HERO SEARCH ═════════════════════════════════════════════════════════ --}}
|
||||||
<div class="mob-hero-compact" style="background:linear-gradient(135deg,#0f172a 0%,#1e3a5f 100%);padding:2.5rem 0 3rem;">
|
<div class="mob-hero-compact" style="background:linear-gradient(135deg,#0f172a 0%,#1e3a5f 100%);padding:2.5rem 0 3rem;">
|
||||||
<div class="container-fluid text-center">
|
<div class="container text-center">
|
||||||
<h1 class="fw-800 mb-2 d-none d-md-block" style="color:#f8fafc;font-size:clamp(1.4rem,4vw,2rem);">
|
<h1 class="fw-800 mb-2 d-none d-md-block" style="color:#f8fafc;font-size:clamp(1.4rem,4vw,2rem);">
|
||||||
ابحث عن موقف سيارات في دمشق
|
ابحث عن موقف سيارات في دمشق
|
||||||
</h1>
|
</h1>
|
||||||
@ -108,7 +72,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{-- ══ MAIN CONTENT ════════════════════════════════════════════════════════ --}}
|
{{-- ══ MAIN CONTENT ════════════════════════════════════════════════════════ --}}
|
||||||
<div id="mainContent" class="container-fluid py-4 mob-nav-pad">
|
<div class="container py-4 mob-nav-pad">
|
||||||
<div class="row g-3">
|
<div class="row g-3">
|
||||||
|
|
||||||
{{-- Parking List (RTL: renders on right side, but we put it second so it's on LEFT visually) --}}
|
{{-- Parking List (RTL: renders on right side, but we put it second so it's on LEFT visually) --}}
|
||||||
@ -117,7 +81,7 @@
|
|||||||
|
|
||||||
{{-- MAP --}}
|
{{-- MAP --}}
|
||||||
<div class="col-lg-8 order-lg-1 mob-section-map">
|
<div class="col-lg-8 order-lg-1 mob-section-map">
|
||||||
<div class="card h-100">
|
<div class="card h-100" style="min-height:520px;">
|
||||||
<div class="card-header d-flex align-items-center justify-content-between">
|
<div class="card-header d-flex align-items-center justify-content-between">
|
||||||
<span class="fw-700 text-sm" style="color:#0f172a;">
|
<span class="fw-700 text-sm" style="color:#0f172a;">
|
||||||
<i class="bi bi-map me-1" style="color:#6366f1;"></i>
|
<i class="bi bi-map me-1" style="color:#6366f1;"></i>
|
||||||
@ -126,7 +90,7 @@
|
|||||||
<span class="badge badge-soft-primary text-xs" id="map-count">--</span>
|
<span class="badge badge-soft-primary text-xs" id="map-count">--</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body p-0" style="border-radius:0 0 .75rem .75rem;overflow:hidden;">
|
<div class="card-body p-0" style="border-radius:0 0 .75rem .75rem;overflow:hidden;">
|
||||||
<div id="map"></div>
|
<div id="map" style="height:500px;"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -141,7 +105,7 @@
|
|||||||
</span>
|
</span>
|
||||||
<span class="badge badge-soft-success text-xs" id="list-count">{{ $lots->count() }} موقف</span>
|
<span class="badge badge-soft-success text-xs" id="list-count">{{ $lots->count() }} موقف</span>
|
||||||
</div>
|
</div>
|
||||||
<div style="overflow-y:auto;scrollbar-width:thin;" id="parkingList">
|
<div style="max-height:500px;overflow-y:auto;scrollbar-width:thin;" id="parkingList">
|
||||||
@forelse($lots as $lot)
|
@forelse($lots as $lot)
|
||||||
@php
|
@php
|
||||||
$avail = $lot['avail'];
|
$avail = $lot['avail'];
|
||||||
@ -166,7 +130,7 @@
|
|||||||
<p class="text-xs mb-2" style="color:#94a3b8;">{{ $lot['address'] }}</p>
|
<p class="text-xs mb-2" style="color:#94a3b8;">{{ $lot['address'] }}</p>
|
||||||
<div class="d-flex gap-3 text-xs" style="color:#64748b;">
|
<div class="d-flex gap-3 text-xs" style="color:#64748b;">
|
||||||
<span><i class="bi bi-car-front me-1"></i>{{ $total }}</span>
|
<span><i class="bi bi-car-front me-1"></i>{{ $total }}</span>
|
||||||
<span><i class="bi bi-currency-exchange me-1"></i>{{ number_format($lot['price']) }} ليرة سورية</span>
|
<span><i class="bi bi-currency-exchange me-1"></i>{{ number_format($lot['price']) }} ر.س</span>
|
||||||
<span><i class="bi bi-clock me-1"></i>{{ $lot['hours'] }}</span>
|
<span><i class="bi bi-clock me-1"></i>{{ $lot['hours'] }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -406,7 +370,7 @@
|
|||||||
if (l.avail < l.total * .2) return { cls: 'avail-limited', text: `${l.avail} محدود`, pct: Math.round((l.total - l.avail) / l.total * 100) };
|
if (l.avail < l.total * .2) return { cls: 'avail-limited', text: `${l.avail} محدود`, pct: Math.round((l.total - l.avail) / l.total * 100) };
|
||||||
return { cls: 'avail-open', text: `${l.avail} متاح`, pct: Math.round((l.total - l.avail) / l.total * 100) };
|
return { cls: 'avail-open', text: `${l.avail} متاح`, pct: Math.round((l.total - l.avail) / l.total * 100) };
|
||||||
}
|
}
|
||||||
const fmtPrice = p => new Intl.NumberFormat('ar-SY').format(p) + ' ليرة سورية';
|
const fmtPrice = p => new Intl.NumberFormat('ar-SY').format(p) + ' ر.س';
|
||||||
|
|
||||||
// ── Map (lazy-init on mobile) ─────────────────────────────────────────
|
// ── Map (lazy-init on mobile) ─────────────────────────────────────────
|
||||||
let map = null, mapReady = false;
|
let map = null, mapReady = false;
|
||||||
@ -415,7 +379,7 @@
|
|||||||
if (mapReady) { map?.invalidateSize(); return; }
|
if (mapReady) { map?.invalidateSize(); return; }
|
||||||
mapReady = true;
|
mapReady = true;
|
||||||
|
|
||||||
map = L.map('map', { attributionControl: false }).setView([33.5138, 36.2765], 12);
|
map = L.map('map').setView([33.5138, 36.2765], 12);
|
||||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||||
attribution: '© OpenStreetMap'
|
attribution: '© OpenStreetMap'
|
||||||
}).addTo(map);
|
}).addTo(map);
|
||||||
|
|||||||
@ -17,103 +17,152 @@
|
|||||||
<div class="app-layout">
|
<div class="app-layout">
|
||||||
|
|
||||||
{{-- ════════════════════════════════════════
|
{{-- ════════════════════════════════════════
|
||||||
TOPBAR — full width, topmost
|
SIDEBAR (appears on the RIGHT in RTL
|
||||||
|
because it is the first flex child)
|
||||||
════════════════════════════════════════ --}}
|
════════════════════════════════════════ --}}
|
||||||
@include('partials.topbar', ['isAdminLayout' => true])
|
<aside class="app-sidebar" id="appSidebar">
|
||||||
|
|
||||||
|
{{-- Logo --}}
|
||||||
|
<a href="{{ route('admin.dashboard') }}" class="sidebar-logo">
|
||||||
|
<div class="logo-icon">
|
||||||
|
<i class="bi bi-p-square-fill"></i>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="logo-text">دمشق باركينغ</div>
|
||||||
|
<div class="logo-sub">لوحة الإدارة</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
{{-- Navigation --}}
|
||||||
|
<nav class="sidebar-nav">
|
||||||
|
|
||||||
|
<div class="sidebar-section">الرئيسية</div>
|
||||||
|
|
||||||
|
<a href="{{ route('admin.dashboard') }}"
|
||||||
|
class="sidebar-link {{ request()->routeIs('admin.dashboard') ? 'active' : '' }}">
|
||||||
|
<i class="bi bi-speedometer2 sidebar-icon"></i>
|
||||||
|
<span>لوحة التحكم</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div class="sidebar-section">المواقف والحجوزات</div>
|
||||||
|
|
||||||
|
<a href="{{ route('admin.parking-lots.index') }}"
|
||||||
|
class="sidebar-link {{ request()->routeIs('admin.parking-lots.*') ? 'active' : '' }}">
|
||||||
|
<i class="bi bi-buildings sidebar-icon"></i>
|
||||||
|
<span>المواقف</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="{{ route('admin.bookings.active') }}"
|
||||||
|
class="sidebar-link {{ request()->routeIs('admin.bookings.*') ? 'active' : '' }}">
|
||||||
|
<i class="bi bi-calendar-check sidebar-icon"></i>
|
||||||
|
<span>الحجوزات النشطة</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div class="sidebar-section">التشغيل</div>
|
||||||
|
|
||||||
|
<a href="{{ route('admin.operators.index') }}"
|
||||||
|
class="sidebar-link {{ request()->routeIs('admin.operators.*') ? 'active' : '' }}">
|
||||||
|
<i class="bi bi-people sidebar-icon"></i>
|
||||||
|
<span>المشغّلون</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="{{ route('operator.dashboard') }}"
|
||||||
|
class="sidebar-link {{ request()->routeIs('operator.*') ? 'active' : '' }}">
|
||||||
|
<i class="bi bi-person-badge sidebar-icon"></i>
|
||||||
|
<span>لوحة المشغّل</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
{{-- Footer: user info + logout --}}
|
||||||
|
<div class="sidebar-footer">
|
||||||
|
<div class="d-flex align-items-center gap-2 mb-2">
|
||||||
|
<div class="user-avatar">
|
||||||
|
{{ mb_substr(auth()->user()?->name ?? 'م', 0, 1) }}
|
||||||
|
</div>
|
||||||
|
<div style="min-width:0">
|
||||||
|
<div class="user-name text-truncate">{{ auth()->user()?->name ?? 'المستخدم' }}</div>
|
||||||
|
<div class="user-role">
|
||||||
|
{{ auth()->user()?->role === 'admin' ? 'مدير النظام' : 'مشغّل' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<form method="POST" action="{{ route('logout') }}">
|
||||||
|
@csrf
|
||||||
|
<button type="submit"
|
||||||
|
class="btn btn-sm w-100 mt-1 text-start"
|
||||||
|
style="background:rgba(239,68,68,.12);color:#f87171;border:none;border-radius:.5rem;padding:.45rem .75rem;font-family:'Cairo',sans-serif;font-size:.82rem;">
|
||||||
|
<i class="bi bi-box-arrow-left me-2"></i>تسجيل الخروج
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</aside>
|
||||||
|
|
||||||
{{-- ════════════════════════════════════════
|
{{-- ════════════════════════════════════════
|
||||||
INNER ROW — sidebar + content
|
MAIN BODY
|
||||||
════════════════════════════════════════ --}}
|
════════════════════════════════════════ --}}
|
||||||
<div class="app-inner">
|
<div class="app-body">
|
||||||
|
|
||||||
<aside class="app-sidebar" id="appSidebar">
|
{{-- Topbar --}}
|
||||||
|
<header class="app-topbar">
|
||||||
@php $isAdmin = auth()->user()?->role === 'admin'; @endphp
|
<div class="d-flex align-items-center gap-3">
|
||||||
|
<button class="sidebar-toggle" id="sidebarToggle" aria-label="قائمة التنقل">
|
||||||
{{-- Navigation --}}
|
<i class="bi bi-list"></i>
|
||||||
<nav class="sidebar-nav">
|
</button>
|
||||||
|
<h1 class="topbar-title">@yield('page-title', 'لوحة التحكم')</h1>
|
||||||
@if($isAdmin)
|
|
||||||
<div class="sidebar-section">الرئيسية</div>
|
|
||||||
|
|
||||||
<a href="{{ route('admin.dashboard') }}"
|
|
||||||
class="sidebar-link {{ request()->routeIs('admin.dashboard') ? 'active' : '' }}">
|
|
||||||
<i class="bi bi-speedometer2 sidebar-icon"></i>
|
|
||||||
<span>لوحة التحكم</span>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<div class="sidebar-section">المواقف والحجوزات</div>
|
|
||||||
|
|
||||||
<a href="{{ route('admin.parking-lots.index') }}"
|
|
||||||
class="sidebar-link {{ request()->routeIs('admin.parking-lots.*') ? 'active' : '' }}">
|
|
||||||
<i class="bi bi-buildings sidebar-icon"></i>
|
|
||||||
<span>المواقف</span>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<a href="{{ route('admin.bookings.active') }}"
|
|
||||||
class="sidebar-link {{ request()->routeIs('admin.bookings.*') ? 'active' : '' }}">
|
|
||||||
<i class="bi bi-calendar-check sidebar-icon"></i>
|
|
||||||
<span>الحجوزات النشطة</span>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<div class="sidebar-section">التشغيل</div>
|
|
||||||
|
|
||||||
<a href="{{ route('admin.operators.index') }}"
|
|
||||||
class="sidebar-link {{ request()->routeIs('admin.operators.*') ? 'active' : '' }}">
|
|
||||||
<i class="bi bi-people sidebar-icon"></i>
|
|
||||||
<span>المشغّلون</span>
|
|
||||||
</a>
|
|
||||||
@endif
|
|
||||||
|
|
||||||
<a href="{{ route('operator.stats') }}"
|
|
||||||
class="sidebar-link {{ request()->routeIs('operator.stats') ? 'active' : '' }}">
|
|
||||||
<i class="bi bi-graph-up sidebar-icon"></i>
|
|
||||||
<span>الإحصائيات</span>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<a href="{{ route('operator.dashboard') }}"
|
|
||||||
class="sidebar-link {{ request()->routeIs('operator.dashboard') ? 'active' : '' }}">
|
|
||||||
<i class="bi bi-person-badge sidebar-icon"></i>
|
|
||||||
<span>{{ $isAdmin ? 'لوحة المشغّل' : 'الموقف' }}</span>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
</aside>
|
|
||||||
|
|
||||||
{{-- ════════════════════════════════════════
|
|
||||||
MAIN BODY
|
|
||||||
════════════════════════════════════════ --}}
|
|
||||||
<div class="app-body">
|
|
||||||
|
|
||||||
{{-- Flash Messages --}}
|
|
||||||
<div class="px-4 pt-3">
|
|
||||||
@if(session('success'))
|
|
||||||
<div class="alert alert-success d-flex align-items-center gap-2 border-0 rounded-3 py-2 mb-0"
|
|
||||||
role="alert">
|
|
||||||
<i class="bi bi-check-circle-fill flex-shrink-0"></i>
|
|
||||||
<span>{{ session('success') }}</span>
|
|
||||||
<button type="button" class="btn-close ms-auto" data-bs-dismiss="alert"></button>
|
|
||||||
</div>
|
|
||||||
@endif
|
|
||||||
@if(session('error'))
|
|
||||||
<div class="alert alert-danger d-flex align-items-center gap-2 border-0 rounded-3 py-2 mb-0"
|
|
||||||
role="alert">
|
|
||||||
<i class="bi bi-exclamation-triangle-fill flex-shrink-0"></i>
|
|
||||||
<span>{{ session('error') }}</span>
|
|
||||||
<button type="button" class="btn-close ms-auto" data-bs-dismiss="alert"></button>
|
|
||||||
</div>
|
|
||||||
@endif
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="topbar-actions d-flex align-items-center gap-2">
|
||||||
|
{{-- Desktop: link to public site --}}
|
||||||
|
<a href="{{ route('parking.index') }}"
|
||||||
|
class="btn btn-sm d-none d-md-inline-flex align-items-center"
|
||||||
|
style="background:#f1f5f9;color:#475569;border:none;border-radius:.5rem;font-family:'Cairo',sans-serif;"
|
||||||
|
>
|
||||||
|
<i class="bi bi-globe2 me-1"></i>الموقع العام
|
||||||
|
</a>
|
||||||
|
{{-- Mobile: user avatar + logout --}}
|
||||||
|
<div class="d-flex d-md-none align-items-center gap-2">
|
||||||
|
<div style="width:30px;height:30px;background:rgba(99,102,241,.12);border-radius:50%;display:flex;align-items:center;justify-content:center;color:#6366f1;font-weight:800;font-size:.82rem;flex-shrink:0;">
|
||||||
|
{{ mb_substr(auth()->user()?->name ?? 'م', 0, 1) }}
|
||||||
|
</div>
|
||||||
|
<form method="POST" action="{{ route('logout') }}" style="margin:0;">
|
||||||
|
@csrf
|
||||||
|
<button type="submit"
|
||||||
|
style="background:none;border:none;color:#94a3b8;padding:4px 6px;font-size:1.15rem;cursor:pointer;line-height:1;"
|
||||||
|
title="تسجيل الخروج">
|
||||||
|
<i class="bi bi-box-arrow-left"></i>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
{{-- Page Content --}}
|
{{-- Flash Messages --}}
|
||||||
<main class="app-content">
|
<div class="px-4 pt-3">
|
||||||
@yield('content')
|
@if(session('success'))
|
||||||
</main>
|
<div class="alert alert-success d-flex align-items-center gap-2 border-0 rounded-3 py-2 mb-0"
|
||||||
|
role="alert">
|
||||||
|
<i class="bi bi-check-circle-fill flex-shrink-0"></i>
|
||||||
|
<span>{{ session('success') }}</span>
|
||||||
|
<button type="button" class="btn-close ms-auto" data-bs-dismiss="alert"></button>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
@if(session('error'))
|
||||||
|
<div class="alert alert-danger d-flex align-items-center gap-2 border-0 rounded-3 py-2 mb-0"
|
||||||
|
role="alert">
|
||||||
|
<i class="bi bi-exclamation-triangle-fill flex-shrink-0"></i>
|
||||||
|
<span>{{ session('error') }}</span>
|
||||||
|
<button type="button" class="btn-close ms-auto" data-bs-dismiss="alert"></button>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>{{-- /app-body --}}
|
{{-- Page Content --}}
|
||||||
|
<main class="app-content">
|
||||||
|
@yield('content')
|
||||||
|
</main>
|
||||||
|
|
||||||
</div>{{-- /app-inner --}}
|
</div>{{-- /app-body --}}
|
||||||
|
|
||||||
</div>{{-- /app-layout --}}
|
</div>{{-- /app-layout --}}
|
||||||
|
|
||||||
@ -122,7 +171,6 @@
|
|||||||
|
|
||||||
{{-- ══ MOBILE BOTTOM NAVIGATION ═══════════════════════════════════════════════ --}}
|
{{-- ══ MOBILE BOTTOM NAVIGATION ═══════════════════════════════════════════════ --}}
|
||||||
<nav class="mobile-bottom-nav" aria-label="التنقل">
|
<nav class="mobile-bottom-nav" aria-label="التنقل">
|
||||||
@if($isAdmin)
|
|
||||||
<a href="{{ route('admin.dashboard') }}"
|
<a href="{{ route('admin.dashboard') }}"
|
||||||
class="mob-nav-item {{ request()->routeIs('admin.dashboard') ? 'active' : '' }}">
|
class="mob-nav-item {{ request()->routeIs('admin.dashboard') ? 'active' : '' }}">
|
||||||
<i class="bi bi-speedometer2"></i>
|
<i class="bi bi-speedometer2"></i>
|
||||||
@ -144,22 +192,10 @@
|
|||||||
<span>المشغّلون</span>
|
<span>المشغّلون</span>
|
||||||
</a>
|
</a>
|
||||||
<a href="{{ route('operator.dashboard') }}"
|
<a href="{{ route('operator.dashboard') }}"
|
||||||
class="mob-nav-item {{ request()->routeIs('operator.dashboard') ? 'active' : '' }}">
|
class="mob-nav-item {{ request()->routeIs('operator.*') ? 'active' : '' }}">
|
||||||
<i class="bi bi-person-badge"></i>
|
<i class="bi bi-person-badge"></i>
|
||||||
<span>التشغيل</span>
|
<span>التشغيل</span>
|
||||||
</a>
|
</a>
|
||||||
@else
|
|
||||||
<a href="{{ route('operator.stats') }}"
|
|
||||||
class="mob-nav-item {{ request()->routeIs('operator.stats') ? 'active' : '' }}">
|
|
||||||
<i class="bi bi-graph-up"></i>
|
|
||||||
<span>الإحصائيات</span>
|
|
||||||
</a>
|
|
||||||
<a href="{{ route('operator.dashboard') }}"
|
|
||||||
class="mob-nav-item {{ request()->routeIs('operator.dashboard') ? 'active' : '' }}">
|
|
||||||
<i class="bi bi-person-badge"></i>
|
|
||||||
<span>الموقف</span>
|
|
||||||
</a>
|
|
||||||
@endif
|
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|||||||
@ -14,10 +14,30 @@
|
|||||||
<body style="background:#f1f5f9;font-family:'Cairo',sans-serif;">
|
<body style="background:#f1f5f9;font-family:'Cairo',sans-serif;">
|
||||||
|
|
||||||
{{-- ══ HEADER ══════════════════════════════════════════════════════════════ --}}
|
{{-- ══ HEADER ══════════════════════════════════════════════════════════════ --}}
|
||||||
@include('partials.topbar')
|
<header class="public-header">
|
||||||
|
<div class="container">
|
||||||
|
<div class="d-flex align-items-center justify-content-between">
|
||||||
|
|
||||||
|
{{-- Logo --}}
|
||||||
|
<a href="{{ route('parking.index') }}" class="d-flex align-items-center gap-3 text-decoration-none">
|
||||||
|
<div style="width:40px;height:40px;background:#6366f1;border-radius:.625rem;display:flex;align-items:center;justify-content:center;flex-shrink:0;">
|
||||||
|
<i class="bi bi-p-square-fill text-white" style="font-size:1.25rem;"></i>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="fw-800" style="color:#f8fafc;font-size:1.05rem;line-height:1.2;">دمشق باركينغ</div>
|
||||||
|
<div style="color:#94a3b8;font-size:.72rem;">مواقف السيارات في دمشق</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
{{-- User Dropdown --}}
|
||||||
|
@include('partials.user-dropdown')
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
{{-- ══ CONTENT ═════════════════════════════════════════════════════════════ --}}
|
{{-- ══ CONTENT ═════════════════════════════════════════════════════════════ --}}
|
||||||
<main style="padding:1.5rem;">
|
<main class="container py-4" style="max-width:820px;">
|
||||||
|
|
||||||
{{-- Flash messages --}}
|
{{-- Flash messages --}}
|
||||||
@if(session('success'))
|
@if(session('success'))
|
||||||
|
|||||||
@ -241,7 +241,7 @@ $gradients = [
|
|||||||
|
|
||||||
<div class="lot-card-stats">
|
<div class="lot-card-stats">
|
||||||
<span><i class="bi bi-car-front"></i>{{ $lot['total'] }} مكان</span>
|
<span><i class="bi bi-car-front"></i>{{ $lot['total'] }} مكان</span>
|
||||||
<span><i class="bi bi-currency-exchange"></i>{{ number_format($lot['price']) }} ليرة سورية/س</span>
|
<span><i class="bi bi-currency-exchange"></i>{{ number_format($lot['price']) }} ل.س/س</span>
|
||||||
<span><i class="bi bi-clock"></i>{{ $lot['hours'] }}</span>
|
<span><i class="bi bi-clock"></i>{{ $lot['hours'] }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -290,7 +290,7 @@ $gradients = [
|
|||||||
<div class="d-flex align-items-center gap-3">
|
<div class="d-flex align-items-center gap-3">
|
||||||
<span class="badge badge-soft-success">{{ $selectedLot->available_spaces }} متاح</span>
|
<span class="badge badge-soft-success">{{ $selectedLot->available_spaces }} متاح</span>
|
||||||
<span class="badge badge-soft-warning">{{ $selectedLot->occupied_spaces }} مشغول</span>
|
<span class="badge badge-soft-warning">{{ $selectedLot->occupied_spaces }} مشغول</span>
|
||||||
@if(count($assignedLotIds) > 1)
|
@if(!$assignedLotId)
|
||||||
<a href="{{ route('operator.dashboard') }}"
|
<a href="{{ route('operator.dashboard') }}"
|
||||||
class="btn btn-sm fw-600"
|
class="btn btn-sm fw-600"
|
||||||
style="background:#f1f5f9;color:#475569;border:none;border-radius:.5rem;font-family:'Cairo',sans-serif;">
|
style="background:#f1f5f9;color:#475569;border:none;border-radius:.5rem;font-family:'Cairo',sans-serif;">
|
||||||
@ -962,13 +962,13 @@ async function openReceipt(id) {
|
|||||||
document.getElementById('rcpt-name').textContent = d.customer_name || 'غير محدد';
|
document.getElementById('rcpt-name').textContent = d.customer_name || 'غير محدد';
|
||||||
document.getElementById('rcpt-entry').textContent = d.entry_time;
|
document.getElementById('rcpt-entry').textContent = d.entry_time;
|
||||||
document.getElementById('rcpt-exit').textContent = d.exit_time;
|
document.getElementById('rcpt-exit').textContent = d.exit_time;
|
||||||
document.getElementById('rcpt-total').textContent = Number(d.total_fee).toLocaleString('ar-SA') + ' ليرة سورية';
|
document.getElementById('rcpt-total').textContent = Number(d.total_fee).toLocaleString('ar-SA') + ' ل.س';
|
||||||
|
|
||||||
const rows = d.fee_details.map(r => `
|
const rows = d.fee_details.map(r => `
|
||||||
<div class="fee-row">
|
<div class="fee-row">
|
||||||
<span>${r.day} <small style="color:#94a3b8;">${r.date}</small></span>
|
<span>${r.day} <small style="color:#94a3b8;">${r.date}</small></span>
|
||||||
<span style="color:#64748b;">${r.hours}س × ${Number(r.rate).toLocaleString('ar-SA')}</span>
|
<span style="color:#64748b;">${r.hours}س × ${Number(r.rate).toLocaleString('ar-SA')}</span>
|
||||||
<span class="fw-600" style="color:#0f172a;">${Number(r.subtotal).toLocaleString('ar-SA')} ليرة سورية</span>
|
<span class="fw-600" style="color:#0f172a;">${Number(r.subtotal).toLocaleString('ar-SA')} ل.س</span>
|
||||||
</div>`).join('');
|
</div>`).join('');
|
||||||
document.getElementById('rcpt-breakdown').innerHTML =
|
document.getElementById('rcpt-breakdown').innerHTML =
|
||||||
rows || '<p class="text-xs text-center" style="color:#94a3b8;">لا تفاصيل</p>';
|
rows || '<p class="text-xs text-center" style="color:#94a3b8;">لا تفاصيل</p>';
|
||||||
|
|||||||
@ -1,692 +0,0 @@
|
|||||||
@extends('layouts.admin')
|
|
||||||
@section('title', 'إحصائيات المشغّل — دمشق باركينغ')
|
|
||||||
@section('page-title', 'إحصائيات المشغّل')
|
|
||||||
|
|
||||||
@section('styles')
|
|
||||||
<style>
|
|
||||||
/* ── KPI Cards ───────────────────────────────────────────────────────────── */
|
|
||||||
.kpi-card {
|
|
||||||
border-radius: 1rem;
|
|
||||||
padding: 1.25rem 1.375rem;
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
border: none;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
.kpi-card::after {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
inset-inline-end: -18px;
|
|
||||||
bottom: -18px;
|
|
||||||
width: 90px;
|
|
||||||
height: 90px;
|
|
||||||
border-radius: 50%;
|
|
||||||
opacity: .08;
|
|
||||||
background: currentColor;
|
|
||||||
}
|
|
||||||
.kpi-icon {
|
|
||||||
width: 44px; height: 44px;
|
|
||||||
border-radius: .75rem;
|
|
||||||
display: flex; align-items: center; justify-content: center;
|
|
||||||
font-size: 1.3rem;
|
|
||||||
margin-bottom: .875rem;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
.kpi-value {
|
|
||||||
font-size: 2rem;
|
|
||||||
font-weight: 900;
|
|
||||||
line-height: 1.1;
|
|
||||||
margin-bottom: .2rem;
|
|
||||||
}
|
|
||||||
.kpi-label {
|
|
||||||
font-size: .8rem;
|
|
||||||
font-weight: 600;
|
|
||||||
opacity: .75;
|
|
||||||
}
|
|
||||||
.kpi-sub {
|
|
||||||
font-size: .7rem;
|
|
||||||
opacity: .55;
|
|
||||||
margin-top: .125rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Chart cards ─────────────────────────────────────────────────────────── */
|
|
||||||
.chart-card {
|
|
||||||
border-radius: 1rem;
|
|
||||||
border: none;
|
|
||||||
box-shadow: 0 4px 16px rgba(0,0,0,.06);
|
|
||||||
height: 100%;
|
|
||||||
display: flex; flex-direction: column;
|
|
||||||
}
|
|
||||||
.chart-card .card-header {
|
|
||||||
background: transparent;
|
|
||||||
border-bottom: 1px solid #f1f5f9;
|
|
||||||
padding: 1rem 1.25rem .75rem;
|
|
||||||
border-radius: 1rem 1rem 0 0;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Lot status cards ────────────────────────────────────────────────────── */
|
|
||||||
.lot-stat-card {
|
|
||||||
background: #fff;
|
|
||||||
border-radius: 1rem;
|
|
||||||
border: none;
|
|
||||||
box-shadow: 0 4px 16px rgba(0,0,0,.06);
|
|
||||||
padding: 1.125rem 1.25rem;
|
|
||||||
transition: transform .2s, box-shadow .2s;
|
|
||||||
}
|
|
||||||
.lot-stat-card:hover {
|
|
||||||
transform: translateY(-4px);
|
|
||||||
box-shadow: 0 12px 28px rgba(0,0,0,.1);
|
|
||||||
}
|
|
||||||
.lot-occ-bar { height: 6px; background: #e2e8f0; border-radius: 4px; overflow: hidden; margin: .625rem 0; }
|
|
||||||
.lot-occ-fill { height: 100%; border-radius: 4px; transition: width .6s cubic-bezier(.4,0,.2,1); }
|
|
||||||
|
|
||||||
/* ── Recent table ────────────────────────────────────────────────────────── */
|
|
||||||
.recent-table th { font-size: .75rem; font-weight: 700; color: #64748b; text-transform: uppercase; letter-spacing: .04em; white-space: nowrap; }
|
|
||||||
.recent-table td { font-size: .85rem; vertical-align: middle; }
|
|
||||||
.plate-badge {
|
|
||||||
display: inline-block;
|
|
||||||
font-family: monospace;
|
|
||||||
font-weight: 800;
|
|
||||||
letter-spacing: 2px;
|
|
||||||
font-size: .82rem;
|
|
||||||
direction: ltr;
|
|
||||||
background: #f1f5f9;
|
|
||||||
color: #1e293b;
|
|
||||||
padding: .2em .6em;
|
|
||||||
border-radius: .4rem;
|
|
||||||
}
|
|
||||||
.pay-badge {
|
|
||||||
font-size: .7rem; font-weight: 700; padding: .25em .65em; border-radius: 20px;
|
|
||||||
}
|
|
||||||
.pay-cash { background: rgba(16,185,129,.1); color: #059669; }
|
|
||||||
.pay-upload { background: rgba(99,102,241,.1); color: #4f46e5; }
|
|
||||||
|
|
||||||
/* ── Work progress card ──────────────────────────────────────────────────── */
|
|
||||||
.work-card {
|
|
||||||
background: #fff;
|
|
||||||
border-radius: 1rem;
|
|
||||||
border: none;
|
|
||||||
box-shadow: 0 4px 16px rgba(0,0,0,.06);
|
|
||||||
padding: 1.25rem 1.5rem;
|
|
||||||
}
|
|
||||||
.day-bar-track {
|
|
||||||
height: 8px; background: #e2e8f0; border-radius: 4px; overflow: hidden; margin: .5rem 0 .25rem;
|
|
||||||
}
|
|
||||||
.day-bar-fill {
|
|
||||||
height: 100%; border-radius: 4px;
|
|
||||||
background: linear-gradient(90deg, #6366f1, #10b981);
|
|
||||||
transition: width .8s cubic-bezier(.4,0,.2,1);
|
|
||||||
}
|
|
||||||
.work-stat {
|
|
||||||
display: flex; flex-direction: column; align-items: center;
|
|
||||||
padding: .75rem 1rem; border-radius: .75rem; flex: 1; min-width: 90px;
|
|
||||||
}
|
|
||||||
.work-stat-value { font-size: 1.5rem; font-weight: 900; line-height: 1.1; }
|
|
||||||
.work-stat-label { font-size: .72rem; font-weight: 600; opacity: .65; margin-top: .15rem; text-align: center; }
|
|
||||||
.trend-chip {
|
|
||||||
display: inline-flex; align-items: center; gap: .2rem;
|
|
||||||
font-size: .7rem; font-weight: 700; padding: .15em .55em; border-radius: 20px;
|
|
||||||
}
|
|
||||||
.trend-up { background: rgba(16,185,129,.1); color: #059669; }
|
|
||||||
.trend-down { background: rgba(239,68,68,.1); color: #dc2626; }
|
|
||||||
.trend-flat { background: rgba(100,116,139,.1); color: #64748b; }
|
|
||||||
|
|
||||||
/* ── No-lots empty state ─────────────────────────────────────────────────── */
|
|
||||||
.empty-state {
|
|
||||||
display: flex; flex-direction: column; align-items: center; justify-content: center;
|
|
||||||
padding: 4rem 2rem; text-align: center;
|
|
||||||
}
|
|
||||||
.empty-icon {
|
|
||||||
width: 80px; height: 80px;
|
|
||||||
background: rgba(99,102,241,.08);
|
|
||||||
border-radius: 50%;
|
|
||||||
display: flex; align-items: center; justify-content: center;
|
|
||||||
font-size: 2.5rem; color: #6366f1; margin-bottom: 1.25rem;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@endsection
|
|
||||||
|
|
||||||
@section('content')
|
|
||||||
|
|
||||||
@if(empty($assignedLotIds))
|
|
||||||
{{-- ── No lots assigned ──────────────────────────────────────────────────── --}}
|
|
||||||
<div class="card border-0 shadow-sm" style="border-radius:1rem;">
|
|
||||||
<div class="empty-state">
|
|
||||||
<div class="empty-icon"><i class="bi bi-bar-chart"></i></div>
|
|
||||||
<h5 class="fw-800 mb-2" style="color:#0f172a;">لا توجد مواقف مخصصة</h5>
|
|
||||||
<p class="mb-0" style="color:#64748b;max-width:340px;">
|
|
||||||
لم يتم تعيينك في أي موقف بعد. تواصل مع المدير لتخصيص مواقف لحسابك.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@else
|
|
||||||
{{-- ── KPI Row ───────────────────────────────────────────────────────────────── --}}
|
|
||||||
<div class="row g-3 mb-4">
|
|
||||||
|
|
||||||
<div class="col-xl col-lg-4 col-sm-6">
|
|
||||||
<div class="kpi-card h-100" style="background:linear-gradient(135deg,#0f172a 0%,#1e293b 100%);color:#e2e8f0;">
|
|
||||||
<div class="kpi-icon" style="background:rgba(99,102,241,.2);">
|
|
||||||
<i class="bi bi-car-front" style="color:#818cf8;"></i>
|
|
||||||
</div>
|
|
||||||
<div class="kpi-value" id="kpi-active" style="color:#fff;">--</div>
|
|
||||||
<div class="kpi-label">السيارات داخل المواقف</div>
|
|
||||||
<div class="kpi-sub">الآن · مشغول فعلياً</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-xl col-lg-4 col-sm-6">
|
|
||||||
<div class="kpi-card h-100" style="background:linear-gradient(135deg,#064e3b 0%,#065f46 100%);color:#d1fae5;">
|
|
||||||
<div class="kpi-icon" style="background:rgba(16,185,129,.2);">
|
|
||||||
<i class="bi bi-box-arrow-in-down" style="color:#34d399;"></i>
|
|
||||||
</div>
|
|
||||||
<div class="kpi-value" id="kpi-checkins" style="color:#fff;">--</div>
|
|
||||||
<div class="kpi-label">دخول اليوم</div>
|
|
||||||
<div class="kpi-sub">منذ منتصف الليل</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-xl col-lg-4 col-sm-6">
|
|
||||||
<div class="kpi-card h-100" style="background:linear-gradient(135deg,#1e1b4b 0%,#312e81 100%);color:#e0e7ff;">
|
|
||||||
<div class="kpi-icon" style="background:rgba(129,140,248,.2);">
|
|
||||||
<i class="bi bi-cash-stack" style="color:#a5b4fc;"></i>
|
|
||||||
</div>
|
|
||||||
<div class="kpi-value" id="kpi-revenue" style="color:#fff;font-size:1.4rem;">--</div>
|
|
||||||
<div class="kpi-label">إيرادات اليوم</div>
|
|
||||||
<div class="kpi-sub">ليرة سورية · مدفوع</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-xl col-lg-4 col-sm-6">
|
|
||||||
<div class="kpi-card h-100" style="background:linear-gradient(135deg,#4a044e 0%,#6b21a8 100%);color:#f3e8ff;">
|
|
||||||
<div class="kpi-icon" style="background:rgba(192,132,252,.2);">
|
|
||||||
<i class="bi bi-p-square" style="color:#c084fc;"></i>
|
|
||||||
</div>
|
|
||||||
<div class="kpi-value" id="kpi-available" style="color:#fff;">--</div>
|
|
||||||
<div class="kpi-label">أماكن متاحة</div>
|
|
||||||
<div class="kpi-sub">مجموع المواقف</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-xl col-lg-4 col-sm-6">
|
|
||||||
<div class="kpi-card h-100" style="background:linear-gradient(135deg,#451a03 0%,#92400e 100%);color:#fef3c7;">
|
|
||||||
<div class="kpi-icon" style="background:rgba(251,191,36,.2);">
|
|
||||||
<i class="bi bi-calendar-event" style="color:#fcd34d;"></i>
|
|
||||||
</div>
|
|
||||||
<div class="kpi-value" id="kpi-reservations" style="color:#fff;">--</div>
|
|
||||||
<div class="kpi-label">حجوزات منتظرة</div>
|
|
||||||
<div class="kpi-sub">لم تُفعَّل بعد</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{-- ── Work Progress ────────────────────────────────────────────────────────── --}}
|
|
||||||
<div class="work-card mb-4">
|
|
||||||
{{-- Header row --}}
|
|
||||||
<div class="d-flex align-items-center justify-content-between flex-wrap gap-2 mb-3">
|
|
||||||
<div>
|
|
||||||
<h6 class="fw-800 mb-0" style="color:#0f172a;font-size:.95rem;">
|
|
||||||
<i class="bi bi-person-check me-2" style="color:#6366f1;"></i>
|
|
||||||
تقدم العمل اليوم
|
|
||||||
</h6>
|
|
||||||
<p class="mb-0 mt-1" style="font-size:.75rem;color:#94a3b8;" id="work-date">{{ now()->translatedFormat('l، j F Y') }}</p>
|
|
||||||
</div>
|
|
||||||
<div class="d-flex align-items-center gap-2 flex-wrap">
|
|
||||||
<span class="badge" id="completion-badge" style="font-size:.75rem;padding:.4em .8em;"></span>
|
|
||||||
<span style="font-size:.75rem;color:#64748b;">معدل الإنجاز</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{-- Day progress bar --}}
|
|
||||||
<div class="mb-1" style="font-size:.72rem;color:#94a3b8;display:flex;justify-content:space-between;">
|
|
||||||
<span>بداية اليوم 00:00</span>
|
|
||||||
<span id="work-now-label"></span>
|
|
||||||
<span>نهاية اليوم 24:00</span>
|
|
||||||
</div>
|
|
||||||
<div class="day-bar-track">
|
|
||||||
<div class="day-bar-fill" id="day-progress-fill" style="width:0%;"></div>
|
|
||||||
</div>
|
|
||||||
<p class="mb-3" style="font-size:.7rem;color:#94a3b8;text-align:center;" id="day-pct-label"></p>
|
|
||||||
|
|
||||||
{{-- Work stats row --}}
|
|
||||||
<div class="d-flex flex-wrap gap-2">
|
|
||||||
|
|
||||||
<div class="work-stat" style="background:rgba(16,185,129,.07);color:#065f46;">
|
|
||||||
<div class="work-stat-value" id="ws-checkins">--</div>
|
|
||||||
<div class="work-stat-label">دخول اليوم</div>
|
|
||||||
<div id="ws-vs-yesterday" class="mt-1"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="work-stat" style="background:rgba(99,102,241,.07);color:#312e81;">
|
|
||||||
<div class="work-stat-value" id="ws-checkouts">--</div>
|
|
||||||
<div class="work-stat-label">خروج مكتمل</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="work-stat" style="background:rgba(239,68,68,.07);color:#7f1d1d;">
|
|
||||||
<div class="work-stat-value" id="ws-cancelled">--</div>
|
|
||||||
<div class="work-stat-label">ملغي</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="work-stat" style="background:rgba(245,158,11,.07);color:#78350f;">
|
|
||||||
<div class="work-stat-value" id="ws-avg-stay">--</div>
|
|
||||||
<div class="work-stat-label">متوسط المدة</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="work-stat" style="background:rgba(14,165,233,.07);color:#0c4a6e;">
|
|
||||||
<div class="work-stat-value" id="ws-peak-hour">--</div>
|
|
||||||
<div class="work-stat-label">الساعة الأكثر ازدحاماً</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{-- ── Charts ────────────────────────────────────────────────────────────────── --}}
|
|
||||||
<div class="row g-3 mb-4">
|
|
||||||
|
|
||||||
{{-- Daily check-ins bar chart --}}
|
|
||||||
<div class="col-lg-5">
|
|
||||||
<div class="chart-card card">
|
|
||||||
<div class="card-header d-flex align-items-center justify-content-between">
|
|
||||||
<span class="fw-700" style="font-size:.9rem;">
|
|
||||||
<i class="bi bi-bar-chart-line me-2" style="color:#10b981;"></i>
|
|
||||||
الدخول — آخر 7 أيام
|
|
||||||
</span>
|
|
||||||
<span class="badge" style="background:rgba(16,185,129,.1);color:#059669;font-size:.72rem;">دخول</span>
|
|
||||||
</div>
|
|
||||||
<div class="card-body p-3 flex-grow-1" style="min-height:220px;position:relative;">
|
|
||||||
<canvas id="checkinsChart"></canvas>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{-- Hourly activity today --}}
|
|
||||||
<div class="col-lg-4">
|
|
||||||
<div class="chart-card card">
|
|
||||||
<div class="card-header d-flex align-items-center justify-content-between">
|
|
||||||
<span class="fw-700" style="font-size:.9rem;">
|
|
||||||
<i class="bi bi-clock me-2" style="color:#f59e0b;"></i>
|
|
||||||
توزيع الدخول بالساعة
|
|
||||||
</span>
|
|
||||||
<span class="badge" style="background:rgba(245,158,11,.1);color:#d97706;font-size:.72rem;">اليوم</span>
|
|
||||||
</div>
|
|
||||||
<div class="card-body p-3 flex-grow-1" style="min-height:220px;position:relative;">
|
|
||||||
<canvas id="hourlyChart"></canvas>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{-- Daily revenue line chart --}}
|
|
||||||
<div class="col-lg-3">
|
|
||||||
<div class="chart-card card">
|
|
||||||
<div class="card-header d-flex align-items-center justify-content-between">
|
|
||||||
<span class="fw-700" style="font-size:.9rem;">
|
|
||||||
<i class="bi bi-graph-up-arrow me-2" style="color:#818cf8;"></i>
|
|
||||||
الإيرادات
|
|
||||||
</span>
|
|
||||||
<span class="badge" style="background:rgba(99,102,241,.1);color:#6366f1;font-size:.72rem;">ليرة سورية</span>
|
|
||||||
</div>
|
|
||||||
<div class="card-body p-3 flex-grow-1" style="min-height:220px;position:relative;">
|
|
||||||
<canvas id="revenueChart"></canvas>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{-- ── Per-lot status (only when operator has >1 lot) ────────────────────────── --}}
|
|
||||||
@if(count($lots) > 1)
|
|
||||||
<div class="mb-4">
|
|
||||||
<h6 class="fw-700 mb-3" style="color:#0f172a;font-size:.9rem;">
|
|
||||||
<i class="bi bi-buildings me-2" style="color:#6366f1;"></i>
|
|
||||||
حالة المواقف المخصصة
|
|
||||||
</h6>
|
|
||||||
<div class="row g-3" id="lotCards">
|
|
||||||
@foreach($lots as $lot)
|
|
||||||
<div class="col-lg-4 col-sm-6">
|
|
||||||
<div class="lot-stat-card">
|
|
||||||
<div class="d-flex align-items-start justify-content-between mb-2">
|
|
||||||
<div>
|
|
||||||
<div class="fw-800" style="font-size:.95rem;color:#0f172a;">{{ $lot->name }}</div>
|
|
||||||
<div style="font-size:.75rem;color:#94a3b8;">{{ $lot->address }}</div>
|
|
||||||
</div>
|
|
||||||
<span class="lot-status-badge-{{ $lot->id }} badge" style="font-size:.7rem;"></span>
|
|
||||||
</div>
|
|
||||||
<div class="lot-occ-bar">
|
|
||||||
<div class="lot-occ-fill lot-fill-{{ $lot->id }}" style="width:0;background:#6366f1;"></div>
|
|
||||||
</div>
|
|
||||||
<div class="d-flex justify-content-between" style="font-size:.78rem;color:#64748b;">
|
|
||||||
<span><i class="bi bi-car-front me-1"></i><span class="lot-active-{{ $lot->id }}">—</span> مشغول</span>
|
|
||||||
<span><span class="lot-avail-{{ $lot->id }}">—</span> / {{ $lot->total_capacity }} متاح</span>
|
|
||||||
</div>
|
|
||||||
<div class="mt-2 pt-2 border-top d-flex gap-3" style="font-size:.75rem;color:#64748b;border-color:#f1f5f9!important;">
|
|
||||||
<span><i class="bi bi-box-arrow-in-down me-1"></i>دخول اليوم: <strong class="lot-today-{{ $lot->id }}">—</strong></span>
|
|
||||||
<span><i class="bi bi-cash me-1"></i>إيراد: <strong class="lot-rev-{{ $lot->id }}">—</strong></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@endforeach
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@endif
|
|
||||||
|
|
||||||
{{-- ── Recent completions table ─────────────────────────────────────────────── --}}
|
|
||||||
<div class="card border-0 shadow-sm" style="border-radius:1rem;overflow:hidden;">
|
|
||||||
<div class="card-header bg-transparent d-flex align-items-center justify-content-between"
|
|
||||||
style="padding:1rem 1.25rem .75rem;border-bottom:1px solid #f1f5f9;">
|
|
||||||
<span class="fw-700" style="font-size:.9rem;">
|
|
||||||
<i class="bi bi-clock-history me-2" style="color:#0ea5e9;"></i>
|
|
||||||
آخر المدفوعات المنجزة
|
|
||||||
</span>
|
|
||||||
<span class="badge" id="last-refresh" style="background:#f1f5f9;color:#64748b;font-size:.7rem;"></span>
|
|
||||||
</div>
|
|
||||||
<div class="table-responsive">
|
|
||||||
<table class="table recent-table mb-0">
|
|
||||||
<thead style="background:#f8fafc;">
|
|
||||||
<tr>
|
|
||||||
<th class="px-4 py-3">اللوحة</th>
|
|
||||||
<th class="py-3">الموقف</th>
|
|
||||||
<th class="py-3">العميل</th>
|
|
||||||
<th class="py-3">وقت الدفع</th>
|
|
||||||
<th class="py-3 text-center">الرسوم</th>
|
|
||||||
<th class="py-3 text-center">الدفع</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody id="recent-tbody">
|
|
||||||
<tr>
|
|
||||||
<td colspan="6" class="text-center py-5" style="color:#94a3b8;">
|
|
||||||
<div class="spinner-border spinner-border-sm me-2"></div>
|
|
||||||
جاري التحميل...
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@endif {{-- assignedLotIds not empty --}}
|
|
||||||
|
|
||||||
@endsection
|
|
||||||
|
|
||||||
@push('scripts')
|
|
||||||
@if(!empty($assignedLotIds))
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.7/dist/chart.umd.min.js"></script>
|
|
||||||
<script>
|
|
||||||
let checkinsChart, revenueChart, hourlyChart;
|
|
||||||
|
|
||||||
const LOTS = @json($lots->map(fn($l) => ['id' => $l->id, 'name' => $l->name]));
|
|
||||||
const MULTI_LOT = LOTS.length > 1;
|
|
||||||
|
|
||||||
// ── Day progress bar (runs on its own clock) ──────────────────────────────────
|
|
||||||
function updateDayProgress() {
|
|
||||||
const now = new Date();
|
|
||||||
const pct = Math.round((now.getHours() * 60 + now.getMinutes()) / 1440 * 100);
|
|
||||||
const fill = document.getElementById('day-progress-fill');
|
|
||||||
const lbl = document.getElementById('day-pct-label');
|
|
||||||
const now2 = document.getElementById('work-now-label');
|
|
||||||
if (fill) fill.style.width = pct + '%';
|
|
||||||
if (lbl) lbl.textContent = `${pct}% من اليوم مضى`;
|
|
||||||
if (now2) now2.textContent = now.toLocaleTimeString('ar-SA', {hour:'2-digit', minute:'2-digit'}) + ' الآن';
|
|
||||||
}
|
|
||||||
updateDayProgress();
|
|
||||||
setInterval(updateDayProgress, 60000);
|
|
||||||
|
|
||||||
// ── Duration formatter ────────────────────────────────────────────────────────
|
|
||||||
function fmtDuration(mins) {
|
|
||||||
if (!mins) return '—';
|
|
||||||
const h = Math.floor(mins / 60);
|
|
||||||
const m = mins % 60;
|
|
||||||
return h > 0 ? `${h}س ${m}د` : `${m}د`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Load & render ─────────────────────────────────────────────────────────────
|
|
||||||
async function loadStats() {
|
|
||||||
try {
|
|
||||||
const { success, data } = await fetch('{{ route("operator.statsData") }}').then(r => r.json());
|
|
||||||
if (!success) return;
|
|
||||||
|
|
||||||
// KPI cards
|
|
||||||
document.getElementById('kpi-active').textContent = data.active_cars;
|
|
||||||
document.getElementById('kpi-checkins').textContent = data.checkins_today;
|
|
||||||
document.getElementById('kpi-revenue').textContent = data.revenue_today.toLocaleString('ar-SA') + ' ليرة سورية';
|
|
||||||
document.getElementById('kpi-available').textContent = data.available_spaces;
|
|
||||||
document.getElementById('kpi-reservations').textContent = data.pending_reservations;
|
|
||||||
|
|
||||||
// ── Work progress ──────────────────────────────────────────────────────
|
|
||||||
document.getElementById('ws-checkins').textContent = data.checkins_today;
|
|
||||||
document.getElementById('ws-checkouts').textContent = data.checkouts_today;
|
|
||||||
document.getElementById('ws-cancelled').textContent = data.cancelled_today;
|
|
||||||
document.getElementById('ws-avg-stay').textContent = fmtDuration(data.avg_duration_min);
|
|
||||||
|
|
||||||
const peak = data.peak_hour;
|
|
||||||
const peakStr = peak !== null && peak !== undefined
|
|
||||||
? new Date(2000,0,1,peak).toLocaleTimeString('ar-SA', {hour:'2-digit', minute:'2-digit'})
|
|
||||||
: '—';
|
|
||||||
document.getElementById('ws-peak-hour').textContent = peakStr;
|
|
||||||
|
|
||||||
// Completion rate badge
|
|
||||||
const cr = data.completion_rate ?? 100;
|
|
||||||
const crEl = document.getElementById('completion-badge');
|
|
||||||
if (crEl) {
|
|
||||||
crEl.textContent = cr + '%';
|
|
||||||
crEl.style.cssText = cr >= 80
|
|
||||||
? 'background:rgba(16,185,129,.12);color:#059669;font-size:.75rem;padding:.4em .8em;'
|
|
||||||
: cr >= 50
|
|
||||||
? 'background:rgba(245,158,11,.12);color:#d97706;font-size:.75rem;padding:.4em .8em;'
|
|
||||||
: 'background:rgba(239,68,68,.12);color:#dc2626;font-size:.75rem;padding:.4em .8em;';
|
|
||||||
}
|
|
||||||
|
|
||||||
// vs yesterday trend chip
|
|
||||||
const vsel = document.getElementById('ws-vs-yesterday');
|
|
||||||
if (vsel) {
|
|
||||||
const diff = data.checkins_today - data.checkins_yesterday;
|
|
||||||
const cls = diff > 0 ? 'trend-up' : diff < 0 ? 'trend-down' : 'trend-flat';
|
|
||||||
const icon = diff > 0 ? 'bi-arrow-up' : diff < 0 ? 'bi-arrow-down' : 'bi-dash';
|
|
||||||
const lbl = diff > 0 ? `+${diff} عن أمس` : diff < 0 ? `${diff} عن أمس` : 'مثل أمس';
|
|
||||||
vsel.innerHTML = `<span class="trend-chip ${cls}"><i class="bi ${icon}"></i>${lbl}</span>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Charts
|
|
||||||
renderCheckinsChart(data.daily_checkins, data.daily_dates);
|
|
||||||
renderRevenueChart(data.daily_revenue, data.daily_dates);
|
|
||||||
renderHourlyChart(data.hourly_activity ?? []);
|
|
||||||
|
|
||||||
// Per-lot cards
|
|
||||||
if (MULTI_LOT && data.lot_stats) {
|
|
||||||
data.lot_stats.forEach(lot => {
|
|
||||||
const pct = lot.pct;
|
|
||||||
const fill = document.querySelector(`.lot-fill-${lot.id}`);
|
|
||||||
const badge = document.querySelector(`.lot-status-badge-${lot.id}`);
|
|
||||||
if (fill) { fill.style.width = pct + '%'; fill.style.background = pct >= 90 ? '#ef4444' : pct >= 60 ? '#f59e0b' : '#10b981'; }
|
|
||||||
if (badge) {
|
|
||||||
badge.textContent = pct >= 90 ? 'ممتلئ' : pct >= 60 ? 'مشغول' : 'متاح';
|
|
||||||
badge.style.cssText = pct >= 90
|
|
||||||
? 'background:rgba(239,68,68,.1);color:#dc2626;'
|
|
||||||
: pct >= 60
|
|
||||||
? 'background:rgba(245,158,11,.1);color:#d97706;'
|
|
||||||
: 'background:rgba(16,185,129,.1);color:#059669;';
|
|
||||||
}
|
|
||||||
const setText = (sel, val) => { const el = document.querySelector(sel); if (el) el.textContent = val; };
|
|
||||||
setText(`.lot-active-${lot.id}`, lot.active);
|
|
||||||
setText(`.lot-avail-${lot.id}`, lot.available);
|
|
||||||
setText(`.lot-today-${lot.id}`, lot.today_ins);
|
|
||||||
setText(`.lot-rev-${lot.id}`, lot.today_rev.toLocaleString('ar-SA'));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Recent completions table
|
|
||||||
renderRecentTable(data.recent_completions ?? []);
|
|
||||||
|
|
||||||
document.getElementById('last-refresh').textContent = 'تحديث ' + new Date().toLocaleTimeString('ar-SA', {hour:'2-digit', minute:'2-digit'});
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Bar chart: check-ins ──────────────────────────────────────────────────────
|
|
||||||
function renderCheckinsChart(counts, dates) {
|
|
||||||
const labels = dates.map(d => {
|
|
||||||
const dt = new Date(d + 'T00:00:00');
|
|
||||||
return dt.toLocaleDateString('ar-SA', { weekday: 'short', day: 'numeric' });
|
|
||||||
});
|
|
||||||
const fullLabels = dates.map(d => {
|
|
||||||
const dt = new Date(d + 'T00:00:00');
|
|
||||||
return dt.toLocaleDateString('ar-SA', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' });
|
|
||||||
});
|
|
||||||
|
|
||||||
if (checkinsChart) { checkinsChart.destroy(); }
|
|
||||||
checkinsChart = new Chart(document.getElementById('checkinsChart'), {
|
|
||||||
type: 'bar',
|
|
||||||
data: {
|
|
||||||
labels,
|
|
||||||
datasets: [{
|
|
||||||
data: counts,
|
|
||||||
backgroundColor: counts.map((_, i) => i === counts.length - 1 ? 'rgba(16,185,129,.9)' : 'rgba(16,185,129,.25)'),
|
|
||||||
borderColor: counts.map((_, i) => i === counts.length - 1 ? '#10b981' : 'rgba(16,185,129,.4)'),
|
|
||||||
borderWidth: 2,
|
|
||||||
borderRadius: 8,
|
|
||||||
hoverBackgroundColor: 'rgba(16,185,129,.7)',
|
|
||||||
}],
|
|
||||||
},
|
|
||||||
options: {
|
|
||||||
responsive: true,
|
|
||||||
maintainAspectRatio: false,
|
|
||||||
plugins: {
|
|
||||||
legend: { display: false },
|
|
||||||
tooltip: { callbacks: {
|
|
||||||
title: ctx => fullLabels[ctx[0].dataIndex],
|
|
||||||
label: ctx => ` ${ctx.parsed.y} دخول`,
|
|
||||||
}},
|
|
||||||
},
|
|
||||||
scales: {
|
|
||||||
y: { beginAtZero: true, ticks: { precision: 0 }, grid: { color: '#f8fafc' } },
|
|
||||||
x: { grid: { display: false } },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Line chart: revenue ───────────────────────────────────────────────────────
|
|
||||||
function renderRevenueChart(amounts, dates) {
|
|
||||||
const labels = dates.map(d => {
|
|
||||||
const dt = new Date(d + 'T00:00:00');
|
|
||||||
return dt.toLocaleDateString('ar-SA', { weekday: 'short', day: 'numeric' });
|
|
||||||
});
|
|
||||||
|
|
||||||
if (revenueChart) { revenueChart.destroy(); }
|
|
||||||
revenueChart = new Chart(document.getElementById('revenueChart'), {
|
|
||||||
type: 'line',
|
|
||||||
data: {
|
|
||||||
labels,
|
|
||||||
datasets: [{
|
|
||||||
data: amounts,
|
|
||||||
borderColor: '#818cf8',
|
|
||||||
backgroundColor: 'rgba(99,102,241,.08)',
|
|
||||||
borderWidth: 2.5,
|
|
||||||
pointBackgroundColor: '#6366f1',
|
|
||||||
pointRadius: 4,
|
|
||||||
pointHoverRadius: 6,
|
|
||||||
fill: true,
|
|
||||||
tension: 0.4,
|
|
||||||
}],
|
|
||||||
},
|
|
||||||
options: {
|
|
||||||
responsive: true,
|
|
||||||
maintainAspectRatio: false,
|
|
||||||
plugins: {
|
|
||||||
legend: { display: false },
|
|
||||||
tooltip: { callbacks: {
|
|
||||||
label: ctx => ` ${ctx.parsed.y.toLocaleString('ar-SA')} ليرة سورية`,
|
|
||||||
}},
|
|
||||||
},
|
|
||||||
scales: {
|
|
||||||
y: { beginAtZero: true, grid: { color: '#f8fafc' }, ticks: { callback: v => v.toLocaleString('ar-SA') } },
|
|
||||||
x: { grid: { display: false } },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Hourly activity chart ─────────────────────────────────────────────────────
|
|
||||||
function renderHourlyChart(hourly) {
|
|
||||||
// Show only hours 6–23 to keep the chart readable
|
|
||||||
const hours = Array.from({length: 18}, (_, i) => i + 6);
|
|
||||||
const counts = hours.map(h => hourly[h] ?? 0);
|
|
||||||
const labels = hours.map(h => h + ':00');
|
|
||||||
const nowH = new Date().getHours();
|
|
||||||
const colors = counts.map((_, i) => {
|
|
||||||
const h = hours[i];
|
|
||||||
if (h === nowH) return 'rgba(245,158,11,.9)';
|
|
||||||
return counts[i] === Math.max(...counts) && counts[i] > 0 ? 'rgba(99,102,241,.8)' : 'rgba(99,102,241,.2)';
|
|
||||||
});
|
|
||||||
|
|
||||||
if (hourlyChart) { hourlyChart.destroy(); }
|
|
||||||
hourlyChart = new Chart(document.getElementById('hourlyChart'), {
|
|
||||||
type: 'bar',
|
|
||||||
data: {
|
|
||||||
labels,
|
|
||||||
datasets: [{
|
|
||||||
data: counts,
|
|
||||||
backgroundColor: colors,
|
|
||||||
borderColor: colors.map(c => c.replace(/[\d.]+\)$/, '1)')),
|
|
||||||
borderWidth: 1,
|
|
||||||
borderRadius: 4,
|
|
||||||
}],
|
|
||||||
},
|
|
||||||
options: {
|
|
||||||
responsive: true,
|
|
||||||
maintainAspectRatio: false,
|
|
||||||
plugins: {
|
|
||||||
legend: { display: false },
|
|
||||||
tooltip: { callbacks: {
|
|
||||||
title: ctx => `${hours[ctx[0].dataIndex]}:00`,
|
|
||||||
label: ctx => ` ${ctx.parsed.y} دخول`,
|
|
||||||
}},
|
|
||||||
},
|
|
||||||
scales: {
|
|
||||||
y: { beginAtZero: true, ticks: { precision: 0, font: { size: 10 } }, grid: { color: '#f8fafc' } },
|
|
||||||
x: { grid: { display: false }, ticks: { font: { size: 9 }, maxRotation: 0 } },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Recent completions ────────────────────────────────────────────────────────
|
|
||||||
function renderRecentTable(rows) {
|
|
||||||
const tbody = document.getElementById('recent-tbody');
|
|
||||||
if (!rows.length) {
|
|
||||||
tbody.innerHTML = `<tr><td colspan="6" class="text-center py-5" style="color:#94a3b8;">
|
|
||||||
<i class="bi bi-inbox d-block mb-2" style="font-size:2rem;opacity:.3;"></i>
|
|
||||||
لا توجد مدفوعات منجزة بعد
|
|
||||||
</td></tr>`;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const fmt = iso => {
|
|
||||||
if (!iso) return '—';
|
|
||||||
return new Date(iso).toLocaleString('ar-SA', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' });
|
|
||||||
};
|
|
||||||
|
|
||||||
tbody.innerHTML = rows.map(r => {
|
|
||||||
const payBadge = r.payment_method === 'cash'
|
|
||||||
? '<span class="pay-badge pay-cash"><i class="bi bi-cash me-1"></i>نقدي</span>'
|
|
||||||
: '<span class="pay-badge pay-upload"><i class="bi bi-upload me-1"></i>تحويل</span>';
|
|
||||||
const fee = r.total_fee ? r.total_fee.toLocaleString('ar-SA') + ' ليرة سورية' : '—';
|
|
||||||
return `<tr>
|
|
||||||
<td class="px-4 py-3"><span class="plate-badge">${r.vehicle_plate ?? '—'}</span></td>
|
|
||||||
<td class="py-3" style="color:#475569;">${r.parking_lot?.name ?? '—'}</td>
|
|
||||||
<td class="py-3" style="color:#475569;">${r.customer_name ?? '—'}</td>
|
|
||||||
<td class="py-3" style="color:#64748b;font-size:.8rem;">${fmt(r.paid_at)}</td>
|
|
||||||
<td class="py-3 text-center fw-700" style="color:#0f172a;">${fee}</td>
|
|
||||||
<td class="py-3 text-center">${payBadge}</td>
|
|
||||||
</tr>`;
|
|
||||||
}).join('');
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Boot ──────────────────────────────────────────────────────────────────────
|
|
||||||
loadStats();
|
|
||||||
setInterval(loadStats, 30000);
|
|
||||||
</script>
|
|
||||||
@endif
|
|
||||||
@endpush
|
|
||||||
@ -1,33 +0,0 @@
|
|||||||
<header class="app-topbar">
|
|
||||||
|
|
||||||
{{-- Left: logo + optional admin controls --}}
|
|
||||||
<div class="d-flex align-items-center gap-3">
|
|
||||||
|
|
||||||
{{-- Brand logo (always shown, links to public site) --}}
|
|
||||||
<a href="{{ route('parking.index') }}" class="d-flex align-items-center gap-3 text-decoration-none">
|
|
||||||
<div style="width:36px;height:36px;background:#6366f1;border-radius:.5rem;display:flex;align-items:center;justify-content:center;flex-shrink:0;">
|
|
||||||
<i class="bi bi-p-square-fill text-white" style="font-size:1.1rem;"></i>
|
|
||||||
</div>
|
|
||||||
<div class="d-none d-sm-block">
|
|
||||||
<div class="fw-800" style="color:#f8fafc;font-size:1rem;line-height:1.2;">دمشق باركينغ</div>
|
|
||||||
<div style="color:#94a3b8;font-size:.68rem;">مواقف السيارات في دمشق</div>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
@if($isAdminLayout ?? false)
|
|
||||||
{{-- Sidebar toggle (admin/operator pages only) --}}
|
|
||||||
<button class="sidebar-toggle" id="sidebarToggle" aria-label="قائمة التنقل">
|
|
||||||
<i class="bi bi-list"></i>
|
|
||||||
</button>
|
|
||||||
{{-- Page title --}}
|
|
||||||
<h1 class="topbar-title">@yield('page-title', 'لوحة التحكم')</h1>
|
|
||||||
@endif
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{-- Right: user dropdown --}}
|
|
||||||
<div class="d-flex align-items-center gap-2">
|
|
||||||
@include('partials.user-dropdown')
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</header>
|
|
||||||
@ -6,7 +6,8 @@
|
|||||||
data-bs-toggle="dropdown"
|
data-bs-toggle="dropdown"
|
||||||
aria-expanded="false"
|
aria-expanded="false"
|
||||||
style="background:rgba(255,255,255,.1);color:#f8fafc;border:1px solid rgba(255,255,255,.18);border-radius:.625rem;padding:.35rem .75rem;font-family:'Cairo',sans-serif;">
|
style="background:rgba(255,255,255,.1);color:#f8fafc;border:1px solid rgba(255,255,255,.18);border-radius:.625rem;padding:.35rem .75rem;font-family:'Cairo',sans-serif;">
|
||||||
<div style="width:32px;height:32px;background:linear-gradient(135deg,#6366f1,#8b5cf6);border-radius:50%;display:flex;align-items:center;justify-content:center;font-weight:800;font-size:.9rem;flex-shrink:0;color:#fff;border:2px solid rgba(255,255,255,.25);">
|
{{-- Avatar circle --}}
|
||||||
|
<div style="width:32px;height:32px;background:#6366f1;border-radius:50%;display:flex;align-items:center;justify-content:center;font-weight:800;font-size:.9rem;flex-shrink:0;color:#fff;border:2px solid rgba(255,255,255,.25);">
|
||||||
{{ mb_substr(auth()->user()->name, 0, 1) }}
|
{{ mb_substr(auth()->user()->name, 0, 1) }}
|
||||||
</div>
|
</div>
|
||||||
<span class="d-none d-sm-inline fw-600" style="font-size:.875rem;max-width:120px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">
|
<span class="d-none d-sm-inline fw-600" style="font-size:.875rem;max-width:120px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">
|
||||||
@ -31,6 +32,7 @@
|
|||||||
<div style="color:#64748b;font-size:.75rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">
|
<div style="color:#64748b;font-size:.75rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">
|
||||||
{{ auth()->user()->email }}
|
{{ auth()->user()->email }}
|
||||||
</div>
|
</div>
|
||||||
|
{{-- Role badge --}}
|
||||||
@php
|
@php
|
||||||
$role = auth()->user()->role;
|
$role = auth()->user()->role;
|
||||||
$roleLabel = match($role) {
|
$roleLabel = match($role) {
|
||||||
@ -51,16 +53,6 @@
|
|||||||
|
|
||||||
<li><hr class="dropdown-divider my-1" style="border-color:#f1f5f9;"></li>
|
<li><hr class="dropdown-divider my-1" style="border-color:#f1f5f9;"></li>
|
||||||
|
|
||||||
{{-- Public site --}}
|
|
||||||
<li>
|
|
||||||
<a href="{{ route('parking.index') }}"
|
|
||||||
class="dropdown-item d-flex align-items-center gap-2 rounded-2"
|
|
||||||
style="color:#374151;font-size:.875rem;padding:.5rem .75rem;">
|
|
||||||
<i class="bi bi-globe2" style="color:#6366f1;font-size:1rem;width:18px;text-align:center;"></i>
|
|
||||||
الموقع العام
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
{{-- My profile --}}
|
{{-- My profile --}}
|
||||||
<li>
|
<li>
|
||||||
<a href="{{ route('profile.show') }}"
|
<a href="{{ route('profile.show') }}"
|
||||||
@ -71,8 +63,7 @@
|
|||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
{{-- My reservations (regular users) --}}
|
{{-- My reservations --}}
|
||||||
@if($role === 'user')
|
|
||||||
<li>
|
<li>
|
||||||
<a href="{{ route('user.dashboard') }}"
|
<a href="{{ route('user.dashboard') }}"
|
||||||
class="dropdown-item d-flex align-items-center gap-2 rounded-2"
|
class="dropdown-item d-flex align-items-center gap-2 rounded-2"
|
||||||
@ -81,10 +72,9 @@
|
|||||||
حجوزاتي
|
حجوزاتي
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
@endif
|
|
||||||
|
|
||||||
{{-- Operator panel (operators & admins) --}}
|
{{-- Operator panel (operators & admins) --}}
|
||||||
@if(in_array($role, ['operator', 'admin']))
|
@if(in_array(auth()->user()->role, ['operator', 'admin']))
|
||||||
<li>
|
<li>
|
||||||
<a href="{{ route('operator.dashboard') }}"
|
<a href="{{ route('operator.dashboard') }}"
|
||||||
class="dropdown-item d-flex align-items-center gap-2 rounded-2"
|
class="dropdown-item d-flex align-items-center gap-2 rounded-2"
|
||||||
@ -96,7 +86,7 @@
|
|||||||
@endif
|
@endif
|
||||||
|
|
||||||
{{-- Admin panel (admins only) --}}
|
{{-- Admin panel (admins only) --}}
|
||||||
@if($role === 'admin')
|
@if(auth()->user()->role === 'admin')
|
||||||
<li>
|
<li>
|
||||||
<a href="{{ route('admin.dashboard') }}"
|
<a href="{{ route('admin.dashboard') }}"
|
||||||
class="dropdown-item d-flex align-items-center gap-2 rounded-2"
|
class="dropdown-item d-flex align-items-center gap-2 rounded-2"
|
||||||
|
|||||||
@ -5,15 +5,15 @@
|
|||||||
|
|
||||||
{{-- Page header --}}
|
{{-- Page header --}}
|
||||||
<div class="d-flex align-items-center gap-3 mb-4">
|
<div class="d-flex align-items-center gap-3 mb-4">
|
||||||
|
<a href="{{ route('parking.index') }}"
|
||||||
|
class="btn btn-sm"
|
||||||
|
style="background:#f1f5f9;color:#475569;border:none;border-radius:.5rem;font-family:'Cairo',sans-serif;">
|
||||||
|
<i class="bi bi-arrow-right me-1"></i>العودة
|
||||||
|
</a>
|
||||||
<div>
|
<div>
|
||||||
<h1 class="fw-800 mb-0" style="font-size:1.25rem;color:#0f172a;">حجوزاتي</h1>
|
<h1 class="fw-800 mb-0" style="font-size:1.25rem;color:#0f172a;">حجوزاتي</h1>
|
||||||
<p class="text-sm mb-0" style="color:#64748b;">سجل حجوزاتك في مواقف السيارات</p>
|
<p class="text-sm mb-0" style="color:#64748b;">سجل حجوزاتك في مواقف السيارات</p>
|
||||||
</div>
|
</div>
|
||||||
<a href="{{ route('parking.index') }}"
|
|
||||||
class="btn btn-sm ms-auto"
|
|
||||||
style="background:#f1f5f9;color:#475569;border:none;border-radius:.5rem;font-family:'Cairo',sans-serif;">
|
|
||||||
<i class="bi bi-arrow-left me-1"></i>العودة
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{-- ── Debt banner ─────────────────────────────────────────────────────────── --}}
|
{{-- ── Debt banner ─────────────────────────────────────────────────────────── --}}
|
||||||
@ -27,12 +27,12 @@
|
|||||||
<div class="fw-700" style="color:#92400e;font-size:.9rem;">رصيد مستحق غير مدفوع</div>
|
<div class="fw-700" style="color:#92400e;font-size:.9rem;">رصيد مستحق غير مدفوع</div>
|
||||||
<div class="text-sm" style="color:#b45309;">
|
<div class="text-sm" style="color:#b45309;">
|
||||||
لديك رسوم إلغاء متراكمة بقيمة
|
لديك رسوم إلغاء متراكمة بقيمة
|
||||||
<strong>{{ number_format($pendingDebt) }} ليرة سورية</strong>
|
<strong>{{ number_format($pendingDebt) }} ل.س</strong>
|
||||||
— ستُضاف تلقائياً إلى حجزك القادم.
|
— ستُضاف تلقائياً إلى حجزك القادم.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="fw-800" style="color:#92400e;font-size:1.25rem;white-space:nowrap;">
|
<div class="fw-800" style="color:#92400e;font-size:1.25rem;white-space:nowrap;">
|
||||||
{{ number_format($pendingDebt) }} ليرة سورية
|
{{ number_format($pendingDebt) }} ل.س
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
@ -130,13 +130,13 @@
|
|||||||
{{-- Fee column --}}
|
{{-- Fee column --}}
|
||||||
<td>
|
<td>
|
||||||
@if($hasDebt)
|
@if($hasDebt)
|
||||||
<span class="fw-700" style="color:#ef4444;">{{ number_format($booking->total_fee) }} ليرة سورية</span>
|
<span class="fw-700" style="color:#ef4444;">{{ number_format($booking->total_fee) }} ل.س</span>
|
||||||
<div class="text-xs fw-600" style="color:#ef4444;">مستحق</div>
|
<div class="text-xs fw-600" style="color:#ef4444;">مستحق</div>
|
||||||
@elseif($booking->total_fee > 0 && !is_null($booking->paid_at))
|
@elseif($booking->total_fee > 0 && !is_null($booking->paid_at))
|
||||||
<span class="fw-600 text-sm" style="color:#10b981;">{{ number_format($booking->total_fee) }} ليرة سورية</span>
|
<span class="fw-600 text-sm" style="color:#10b981;">{{ number_format($booking->total_fee) }} ل.س</span>
|
||||||
<div class="text-xs" style="color:#10b981;">مدفوع</div>
|
<div class="text-xs" style="color:#10b981;">مدفوع</div>
|
||||||
@elseif($booking->total_fee > 0)
|
@elseif($booking->total_fee > 0)
|
||||||
<span class="fw-600 text-sm" style="color:#475569;">{{ number_format($booking->total_fee) }} ليرة سورية</span>
|
<span class="fw-600 text-sm" style="color:#475569;">{{ number_format($booking->total_fee) }} ل.س</span>
|
||||||
@elseif($isFree && $booking->status === 'cancelled')
|
@elseif($isFree && $booking->status === 'cancelled')
|
||||||
<span class="text-xs" style="color:#94a3b8;">مجاني</span>
|
<span class="text-xs" style="color:#94a3b8;">مجاني</span>
|
||||||
@else
|
@else
|
||||||
@ -304,16 +304,16 @@
|
|||||||
|
|
||||||
{{-- After pay-now chosen: back + confirm --}}
|
{{-- After pay-now chosen: back + confirm --}}
|
||||||
<div id="cancelPayNowActions" style="display:none;" class="d-flex gap-2">
|
<div id="cancelPayNowActions" style="display:none;" class="d-flex gap-2">
|
||||||
|
<button type="button" class="btn btn-light fw-600"
|
||||||
|
style="font-family:'Cairo',sans-serif;border-radius:.5rem;"
|
||||||
|
onclick="cancelBackToActions()">
|
||||||
|
<i class="bi bi-arrow-right me-1"></i>رجوع
|
||||||
|
</button>
|
||||||
<button type="button" id="cancelConfirmPayBtn"
|
<button type="button" id="cancelConfirmPayBtn"
|
||||||
class="btn btn-success fw-bold flex-grow-1"
|
class="btn btn-success fw-bold flex-grow-1"
|
||||||
style="font-family:'Cairo',sans-serif;border-radius:.5rem;">
|
style="font-family:'Cairo',sans-serif;border-radius:.5rem;">
|
||||||
<i class="bi bi-check2-circle me-1"></i>تأكيد الدفع والإلغاء
|
<i class="bi bi-check2-circle me-1"></i>تأكيد الدفع والإلغاء
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="btn btn-light fw-600"
|
|
||||||
style="font-family:'Cairo',sans-serif;border-radius:.5rem;"
|
|
||||||
onclick="cancelBackToActions()">
|
|
||||||
<i class="bi bi-arrow-left me-1"></i>رجوع
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@ -382,12 +382,12 @@ async function openCancelModal(id, lotName, startIso) {
|
|||||||
<div style="display:flex;justify-content:space-between;padding:.25rem 0;border-bottom:1px dashed #f1f5f9;">
|
<div style="display:flex;justify-content:space-between;padding:.25rem 0;border-bottom:1px dashed #f1f5f9;">
|
||||||
<span>${r.day} <small style="color:#94a3b8;">${r.date}</small></span>
|
<span>${r.day} <small style="color:#94a3b8;">${r.date}</small></span>
|
||||||
<span style="color:#64748b;">${r.hours}س × ${Number(r.rate).toLocaleString('ar-SA')}</span>
|
<span style="color:#64748b;">${r.hours}س × ${Number(r.rate).toLocaleString('ar-SA')}</span>
|
||||||
<span class="fw-600" style="color:#0f172a;">${Number(r.subtotal).toLocaleString('ar-SA')} ليرة سورية</span>
|
<span class="fw-600" style="color:#0f172a;">${Number(r.subtotal).toLocaleString('ar-SA')} ل.س</span>
|
||||||
</div>`).join('');
|
</div>`).join('');
|
||||||
document.getElementById('cancelFeeBreakdown').innerHTML =
|
document.getElementById('cancelFeeBreakdown').innerHTML =
|
||||||
rows || '<span style="color:#94a3b8;font-size:.8rem;">— مدة قصيرة جداً —</span>';
|
rows || '<span style="color:#94a3b8;font-size:.8rem;">— مدة قصيرة جداً —</span>';
|
||||||
document.getElementById('cancelFeeTotal').textContent =
|
document.getElementById('cancelFeeTotal').textContent =
|
||||||
Number(d.fee).toLocaleString('ar-SA') + ' ليرة سورية';
|
Number(d.fee).toLocaleString('ar-SA') + ' ل.س';
|
||||||
|
|
||||||
// Reset UI
|
// Reset UI
|
||||||
document.getElementById('cancelPayMethodWrap').style.display = 'none';
|
document.getElementById('cancelPayMethodWrap').style.display = 'none';
|
||||||
|
|||||||
@ -5,15 +5,15 @@
|
|||||||
|
|
||||||
{{-- Page header --}}
|
{{-- Page header --}}
|
||||||
<div class="d-flex align-items-center gap-3 mb-4">
|
<div class="d-flex align-items-center gap-3 mb-4">
|
||||||
|
<a href="{{ route('parking.index') }}"
|
||||||
|
class="btn btn-sm"
|
||||||
|
style="background:#f1f5f9;color:#475569;border:none;border-radius:.5rem;font-family:'Cairo',sans-serif;">
|
||||||
|
<i class="bi bi-arrow-right me-1"></i>العودة
|
||||||
|
</a>
|
||||||
<div>
|
<div>
|
||||||
<h1 class="fw-800 mb-0" style="font-size:1.25rem;color:#0f172a;">معلوماتي</h1>
|
<h1 class="fw-800 mb-0" style="font-size:1.25rem;color:#0f172a;">معلوماتي</h1>
|
||||||
<p class="text-sm mb-0" style="color:#64748b;">إدارة بيانات حسابك الشخصي</p>
|
<p class="text-sm mb-0" style="color:#64748b;">إدارة بيانات حسابك الشخصي</p>
|
||||||
</div>
|
</div>
|
||||||
<a href="{{ route('parking.index') }}"
|
|
||||||
class="btn btn-sm ms-auto"
|
|
||||||
style="background:#f1f5f9;color:#475569;border:none;border-radius:.5rem;font-family:'Cairo',sans-serif;">
|
|
||||||
<i class="bi bi-arrow-left me-1"></i>العودة
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row g-4">
|
<div class="row g-4">
|
||||||
|
|||||||
@ -54,8 +54,6 @@ Route::prefix('admin')->middleware('admin')->name('admin.')->group(function () {
|
|||||||
// Operator Dashboard
|
// Operator Dashboard
|
||||||
Route::prefix('operator')->middleware('operator')->name('operator.')->group(function () {
|
Route::prefix('operator')->middleware('operator')->name('operator.')->group(function () {
|
||||||
Route::get('/dashboard', [\App\Http\Controllers\Operator\OperatorController::class, 'dashboard'])->name('dashboard');
|
Route::get('/dashboard', [\App\Http\Controllers\Operator\OperatorController::class, 'dashboard'])->name('dashboard');
|
||||||
Route::get('/stats', [\App\Http\Controllers\Operator\OperatorController::class, 'statsPage'])->name('stats');
|
|
||||||
Route::get('/stats-data', [\App\Http\Controllers\Operator\OperatorController::class, 'statsJson'])->name('statsData');
|
|
||||||
Route::post('/check-in', [\App\Http\Controllers\Operator\OperatorController::class, 'checkIn'])->name('checkIn');
|
Route::post('/check-in', [\App\Http\Controllers\Operator\OperatorController::class, 'checkIn'])->name('checkIn');
|
||||||
Route::post('/{booking}/activate', [\App\Http\Controllers\Operator\OperatorController::class, 'activateReservation'])->name('activate');
|
Route::post('/{booking}/activate', [\App\Http\Controllers\Operator\OperatorController::class, 'activateReservation'])->name('activate');
|
||||||
Route::get('/{booking}/checkout-preview', [\App\Http\Controllers\Operator\OperatorController::class, 'checkoutPreview'])->name('checkoutPreview');
|
Route::get('/{booking}/checkout-preview', [\App\Http\Controllers\Operator\OperatorController::class, 'checkoutPreview'])->name('checkoutPreview');
|
||||||
|
|||||||
0
storage/app/.gitignore
vendored
Executable file → Normal file
0
storage/app/.gitignore
vendored
Executable file → Normal file
0
storage/app/private/.gitignore
vendored
Executable file → Normal file
0
storage/app/private/.gitignore
vendored
Executable file → Normal file
0
storage/app/public/.gitignore
vendored
Executable file → Normal file
0
storage/app/public/.gitignore
vendored
Executable file → Normal file
0
storage/framework/.gitignore
vendored
Executable file → Normal file
0
storage/framework/.gitignore
vendored
Executable file → Normal file
0
storage/framework/cache/.gitignore
vendored
Executable file → Normal file
0
storage/framework/cache/.gitignore
vendored
Executable file → Normal file
0
storage/framework/cache/data/.gitignore
vendored
Executable file → Normal file
0
storage/framework/cache/data/.gitignore
vendored
Executable file → Normal file
0
storage/framework/sessions/.gitignore
vendored
Executable file → Normal file
0
storage/framework/sessions/.gitignore
vendored
Executable file → Normal file
0
storage/framework/testing/.gitignore
vendored
Executable file → Normal file
0
storage/framework/testing/.gitignore
vendored
Executable file → Normal file
0
storage/framework/views/.gitignore
vendored
Executable file → Normal file
0
storage/framework/views/.gitignore
vendored
Executable file → Normal file
0
storage/logs/.gitignore
vendored
Executable file → Normal file
0
storage/logs/.gitignore
vendored
Executable file → Normal file
24
vendor/composer/autoload_classmap.php
vendored
24
vendor/composer/autoload_classmap.php
vendored
@ -6,28 +6,7 @@ $vendorDir = dirname(__DIR__);
|
|||||||
$baseDir = dirname($vendorDir);
|
$baseDir = dirname($vendorDir);
|
||||||
|
|
||||||
return array(
|
return array(
|
||||||
'App\\Http\\Controllers\\Admin\\AdminOperatorController' => $baseDir . '/app/Http/Controllers/Admin/AdminOperatorController.php',
|
|
||||||
'App\\Http\\Controllers\\Admin\\BookingController' => $baseDir . '/app/Http/Controllers/Admin/BookingController.php',
|
|
||||||
'App\\Http\\Controllers\\Admin\\DashboardController' => $baseDir . '/app/Http/Controllers/Admin/DashboardController.php',
|
|
||||||
'App\\Http\\Controllers\\Admin\\ParkingLotController' => $baseDir . '/app/Http/Controllers/Admin/ParkingLotController.php',
|
|
||||||
'App\\Http\\Controllers\\Api\\BookingController' => $baseDir . '/app/Http/Controllers/Api/BookingController.php',
|
|
||||||
'App\\Http\\Controllers\\Api\\CarRegistryController' => $baseDir . '/app/Http/Controllers/Api/CarRegistryController.php',
|
|
||||||
'App\\Http\\Controllers\\Api\\ParkingLotController' => $baseDir . '/app/Http/Controllers/Api/ParkingLotController.php',
|
|
||||||
'App\\Http\\Controllers\\Controller' => $baseDir . '/app/Http/Controllers/Controller.php',
|
'App\\Http\\Controllers\\Controller' => $baseDir . '/app/Http/Controllers/Controller.php',
|
||||||
'App\\Http\\Controllers\\Operator\\OperatorController' => $baseDir . '/app/Http/Controllers/Operator/OperatorController.php',
|
|
||||||
'App\\Http\\Controllers\\ParkingController' => $baseDir . '/app/Http/Controllers/ParkingController.php',
|
|
||||||
'App\\Http\\Controllers\\ProfileController' => $baseDir . '/app/Http/Controllers/ProfileController.php',
|
|
||||||
'App\\Http\\Middleware\\EnsureAdmin' => $baseDir . '/app/Http/Middleware/EnsureAdmin.php',
|
|
||||||
'App\\Http\\Middleware\\EnsureOperator' => $baseDir . '/app/Http/Middleware/EnsureOperator.php',
|
|
||||||
'App\\Http\\Requests\\StoreBookingRequest' => $baseDir . '/app/Http/Requests/StoreBookingRequest.php',
|
|
||||||
'App\\Http\\Requests\\StoreOperatorCheckInRequest' => $baseDir . '/app/Http/Requests/StoreOperatorCheckInRequest.php',
|
|
||||||
'App\\Http\\Requests\\StoreParkingLotRequest' => $baseDir . '/app/Http/Requests/StoreParkingLotRequest.php',
|
|
||||||
'App\\Http\\Resources\\BookingResource' => $baseDir . '/app/Http/Resources/BookingResource.php',
|
|
||||||
'App\\Http\\Resources\\CarRegistryResource' => $baseDir . '/app/Http/Resources/CarRegistryResource.php',
|
|
||||||
'App\\Http\\Resources\\ParkingLotResource' => $baseDir . '/app/Http/Resources/ParkingLotResource.php',
|
|
||||||
'App\\Models\\Booking' => $baseDir . '/app/Models/Booking.php',
|
|
||||||
'App\\Models\\CarRegistry' => $baseDir . '/app/Models/CarRegistry.php',
|
|
||||||
'App\\Models\\ParkingLot' => $baseDir . '/app/Models/ParkingLot.php',
|
|
||||||
'App\\Models\\User' => $baseDir . '/app/Models/User.php',
|
'App\\Models\\User' => $baseDir . '/app/Models/User.php',
|
||||||
'App\\Providers\\AppServiceProvider' => $baseDir . '/app/Providers/AppServiceProvider.php',
|
'App\\Providers\\AppServiceProvider' => $baseDir . '/app/Providers/AppServiceProvider.php',
|
||||||
'Attribute' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/Attribute.php',
|
'Attribute' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/Attribute.php',
|
||||||
@ -153,10 +132,7 @@ return array(
|
|||||||
'Cron\\MinutesField' => $vendorDir . '/dragonmantank/cron-expression/src/Cron/MinutesField.php',
|
'Cron\\MinutesField' => $vendorDir . '/dragonmantank/cron-expression/src/Cron/MinutesField.php',
|
||||||
'Cron\\MonthField' => $vendorDir . '/dragonmantank/cron-expression/src/Cron/MonthField.php',
|
'Cron\\MonthField' => $vendorDir . '/dragonmantank/cron-expression/src/Cron/MonthField.php',
|
||||||
'Database\\Factories\\UserFactory' => $baseDir . '/database/factories/UserFactory.php',
|
'Database\\Factories\\UserFactory' => $baseDir . '/database/factories/UserFactory.php',
|
||||||
'Database\\Seeders\\AdminUserSeeder' => $baseDir . '/database/seeders/AdminUserSeeder.php',
|
|
||||||
'Database\\Seeders\\BookingSeeder' => $baseDir . '/database/seeders/BookingSeeder.php',
|
|
||||||
'Database\\Seeders\\DatabaseSeeder' => $baseDir . '/database/seeders/DatabaseSeeder.php',
|
'Database\\Seeders\\DatabaseSeeder' => $baseDir . '/database/seeders/DatabaseSeeder.php',
|
||||||
'Database\\Seeders\\ParkingLotSeeder' => $baseDir . '/database/seeders/ParkingLotSeeder.php',
|
|
||||||
'DateError' => $vendorDir . '/symfony/polyfill-php83/Resources/stubs/DateError.php',
|
'DateError' => $vendorDir . '/symfony/polyfill-php83/Resources/stubs/DateError.php',
|
||||||
'DateException' => $vendorDir . '/symfony/polyfill-php83/Resources/stubs/DateException.php',
|
'DateException' => $vendorDir . '/symfony/polyfill-php83/Resources/stubs/DateException.php',
|
||||||
'DateInvalidOperationException' => $vendorDir . '/symfony/polyfill-php83/Resources/stubs/DateInvalidOperationException.php',
|
'DateInvalidOperationException' => $vendorDir . '/symfony/polyfill-php83/Resources/stubs/DateInvalidOperationException.php',
|
||||||
|
|||||||
24
vendor/composer/autoload_static.php
vendored
24
vendor/composer/autoload_static.php
vendored
@ -536,28 +536,7 @@ class ComposerStaticInit53b5d56b3b7e3cbac1713e68c8850f6c
|
|||||||
);
|
);
|
||||||
|
|
||||||
public static $classMap = array (
|
public static $classMap = array (
|
||||||
'App\\Http\\Controllers\\Admin\\AdminOperatorController' => __DIR__ . '/../..' . '/app/Http/Controllers/Admin/AdminOperatorController.php',
|
|
||||||
'App\\Http\\Controllers\\Admin\\BookingController' => __DIR__ . '/../..' . '/app/Http/Controllers/Admin/BookingController.php',
|
|
||||||
'App\\Http\\Controllers\\Admin\\DashboardController' => __DIR__ . '/../..' . '/app/Http/Controllers/Admin/DashboardController.php',
|
|
||||||
'App\\Http\\Controllers\\Admin\\ParkingLotController' => __DIR__ . '/../..' . '/app/Http/Controllers/Admin/ParkingLotController.php',
|
|
||||||
'App\\Http\\Controllers\\Api\\BookingController' => __DIR__ . '/../..' . '/app/Http/Controllers/Api/BookingController.php',
|
|
||||||
'App\\Http\\Controllers\\Api\\CarRegistryController' => __DIR__ . '/../..' . '/app/Http/Controllers/Api/CarRegistryController.php',
|
|
||||||
'App\\Http\\Controllers\\Api\\ParkingLotController' => __DIR__ . '/../..' . '/app/Http/Controllers/Api/ParkingLotController.php',
|
|
||||||
'App\\Http\\Controllers\\Controller' => __DIR__ . '/../..' . '/app/Http/Controllers/Controller.php',
|
'App\\Http\\Controllers\\Controller' => __DIR__ . '/../..' . '/app/Http/Controllers/Controller.php',
|
||||||
'App\\Http\\Controllers\\Operator\\OperatorController' => __DIR__ . '/../..' . '/app/Http/Controllers/Operator/OperatorController.php',
|
|
||||||
'App\\Http\\Controllers\\ParkingController' => __DIR__ . '/../..' . '/app/Http/Controllers/ParkingController.php',
|
|
||||||
'App\\Http\\Controllers\\ProfileController' => __DIR__ . '/../..' . '/app/Http/Controllers/ProfileController.php',
|
|
||||||
'App\\Http\\Middleware\\EnsureAdmin' => __DIR__ . '/../..' . '/app/Http/Middleware/EnsureAdmin.php',
|
|
||||||
'App\\Http\\Middleware\\EnsureOperator' => __DIR__ . '/../..' . '/app/Http/Middleware/EnsureOperator.php',
|
|
||||||
'App\\Http\\Requests\\StoreBookingRequest' => __DIR__ . '/../..' . '/app/Http/Requests/StoreBookingRequest.php',
|
|
||||||
'App\\Http\\Requests\\StoreOperatorCheckInRequest' => __DIR__ . '/../..' . '/app/Http/Requests/StoreOperatorCheckInRequest.php',
|
|
||||||
'App\\Http\\Requests\\StoreParkingLotRequest' => __DIR__ . '/../..' . '/app/Http/Requests/StoreParkingLotRequest.php',
|
|
||||||
'App\\Http\\Resources\\BookingResource' => __DIR__ . '/../..' . '/app/Http/Resources/BookingResource.php',
|
|
||||||
'App\\Http\\Resources\\CarRegistryResource' => __DIR__ . '/../..' . '/app/Http/Resources/CarRegistryResource.php',
|
|
||||||
'App\\Http\\Resources\\ParkingLotResource' => __DIR__ . '/../..' . '/app/Http/Resources/ParkingLotResource.php',
|
|
||||||
'App\\Models\\Booking' => __DIR__ . '/../..' . '/app/Models/Booking.php',
|
|
||||||
'App\\Models\\CarRegistry' => __DIR__ . '/../..' . '/app/Models/CarRegistry.php',
|
|
||||||
'App\\Models\\ParkingLot' => __DIR__ . '/../..' . '/app/Models/ParkingLot.php',
|
|
||||||
'App\\Models\\User' => __DIR__ . '/../..' . '/app/Models/User.php',
|
'App\\Models\\User' => __DIR__ . '/../..' . '/app/Models/User.php',
|
||||||
'App\\Providers\\AppServiceProvider' => __DIR__ . '/../..' . '/app/Providers/AppServiceProvider.php',
|
'App\\Providers\\AppServiceProvider' => __DIR__ . '/../..' . '/app/Providers/AppServiceProvider.php',
|
||||||
'Attribute' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/Attribute.php',
|
'Attribute' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/Attribute.php',
|
||||||
@ -683,10 +662,7 @@ class ComposerStaticInit53b5d56b3b7e3cbac1713e68c8850f6c
|
|||||||
'Cron\\MinutesField' => __DIR__ . '/..' . '/dragonmantank/cron-expression/src/Cron/MinutesField.php',
|
'Cron\\MinutesField' => __DIR__ . '/..' . '/dragonmantank/cron-expression/src/Cron/MinutesField.php',
|
||||||
'Cron\\MonthField' => __DIR__ . '/..' . '/dragonmantank/cron-expression/src/Cron/MonthField.php',
|
'Cron\\MonthField' => __DIR__ . '/..' . '/dragonmantank/cron-expression/src/Cron/MonthField.php',
|
||||||
'Database\\Factories\\UserFactory' => __DIR__ . '/../..' . '/database/factories/UserFactory.php',
|
'Database\\Factories\\UserFactory' => __DIR__ . '/../..' . '/database/factories/UserFactory.php',
|
||||||
'Database\\Seeders\\AdminUserSeeder' => __DIR__ . '/../..' . '/database/seeders/AdminUserSeeder.php',
|
|
||||||
'Database\\Seeders\\BookingSeeder' => __DIR__ . '/../..' . '/database/seeders/BookingSeeder.php',
|
|
||||||
'Database\\Seeders\\DatabaseSeeder' => __DIR__ . '/../..' . '/database/seeders/DatabaseSeeder.php',
|
'Database\\Seeders\\DatabaseSeeder' => __DIR__ . '/../..' . '/database/seeders/DatabaseSeeder.php',
|
||||||
'Database\\Seeders\\ParkingLotSeeder' => __DIR__ . '/../..' . '/database/seeders/ParkingLotSeeder.php',
|
|
||||||
'DateError' => __DIR__ . '/..' . '/symfony/polyfill-php83/Resources/stubs/DateError.php',
|
'DateError' => __DIR__ . '/..' . '/symfony/polyfill-php83/Resources/stubs/DateError.php',
|
||||||
'DateException' => __DIR__ . '/..' . '/symfony/polyfill-php83/Resources/stubs/DateException.php',
|
'DateException' => __DIR__ . '/..' . '/symfony/polyfill-php83/Resources/stubs/DateException.php',
|
||||||
'DateInvalidOperationException' => __DIR__ . '/..' . '/symfony/polyfill-php83/Resources/stubs/DateInvalidOperationException.php',
|
'DateInvalidOperationException' => __DIR__ . '/..' . '/symfony/polyfill-php83/Resources/stubs/DateInvalidOperationException.php',
|
||||||
|
|||||||
12
vendor/composer/installed.php
vendored
12
vendor/composer/installed.php
vendored
@ -1,9 +1,9 @@
|
|||||||
<?php return array(
|
<?php return array(
|
||||||
'root' => array(
|
'root' => array(
|
||||||
'name' => 'laravel/laravel',
|
'name' => 'laravel/laravel',
|
||||||
'pretty_version' => 'dev-main',
|
'pretty_version' => '1.0.0+no-version-set',
|
||||||
'version' => 'dev-main',
|
'version' => '1.0.0.0',
|
||||||
'reference' => 'b03c1d4619de986420534fe46341f4da4d088f76',
|
'reference' => null,
|
||||||
'type' => 'project',
|
'type' => 'project',
|
||||||
'install_path' => __DIR__ . '/../../',
|
'install_path' => __DIR__ . '/../../',
|
||||||
'aliases' => array(),
|
'aliases' => array(),
|
||||||
@ -398,9 +398,9 @@
|
|||||||
'dev_requirement' => false,
|
'dev_requirement' => false,
|
||||||
),
|
),
|
||||||
'laravel/laravel' => array(
|
'laravel/laravel' => array(
|
||||||
'pretty_version' => 'dev-main',
|
'pretty_version' => '1.0.0+no-version-set',
|
||||||
'version' => 'dev-main',
|
'version' => '1.0.0.0',
|
||||||
'reference' => 'b03c1d4619de986420534fe46341f4da4d088f76',
|
'reference' => null,
|
||||||
'type' => 'project',
|
'type' => 'project',
|
||||||
'install_path' => __DIR__ . '/../../',
|
'install_path' => __DIR__ . '/../../',
|
||||||
'aliases' => array(),
|
'aliases' => array(),
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user