latest working update
This commit is contained in:
parent
ba453d7b82
commit
eb0d79d503
92
app/Http/Controllers/Admin/AdminOperatorController.php
Normal file
92
app/Http/Controllers/Admin/AdminOperatorController.php
Normal 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' => 'تم حذف المشغّل.']);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -7,6 +7,7 @@ use App\Models\Booking;
|
|||||||
use App\Models\ParkingLot;
|
use App\Models\ParkingLot;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
use Illuminate\Support\Facades\Storage;
|
use Illuminate\Support\Facades\Storage;
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
|
|
||||||
@ -16,23 +17,39 @@ class OperatorController extends Controller
|
|||||||
|
|
||||||
public function dashboard(Request $request): \Illuminate\View\View
|
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();
|
$rawLots = ParkingLot::active()->withStatus()->get();
|
||||||
|
|
||||||
$parkingLots = $rawLots->map(fn($lot) => [
|
$parkingLots = $rawLots->map(fn($lot) => [
|
||||||
'id' => $lot->id,
|
'id' => $lot->id,
|
||||||
'name' => $lot->name,
|
'name' => $lot->name,
|
||||||
'address' => $lot->address,
|
'address' => $lot->address,
|
||||||
'total' => $lot->total_capacity,
|
'total' => $lot->total_capacity,
|
||||||
'avail' => max(0, $lot->total_capacity - ($lot->active_bookings_count + $lot->active_registries_count)),
|
'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,
|
'price' => (float) $lot->price_per_hour,
|
||||||
'hours' => $lot->working_hours,
|
'hours' => $lot->working_hours,
|
||||||
'lat' => (float) $lot->latitude,
|
'lat' => (float) $lot->latitude,
|
||||||
'lng' => (float) $lot->longitude,
|
'lng' => (float) $lot->longitude,
|
||||||
'image' => $lot->image ? Storage::url($lot->image) : null,
|
'image' => $lot->image ? Storage::url($lot->image) : null,
|
||||||
|
'locked' => $assignedLotId !== null && $lot->id !== $assignedLotId,
|
||||||
])->values();
|
])->values();
|
||||||
|
|
||||||
$selectedLotId = $request->get('lot_id');
|
// 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;
|
$selectedLot = null;
|
||||||
$activeCars = collect();
|
$activeCars = collect();
|
||||||
$reservations = collect();
|
$reservations = collect();
|
||||||
@ -56,7 +73,7 @@ class OperatorController extends Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
return view('operator.dashboard', compact(
|
return view('operator.dashboard', compact(
|
||||||
'parkingLots', 'selectedLot', 'activeCars', 'reservations', 'selectedLotId'
|
'parkingLots', 'selectedLot', 'activeCars', 'reservations', 'selectedLotId', 'assignedLotId'
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -24,6 +24,7 @@ class User extends Authenticatable
|
|||||||
'phone',
|
'phone',
|
||||||
'password',
|
'password',
|
||||||
'role',
|
'role',
|
||||||
|
'parking_lot_id',
|
||||||
];
|
];
|
||||||
|
|
||||||
protected $casts = [
|
protected $casts = [
|
||||||
@ -54,4 +55,9 @@ class User extends Authenticatable
|
|||||||
'password' => 'hashed',
|
'password' => 'hashed',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function assignedLot(): \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(ParkingLot::class, 'parking_lot_id');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
392
resources/views/admin/operators/index.blade.php
Normal file
392
resources/views/admin/operators/index.blade.php
Normal 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
|
||||||
@ -60,6 +60,12 @@
|
|||||||
|
|
||||||
<div class="sidebar-section">التشغيل</div>
|
<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') }}"
|
<a href="{{ route('operator.dashboard') }}"
|
||||||
class="sidebar-link {{ request()->routeIs('operator.*') ? 'active' : '' }}">
|
class="sidebar-link {{ request()->routeIs('operator.*') ? 'active' : '' }}">
|
||||||
<i class="bi bi-person-badge sidebar-icon"></i>
|
<i class="bi bi-person-badge sidebar-icon"></i>
|
||||||
@ -180,6 +186,11 @@
|
|||||||
<i class="bi bi-calendar-check"></i>
|
<i class="bi bi-calendar-check"></i>
|
||||||
<span>الحجوزات</span>
|
<span>الحجوزات</span>
|
||||||
</a>
|
</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') }}"
|
<a href="{{ route('operator.dashboard') }}"
|
||||||
class="mob-nav-item {{ request()->routeIs('operator.*') ? 'active' : '' }}">
|
class="mob-nav-item {{ request()->routeIs('operator.*') ? 'active' : '' }}">
|
||||||
<i class="bi bi-person-badge"></i>
|
<i class="bi bi-person-badge"></i>
|
||||||
|
|||||||
@ -20,6 +20,13 @@
|
|||||||
transform:translateY(-8px);
|
transform:translateY(-8px);
|
||||||
box-shadow:0 18px 40px rgba(0,0,0,.13);
|
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 */
|
/* Image area */
|
||||||
.lot-card-img-wrap {
|
.lot-card-img-wrap {
|
||||||
@ -185,6 +192,7 @@ $gradients = [
|
|||||||
$pct = $lot['total'] > 0 ? round($lot['occupied'] / $lot['total'] * 100) : 0;
|
$pct = $lot['total'] > 0 ? round($lot['occupied'] / $lot['total'] * 100) : 0;
|
||||||
$avail = $lot['avail'];
|
$avail = $lot['avail'];
|
||||||
$gradient = $gradients[$lot['id'] % 6];
|
$gradient = $gradients[$lot['id'] % 6];
|
||||||
|
$locked = $lot['locked'] ?? false;
|
||||||
if ($avail === 0) { $badgeColor='rgba(239,68,68,.82)'; $badgeTxt='ممتلئ'; $barCol='#ef4444'; }
|
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'; }
|
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'; }
|
else { $badgeColor='rgba(16,185,129,.82)'; $badgeTxt=$avail.' متاح'; $barCol='#10b981'; }
|
||||||
@ -193,25 +201,34 @@ $gradients = [
|
|||||||
data-name="{{ mb_strtolower($lot['name']) }}"
|
data-name="{{ mb_strtolower($lot['name']) }}"
|
||||||
data-address="{{ mb_strtolower($lot['address']) }}">
|
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 --}}
|
{{-- Image or gradient placeholder --}}
|
||||||
<div class="lot-card-img-wrap">
|
<div class="lot-card-img-wrap">
|
||||||
@if($lot['image'])
|
@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
|
@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>
|
<i class="bi bi-buildings"></i>
|
||||||
<span>{{ $lot['name'] }}</span>
|
<span>{{ $lot['name'] }}</span>
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
<span class="lot-card-avail-badge" style="background:{{ $badgeColor }};">
|
@if($locked)
|
||||||
{{ $badgeTxt }}
|
<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;">
|
||||||
</span>
|
<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>
|
</div>
|
||||||
|
|
||||||
{{-- Card body --}}
|
{{-- 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-title">{{ $lot['name'] }}</div>
|
||||||
<div class="lot-card-address">
|
<div class="lot-card-address">
|
||||||
<i class="bi bi-geo-alt flex-shrink-0"></i>
|
<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>
|
<span><i class="bi bi-clock"></i>{{ $lot['hours'] }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@if(!$locked)
|
||||||
<div class="lot-card-select-wrap">
|
<div class="lot-card-select-wrap">
|
||||||
<button class="lot-card-select-btn" onclick="event.stopPropagation();selectLot({{ $lot['id'] }})">
|
<button class="lot-card-select-btn" onclick="event.stopPropagation();selectLot({{ $lot['id'] }})">
|
||||||
<i class="bi bi-check2-circle me-1"></i>اختر هذا الموقف
|
<i class="bi bi-check2-circle me-1"></i>اختر هذا الموقف
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@endif
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@ -271,11 +290,13 @@ $gradients = [
|
|||||||
<div class="d-flex align-items-center gap-3">
|
<div class="d-flex align-items-center gap-3">
|
||||||
<span class="badge badge-soft-success">{{ $selectedLot->available_spaces }} متاح</span>
|
<span class="badge badge-soft-success">{{ $selectedLot->available_spaces }} متاح</span>
|
||||||
<span class="badge badge-soft-warning">{{ $selectedLot->occupied_spaces }} مشغول</span>
|
<span class="badge badge-soft-warning">{{ $selectedLot->occupied_spaces }} مشغول</span>
|
||||||
|
@if(!$assignedLotId)
|
||||||
<a href="{{ route('operator.dashboard') }}"
|
<a href="{{ route('operator.dashboard') }}"
|
||||||
class="btn btn-sm fw-600"
|
class="btn btn-sm fw-600"
|
||||||
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;">
|
||||||
<i class="bi bi-arrow-repeat me-1"></i>تغيير الموقف
|
<i class="bi bi-arrow-repeat me-1"></i>تغيير الموقف
|
||||||
</a>
|
</a>
|
||||||
|
@endif
|
||||||
<span class="badge badge-soft-secondary text-xs" id="refresh-badge"></span>
|
<span class="badge badge-soft-secondary text-xs" id="refresh-badge"></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -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::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');
|
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
|
// Active Bookings
|
||||||
Route::get('/bookings/active', [\App\Http\Controllers\Admin\BookingController::class, 'activeIndex'])->name('bookings.active');
|
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');
|
Route::get('/bookings/{booking}/checkout-preview', [\App\Http\Controllers\Admin\BookingController::class, 'checkoutPreview'])->name('bookings.checkout-preview');
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user