From be06e60f2c3bf96645e5cc180a73e375950880f8 Mon Sep 17 00:00:00 2001 From: mina-esn Date: Thu, 20 Aug 2026 12:00:47 +0330 Subject: [PATCH] feat: implement tour management logic and add store/update form requests --- app/Http/Controllers/Admin/TourController.php | 330 +++++++++++++++++- .../Requests/Admin/Tour/TourStoreRequest.php | 97 +++++ .../Requests/Admin/Tour/TourUpdateRequest.php | 96 +++++ 3 files changed, 522 insertions(+), 1 deletion(-) create mode 100644 app/Http/Requests/Admin/Tour/TourStoreRequest.php create mode 100644 app/Http/Requests/Admin/Tour/TourUpdateRequest.php diff --git a/app/Http/Controllers/Admin/TourController.php b/app/Http/Controllers/Admin/TourController.php index bcd6c7f..047fd91 100644 --- a/app/Http/Controllers/Admin/TourController.php +++ b/app/Http/Controllers/Admin/TourController.php @@ -2,10 +2,338 @@ namespace App\Http\Controllers\Admin; +use App\Enums\Admin\Tour\TourScope; use App\Http\Controllers\Controller; +use App\Http\Requests\Admin\Tour\TourStoreRequest; +use App\Http\Requests\Admin\Tour\TourUpdateRequest; +use App\Models\City; +use App\Models\Country; +use App\Models\File; +use App\Models\Tour; +use App\Models\TourDeparture; +use App\Models\TourImage; +use Hekmatinasser\Verta\Verta; use Illuminate\Http\Request; +use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Log; + class TourController extends Controller { - // + public function index(Request $request) + { + $title = 'جزئیات تور'; + $search = $request->input('search'); + $sort = $request->input('sort'); + + $tours = Tour::query() + ->with([ + 'originCity', + 'city', + 'file', + ]) + ->withCount('tourDepartures') + ->when($search, function ($query) use ($search) { + $query->where(function ($query) use ($search) { + $query->where('title', 'like', "%{$search}%") + ->orWhere('summary', 'like', "%{$search}%") + ->orWhereHas('originCity', function ($query) use ($search) { + $query->where('name', 'like', "%{$search}%"); + }) + ->orWhereHas('city', function ($query) use ($search) { + $query->where('name', 'like', "%{$search}%"); + }); + }); + }) + ->applySort($sort) + ->paginate(3) + ->withQueryString(); + + return view('admin.tours.index', compact('title', 'tours')); + } + + public function create() + { + $title = 'ایجاد تور'; + + $cities = City::query() + ->orderBy('name') + ->get(); + + $countries = Country::query() + ->orderBy('name') + ->get(); + + return view('admin.tours.create', compact('title', 'cities', 'countries')); + } + + public function citiesByCountry(Country $country) + { + $cities = City::query() + ->where('county_id', $country->id) + ->orderBy('name') + ->get(['id', 'name']); + + return response()->json($cities); + } + + public function store(TourStoreRequest $request) + { + $data = $request->validated(); + + try { + $startDate = Verta::parseFormat('Y/m/d H:i', $data['start_date'])->toCarbon(); + $endDate = Verta::parseFormat('Y/m/d H:i', $data['end_date'])->toCarbon(); + } catch (\Throwable $e) { + return back() + ->withErrors(['start_date' => 'فرمت تاریخ و ساعت وارد شده معتبر نیست.']) + ->withInput(); + } + + if ($startDate < now()) { + return back() + ->withErrors(['start_date' => 'تاریخ حرکت نمی‌تواند قبل از زمان و ساعت جاری باشد.']) + ->withInput(); + } + + if ($endDate <= $startDate) { + return back() + ->withErrors(['end_date' => 'تاریخ برگشت باید بعد از تاریخ حرکت باشد.']) + ->withInput(); + } + + DB::beginTransaction(); + + try { + $tour = Tour::create([ + 'title' => $data['title'], + 'summary' => $data['summary'] ?? null, + 'description' => $data['description'] ?? null, + 'origin_city_id' => $data['origin_city_id'], + 'destination_city_id' => $data['destination_city_id'], + 'tour_scope' => $data['tour_scope'], + 'tour_mode' => $data['tour_mode'], + 'tour_type' => $data['tour_type'], + 'views_count' => 0, + ]); + + TourDeparture::create([ + 'tour_id' => $tour->id, + 'start_date' => $startDate, + 'end_date' => $endDate, + 'price' => $data['price'], + 'capacity' => $data['capacity'], + 'status' => 1, + ]); + if (!empty($data['images'])) { + $isDefault = true; + + foreach ($data['images'] as $index => $image) { + $imageName = $tour->id . '_' . time() . rand(11111, 99999) . '.' . $image->extension(); + + $path = $image->storeAs('tour_images', $imageName, 'public'); + + $file = File::create([ + 'name' => $imageName, + 'size' => $image->getSize(), + 'original_name' => $image->getClientOriginalName(), + 'path' => $path, + 'extension' => $image->extension() + ]); + + TourImage::create([ + 'tour_id' => $tour->id, + 'file_id' => $file->id, + 'sort_order' => $index + 1, + 'is_default' => $isDefault + ]); + + if ($isDefault) { + $isDefault = false; + } + } + } + + DB::commit(); + + return redirect() + ->route('admin.tours.index') + ->with('success', 'تور با موفقیت ایجاد شد.'); + + } catch (\Throwable $e) { + DB::rollBack(); + Log::error('خطا در ایجاد تور: ' . $e->getMessage()); + + return back() + ->withErrors(['error' => 'خطایی در ثبت اطلاعات رخ داد. لطفاً دوباره تلاش کنید.']) + ->withInput(); + } + } + + public function edit(Tour $tour) + { + $title = 'ویرایش تور'; + + $tour->load(['originCity', 'city', 'images', 'tourDepartures']); + + $departure = $tour->tourDepartures->first(); + + $cities = City::query()->orderBy('name')->get(); + $countries = Country::query()->orderBy('name')->get(); + + $selectedCountryId = null; + $destinationCities = $cities; + + if ($tour->tour_scope === TourScope::INTERNATIONAL && $tour->city) { + $selectedCountryId = $tour->city->county_id; + + $destinationCities = City::query() + ->where('county_id', $selectedCountryId) + ->orderBy('name') + ->get(); + } + + return view('admin.tours.edit', compact( + 'title', 'tour', 'departure', 'cities', 'countries', + 'selectedCountryId', 'destinationCities' + )); + } + + public function update(TourUpdateRequest $request, Tour $tour) + { + $data = $request->validated(); + try { + $startDate = Verta::parse($data['start_date'])->datetime(); + $endDate = Verta::parse($data['end_date'])->datetime(); + } catch (\Throwable $e) { + return back() + ->withErrors(['start_date' => 'فرمت تاریخ و ساعت وارد شده معتبر نیست.']) + ->withInput(); + } + + if ($endDate <= $startDate) { + return back() + ->withErrors(['end_date' => 'تاریخ برگشت باید بعد از تاریخ حرکت باشد.']) + ->withInput(); + } + + DB::beginTransaction(); + + try { + $tour->update([ + 'title' => $data['title'], + 'summary' => $data['summary'] ?? null, + 'description' => $data['description'] ?? null, + 'origin_city_id' => $data['origin_city_id'], + 'destination_city_id' => $data['destination_city_id'], + 'tour_scope' => $data['tour_scope'], + 'tour_mode' => $data['tour_mode'], + 'tour_type' => $data['tour_type'], + ]); + + $departure = $tour->tourDepartures()->first(); + + if ($departure) { + $departure->update([ + 'start_date' => $startDate, + 'end_date' => $endDate, + 'price' => $data['price'], + 'capacity' => $data['capacity'], + ]); + } else { + TourDeparture::create([ + 'tour_id' => $tour->id, + 'start_date' => $startDate, + 'end_date' => $endDate, + 'price' => $data['price'], + 'capacity' => $data['capacity'], + 'status' => 1, + ]); + } + + if (!empty($data['images'])) { + $hasDefault = $tour->images()->where('is_default', true)->exists(); + $nextSort = (int)$tour->images()->max('sort_order'); + + foreach ($data['images'] as $index => $image) { + $imageName = $tour->id . '_' . time() . rand(11111, 99999) . '.' . $image->extension(); + $path = $image->storeAs('tour_images', $imageName, 'public'); + + $file = File::create([ + 'name' => $imageName, + 'size' => $image->getSize(), + 'original_name' => $image->getClientOriginalName(), + 'path' => $path, + 'extension' => $image->extension(), + ]); + + TourImage::create([ + 'tour_id' => $tour->id, + 'file_id' => $file->id, + 'sort_order' => $nextSort + $index + 1, + 'is_default' => !$hasDefault && $index === 0, + ]); + } + } + + DB::commit(); + + return redirect() + ->route('admin.tours.index') + ->with('success', 'تور با موفقیت ویرایش شد.'); + + } catch (\Exception $exception) { + DB::rollBack(); + Log::error('خطا در ویرایش تور: ' . $exception->getMessage()); + + return back() + ->withErrors(['error' => 'خطایی در ثبت اطلاعات رخ داد. لطفاً دوباره تلاش کنید.']) + ->withInput(); + } + } + + public function trash() + { + $title = 'تورهای حذف شده'; + $tours = Tour::onlyTrashed() + ->with([ + 'originCity', + 'city', + 'coverImage.file', + ]) + ->orderByDesc('deleted_at') + ->paginate(4); + return view('admin.tours.trash', compact('title', 'tours')); + } + + public function destroy(Tour $tour) + { + $tour->delete(); + + return redirect() + ->route('admin.tours.index') + ->with('success', 'تور با موفقیت به سطل زباله منتقل شد.'); + } + + public function restore( $id) + { + $tour = Tour::onlyTrashed()->findOrFail($id); + + $tour->restore(); + + return redirect() + ->route('admin.tours.trash.index') + ->with('success', 'تور با موفقیت بازگردانی شد.'); + } + + public function forceDelete($id) + { + $tour = Tour::onlyTrashed()->findOrFail($id); + + $tour->forceDelete(); + + return redirect() + ->route('admin.tours.trash.index') + ->with('success', 'تور به‌صورت دائمی حذف شد.'); + } } diff --git a/app/Http/Requests/Admin/Tour/TourStoreRequest.php b/app/Http/Requests/Admin/Tour/TourStoreRequest.php new file mode 100644 index 0000000..9d39035 --- /dev/null +++ b/app/Http/Requests/Admin/Tour/TourStoreRequest.php @@ -0,0 +1,97 @@ +|string> + */ + public function rules(): array + { + return [ + 'title' => [ + 'required', + 'string', + 'max:255' + ], + 'summary' => [ + 'required', + 'string' + ], + 'description' => [ + 'nullable', + 'string' + ], + 'origin_city_id' => [ + 'required', + 'integer', + 'exists:cities,id' + ], + 'destination_city_id' => [ + 'required', + 'integer', + 'exists:cities,id', + 'different:origin_city_id' + ], + 'tour_scope' => [ + 'required', + Rule::enum(TourScope::class), + ], + 'tour_mode' => [ + 'required', + Rule::enum(TourMode::class), + ], + 'tour_type' => [ + 'required', + Rule::enum(TourType::class), + ], + 'start_date' => [ + 'required', + 'string' + ], + 'end_date' => [ + 'required', + 'string' + ], + 'price' => [ + 'required', + 'integer', + 'min:0' + ], + 'capacity' => [ + 'required', + 'integer', + 'min:1' + ], + 'images' => [ + 'nullable', + 'array', + 'max:5', + ], + + 'images.*' => [ + 'file', + 'mimes:jpg,jpeg,png,webp', + 'max:800', + ], + ]; + } +} diff --git a/app/Http/Requests/Admin/Tour/TourUpdateRequest.php b/app/Http/Requests/Admin/Tour/TourUpdateRequest.php new file mode 100644 index 0000000..3606fb3 --- /dev/null +++ b/app/Http/Requests/Admin/Tour/TourUpdateRequest.php @@ -0,0 +1,96 @@ +|string> + */ + public function rules(): array + { + return [ + 'title' => [ + 'required', + 'string', + 'max:255' + ], + 'summary' => [ + 'required', + 'string' + ], + 'description' => [ + 'nullable', + 'string' + ], + 'origin_city_id' => [ + 'required', + 'integer', + 'exists:cities,id' + ], + 'destination_city_id' => [ + 'required', + 'integer', + 'exists:cities,id', + 'different:origin_city_id' + ], + 'tour_scope' => [ + 'required', + Rule::enum(TourScope::class), + ], + 'tour_mode' => [ + 'required', + Rule::enum(TourMode::class), + ], + 'tour_type' => [ + 'required', + Rule::enum(TourType::class), + ], + 'start_date' => [ + 'required', + 'string' + ], + 'end_date' => [ + 'required', + 'string' + ], + 'price' => [ + 'required', + 'integer', + 'min:0' + ], + 'capacity' => [ + 'required', + 'integer', + 'min:1' + ], + 'images' => [ + 'nullable', + 'array', + 'max:5', + ], + 'images.*' => [ + 'file', + 'mimes:jpg,jpeg,png,webp', + 'max:800', + ], + ]; + } +}