diff --git a/app/Enums/Admin/AdminPermission.php b/app/Enums/Admin/AdminPermission.php index 1fcfca8..fc25436 100644 --- a/app/Enums/Admin/AdminPermission.php +++ b/app/Enums/Admin/AdminPermission.php @@ -7,9 +7,7 @@ enum AdminPermission:string case MANAGE_ADMINS = 'manage-admins'; case MANAGE_TOURS = 'manage-tours'; case MANAGE_FLIGHTS = 'manage-flights'; - case MANAGE_COMMENTS = 'manage-comments'; case MANAGE_USERS = 'manage-users'; - case MANAGE_SETTINGS = 'manage-settings'; public function label(): string { @@ -17,9 +15,7 @@ enum AdminPermission:string self::MANAGE_ADMINS => 'مدیریت مدیران', self::MANAGE_TOURS => 'مدیریت تورها', self::MANAGE_FLIGHTS => 'مدیریت پروازها', - self::MANAGE_COMMENTS => 'مدیریت نظرات', self::MANAGE_USERS => 'مدیریت کاربران', - self::MANAGE_SETTINGS => 'تنظیمات سایت', }; } } diff --git a/app/Http/Controllers/Admin/FlightController.php b/app/Http/Controllers/Admin/FlightController.php index 176a450..0756c60 100644 --- a/app/Http/Controllers/Admin/FlightController.php +++ b/app/Http/Controllers/Admin/FlightController.php @@ -4,13 +4,20 @@ namespace App\Http\Controllers\Admin; use App\Http\Controllers\Controller; use App\Http\Requests\Admin\Flight\FlightStoreRequest; +use App\Http\Requests\Admin\Flight\FlightUpdateRequest; use App\Models\Airline; use App\Models\Airport; use App\Models\City; use App\Models\Country; +use App\Models\File; use App\Models\Flight; +use App\Models\FlightImage; +use Hekmatinasser\Verta\Verta; use Illuminate\Http\Request; use Illuminate\Http\JsonResponse; +use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Log; +use Illuminate\Support\Facades\Storage; class FlightController extends Controller { @@ -62,6 +69,7 @@ class FlightController extends Controller )); } + public function citiesByCountry(Country $country): JsonResponse { $cities = City::query() @@ -74,6 +82,7 @@ class FlightController extends Controller return response()->json($cities); } + public function airportsByCity(City $city): JsonResponse { $airports = Airport::query() @@ -89,7 +98,78 @@ class FlightController extends Controller public function store(FlightStoreRequest $request) { + $data = $request->validated(); + DB::beginTransaction(); + + try { + $departureTime = Verta::parse( + $data['departure_time'] + )->datetime(); + + $arrivalTime = Verta::parse( + $data['arrival_time'] + )->datetime(); + + $flight = Flight::create([ + 'airline_id' => $data['airline_id'], + 'origin_airport_id' => $data['origin_airport_id'], + 'destination_airport_id' => $data['destination_airport_id'], + + 'flight_number' => $data['flight_number'], + 'trip_type' => $data['trip_type'], + 'flight_type' => $data['flight_type'], + 'flight_scope' => $data['flight_scope'], + + 'departure_time' => $departureTime, + 'arrival_time' => $arrivalTime, + + 'price' => $data['price'], + 'capacity' => $data['capacity'], + 'status' => $data['status'], + ]); + + $isDefault = true; + + if (!empty($data['images'])) { + foreach ($data['images'] as $image) { + $imageName = $flight->id . '_' . time() . rand(11111, 99999) . '.' . $image->extension(); + + $path = $image->storeAs('flight_images', $imageName, 'public'); + + $file = File::create([ + 'name' => $imageName, + 'size' => $image->getSize(), + 'original_name' => $image->getClientOriginalName(), + 'path' => $path, + 'extension' => $image->extension(), + ]); + + FlightImage::create([ + 'flight_id' => $flight->id, + 'file_id' => $file->id, + 'is_default' => $isDefault, + ]); + + $isDefault = false; + } + } + + DB::commit(); + + } catch (\Exception $exception) { + DB::rollBack(); + + Log::error($exception); + + return back() + ->withInput() + ->with('error', 'ثبت پرواز با خطا مواجه شد.'); + } + + return redirect() + ->route('admin.flights.index') + ->with('success', 'پرواز با موفقیت ثبت شد.'); } public function edit(Flight $flight) @@ -117,21 +197,195 @@ class FlightController extends Controller )); } - public function update() + public function destroyImage(Flight $flight, FlightImage $image) { + $image = $flight->images() + ->with('file') + ->findOrFail($image->id); + $file = $image->file; + + DB::transaction(function () use ($flight, $image) { + $image->delete(); + + $hasDefaultImage = $flight->images() + ->where('is_default', true) + ->exists(); + + if (! $hasDefaultImage) { + $newDefaultImage = $flight->images() + ->orderBy('sort_order') + ->orderBy('id') + ->first(); + + $newDefaultImage?->update([ + 'is_default' => true, + ]); + } + }); + + deleteFile($file); + + return redirect() + ->route('admin.flights.edit', $flight) + ->with('success', 'تصویر پرواز با موفقیت حذف شد.'); } - public function destroy() + public function update(FlightUpdateRequest $request, Flight $flight) { + $data = $request->validated(); + DB::beginTransaction(); + + try { + $departureTime = Verta::parse( + $data['departure_time'] + )->datetime(); + + $arrivalTime = Verta::parse( + $data['arrival_time'] + )->datetime(); + + $flight->update([ + 'airline_id' => $data['airline_id'], + 'origin_airport_id' => $data['origin_airport_id'], + 'destination_airport_id' => $data['destination_airport_id'], + + 'flight_number' => $data['flight_number'], + 'trip_type' => $data['trip_type'], + 'flight_type' => $data['flight_type'], + 'flight_scope' => $data['flight_scope'], + + 'departure_time' => $departureTime, + 'arrival_time' => $arrivalTime, + + 'price' => $data['price'], + 'capacity' => $data['capacity'], + 'status' => $data['status'], + ]); + if (!empty($data['images'])) { + $hasDefault = $flight->images() + ->where('is_default', true) + ->exists(); + + $nextSort = (int)$flight->images() + ->max('sort_order'); + + foreach ($data['images'] as $index => $image) { + $imageName = $flight->id . '_' . time() . rand(11111, 99999) . '.' . $image->extension(); + + $path = $image->storeAs( + 'flight_images', + $imageName, + 'public' + ); + + $file = File::create([ + 'name' => $imageName, + 'size' => $image->getSize(), + 'original_name' => $image->getClientOriginalName(), + 'path' => $path, + 'extension' => $image->extension(), + ]); + + FlightImage::create([ + 'flight_id' => $flight->id, + 'file_id' => $file->id, + 'sort_order' => $nextSort + $index + 1, + 'is_default' => !$hasDefault && $index === 0, + ]); + } + } + + DB::commit(); + + } catch (\Exception $exception) { + + DB::rollBack(); + + Log::error($exception); + + return back() + ->withInput() + ->with('error', 'ویرایش پرواز با خطا مواجه شد.'); + } + + return redirect() + ->route('admin.flights.index') + ->with('success', 'پرواز با موفقیت ویرایش شد.'); } - public function restore() - { + + public function destroy(Flight $flight) + { + $flight->delete(); + + return redirect() + ->route('admin.flights.index') + ->with('success', 'پرواز با موفقیت به سطل زباله منتقل شد.'); } - public function forceDelete() - { + public function trash() + { + $title = 'پروازهای حذف شده'; + + $flights = Flight::onlyTrashed() + ->with([ + 'airline', + 'origin_airport.city', + 'airport.city', + 'coverImage.file', + ]) + ->orderByDesc('deleted_at') + ->paginate(4); + + return view('admin.flights.trash', compact('title', 'flights')); + } + + public function restore($id) + { + $flight = Flight::onlyTrashed() + ->findOrFail($id); + + $flight->restore(); + + return redirect() + ->route('admin.flights.trash.index') + ->with('success', 'پرواز با موفقیت بازگردانی شد.'); + } + + public function forceDelete($id) + { + $flight = Flight::onlyTrashed() + ->with('images.file') + ->findOrFail($id); + + try { + DB::transaction(function () use ($flight) { + foreach ($flight->images as $image) { + $file = $image->file; + + if ($file) { + Storage::disk('public')->delete($file->path); + + $file->delete(); + } + + $image->delete(); + } + + $flight->forceDelete(); + }); + + return redirect() + ->route('admin.flights.trash.index') + ->with('success', 'پرواز و تمام اطلاعات مرتبط با موفقیت حذف دائمی شدند.'); + + } catch (\Throwable $exception) { + report($exception); + + return back() + ->with('error', 'حذف دائمی پرواز انجام نشد.'); + } } } diff --git a/app/Http/Requests/Admin/Flight/FlightStoreRequest.php b/app/Http/Requests/Admin/Flight/FlightStoreRequest.php index 4c6a929..dc59476 100644 --- a/app/Http/Requests/Admin/Flight/FlightStoreRequest.php +++ b/app/Http/Requests/Admin/Flight/FlightStoreRequest.php @@ -2,8 +2,14 @@ namespace App\Http\Requests\Admin\Flight; +use App\Enums\Flight\FlightScope; +use App\Enums\Flight\FlightStatus; +use App\Enums\Flight\FlightTripType; +use App\Enums\Flight\FlightType; +use Hekmatinasser\Verta\Facades\Verta; use Illuminate\Contracts\Validation\ValidationRule; use Illuminate\Foundation\Http\FormRequest; +use Illuminate\Validation\Rule; class FlightStoreRequest extends FormRequest { @@ -12,7 +18,7 @@ class FlightStoreRequest extends FormRequest */ public function authorize(): bool { - return false; + return true; } /** @@ -23,7 +29,112 @@ class FlightStoreRequest extends FormRequest public function rules(): array { return [ - // + 'flight_number' => [ + 'required', + 'string', + 'max:20', + 'unique:flights,flight_number', + ], + + 'airline_id' => [ + 'required', + 'integer', + 'exists:airlines,id', + ], + + 'flight_scope' => [ + 'required', + Rule::enum(FlightScope::class), + ], + + 'flight_type' => [ + 'required', + Rule::enum(FlightType::class), + ], + + 'trip_type' => [ + 'required', + Rule::enum(FlightTripType::class), + ], + + 'status' => [ + 'required', + Rule::enum(FlightStatus::class), + ], + + 'origin_country_id' => [ + 'required', + 'integer', + 'exists:countries,id', + ], + + 'destination_country_id' => [ + 'required', + 'integer', + 'exists:countries,id', + ], + + 'origin_city_id' => [ + 'required', + 'integer', + 'exists:cities,id', + 'different:destination_city_id' + ], + + 'destination_city_id' => [ + 'required', + 'integer', + 'exists:cities,id', + ], + + 'origin_airport_id' => [ + 'required', + 'integer', + 'exists:airports,id', + 'different:destination_airport_id', + ], + + 'destination_airport_id' => [ + 'required', + 'integer', + 'exists:airports,id', + ], + 'departure_time' => ['required', 'string'], + 'arrival_time' => [ + 'required', + 'string', + function ($attribute, $value, $fail) { + $departure = $this->input('departure_time'); + if ($departure && Verta::parse($value)->datetime() <= Verta::parse($departure)->datetime()) { + $fail('زمان رسیدن باید بعد از زمان حرکت باشد.'); + } + }, + ], + 'price' => [ + 'required', + 'integer', + 'min:0', + ], + + 'capacity' => [ + 'required', + 'integer', + 'min:1', + 'max:1000', + ], + + 'images' => [ + 'nullable', + 'array', + 'max:5', + ], + + 'images.*' => [ + 'file', + 'image', + 'mimes:jpg,jpeg,png,webp', + 'max:800', + ], ]; } } diff --git a/app/Http/Requests/Admin/Flight/FlightUpdateRequest.php b/app/Http/Requests/Admin/Flight/FlightUpdateRequest.php new file mode 100644 index 0000000..f416f16 --- /dev/null +++ b/app/Http/Requests/Admin/Flight/FlightUpdateRequest.php @@ -0,0 +1,151 @@ +|string> + */ + public function rules(): array + { + return [ + 'flight_number' => [ + 'required', + 'string', + 'max:20', + Rule::unique('flights', 'flight_number') + ->ignore($this->route('flight')), + ], + + 'airline_id' => [ + 'required', + 'exists:airlines,id', + ], + + 'flight_scope' => [ + 'required', + Rule::enum(FlightScope::class), + ], + + 'flight_type' => [ + 'required', + Rule::enum(FlightType::class), + ], + + 'trip_type' => [ + 'required', + Rule::enum(FlightTripType::class), + ], + + 'status' => [ + 'required', + Rule::enum(FlightStatus::class), + ], + + 'origin_country_id' => [ + 'required', + 'exists:countries,id', + ], + + 'destination_country_id' => [ + 'required', + 'exists:countries,id', + ], + + 'origin_city_id' => [ + 'required', + 'exists:cities,id', + 'different:destination_city_id', + ], + + 'destination_city_id' => [ + 'required', + 'exists:cities,id', + ], + + 'origin_airport_id' => [ + 'required', + 'exists:airports,id', + 'different:destination_airport_id', + ], + + 'destination_airport_id' => [ + 'required', + 'exists:airports,id', + ], + + 'departure_time' => [ + 'required', + 'string', + function (string $attribute, mixed $value, \Closure $fail) { + try { + Verta::parse($value); + } catch (\Throwable $e) { + $fail('فرمت زمان حرکت معتبر نیست.'); + } + }, + ], + + 'arrival_time' => [ + 'required', + 'string', + function (string $attribute, mixed $value, \Closure $fail) { + try { + $departureTime = Verta::parse($this->input('departure_time')); + $arrivalTime = Verta::parse($value); + + if ($arrivalTime->timestamp <= $departureTime->timestamp) { + $fail('زمان رسیدن باید بعد از زمان حرکت باشد.'); + } + } catch (\Throwable $e) { + $fail('فرمت زمان رسیدن معتبر نیست.'); + } + }, + ], + + 'price' => [ + 'required', + 'numeric', + 'min:0', + ], + + 'capacity' => [ + 'required', + 'integer', + 'min:1', + ], + + 'images' => [ + 'nullable', + 'array', + 'max:5', + ], + + 'images.*' => [ + 'image', + 'mimes:jpg,jpeg,png,webp', + 'max:800', + ], + ]; + } +} diff --git a/app/Models/Flight.php b/app/Models/Flight.php index 575b201..f587c82 100644 --- a/app/Models/Flight.php +++ b/app/Models/Flight.php @@ -16,6 +16,7 @@ use Illuminate\Database\Eloquent\Attributes\Scope; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\SoftDeletes; /** * Class Flight @@ -43,6 +44,7 @@ use Illuminate\Database\Eloquent\Model; */ class Flight extends Model { + use SoftDeletes; protected $table = 'flights'; public static $snakeAttributes = false; @@ -52,12 +54,13 @@ class Flight extends Model 'destination_airport_id' => 'int', 'file_id' => 'int', 'departure_time' => 'datetime', + 'arrival_time'=> 'datetime', 'price' => 'int', 'capacity' => 'int', 'status' => FlightStatus::class, 'trip_type' => FlightTripType::class, 'flight_scope'=> FlightScope::class, - 'flight_type', FlightType::class, + 'flight_type'=> FlightType::class, 'views_count' => 'int' ]; @@ -67,6 +70,7 @@ class Flight extends Model 'destination_airport_id', 'file_id', 'flight_number', + 'arrival_time', 'departure_time', 'price', 'capacity', @@ -74,7 +78,8 @@ class Flight extends Model 'trip_type', 'flight_scope', 'flight_type', - 'views_count' + 'views_count', + 'is_default' ]; #[Scope] protected function applySort(Builder $query, ?string $sort = null): void diff --git a/resources/views/admin/flights/create.blade.php b/resources/views/admin/flights/create.blade.php index f82ce67..23030e1 100644 --- a/resources/views/admin/flights/create.blade.php +++ b/resources/views/admin/flights/create.blade.php @@ -124,18 +124,18 @@ * - @foreach(\App\Enums\Flight\FlightTripType::cases() as $tripType) @endforeach - @error('flight_trip_type') + @error('trip_type') {{ $message }} @enderror @@ -168,7 +168,7 @@ * - @foreach($countries as $country) @@ -178,6 +178,9 @@ @endforeach + @error('origin_country_id') + {{ $message }} + @enderror
@@ -186,7 +189,7 @@ * - @foreach($countries as $country) @@ -196,6 +199,9 @@ @endforeach + @error('destination_country_id') + {{ $message }} + @enderror
@@ -204,10 +210,12 @@ * - + @error('origin_city_id') + {{ $message }} + @enderror
@@ -216,10 +224,12 @@ * - + @error('destination_city_id') + {{ $message }} + @enderror
@@ -246,12 +256,12 @@ - @error('airport_id') + @error('destination_airport_id') {{ $message }} @enderror
@@ -399,7 +409,7 @@ const airportsRoute = '{{ route('admin.flights.airports-by-city', ['city' => '__CITY__']) }}'; const oldOriginAirportId = '{{ old('origin_airport_id') }}'; - const oldDestinationAirportId = '{{ old('airport_id') }}'; + const oldDestinationAirportId = '{{ old('destination_airport_id') }}'; const datePickerOptions = { format: 'YYYY/MM/DD HH:mm', @@ -515,7 +525,7 @@ resetSelect('#origin_city_id', 'ابتدا کشور مبدأ را انتخاب کنید'); resetSelect('#destination_city_id', 'ابتدا کشور مقصد را انتخاب کنید'); resetSelect('#origin_airport_id', 'ابتدا شهر مبدأ را انتخاب کنید'); - resetSelect('#airport_id', 'ابتدا شهر مقصد را انتخاب کنید'); + resetSelect('#destination_airport_id', 'ابتدا شهر مقصد را انتخاب کنید'); } function toggleScopeUI() { @@ -523,7 +533,7 @@ resetLocations(); - if (scope === '1') { + if (scope === '0') { $('.country-wrapper').hide(); $('#origin_country_id').val(iranCountryId).trigger('change'); @@ -532,7 +542,7 @@ return; } - if (scope === '2') { + if (scope === '1') { $('.country-wrapper').show(); $('#origin_country_id').val('').trigger('change'); @@ -561,7 +571,7 @@ $('#destination_country_id').on('change', function () { const countryId = $(this).val(); - resetSelect('#airport_id', 'ابتدا شهر مقصد را انتخاب کنید'); + resetSelect('#destination_airport_id', 'ابتدا شهر مقصد را انتخاب کنید'); loadCities( countryId, @@ -582,7 +592,7 @@ $('#destination_city_id').on('change', function () { loadAirports( $(this).val(), - '#airport_id', + '#destination_airport_id', 'فرودگاه مقصد را انتخاب کنید', oldDestinationAirportId ); diff --git a/resources/views/admin/flights/edit.blade.php b/resources/views/admin/flights/edit.blade.php index 11ee991..1415a2f 100644 --- a/resources/views/admin/flights/edit.blade.php +++ b/resources/views/admin/flights/edit.blade.php @@ -7,8 +7,11 @@