15 KiB
CLAUDE.md — Damascus Parking (SCP-Syria)
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.
{{-- 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.
The fix is already applied globally in the [dir="rtl"] block at the bottom of app.scss:
[dir="rtl"] .modal-header .btn-close {
margin-left: 0;
margin-right: auto;
}
Rule: never add inline styles or per-modal hacks to reposition the close button — the global fix handles it. Do not remove this override.
Never Open Links in a New Tab
Hard rule. Links must never use target="_blank" unless the user explicitly requests it.
- Remove
target="_blank"andrel="noopener"from all<a>tags unless told otherwise. - This applies to every view, every layout, every page — no exceptions.
Never Use JavaScript alert() / confirm() / prompt()
Hard rule. These native browser dialogs are ugly and break the UI.
- Never use
alert(...),confirm(...), orprompt(...)anywhere in JavaScript. - For confirmations: build a Bootstrap modal with Cancel / Confirm buttons.
- For success/error feedback: use a toast notification or an inline alert element.
- This applies to every view, every page, every script — no exceptions.
Mandatory: Branch Before Every Change
This is a hard rule. Never modify any file without first creating a git branch and committing the current state.
- Create a branch named after the feature or fix being worked on
- Stage and commit every file that will be touched — this is the rollback point
- Do not push to remote
- Then make the changes on that same branch
git checkout -b feature/my-feature-name
git add <files being changed>
git commit -m "backup: before <description of change>"
# now make the changes
To revert a file: git checkout <branch>^ -- path/to/file
To revert everything: git checkout <branch>^
Project Overview
Damascus Parking (دمشق باركينغ) is a full-stack web application for parking lot management in Damascus, Syria. It allows the public to search and book parking spaces, operators to manage vehicle check-ins/check-outs, and admins to oversee all operations.
- Framework: Laravel 12.0 (PHP ^8.2)
- Database: SQLite (
database/database.sqlite) - Auth: Laravel Sanctum 4.0 (session-based for web, token-based for API)
- Frontend: Vite 7 + Bootstrap 5.3 + Sass + Leaflet maps
- Language/Layout: Arabic (RTL throughout), font: Cairo (Google Fonts)
- Timezone: Asia/Damascus
- Local URL: http://localhost:8000
Development Setup
# Full install + migrate + build
composer setup
# Start all dev services (server, queue, logs, Vite HMR)
composer dev
# OR individually:
php artisan serve
npm run dev
# Run tests
composer test
# or: php artisan config:clear && php artisan test
Vite entry points: resources/css/app.scss, resources/js/app.js
Architecture
Directory Structure
app/
Http/
Controllers/
Admin/ # Admin-only: Dashboard, ParkingLot, Booking
Api/ # REST API: ParkingLot, Booking, CarRegistry
Operator/ # Operator-only: OperatorController
ParkingController.php # Public landing page
Middleware/
EnsureAdmin.php
EnsureOperator.php
Requests/ # Form validation: Booking, CheckIn, ParkingLot
Resources/ # API response transformers
Models/
User.php # Roles: admin | operator | user
ParkingLot.php # Scopes: active(), withStatus(); computed attributes
Booking.php # Status enum: active | completed | cancelled
CarRegistry.php # Scope: active() (exit_time IS NULL)
routes/
web.php # Pages + auth
api.php # /api/v1/* REST endpoints
auth.php # Login/register/logout
resources/
views/
admin/ # Admin Blade templates
operator/ # Operator Blade templates
auth/ # Login & register pages
layouts/ # Base layouts
index.blade.php # Public landing page
css/app.scss # ~780 lines of custom RTL-aware styles
js/app.js # Bootstrap + Axios setup
Roles & Access
| Role | Middleware | Access |
|---|---|---|
admin |
EnsureAdmin |
Full admin dashboard, parking lot CRUD, all bookings, operator management |
operator |
EnsureOperator |
Vehicle check-in/check-out, stats dashboard — restricted to assigned lots only |
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
users
| Column | Type | Notes |
|---|---|---|
| id | bigint PK | |
| name | string | |
| 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 |
|---|---|---|
| id | bigint PK | |
| name | string | |
| address | string | |
| total_capacity | integer | |
| price_per_hour | decimal | |
| latitude / longitude | decimal | Used by Leaflet map |
| working_hours | string | Default '24/7' |
| is_active | boolean | Toggleable by admin |
Computed attributes (not stored): available_spaces, occupied_spaces, usage_percentage
bookings
| Column | Type | Notes |
|---|---|---|
| parking_lot_id | FK | |
| customer_name | string | |
| phone | string | Format: 09xxxxxxxx |
| start_time / end_time | datetime | |
| status | enum | 'active' | 'completed' | 'cancelled' |
car_registries
| Column | Type | Notes |
|---|---|---|
| parking_lot_id | FK | |
| plate_number | string | |
| entry_time | datetime | |
| exit_time | datetime nullable | NULL = currently parked |
API Reference (/api/v1)
All responses follow: { "success": bool, "data": ..., "message": "..." }
Parking Lots
GET /api/v1/parking-lots # Paginated list with occupancy status
GET /api/v1/parking-lots/{id} # Single lot details
GET /api/v1/parking-lots/search?q= # Search by name/address
GET /api/v1/parking-lots/{id}/status # Real-time occupancy
Bookings
POST /api/v1/bookings # Create booking
GET /api/v1/bookings # Recent bookings (paginated)
Required fields: parking_lot_id, customer_name, phone (09xxxxxxxx), start_time, end_time
Car Registry (Check-in/Check-out)
POST /api/v1/car-registries # Register car entry
PUT /api/v1/car-registries/{id}/exit # Register car exit
Check-in requires: parking_lot_id, vehicle_plate, duration_hours (0.25–24)
Web Routes Summary
| Path | Middleware | Description |
|---|---|---|
GET / |
— | Public landing page |
GET /login |
guest | Login form |
GET /register |
guest | Register form |
POST /logout |
auth | Logout |
GET /admin/dashboard |
admin | Admin stats & charts |
GET /admin/parking-lots |
admin | List/manage parking lots |
POST /admin/parking-lots |
admin | Create parking lot |
PUT /admin/parking-lots/{id} |
admin | Update parking lot |
POST /admin/parking-lots/{id}/toggle |
admin | Toggle active status |
GET /admin/bookings/active |
admin | Active bookings |
POST /admin/bookings/{id}/complete |
admin | Mark booking complete |
GET /operator/dashboard |
operator | Operator lot-picker + operations panel |
GET /operator/stats |
operator | Operator statistics & charts dashboard |
GET /operator/stats-data |
operator | JSON stats data for operator dashboard (AJAX) |
POST /operator/check-in |
operator | Vehicle entry |
POST /operator/{booking}/checkout |
operator | Vehicle exit + fee |
GET /admin/stats |
admin | JSON stats (AJAX) |
GET /admin/charts |
admin | JSON chart data (AJAX) |
Validation Rules
Phone: must match ^09[0-9]{8}$ (Syrian mobile format)
Booking:
start_time: required, after:nowend_time: required, after:start_time- Custom: parking lot must have available capacity
Parking Lot:
total_capacity: integer, 1–10000price_per_hour: numeric, 0.01–1000latitude: -90 to 90,longitude: -180 to 180
Operator Check-in:
duration_hours: numeric, 0.25–24
Frontend & Styling
Color palette (CSS variables in app.scss):
- Primary:
#6366f1(indigo) - Sidebar bg:
#0f172a - Muted text:
#64748b - Page bg:
#f1f5f9
RTL-specific patterns:
direction: rtlon<html>- Input groups reverse border-radius via CSS overrides in
[dir="rtl"]block inapp.scss - Flexbox rows appear mirrored naturally; no special JS needed
- Margin/padding use
inset-inline-*where needed
Bootstrap RTL spacing — critical rule:
Bootstrap is imported as SCSS (@import "bootstrap/scss/bootstrap"), which compiles me-* to physical margin-right and ms-* to margin-left. In RTL, icons sit on the right and text on the left, so margin-right on an icon pushes away from the text — the gap appears on the wrong side.
The fix is already applied in the [dir="rtl"] block at the bottom of app.scss: it swaps all me-*/ms-* physical margins so they behave like Bootstrap's logical properties.
Rule: never add me-* to fix icon-to-text spacing — it already works correctly via the global [dir="rtl"] override. Do not remove or change those overrides. If a new icon appears stuck to its label, the cause is always the same: Bootstrap's LTR SCSS compilation. The fix is already in place globally — just use the standard Bootstrap spacing classes (me-1, me-2, etc.) and they will work correctly in RTL.
Responsive breakpoints:
<768px: sidebar hidden, bottom nav visible (56px)768px–992px: sidebar can toggle>992px: full sidebar + content layout
Libraries:
- Leaflet 1.9.4 — interactive parking lot map
- Bootstrap Icons 1.11.3 — icon set
- Axios 1.11.0 — AJAX calls from admin/operator JS
Fee Calculation Logic
$duration = ceil(now()->diffInHours($entry_time)); // rounds up
$fee = $duration * $parking_lot->price_per_hour;
Fees are always rounded up to the nearest hour.
Availability Calculation
Available spaces = total_capacity − (active bookings count + active car registries count)
CarRegistry::active() scope: whereNull('exit_time') (or exit_time > now())
Booking::active() scope: where('status', 'active')
Overbooking is prevented at API validation time in StoreBookingRequest.
Key Artisan Commands
php artisan migrate # Run migrations
php artisan migrate:fresh --seed # Reset DB and seed
php artisan tinker # REPL for debugging
php artisan route:list # List all routes
php artisan config:clear # Clear config cache
php artisan make:model Foo -mcr # Model + migration + controller + resource
Testing
Tests live in tests/Feature/ and tests/Unit/. PHPUnit 11 is configured in phpunit.xml. The test database uses the SQLite in-memory driver by default.
composer test
# or
php artisan config:clear && php artisan test
User-Facing Pages (authenticated)
| Route | Name | Description |
|---|---|---|
GET /profile |
profile.show |
User info + edit name + change password |
PATCH /profile |
profile.update |
Update name (error bag: updateName) |
PATCH /profile/password |
profile.password |
Change password (error bag: updatePassword) |
GET /dashboard |
user.dashboard |
User's booking history + stats |
Profile dropdown is a Blade partial at resources/views/partials/user-dropdown.blade.php. It is included in both layouts/user.blade.php and index.blade.php. When the user is a guest it shows the login button; when authenticated it shows an avatar circle with a dropdown containing profile, reservations, operator/admin links (role-gated), and sign-out.
Post-login redirect (in routes/auth.php) is role-based:
admin→/admin/dashboardoperator→/operator/dashboarduser→/dashboard
bookings.user_id — nullable FK added via migration 2026_04_15_072226. Existing seeded bookings have user_id = NULL. New bookings made by a logged-in user should set user_id = Auth::id().
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
CarRegistrymodel tracks physical vehicle presence;Bookingtracks reservations — they are separate but both count toward capacity. - Multi-lot operator assignment:
User::assignedLots()is a BelongsToMany throughoperator_parking_lot. Use$user->assignedLots->pluck('id')to get the operator's allowed lot IDs. An operator with an emptyassignedLotscollection sees no lots (unlike the old single-FK system where null = all lots). StoreParkingLotRequestandStoreBookingRequestare inapp/Http/Requests/.- Arabic error messages are returned by both middleware and validation responses.
- The
TODO.mdat the root is nearly empty — "Update with checked" is the only entry. - Some API routes have commented-out
auth:sanctummiddleware; add it back before deploying to production.