diff --git a/app/Http/Controllers/Client/FastRequestController.php b/app/Http/Controllers/Client/FastRequestController.php index 3c46dc7..93315bf 100644 --- a/app/Http/Controllers/Client/FastRequestController.php +++ b/app/Http/Controllers/Client/FastRequestController.php @@ -2,32 +2,32 @@ namespace App\Http\Controllers\Client; -use App\Enums\UserEnum; use App\Http\Controllers\Controller; use App\Http\Requests\Client\FastRequestRequest; use App\Http\Resources\Client\DoctorResource; use App\Http\Resources\Client\FastRequestResource; use App\Http\Resources\Client\PetArchiveResource; -use App\Models\User; use App\Services\Document\FastRequestService; +use App\Services\Shift\ShiftScheduleService; use Illuminate\Support\Facades\Auth; class FastRequestController extends Controller { protected FastRequestService $fastRequestService; - public function __construct(FastRequestService $fastRequestService) + protected ShiftScheduleService $shiftScheduleService; + + public function __construct(FastRequestService $fastRequestService, ShiftScheduleService $shiftScheduleService) { $this->fastRequestService = $fastRequestService; + $this->shiftScheduleService = $shiftScheduleService; } public function create() { $pets = Auth::user()->pets()->latest()->get(); - $doctors = User::where('level', UserEnum::Doctor->value) - ->latest() - ->get(); + $doctors = $this->shiftScheduleService->availableDoctorsAt(now()); return response()->json([ 'pets' => PetArchiveResource::collection($pets), diff --git a/app/Http/Controllers/Client/PaymentController.php b/app/Http/Controllers/Client/PaymentController.php index e6ac324..c5c2195 100644 --- a/app/Http/Controllers/Client/PaymentController.php +++ b/app/Http/Controllers/Client/PaymentController.php @@ -17,6 +17,7 @@ use App\Services\Payment\ZibalPaymentService; use App\Services\Wallet\WalletService; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; +use Illuminate\Support\Facades\DB; class PaymentController extends Controller { @@ -68,7 +69,7 @@ class PaymentController extends Controller } if ($wasPending && $payment->status === PaymentStatusEnum::Paid) { - $this->settle($payment); + DB::transaction(fn () => $this->settle($payment)); } session()->flash('payment_result', [ diff --git a/app/Http/Requests/Client/FastRequestRequest.php b/app/Http/Requests/Client/FastRequestRequest.php index bb1b564..62957d7 100644 --- a/app/Http/Requests/Client/FastRequestRequest.php +++ b/app/Http/Requests/Client/FastRequestRequest.php @@ -8,6 +8,7 @@ use App\Enums\DocumentModalityEnum; use App\Enums\DocumentPriorityEnum; use App\Enums\PetGenderEnum; use App\Enums\PetTypeEnum; +use App\Enums\UserEnum; class FastRequestRequest extends FormRequest { @@ -22,7 +23,7 @@ class FastRequestRequest extends FormRequest 'modality' => ['required', 'string', Rule::in(DocumentModalityEnum::values())], 'priority' => ['required', 'string', Rule::in(DocumentPriorityEnum::keys())], 'note' => ['nullable', 'string', 'max:1000'], - 'pet_id' => ['nullable', 'exists:pets,id'], + 'pet_id' => ['nullable', Rule::exists('pets', 'id')->where('owner_id', $this->user()->id)], 'pet' => ['required_without:pet_id', 'array'], 'pet.name' => ['required_without:pet_id', 'nullable', 'string', 'max:255'], 'pet.type' => ['required_without:pet_id', 'nullable', 'string', Rule::in(PetTypeEnum::keys())], @@ -31,7 +32,7 @@ class FastRequestRequest extends FormRequest 'pet.age' => ['nullable', 'numeric', 'min:0', 'max:100'], 'pet.weight' => ['nullable', 'numeric', 'min:0', 'max:1000'], 'pet.sterilized' => ['nullable', 'boolean'], - 'doctor_id' => ['nullable', 'exists:users,id'], + 'doctor_id' => ['nullable', Rule::exists('users', 'id')->where('level', UserEnum::Doctor->value)], 'history_files' => ['nullable', 'array'], 'history_files.*' => ['file', 'image', 'max:10240'], 'scan_files' => ['nullable', 'array'], diff --git a/app/Services/Document/CaseService.php b/app/Services/Document/CaseService.php index c9d95b3..411a435 100644 --- a/app/Services/Document/CaseService.php +++ b/app/Services/Document/CaseService.php @@ -156,12 +156,13 @@ class CaseService ->where('invoices.paid_at', '>=', $monthStart); $nextShift = $isDoctor ? $this->shiftScheduleService->nextShiftStart($user, now()) : null; + $onShift = $isDoctor && $this->shiftScheduleService->isOnShift($user, now()); return [ 'pending_cases' => $this->pendingForUser($user)->count(), 'monthly_reviewed' => $reviewedQuery->count(), 'monthly_amount' => (int) $amountQuery->sum('invoices.amount'), - 'next_shift_hours' => $nextShift ? (int) ceil(now()->diffInMinutes($nextShift, true) / 60) : null, + 'next_shift_hours' => $onShift ? 0 : ($nextShift ? (int) ceil(now()->diffInMinutes($nextShift, true) / 60) : null), 'is_doctor' => $isDoctor, ]; } diff --git a/app/Services/Document/FastRequestService.php b/app/Services/Document/FastRequestService.php index ea6eca6..bb9567d 100644 --- a/app/Services/Document/FastRequestService.php +++ b/app/Services/Document/FastRequestService.php @@ -71,7 +71,7 @@ class FastRequestService protected function resolvePet(User $requester, array $data): Pet { if (!empty($data['pet_id'])) { - return Pet::findOrFail($data['pet_id']); + return $requester->pets()->findOrFail($data['pet_id']); } $pet = $data['pet']; diff --git a/app/Services/Shift/ShiftScheduleService.php b/app/Services/Shift/ShiftScheduleService.php index d66f026..1d84a00 100644 --- a/app/Services/Shift/ShiftScheduleService.php +++ b/app/Services/Shift/ShiftScheduleService.php @@ -60,12 +60,17 @@ class ShiftScheduleService ->get(); } + public function isOnShift(User $doctor, Carbon $moment): bool + { + return $this->shiftsCoveringMoment($moment)->contains('user_id', $doctor->id); + } + public function autoSelectDoctor(Carbon $moment): ?User { $doctorIds = $this->shiftsCoveringMoment($moment)->pluck('user_id')->filter()->unique(); if ($doctorIds->isEmpty()) { - return null; + return $this->doctorWithNearestShift($moment); } return User::where('level', UserEnum::Doctor->value) @@ -81,6 +86,20 @@ class ShiftScheduleService ->first(); } + // fallback when nobody is on shift: the doctor whose next shift starts soonest + public function doctorWithNearestShift(Carbon $from): ?User + { + $doctorIds = Shift::active()->whereNotNull('user_id')->distinct()->pluck('user_id'); + + return User::where('level', UserEnum::Doctor->value) + ->whereIn('id', $doctorIds) + ->get() + ->map(fn (User $doctor) => ['doctor' => $doctor, 'start' => $this->nextShiftStart($doctor, $from)]) + ->filter(fn (array $item) => $item['start'] !== null) + ->sortBy('start') + ->first()['doctor'] ?? null; + } + public function nextShiftStart(User $doctor, Carbon $from): ?Carbon { $shifts = Shift::active()->where('user_id', $doctor->id)->get(); diff --git a/resources/js/Client/components/dashboard/FastRequestModal.vue b/resources/js/Client/components/dashboard/FastRequestModal.vue index 3ac591e..a4aa89b 100644 --- a/resources/js/Client/components/dashboard/FastRequestModal.vue +++ b/resources/js/Client/components/dashboard/FastRequestModal.vue @@ -11,10 +11,14 @@ import RequestDoctor from './fastRequest/RequestDoctor.vue'; import RequestReview from './fastRequest/RequestReview.vue'; import InvoiceModal from './fastRequest/InvoiceModal.vue'; import { useFastRequestStore } from '../../store/FastRequest'; +import { useAnimatedHeight } from '../../composables/useAnimatedHeight'; const visible = defineModel('visible'); const fastRequestStore = useFastRequestStore(); +const stepContentRef = ref(null); +const stepContentHeight = useAnimatedHeight(stepContentRef); + const invoiceVisible = ref(false); const invoiceDocument = ref(null); @@ -83,7 +87,11 @@ const handleNext = async () => { - +
+
+ +
+
diff --git a/resources/js/Client/components/dashboard/fastRequest/RequestDoctor.vue b/resources/js/Client/components/dashboard/fastRequest/RequestDoctor.vue index 885a788..66b550f 100644 --- a/resources/js/Client/components/dashboard/fastRequest/RequestDoctor.vue +++ b/resources/js/Client/components/dashboard/fastRequest/RequestDoctor.vue @@ -19,6 +19,9 @@ const selectDoctor = (doctorId) => {

انتخاب خودکار

انتخاب در دسترس‌ ترین پزشک جهت بررسی سریع درخواست شما

+

+ در حال حاضر پزشکی در شیفت نیست؛ درخواست شما به پزشک نزدیک‌ترین شیفت آینده ارجاع داده می‌شود. +

-
+
diff --git a/resources/js/Client/composables/useAnimatedHeight.js b/resources/js/Client/composables/useAnimatedHeight.js new file mode 100644 index 0000000..db0b4ab --- /dev/null +++ b/resources/js/Client/composables/useAnimatedHeight.js @@ -0,0 +1,25 @@ +import { onBeforeUnmount, ref, watch } from 'vue' + +export function useAnimatedHeight(contentRef) { + const height = ref(null) + let observer = null + + watch(contentRef, (element) => { + observer?.disconnect() + observer = null + + if (!element) { + height.value = null + return + } + + observer = new ResizeObserver(([entry]) => { + height.value = entry.contentRect.height + }) + observer.observe(element) + }) + + onBeforeUnmount(() => observer?.disconnect()) + + return height +} diff --git a/routes/web.php b/routes/web.php index c1f8d76..0874cfd 100644 --- a/routes/web.php +++ b/routes/web.php @@ -49,6 +49,8 @@ Route::group(['prefix' => 'auth','middleware' => 'guest'] , function (){ }); }); +Route::get('dashboard/payments/callback', [PaymentController::class, 'callback'])->name('dashboard.payments.callback'); + Route::group(['prefix' => 'dashboard' , 'middleware' => 'auth' , 'as' => 'dashboard.'] , function (){ Route::get('/', [DashboardController::class, 'index'])->name('index'); @@ -58,7 +60,6 @@ Route::group(['prefix' => 'dashboard' , 'middleware' => 'auth' , 'as' => 'dashbo Route::post('profile', [ProfileController::class, 'update'])->name('profile.update'); Route::post('payments', [PaymentController::class, 'store'])->name('payments.store'); - Route::get('payments/callback', [PaymentController::class, 'callback'])->name('payments.callback'); Route::get('wallet', [WalletController::class, 'index'])->name('wallet.index'); Route::post('wallet/deposit', [WalletController::class, 'deposit'])->name('wallet.deposit');