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