feat: add withdraw request functionality and UI components

This commit is contained in:
2026-08-27 02:39:57 +03:30
parent aeef95339d
commit 4ce663c669
115 changed files with 4064 additions and 315 deletions
@@ -0,0 +1,231 @@
<script setup>
import { computed, onMounted, onUnmounted, ref } from 'vue';
import { useForm } from '@inertiajs/inertia-vue3';
import { Inertia } from '@inertiajs/inertia';
import Dialog from 'primevue/dialog';
import { toast } from 'vue3-toastify';
import ClientLayout from '../../../layouts/ClientLayout.vue';
import UiButton from '../../../components/ui/UiButton.vue';
import CaseSpecsCard from '../../../components/dashboard/cases/CaseSpecsCard.vue';
import ReportSectionCard from '../../../components/dashboard/cases/ReportSectionCard.vue';
import FileUploadBox from '../../../components/dashboard/fastRequest/FileUploadBox.vue';
import { modalityTitle } from '../../../data/fastRequestOptions';
import { convertEnglishToPersian } from '../../../utils/numberUtils';
const props = defineProps({
case: {
type: Object,
required: true
},
canReport: {
type: Boolean,
required: true
}
});
const petRows = computed(() => [
{ label: 'نام', value: props.case.pet?.name },
{ label: 'نوع', value: props.case.pet?.type },
{ label: 'جنسیت', value: props.case.pet?.gender },
{ label: 'نژاد', value: props.case.pet?.breed },
{ label: 'سن', value: props.case.pet?.age !== null && props.case.pet?.age !== undefined ? `${convertEnglishToPersian(props.case.pet.age)} سال` : null },
{ label: 'وزن', value: props.case.pet?.weight ? `${convertEnglishToPersian(props.case.pet.weight)} کیلو گرم` : null },
{ label: 'وضعیت عقیم‌سازی', value: props.case.pet?.sterilized === null ? null : (props.case.pet?.sterilized ? 'عقیم شده' : 'عقیم نشده') },
]);
const requesterRows = computed(() => {
const requester = props.case.requester || {};
return [
{ label: 'نوع', value: requester.level },
{ label: 'نام و نام خانوادگی', value: requester.fullname },
{ label: 'شماره موبایل', value: requester.phone ? convertEnglishToPersian(requester.phone) : null },
{ label: 'تخصص', value: requester.specialty },
{ label: 'کلینیک', value: requester.clinic_name },
{ label: 'تلفن کلینیک', value: requester.clinic_phone ? convertEnglishToPersian(requester.clinic_phone) : null },
{ label: 'محل اقامت', value: [requester.province, requester.city].filter(Boolean).join('، ') || null },
].filter((row) => row.value);
});
const remainingLabel = ref(null);
let timerId = null;
const updateRemaining = () => {
if (!props.case.processing_deadline) {
remainingLabel.value = null;
return;
}
const diff = Math.floor((new Date(props.case.processing_deadline).getTime() - Date.now()) / 1000);
if (diff <= 0) {
remainingLabel.value = 'اتمام فرصت رسیدگی';
return;
}
const pad = (value) => String(value).padStart(2, '0');
remainingLabel.value = convertEnglishToPersian(`${pad(Math.floor(diff / 3600))}:${pad(Math.floor((diff % 3600) / 60))}:${pad(diff % 60)}`) + ' تا پایان زمان ثبت گزارش';
};
onMounted(() => {
updateRemaining();
timerId = setInterval(updateRemaining, 1000);
});
onUnmounted(() => clearInterval(timerId));
const openFiles = (files) => {
files.forEach((file) => window.open(file.url, '_blank'));
};
const reportForm = useForm({
document_id: props.case.id,
technique: props.case.report?.technique || '',
finding: props.case.report?.finding || '',
diagnose: props.case.report?.diagnose || '',
recommendation: props.case.report?.recommendation || '',
files: [],
});
const submitReport = () => {
reportForm.post('/dashboard/reports', {
forceFormData: true,
onError: () => toast.error('لطفا موارد مشخص‌شده را بررسی و اصلاح کنید.'),
});
};
const rejectModalVisible = ref(false);
const rejectReason = ref('');
const rejecting = ref(false);
const submitReject = () => {
if (!rejectReason.value.trim()) {
toast.error('لطفا دلیل رد درخواست را وارد کنید.');
return;
}
rejecting.value = true;
Inertia.post(`/dashboard/cases/${props.case.id}/reject`, { reason: rejectReason.value }, {
onFinish: () => {
rejecting.value = false;
rejectModalVisible.value = false;
},
});
};
</script>
<template>
<ClientLayout>
<div class="flex flex-col gap-4">
<header class="bg-white border border-complement-50 rounded-2xl px-4 py-3 flex items-center justify-between flex-wrap gap-2">
<h1 class="text-[13px] font-bold text-[#3E4C63]">پرونده شماره {{ convertEnglishToPersian(props.case.code) }}</h1>
<div class="flex items-center gap-3 text-[11px]">
<span v-if="canReport && remainingLabel" class="font-semibold text-[#7A1419]">{{ remainingLabel }}</span>
<span v-else class="font-normal text-[#576989]">وضعیت فعلی</span>
<span class="font-semibold text-[#3E4C63] border-r border-[#DEE3EB] pr-3">{{ props.case.status }}</span>
</div>
</header>
<div class="grid grid-cols-[16rem_1fr] max-lg:grid-cols-1 gap-4 items-start">
<aside class="flex flex-col gap-4">
<CaseSpecsCard title="مشخصات کیس" :rows="petRows" />
<CaseSpecsCard v-if="canReport && requesterRows.length" title="مشخصات درخواست‌دهنده" :rows="requesterRows" />
</aside>
<div class="flex flex-col gap-4">
<section class="bg-white border border-complement-50 rounded-2xl p-4 flex flex-col gap-3">
<h2 class="flex items-center gap-2 text-[12px] font-bold text-[#3E4C63]">
<span class="w-1.5 h-1.5 rounded-full bg-[#7A1419]"></span>
جزئیات درخواست
</h2>
<div class="flex items-center gap-3 flex-wrap text-[11px]">
<p><span class="font-normal text-[#576989]">گزارش درخواست‌شده:</span> <span class="font-semibold text-[#7A1419]">{{ modalityTitle(props.case.modality) }}</span></p>
<span class="text-[#DEE3EB]">|</span>
<p><span class="font-normal text-[#576989]">نوع درخواست:</span> <span class="font-semibold text-[#7A1419]">{{ props.case.priority }}</span></p>
<span class="text-[#DEE3EB]">|</span>
<p><span class="font-normal text-[#576989]">زمان درخواست:</span> <span class="font-semibold text-[#3E4C63]">{{ props.case.submitted_at ? convertEnglishToPersian(props.case.submitted_at) : '-' }}</span></p>
</div>
<div class="text-[12px] leading-6">
<p class="font-normal text-[#576989]">تاریخچه بیماری:</p>
<p class="font-normal text-[#3E4C63] whitespace-pre-line">{{ props.case.note || 'توضیحی ثبت نشده است.' }}</p>
</div>
<div class="flex items-center gap-3 flex-wrap pt-2 border-t border-[#EBEBEB]">
<UiButton label="دانلود فایل درخواست" severity="secondary" size="small" :disabled="!props.case.primary_files.length" @click="openFiles(props.case.primary_files)" />
<UiButton label="دانلود سوابق پزشکی قبلی" severity="secondary" size="small" :disabled="!props.case.history_files.length" @click="openFiles(props.case.history_files)" />
<UiButton v-if="!canReport && props.case.report" label="دانلود فایل گزارش" severity="secondary" size="small" :disabled="!props.case.report.attachments.length" @click="openFiles(props.case.report.attachments)" />
</div>
</section>
<p v-if="props.case.rejection_reason" class="bg-[#F8E9EA] border border-[#E7C3C5] rounded-2xl p-4 text-[12px] font-normal text-[#7A1419] leading-6">
دلیل رد درخواست: {{ props.case.rejection_reason }}
</p>
<template v-if="canReport">
<ReportSectionCard v-model="reportForm.technique" title="تکنیک (Technique)" editable placeholder="شرح تکنیک را وارد کنید" :error="reportForm.errors.technique" />
<ReportSectionCard v-model="reportForm.finding" title="یافته‌ها (Finding)" editable placeholder="یافته‌ها را وارد کنید" :error="reportForm.errors.finding" />
<ReportSectionCard v-model="reportForm.diagnose" title="تشخیص (Diagnose)" editable placeholder="تشخیص را وارد کنید" :error="reportForm.errors.diagnose" />
<ReportSectionCard v-model="reportForm.recommendation" title="پیشنهادات (Recommendation)" editable placeholder="پیشنهادات را وارد کنید" :error="reportForm.errors.recommendation" />
<section class="bg-white border border-complement-50 rounded-2xl p-4 flex flex-col gap-3">
<h2 class="flex items-center gap-2 text-[12px] font-bold text-[#3E4C63]">
<span class="w-1.5 h-1.5 rounded-full bg-[#7A1419]"></span>
آپلود فایل و مستندات
</h2>
<FileUploadBox v-model="reportForm.files" title="آپلود فایل و مستندات گزارش" />
</section>
<footer class="bg-white border border-complement-50 rounded-2xl p-4 flex items-center justify-between flex-wrap gap-3">
<p class="text-[11px] font-normal text-[#576989]">با تایید گزارش و یا رد درخواست دیگر امکان ویرایش آن را نخواهید داشت.</p>
<div class="flex items-center gap-3">
<button type="button" class="text-[11px] font-semibold text-[#7A1419]" @click="rejectModalVisible = true">رد درخواست</button>
<UiButton label="تایید و ثبت گزارش" severity="success" size="small" :loading="reportForm.processing" :disabled="reportForm.processing" @click="submitReport" />
</div>
</footer>
</template>
<template v-else>
<ReportSectionCard :model-value="props.case.report?.technique || ''" title="تکنیک (Technique)" />
<ReportSectionCard :model-value="props.case.report?.finding || ''" title="یافته‌ها (Finding)" />
<ReportSectionCard :model-value="props.case.report?.diagnose || ''" title="تشخیص (Diagnose)" />
<ReportSectionCard :model-value="props.case.report?.recommendation || ''" title="پیشنهادات (Recommendation)" />
<section class="bg-white border border-complement-50 rounded-2xl p-4 flex flex-col gap-3">
<h2 class="flex items-center gap-2 text-[12px] font-bold text-[#3E4C63]">
<span class="w-1.5 h-1.5 rounded-full bg-[#7A1419]"></span>
آپلود فایل و مستندات
</h2>
<div v-if="props.case.report?.attachments?.length" class="flex flex-wrap gap-4">
<a v-for="file in props.case.report.attachments" :key="file.id" :href="file.url" target="_blank" class="relative w-[5rem] h-[5rem] rounded-lg border border-gray-200 overflow-hidden">
<img :alt="file.filename" :src="file.url" class="absolute top-0 left-0 w-full h-full object-cover" />
</a>
</div>
<p v-else class="w-full min-h-[5rem] bg-[#F8FAFC] border border-[#DEE3EB] rounded-xl p-4 text-[12px] font-normal text-[#576989] leading-6 flex items-center justify-center text-center">
عکسی برای این پرونده آپلود نشده است.
</p>
</section>
</template>
</div>
</div>
</div>
<Dialog v-model:visible="rejectModalVisible" :modal="true" :closable="true" :dismissableMask="false" :style="{ width: '480px' }" class="fast-request-dialog" :pt="{ header: '!bg-client-primaryBlue' }">
<template #header>
<div class="w-full flex items-center justify-start">
<h2 class="text-[14px] font-[700] text-[#576989]">رد درخواست</h2>
</div>
</template>
<div class="w-full flex flex-col gap-3 border-t border-[#EBEBEB] pt-4">
<p class="text-[12px] font-normal text-[#3E4C63] leading-6">پس از رد درخواست، مبلغ فاکتور به کیف پول درخواست‌کننده بازگردانده می‌شود.</p>
<textarea
v-model="rejectReason"
rows="4"
placeholder="دلیل رد درخواست را وارد کنید"
class="w-full bg-[#F8FAFC] border border-[#DEE3EB] rounded-xl p-4 text-[12px] font-normal text-[#3E4C63] leading-6 outline-none focus:border-[#C1D0E8] resize-y"
></textarea>
</div>
<div class="w-full pt-3 flex items-center justify-start gap-2 border-t border-[#EBEBEB]">
<UiButton label="رد درخواست" severity="danger" size="small" :loading="rejecting" :disabled="rejecting" @click="submitReject" />
<UiButton label="انصراف" severity="secondary" size="small" :disabled="rejecting" @click="rejectModalVisible = false" />
</div>
</Dialog>
</ClientLayout>
</template>