Compare commits

...

1 Commits

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

View File

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

48
.env
View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

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

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

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

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

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

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

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

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

View File

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

View File

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

View File

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