backup: before auth-required booking and phone number feature
This commit is contained in:
parent
83d9a67188
commit
900eb24b37
@ -15,7 +15,11 @@
|
||||
"Bash(git reset:*)",
|
||||
"Bash(php -l app/Http/Controllers/Admin/ParkingLotController.php)",
|
||||
"Bash(php -l app/Http/Requests/StoreParkingLotRequest.php)",
|
||||
"Bash(php -l app/Http/Controllers/Admin/BookingController.php)"
|
||||
"Bash(php -l app/Http/Controllers/Admin/BookingController.php)",
|
||||
"Bash(php -r ':*)",
|
||||
"Bash(php -r \"echo file_put_contents\\('C:/xampp/htdocs/scp-syria/storage/app/public/test.txt', 'ok'\\) ? 'write OK' : 'write FAIL';\")",
|
||||
"Bash(curl:*)",
|
||||
"Bash(php:*)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@ -52,6 +52,10 @@ class ParkingLotController extends Controller
|
||||
{
|
||||
$data = $request->validated();
|
||||
|
||||
// Never let validated() overwrite the stored image path with null.
|
||||
// Only update the image column when a new file is actually uploaded.
|
||||
unset($data['image']);
|
||||
|
||||
if ($request->hasFile('image')) {
|
||||
if ($parkingLot->image) {
|
||||
Storage::disk('public')->delete($parkingLot->image);
|
||||
@ -91,6 +95,38 @@ class ParkingLotController extends Controller
|
||||
]);
|
||||
}
|
||||
|
||||
public function updatePricing(Request $request, ParkingLot $parkingLot): JsonResponse
|
||||
{
|
||||
$data = $request->validate([
|
||||
'price_per_hour' => 'required|numeric|min:0|max:100000',
|
||||
'pricing_rules' => 'nullable|array',
|
||||
'pricing_rules.*'=> 'nullable|numeric|min:0|max:100000',
|
||||
]);
|
||||
|
||||
// Build clean rules array: only include days that differ from base price.
|
||||
// Keys 1-7 (Mon-Sun). Null or empty string means "use base price".
|
||||
$rules = [];
|
||||
foreach ($data['pricing_rules'] ?? [] as $day => $rate) {
|
||||
if ($rate !== null && $rate !== '' && (float) $rate !== (float) $data['price_per_hour']) {
|
||||
$rules[(int) $day] = (float) $rate;
|
||||
}
|
||||
}
|
||||
|
||||
$parkingLot->update([
|
||||
'price_per_hour' => $data['price_per_hour'],
|
||||
'pricing_rules' => empty($rules) ? null : $rules,
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'تم حفظ أسعار الموقف بنجاح',
|
||||
'data' => [
|
||||
'price_per_hour' => (float) $parkingLot->price_per_hour,
|
||||
'pricing_rules' => $parkingLot->pricing_rules,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function toggleStatus(ParkingLot $parkingLot): JsonResponse
|
||||
{
|
||||
$parkingLot->update(['is_active' => !$parkingLot->is_active]);
|
||||
|
||||
@ -18,6 +18,9 @@ class BookingController extends Controller
|
||||
{
|
||||
$validated = $request->validated();
|
||||
|
||||
$lot = \App\Models\ParkingLot::find($validated['parking_lot_id']);
|
||||
$validated['pricing_snapshot'] = $lot?->pricingSnapshot();
|
||||
|
||||
$booking = \App\Models\Booking::create($validated);
|
||||
|
||||
// Note: In production, create CarRegistry here for the booking car
|
||||
|
||||
@ -80,14 +80,15 @@ class OperatorController extends Controller
|
||||
}
|
||||
|
||||
$booking = Booking::create([
|
||||
'parking_lot_id' => $lot->id,
|
||||
'vehicle_plate' => $data['vehicle_plate'],
|
||||
'customer_name' => $data['customer_name'] ?? null,
|
||||
'phone' => $data['phone'] ?? null,
|
||||
'source' => 'walk_in',
|
||||
'start_time' => now(),
|
||||
'end_time' => now()->addHours($data['duration_hours']),
|
||||
'status' => 'active',
|
||||
'parking_lot_id' => $lot->id,
|
||||
'vehicle_plate' => $data['vehicle_plate'],
|
||||
'customer_name' => $data['customer_name'] ?? null,
|
||||
'phone' => $data['phone'] ?? null,
|
||||
'source' => 'walk_in',
|
||||
'start_time' => now(),
|
||||
'end_time' => now()->addHours($data['duration_hours']),
|
||||
'status' => 'active',
|
||||
'pricing_snapshot' => $lot->pricingSnapshot(),
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
@ -128,7 +129,7 @@ class OperatorController extends Controller
|
||||
$start = $booking->start_time;
|
||||
$end = now();
|
||||
$duration = $start->diffInMinutes($end);
|
||||
$calc = $lot->calculateFee($start, $end);
|
||||
$calc = $lot->calculateFee($start, $end, $booking->pricing_snapshot);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
@ -163,7 +164,7 @@ class OperatorController extends Controller
|
||||
$lot = $booking->parkingLot;
|
||||
$start = $booking->start_time;
|
||||
$end = now();
|
||||
$calc = $lot->calculateFee($start, $end);
|
||||
$calc = $lot->calculateFee($start, $end, $booking->pricing_snapshot);
|
||||
|
||||
$proofPath = null;
|
||||
if ($request->hasFile('payment_proof')) {
|
||||
|
||||
@ -25,13 +25,15 @@ class Booking extends Model
|
||||
'payment_method',
|
||||
'payment_proof',
|
||||
'paid_at',
|
||||
'pricing_snapshot',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'start_time' => 'datetime',
|
||||
'end_time' => 'datetime',
|
||||
'paid_at' => 'datetime',
|
||||
'total_fee' => 'decimal:2',
|
||||
'total_fee' => 'decimal:2',
|
||||
'pricing_snapshot' => 'array',
|
||||
];
|
||||
|
||||
public function parkingLot(): BelongsTo
|
||||
|
||||
@ -81,15 +81,33 @@ class ParkingLot extends Model
|
||||
return $activeBookings + $activeRegistries;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the pricing snapshot array to store on a booking at creation time.
|
||||
* Format: ['base' => float, 'rules' => [1 => float, ..., 7 => float]]
|
||||
*/
|
||||
public function pricingSnapshot(): array
|
||||
{
|
||||
return [
|
||||
'base' => (float) $this->price_per_hour,
|
||||
'rules' => $this->pricing_rules ?? [],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate fee and per-day breakdown between two timestamps.
|
||||
* Uses pricing_rules (ISO weekday keys 1–7) if set, otherwise price_per_hour.
|
||||
*
|
||||
* Pass a $snapshot (from booking->pricing_snapshot) to use the rates that were
|
||||
* in effect when the booking was created, guaranteeing price changes never
|
||||
* retroactively affect historical or in-progress bookings.
|
||||
*
|
||||
* Returns ['total' => float, 'details' => [['day','date','hours','rate','subtotal'], ...]]
|
||||
*/
|
||||
public function calculateFee(Carbon $start, Carbon $end): array
|
||||
public function calculateFee(Carbon $start, Carbon $end, ?array $snapshot = null): array
|
||||
{
|
||||
$rules = $this->pricing_rules ?? [];
|
||||
$base = $snapshot ? (float) ($snapshot['base'] ?? $this->price_per_hour)
|
||||
: (float) $this->price_per_hour;
|
||||
$rules = $snapshot ? ($snapshot['rules'] ?? [])
|
||||
: ($this->pricing_rules ?? []);
|
||||
$details = [];
|
||||
$total = 0.0;
|
||||
$cursor = $start->copy()->seconds(0);
|
||||
@ -99,7 +117,7 @@ class ParkingLot extends Model
|
||||
$segEnd = ($dayEnd < $end) ? $dayEnd : $end;
|
||||
|
||||
$dow = (int) $cursor->format('N'); // 1=Mon … 7=Sun
|
||||
$rate = isset($rules[$dow]) ? (float) $rules[$dow] : (float) $this->price_per_hour;
|
||||
$rate = isset($rules[$dow]) ? (float) $rules[$dow] : $base;
|
||||
$hours = round($cursor->diffInMinutes($segEnd) / 60, 4);
|
||||
$subtotal = $hours * $rate;
|
||||
|
||||
|
||||
@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('bookings', function (Blueprint $table) {
|
||||
// Stores {base: float, rules: {1:float, ..., 7:float}} at booking creation.
|
||||
// Checkout fee calculation uses this snapshot so price changes never affect
|
||||
// in-progress or historical bookings.
|
||||
$table->json('pricing_snapshot')->nullable()->after('paid_at');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('bookings', function (Blueprint $table) {
|
||||
$table->dropColumn('pricing_snapshot');
|
||||
});
|
||||
}
|
||||
};
|
||||
3
resources/js/bootstrap.js
vendored
3
resources/js/bootstrap.js
vendored
@ -1,4 +1,5 @@
|
||||
import 'bootstrap';
|
||||
import * as bootstrap from 'bootstrap';
|
||||
window.bootstrap = bootstrap;
|
||||
|
||||
import axios from 'axios';
|
||||
window.axios = axios;
|
||||
|
||||
@ -5,11 +5,91 @@
|
||||
@section('styles')
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
|
||||
<style>
|
||||
#lotsMap { height: 380px; border-radius: .5rem; z-index: 0; }
|
||||
#modalMap { height: 280px; border-radius: .5rem; z-index: 0; }
|
||||
.map-hint { font-size: .78rem; color: #64748b; margin-top: .35rem; }
|
||||
/* keep Leaflet tiles crisp inside RTL layout */
|
||||
#modalMap { height: 280px; border-radius: .5rem; z-index: 0; }
|
||||
.map-hint { font-size: .78rem; color: #64748b; margin-top: .35rem; }
|
||||
.leaflet-container { direction: ltr; }
|
||||
|
||||
/* lot portrait cards */
|
||||
.lot-admin-card {
|
||||
background: #fff;
|
||||
border-radius: .75rem;
|
||||
border: 1px solid #e2e8f0;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: box-shadow .2s, transform .2s;
|
||||
}
|
||||
.lot-admin-card:hover {
|
||||
box-shadow: 0 8px 24px rgba(99,102,241,.12);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
.lot-card-img-wrap {
|
||||
position: relative;
|
||||
height: 160px;
|
||||
flex-shrink: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
.lot-card-img-wrap img {
|
||||
width: 100%; height: 100%; object-fit: cover;
|
||||
}
|
||||
.lot-card-placeholder {
|
||||
width: 100%; height: 100%;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
}
|
||||
.lot-card-placeholder i { font-size: 2.5rem; color: rgba(255,255,255,.6); }
|
||||
.lot-status-dot {
|
||||
position: absolute; top: .6rem; left: .6rem;
|
||||
width: 10px; height: 10px; border-radius: 50%;
|
||||
border: 2px solid #fff;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,.3);
|
||||
}
|
||||
.lot-active-badge {
|
||||
position: absolute; top: .55rem; right: .55rem;
|
||||
background: rgba(0,0,0,.45); color: #fff;
|
||||
font-size: .68rem; font-weight: 700;
|
||||
padding: .2rem .5rem; border-radius: 2rem;
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
.lot-card-body {
|
||||
padding: .9rem 1rem .5rem;
|
||||
flex: 1;
|
||||
}
|
||||
.lot-card-body h6 {
|
||||
font-size: .92rem; font-weight: 700; color: #0f172a;
|
||||
margin-bottom: .2rem;
|
||||
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
||||
}
|
||||
.lot-card-address {
|
||||
font-size: .75rem; color: #64748b;
|
||||
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
||||
margin-bottom: .65rem;
|
||||
}
|
||||
.lot-card-stats {
|
||||
display: flex; gap: .4rem; flex-wrap: wrap; margin-bottom: .5rem;
|
||||
}
|
||||
.lot-card-stat {
|
||||
background: #f8fafc; border: 1px solid #e2e8f0;
|
||||
border-radius: .375rem; padding: .2rem .5rem;
|
||||
font-size: .72rem; color: #475569; white-space: nowrap;
|
||||
}
|
||||
.lot-card-stat strong { color: #0f172a; }
|
||||
.lot-card-footer {
|
||||
padding: .6rem 1rem;
|
||||
border-top: 1px solid #f1f5f9;
|
||||
display: flex; justify-content: flex-end; gap: .35rem;
|
||||
}
|
||||
.lot-action-btn {
|
||||
width: 30px; height: 30px; border: none; border-radius: .375rem;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-size: .8rem; cursor: pointer; transition: opacity .15s;
|
||||
}
|
||||
.lot-action-btn:hover { opacity: .8; }
|
||||
|
||||
/* modal scrollable */
|
||||
#lotModal .modal-content { max-height: 90vh; display: flex; flex-direction: column; overflow: hidden; }
|
||||
#lotModal .modal-content > form { display: flex; flex-direction: column; flex: 1 1 auto; min-height: 0; overflow: hidden; }
|
||||
#lotModal .modal-body { overflow-y: auto; flex: 1 1 auto; min-height: 0; }
|
||||
#lotModal .modal-footer { flex-shrink: 0; }
|
||||
</style>
|
||||
@endsection
|
||||
|
||||
@ -31,174 +111,168 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{{-- ── Overview Map ─────────────────────────────────────────────────────────── --}}
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<span class="fw-700 text-sm">
|
||||
<i class="bi bi-map me-1" style="color:#6366f1;"></i>
|
||||
خريطة المواقف
|
||||
</span>
|
||||
</div>
|
||||
<div class="p-2">
|
||||
<div id="lotsMap"></div>
|
||||
{{-- ── Search bar ───────────────────────────────────────────────────────────── --}}
|
||||
<div class="d-flex align-items-center justify-content-between gap-3 mb-4 flex-wrap">
|
||||
<span class="text-sm fw-600" style="color:#64748b;">
|
||||
@if(request('search'))
|
||||
نتائج البحث عن «{{ request('search') }}»
|
||||
@else
|
||||
عرض جميع المواقف
|
||||
@endif
|
||||
</span>
|
||||
<div class="input-group" style="max-width:280px;">
|
||||
<input type="text" id="searchInput"
|
||||
class="form-control form-control-sm"
|
||||
style="border-color:#e2e8f0;border-inline-end:none;"
|
||||
placeholder="ابحث باسم الموقف أو العنوان..." value="{{ request('search') }}">
|
||||
<button class="btn btn-sm" style="background:#6366f1;color:#fff;border:none;" onclick="doSearch()">
|
||||
<i class="bi bi-search"></i>
|
||||
</button>
|
||||
@if(request('search'))
|
||||
<a href="{{ route('admin.parking-lots.index') }}"
|
||||
class="btn btn-sm" style="background:#f1f5f9;color:#64748b;border:none;">
|
||||
<i class="bi bi-x-lg"></i>
|
||||
</a>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- ── Table Card ───────────────────────────────────────────────────────────── --}}
|
||||
<div class="card">
|
||||
{{-- ── Cards grid ───────────────────────────────────────────────────────────── --}}
|
||||
@php
|
||||
$gradients = [
|
||||
'linear-gradient(135deg,#667eea,#764ba2)',
|
||||
'linear-gradient(135deg,#f093fb,#f5576c)',
|
||||
'linear-gradient(135deg,#4facfe,#00f2fe)',
|
||||
'linear-gradient(135deg,#43e97b,#38f9d7)',
|
||||
'linear-gradient(135deg,#fa709a,#fee140)',
|
||||
'linear-gradient(135deg,#a18cd1,#fbc2eb)',
|
||||
];
|
||||
@endphp
|
||||
|
||||
@if($parkingLots->isEmpty())
|
||||
<div class="text-center py-5">
|
||||
<i class="bi bi-buildings d-block mb-3" style="font-size:3rem;color:#cbd5e1;"></i>
|
||||
<p class="fw-600 mb-1" style="color:#475569;">لا توجد مواقف بعد</p>
|
||||
<p class="text-sm mb-3" style="color:#94a3b8;">ابدأ بإضافة أول موقف سيارات</p>
|
||||
<button class="btn fw-600"
|
||||
style="background:#6366f1;color:#fff;border:none;border-radius:.5rem;font-family:'Cairo',sans-serif;"
|
||||
data-bs-toggle="modal" data-bs-target="#lotModal" id="addLotBtnEmpty">
|
||||
<i class="bi bi-plus-lg me-1"></i>إضافة موقف
|
||||
</button>
|
||||
</div>
|
||||
@else
|
||||
<div class="row g-3">
|
||||
@foreach($parkingLots as $lot)
|
||||
@php $grad = $gradients[$lot->id % count($gradients)]; @endphp
|
||||
<div class="col-sm-6 col-md-4 col-xl-3">
|
||||
<div class="lot-admin-card">
|
||||
|
||||
{{-- Image / Placeholder --}}
|
||||
<div class="lot-card-img-wrap">
|
||||
@if($lot->image)
|
||||
<img src="{{ Storage::url($lot->image) }}" alt="{{ $lot->name }}">
|
||||
@else
|
||||
<div class="lot-card-placeholder" style="background: {{ $grad }};">
|
||||
<i class="bi bi-buildings"></i>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Status dot --}}
|
||||
<span class="lot-status-dot"
|
||||
style="background:{{ $lot->is_active ? '#10b981' : '#ef4444' }};"
|
||||
title="{{ $lot->is_active ? 'نشط' : 'معطل' }}">
|
||||
</span>
|
||||
|
||||
{{-- Active bookings badge --}}
|
||||
@if(($lot->active_bookings_count ?? 0) > 0)
|
||||
<span class="lot-active-badge">
|
||||
<i class="bi bi-car-front-fill me-1"></i>{{ $lot->active_bookings_count }} نشط
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
{{-- Card body --}}
|
||||
<div class="lot-card-body">
|
||||
<h6 title="{{ $lot->name }}">{{ $lot->name }}</h6>
|
||||
<p class="lot-card-address" title="{{ $lot->address }}">
|
||||
<i class="bi bi-geo-alt me-1" style="color:#94a3b8;"></i>{{ $lot->address }}
|
||||
</p>
|
||||
|
||||
<div class="lot-card-stats">
|
||||
<span class="lot-card-stat">
|
||||
<i class="bi bi-car-front" style="color:#6366f1;"></i>
|
||||
<strong>{{ $lot->total_capacity }}</strong> مركبة
|
||||
</span>
|
||||
<span class="lot-card-stat">
|
||||
<i class="bi bi-clock" style="color:#10b981;"></i>
|
||||
{{ $lot->working_hours }}
|
||||
</span>
|
||||
<span class="lot-card-stat" style="{{ !empty($lot->pricing_rules) ? 'border-color:#fbbf24;color:#92400e;' : '' }}">
|
||||
<i class="bi bi-{{ !empty($lot->pricing_rules) ? 'tags' : 'tag' }}" style="color:{{ !empty($lot->pricing_rules) ? '#f59e0b' : '#10b981' }};"></i>
|
||||
<strong>{{ number_format($lot->price_per_hour, 0) }}</strong> ر.س/س
|
||||
@if(!empty($lot->pricing_rules))
|
||||
<span style="font-size:.6rem;color:#f59e0b;"> (مخصص)</span>
|
||||
@endif
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Actions --}}
|
||||
<div class="lot-card-footer">
|
||||
<button class="lot-action-btn"
|
||||
style="background:#f1f5f9;color:#475569;"
|
||||
data-bs-toggle="modal" data-bs-target="#lotModal"
|
||||
onclick="editLot({{ $lot->id }})" title="تعديل">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</button>
|
||||
<button class="lot-action-btn"
|
||||
style="background:rgba(99,102,241,.1);color:#6366f1;"
|
||||
onclick="openPricingModal({{ $lot->id }}, '{{ addslashes($lot->name) }}')"
|
||||
title="التسعير الأسبوعي">
|
||||
<i class="bi bi-tags"></i>
|
||||
</button>
|
||||
<button class="lot-action-btn"
|
||||
style="background:{{ $lot->is_active ? 'rgba(239,68,68,.1)' : 'rgba(16,185,129,.1)' }};color:{{ $lot->is_active ? '#dc2626' : '#059669' }};"
|
||||
onclick="toggleStatus({{ $lot->id }})"
|
||||
title="{{ $lot->is_active ? 'تعطيل' : 'تفعيل' }}">
|
||||
<i class="bi {{ $lot->is_active ? 'bi-slash-circle' : 'bi-check-circle' }}"></i>
|
||||
</button>
|
||||
<button class="lot-action-btn"
|
||||
style="background:rgba(239,68,68,.08);color:#dc2626;"
|
||||
onclick="deleteLot({{ $lot->id }}, '{{ addslashes($lot->name) }}')"
|
||||
title="حذف">
|
||||
<i class="bi bi-trash3"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{{-- Toolbar --}}
|
||||
<div class="card-header d-flex align-items-center justify-content-between gap-3 flex-wrap">
|
||||
<span class="fw-700 text-sm">
|
||||
<i class="bi bi-list-ul me-1" style="color:#6366f1;"></i>
|
||||
قائمة المواقف
|
||||
</span>
|
||||
<div class="input-group" style="max-width:260px;">
|
||||
<input type="text" id="searchInput"
|
||||
class="form-control form-control-sm"
|
||||
style="border-color:#e2e8f0;border-inline-end:none;"
|
||||
placeholder="بحث..." value="{{ request('search') }}">
|
||||
<button class="btn btn-sm" style="background:#6366f1;color:#fff;border:none;" onclick="doSearch()">
|
||||
<i class="bi bi-search"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="app-table w-100">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>الموقف</th>
|
||||
<th>العنوان</th>
|
||||
<th class="text-center">السعة</th>
|
||||
<th class="text-center">السعر / ساعة</th>
|
||||
<th class="text-center">ساعات العمل</th>
|
||||
<th class="text-center">الحالة</th>
|
||||
<th class="text-center">نشط حالياً</th>
|
||||
<th class="text-center">إجراءات</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse($parkingLots as $lot)
|
||||
<tr>
|
||||
<td>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
@if($lot->image)
|
||||
<img src="{{ Storage::url($lot->image) }}" alt=""
|
||||
style="width:36px;height:36px;object-fit:cover;border-radius:.375rem;flex-shrink:0;">
|
||||
@else
|
||||
<div style="width:36px;height:36px;background:#e2e8f0;border-radius:.375rem;display:flex;align-items:center;justify-content:center;flex-shrink:0;">
|
||||
<i class="bi bi-buildings" style="color:#94a3b8;font-size:.85rem;"></i>
|
||||
</div>
|
||||
@endif
|
||||
<div>
|
||||
<div class="fw-600" style="color:#0f172a;">{{ $lot->name }}</div>
|
||||
<div class="text-xs" style="color:#94a3b8;direction:ltr;text-align:right;">
|
||||
{{ number_format($lot->latitude, 5) }}, {{ number_format($lot->longitude, 5) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<span class="text-sm" style="color:#475569;" title="{{ $lot->address }}">
|
||||
{{ Str::limit($lot->address, 40) }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span class="badge badge-soft-info fw-600">{{ $lot->total_capacity }}</span>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span class="badge badge-soft-success fw-600">
|
||||
{{ number_format($lot->price_per_hour, 0) }} ر.س
|
||||
</span>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span class="badge badge-soft-secondary text-xs">{{ $lot->working_hours }}</span>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
@if($lot->is_active)
|
||||
<span class="badge badge-soft-success">
|
||||
<i class="bi bi-circle-fill me-1" style="font-size:.45rem;vertical-align:middle;"></i>نشط
|
||||
</span>
|
||||
@else
|
||||
<span class="badge badge-soft-danger">
|
||||
<i class="bi bi-circle-fill me-1" style="font-size:.45rem;vertical-align:middle;"></i>معطل
|
||||
</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span class="badge badge-soft-warning fw-600">{{ $lot->active_bookings_count ?? 0 }}</span>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<div class="d-inline-flex gap-1">
|
||||
<button class="btn btn-sm"
|
||||
style="background:#f1f5f9;color:#475569;border:none;border-radius:.375rem;width:30px;height:30px;padding:0;"
|
||||
data-bs-toggle="modal" data-bs-target="#lotModal"
|
||||
onclick="editLot({{ $lot->id }})" title="تعديل">
|
||||
<i class="bi bi-pencil" style="font-size:.8rem;"></i>
|
||||
</button>
|
||||
<button class="btn btn-sm"
|
||||
style="background:{{ $lot->is_active ? 'rgba(239,68,68,.1)' : 'rgba(16,185,129,.1)' }};color:{{ $lot->is_active ? '#dc2626' : '#059669' }};border:none;border-radius:.375rem;width:30px;height:30px;padding:0;"
|
||||
onclick="toggleStatus({{ $lot->id }})"
|
||||
title="{{ $lot->is_active ? 'تعطيل' : 'تفعيل' }}">
|
||||
<i class="bi {{ $lot->is_active ? 'bi-slash-circle' : 'bi-check-circle' }}" style="font-size:.8rem;"></i>
|
||||
</button>
|
||||
<button class="btn btn-sm"
|
||||
style="background:rgba(239,68,68,.08);color:#dc2626;border:none;border-radius:.375rem;width:30px;height:30px;padding:0;"
|
||||
onclick="deleteLot({{ $lot->id }}, '{{ addslashes($lot->name) }}')"
|
||||
title="حذف">
|
||||
<i class="bi bi-trash3" style="font-size:.8rem;"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="8" class="text-center py-5">
|
||||
<i class="bi bi-buildings d-block mb-3" style="font-size:2.5rem;color:#cbd5e1;"></i>
|
||||
<p class="fw-600 mb-1" style="color:#475569;">لا توجد مواقف بعد</p>
|
||||
<p class="text-sm mb-3" style="color:#94a3b8;">ابدأ بإضافة أول موقف سيارات</p>
|
||||
<button class="btn btn-sm fw-600"
|
||||
style="background:#6366f1;color:#fff;border:none;border-radius:.5rem;font-family:'Cairo',sans-serif;"
|
||||
data-bs-toggle="modal" data-bs-target="#lotModal">
|
||||
<i class="bi bi-plus-lg me-1"></i>إضافة موقف
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@if($parkingLots->hasPages())
|
||||
<div class="card-header d-flex align-items-center justify-content-between flex-wrap gap-2">
|
||||
<span class="text-xs" style="color:#64748b;">
|
||||
عرض {{ $parkingLots->firstItem() ?? 0 }}–{{ $parkingLots->lastItem() ?? 0 }}
|
||||
من {{ $parkingLots->total() }}
|
||||
</span>
|
||||
{{ $parkingLots->appends(request()->query())->links('pagination::bootstrap-5') }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
{{-- Pagination --}}
|
||||
@if($parkingLots->hasPages())
|
||||
<div class="d-flex align-items-center justify-content-between flex-wrap gap-2 mt-4">
|
||||
<span class="text-xs" style="color:#64748b;">
|
||||
عرض {{ $parkingLots->firstItem() }}–{{ $parkingLots->lastItem() }}
|
||||
من {{ $parkingLots->total() }}
|
||||
</span>
|
||||
{{ $parkingLots->appends(request()->query())->links('pagination::bootstrap-5') }}
|
||||
</div>
|
||||
@endif
|
||||
@endif
|
||||
|
||||
{{-- ── Add / Edit Modal ────────────────────────────────────────────────────── --}}
|
||||
<div class="modal fade" id="lotModal" tabindex="-1" data-bs-backdrop="static">
|
||||
<div class="modal-dialog modal-lg modal-dialog-centered modal-dialog-scrollable">
|
||||
<div class="modal-content">
|
||||
|
||||
<div class="modal-header" style="border-bottom:1px solid #f1f5f9;">
|
||||
<h5 class="modal-title fw-700" id="modalLabel" style="font-size:1rem;color:#0f172a;">
|
||||
إضافة موقف جديد
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
|
||||
<form id="lotForm">
|
||||
@csrf
|
||||
<input type="hidden" id="lotId">
|
||||
|
||||
<div class="modal-body p-4">
|
||||
<p id="modalLabel" class="fw-700 mb-3" style="font-size:1rem;color:#0f172a;">إضافة موقف جديد</p>
|
||||
<div class="row g-3">
|
||||
|
||||
{{-- Basic Info --}}
|
||||
@ -273,7 +347,7 @@
|
||||
<button type="button" class="btn btn-sm fw-600"
|
||||
style="background:#f1f5f9;color:#475569;border:none;border-radius:.5rem;font-family:'Cairo',sans-serif;"
|
||||
data-bs-dismiss="modal">إلغاء</button>
|
||||
<button type="submit" id="submitBtn"
|
||||
<button type="button" id="submitBtn" onclick="saveLot()"
|
||||
class="btn btn-sm fw-600"
|
||||
style="background:#6366f1;color:#fff;border:none;border-radius:.5rem;font-family:'Cairo',sans-serif;">
|
||||
<span id="submitSpinner" class="spinner-border spinner-border-sm me-1 d-none"></span>
|
||||
@ -285,59 +359,142 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- ── Pricing Modal ──────────────────────────────────────────────────────── --}}
|
||||
<div class="modal fade" id="pricingModal" tabindex="-1" data-bs-backdrop="static">
|
||||
<div class="modal-dialog modal-dialog-centered" style="max-width:520px;">
|
||||
<div class="modal-content">
|
||||
|
||||
<div class="modal-header" style="border-bottom:1px solid #f1f5f9;">
|
||||
<h5 class="modal-title fw-700" id="pricingModalLabel" style="font-size:1rem;color:#0f172a;">
|
||||
<i class="bi bi-tags me-1" style="color:#6366f1;"></i>
|
||||
التسعير الأسبوعي
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
|
||||
<div class="modal-body p-4">
|
||||
<input type="hidden" id="pricingLotId">
|
||||
|
||||
{{-- Base rate --}}
|
||||
<div class="mb-4">
|
||||
<label class="form-label fw-600">
|
||||
السعر الأساسي (لجميع الأيام غير المخصصة)
|
||||
<span style="color:#ef4444;">*</span>
|
||||
</label>
|
||||
<div class="input-group">
|
||||
<input type="number" id="p_base" class="form-control"
|
||||
step="0.01" min="0" placeholder="0.00"
|
||||
oninput="syncBaseHints()">
|
||||
<span class="input-group-text" style="font-family:'Cairo',sans-serif;background:#f8fafc;color:#64748b;border-color:#e2e8f0;">ر.س / ساعة</span>
|
||||
</div>
|
||||
<p class="text-xs mt-1 mb-0" style="color:#94a3b8;">
|
||||
الأيام التي لا تحمل سعراً مخصصاً ستستخدم هذا السعر تلقائياً.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{{-- Per-day rules --}}
|
||||
<p class="text-xs fw-700 text-uppercase mb-3" style="color:#94a3b8;letter-spacing:.05em;">
|
||||
أسعار مخصصة حسب اليوم <span class="fw-400 text-lowercase" style="color:#b0bec5;">(اختياري)</span>
|
||||
</p>
|
||||
|
||||
<div id="dayRulesGrid">
|
||||
@php
|
||||
$days = [
|
||||
1 => ['الاثنين', 'Monday', '#6366f1'],
|
||||
2 => ['الثلاثاء', 'Tuesday', '#6366f1'],
|
||||
3 => ['الأربعاء', 'Wednesday', '#6366f1'],
|
||||
4 => ['الخميس', 'Thursday', '#6366f1'],
|
||||
5 => ['الجمعة', 'Friday', '#f59e0b'],
|
||||
6 => ['السبت', 'Saturday', '#10b981'],
|
||||
7 => ['الأحد', 'Sunday', '#10b981'],
|
||||
];
|
||||
@endphp
|
||||
@foreach($days as $dow => [$ar, $en, $color])
|
||||
<div class="d-flex align-items-center gap-3 py-2"
|
||||
style="border-bottom:1px solid #f1f5f9;">
|
||||
<div style="width:90px;flex-shrink:0;">
|
||||
<span class="fw-600 text-sm" style="color:#0f172a;">{{ $ar }}</span>
|
||||
<div class="text-xs" style="color:#94a3b8;">{{ $en }}</div>
|
||||
</div>
|
||||
<div class="flex-grow-1">
|
||||
<div class="input-group input-group-sm">
|
||||
<input type="number"
|
||||
id="p_day_{{ $dow }}"
|
||||
class="form-control day-rate-input"
|
||||
data-dow="{{ $dow }}"
|
||||
step="0.01" min="0"
|
||||
placeholder="مثل السعر الأساسي"
|
||||
style="font-family:'Cairo',sans-serif;">
|
||||
<span class="input-group-text" style="background:#f8fafc;color:#94a3b8;border-color:#e2e8f0;font-size:.75rem;">ر.س</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style="width:70px;text-align:center;">
|
||||
<span id="p_badge_{{ $dow }}"
|
||||
class="badge d-none"
|
||||
style="font-size:.65rem;background:rgba(99,102,241,.1);color:#6366f1;border-radius:.3rem;">
|
||||
مخصص
|
||||
</span>
|
||||
</div>
|
||||
<button type="button"
|
||||
class="btn btn-sm p-0"
|
||||
style="color:#94a3b8;border:none;background:none;font-size:.75rem;"
|
||||
onclick="clearDayRate({{ $dow }})"
|
||||
title="استخدام السعر الأساسي">
|
||||
<i class="bi bi-x-circle"></i>
|
||||
</button>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
{{-- Weekly preview --}}
|
||||
<div class="mt-4 p-3" style="background:#f8fafc;border-radius:.5rem;border:1px solid #e2e8f0;">
|
||||
<p class="text-xs fw-700 mb-2" style="color:#64748b;">معاينة التسعير الأسبوعي</p>
|
||||
<div id="pricingPreview" class="d-flex gap-1 flex-wrap">
|
||||
@foreach($days as $dow => [$ar, $en, $color])
|
||||
<div id="prev_{{ $dow }}"
|
||||
class="text-center"
|
||||
style="flex:1;min-width:52px;background:#fff;border:1px solid #e2e8f0;border-radius:.375rem;padding:.35rem .25rem;">
|
||||
<div class="text-xs fw-600" style="color:#475569;">{{ $ar }}</div>
|
||||
<div id="prev_val_{{ $dow }}"
|
||||
class="fw-700 mt-1"
|
||||
style="font-size:.78rem;color:#6366f1;">—</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
<p class="text-xs mt-2 mb-0" style="color:#94a3b8;">
|
||||
السعر المعروض هو السعر الفعلي الذي سيُطبّق على كل يوم.
|
||||
الحجوزات القائمة حالياً <strong>لن تتأثر</strong> بأي تغيير.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer" style="border-top:1px solid #f1f5f9;">
|
||||
<button type="button" class="btn btn-sm fw-600"
|
||||
style="background:#f1f5f9;color:#475569;border:none;border-radius:.5rem;font-family:'Cairo',sans-serif;"
|
||||
data-bs-dismiss="modal">إلغاء</button>
|
||||
<button type="button" id="savePricingBtn"
|
||||
class="btn btn-sm fw-600"
|
||||
style="background:#6366f1;color:#fff;border:none;border-radius:.5rem;font-family:'Cairo',sans-serif;"
|
||||
onclick="savePricing()">
|
||||
<span id="pricingSpinner" class="spinner-border spinner-border-sm me-1 d-none"></span>
|
||||
<i class="bi bi-check2 me-1"></i>
|
||||
حفظ الأسعار
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@push('scripts')
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||
<script>
|
||||
window.onerror = (msg, src, line) => { alert('JS خطأ في السطر ' + line + ':\n' + msg); };
|
||||
// ── Parking lots data from server ─────────────────────────────────────────
|
||||
const lotsData = @json($parkingLots->items());
|
||||
|
||||
// Damascus city centre fallback
|
||||
const DAMASCUS = [33.5138, 36.2765];
|
||||
|
||||
// ── Overview map ──────────────────────────────────────────────────────────
|
||||
const lotsMap = L.map('lotsMap').setView(DAMASCUS, 13);
|
||||
|
||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',
|
||||
maxZoom: 19
|
||||
}).addTo(lotsMap);
|
||||
|
||||
const activeIcon = L.icon({
|
||||
iconUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon.png',
|
||||
shadowUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-shadow.png',
|
||||
iconSize: [25, 41], iconAnchor: [12, 41], popupAnchor: [1, -34]
|
||||
});
|
||||
|
||||
lotsData.forEach(lot => {
|
||||
const lat = parseFloat(lot.latitude);
|
||||
const lng = parseFloat(lot.longitude);
|
||||
if (isNaN(lat) || isNaN(lng)) return;
|
||||
|
||||
const statusBadge = lot.is_active
|
||||
? '<span style="color:#059669;font-weight:700;">● نشط</span>'
|
||||
: '<span style="color:#dc2626;font-weight:700;">● معطل</span>';
|
||||
|
||||
L.marker([lat, lng], { icon: activeIcon })
|
||||
.addTo(lotsMap)
|
||||
.bindPopup(`
|
||||
<div style="font-family:'Cairo',sans-serif;min-width:160px;direction:rtl;">
|
||||
<div style="font-weight:700;font-size:.92rem;margin-bottom:4px;">${lot.name}</div>
|
||||
<div style="font-size:.8rem;color:#475569;margin-bottom:4px;">${lot.address ?? ''}</div>
|
||||
<div style="font-size:.8rem;margin-bottom:6px;">${statusBadge}</div>
|
||||
<div style="font-size:.78rem;color:#64748b;">
|
||||
السعة: ${lot.total_capacity} | ${lot.price_per_hour} ر.س/س
|
||||
</div>
|
||||
</div>
|
||||
`, { maxWidth: 220 });
|
||||
});
|
||||
|
||||
// Fit map to markers if any exist
|
||||
const validLots = lotsData.filter(l => !isNaN(parseFloat(l.latitude)) && !isNaN(parseFloat(l.longitude)));
|
||||
if (validLots.length > 0) {
|
||||
const bounds = L.latLngBounds(validLots.map(l => [parseFloat(l.latitude), parseFloat(l.longitude)]));
|
||||
lotsMap.fitBounds(bounds, { padding: [40, 40], maxZoom: 15 });
|
||||
}
|
||||
|
||||
// ── Modal map picker ──────────────────────────────────────────────────────
|
||||
let modalMap = null;
|
||||
let modalMarker = null;
|
||||
@ -388,8 +545,10 @@ function placeModalMarker(lat, lng) {
|
||||
}
|
||||
|
||||
// Sync map marker when user types into lat/lng fields
|
||||
['f_lat', 'f_lng'].forEach(id => {
|
||||
document.getElementById(id).addEventListener('input', () => {
|
||||
['f_lat', 'f_lng'].forEach(elId => {
|
||||
const el = document.getElementById(elId);
|
||||
if (!el) return;
|
||||
el.addEventListener('input', () => {
|
||||
const lat = parseFloat(document.getElementById('f_lat').value);
|
||||
const lng = parseFloat(document.getElementById('f_lng').value);
|
||||
if (!isNaN(lat) && !isNaN(lng) && modalMap) {
|
||||
@ -409,7 +568,6 @@ lotModalEl.addEventListener('shown.bs.modal', () => {
|
||||
|
||||
// ── Modal logic ───────────────────────────────────────────────────────────
|
||||
let editingId = null;
|
||||
const modal = new bootstrap.Modal(lotModalEl, { backdrop: 'static' });
|
||||
|
||||
document.getElementById('addLotBtn').onclick = () => {
|
||||
editingId = null;
|
||||
@ -417,7 +575,7 @@ document.getElementById('addLotBtn').onclick = () => {
|
||||
document.getElementById('modalLabel').textContent = 'إضافة موقف جديد';
|
||||
document.getElementById('submitText').textContent = 'حفظ الموقف';
|
||||
document.getElementById('imagePreviewWrap').style.display = 'none';
|
||||
modal.show();
|
||||
// modal opened by data-bs-toggle on the button
|
||||
};
|
||||
|
||||
function previewImage(input) {
|
||||
@ -431,50 +589,57 @@ function previewImage(input) {
|
||||
}
|
||||
}
|
||||
|
||||
async function editLot(id) {
|
||||
function editLot(id) {
|
||||
const lot = lotsData.find(l => l.id === id);
|
||||
if (!lot) { alert('لم يتم العثور على بيانات الموقف'); return; }
|
||||
|
||||
editingId = id;
|
||||
setBtnLoading(true);
|
||||
try {
|
||||
const { data } = await fetch(`/admin/parking-lots/${id}`).then(r => r.json());
|
||||
document.getElementById('modalLabel').textContent = 'تعديل: ' + data.name;
|
||||
document.getElementById('submitText').textContent = 'تحديث الموقف';
|
||||
document.getElementById('f_name').value = data.name;
|
||||
document.getElementById('f_capacity').value = data.total_capacity;
|
||||
document.getElementById('f_price').value = data.price_per_hour;
|
||||
document.getElementById('f_hours').value = data.working_hours;
|
||||
document.getElementById('f_lat').value = data.latitude;
|
||||
document.getElementById('f_lng').value = data.longitude;
|
||||
document.getElementById('f_address').value = data.address;
|
||||
document.getElementById('f_image').value = '';
|
||||
// Show existing image preview
|
||||
const wrap = document.getElementById('imagePreviewWrap');
|
||||
const img = document.getElementById('imagePreview');
|
||||
if (data.image) {
|
||||
img.src = '/storage/' + data.image;
|
||||
wrap.style.display = 'block';
|
||||
} else {
|
||||
wrap.style.display = 'none';
|
||||
}
|
||||
modal.show();
|
||||
} catch { alert('خطأ في تحميل البيانات'); }
|
||||
finally { setBtnLoading(false); }
|
||||
document.getElementById('modalLabel').textContent = 'تعديل: ' + lot.name;
|
||||
document.getElementById('submitText').textContent = 'تحديث الموقف';
|
||||
document.getElementById('f_name').value = lot.name;
|
||||
document.getElementById('f_capacity').value = lot.total_capacity;
|
||||
document.getElementById('f_price').value = lot.price_per_hour;
|
||||
document.getElementById('f_hours').value = lot.working_hours;
|
||||
document.getElementById('f_lat').value = lot.latitude;
|
||||
document.getElementById('f_lng').value = lot.longitude;
|
||||
document.getElementById('f_address').value = lot.address;
|
||||
document.getElementById('f_image').value = '';
|
||||
|
||||
const wrap = document.getElementById('imagePreviewWrap');
|
||||
const img = document.getElementById('imagePreview');
|
||||
if (lot.image) {
|
||||
img.src = '/storage/' + lot.image;
|
||||
wrap.style.display = 'block';
|
||||
} else {
|
||||
wrap.style.display = 'none';
|
||||
}
|
||||
// modal opened by data-bs-toggle on the button
|
||||
}
|
||||
|
||||
document.getElementById('lotForm').onsubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
async function saveLot() {
|
||||
setBtnLoading(true);
|
||||
try {
|
||||
const fd = new FormData(e.target);
|
||||
const form = document.getElementById('lotForm');
|
||||
const fd = new FormData(form);
|
||||
if (editingId) fd.append('_method', 'PUT');
|
||||
const res = await fetch(editingId ? `/admin/parking-lots/${editingId}` : '/admin/parking-lots', {
|
||||
const url = editingId ? `/admin/parking-lots/${editingId}` : '/admin/parking-lots';
|
||||
const res = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { 'Accept': 'application/json', 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content },
|
||||
body: fd
|
||||
});
|
||||
const result = await res.json();
|
||||
result.success ? location.reload() : alert(result.message || 'خطأ في العملية');
|
||||
} catch { alert('خطأ في الاتصال'); }
|
||||
if (result.success) {
|
||||
location.reload();
|
||||
} else if (result.errors) {
|
||||
const msgs = Object.values(result.errors).flat().join('\n');
|
||||
alert(msgs);
|
||||
} else {
|
||||
alert(result.message || 'خطأ في العملية');
|
||||
}
|
||||
} catch(err) { alert('خطأ في الاتصال: ' + err.message); }
|
||||
finally { setBtnLoading(false); }
|
||||
};
|
||||
}
|
||||
|
||||
function setBtnLoading(on) {
|
||||
document.getElementById('submitBtn').disabled = on;
|
||||
@ -497,6 +662,109 @@ function doSearch() {
|
||||
window.location.href = `/admin/parking-lots?search=${encodeURIComponent(t)}`;
|
||||
}
|
||||
|
||||
// ── Pricing Modal ─────────────────────────────────────────────────────────
|
||||
|
||||
function openPricingModal(id, name) {
|
||||
try {
|
||||
const lot = lotsData.find(l => l.id === id);
|
||||
const base = lot ? (parseFloat(lot.price_per_hour) || 0) : 0;
|
||||
const rules = (lot && lot.pricing_rules) ? lot.pricing_rules : {};
|
||||
|
||||
document.getElementById('pricingLotId').value = id;
|
||||
document.getElementById('pricingModalLabel').innerHTML =
|
||||
`<i class="bi bi-tags me-1" style="color:#6366f1;"></i>التسعير الأسبوعي — ${name}`;
|
||||
|
||||
document.getElementById('p_base').value = base;
|
||||
|
||||
[1,2,3,4,5,6,7].forEach(d => {
|
||||
const custom = rules[d] !== undefined ? parseFloat(rules[d]) : null;
|
||||
document.getElementById(`p_day_${d}`).value = (custom !== null) ? custom : '';
|
||||
document.getElementById(`p_badge_${d}`).classList.add('d-none');
|
||||
});
|
||||
|
||||
updatePricingPreview();
|
||||
bootstrap.Modal.getOrCreateInstance(document.getElementById('pricingModal')).show();
|
||||
} catch(e) {
|
||||
alert('خطأ في فتح نافذة التسعير:\n' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
function syncBaseHints() {
|
||||
updatePricingPreview();
|
||||
}
|
||||
|
||||
function clearDayRate(dow) {
|
||||
document.getElementById(`p_day_${dow}`).value = '';
|
||||
updatePricingPreview();
|
||||
}
|
||||
|
||||
function updatePricingPreview() {
|
||||
const base = parseFloat(document.getElementById('p_base').value) || 0;
|
||||
[1,2,3,4,5,6,7].forEach(d => {
|
||||
const raw = document.getElementById(`p_day_${d}`).value;
|
||||
const custom = raw !== '' ? parseFloat(raw) : null;
|
||||
const rate = custom !== null ? custom : base;
|
||||
const isCustom = custom !== null && custom !== base;
|
||||
|
||||
document.getElementById(`p_badge_${d}`).classList.toggle('d-none', !isCustom);
|
||||
const valEl = document.getElementById(`prev_val_${d}`);
|
||||
valEl.textContent = rate > 0 ? rate.toLocaleString('ar-SA') : '—';
|
||||
valEl.style.color = isCustom ? '#f59e0b' : '#6366f1';
|
||||
document.getElementById(`prev_${d}`).style.borderColor = isCustom ? '#fbbf24' : '#e2e8f0';
|
||||
});
|
||||
}
|
||||
|
||||
// Update preview when any day input changes
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
document.querySelectorAll('.day-rate-input').forEach(inp => {
|
||||
inp.addEventListener('input', updatePricingPreview);
|
||||
});
|
||||
});
|
||||
|
||||
async function savePricing() {
|
||||
const id = document.getElementById('pricingLotId').value;
|
||||
const base = parseFloat(document.getElementById('p_base').value);
|
||||
|
||||
if (isNaN(base) || base < 0) {
|
||||
alert('يرجى إدخال سعر أساسي صحيح');
|
||||
return;
|
||||
}
|
||||
|
||||
const rules = {};
|
||||
[1,2,3,4,5,6,7].forEach(d => {
|
||||
const val = document.getElementById(`p_day_${d}`).value;
|
||||
if (val !== '') rules[d] = parseFloat(val);
|
||||
});
|
||||
|
||||
document.getElementById('savePricingBtn').disabled = true;
|
||||
document.getElementById('pricingSpinner').classList.remove('d-none');
|
||||
|
||||
try {
|
||||
const res = await fetch(`/admin/parking-lots/${id}/pricing`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ price_per_hour: base, pricing_rules: rules }),
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
if (data.success) {
|
||||
bootstrap.Modal.getInstance(document.getElementById('pricingModal'))?.hide();
|
||||
location.reload();
|
||||
} else {
|
||||
alert(data.message || 'خطأ في الحفظ');
|
||||
}
|
||||
} catch {
|
||||
alert('خطأ في الاتصال');
|
||||
} finally {
|
||||
document.getElementById('savePricingBtn').disabled = false;
|
||||
document.getElementById('pricingSpinner').classList.add('d-none');
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteLot(id, name) {
|
||||
if (!confirm(`حذف الموقف "${name}"؟\nسيتم حذف جميع بيانات الموقف نهائياً.`)) return;
|
||||
|
||||
|
||||
@ -28,6 +28,7 @@ Route::prefix('admin')->middleware('admin')->name('admin.')->group(function () {
|
||||
Route::get('/parking-lots/{parkingLot}', [\App\Http\Controllers\Admin\ParkingLotController::class, 'show'])->name('parking-lots.show');
|
||||
Route::post('/parking-lots', [\App\Http\Controllers\Admin\ParkingLotController::class, 'store'])->name('parking-lots.store');
|
||||
Route::put('/parking-lots/{parkingLot}', [\App\Http\Controllers\Admin\ParkingLotController::class, 'update'])->name('parking-lots.update');
|
||||
Route::put('/parking-lots/{parkingLot}/pricing', [\App\Http\Controllers\Admin\ParkingLotController::class, 'updatePricing'])->name('parking-lots.pricing');
|
||||
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');
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user