backup: before cancel reservation feature on operator dashboard
This commit is contained in:
parent
51bdf50292
commit
c481135280
36
CLAUDE.md
36
CLAUDE.md
@ -2,6 +2,42 @@
|
|||||||
|
|
||||||
## Working Rules
|
## 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 `<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(...)`, 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
|
### 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.**
|
**This is a hard rule. Never modify any file without first creating a git branch and committing the current state.**
|
||||||
|
|||||||
@ -85,7 +85,8 @@ class DashboardController extends Controller
|
|||||||
'success' => true,
|
'success' => true,
|
||||||
'data' => [
|
'data' => [
|
||||||
'daily_bookings' => array_values($dates),
|
'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,
|
'occupancy_trend' => $trend,
|
||||||
]
|
]
|
||||||
]);
|
]);
|
||||||
|
|||||||
@ -20,6 +20,7 @@ class BookingController extends Controller
|
|||||||
|
|
||||||
$lot = \App\Models\ParkingLot::find($validated['parking_lot_id']);
|
$lot = \App\Models\ParkingLot::find($validated['parking_lot_id']);
|
||||||
$validated['pricing_snapshot'] = $lot?->pricingSnapshot();
|
$validated['pricing_snapshot'] = $lot?->pricingSnapshot();
|
||||||
|
$validated['user_id'] = $request->user()->id;
|
||||||
|
|
||||||
$booking = \App\Models\Booking::create($validated);
|
$booking = \App\Models\Booking::create($validated);
|
||||||
|
|
||||||
@ -34,9 +35,17 @@ class BookingController extends Controller
|
|||||||
|
|
||||||
public function index(Request $request)
|
public function index(Request $request)
|
||||||
{
|
{
|
||||||
$bookings = Booking::with('parkingLot')
|
$query = Booking::with('parkingLot')->latest();
|
||||||
->latest()
|
|
||||||
->paginate(10);
|
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([
|
return response()->json([
|
||||||
'success' => true,
|
'success' => true,
|
||||||
|
|||||||
@ -86,7 +86,7 @@ class OperatorController extends Controller
|
|||||||
'phone' => $data['phone'] ?? null,
|
'phone' => $data['phone'] ?? null,
|
||||||
'source' => 'walk_in',
|
'source' => 'walk_in',
|
||||||
'start_time' => now(),
|
'start_time' => now(),
|
||||||
'end_time' => now()->addHours($data['duration_hours']),
|
'end_time' => now()->addHours((float) $data['duration_hours']),
|
||||||
'status' => 'active',
|
'status' => 'active',
|
||||||
'pricing_snapshot' => $lot->pricingSnapshot(),
|
'pricing_snapshot' => $lot->pricingSnapshot(),
|
||||||
]);
|
]);
|
||||||
|
|||||||
@ -6,6 +6,7 @@ use Illuminate\Http\Request;
|
|||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
use Illuminate\Support\Facades\Hash;
|
use Illuminate\Support\Facades\Hash;
|
||||||
use App\Models\Booking;
|
use App\Models\Booking;
|
||||||
|
use App\Models\ParkingLot;
|
||||||
|
|
||||||
class ProfileController extends Controller
|
class ProfileController extends Controller
|
||||||
{
|
{
|
||||||
@ -16,13 +17,20 @@ class ProfileController extends Controller
|
|||||||
|
|
||||||
public function update(Request $request)
|
public function update(Request $request)
|
||||||
{
|
{
|
||||||
$validated = $request->validateWithBag('updateName', [
|
$request->validateWithBag('updateName', [
|
||||||
'name' => 'required|string|max:255',
|
'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)
|
public function updatePassword(Request $request)
|
||||||
@ -44,6 +52,48 @@ class ProfileController extends Controller
|
|||||||
return back()->with('success', 'تم تغيير كلمة السر بنجاح.');
|
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()
|
public function dashboard()
|
||||||
{
|
{
|
||||||
$bookings = Booking::with('parkingLot')
|
$bookings = Booking::with('parkingLot')
|
||||||
|
|||||||
@ -26,7 +26,8 @@ class StoreBookingRequest extends FormRequest
|
|||||||
return [
|
return [
|
||||||
'parking_lot_id' => 'required|exists:parking_lots,id',
|
'parking_lot_id' => 'required|exists:parking_lots,id',
|
||||||
'customer_name' => 'required|string|max:255',
|
'customer_name' => 'required|string|max:255',
|
||||||
'phone' => 'required|string|max:20|regex:/^09[0-9]{8}$/',
|
'phone' => 'required|string|max:30',
|
||||||
|
'vehicle_plate' => 'nullable|string|max:20',
|
||||||
'start_time' => 'required|date|after:now',
|
'start_time' => 'required|date|after:now',
|
||||||
'end_time' => 'required|date|after:start_time',
|
'end_time' => 'required|date|after:start_time',
|
||||||
];
|
];
|
||||||
@ -35,13 +36,6 @@ class StoreBookingRequest extends FormRequest
|
|||||||
/**
|
/**
|
||||||
* Get custom messages for validator errors.
|
* Get custom messages for validator errors.
|
||||||
*/
|
*/
|
||||||
public function messages(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'phone.regex' => 'رقم الهاتف يجب أن يكون رقم سوري صالح (09xxxxxxxx).',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Configure the validator after the validation rules have run.
|
* Configure the validator after the validation rules have run.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -21,6 +21,7 @@ class User extends Authenticatable
|
|||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'name',
|
'name',
|
||||||
'email',
|
'email',
|
||||||
|
'phone',
|
||||||
'password',
|
'password',
|
||||||
'role',
|
'role',
|
||||||
];
|
];
|
||||||
|
|||||||
@ -13,6 +13,7 @@ return Application::configure(basePath: dirname(__DIR__))
|
|||||||
)
|
)
|
||||||
->withMiddleware(function (Middleware $middleware): void {
|
->withMiddleware(function (Middleware $middleware): void {
|
||||||
$middleware->api(prepend: [
|
$middleware->api(prepend: [
|
||||||
|
\Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
|
||||||
\Illuminate\Http\Middleware\TrustProxies::class,
|
\Illuminate\Http\Middleware\TrustProxies::class,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('users', function (Blueprint $table) {
|
||||||
|
$table->string('phone', 20)->nullable()->after('email');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('users', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('phone');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -796,4 +796,12 @@ body {
|
|||||||
border-top-right-radius: 0 !important;
|
border-top-right-radius: 0 !important;
|
||||||
border-bottom-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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
5
resources/js/bootstrap.js
vendored
5
resources/js/bootstrap.js
vendored
@ -5,3 +5,8 @@ import axios from 'axios';
|
|||||||
window.axios = axios;
|
window.axios = axios;
|
||||||
|
|
||||||
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@ -307,7 +307,7 @@
|
|||||||
<td class="text-center">
|
<td class="text-center">
|
||||||
<button class="btn btn-sm fw-600"
|
<button class="btn btn-sm fw-600"
|
||||||
style="background:rgba(239,68,68,.1);color:#dc2626;border:none;border-radius:.5rem;font-family:'Cairo',sans-serif;padding:.35rem .9rem;"
|
style="background:rgba(239,68,68,.1);color:#dc2626;border:none;border-radius:.5rem;font-family:'Cairo',sans-serif;padding:.35rem .9rem;"
|
||||||
onclick="completeBooking({{ $booking->id }}, this)">
|
onclick="askComplete({{ $booking->id }}, '{{ addslashes($booking->vehicle_plate ?? '—') }}', '{{ addslashes($booking->parkingLot->name) }}')">
|
||||||
<i class="bi bi-stop-circle me-1"></i>إنهاء
|
<i class="bi bi-stop-circle me-1"></i>إنهاء
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
@ -338,8 +338,57 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{{-- ── Confirm complete modal ───────────────────────────────────────────────── --}}
|
||||||
|
<div class="modal fade" id="confirmCompleteModal" tabindex="-1" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-dialog-centered" style="max-width:380px;">
|
||||||
|
<div class="modal-content" style="border:none;border-radius:1rem;overflow:hidden;box-shadow:0 20px 60px rgba(0,0,0,.15);">
|
||||||
|
<div style="background:linear-gradient(135deg,#dc2626,#ef4444);padding:1.25rem 1.5rem;">
|
||||||
|
<div class="d-flex align-items-center gap-3">
|
||||||
|
<div style="width:44px;height:44px;background:rgba(255,255,255,.15);border-radius:.75rem;display:flex;align-items:center;justify-content:center;flex-shrink:0;">
|
||||||
|
<i class="bi bi-stop-circle-fill" style="font-size:1.3rem;color:#fff;"></i>
|
||||||
|
</div>
|
||||||
|
<div class="flex-grow-1">
|
||||||
|
<div class="fw-800" style="color:#fff;font-size:1rem;">إنهاء الحجز</div>
|
||||||
|
<div class="text-xs mt-1" style="color:rgba(255,255,255,.75);">تأكيد تسجيل خروج السيارة</div>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="btn-close btn-close-white flex-shrink-0" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="p-4">
|
||||||
|
<div class="p-3 rounded-3 mb-4" style="background:#f8fafc;border:1px solid #f1f5f9;">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||||
|
<span class="text-xs fw-600" style="color:#64748b;">رقم اللوحة</span>
|
||||||
|
<span class="fw-800" id="confirmPlate" style="font-family:monospace;font-size:1rem;color:#0f172a;letter-spacing:.05em;"></span>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
|
<span class="text-xs fw-600" style="color:#64748b;">الموقف</span>
|
||||||
|
<span class="fw-600 text-sm" id="confirmLot" style="color:#475569;"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="text-sm mb-0" style="color:#64748b;text-align:center;">
|
||||||
|
هل تريد إنهاء هذا الحجز وتسجيل خروج السيارة؟
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="px-4 pb-4 d-flex gap-2">
|
||||||
|
<button type="button" class="btn btn-sm fw-600"
|
||||||
|
style="background:#f1f5f9;color:#475569;border:none;border-radius:.5rem;font-family:'Cairo',sans-serif;padding:.5rem 1rem;"
|
||||||
|
data-bs-dismiss="modal">إلغاء</button>
|
||||||
|
<button type="button" id="confirmCompleteBtn"
|
||||||
|
class="btn btn-sm fw-700 flex-grow-1"
|
||||||
|
style="background:#ef4444;color:#fff;border:none;border-radius:.5rem;font-family:'Cairo',sans-serif;padding:.5rem;">
|
||||||
|
<i class="bi bi-stop-circle me-1"></i>تأكيد الإنهاء
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- ── Toast ────────────────────────────────────────────────────────────────── --}}
|
||||||
|
<div id="toastWrap" style="position:fixed;bottom:1.5rem;inset-inline-start:50%;transform:translateX(-50%);z-index:9999;pointer-events:none;"></div>
|
||||||
|
|
||||||
@push('scripts')
|
@push('scripts')
|
||||||
<script>
|
<script>
|
||||||
|
// ── Filters ───────────────────────────────────────────────────────────────────
|
||||||
function applyFilters() {
|
function applyFilters() {
|
||||||
const url = new URL(window.location);
|
const url = new URL(window.location);
|
||||||
const lot = document.getElementById('lotFilter').value;
|
const lot = document.getElementById('lotFilter').value;
|
||||||
@ -349,48 +398,88 @@ function applyFilters() {
|
|||||||
url.searchParams.delete('page');
|
url.searchParams.delete('page');
|
||||||
window.location.href = url.toString();
|
window.location.href = url.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById('lotFilter').addEventListener('change', applyFilters);
|
document.getElementById('lotFilter').addEventListener('change', applyFilters);
|
||||||
document.getElementById('searchInput').addEventListener('keydown', e => {
|
document.getElementById('searchInput').addEventListener('keydown', e => {
|
||||||
if (e.key === 'Enter') applyFilters();
|
if (e.key === 'Enter') applyFilters();
|
||||||
});
|
});
|
||||||
|
|
||||||
async function completeBooking(id, btn) {
|
// ── Toast ─────────────────────────────────────────────────────────────────────
|
||||||
if (!confirm('إنهاء هذا الحجز؟')) return;
|
function showToast(msg, type = 'success') {
|
||||||
const orig = btn.innerHTML;
|
const color = type === 'success' ? '#10b981' : '#ef4444';
|
||||||
|
const icon = type === 'success' ? 'bi-check-circle-fill' : 'bi-exclamation-triangle-fill';
|
||||||
|
const el = document.createElement('div');
|
||||||
|
el.style.cssText = `background:#0f172a;color:#f8fafc;padding:.65rem 1.25rem;border-radius:.625rem;font-size:.85rem;font-weight:600;box-shadow:0 4px 20px rgba(0,0,0,.25);display:flex;align-items:center;gap:.6rem;pointer-events:auto;margin-top:.5rem;`;
|
||||||
|
el.innerHTML = `<i class="bi ${icon}" style="color:${color};font-size:1rem;flex-shrink:0;"></i>${msg}`;
|
||||||
|
document.getElementById('toastWrap').appendChild(el);
|
||||||
|
setTimeout(() => { el.style.transition='opacity .4s'; el.style.opacity='0'; setTimeout(()=>el.remove(),400); }, 3500);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Confirm complete modal ────────────────────────────────────────────────────
|
||||||
|
let pendingId = null;
|
||||||
|
let confirmModal = null;
|
||||||
|
|
||||||
|
function getConfirmModal() {
|
||||||
|
if (!confirmModal) confirmModal = new bootstrap.Modal(document.getElementById('confirmCompleteModal'));
|
||||||
|
return confirmModal;
|
||||||
|
}
|
||||||
|
|
||||||
|
function askComplete(id, plate, lot) {
|
||||||
|
pendingId = id;
|
||||||
|
document.getElementById('confirmPlate').textContent = plate;
|
||||||
|
document.getElementById('confirmLot').textContent = lot;
|
||||||
|
getConfirmModal().show();
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('confirmCompleteBtn').addEventListener('click', async () => {
|
||||||
|
if (!pendingId) return;
|
||||||
|
const btn = document.getElementById('confirmCompleteBtn');
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
btn.innerHTML = '<span class="spinner-border spinner-border-sm"></span>';
|
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>جاري الإنهاء...';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/admin/bookings/${id}/complete`, {
|
const res = await fetch(`/admin/bookings/${pendingId}/complete`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
|
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json',
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
|
getConfirmModal().hide();
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
const row = document.getElementById('row-' + id);
|
const row = document.getElementById('row-' + pendingId);
|
||||||
|
if (row) {
|
||||||
row.style.transition = 'opacity .4s, transform .4s';
|
row.style.transition = 'opacity .4s, transform .4s';
|
||||||
row.style.opacity = '0';
|
row.style.opacity = '0';
|
||||||
row.style.transform = 'translateX(20px)';
|
row.style.transform = 'translateX(20px)';
|
||||||
setTimeout(() => row.remove(), 420);
|
setTimeout(() => row.remove(), 420);
|
||||||
|
}
|
||||||
|
showToast('تم إنهاء الحجز بنجاح.');
|
||||||
} else {
|
} else {
|
||||||
alert(data.message || 'خطأ');
|
showToast(data.message || 'حدث خطأ أثناء الإنهاء.', 'error');
|
||||||
btn.innerHTML = orig;
|
|
||||||
btn.disabled = false;
|
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
alert('خطأ في الاتصال');
|
getConfirmModal().hide();
|
||||||
btn.innerHTML = orig;
|
showToast('تعذّر الاتصال بالخادم. حاول مجدداً.', 'error');
|
||||||
|
} finally {
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
|
btn.innerHTML = '<i class="bi bi-stop-circle me-1"></i>تأكيد الإنهاء';
|
||||||
|
pendingId = null;
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
|
|
||||||
// Countdown refresh
|
// ── Auto-refresh (pauses while modal is open) ─────────────────────────────────
|
||||||
let t = 30;
|
let t = 30;
|
||||||
|
let refreshPaused = false;
|
||||||
const badge = document.getElementById('refresh-badge');
|
const badge = document.getElementById('refresh-badge');
|
||||||
|
|
||||||
|
document.querySelectorAll('.modal').forEach(m => {
|
||||||
|
m.addEventListener('show.bs.modal', () => { refreshPaused = true; });
|
||||||
|
m.addEventListener('hidden.bs.modal', () => { refreshPaused = false; t = 30; });
|
||||||
|
});
|
||||||
|
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
|
if (refreshPaused) return;
|
||||||
t--;
|
t--;
|
||||||
badge.textContent = `تحديث بعد ${t}ث`;
|
badge.textContent = `تحديث بعد ${t}ث`;
|
||||||
if (t <= 0) location.reload();
|
if (t <= 0) location.reload();
|
||||||
|
|||||||
@ -74,7 +74,7 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-lg-3 col-sm-6">
|
<div class="col-lg-3 col-sm-6">
|
||||||
<div class="quick-card card h-100 p-4" onclick="alert('قريباً...')">
|
<div class="quick-card card h-100 p-4" style="cursor:default;">
|
||||||
<div class="quick-icon" style="background:rgba(100,116,139,.1);">
|
<div class="quick-icon" style="background:rgba(100,116,139,.1);">
|
||||||
<i class="bi bi-gear" style="color:#64748b;"></i>
|
<i class="bi bi-gear" style="color:#64748b;"></i>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -485,10 +485,81 @@ $gradients = [
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{{-- Toggle Status Confirmation Modal --}}
|
||||||
|
<div class="modal fade" id="toggleStatusModal" tabindex="-1" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-dialog-centered" style="max-width:380px;">
|
||||||
|
<div class="modal-content border-0 shadow-lg" style="border-radius:16px;overflow:hidden;">
|
||||||
|
<div class="modal-header border-0 text-white" style="background:linear-gradient(135deg,#f59e0b,#d97706);">
|
||||||
|
<h6 class="modal-title fw-bold mb-0" style="font-family:'Cairo',sans-serif;">
|
||||||
|
<i class="bi bi-toggle-on me-2"></i>تغيير حالة الموقف
|
||||||
|
</h6>
|
||||||
|
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body text-center p-4">
|
||||||
|
<p class="mb-0 fw-600" style="color:#0f172a;font-family:'Cairo',sans-serif;">
|
||||||
|
هل تريد تغيير حالة هذا الموقف؟
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer border-0 pt-0 pb-4 px-4 gap-2">
|
||||||
|
<button type="button" class="btn btn-light flex-fill fw-600" style="font-family:'Cairo',sans-serif;border-radius:10px;"
|
||||||
|
data-bs-dismiss="modal">إلغاء</button>
|
||||||
|
<button type="button" id="toggleStatusConfirmBtn"
|
||||||
|
class="btn btn-warning flex-fill fw-bold text-white"
|
||||||
|
style="font-family:'Cairo',sans-serif;border-radius:10px;">
|
||||||
|
<i class="bi bi-check2 me-1"></i>تأكيد
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Delete Lot Confirmation Modal --}}
|
||||||
|
<div class="modal fade" id="deleteLotModal" tabindex="-1" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-dialog-centered" style="max-width:420px;">
|
||||||
|
<div class="modal-content border-0 shadow-lg" style="border-radius:16px;overflow:hidden;">
|
||||||
|
<div class="modal-header border-0 text-white" style="background:linear-gradient(135deg,#dc2626,#991b1b);">
|
||||||
|
<h6 class="modal-title fw-bold mb-0" style="font-family:'Cairo',sans-serif;">
|
||||||
|
<i class="bi bi-trash3 me-2"></i>حذف الموقف
|
||||||
|
</h6>
|
||||||
|
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body text-center p-4">
|
||||||
|
<div class="mb-3" style="font-size:2.5rem;">⚠️</div>
|
||||||
|
<p class="fw-700 mb-1" style="color:#0f172a;font-family:'Cairo',sans-serif;">
|
||||||
|
حذف "<span id="deleteLotName"></span>"
|
||||||
|
</p>
|
||||||
|
<p class="text-muted small mb-0">سيتم حذف جميع بيانات الموقف نهائياً ولا يمكن التراجع.</p>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer border-0 pt-0 pb-4 px-4 gap-2">
|
||||||
|
<button type="button" class="btn btn-light flex-fill fw-600" style="font-family:'Cairo',sans-serif;border-radius:10px;"
|
||||||
|
data-bs-dismiss="modal">إلغاء</button>
|
||||||
|
<button type="button" id="deleteLotConfirmBtn"
|
||||||
|
class="btn btn-danger flex-fill fw-bold"
|
||||||
|
style="font-family:'Cairo',sans-serif;border-radius:10px;">
|
||||||
|
<i class="bi bi-trash3 me-1"></i>حذف نهائياً
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
@push('scripts')
|
@push('scripts')
|
||||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||||
<script>
|
<script>
|
||||||
window.onerror = (msg, src, line) => { alert('JS خطأ في السطر ' + line + ':\n' + msg); };
|
// ── Toast helper ─────────────────────────────────────────────────────────
|
||||||
|
function showToast(msg, type = 'success') {
|
||||||
|
const colors = { success:'#10b981', danger:'#ef4444', warning:'#f59e0b', info:'#3b82f6' };
|
||||||
|
const t = document.createElement('div');
|
||||||
|
t.style.cssText = `position:fixed;bottom:1.25rem;inset-inline-end:1.25rem;z-index:9999;
|
||||||
|
background:${colors[type]||colors.success};color:#fff;padding:.75rem 1.25rem;border-radius:10px;
|
||||||
|
font-family:'Cairo',sans-serif;font-size:.9rem;font-weight:600;
|
||||||
|
box-shadow:0 8px 24px rgba(0,0,0,.18);opacity:0;transition:opacity .25s;max-width:360px;`;
|
||||||
|
t.textContent = msg;
|
||||||
|
document.body.appendChild(t);
|
||||||
|
requestAnimationFrame(() => { t.style.opacity = '1'; });
|
||||||
|
setTimeout(() => { t.style.opacity = '0'; setTimeout(() => t.remove(), 300); }, 3500);
|
||||||
|
}
|
||||||
|
|
||||||
// ── Parking lots data from server ─────────────────────────────────────────
|
// ── Parking lots data from server ─────────────────────────────────────────
|
||||||
const lotsData = @json($parkingLots->items());
|
const lotsData = @json($parkingLots->items());
|
||||||
|
|
||||||
@ -591,7 +662,7 @@ function previewImage(input) {
|
|||||||
|
|
||||||
function editLot(id) {
|
function editLot(id) {
|
||||||
const lot = lotsData.find(l => l.id === id);
|
const lot = lotsData.find(l => l.id === id);
|
||||||
if (!lot) { alert('لم يتم العثور على بيانات الموقف'); return; }
|
if (!lot) { showToast('لم يتم العثور على بيانات الموقف', 'danger'); return; }
|
||||||
|
|
||||||
editingId = id;
|
editingId = id;
|
||||||
document.getElementById('modalLabel').textContent = 'تعديل: ' + lot.name;
|
document.getElementById('modalLabel').textContent = 'تعديل: ' + lot.name;
|
||||||
@ -632,12 +703,12 @@ async function saveLot() {
|
|||||||
if (result.success) {
|
if (result.success) {
|
||||||
location.reload();
|
location.reload();
|
||||||
} else if (result.errors) {
|
} else if (result.errors) {
|
||||||
const msgs = Object.values(result.errors).flat().join('\n');
|
const msgs = Object.values(result.errors).flat().join(' — ');
|
||||||
alert(msgs);
|
showToast(msgs, 'danger');
|
||||||
} else {
|
} else {
|
||||||
alert(result.message || 'خطأ في العملية');
|
showToast(result.message || 'خطأ في العملية', 'danger');
|
||||||
}
|
}
|
||||||
} catch(err) { alert('خطأ في الاتصال: ' + err.message); }
|
} catch(err) { showToast('خطأ في الاتصال', 'danger'); }
|
||||||
finally { setBtnLoading(false); }
|
finally { setBtnLoading(false); }
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -646,13 +717,26 @@ function setBtnLoading(on) {
|
|||||||
document.getElementById('submitSpinner').classList.toggle('d-none', !on);
|
document.getElementById('submitSpinner').classList.toggle('d-none', !on);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let pendingToggleId = null;
|
||||||
|
const toggleModal = new bootstrap.Modal(document.getElementById('toggleStatusModal'));
|
||||||
|
|
||||||
function toggleStatus(id) {
|
function toggleStatus(id) {
|
||||||
if (!confirm('تغيير حالة الموقف؟')) return;
|
pendingToggleId = id;
|
||||||
fetch(`/admin/parking-lots/${id}/toggle`, {
|
toggleModal.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('toggleStatusConfirmBtn').addEventListener('click', async () => {
|
||||||
|
if (!pendingToggleId) return;
|
||||||
|
toggleModal.hide();
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/admin/parking-lots/${pendingToggleId}/toggle`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content }
|
headers: { 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content }
|
||||||
}).then(r => r.json()).then(d => d.success ? location.reload() : alert(d.message));
|
});
|
||||||
}
|
const d = await res.json();
|
||||||
|
d.success ? location.reload() : showToast(d.message || 'حدث خطأ', 'danger');
|
||||||
|
} catch { showToast('خطأ في الاتصال', 'danger'); }
|
||||||
|
});
|
||||||
|
|
||||||
document.getElementById('searchInput').addEventListener('keypress', e => {
|
document.getElementById('searchInput').addEventListener('keypress', e => {
|
||||||
if (e.key === 'Enter') doSearch();
|
if (e.key === 'Enter') doSearch();
|
||||||
@ -685,7 +769,7 @@ function openPricingModal(id, name) {
|
|||||||
updatePricingPreview();
|
updatePricingPreview();
|
||||||
bootstrap.Modal.getOrCreateInstance(document.getElementById('pricingModal')).show();
|
bootstrap.Modal.getOrCreateInstance(document.getElementById('pricingModal')).show();
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
alert('خطأ في فتح نافذة التسعير:\n' + e.message);
|
showToast('خطأ في فتح نافذة التسعير', 'danger');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -726,7 +810,7 @@ async function savePricing() {
|
|||||||
const base = parseFloat(document.getElementById('p_base').value);
|
const base = parseFloat(document.getElementById('p_base').value);
|
||||||
|
|
||||||
if (isNaN(base) || base < 0) {
|
if (isNaN(base) || base < 0) {
|
||||||
alert('يرجى إدخال سعر أساسي صحيح');
|
showToast('يرجى إدخال سعر أساسي صحيح', 'warning');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -755,21 +839,33 @@ async function savePricing() {
|
|||||||
bootstrap.Modal.getInstance(document.getElementById('pricingModal'))?.hide();
|
bootstrap.Modal.getInstance(document.getElementById('pricingModal'))?.hide();
|
||||||
location.reload();
|
location.reload();
|
||||||
} else {
|
} else {
|
||||||
alert(data.message || 'خطأ في الحفظ');
|
showToast(data.message || 'خطأ في الحفظ', 'danger');
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
alert('خطأ في الاتصال');
|
showToast('خطأ في الاتصال', 'danger');
|
||||||
} finally {
|
} finally {
|
||||||
document.getElementById('savePricingBtn').disabled = false;
|
document.getElementById('savePricingBtn').disabled = false;
|
||||||
document.getElementById('pricingSpinner').classList.add('d-none');
|
document.getElementById('pricingSpinner').classList.add('d-none');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteLot(id, name) {
|
let pendingDeleteId = null;
|
||||||
if (!confirm(`حذف الموقف "${name}"؟\nسيتم حذف جميع بيانات الموقف نهائياً.`)) return;
|
const deleteModal = new bootstrap.Modal(document.getElementById('deleteLotModal'));
|
||||||
|
|
||||||
|
function deleteLot(id, name) {
|
||||||
|
pendingDeleteId = id;
|
||||||
|
document.getElementById('deleteLotName').textContent = name;
|
||||||
|
deleteModal.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('deleteLotConfirmBtn').addEventListener('click', async () => {
|
||||||
|
if (!pendingDeleteId) return;
|
||||||
|
const btn = document.getElementById('deleteLotConfirmBtn');
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>جاري الحذف...';
|
||||||
|
deleteModal.hide();
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/admin/parking-lots/${id}`, {
|
const res = await fetch(`/admin/parking-lots/${pendingDeleteId}`, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
headers: {
|
headers: {
|
||||||
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
|
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
|
||||||
@ -780,12 +876,16 @@ async function deleteLot(id, name) {
|
|||||||
if (data.success) {
|
if (data.success) {
|
||||||
location.reload();
|
location.reload();
|
||||||
} else {
|
} else {
|
||||||
alert(data.message || 'تعذّر الحذف');
|
showToast(data.message || 'تعذّر الحذف', 'danger');
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.innerHTML = '<i class="bi bi-trash3 me-1"></i>حذف نهائياً';
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
alert('خطأ في الاتصال');
|
showToast('خطأ في الاتصال', 'danger');
|
||||||
}
|
btn.disabled = false;
|
||||||
|
btn.innerHTML = '<i class="bi bi-trash3 me-1"></i>حذف نهائياً';
|
||||||
}
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
@endpush
|
@endpush
|
||||||
|
|
||||||
|
|||||||
@ -45,6 +45,42 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">رقم الهاتف</label>
|
||||||
|
@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
|
||||||
|
<div class="input-group @error('phone') is-invalid @enderror">
|
||||||
|
<input type="tel" name="phone_local" value="{{ old('phone_local') }}" required
|
||||||
|
class="form-control @error('phone') is-invalid @enderror"
|
||||||
|
style="border-color:#e2e8f0;"
|
||||||
|
placeholder="912345678" dir="ltr">
|
||||||
|
<select name="phone_country" dir="ltr"
|
||||||
|
style="max-width:130px;background:#f8fafc;border-color:#e2e8f0;color:#374151;font-family:'Cairo',sans-serif;font-size:.875rem;cursor:pointer;"
|
||||||
|
class="form-select">
|
||||||
|
@foreach($countries as [$flag, $label, $code])
|
||||||
|
<option value="{{ $code }}" {{ $oldCountry === $code ? 'selected' : '' }}>
|
||||||
|
{{ $flag }} {{ $code }}
|
||||||
|
</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
@error('phone')<div class="invalid-feedback d-block">{{ $message }}</div>@enderror
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label">كلمة السر</label>
|
<label class="form-label">كلمة السر</label>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
|
|||||||
@ -218,6 +218,100 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{{-- ══ BOOKING FORM MODAL ══════════════════════════════════════════════════ --}}
|
||||||
|
<div class="modal fade" id="bookingModal" tabindex="-1" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-dialog-centered" style="max-width:420px;">
|
||||||
|
<div class="modal-content" style="border:none;border-radius:1rem;overflow:hidden;">
|
||||||
|
|
||||||
|
{{-- Top banner: lot info --}}
|
||||||
|
<div style="background:linear-gradient(135deg,#0f172a,#1e3a5f);padding:1.25rem 1.5rem;">
|
||||||
|
<div class="d-flex align-items-center gap-3">
|
||||||
|
<div style="width:42px;height:42px;background:rgba(99,102,241,.25);border-radius:.625rem;display:flex;align-items:center;justify-content:center;flex-shrink:0;">
|
||||||
|
<i class="bi bi-p-square-fill" style="color:#a5b4fc;font-size:1.2rem;"></i>
|
||||||
|
</div>
|
||||||
|
<div class="flex-grow-1 min-width-0">
|
||||||
|
<div id="bookingLotName" class="fw-700 text-truncate" style="color:#f8fafc;font-size:.95rem;"></div>
|
||||||
|
<div id="bookingLotMeta" class="text-xs mt-1" style="color:#94a3b8;"></div>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="btn-close btn-close-white flex-shrink-0" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Booker identity (read-only) --}}
|
||||||
|
<div style="background:#f8fafc;padding:.75rem 1.5rem;border-bottom:1px solid #f1f5f9;">
|
||||||
|
<div class="d-flex align-items-center gap-2">
|
||||||
|
<i class="bi bi-person-check-fill" style="color:#10b981;font-size:.95rem;"></i>
|
||||||
|
<span class="text-xs fw-600" style="color:#475569;" id="bookingUserInfo"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="p-4">
|
||||||
|
{{-- Alert --}}
|
||||||
|
<div id="bookingAlert" class="d-none mb-3"></div>
|
||||||
|
|
||||||
|
<div class="row g-3">
|
||||||
|
{{-- Plate number --}}
|
||||||
|
<div class="col-12">
|
||||||
|
<label class="form-label fw-600" style="font-size:.82rem;color:#374151;">
|
||||||
|
<i class="bi bi-car-front me-1" style="color:#6366f1;"></i>
|
||||||
|
رقم لوحة السيارة
|
||||||
|
</label>
|
||||||
|
<input type="text" id="bkPlate" class="form-control fw-700"
|
||||||
|
style="letter-spacing:.1em;text-transform:uppercase;font-size:1rem;text-align:center;"
|
||||||
|
placeholder="أ ب ج 1234" dir="ltr">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Date/time --}}
|
||||||
|
<div class="col-6">
|
||||||
|
<label class="form-label fw-600" style="font-size:.82rem;color:#374151;">
|
||||||
|
<i class="bi bi-play-circle me-1" style="color:#10b981;"></i>
|
||||||
|
من
|
||||||
|
</label>
|
||||||
|
<input type="datetime-local" id="bkStart" class="form-control" dir="ltr" style="font-size:.85rem;">
|
||||||
|
</div>
|
||||||
|
<div class="col-6">
|
||||||
|
<label class="form-label fw-600" style="font-size:.82rem;color:#374151;">
|
||||||
|
<i class="bi bi-stop-circle me-1" style="color:#ef4444;"></i>
|
||||||
|
إلى
|
||||||
|
</label>
|
||||||
|
<input type="datetime-local" id="bkEnd" class="form-control" dir="ltr" style="font-size:.85rem;">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Price summary --}}
|
||||||
|
<div class="col-12">
|
||||||
|
<div id="bookingPriceSummary" style="background:#f0fdf4;border:1px solid #bbf7d0;border-radius:.625rem;padding:.875rem 1rem;">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||||
|
<span class="text-xs" style="color:#64748b;">المدة</span>
|
||||||
|
<span class="fw-600 text-xs" id="bkDuration" style="color:#0f172a;">--</span>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||||
|
<span class="text-xs" style="color:#64748b;">السعر / ساعة</span>
|
||||||
|
<span class="fw-600 text-xs" id="bkHourlyRate" style="color:#0f172a;">--</span>
|
||||||
|
</div>
|
||||||
|
<div style="border-top:1px dashed #bbf7d0;margin:.5rem 0;"></div>
|
||||||
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
|
<span class="fw-700" style="color:#065f46;font-size:.85rem;">المبلغ الإجمالي</span>
|
||||||
|
<span class="fw-800" id="bkTotal" style="color:#059669;font-size:1.15rem;">--</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="px-4 pb-4 d-flex gap-2">
|
||||||
|
<button type="button" class="btn btn-sm fw-600"
|
||||||
|
style="background:#f1f5f9;color:#475569;border:none;border-radius:.5rem;font-family:'Cairo',sans-serif;padding:.5rem 1rem;"
|
||||||
|
data-bs-dismiss="modal">إلغاء</button>
|
||||||
|
<button type="button" id="confirmBookingBtn"
|
||||||
|
class="btn btn-sm fw-700 flex-grow-1"
|
||||||
|
style="background:#10b981;color:#fff;border:none;border-radius:.5rem;font-family:'Cairo',sans-serif;padding:.5rem;">
|
||||||
|
<i class="bi bi-check-lg me-1"></i>تأكيد الحجز
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{{-- ══ MOBILE BOTTOM NAV ══════════════════════════════════════════════════ --}}
|
{{-- ══ MOBILE BOTTOM NAV ══════════════════════════════════════════════════ --}}
|
||||||
<nav class="mobile-bottom-nav" aria-label="التنقل الرئيسي">
|
<nav class="mobile-bottom-nav" aria-label="التنقل الرئيسي">
|
||||||
<a href="#" class="mob-nav-item active" data-tab="list"
|
<a href="#" class="mob-nav-item active" data-tab="list"
|
||||||
@ -402,15 +496,138 @@
|
|||||||
getModal().show();
|
getModal().show();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Auth state (from server) ──────────────────────────────────────────
|
||||||
|
const authUser = @auth {
|
||||||
|
id: {{ auth()->id() }},
|
||||||
|
name: {{ Js::from(auth()->user()->name) }},
|
||||||
|
phone: {{ Js::from(auth()->user()->phone ?? '') }}
|
||||||
|
} @else null @endauth;
|
||||||
|
|
||||||
// ── Reserve ───────────────────────────────────────────────────────────
|
// ── Reserve ───────────────────────────────────────────────────────────
|
||||||
document.getElementById('reserveBtn').addEventListener('click', () => {
|
document.getElementById('reserveBtn').addEventListener('click', () => {
|
||||||
if (!current || current.avail <= 0) return;
|
if (!current || current.avail <= 0) return;
|
||||||
alert(`تم حجز مكان في ${current.name}!\nالعنوان: ${current.address}\nالسعر: ${fmtPrice(current.price)} / ساعة`);
|
|
||||||
current.avail--;
|
if (!authUser) {
|
||||||
|
// Guest — redirect to login, come back after
|
||||||
|
window.location.href = '{{ route('login') }}';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logged in — open booking form
|
||||||
getModal().hide();
|
getModal().hide();
|
||||||
renderList(lots);
|
openBookingModal(current);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── Booking modal ─────────────────────────────────────────────────────
|
||||||
|
let bookingModal = null;
|
||||||
|
function getBookingModal() {
|
||||||
|
if (!bookingModal) bookingModal = new bootstrap.Modal(document.getElementById('bookingModal'));
|
||||||
|
return bookingModal;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toLocalInput(date) {
|
||||||
|
const pad = n => String(n).padStart(2, '0');
|
||||||
|
return `${date.getFullYear()}-${pad(date.getMonth()+1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function calcTotal() {
|
||||||
|
const start = document.getElementById('bkStart').value;
|
||||||
|
const end = document.getElementById('bkEnd').value;
|
||||||
|
if (!start || !end || !current) return;
|
||||||
|
const diffMs = new Date(end) - new Date(start);
|
||||||
|
if (diffMs <= 0) { resetPrice(); return; }
|
||||||
|
const hours = Math.ceil(diffMs / 3600000); // round up per fee logic
|
||||||
|
const total = hours * current.price;
|
||||||
|
document.getElementById('bkDuration').textContent = hours + (hours === 1 ? ' ساعة' : ' ساعات');
|
||||||
|
document.getElementById('bkHourlyRate').textContent = fmtPrice(current.price);
|
||||||
|
document.getElementById('bkTotal').textContent = fmtPrice(total);
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetPrice() {
|
||||||
|
document.getElementById('bkDuration').textContent = '--';
|
||||||
|
document.getElementById('bkHourlyRate').textContent = '--';
|
||||||
|
document.getElementById('bkTotal').textContent = '--';
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('bkStart').addEventListener('change', calcTotal);
|
||||||
|
document.getElementById('bkEnd').addEventListener('change', calcTotal);
|
||||||
|
|
||||||
|
function openBookingModal(lot) {
|
||||||
|
document.getElementById('bookingLotName').textContent = lot.name;
|
||||||
|
document.getElementById('bookingLotMeta').textContent = lot.address + ' · ' + fmtPrice(lot.price) + ' / ساعة';
|
||||||
|
document.getElementById('bookingUserInfo').textContent =
|
||||||
|
authUser.name + (authUser.phone ? ' | ' + authUser.phone : '');
|
||||||
|
document.getElementById('bkPlate').value = '';
|
||||||
|
|
||||||
|
// Default: now → now+2h
|
||||||
|
const now = new Date();
|
||||||
|
const later = new Date(now.getTime() + 2 * 3600 * 1000);
|
||||||
|
document.getElementById('bkStart').value = toLocalInput(now);
|
||||||
|
document.getElementById('bkEnd').value = toLocalInput(later);
|
||||||
|
calcTotal();
|
||||||
|
|
||||||
|
document.getElementById('bookingAlert').className = 'd-none mb-3';
|
||||||
|
document.getElementById('bookingAlert').innerHTML = '';
|
||||||
|
|
||||||
|
getBookingModal().show();
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('confirmBookingBtn').addEventListener('click', async () => {
|
||||||
|
const plate = document.getElementById('bkPlate').value.trim();
|
||||||
|
const start = document.getElementById('bkStart').value;
|
||||||
|
const end = document.getElementById('bkEnd').value;
|
||||||
|
const alertEl = document.getElementById('bookingAlert');
|
||||||
|
|
||||||
|
const showErr = msg => {
|
||||||
|
alertEl.className = 'alert border-0 rounded-3 py-2 mb-3 text-sm';
|
||||||
|
alertEl.style.cssText = 'background:rgba(239,68,68,.1);color:#b91c1c;';
|
||||||
|
alertEl.innerHTML = `<i class="bi bi-exclamation-triangle me-1"></i>${msg}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!plate) return showErr('الرجاء إدخال رقم لوحة السيارة.');
|
||||||
|
if (!start) return showErr('الرجاء تحديد وقت البدء.');
|
||||||
|
if (!end) return showErr('الرجاء تحديد وقت الانتهاء.');
|
||||||
|
if (new Date(end) <= new Date(start)) return showErr('وقت الانتهاء يجب أن يكون بعد وقت البدء.');
|
||||||
|
|
||||||
|
const btn = document.getElementById('confirmBookingBtn');
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>جاري الحجز...';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const resp = await axios.post('/reserve', {
|
||||||
|
parking_lot_id: current.id,
|
||||||
|
vehicle_plate: plate,
|
||||||
|
start_time: start.replace('T', ' ') + ':00',
|
||||||
|
end_time: end.replace('T', ' ') + ':00',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (resp.data.success) {
|
||||||
|
getBookingModal().hide();
|
||||||
|
current.avail = Math.max(0, current.avail - 1);
|
||||||
|
renderList(lots);
|
||||||
|
showToast('تم الحجز بنجاح! يمكنك متابعة حجوزاتك من حسابي.');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
const errors = err.response?.data?.errors;
|
||||||
|
const msg = errors
|
||||||
|
? Object.values(errors).flat().join(' — ')
|
||||||
|
: (err.response?.data?.message || 'حدث خطأ، الرجاء المحاولة مجدداً.');
|
||||||
|
showErr(msg);
|
||||||
|
} finally {
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.innerHTML = '<i class="bi bi-check-lg me-1"></i>تأكيد الحجز';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Toast ─────────────────────────────────────────────────────────────
|
||||||
|
function showToast(msg) {
|
||||||
|
const t = document.createElement('div');
|
||||||
|
t.style.cssText = 'position:fixed;bottom:80px;inset-inline-start:50%;transform:translateX(-50%);background:#0f172a;color:#f8fafc;padding:.6rem 1.25rem;border-radius:.625rem;font-size:.85rem;font-weight:600;z-index:9999;box-shadow:0 4px 20px rgba(0,0,0,.25);white-space:nowrap;';
|
||||||
|
t.innerHTML = `<i class="bi bi-check-circle-fill me-2" style="color:#10b981;"></i>${msg}`;
|
||||||
|
document.body.appendChild(t);
|
||||||
|
setTimeout(() => t.remove(), 4000);
|
||||||
|
}
|
||||||
|
|
||||||
// ── Search ────────────────────────────────────────────────────────────
|
// ── Search ────────────────────────────────────────────────────────────
|
||||||
function doSearch() {
|
function doSearch() {
|
||||||
const term = document.getElementById('searchInput').value.trim().toLowerCase();
|
const term = document.getElementById('searchInput').value.trim().toLowerCase();
|
||||||
|
|||||||
@ -111,7 +111,7 @@
|
|||||||
<a href="{{ route('parking.index') }}"
|
<a href="{{ route('parking.index') }}"
|
||||||
class="btn btn-sm d-none d-md-inline-flex align-items-center"
|
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;"
|
style="background:#f1f5f9;color:#475569;border:none;border-radius:.5rem;font-family:'Cairo',sans-serif;"
|
||||||
target="_blank">
|
>
|
||||||
<i class="bi bi-globe2 me-1"></i>الموقع العام
|
<i class="bi bi-globe2 me-1"></i>الموقع العام
|
||||||
</a>
|
</a>
|
||||||
{{-- Mobile: user avatar + logout --}}
|
{{-- Mobile: user avatar + logout --}}
|
||||||
|
|||||||
@ -590,11 +590,57 @@ $gradients = [
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{{-- ══════════════════════════════════════════════════════════════════════
|
||||||
|
ACTIVATE RESERVATION CONFIRM MODAL
|
||||||
|
══════════════════════════════════════════════════════════════════════ --}}
|
||||||
|
<div class="modal fade" id="activateConfirmModal" tabindex="-1" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-dialog-centered" style="max-width:400px;">
|
||||||
|
<div class="modal-content border-0 shadow-lg" style="border-radius:16px;overflow:hidden;">
|
||||||
|
<div class="modal-header text-white border-0" style="background:linear-gradient(135deg,#1e3c72,#2a5298);">
|
||||||
|
<h6 class="modal-title fw-bold mb-0" style="font-family:'Cairo',sans-serif;">
|
||||||
|
<i class="bi bi-box-arrow-in-right me-2"></i>تأكيد فتح البوابة
|
||||||
|
</h6>
|
||||||
|
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body text-center p-4">
|
||||||
|
<div class="mb-3" style="font-size:2.5rem;">🚗</div>
|
||||||
|
<p class="mb-1 fw-600" style="color:#0f172a;font-family:'Cairo',sans-serif;">
|
||||||
|
هل تريد تفعيل هذا الحجز؟
|
||||||
|
</p>
|
||||||
|
<p class="text-muted small mb-0">سيتم تسجيل وقت الدخول الآن وفتح البوابة للسيارة.</p>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer border-0 pt-0 pb-4 px-4 gap-2">
|
||||||
|
<button type="button" class="btn btn-light flex-fill fw-600" style="font-family:'Cairo',sans-serif;border-radius:10px;"
|
||||||
|
data-bs-dismiss="modal">إلغاء</button>
|
||||||
|
<button type="button" id="activateConfirmBtn"
|
||||||
|
class="btn btn-primary flex-fill fw-bold"
|
||||||
|
style="font-family:'Cairo',sans-serif;border-radius:10px;">
|
||||||
|
<i class="bi bi-check2-circle me-1"></i>تأكيد الدخول
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
@push('scripts')
|
@push('scripts')
|
||||||
<script>
|
<script>
|
||||||
const lots = {!! $parkingLots->toJson() !!};
|
const lots = {!! $parkingLots->toJson() !!};
|
||||||
const csrf = document.querySelector('meta[name="csrf-token"]').content;
|
const csrf = document.querySelector('meta[name="csrf-token"]').content;
|
||||||
|
|
||||||
|
// ── Toast helper ───────────────────────────────────────────────────────
|
||||||
|
function showToast(msg, type = 'success') {
|
||||||
|
const colors = { success: '#10b981', danger: '#ef4444', warning: '#f59e0b', info: '#3b82f6' };
|
||||||
|
const t = document.createElement('div');
|
||||||
|
t.style.cssText = `position:fixed;bottom:1.25rem;inset-inline-end:1.25rem;z-index:9999;
|
||||||
|
background:${colors[type]||colors.success};color:#fff;padding:.75rem 1.25rem;border-radius:10px;
|
||||||
|
font-family:'Cairo',sans-serif;font-size:.9rem;font-weight:600;
|
||||||
|
box-shadow:0 8px 24px rgba(0,0,0,.18);opacity:0;transition:opacity .25s;max-width:320px;`;
|
||||||
|
t.textContent = msg;
|
||||||
|
document.body.appendChild(t);
|
||||||
|
requestAnimationFrame(() => { t.style.opacity = '1'; });
|
||||||
|
setTimeout(() => { t.style.opacity = '0'; setTimeout(() => t.remove(), 300); }, 3500);
|
||||||
|
}
|
||||||
|
|
||||||
function selectLot(id) { window.location = '/operator/dashboard?lot_id=' + id; }
|
function selectLot(id) { window.location = '/operator/dashboard?lot_id=' + id; }
|
||||||
|
|
||||||
@if(!$selectedLot)
|
@if(!$selectedLot)
|
||||||
@ -670,12 +716,12 @@ async function submitCheckIn() {
|
|||||||
btn.innerHTML = '<i class="bi bi-check2-circle me-1"></i>تم!';
|
btn.innerHTML = '<i class="bi bi-check2-circle me-1"></i>تم!';
|
||||||
setTimeout(() => location.reload(), 700);
|
setTimeout(() => location.reload(), 700);
|
||||||
} else {
|
} else {
|
||||||
alert(data.message || 'حدث خطأ');
|
showToast(data.message || 'حدث خطأ', 'danger');
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
btn.innerHTML = '<i class="bi bi-check-lg me-1"></i>تأكيد الدخول';
|
btn.innerHTML = '<i class="bi bi-check-lg me-1"></i>تأكيد الدخول';
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
alert('خطأ في الاتصال');
|
showToast('خطأ في الاتصال', 'danger');
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
btn.innerHTML = '<i class="bi bi-check-lg me-1"></i>تأكيد الدخول';
|
btn.innerHTML = '<i class="bi bi-check-lg me-1"></i>تأكيد الدخول';
|
||||||
}
|
}
|
||||||
@ -692,8 +738,28 @@ document.getElementById('newEntryModal').addEventListener('hidden.bs.modal', ()
|
|||||||
});
|
});
|
||||||
|
|
||||||
// ── Activate reservation ───────────────────────────────────────────────
|
// ── Activate reservation ───────────────────────────────────────────────
|
||||||
async function activateRes(id, btn) {
|
let pendingActivateId = null;
|
||||||
if (!confirm('تأكيد فتح البوابة لهذا الحجز؟')) return;
|
let pendingActivateBtn = null;
|
||||||
|
let activateModal = null;
|
||||||
|
|
||||||
|
function getActivateModal() {
|
||||||
|
if (!activateModal) activateModal = new bootstrap.Modal(document.getElementById('activateConfirmModal'));
|
||||||
|
return activateModal;
|
||||||
|
}
|
||||||
|
|
||||||
|
function activateRes(id, btn) {
|
||||||
|
pendingActivateId = id;
|
||||||
|
pendingActivateBtn = btn;
|
||||||
|
getActivateModal().show();
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('activateConfirmBtn').addEventListener('click', async () => {
|
||||||
|
const id = pendingActivateId;
|
||||||
|
const btn = pendingActivateBtn;
|
||||||
|
if (!id || !btn) return;
|
||||||
|
|
||||||
|
getActivateModal().hide();
|
||||||
|
|
||||||
const orig = btn.innerHTML;
|
const orig = btn.innerHTML;
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
btn.innerHTML = '<span class="spinner-border spinner-border-sm"></span>';
|
btn.innerHTML = '<span class="spinner-border spinner-border-sm"></span>';
|
||||||
@ -703,21 +769,19 @@ async function activateRes(id, btn) {
|
|||||||
});
|
});
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
// Check-in button → success state
|
|
||||||
btn.classList.replace('btn-primary', 'btn-success');
|
btn.classList.replace('btn-primary', 'btn-success');
|
||||||
btn.innerHTML = '<i class="bi bi-check-circle me-1"></i>تم الدخول';
|
btn.innerHTML = '<i class="bi bi-check-circle me-1"></i>تم الدخول';
|
||||||
// Enable checkout button
|
|
||||||
const co = document.getElementById('checkout-' + id);
|
const co = document.getElementById('checkout-' + id);
|
||||||
if (co) { co.disabled = false; }
|
if (co) { co.disabled = false; }
|
||||||
} else {
|
} else {
|
||||||
alert(data.message || 'حدث خطأ');
|
showToast(data.message || 'حدث خطأ', 'danger');
|
||||||
btn.innerHTML = orig; btn.disabled = false;
|
btn.innerHTML = orig; btn.disabled = false;
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
alert('خطأ في الاتصال');
|
showToast('خطأ في الاتصال', 'danger');
|
||||||
btn.innerHTML = orig; btn.disabled = false;
|
btn.innerHTML = orig; btn.disabled = false;
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
|
|
||||||
// ── Receipt modal ──────────────────────────────────────────────────────
|
// ── Receipt modal ──────────────────────────────────────────────────────
|
||||||
let receiptModal = null;
|
let receiptModal = null;
|
||||||
@ -748,7 +812,7 @@ async function openReceipt(id) {
|
|||||||
try {
|
try {
|
||||||
const res = await fetch(`/operator/${id}/checkout-preview`);
|
const res = await fetch(`/operator/${id}/checkout-preview`);
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
if (!data.success) { alert(data.message); return; }
|
if (!data.success) { showToast(data.message || 'حدث خطأ', 'danger'); getModal().hide(); return; }
|
||||||
const d = data.data;
|
const d = data.data;
|
||||||
|
|
||||||
document.getElementById('rcpt-lot').textContent = d.lot_name;
|
document.getElementById('rcpt-lot').textContent = d.lot_name;
|
||||||
@ -766,7 +830,7 @@ async function openReceipt(id) {
|
|||||||
</div>`).join('');
|
</div>`).join('');
|
||||||
document.getElementById('rcpt-breakdown').innerHTML =
|
document.getElementById('rcpt-breakdown').innerHTML =
|
||||||
rows || '<p class="text-xs text-center" style="color:#94a3b8;">لا تفاصيل</p>';
|
rows || '<p class="text-xs text-center" style="color:#94a3b8;">لا تفاصيل</p>';
|
||||||
} catch { alert('تعذّر تحميل بيانات الفاتورة'); }
|
} catch { showToast('تعذّر تحميل بيانات الفاتورة', 'danger'); getModal().hide(); }
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectPayment(method) {
|
function selectPayment(method) {
|
||||||
@ -777,7 +841,7 @@ function selectPayment(method) {
|
|||||||
document.getElementById('confirmPayBtn').addEventListener('click', async () => {
|
document.getElementById('confirmPayBtn').addEventListener('click', async () => {
|
||||||
if (!currentBookingId) return;
|
if (!currentBookingId) return;
|
||||||
if (selectedPayment === 'upload' && !document.getElementById('paymentProofFile').files.length) {
|
if (selectedPayment === 'upload' && !document.getElementById('paymentProofFile').files.length) {
|
||||||
alert('يرجى رفع إيصال الدفع'); return;
|
showToast('يرجى رفع إيصال الدفع', 'warning'); return;
|
||||||
}
|
}
|
||||||
const btn = document.getElementById('confirmPayBtn');
|
const btn = document.getElementById('confirmPayBtn');
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
@ -800,12 +864,12 @@ document.getElementById('confirmPayBtn').addEventListener('click', async () => {
|
|||||||
if (card) { card.style.transition='opacity .4s'; card.style.opacity='0'; setTimeout(()=>card.remove(),400); }
|
if (card) { card.style.transition='opacity .4s'; card.style.opacity='0'; setTimeout(()=>card.remove(),400); }
|
||||||
setTimeout(() => location.reload(), 600);
|
setTimeout(() => location.reload(), 600);
|
||||||
} else {
|
} else {
|
||||||
alert(data.message || 'حدث خطأ');
|
showToast(data.message || 'حدث خطأ', 'danger');
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
btn.innerHTML = '<i class="bi bi-check2-circle me-1"></i>تأكيد الدفع';
|
btn.innerHTML = '<i class="bi bi-check2-circle me-1"></i>تأكيد الدفع';
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
alert('خطأ في الاتصال');
|
showToast('خطأ في الاتصال', 'danger');
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
btn.innerHTML = '<i class="bi bi-check2-circle me-1"></i>تأكيد الدفع';
|
btn.innerHTML = '<i class="bi bi-check2-circle me-1"></i>تأكيد الدفع';
|
||||||
}
|
}
|
||||||
|
|||||||
@ -30,6 +30,9 @@
|
|||||||
<div>
|
<div>
|
||||||
<h2 class="fw-800 mb-1" style="font-size:1.15rem;color:#0f172a;">{{ $user->name }}</h2>
|
<h2 class="fw-800 mb-1" style="font-size:1.15rem;color:#0f172a;">{{ $user->name }}</h2>
|
||||||
<p class="mb-1 text-sm" style="color:#64748b;">{{ $user->email }}</p>
|
<p class="mb-1 text-sm" style="color:#64748b;">{{ $user->email }}</p>
|
||||||
|
@if($user->phone)
|
||||||
|
<p class="mb-1 text-sm" style="color:#64748b;" dir="ltr">{{ $user->phone }}</p>
|
||||||
|
@endif
|
||||||
@php
|
@php
|
||||||
$roleLabel = match($user->role) {
|
$roleLabel = match($user->role) {
|
||||||
'admin' => 'مدير النظام',
|
'admin' => 'مدير النظام',
|
||||||
@ -52,15 +55,20 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{-- ── Edit name ───────────────────────────────────────────────────────── --}}
|
{{-- ── Edit profile ──────────────────────────────────────────────────── --}}
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="card h-100">
|
<div class="card h-100">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<span class="fw-700" style="font-size:.9rem;">
|
<span class="fw-700" style="font-size:.9rem;">
|
||||||
<i class="bi bi-person me-2" style="color:#6366f1;"></i>تعديل الاسم
|
<i class="bi bi-person me-2" style="color:#6366f1;"></i>تعديل البيانات الشخصية
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body p-4">
|
<div class="card-body p-4">
|
||||||
|
@if(session('success'))
|
||||||
|
<div class="alert border-0 rounded-3 py-2 mb-3 text-sm" style="background:rgba(16,185,129,.1);color:#065f46;">
|
||||||
|
<i class="bi bi-check-circle me-1"></i>{{ session('success') }}
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
@if($errors->updateName->any())
|
@if($errors->updateName->any())
|
||||||
<div class="alert alert-danger border-0 rounded-3 py-2 mb-3 text-sm">
|
<div class="alert alert-danger border-0 rounded-3 py-2 mb-3 text-sm">
|
||||||
{{ $errors->updateName->first() }}
|
{{ $errors->updateName->first() }}
|
||||||
@ -76,6 +84,59 @@
|
|||||||
value="{{ old('name', $user->name) }}"
|
value="{{ old('name', $user->name) }}"
|
||||||
placeholder="أدخل اسمك الكامل">
|
placeholder="أدخل اسمك الكامل">
|
||||||
</div>
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">رقم الهاتف</label>
|
||||||
|
@php
|
||||||
|
// Split stored phone back into country code + local number
|
||||||
|
$storedPhone = $user->phone ?? '';
|
||||||
|
$knownCodes = ['+380','+351','+234','+249','+218','+216','+213','+212',
|
||||||
|
'+974','+973','+971','+970','+968','+967','+966','+965',
|
||||||
|
'+964','+963','+962','+961','+92','+91','+90','+86',
|
||||||
|
'+81','+61','+55','+49','+39','+34','+33','+20',
|
||||||
|
'+98','+44','+7','+1'];
|
||||||
|
$savedCountry = old('phone_country', '+963');
|
||||||
|
$savedLocal = old('phone_local', $storedPhone);
|
||||||
|
foreach ($knownCodes as $kc) {
|
||||||
|
if (str_starts_with($storedPhone, $kc)) {
|
||||||
|
$savedCountry = $kc;
|
||||||
|
$savedLocal = substr($storedPhone, strlen($kc));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$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'],
|
||||||
|
];
|
||||||
|
@endphp
|
||||||
|
<div class="input-group @if($errors->updateName->has('phone_local')) is-invalid @endif">
|
||||||
|
<input type="tel" name="phone_local"
|
||||||
|
value="{{ $savedLocal }}"
|
||||||
|
class="form-control @if($errors->updateName->has('phone_local')) is-invalid @endif"
|
||||||
|
style="border-color:#e2e8f0;"
|
||||||
|
placeholder="912345678" dir="ltr">
|
||||||
|
<select name="phone_country" dir="ltr"
|
||||||
|
style="max-width:130px;background:#f8fafc;border-color:#e2e8f0;color:#374151;font-family:'Cairo',sans-serif;font-size:.875rem;cursor:pointer;"
|
||||||
|
class="form-select">
|
||||||
|
@foreach($countries as [$flag, $label, $code])
|
||||||
|
<option value="{{ $code }}" {{ $savedCountry === $code ? 'selected' : '' }}>
|
||||||
|
{{ $flag }} {{ $code }}
|
||||||
|
</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
@if($errors->updateName->has('phone_local'))
|
||||||
|
<div class="invalid-feedback d-block">{{ $errors->updateName->first('phone_local') }}</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label">البريد الإلكتروني</label>
|
<label class="form-label">البريد الإلكتروني</label>
|
||||||
<input type="email"
|
<input type="email"
|
||||||
|
|||||||
@ -20,14 +20,19 @@ Route::middleware('guest')->group(function () {
|
|||||||
$request->validate([
|
$request->validate([
|
||||||
'name' => 'required|string|max:255',
|
'name' => 'required|string|max:255',
|
||||||
'email' => 'required|email|unique:users,email',
|
'email' => 'required|email|unique:users,email',
|
||||||
|
'phone_country' => 'required|string|max:10',
|
||||||
|
'phone_local' => 'required|string|max:20',
|
||||||
'password' => 'required|confirmed|min:8',
|
'password' => 'required|confirmed|min:8',
|
||||||
|
], [
|
||||||
|
'phone_local.required' => 'رقم الهاتف مطلوب.',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$user = User::create([
|
$user = User::create([
|
||||||
'name' => $request->name,
|
'name' => $request->name,
|
||||||
'email' => $request->email,
|
'email' => $request->email,
|
||||||
|
'phone' => $request->phone_country . $request->phone_local,
|
||||||
'password' => Hash::make($request->password),
|
'password' => Hash::make($request->password),
|
||||||
'role' => 'user', // default
|
'role' => 'user',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
Auth::login($user);
|
Auth::login($user);
|
||||||
|
|||||||
@ -14,6 +14,9 @@ Route::middleware('auth')->group(function () {
|
|||||||
Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update');
|
Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update');
|
||||||
Route::patch('/profile/password', [ProfileController::class, 'updatePassword'])->name('profile.password');
|
Route::patch('/profile/password', [ProfileController::class, 'updatePassword'])->name('profile.password');
|
||||||
Route::get('/dashboard', [ProfileController::class, 'dashboard'])->name('user.dashboard');
|
Route::get('/dashboard', [ProfileController::class, 'dashboard'])->name('user.dashboard');
|
||||||
|
|
||||||
|
// Booking from the public landing page (web session auth)
|
||||||
|
Route::post('/reserve', [ProfileController::class, 'reserve'])->name('reserve');
|
||||||
});
|
});
|
||||||
|
|
||||||
// Admin Dashboard routes (protected - uncomment auth middleware for production)
|
// Admin Dashboard routes (protected - uncomment auth middleware for production)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user