feat: implement tour management logic and add store/update form requests
This commit is contained in:
@@ -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', 'تور بهصورت دائمی حذف شد.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Admin\Tour;
|
||||
|
||||
use App\Enums\Admin\Tour\TourMode;
|
||||
use App\Enums\Admin\Tour\TourScope;
|
||||
use App\Enums\Admin\Tour\TourType;
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class TourStoreRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, ValidationRule|array<mixed>|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',
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Admin\Tour;
|
||||
|
||||
use App\Enums\Admin\Tour\TourMode;
|
||||
use App\Enums\Admin\Tour\TourScope;
|
||||
use App\Enums\Admin\Tour\TourType;
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class TourUpdateRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, ValidationRule|array<mixed>|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',
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user