diff --git a/CLAUDE.md b/CLAUDE.md index bbaeddb..3f9467f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2,6 +2,42 @@ ## Working Rules +### 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.** diff --git a/app/Http/Controllers/Admin/DashboardController.php b/app/Http/Controllers/Admin/DashboardController.php index ed43b94..8e94f37 100644 --- a/app/Http/Controllers/Admin/DashboardController.php +++ b/app/Http/Controllers/Admin/DashboardController.php @@ -85,7 +85,8 @@ class DashboardController extends Controller 'success' => true, 'data' => [ 'daily_bookings' => array_values($dates), - 'top_parking_lots' => $topLots->map(fn($lot) => ['name' => $lot->name, 'value' => $lot->bookings_count])->toArray(), + 'top_parking_lots' => $topLots->map(fn($lot) => ['id' => $lot->id, 'name' => $lot->name, 'value' => $lot->bookings_count])->toArray(), + 'daily_dates' => array_keys($dates), 'occupancy_trend' => $trend, ] ]); diff --git a/app/Http/Controllers/Api/BookingController.php b/app/Http/Controllers/Api/BookingController.php index 8651659..9bb0192 100644 --- a/app/Http/Controllers/Api/BookingController.php +++ b/app/Http/Controllers/Api/BookingController.php @@ -20,6 +20,7 @@ class BookingController extends Controller $lot = \App\Models\ParkingLot::find($validated['parking_lot_id']); $validated['pricing_snapshot'] = $lot?->pricingSnapshot(); + $validated['user_id'] = $request->user()->id; $booking = \App\Models\Booking::create($validated); @@ -34,9 +35,17 @@ class BookingController extends Controller public function index(Request $request) { - $bookings = Booking::with('parkingLot') - ->latest() - ->paginate(10); + $query = Booking::with('parkingLot')->latest(); + + if ($request->filled('date')) { + $query->whereDate('created_at', $request->date); + } + + if ($request->filled('parking_lot_id')) { + $query->where('parking_lot_id', $request->parking_lot_id); + } + + $bookings = $query->paginate($request->integer('per_page', 10)); return response()->json([ 'success' => true, diff --git a/app/Http/Controllers/Operator/OperatorController.php b/app/Http/Controllers/Operator/OperatorController.php index 2b90403..d7c64d0 100644 --- a/app/Http/Controllers/Operator/OperatorController.php +++ b/app/Http/Controllers/Operator/OperatorController.php @@ -86,7 +86,7 @@ class OperatorController extends Controller 'phone' => $data['phone'] ?? null, 'source' => 'walk_in', 'start_time' => now(), - 'end_time' => now()->addHours($data['duration_hours']), + 'end_time' => now()->addHours((float) $data['duration_hours']), 'status' => 'active', 'pricing_snapshot' => $lot->pricingSnapshot(), ]); diff --git a/app/Http/Controllers/ProfileController.php b/app/Http/Controllers/ProfileController.php index 5e53ebd..86ba32d 100644 --- a/app/Http/Controllers/ProfileController.php +++ b/app/Http/Controllers/ProfileController.php @@ -6,6 +6,7 @@ use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Hash; use App\Models\Booking; +use App\Models\ParkingLot; class ProfileController extends Controller { @@ -16,13 +17,20 @@ class ProfileController extends Controller public function update(Request $request) { - $validated = $request->validateWithBag('updateName', [ - 'name' => 'required|string|max:255', + $request->validateWithBag('updateName', [ + 'name' => 'required|string|max:255', + 'phone_country' => 'required|string|max:10', + 'phone_local' => 'required|string|max:20', + ], [ + 'phone_local.required' => 'رقم الهاتف مطلوب.', ]); - Auth::user()->update(['name' => $validated['name']]); + Auth::user()->update([ + 'name' => $request->name, + 'phone' => $request->phone_country . $request->phone_local, + ]); - return back()->with('success', 'تم تحديث الاسم بنجاح.'); + return back()->with('success', 'تم تحديث البيانات بنجاح.'); } public function updatePassword(Request $request) @@ -44,6 +52,48 @@ class ProfileController extends Controller return back()->with('success', 'تم تغيير كلمة السر بنجاح.'); } + public function reserve(Request $request) + { + $user = Auth::user(); + + $request->validate([ + 'parking_lot_id' => 'required|exists:parking_lots,id', + 'vehicle_plate' => 'required|string|max:20', + 'start_time' => 'required|date|after:now', + 'end_time' => 'required|date|after:start_time', + ]); + + $lot = ParkingLot::findOrFail($request->parking_lot_id); + + // Check capacity + $active = $lot->carRegistries()->active()->count() + + $lot->bookings()->where('status', 'active')->count(); + if ($active >= $lot->total_capacity) { + return response()->json([ + 'success' => false, + 'message' => 'الموقف ممتلئ حالياً.', + ], 409); + } + + $booking = Booking::create([ + 'user_id' => $user->id, + 'parking_lot_id' => $lot->id, + 'customer_name' => $user->name, + 'phone' => $user->phone ?? '', + 'vehicle_plate' => $request->vehicle_plate, + 'start_time' => $request->start_time, + 'end_time' => $request->end_time, + 'status' => 'active', + 'pricing_snapshot' => $lot->pricingSnapshot(), + ]); + + return response()->json([ + 'success' => true, + 'message' => 'تم الحجز بنجاح.', + 'data' => ['id' => $booking->id], + ], 201); + } + public function dashboard() { $bookings = Booking::with('parkingLot') diff --git a/app/Http/Requests/StoreBookingRequest.php b/app/Http/Requests/StoreBookingRequest.php index 5e48b51..fdc4e04 100644 --- a/app/Http/Requests/StoreBookingRequest.php +++ b/app/Http/Requests/StoreBookingRequest.php @@ -25,23 +25,17 @@ class StoreBookingRequest extends FormRequest { return [ 'parking_lot_id' => 'required|exists:parking_lots,id', - 'customer_name' => 'required|string|max:255', - 'phone' => 'required|string|max:20|regex:/^09[0-9]{8}$/', - 'start_time' => 'required|date|after:now', - 'end_time' => 'required|date|after:start_time', + 'customer_name' => 'required|string|max:255', + 'phone' => 'required|string|max:30', + 'vehicle_plate' => 'nullable|string|max:20', + 'start_time' => 'required|date|after:now', + 'end_time' => 'required|date|after:start_time', ]; } /** * Get custom messages for validator errors. */ - public function messages(): array - { - return [ - 'phone.regex' => 'رقم الهاتف يجب أن يكون رقم سوري صالح (09xxxxxxxx).', - ]; - } - /** * Configure the validator after the validation rules have run. */ diff --git a/app/Models/User.php b/app/Models/User.php index a429070..977decd 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -21,6 +21,7 @@ class User extends Authenticatable protected $fillable = [ 'name', 'email', + 'phone', 'password', 'role', ]; diff --git a/bootstrap/app.php b/bootstrap/app.php index c7dbd95..cb8a6d8 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -13,6 +13,7 @@ return Application::configure(basePath: dirname(__DIR__)) ) ->withMiddleware(function (Middleware $middleware): void { $middleware->api(prepend: [ + \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class, \Illuminate\Http\Middleware\TrustProxies::class, ]); diff --git a/database/migrations/2026_04_16_000001_add_phone_to_users_table.php b/database/migrations/2026_04_16_000001_add_phone_to_users_table.php new file mode 100644 index 0000000..821629e --- /dev/null +++ b/database/migrations/2026_04_16_000001_add_phone_to_users_table.php @@ -0,0 +1,22 @@ +string('phone', 20)->nullable()->after('email'); + }); + } + + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + $table->dropColumn('phone'); + }); + } +}; diff --git a/resources/css/app.scss b/resources/css/app.scss index 07057ed..6003f7c 100644 --- a/resources/css/app.scss +++ b/resources/css/app.scss @@ -796,4 +796,12 @@ body { border-top-right-radius: 0 !important; border-bottom-right-radius: 0 !important; } + + // Bootstrap compiles .modal-header .btn-close with physical margin-left:auto, + // which in RTL pushes the button to the right (next to the title). + // Swap it so the × always appears at the far LEFT end of the header. + .modal-header .btn-close { + margin-left: 0; + margin-right: auto; + } } diff --git a/resources/js/bootstrap.js b/resources/js/bootstrap.js index acd7ae8..20cf5d0 100644 --- a/resources/js/bootstrap.js +++ b/resources/js/bootstrap.js @@ -5,3 +5,8 @@ import axios from 'axios'; window.axios = axios; window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'; + +const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content'); +if (csrfToken) { + window.axios.defaults.headers.common['X-CSRF-TOKEN'] = csrfToken; +} diff --git a/resources/views/admin/bookings/active.blade.php b/resources/views/admin/bookings/active.blade.php index 2ffbc4b..d82a2d6 100644 --- a/resources/views/admin/bookings/active.blade.php +++ b/resources/views/admin/bookings/active.blade.php @@ -307,7 +307,7 @@ @@ -338,59 +338,148 @@ +{{-- ── Confirm complete modal ───────────────────────────────────────────────── --}} + + +{{-- ── Toast ────────────────────────────────────────────────────────────────── --}} +
+ @push('scripts') @endpush diff --git a/resources/views/auth/register.blade.php b/resources/views/auth/register.blade.php index ef82f5c..693fe99 100644 --- a/resources/views/auth/register.blade.php +++ b/resources/views/auth/register.blade.php @@ -45,6 +45,42 @@ +
+ + @php + $countries = [ + ['🇸🇾','سوريا','+963'],['🇸🇦','السعودية','+966'],['🇦🇪','الإمارات','+971'], + ['🇯🇴','الأردن','+962'],['🇱🇧','لبنان','+961'],['🇮🇶','العراق','+964'], + ['🇪🇬','مصر','+20'],['🇴🇲','عُمان','+968'],['🇶🇦','قطر','+974'], + ['🇰🇼','الكويت','+965'],['🇧🇭','البحرين','+973'],['🇾🇪','اليمن','+967'], + ['🇵🇸','فلسطين','+970'],['🇲🇦','المغرب','+212'],['🇩🇿','الجزائر','+213'], + ['🇹🇳','تونس','+216'],['🇱🇾','ليبيا','+218'],['🇸🇩','السودان','+249'], + ['🇹🇷','تركيا','+90'],['🇮🇷','إيران','+98'],['🇩🇪','ألمانيا','+49'], + ['🇫🇷','فرنسا','+33'],['🇬🇧','بريطانيا','+44'],['🇺🇸','أمريكا','+1'], + ['🇷🇺','روسيا','+7'],['🇨🇳','الصين','+86'],['🇮🇳','الهند','+91'], + ['🇦🇺','أستراليا','+61'],['🇮🇹','إيطاليا','+39'],['🇪🇸','إسبانيا','+34'], + ['🇧🇷','البرازيل','+55'],['🇵🇰','باكستان','+92'],['🇳🇬','نيجيريا','+234'], + ]; + $oldCountry = old('phone_country', '+963'); + @endphp +
+ + +
+ @error('phone')
{{ $message }}
@enderror +
+
diff --git a/resources/views/index.blade.php b/resources/views/index.blade.php index b1a7e67..0ecc78f 100644 --- a/resources/views/index.blade.php +++ b/resources/views/index.blade.php @@ -218,6 +218,100 @@
+ {{-- ══ BOOKING FORM MODAL ══════════════════════════════════════════════════ --}} + + {{-- ══ MOBILE BOTTOM NAV ══════════════════════════════════════════════════ --}}