# 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.
```html
{{-- CORRECT: button last in RTL flex row → appears on the far left --}}
```
**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`:
```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"` and `rel="noopener"` from all `` 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(...)`, or `prompt(...)` 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.**
1. Create a branch named after the feature or fix being worked on
2. Stage and commit every file that will be touched — this is the rollback point
3. Do **not** push to remote
4. Then make the changes on that same branch
```bash
git checkout -b feature/my-feature-name
git add
git commit -m "backup: before "
# now make the changes
```
To revert a file: `git checkout ^ -- path/to/file`
To revert everything: `git checkout ^`
---
## 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
```bash
# 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 | |
| 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 |
|--------|------|-------|
| 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:now
- `end_time`: required, after:start_time
- Custom: parking lot must have available capacity
**Parking Lot:**
- `total_capacity`: integer, 1–10000
- `price_per_hour`: numeric, 0.01–1000
- `latitude`: -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: rtl` on ``
- Input groups reverse border-radius via CSS overrides in `[dir="rtl"]` block in `app.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
```php
$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
```bash
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.
```bash
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/dashboard`
- `operator` → `/operator/dashboard`
- `user` → `/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 `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.
- Some API routes have commented-out `auth:sanctum` middleware; add it back before deploying to production.