latest working update

This commit is contained in:
Ghassan Yusuf 2026-04-16 19:42:37 +03:00
parent ba453d7b82
commit eb0d79d503
8 changed files with 592 additions and 20 deletions

View File

@ -0,0 +1,92 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\ParkingLot;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\Rules\Password;
class AdminOperatorController extends Controller
{
public function index()
{
$operators = User::where('role', 'operator')
->with('assignedLot')
->orderBy('name')
->get();
$lots = ParkingLot::orderBy('name')->get();
return view('admin.operators.index', compact('operators', 'lots'));
}
public function store(Request $request)
{
$data = $request->validate([
'name' => 'required|string|max:255',
'email' => 'required|email|unique:users,email',
'phone' => 'nullable|string|max:20',
'password' => ['required', Password::min(8)],
'parking_lot_id' => 'nullable|exists:parking_lots,id',
], [
'email.unique' => 'هذا البريد الإلكتروني مستخدم بالفعل.',
]);
User::create([
'name' => $data['name'],
'email' => $data['email'],
'phone' => $data['phone'] ?? null,
'password' => Hash::make($data['password']),
'role' => 'operator',
'parking_lot_id' => $data['parking_lot_id'] ?? null,
]);
return response()->json(['success' => true, 'message' => 'تم إنشاء حساب المشغّل بنجاح.']);
}
public function update(Request $request, User $operator)
{
if ($operator->role !== 'operator') {
return response()->json(['success' => false, 'message' => 'المستخدم ليس مشغّلاً.'], 403);
}
$data = $request->validate([
'name' => 'required|string|max:255',
'email' => 'required|email|unique:users,email,' . $operator->id,
'phone' => 'nullable|string|max:20',
'password' => ['nullable', Password::min(8)],
'parking_lot_id' => 'nullable|exists:parking_lots,id',
], [
'email.unique' => 'هذا البريد الإلكتروني مستخدم بالفعل.',
]);
$updates = [
'name' => $data['name'],
'email' => $data['email'],
'phone' => $data['phone'] ?? null,
'parking_lot_id' => $data['parking_lot_id'] ?? null,
];
if (!empty($data['password'])) {
$updates['password'] = Hash::make($data['password']);
}
$operator->update($updates);
return response()->json(['success' => true, 'message' => 'تم تحديث بيانات المشغّل.']);
}
public function destroy(User $operator)
{
if ($operator->role !== 'operator') {
return response()->json(['success' => false, 'message' => 'المستخدم ليس مشغّلاً.'], 403);
}
$operator->delete();
return response()->json(['success' => true, 'message' => 'تم حذف المشغّل.']);
}
}

View File

@ -7,6 +7,7 @@ use App\Models\Booking;
use App\Models\ParkingLot;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Storage;
use Carbon\Carbon;
@ -16,6 +17,9 @@ class OperatorController extends Controller
public function dashboard(Request $request): \Illuminate\View\View
{
$user = Auth::user();
$assignedLotId = $user->parking_lot_id; // null = no restriction (e.g. admin)
$rawLots = ParkingLot::active()->withStatus()->get();
$parkingLots = $rawLots->map(fn($lot) => [
@ -24,15 +28,28 @@ class OperatorController extends Controller
'address' => $lot->address,
'total' => $lot->total_capacity,
'avail' => max(0, $lot->total_capacity - ($lot->active_bookings_count + $lot->active_registries_count)),
'occupied'=> $lot->active_bookings_count + $lot->active_registries_count,
'occupied' => $lot->active_bookings_count + $lot->active_registries_count,
'price' => (float) $lot->price_per_hour,
'hours' => $lot->working_hours,
'lat' => (float) $lot->latitude,
'lng' => (float) $lot->longitude,
'image' => $lot->image ? Storage::url($lot->image) : null,
'locked' => $assignedLotId !== null && $lot->id !== $assignedLotId,
])->values();
// If operator has an assigned lot, force that lot
$selectedLotId = $request->get('lot_id');
if ($assignedLotId !== null) {
// Reject any attempt to view a different lot
if ($selectedLotId && (int) $selectedLotId !== $assignedLotId) {
return redirect()->route('operator.dashboard', ['lot_id' => $assignedLotId]);
}
// Auto-select assigned lot if nothing selected
if (!$selectedLotId) {
$selectedLotId = $assignedLotId;
}
}
$selectedLot = null;
$activeCars = collect();
$reservations = collect();
@ -56,7 +73,7 @@ class OperatorController extends Controller
}
return view('operator.dashboard', compact(
'parkingLots', 'selectedLot', 'activeCars', 'reservations', 'selectedLotId'
'parkingLots', 'selectedLot', 'activeCars', 'reservations', 'selectedLotId', 'assignedLotId'
));
}

View File

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

View File

@ -0,0 +1,27 @@
<?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->foreignId('parking_lot_id')
->nullable()
->after('role')
->constrained('parking_lots')
->nullOnDelete();
});
}
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropForeignIdFor(\App\Models\ParkingLot::class);
$table->dropColumn('parking_lot_id');
});
}
};

View File

@ -0,0 +1,392 @@
@extends('layouts.admin')
@section('title', 'إدارة المشغّلين — دمشق باركينغ')
@section('page-title', 'إدارة المشغّلين')
@section('styles')
<style>
.op-table th { font-size:.8rem; font-weight:700; color:#64748b; text-transform:uppercase; letter-spacing:.04em; white-space:nowrap; }
.op-table td { vertical-align:middle; font-size:.875rem; }
.op-avatar {
width:38px; height:38px; border-radius:50%;
background:rgba(99,102,241,.12); color:#6366f1;
display:flex; align-items:center; justify-content:center;
font-weight:800; font-size:.9rem; flex-shrink:0;
}
.lot-pill {
display:inline-flex; align-items:center; gap:.35rem;
padding:.25em .75em; border-radius:20px; font-size:.75rem; font-weight:700;
background:rgba(16,185,129,.1); color:#059669;
}
.no-lot-pill {
display:inline-flex; align-items:center; gap:.35rem;
padding:.25em .75em; border-radius:20px; font-size:.75rem; font-weight:600;
background:#f1f5f9; color:#94a3b8;
}
</style>
@endsection
@section('content')
<div class="d-flex align-items-center justify-content-between flex-wrap gap-3 mb-4">
<div>
<p class="mb-0" style="color:#64748b;font-size:.875rem;">إنشاء حسابات المشغّلين وتخصيص مواقف لهم</p>
</div>
<button id="btnOpenCreate" class="btn btn-primary fw-700" style="font-family:'Cairo',sans-serif;border-radius:.625rem;">
<i class="bi bi-person-plus me-1"></i>إضافة مشغّل
</button>
</div>
{{-- Operators table --}}
<div class="card border-0 shadow-sm" style="border-radius:1rem;overflow:hidden;">
<div class="table-responsive">
<table class="table op-table mb-0">
<thead style="background:#f8fafc;">
<tr>
<th class="px-4 py-3">المشغّل</th>
<th class="py-3">البريد الإلكتروني</th>
<th class="py-3">الهاتف</th>
<th class="py-3">الموقف المخصص</th>
<th class="py-3 text-center">إجراءات</th>
</tr>
</thead>
<tbody>
@forelse($operators as $op)
<tr>
<td class="px-4 py-3">
<div class="d-flex align-items-center gap-2">
<div class="op-avatar">{{ mb_substr($op->name, 0, 1) }}</div>
<span class="fw-600" style="color:#0f172a;">{{ $op->name }}</span>
</div>
</td>
<td class="py-3" style="color:#475569;">{{ $op->email }}</td>
<td class="py-3" style="color:#475569;">{{ $op->phone ?? '—' }}</td>
<td class="py-3">
@if($op->assignedLot)
<span class="lot-pill">
<i class="bi bi-buildings"></i>
{{ $op->assignedLot->name }}
</span>
@else
<span class="no-lot-pill">
<i class="bi bi-dash-circle"></i>
غير مخصص
</span>
@endif
</td>
<td class="py-3 text-center">
<div class="d-flex align-items-center justify-content-center gap-2">
<button class="btn btn-sm fw-600 btn-edit-op"
style="background:#f1f5f9;color:#475569;border:none;border-radius:.5rem;font-family:'Cairo',sans-serif;"
data-id="{{ $op->id }}"
data-name="{{ $op->name }}"
data-email="{{ $op->email }}"
data-phone="{{ $op->phone ?? '' }}"
data-lot="{{ $op->parking_lot_id ?? '' }}">
<i class="bi bi-pencil me-1"></i>تعديل
</button>
<button class="btn btn-sm fw-600 btn-delete-op"
style="background:rgba(239,68,68,.1);color:#ef4444;border:none;border-radius:.5rem;font-family:'Cairo',sans-serif;"
data-id="{{ $op->id }}"
data-name="{{ $op->name }}">
<i class="bi bi-trash me-1"></i>حذف
</button>
</div>
</td>
</tr>
@empty
<tr>
<td colspan="5" class="text-center py-5" style="color:#94a3b8;">
<i class="bi bi-people d-block mb-2" style="font-size:2.5rem;opacity:.3;"></i>
<span>لا يوجد مشغّلون بعد. أضف أول مشغّل الآن.</span>
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
</div>
{{-- ══ CREATE MODAL ══════════════════════════════════════════════════════════ --}}
<div class="modal fade" id="createModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content border-0" style="border-radius:1rem;">
<div class="modal-header border-0 pb-0">
<h5 class="modal-title fw-800" style="color:#0f172a;">إضافة مشغّل جديد</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body pt-3">
<div id="createError" class="alert alert-danger d-none border-0 rounded-3 py-2 mb-3" role="alert"></div>
<div class="mb-3">
<label class="form-label fw-600" style="font-size:.875rem;color:#374151;">الاسم الكامل</label>
<input type="text" id="createName" class="form-control" placeholder="مثال: أحمد محمد">
</div>
<div class="mb-3">
<label class="form-label fw-600" style="font-size:.875rem;color:#374151;">البريد الإلكتروني</label>
<input type="email" id="createEmail" class="form-control" placeholder="operator@example.com" dir="ltr">
</div>
<div class="mb-3">
<label class="form-label fw-600" style="font-size:.875rem;color:#374151;">رقم الهاتف <span style="color:#94a3b8;font-weight:400;">(اختياري)</span></label>
<input type="text" id="createPhone" class="form-control" placeholder="09xxxxxxxx">
</div>
<div class="mb-3">
<label class="form-label fw-600" style="font-size:.875rem;color:#374151;">كلمة المرور</label>
<input type="password" id="createPassword" class="form-control" placeholder="8 أحرف على الأقل" dir="ltr">
</div>
<div class="mb-1">
<label class="form-label fw-600" style="font-size:.875rem;color:#374151;">الموقف المخصص <span style="color:#94a3b8;font-weight:400;">(اختياري)</span></label>
<select id="createLot" class="form-select">
<option value=""> بدون تخصيص </option>
@foreach($lots as $lot)
<option value="{{ $lot->id }}">{{ $lot->name }}</option>
@endforeach
</select>
</div>
</div>
<div class="modal-footer border-0 pt-1">
<button type="button" class="btn fw-600"
style="background:#f1f5f9;color:#475569;border:none;border-radius:.625rem;font-family:'Cairo',sans-serif;"
data-bs-dismiss="modal">إلغاء</button>
<button type="button" class="btn btn-primary fw-700" id="createBtn"
style="border-radius:.625rem;font-family:'Cairo',sans-serif;">
<span id="createSpinner" class="spinner-border spinner-border-sm me-1 d-none"></span>
إنشاء الحساب
</button>
</div>
</div>
</div>
</div>
{{-- ══ EDIT MODAL ════════════════════════════════════════════════════════════ --}}
<div class="modal fade" id="editModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content border-0" style="border-radius:1rem;">
<div class="modal-header border-0 pb-0">
<h5 class="modal-title fw-800" style="color:#0f172a;">تعديل بيانات المشغّل</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body pt-3">
<div id="editError" class="alert alert-danger d-none border-0 rounded-3 py-2 mb-3" role="alert"></div>
<input type="hidden" id="editId">
<div class="mb-3">
<label class="form-label fw-600" style="font-size:.875rem;color:#374151;">الاسم الكامل</label>
<input type="text" id="editName" class="form-control">
</div>
<div class="mb-3">
<label class="form-label fw-600" style="font-size:.875rem;color:#374151;">البريد الإلكتروني</label>
<input type="email" id="editEmail" class="form-control" dir="ltr">
</div>
<div class="mb-3">
<label class="form-label fw-600" style="font-size:.875rem;color:#374151;">رقم الهاتف <span style="color:#94a3b8;font-weight:400;">(اختياري)</span></label>
<input type="text" id="editPhone" class="form-control">
</div>
<div class="mb-3">
<label class="form-label fw-600" style="font-size:.875rem;color:#374151;">كلمة مرور جديدة <span style="color:#94a3b8;font-weight:400;">(اتركها فارغة للإبقاء على الحالية)</span></label>
<input type="password" id="editPassword" class="form-control" placeholder="8 أحرف على الأقل" dir="ltr">
</div>
<div class="mb-1">
<label class="form-label fw-600" style="font-size:.875rem;color:#374151;">الموقف المخصص</label>
<select id="editLot" class="form-select">
<option value=""> بدون تخصيص </option>
@foreach($lots as $lot)
<option value="{{ $lot->id }}">{{ $lot->name }}</option>
@endforeach
</select>
</div>
</div>
<div class="modal-footer border-0 pt-1">
<button type="button" class="btn fw-600"
style="background:#f1f5f9;color:#475569;border:none;border-radius:.625rem;font-family:'Cairo',sans-serif;"
data-bs-dismiss="modal">إلغاء</button>
<button type="button" class="btn btn-primary fw-700" id="editBtn"
style="border-radius:.625rem;font-family:'Cairo',sans-serif;">
<span id="editSpinner" class="spinner-border spinner-border-sm me-1 d-none"></span>
حفظ التغييرات
</button>
</div>
</div>
</div>
</div>
{{-- ══ DELETE MODAL ══════════════════════════════════════════════════════════ --}}
<div class="modal fade" id="deleteModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered modal-sm">
<div class="modal-content border-0" style="border-radius:1rem;">
<div class="modal-header border-0 pb-0">
<h5 class="modal-title fw-800" style="color:#0f172a;">حذف المشغّل</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body text-center py-3">
<div style="width:56px;height:56px;background:rgba(239,68,68,.1);border-radius:50%;display:flex;align-items:center;justify-content:center;margin:0 auto 1rem;">
<i class="bi bi-person-x-fill" style="font-size:1.6rem;color:#ef4444;"></i>
</div>
<p class="mb-1" style="color:#374151;font-size:.9rem;">هل تريد حذف حساب</p>
<p class="fw-800 mb-0" style="color:#0f172a;" id="deleteOpName"></p>
<p class="mt-2 mb-0" style="color:#94a3b8;font-size:.8rem;">لا يمكن التراجع عن هذا الإجراء.</p>
</div>
<div class="modal-footer border-0 pt-0 justify-content-center gap-2">
<button type="button" class="btn fw-600"
style="background:#f1f5f9;color:#475569;border:none;border-radius:.625rem;font-family:'Cairo',sans-serif;"
data-bs-dismiss="modal">إلغاء</button>
<button type="button" class="btn fw-700" id="deleteBtn"
style="background:#ef4444;color:#fff;border:none;border-radius:.625rem;font-family:'Cairo',sans-serif;">
<span id="deleteSpinner" class="spinner-border spinner-border-sm me-1 d-none"></span>
حذف
</button>
</div>
</div>
</div>
</div>
{{-- Toast --}}
<div id="opToast" class="toast align-items-center border-0 position-fixed bottom-0 end-0 m-3"
style="z-index:9999;min-width:260px;" role="alert">
<div class="d-flex">
<div class="toast-body fw-600" id="opToastMsg"></div>
<button type="button" class="btn-close me-2 m-auto" data-bs-dismiss="toast"></button>
</div>
</div>
@endsection
@push('scripts')
<script>
document.addEventListener('DOMContentLoaded', function () {
const csrf = document.querySelector('meta[name="csrf-token"]').content;
// ── Toast ─────────────────────────────────────────────────────────────────
function showToast(msg, success) {
const el = document.getElementById('opToast');
document.getElementById('opToastMsg').textContent = msg;
el.classList.remove('text-bg-success', 'text-bg-danger');
el.classList.add(success !== false ? 'text-bg-success' : 'text-bg-danger');
bootstrap.Toast.getOrCreateInstance(el, { delay: 3500 }).show();
}
// ── Modals (lazy) ─────────────────────────────────────────────────────────
function modal(id) { return bootstrap.Modal.getOrCreateInstance(document.getElementById(id)); }
// ── Open create ───────────────────────────────────────────────────────────
document.getElementById('btnOpenCreate').addEventListener('click', function () {
['createName','createEmail','createPhone','createPassword'].forEach(id => document.getElementById(id).value = '');
document.getElementById('createLot').value = '';
document.getElementById('createError').classList.add('d-none');
modal('createModal').show();
});
// ── Open edit ─────────────────────────────────────────────────────────────
document.querySelectorAll('.btn-edit-op').forEach(btn => {
btn.addEventListener('click', function () {
document.getElementById('editId').value = this.dataset.id;
document.getElementById('editName').value = this.dataset.name;
document.getElementById('editEmail').value = this.dataset.email;
document.getElementById('editPhone').value = this.dataset.phone || '';
document.getElementById('editPassword').value = '';
document.getElementById('editLot').value = this.dataset.lot || '';
document.getElementById('editError').classList.add('d-none');
modal('editModal').show();
});
});
// ── Open delete ───────────────────────────────────────────────────────────
let pendingDeleteId = null;
document.querySelectorAll('.btn-delete-op').forEach(btn => {
btn.addEventListener('click', function () {
pendingDeleteId = this.dataset.id;
document.getElementById('deleteOpName').textContent = this.dataset.name;
modal('deleteModal').show();
});
});
// ── Submit create ─────────────────────────────────────────────────────────
document.getElementById('createBtn').addEventListener('click', async function () {
const btn = this;
const spinner = document.getElementById('createSpinner');
const errEl = document.getElementById('createError');
btn.disabled = true;
spinner.classList.remove('d-none');
errEl.classList.add('d-none');
try {
const res = await fetch('{{ route("admin.operators.store") }}', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': csrf },
body: JSON.stringify({
name: document.getElementById('createName').value.trim(),
email: document.getElementById('createEmail').value.trim(),
phone: document.getElementById('createPhone').value.trim() || null,
password: document.getElementById('createPassword').value,
parking_lot_id: document.getElementById('createLot').value || null,
}),
});
const json = await res.json();
if (json.success) {
modal('createModal').hide();
showToast(json.message);
setTimeout(() => location.reload(), 800);
} else {
errEl.textContent = json.message || 'حدث خطأ.';
errEl.classList.remove('d-none');
}
} catch { errEl.textContent = 'تعذّر الاتصال بالخادم.'; errEl.classList.remove('d-none'); }
finally { btn.disabled = false; spinner.classList.add('d-none'); }
});
// ── Submit edit ───────────────────────────────────────────────────────────
document.getElementById('editBtn').addEventListener('click', async function () {
const id = document.getElementById('editId').value;
const btn = this;
const spinner = document.getElementById('editSpinner');
const errEl = document.getElementById('editError');
btn.disabled = true;
spinner.classList.remove('d-none');
errEl.classList.add('d-none');
try {
const res = await fetch(`/admin/operators/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': csrf },
body: JSON.stringify({
name: document.getElementById('editName').value.trim(),
email: document.getElementById('editEmail').value.trim(),
phone: document.getElementById('editPhone').value.trim() || null,
password: document.getElementById('editPassword').value || null,
parking_lot_id: document.getElementById('editLot').value || null,
}),
});
const json = await res.json();
if (json.success) {
modal('editModal').hide();
showToast(json.message);
setTimeout(() => location.reload(), 800);
} else {
errEl.textContent = json.message || 'حدث خطأ.';
errEl.classList.remove('d-none');
}
} catch { errEl.textContent = 'تعذّر الاتصال بالخادم.'; errEl.classList.remove('d-none'); }
finally { btn.disabled = false; spinner.classList.add('d-none'); }
});
// ── Submit delete ─────────────────────────────────────────────────────────
document.getElementById('deleteBtn').addEventListener('click', async function () {
if (!pendingDeleteId) return;
const btn = this;
const spinner = document.getElementById('deleteSpinner');
btn.disabled = true;
spinner.classList.remove('d-none');
try {
const res = await fetch(`/admin/operators/${pendingDeleteId}`, {
method: 'DELETE',
headers: { 'X-CSRF-TOKEN': csrf },
});
const json = await res.json();
modal('deleteModal').hide();
showToast(json.message, json.success);
if (json.success) setTimeout(() => location.reload(), 800);
} catch { showToast('تعذّر الاتصال بالخادم.', false); }
finally { btn.disabled = false; spinner.classList.add('d-none'); }
});
}); // DOMContentLoaded
</script>
@endpush

View File

@ -60,6 +60,12 @@
<div class="sidebar-section">التشغيل</div>
<a href="{{ route('admin.operators.index') }}"
class="sidebar-link {{ request()->routeIs('admin.operators.*') ? 'active' : '' }}">
<i class="bi bi-people sidebar-icon"></i>
<span>المشغّلون</span>
</a>
<a href="{{ route('operator.dashboard') }}"
class="sidebar-link {{ request()->routeIs('operator.*') ? 'active' : '' }}">
<i class="bi bi-person-badge sidebar-icon"></i>
@ -180,6 +186,11 @@
<i class="bi bi-calendar-check"></i>
<span>الحجوزات</span>
</a>
<a href="{{ route('admin.operators.index') }}"
class="mob-nav-item {{ request()->routeIs('admin.operators.*') ? 'active' : '' }}">
<i class="bi bi-people"></i>
<span>المشغّلون</span>
</a>
<a href="{{ route('operator.dashboard') }}"
class="mob-nav-item {{ request()->routeIs('operator.*') ? 'active' : '' }}">
<i class="bi bi-person-badge"></i>

View File

@ -20,6 +20,13 @@
transform:translateY(-8px);
box-shadow:0 18px 40px rgba(0,0,0,.13);
}
.lot-portrait-card.lot-locked {
cursor:not-allowed;
}
.lot-portrait-card.lot-locked:hover {
transform:none;
box-shadow:0 6px 20px rgba(0,0,0,.07);
}
/* Image area */
.lot-card-img-wrap {
@ -185,6 +192,7 @@ $gradients = [
$pct = $lot['total'] > 0 ? round($lot['occupied'] / $lot['total'] * 100) : 0;
$avail = $lot['avail'];
$gradient = $gradients[$lot['id'] % 6];
$locked = $lot['locked'] ?? false;
if ($avail === 0) { $badgeColor='rgba(239,68,68,.82)'; $badgeTxt='ممتلئ'; $barCol='#ef4444'; }
elseif ($avail < $lot['total'] * 0.2) { $badgeColor='rgba(245,158,11,.82)'; $badgeTxt=$avail.' مكان'; $barCol='#f59e0b'; }
else { $badgeColor='rgba(16,185,129,.82)'; $badgeTxt=$avail.' متاح'; $barCol='#10b981'; }
@ -193,25 +201,34 @@ $gradients = [
data-name="{{ mb_strtolower($lot['name']) }}"
data-address="{{ mb_strtolower($lot['address']) }}">
<div class="lot-portrait-card" onclick="selectLot({{ $lot['id'] }})">
<div class="lot-portrait-card {{ $locked ? 'lot-locked' : '' }}"
@if(!$locked) onclick="selectLot({{ $lot['id'] }})" @endif>
{{-- Image or gradient placeholder --}}
<div class="lot-card-img-wrap">
@if($lot['image'])
<img src="{{ $lot['image'] }}" alt="{{ $lot['name'] }}">
<img src="{{ $lot['image'] }}" alt="{{ $lot['name'] }}"
style="{{ $locked ? 'filter:grayscale(1);opacity:.55;' : '' }}">
@else
<div class="lot-card-placeholder" style="background:{{ $gradient }};">
<div class="lot-card-placeholder" style="background:{{ $gradient }};{{ $locked ? 'filter:grayscale(1);opacity:.55;' : '' }}">
<i class="bi bi-buildings"></i>
<span>{{ $lot['name'] }}</span>
</div>
@endif
@if($locked)
<div style="position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;background:rgba(15,23,42,.45);gap:.4rem;">
<i class="bi bi-lock-fill" style="font-size:2rem;color:#fff;opacity:.9;"></i>
<span style="color:#fff;font-size:.75rem;font-weight:700;">غير مخصص لك</span>
</div>
@else
<span class="lot-card-avail-badge" style="background:{{ $badgeColor }};">
{{ $badgeTxt }}
</span>
@endif
</div>
{{-- Card body --}}
<div class="lot-card-body">
<div class="lot-card-body" style="{{ $locked ? 'opacity:.5;' : '' }}">
<div class="lot-card-title">{{ $lot['name'] }}</div>
<div class="lot-card-address">
<i class="bi bi-geo-alt flex-shrink-0"></i>
@ -228,11 +245,13 @@ $gradients = [
<span><i class="bi bi-clock"></i>{{ $lot['hours'] }}</span>
</div>
@if(!$locked)
<div class="lot-card-select-wrap">
<button class="lot-card-select-btn" onclick="event.stopPropagation();selectLot({{ $lot['id'] }})">
<i class="bi bi-check2-circle me-1"></i>اختر هذا الموقف
</button>
</div>
@endif
</div>
</div>
@ -271,11 +290,13 @@ $gradients = [
<div class="d-flex align-items-center gap-3">
<span class="badge badge-soft-success">{{ $selectedLot->available_spaces }} متاح</span>
<span class="badge badge-soft-warning">{{ $selectedLot->occupied_spaces }} مشغول</span>
@if(!$assignedLotId)
<a href="{{ route('operator.dashboard') }}"
class="btn btn-sm fw-600"
style="background:#f1f5f9;color:#475569;border:none;border-radius:.5rem;font-family:'Cairo',sans-serif;">
<i class="bi bi-arrow-repeat me-1"></i>تغيير الموقف
</a>
@endif
<span class="badge badge-soft-secondary text-xs" id="refresh-badge"></span>
</div>
</div>

View File

@ -38,6 +38,12 @@ Route::prefix('admin')->middleware('admin')->name('admin.')->group(function () {
Route::post('/parking-lots/{parkingLot}/toggle', [\App\Http\Controllers\Admin\ParkingLotController::class, 'toggleStatus'])->name('parking-lots.toggle');
Route::delete('/parking-lots/{parkingLot}', [\App\Http\Controllers\Admin\ParkingLotController::class, 'destroy'])->name('parking-lots.destroy');
// Operators management
Route::get('/operators', [\App\Http\Controllers\Admin\AdminOperatorController::class, 'index'])->name('operators.index');
Route::post('/operators', [\App\Http\Controllers\Admin\AdminOperatorController::class, 'store'])->name('operators.store');
Route::put('/operators/{operator}', [\App\Http\Controllers\Admin\AdminOperatorController::class, 'update'])->name('operators.update');
Route::delete('/operators/{operator}', [\App\Http\Controllers\Admin\AdminOperatorController::class, 'destroy'])->name('operators.destroy');
// Active Bookings
Route::get('/bookings/active', [\App\Http\Controllers\Admin\BookingController::class, 'activeIndex'])->name('bookings.active');
Route::get('/bookings/{booking}/checkout-preview', [\App\Http\Controllers\Admin\BookingController::class, 'checkoutPreview'])->name('bookings.checkout-preview');