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(git reset:*)",
|
||||||
"Bash(php -l app/Http/Controllers/Admin/ParkingLotController.php)",
|
"Bash(php -l app/Http/Controllers/Admin/ParkingLotController.php)",
|
||||||
"Bash(php -l app/Http/Requests/StoreParkingLotRequest.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();
|
$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 ($request->hasFile('image')) {
|
||||||
if ($parkingLot->image) {
|
if ($parkingLot->image) {
|
||||||
Storage::disk('public')->delete($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
|
public function toggleStatus(ParkingLot $parkingLot): JsonResponse
|
||||||
{
|
{
|
||||||
$parkingLot->update(['is_active' => !$parkingLot->is_active]);
|
$parkingLot->update(['is_active' => !$parkingLot->is_active]);
|
||||||
|
|||||||
@ -18,6 +18,9 @@ class BookingController extends Controller
|
|||||||
{
|
{
|
||||||
$validated = $request->validated();
|
$validated = $request->validated();
|
||||||
|
|
||||||
|
$lot = \App\Models\ParkingLot::find($validated['parking_lot_id']);
|
||||||
|
$validated['pricing_snapshot'] = $lot?->pricingSnapshot();
|
||||||
|
|
||||||
$booking = \App\Models\Booking::create($validated);
|
$booking = \App\Models\Booking::create($validated);
|
||||||
|
|
||||||
// Note: In production, create CarRegistry here for the booking car
|
// Note: In production, create CarRegistry here for the booking car
|
||||||
|
|||||||
@ -88,6 +88,7 @@ class OperatorController extends Controller
|
|||||||
'start_time' => now(),
|
'start_time' => now(),
|
||||||
'end_time' => now()->addHours($data['duration_hours']),
|
'end_time' => now()->addHours($data['duration_hours']),
|
||||||
'status' => 'active',
|
'status' => 'active',
|
||||||
|
'pricing_snapshot' => $lot->pricingSnapshot(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return response()->json([
|
return response()->json([
|
||||||
@ -128,7 +129,7 @@ class OperatorController extends Controller
|
|||||||
$start = $booking->start_time;
|
$start = $booking->start_time;
|
||||||
$end = now();
|
$end = now();
|
||||||
$duration = $start->diffInMinutes($end);
|
$duration = $start->diffInMinutes($end);
|
||||||
$calc = $lot->calculateFee($start, $end);
|
$calc = $lot->calculateFee($start, $end, $booking->pricing_snapshot);
|
||||||
|
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'success' => true,
|
'success' => true,
|
||||||
@ -163,7 +164,7 @@ class OperatorController extends Controller
|
|||||||
$lot = $booking->parkingLot;
|
$lot = $booking->parkingLot;
|
||||||
$start = $booking->start_time;
|
$start = $booking->start_time;
|
||||||
$end = now();
|
$end = now();
|
||||||
$calc = $lot->calculateFee($start, $end);
|
$calc = $lot->calculateFee($start, $end, $booking->pricing_snapshot);
|
||||||
|
|
||||||
$proofPath = null;
|
$proofPath = null;
|
||||||
if ($request->hasFile('payment_proof')) {
|
if ($request->hasFile('payment_proof')) {
|
||||||
|
|||||||
@ -25,6 +25,7 @@ class Booking extends Model
|
|||||||
'payment_method',
|
'payment_method',
|
||||||
'payment_proof',
|
'payment_proof',
|
||||||
'paid_at',
|
'paid_at',
|
||||||
|
'pricing_snapshot',
|
||||||
];
|
];
|
||||||
|
|
||||||
protected $casts = [
|
protected $casts = [
|
||||||
@ -32,6 +33,7 @@ class Booking extends Model
|
|||||||
'end_time' => 'datetime',
|
'end_time' => 'datetime',
|
||||||
'paid_at' => 'datetime',
|
'paid_at' => 'datetime',
|
||||||
'total_fee' => 'decimal:2',
|
'total_fee' => 'decimal:2',
|
||||||
|
'pricing_snapshot' => 'array',
|
||||||
];
|
];
|
||||||
|
|
||||||
public function parkingLot(): BelongsTo
|
public function parkingLot(): BelongsTo
|
||||||
|
|||||||
@ -81,15 +81,33 @@ class ParkingLot extends Model
|
|||||||
return $activeBookings + $activeRegistries;
|
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.
|
* 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'], ...]]
|
* 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 = [];
|
$details = [];
|
||||||
$total = 0.0;
|
$total = 0.0;
|
||||||
$cursor = $start->copy()->seconds(0);
|
$cursor = $start->copy()->seconds(0);
|
||||||
@ -99,7 +117,7 @@ class ParkingLot extends Model
|
|||||||
$segEnd = ($dayEnd < $end) ? $dayEnd : $end;
|
$segEnd = ($dayEnd < $end) ? $dayEnd : $end;
|
||||||
|
|
||||||
$dow = (int) $cursor->format('N'); // 1=Mon … 7=Sun
|
$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);
|
$hours = round($cursor->diffInMinutes($segEnd) / 60, 4);
|
||||||
$subtotal = $hours * $rate;
|
$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';
|
import axios from 'axios';
|
||||||
window.axios = axios;
|
window.axios = axios;
|
||||||
|
|||||||
@ -5,11 +5,91 @@
|
|||||||
@section('styles')
|
@section('styles')
|
||||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
|
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
|
||||||
<style>
|
<style>
|
||||||
#lotsMap { height: 380px; border-radius: .5rem; z-index: 0; }
|
|
||||||
#modalMap { height: 280px; 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; }
|
.map-hint { font-size: .78rem; color: #64748b; margin-top: .35rem; }
|
||||||
/* keep Leaflet tiles crisp inside RTL layout */
|
|
||||||
.leaflet-container { direction: ltr; }
|
.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>
|
</style>
|
||||||
@endsection
|
@endsection
|
||||||
|
|
||||||
@ -31,174 +111,168 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{-- ── Overview Map ─────────────────────────────────────────────────────────── --}}
|
{{-- ── Search bar ───────────────────────────────────────────────────────────── --}}
|
||||||
<div class="card mb-4">
|
<div class="d-flex align-items-center justify-content-between gap-3 mb-4 flex-wrap">
|
||||||
<div class="card-header">
|
<span class="text-sm fw-600" style="color:#64748b;">
|
||||||
<span class="fw-700 text-sm">
|
@if(request('search'))
|
||||||
<i class="bi bi-map me-1" style="color:#6366f1;"></i>
|
نتائج البحث عن «{{ request('search') }}»
|
||||||
خريطة المواقف
|
@else
|
||||||
|
عرض جميع المواقف
|
||||||
|
@endif
|
||||||
</span>
|
</span>
|
||||||
</div>
|
<div class="input-group" style="max-width:280px;">
|
||||||
<div class="p-2">
|
|
||||||
<div id="lotsMap"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{-- ── Table Card ───────────────────────────────────────────────────────────── --}}
|
|
||||||
<div class="card">
|
|
||||||
|
|
||||||
{{-- 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"
|
<input type="text" id="searchInput"
|
||||||
class="form-control form-control-sm"
|
class="form-control form-control-sm"
|
||||||
style="border-color:#e2e8f0;border-inline-end:none;"
|
style="border-color:#e2e8f0;border-inline-end:none;"
|
||||||
placeholder="بحث..." value="{{ request('search') }}">
|
placeholder="ابحث باسم الموقف أو العنوان..." value="{{ request('search') }}">
|
||||||
<button class="btn btn-sm" style="background:#6366f1;color:#fff;border:none;" onclick="doSearch()">
|
<button class="btn btn-sm" style="background:#6366f1;color:#fff;border:none;" onclick="doSearch()">
|
||||||
<i class="bi bi-search"></i>
|
<i class="bi bi-search"></i>
|
||||||
</button>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="table-responsive">
|
{{-- ── Cards grid ───────────────────────────────────────────────────────────── --}}
|
||||||
<table class="app-table w-100">
|
@php
|
||||||
<thead>
|
$gradients = [
|
||||||
<tr>
|
'linear-gradient(135deg,#667eea,#764ba2)',
|
||||||
<th>الموقف</th>
|
'linear-gradient(135deg,#f093fb,#f5576c)',
|
||||||
<th>العنوان</th>
|
'linear-gradient(135deg,#4facfe,#00f2fe)',
|
||||||
<th class="text-center">السعة</th>
|
'linear-gradient(135deg,#43e97b,#38f9d7)',
|
||||||
<th class="text-center">السعر / ساعة</th>
|
'linear-gradient(135deg,#fa709a,#fee140)',
|
||||||
<th class="text-center">ساعات العمل</th>
|
'linear-gradient(135deg,#a18cd1,#fbc2eb)',
|
||||||
<th class="text-center">الحالة</th>
|
];
|
||||||
<th class="text-center">نشط حالياً</th>
|
@endphp
|
||||||
<th class="text-center">إجراءات</th>
|
|
||||||
</tr>
|
@if($parkingLots->isEmpty())
|
||||||
</thead>
|
<div class="text-center py-5">
|
||||||
<tbody>
|
<i class="bi bi-buildings d-block mb-3" style="font-size:3rem;color:#cbd5e1;"></i>
|
||||||
@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="fw-600 mb-1" style="color:#475569;">لا توجد مواقف بعد</p>
|
||||||
<p class="text-sm mb-3" style="color:#94a3b8;">ابدأ بإضافة أول موقف سيارات</p>
|
<p class="text-sm mb-3" style="color:#94a3b8;">ابدأ بإضافة أول موقف سيارات</p>
|
||||||
<button class="btn btn-sm fw-600"
|
<button class="btn fw-600"
|
||||||
style="background:#6366f1;color:#fff;border:none;border-radius:.5rem;font-family:'Cairo',sans-serif;"
|
style="background:#6366f1;color:#fff;border:none;border-radius:.5rem;font-family:'Cairo',sans-serif;"
|
||||||
data-bs-toggle="modal" data-bs-target="#lotModal">
|
data-bs-toggle="modal" data-bs-target="#lotModal" id="addLotBtnEmpty">
|
||||||
<i class="bi bi-plus-lg me-1"></i>إضافة موقف
|
<i class="bi bi-plus-lg me-1"></i>إضافة موقف
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</div>
|
||||||
</tr>
|
@else
|
||||||
@endforelse
|
<div class="row g-3">
|
||||||
</tbody>
|
@foreach($parkingLots as $lot)
|
||||||
</table>
|
@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>
|
</div>
|
||||||
|
|
||||||
@if($parkingLots->hasPages())
|
{{-- Card body --}}
|
||||||
<div class="card-header d-flex align-items-center justify-content-between flex-wrap gap-2">
|
<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>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@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;">
|
<span class="text-xs" style="color:#64748b;">
|
||||||
عرض {{ $parkingLots->firstItem() ?? 0 }}–{{ $parkingLots->lastItem() ?? 0 }}
|
عرض {{ $parkingLots->firstItem() }}–{{ $parkingLots->lastItem() }}
|
||||||
من {{ $parkingLots->total() }}
|
من {{ $parkingLots->total() }}
|
||||||
</span>
|
</span>
|
||||||
{{ $parkingLots->appends(request()->query())->links('pagination::bootstrap-5') }}
|
{{ $parkingLots->appends(request()->query())->links('pagination::bootstrap-5') }}
|
||||||
</div>
|
|
||||||
@endif
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
@endif
|
||||||
|
@endif
|
||||||
|
|
||||||
{{-- ── Add / Edit Modal ────────────────────────────────────────────────────── --}}
|
{{-- ── Add / Edit Modal ────────────────────────────────────────────────────── --}}
|
||||||
<div class="modal fade" id="lotModal" tabindex="-1" data-bs-backdrop="static">
|
<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-dialog modal-lg modal-dialog-centered modal-dialog-scrollable">
|
||||||
<div class="modal-content">
|
<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">
|
<form id="lotForm">
|
||||||
@csrf
|
@csrf
|
||||||
<input type="hidden" id="lotId">
|
<input type="hidden" id="lotId">
|
||||||
|
|
||||||
<div class="modal-body p-4">
|
<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">
|
<div class="row g-3">
|
||||||
|
|
||||||
{{-- Basic Info --}}
|
{{-- Basic Info --}}
|
||||||
@ -273,7 +347,7 @@
|
|||||||
<button type="button" class="btn btn-sm fw-600"
|
<button type="button" 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;"
|
||||||
data-bs-dismiss="modal">إلغاء</button>
|
data-bs-dismiss="modal">إلغاء</button>
|
||||||
<button type="submit" id="submitBtn"
|
<button type="button" id="submitBtn" onclick="saveLot()"
|
||||||
class="btn btn-sm fw-600"
|
class="btn btn-sm fw-600"
|
||||||
style="background:#6366f1;color:#fff;border:none;border-radius:.5rem;font-family:'Cairo',sans-serif;">
|
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>
|
<span id="submitSpinner" class="spinner-border spinner-border-sm me-1 d-none"></span>
|
||||||
@ -285,59 +359,142 @@
|
|||||||
</div>
|
</div>
|
||||||
</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')
|
@push('scripts')
|
||||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||||
<script>
|
<script>
|
||||||
|
window.onerror = (msg, src, line) => { alert('JS خطأ في السطر ' + line + ':\n' + msg); };
|
||||||
// ── Parking lots data from server ─────────────────────────────────────────
|
// ── Parking lots data from server ─────────────────────────────────────────
|
||||||
const lotsData = @json($parkingLots->items());
|
const lotsData = @json($parkingLots->items());
|
||||||
|
|
||||||
// Damascus city centre fallback
|
// Damascus city centre fallback
|
||||||
const DAMASCUS = [33.5138, 36.2765];
|
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 ──────────────────────────────────────────────────────
|
// ── Modal map picker ──────────────────────────────────────────────────────
|
||||||
let modalMap = null;
|
let modalMap = null;
|
||||||
let modalMarker = null;
|
let modalMarker = null;
|
||||||
@ -388,8 +545,10 @@ function placeModalMarker(lat, lng) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Sync map marker when user types into lat/lng fields
|
// Sync map marker when user types into lat/lng fields
|
||||||
['f_lat', 'f_lng'].forEach(id => {
|
['f_lat', 'f_lng'].forEach(elId => {
|
||||||
document.getElementById(id).addEventListener('input', () => {
|
const el = document.getElementById(elId);
|
||||||
|
if (!el) return;
|
||||||
|
el.addEventListener('input', () => {
|
||||||
const lat = parseFloat(document.getElementById('f_lat').value);
|
const lat = parseFloat(document.getElementById('f_lat').value);
|
||||||
const lng = parseFloat(document.getElementById('f_lng').value);
|
const lng = parseFloat(document.getElementById('f_lng').value);
|
||||||
if (!isNaN(lat) && !isNaN(lng) && modalMap) {
|
if (!isNaN(lat) && !isNaN(lng) && modalMap) {
|
||||||
@ -409,7 +568,6 @@ lotModalEl.addEventListener('shown.bs.modal', () => {
|
|||||||
|
|
||||||
// ── Modal logic ───────────────────────────────────────────────────────────
|
// ── Modal logic ───────────────────────────────────────────────────────────
|
||||||
let editingId = null;
|
let editingId = null;
|
||||||
const modal = new bootstrap.Modal(lotModalEl, { backdrop: 'static' });
|
|
||||||
|
|
||||||
document.getElementById('addLotBtn').onclick = () => {
|
document.getElementById('addLotBtn').onclick = () => {
|
||||||
editingId = null;
|
editingId = null;
|
||||||
@ -417,7 +575,7 @@ document.getElementById('addLotBtn').onclick = () => {
|
|||||||
document.getElementById('modalLabel').textContent = 'إضافة موقف جديد';
|
document.getElementById('modalLabel').textContent = 'إضافة موقف جديد';
|
||||||
document.getElementById('submitText').textContent = 'حفظ الموقف';
|
document.getElementById('submitText').textContent = 'حفظ الموقف';
|
||||||
document.getElementById('imagePreviewWrap').style.display = 'none';
|
document.getElementById('imagePreviewWrap').style.display = 'none';
|
||||||
modal.show();
|
// modal opened by data-bs-toggle on the button
|
||||||
};
|
};
|
||||||
|
|
||||||
function previewImage(input) {
|
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;
|
editingId = id;
|
||||||
setBtnLoading(true);
|
document.getElementById('modalLabel').textContent = 'تعديل: ' + lot.name;
|
||||||
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('submitText').textContent = 'تحديث الموقف';
|
||||||
document.getElementById('f_name').value = data.name;
|
document.getElementById('f_name').value = lot.name;
|
||||||
document.getElementById('f_capacity').value = data.total_capacity;
|
document.getElementById('f_capacity').value = lot.total_capacity;
|
||||||
document.getElementById('f_price').value = data.price_per_hour;
|
document.getElementById('f_price').value = lot.price_per_hour;
|
||||||
document.getElementById('f_hours').value = data.working_hours;
|
document.getElementById('f_hours').value = lot.working_hours;
|
||||||
document.getElementById('f_lat').value = data.latitude;
|
document.getElementById('f_lat').value = lot.latitude;
|
||||||
document.getElementById('f_lng').value = data.longitude;
|
document.getElementById('f_lng').value = lot.longitude;
|
||||||
document.getElementById('f_address').value = data.address;
|
document.getElementById('f_address').value = lot.address;
|
||||||
document.getElementById('f_image').value = '';
|
document.getElementById('f_image').value = '';
|
||||||
// Show existing image preview
|
|
||||||
const wrap = document.getElementById('imagePreviewWrap');
|
const wrap = document.getElementById('imagePreviewWrap');
|
||||||
const img = document.getElementById('imagePreview');
|
const img = document.getElementById('imagePreview');
|
||||||
if (data.image) {
|
if (lot.image) {
|
||||||
img.src = '/storage/' + data.image;
|
img.src = '/storage/' + lot.image;
|
||||||
wrap.style.display = 'block';
|
wrap.style.display = 'block';
|
||||||
} else {
|
} else {
|
||||||
wrap.style.display = 'none';
|
wrap.style.display = 'none';
|
||||||
}
|
}
|
||||||
modal.show();
|
// modal opened by data-bs-toggle on the button
|
||||||
} catch { alert('خطأ في تحميل البيانات'); }
|
|
||||||
finally { setBtnLoading(false); }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById('lotForm').onsubmit = async (e) => {
|
async function saveLot() {
|
||||||
e.preventDefault();
|
|
||||||
setBtnLoading(true);
|
setBtnLoading(true);
|
||||||
try {
|
try {
|
||||||
const fd = new FormData(e.target);
|
const form = document.getElementById('lotForm');
|
||||||
|
const fd = new FormData(form);
|
||||||
if (editingId) fd.append('_method', 'PUT');
|
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',
|
method: 'POST',
|
||||||
|
headers: { 'Accept': 'application/json', 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content },
|
||||||
body: fd
|
body: fd
|
||||||
});
|
});
|
||||||
const result = await res.json();
|
const result = await res.json();
|
||||||
result.success ? location.reload() : alert(result.message || 'خطأ في العملية');
|
if (result.success) {
|
||||||
} catch { alert('خطأ في الاتصال'); }
|
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); }
|
finally { setBtnLoading(false); }
|
||||||
};
|
}
|
||||||
|
|
||||||
function setBtnLoading(on) {
|
function setBtnLoading(on) {
|
||||||
document.getElementById('submitBtn').disabled = on;
|
document.getElementById('submitBtn').disabled = on;
|
||||||
@ -497,6 +662,109 @@ function doSearch() {
|
|||||||
window.location.href = `/admin/parking-lots?search=${encodeURIComponent(t)}`;
|
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) {
|
async function deleteLot(id, name) {
|
||||||
if (!confirm(`حذف الموقف "${name}"؟\nسيتم حذف جميع بيانات الموقف نهائياً.`)) return;
|
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::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::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}', [\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::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');
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user