feat: implement media download functionality and enhance case handling in reports
This commit is contained in:
@@ -4,6 +4,7 @@ namespace App\Http\Controllers\Client\Doctor;
|
|||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Http\Requests\Client\ReportSubmitRequest;
|
use App\Http\Requests\Client\ReportSubmitRequest;
|
||||||
|
use App\Http\Resources\Client\CaseResource;
|
||||||
use App\Http\Resources\Client\ReportResource;
|
use App\Http\Resources\Client\ReportResource;
|
||||||
use App\Models\Document;
|
use App\Models\Document;
|
||||||
use App\Models\Report;
|
use App\Models\Report;
|
||||||
@@ -32,8 +33,13 @@ class ReportController extends Controller
|
|||||||
->withQueryString()
|
->withQueryString()
|
||||||
->onEachSide(1);
|
->onEachSide(1);
|
||||||
|
|
||||||
|
$pendingCases = $this->caseService->isDoctor(Auth::user())
|
||||||
|
? CaseResource::collection($this->caseService->pendingForUser(Auth::user())->get())
|
||||||
|
: null;
|
||||||
|
|
||||||
return Inertia::client('Dashboard/Reports/Index', [
|
return Inertia::client('Dashboard/Reports/Index', [
|
||||||
'reports' => ReportResource::collection($reports),
|
'reports' => ReportResource::collection($reports),
|
||||||
|
'pendingCases' => $pendingCases,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Client;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\Medium;
|
||||||
|
use App\Services\Document\CaseService;
|
||||||
|
use App\Services\FileStorageService;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
|
||||||
|
class MediaController extends Controller
|
||||||
|
{
|
||||||
|
protected CaseService $caseService;
|
||||||
|
protected FileStorageService $fileStorageService;
|
||||||
|
|
||||||
|
public function __construct(CaseService $caseService, FileStorageService $fileStorageService)
|
||||||
|
{
|
||||||
|
$this->caseService = $caseService;
|
||||||
|
$this->fileStorageService = $fileStorageService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function download(Medium $medium)
|
||||||
|
{
|
||||||
|
abort_unless($this->caseService->canAccessMedium($medium, Auth::user()), 403);
|
||||||
|
|
||||||
|
return $this->fileStorageService->downloadFile($medium);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -69,7 +69,7 @@ class CaseDetailResource extends JsonResource
|
|||||||
return [
|
return [
|
||||||
'id' => $medium->id,
|
'id' => $medium->id,
|
||||||
'filename' => $medium->filename,
|
'filename' => $medium->filename,
|
||||||
'url' => $medium->url,
|
'url' => $medium->isPrivate() ? route('dashboard.media.download', $medium) : $medium->url,
|
||||||
'mime_type' => $medium->mime_type,
|
'mime_type' => $medium->mime_type,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ use App\Enums\ReportStatusEnum;
|
|||||||
use App\Enums\UserEnum;
|
use App\Enums\UserEnum;
|
||||||
use App\Enums\WalletTransactionReasonEnum;
|
use App\Enums\WalletTransactionReasonEnum;
|
||||||
use App\Models\Document;
|
use App\Models\Document;
|
||||||
|
use App\Models\Medium;
|
||||||
use App\Models\Report;
|
use App\Models\Report;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Services\FileStorageService;
|
use App\Services\FileStorageService;
|
||||||
@@ -51,6 +52,14 @@ class CaseService
|
|||||||
|| $document->assignments()->where('user_id', $user->id)->exists();
|
|| $document->assignments()->where('user_id', $user->id)->exists();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function canAccessMedium(Medium $medium, User $user): bool
|
||||||
|
{
|
||||||
|
$documents = $medium->documents()->get()
|
||||||
|
->merge($medium->reports()->with('document')->get()->pluck('document'));
|
||||||
|
|
||||||
|
return $documents->contains(fn ($document) => $this->canAccess($document, $user));
|
||||||
|
}
|
||||||
|
|
||||||
public function canReport(Document $document, User $user): bool
|
public function canReport(Document $document, User $user): bool
|
||||||
{
|
{
|
||||||
return $this->isDoctor($user)
|
return $this->isDoctor($user)
|
||||||
@@ -162,7 +171,7 @@ class CaseService
|
|||||||
->orWhereHas('assignments', fn ($assignmentQuery) => $assignmentQuery->where('user_id', $user->id));
|
->orWhereHas('assignments', fn ($assignmentQuery) => $assignmentQuery->where('user_id', $user->id));
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function isDoctor(User $user): bool
|
public function isDoctor(User $user): bool
|
||||||
{
|
{
|
||||||
return $user->level === UserEnum::Doctor->value;
|
return $user->level === UserEnum::Doctor->value;
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -44,7 +44,7 @@ return [
|
|||||||
'iranpayamak' => [
|
'iranpayamak' => [
|
||||||
'api_url' => env('IRANPAYAMAK_SMS_API_URL', 'https://api.iranpayamak.com/ws/v1/sms/pattern'),
|
'api_url' => env('IRANPAYAMAK_SMS_API_URL', 'https://api.iranpayamak.com/ws/v1/sms/pattern'),
|
||||||
'line_number' => env('IRANPAYAMAK_SMS_API_URL', '50002178584000'),
|
'line_number' => env('IRANPAYAMAK_SMS_API_URL', '50002178584000'),
|
||||||
'api_key' => env('IRANPAYAMAK_SMS_API_URL', 'jnCyVq4ein7uGsgIGjHsVRrWz0112IA2v2qbZMHq0kHtEXubsh'),
|
'api_key' => env('IRANPAYAMAK_SMS_API_URL', 'jnCyVq4ein7uGsgIGjHsVRrWz0112IA2v2qbZM5q0kHtEXubsh'),
|
||||||
|
|
||||||
|
|
||||||
'patterns' => [
|
'patterns' => [
|
||||||
|
|||||||
@@ -41,19 +41,11 @@
|
|||||||
|
|
||||||
|
|
||||||
.ui-button--success.p-button {
|
.ui-button--success.p-button {
|
||||||
@apply !bg-success !border-success font-normal !text-white;
|
@apply !bg-complement-700 !border-complement-700 !text-white;
|
||||||
}
|
|
||||||
|
|
||||||
.ui-button--success.p-button:hover {
|
|
||||||
@apply !bg-success-active !border-success-active;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ui-button--success.p-button:active {
|
|
||||||
@apply !bg-success-active !border-success-active;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.ui-button--success.p-button:focus {
|
.ui-button--success.p-button:focus {
|
||||||
box-shadow: 0 0 0 2px rgb(80 205 137 / 0.25);
|
box-shadow: 0 0 0 2px rgb(87 105 137 / 0.25);
|
||||||
}
|
}
|
||||||
|
|
||||||
.ui-button-icon {
|
.ui-button-icon {
|
||||||
|
|||||||
@@ -41,12 +41,6 @@
|
|||||||
padding: 8px 16px !important;
|
padding: 8px 16px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fast-request-dialog .ui-button .p-button.p-button-success {
|
|
||||||
background: #576989 !important;
|
|
||||||
border-color: #576989 !important;
|
|
||||||
color: #FFFFFF !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fast-request-dialog .p-inputtext::placeholder {
|
.fast-request-dialog .p-inputtext::placeholder {
|
||||||
color: #6E91C9;
|
color: #6E91C9;
|
||||||
}
|
}
|
||||||
@@ -87,26 +81,3 @@
|
|||||||
background: #576989 !important;
|
background: #576989 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fast-request-dialog .p-fileupload-choose-button,
|
|
||||||
.fast-request-dialog .p-fileupload-header {
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fast-request-dialog .p-fileupload {
|
|
||||||
width: 100% !important;
|
|
||||||
height: 100% !important;
|
|
||||||
display: flex !important;
|
|
||||||
align-items: center !important;
|
|
||||||
justify-content: center !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fast-request-dialog .p-fileupload-content {
|
|
||||||
pointer-events: auto !important;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fast-request-dialog .p-fileupload-advanced {
|
|
||||||
border: 2px solid #C1D0E8 !important;
|
|
||||||
border-style: dashed !important;
|
|
||||||
border-radius: 6px !important;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
.ui-file-upload .p-fileupload {
|
||||||
|
@apply w-full;
|
||||||
|
border: 1.5px dashed #C1D0E8;
|
||||||
|
border-radius: 16px;
|
||||||
|
background: #F8FAFC;
|
||||||
|
transition: border-color 0.2s, background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-file-upload .p-fileupload-header {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-file-upload--filled .p-fileupload {
|
||||||
|
border-style: solid;
|
||||||
|
border-color: #DEE3EB;
|
||||||
|
background: #FFFFFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-file-upload:hover .p-fileupload,
|
||||||
|
.ui-file-upload--dragging .p-fileupload {
|
||||||
|
border-color: #365891;
|
||||||
|
background: #EEF2F8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-file-upload .p-fileupload-content {
|
||||||
|
@apply w-full p-3;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-file-upload-dropzone {
|
||||||
|
@apply w-full min-h-[8.5rem] flex flex-col items-center justify-center gap-1 text-center cursor-pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-file-upload-icon {
|
||||||
|
@apply w-11 h-11 mb-1 flex items-center justify-center rounded-full bg-[#EEF2F8] text-[#365891] transition-transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-file-upload:hover .ui-file-upload-icon,
|
||||||
|
.ui-file-upload--dragging .ui-file-upload-icon {
|
||||||
|
@apply scale-110 bg-white;
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@
|
|||||||
@import "./Client/ui/card.css";
|
@import "./Client/ui/card.css";
|
||||||
@import "./Client/ui/select.css";
|
@import "./Client/ui/select.css";
|
||||||
@import "./Client/ui/fast-request.css";
|
@import "./Client/ui/fast-request.css";
|
||||||
|
@import "./Client/ui/file-upload.css";
|
||||||
@import "./Client/ui/table.css";
|
@import "./Client/ui/table.css";
|
||||||
@import "./Shared/shift-calendar.css";
|
@import "./Shared/shift-calendar.css";
|
||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import ReportSectionCard from '../../../components/dashboard/cases/ReportSection
|
|||||||
import FileUploadBox from '../../../components/dashboard/fastRequest/FileUploadBox.vue';
|
import FileUploadBox from '../../../components/dashboard/fastRequest/FileUploadBox.vue';
|
||||||
import { modalityTitle } from '../../../data/fastRequestOptions';
|
import { modalityTitle } from '../../../data/fastRequestOptions';
|
||||||
import { convertEnglishToPersian } from '../../../utils/numberUtils';
|
import { convertEnglishToPersian } from '../../../utils/numberUtils';
|
||||||
|
import { downloadFiles } from '../../../utils/fileUtils';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
case: {
|
case: {
|
||||||
@@ -73,10 +74,6 @@ onMounted(() => {
|
|||||||
|
|
||||||
onUnmounted(() => clearInterval(timerId));
|
onUnmounted(() => clearInterval(timerId));
|
||||||
|
|
||||||
const openFiles = (files) => {
|
|
||||||
files.forEach((file) => window.open(file.url, '_blank'));
|
|
||||||
};
|
|
||||||
|
|
||||||
const reportForm = useForm({
|
const reportForm = useForm({
|
||||||
document_id: props.case.id,
|
document_id: props.case.id,
|
||||||
technique: props.case.report?.technique || '',
|
technique: props.case.report?.technique || '',
|
||||||
@@ -149,9 +146,9 @@ const submitReject = () => {
|
|||||||
<p class="font-normal text-[#3E4C63] whitespace-pre-line">{{ props.case.note || 'توضیحی ثبت نشده است.' }}</p>
|
<p class="font-normal text-[#3E4C63] whitespace-pre-line">{{ props.case.note || 'توضیحی ثبت نشده است.' }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-3 flex-wrap pt-2 border-t border-[#EBEBEB]">
|
<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.primary_files.length" @click="downloadFiles(props.case.primary_files)" />
|
||||||
<UiButton label="دانلود سوابق پزشکی قبلی" severity="secondary" size="small" :disabled="!props.case.history_files.length" @click="openFiles(props.case.history_files)" />
|
<UiButton label="دانلود سوابق پزشکی قبلی" severity="secondary" size="small" :disabled="!props.case.history_files.length" @click="downloadFiles(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)" />
|
<UiButton v-if="!canReport && props.case.report" label="دانلود فایل گزارش" severity="secondary" size="small" :disabled="!props.case.report.attachments.length" @click="downloadFiles(props.case.report.attachments)" />
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ const openInvoice = (invoice) => {
|
|||||||
<UiButton
|
<UiButton
|
||||||
v-if="data.status === 'در انتظار پرداخت'"
|
v-if="data.status === 'در انتظار پرداخت'"
|
||||||
label="پرداخت"
|
label="پرداخت"
|
||||||
|
icon="fi fi-rr-credit-card"
|
||||||
severity="success"
|
severity="success"
|
||||||
size="small"
|
size="small"
|
||||||
@click="openInvoice(data)"
|
@click="openInvoice(data)"
|
||||||
|
|||||||
@@ -2,20 +2,47 @@
|
|||||||
import ClientLayout from '../../../layouts/ClientLayout.vue';
|
import ClientLayout from '../../../layouts/ClientLayout.vue';
|
||||||
import TableGeneric from '../../../components/ui/TableGeneric.vue';
|
import TableGeneric from '../../../components/ui/TableGeneric.vue';
|
||||||
import UiCard from '../../../components/ui/UiCard.vue';
|
import UiCard from '../../../components/ui/UiCard.vue';
|
||||||
|
import UiButton from '../../../components/ui/UiButton.vue';
|
||||||
import EyeSvg from '../../../components/svg/eye.svg';
|
import EyeSvg from '../../../components/svg/eye.svg';
|
||||||
import { Link } from '@inertiajs/inertia-vue3';
|
import { Link } from '@inertiajs/inertia-vue3';
|
||||||
|
import { Inertia } from '@inertiajs/inertia';
|
||||||
import { reportColumns } from '../../../data/reportsTableColumns';
|
import { reportColumns } from '../../../data/reportsTableColumns';
|
||||||
|
import { caseColumns } from '../../../data/casesTableColumns';
|
||||||
|
|
||||||
defineProps({
|
defineProps({
|
||||||
reports: {
|
reports: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
required: true
|
||||||
|
},
|
||||||
|
pendingCases: {
|
||||||
|
type: Object,
|
||||||
|
default: null
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ClientLayout>
|
<ClientLayout>
|
||||||
|
<div class="flex flex-col gap-4">
|
||||||
|
<UiCard v-if="pendingCases" title="پروندههای در انتظار گزارش" variant="default">
|
||||||
|
<TableGeneric
|
||||||
|
class="bg-white !rounded-2xl"
|
||||||
|
:data="pendingCases.data"
|
||||||
|
customStyle="!text-[11px] !font-normal"
|
||||||
|
:searchable="false"
|
||||||
|
:columns="caseColumns"
|
||||||
|
:selectable="false"
|
||||||
|
:pagination="null"
|
||||||
|
:model-value="[]"
|
||||||
|
>
|
||||||
|
<template #column-actions="{ data }">
|
||||||
|
<div class="flex items-center justify-end">
|
||||||
|
<UiButton label="ثبت گزارش" severity="success" size="small" @click="Inertia.visit(`/dashboard/cases/${data.id}`)" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</TableGeneric>
|
||||||
|
</UiCard>
|
||||||
|
|
||||||
<UiCard title="گزارشات" variant="default">
|
<UiCard title="گزارشات" variant="default">
|
||||||
<TableGeneric
|
<TableGeneric
|
||||||
class="bg-white !rounded-2xl"
|
class="bg-white !rounded-2xl"
|
||||||
@@ -38,5 +65,6 @@ defineProps({
|
|||||||
</template>
|
</template>
|
||||||
</TableGeneric>
|
</TableGeneric>
|
||||||
</UiCard>
|
</UiCard>
|
||||||
|
</div>
|
||||||
</ClientLayout>
|
</ClientLayout>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
import FileUpload from 'primevue/fileupload';
|
import FileUpload from 'primevue/fileupload';
|
||||||
import Badge from 'primevue/badge';
|
|
||||||
import { formatFileSize } from '../../../utils/fileUtils';
|
import { formatFileSize } from '../../../utils/fileUtils';
|
||||||
|
import { convertEnglishToPersian } from '../../../utils/numberUtils';
|
||||||
|
|
||||||
const files = defineModel({ type: Array, default: () => [] });
|
const files = defineModel({ type: Array, default: () => [] });
|
||||||
|
|
||||||
@@ -13,56 +13,106 @@ defineProps({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const MAX_FILE_SIZE = 10485760;
|
||||||
|
|
||||||
const fileUpload = ref(null);
|
const fileUpload = ref(null);
|
||||||
|
const dragging = ref(false);
|
||||||
|
|
||||||
|
const totalSize = computed(() => files.value.reduce((sum, file) => sum + file.size, 0));
|
||||||
|
|
||||||
const openFileDialog = () => {
|
const openFileDialog = () => {
|
||||||
fileUpload.value.choose();
|
fileUpload.value.choose();
|
||||||
}
|
};
|
||||||
|
|
||||||
const onSelectedFiles = (event) => {
|
const onSelectedFiles = (event) => {
|
||||||
files.value = event.files;
|
files.value = event.files;
|
||||||
}
|
};
|
||||||
|
|
||||||
const onRemoveFile = (removeFileCallback, index) => {
|
const onRemoveFile = (removeFileCallback, index) => {
|
||||||
removeFileCallback(index);
|
removeFileCallback(index);
|
||||||
files.value = fileUpload.value.files;
|
files.value = fileUpload.value.files;
|
||||||
}
|
};
|
||||||
|
|
||||||
|
const onClearFiles = (clearCallback) => {
|
||||||
|
clearCallback();
|
||||||
|
files.value = [];
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
<div
|
||||||
|
class="ui-file-upload"
|
||||||
|
:class="{ 'ui-file-upload--dragging': dragging, 'ui-file-upload--filled': files.length }"
|
||||||
|
@dragover="dragging = true"
|
||||||
|
@dragleave="dragging = false"
|
||||||
|
@drop="dragging = false"
|
||||||
|
>
|
||||||
<FileUpload
|
<FileUpload
|
||||||
ref="fileUpload"
|
ref="fileUpload"
|
||||||
class="w-full !border-dashed !border-[#C1D0E8] !min-h-[100px] flex items-center justify-center"
|
|
||||||
name="files[]"
|
name="files[]"
|
||||||
:customUpload="true"
|
:customUpload="true"
|
||||||
:showUploadButton="false"
|
:showUploadButton="false"
|
||||||
:showCancelButton="false"
|
:showCancelButton="false"
|
||||||
:showChooseButton="false"
|
|
||||||
:multiple="true"
|
:multiple="true"
|
||||||
accept="image/*"
|
accept="image/*"
|
||||||
:maxFileSize="10485760"
|
:maxFileSize="MAX_FILE_SIZE"
|
||||||
@select="onSelectedFiles"
|
@select="onSelectedFiles"
|
||||||
>
|
>
|
||||||
<template #content="{ files: selectedFiles, removeFileCallback }">
|
<template #content="{ files: selectedFiles, removeFileCallback, clearCallback }">
|
||||||
<div v-if="selectedFiles.length > 0" class="flex flex-wrap gap-4 pt-4">
|
<div v-if="selectedFiles.length > 0" class="flex flex-col gap-3">
|
||||||
<div v-for="(file, index) of selectedFiles" :key="file.name + file.type + file.size" class="relative w-[5rem] h-[5rem] rounded-lg border border-gray-200 flex flex-col items-center overflow-hidden">
|
<ul class="grid grid-cols-[repeat(auto-fill,5rem)] gap-2">
|
||||||
<img role="presentation" :alt="file.name" :src="file.objectURL" class="absolute top-0 left-0 w-full h-full object-cover"/>
|
<li
|
||||||
<div class="absolute bottom-0 left-0 w-full flex flex-col items-center gap-1 bg-white/80 p-1">
|
v-for="(file, index) of selectedFiles"
|
||||||
<span class="w-full text-[10px] font-[400] text-center text-ellipsis whitespace-nowrap overflow-hidden">{{ file.name }}</span>
|
:key="file.name + file.type + file.size"
|
||||||
<span class="text-[9px]">{{ formatFileSize(file.size) }}</span>
|
class="group relative aspect-square rounded-xl overflow-hidden border border-[#DEE3EB] bg-[#F8FAFC]"
|
||||||
|
>
|
||||||
|
<img role="presentation" :alt="file.name" :src="file.objectURL" class="w-full h-full object-cover" />
|
||||||
|
<div class="absolute inset-x-0 bottom-0 px-2 pb-1.5 pt-4 bg-gradient-to-t from-[#2F343D]/80 to-transparent text-white">
|
||||||
|
<p class="text-[10px] font-[600] truncate" dir="ltr">{{ file.name }}</p>
|
||||||
|
<p class="text-[9px] font-normal opacity-80" dir="ltr">{{ formatFileSize(file.size) }}</p>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" class="absolute top-1 left-1 w-4 h-4 flex items-center justify-center rounded-full bg-white text-[#7A1419] text-[10px] cursor-pointer" @click.stop="onRemoveFile(removeFileCallback, index)">✕</button>
|
<button
|
||||||
|
type="button"
|
||||||
|
class="absolute top-1.5 left-1.5 w-5 h-5 flex items-center justify-center rounded-full bg-white/90 text-[#7A1419] shadow-sm transition-opacity opacity-0 group-hover:opacity-100 focus:opacity-100"
|
||||||
|
:aria-label="`حذف ${file.name}`"
|
||||||
|
@click.stop="onRemoveFile(removeFileCallback, index)"
|
||||||
|
>
|
||||||
|
<i class="fi fi-rr-cross-small text-[12px] leading-none"></i>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="w-full aspect-square rounded-xl border border-dashed border-[#C1D0E8] bg-white flex flex-col items-center justify-center gap-1 text-[#365891] transition-colors hover:border-[#365891] hover:bg-[#EEF2F8]"
|
||||||
|
@click="openFileDialog"
|
||||||
|
>
|
||||||
|
<i class="fi fi-rr-plus text-[16px] leading-none"></i>
|
||||||
|
<span class="text-[10px] font-[600]">افزودن</span>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<div class="flex items-center justify-between gap-2 border-t border-[#EBEBEB] pt-2">
|
||||||
|
<p class="text-[11px] font-normal text-[#576989]">
|
||||||
|
{{ convertEnglishToPersian(String(selectedFiles.length)) }} فایل · <span dir="ltr">{{ formatFileSize(totalSize) }}</span>
|
||||||
|
</p>
|
||||||
|
<button type="button" class="text-[11px] font-[600] text-[#7A1419]" @click="onClearFiles(clearCallback)">حذف همه</button>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" class="w-[5rem] h-[5rem] rounded-lg border border-dashed border-[#C1D0E8] text-[11px] text-[#365891] cursor-pointer" @click="openFileDialog">افزودن</button>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #empty>
|
<template #empty>
|
||||||
<div class="w-full flex items-center justify-center flex-col cursor-pointer" @click="openFileDialog">
|
<button type="button" class="ui-file-upload-dropzone" @click="openFileDialog">
|
||||||
<p class="text-[11px] font-[600] text-[#365891]">{{ title }}</p>
|
<span class="ui-file-upload-icon">
|
||||||
<p class="mt-1 mb-0 text-[11px] font-[400] text-[#365891]">
|
<i class="fi fi-rr-cloud-upload text-[20px] leading-none"></i>
|
||||||
جهت آپلود تصویر کلیک کنید و یا فایل خود را کشیده و در این قسمت رها (Drag and Drop) کنید.
|
</span>
|
||||||
</p>
|
<span class="text-[12px] font-[700] text-[#3E4C63]">{{ title }}</span>
|
||||||
</div>
|
<span class="text-[11px] font-normal text-[#576989] leading-5">
|
||||||
|
فایل را اینجا رها کنید یا برای انتخاب کلیک کنید
|
||||||
|
</span>
|
||||||
|
<span class="mt-1 text-[10px] font-normal text-[#576989] bg-white border border-[#DEE3EB] rounded-md px-2 py-0.5">
|
||||||
|
JPG، PNG · حداکثر {{ formatFileSize(MAX_FILE_SIZE) }}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
</template>
|
</template>
|
||||||
</FileUpload>
|
</FileUpload>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -23,8 +23,21 @@ const walletBalance = computed(() => Number(pageProps.value.auth?.wallet_balance
|
|||||||
const amount = ref('');
|
const amount = ref('');
|
||||||
const selectedCardId = ref(props.bankCards[0]?.id ?? null);
|
const selectedCardId = ref(props.bankCards[0]?.id ?? null);
|
||||||
|
|
||||||
|
const MIN_WITHDRAW_AMOUNT = 10000;
|
||||||
|
|
||||||
const numericAmount = computed(() => Number(convertPersianToEnglish(amount.value).replace(/\D/g, '')) || 0);
|
const numericAmount = computed(() => Number(convertPersianToEnglish(amount.value).replace(/\D/g, '')) || 0);
|
||||||
|
|
||||||
|
const selectedCard = computed(() => props.bankCards.find((card) => card.id === selectedCardId.value) ?? null);
|
||||||
|
|
||||||
|
const amountError = computed(() => {
|
||||||
|
if (!amount.value) return '';
|
||||||
|
if (numericAmount.value < MIN_WITHDRAW_AMOUNT) return `حداقل مبلغ برداشت ${formatPrice(MIN_WITHDRAW_AMOUNT)} تومان است.`;
|
||||||
|
if (numericAmount.value > walletBalance.value) return 'مبلغ برداشت از موجودی کیف پول بیشتر است.';
|
||||||
|
return '';
|
||||||
|
});
|
||||||
|
|
||||||
|
const canSubmit = computed(() => Boolean(selectedCard.value) && numericAmount.value >= MIN_WITHDRAW_AMOUNT && !amountError.value);
|
||||||
|
|
||||||
const formatCardNumber = (cardNumber) => convertEnglishToPersian(cardNumber.replace(/(\d{4})(?=\d)/g, '$1 - '));
|
const formatCardNumber = (cardNumber) => convertEnglishToPersian(cardNumber.replace(/(\d{4})(?=\d)/g, '$1 - '));
|
||||||
|
|
||||||
const selectFullBalance = () => {
|
const selectFullBalance = () => {
|
||||||
@@ -37,15 +50,7 @@ const withdrawForm = useForm({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const submitWithdraw = () => {
|
const submitWithdraw = () => {
|
||||||
if (numericAmount.value < 10000) {
|
if (!canSubmit.value) return;
|
||||||
toast.error('حداقل مبلغ برداشت ۱۰٬۰۰۰ تومان است.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!selectedCardId.value) {
|
|
||||||
toast.error('لطفا کارت بانکی مقصد را انتخاب کنید.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
withdrawForm.amount = numericAmount.value;
|
withdrawForm.amount = numericAmount.value;
|
||||||
withdrawForm.bank_card_id = selectedCardId.value;
|
withdrawForm.bank_card_id = selectedCardId.value;
|
||||||
@@ -95,52 +100,31 @@ const deleteCard = (card) => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<UiCard title="درخواست برداشت" variant="default">
|
<UiCard title="درخواست برداشت" variant="default">
|
||||||
<div class="grid grid-cols-2 max-lg:grid-cols-1 gap-6 items-start">
|
<div class="flex flex-col gap-5">
|
||||||
<div class="flex flex-col gap-4">
|
<div class="flex items-start justify-between gap-4 max-md:flex-col">
|
||||||
<p class="text-[11px] font-normal text-[#576989] leading-6">
|
<p class="text-[11px] font-normal text-[#576989] leading-6 max-w-xl">
|
||||||
مبلغ مورد نظر پس از بررسی و تایید توسط کارشناسان، از کیف پول شما کسر و به کارت انتخابی واریز میشود.
|
مبلغ مورد نظر پس از بررسی و تایید توسط کارشناسان، از کیف پول شما کسر و به کارت انتخابی واریز میشود.
|
||||||
</p>
|
</p>
|
||||||
<div class="flex items-end gap-3 max-sm:flex-col max-sm:items-stretch">
|
<div class="flex items-center gap-2 bg-[#F5F7FA] border border-[#DEE3EB] rounded-lg px-3 py-2 shrink-0">
|
||||||
<UiInput
|
<span class="text-[11px] font-normal text-[#576989]">موجودی قابل برداشت:</span>
|
||||||
v-model="amount"
|
<span class="text-[12px] font-semibold text-[#3E4C63]">{{ formatPrice(walletBalance) }} تومان</span>
|
||||||
label="مبلغ برداشت"
|
|
||||||
placeholder="مبلغ مورد نظر را به تومان وارد کنید"
|
|
||||||
prefixEnd="تومان"
|
|
||||||
prefixClasses="!border-dominant-50"
|
|
||||||
class="!w-full"
|
|
||||||
customClass="!border-dominant-50 !font-normal !text-[12px]"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2 flex-wrap">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="text-[11px] font-normal text-[#576989] bg-[#F5F7FA] border border-[#DEE3EB] rounded-lg px-3 py-1.5 hover:bg-[#EEF2F8]"
|
|
||||||
@click="selectFullBalance"
|
|
||||||
>
|
|
||||||
کل موجودی ({{ formatPrice(walletBalance) }} تومان)
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<UiButton
|
|
||||||
label="ثبت درخواست برداشت"
|
|
||||||
severity="success"
|
|
||||||
size="small"
|
|
||||||
class="self-start"
|
|
||||||
:loading="withdrawForm.processing"
|
|
||||||
:disabled="withdrawForm.processing"
|
|
||||||
@click="submitWithdraw"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col gap-3">
|
<section class="flex flex-col gap-3 border-t border-[#EBEBEB] pt-4">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<span class="text-[11px] font-semibold text-[#3E4C63]">کارت بانکی مقصد</span>
|
<h3 class="text-[12px] font-semibold text-[#3E4C63]">۱. کارت بانکی مقصد</h3>
|
||||||
<button type="button" class="text-[11px] font-semibold text-[#365891]" @click="cardModalVisible = true">
|
<button v-if="bankCards.length" type="button" class="text-[11px] font-semibold text-[#365891]" @click="cardModalVisible = true">
|
||||||
+ افزودن کارت
|
+ افزودن کارت
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<p v-if="!bankCards.length" class="bg-[#F8FAFC] border border-dashed border-[#C1D0E8] rounded-xl p-4 text-[11px] font-normal text-[#576989] text-center leading-6">
|
<div v-if="!bankCards.length" class="flex flex-col items-center gap-3 bg-[#F8FAFC] border border-dashed border-[#C1D0E8] rounded-xl p-5">
|
||||||
|
<p class="text-[11px] font-normal text-[#576989] text-center leading-6">
|
||||||
هنوز کارتی ثبت نکردهاید. برای ثبت درخواست برداشت ابتدا یک کارت بانکی اضافه کنید.
|
هنوز کارتی ثبت نکردهاید. برای ثبت درخواست برداشت ابتدا یک کارت بانکی اضافه کنید.
|
||||||
</p>
|
</p>
|
||||||
|
<UiButton label="افزودن کارت بانکی" severity="secondary" size="small" @click="cardModalVisible = true" />
|
||||||
|
</div>
|
||||||
|
<div v-else class="grid grid-cols-2 max-lg:grid-cols-1 gap-3">
|
||||||
<label
|
<label
|
||||||
v-for="card in bankCards"
|
v-for="card in bankCards"
|
||||||
:key="card.id"
|
:key="card.id"
|
||||||
@@ -161,6 +145,54 @@ const deleteCard = (card) => {
|
|||||||
<button type="button" class="text-[11px] font-semibold text-[#7A1419]" @click.prevent="deleteCard(card)">حذف</button>
|
<button type="button" class="text-[11px] font-semibold text-[#7A1419]" @click.prevent="deleteCard(card)">حذف</button>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="flex flex-col gap-3 border-t border-[#EBEBEB] pt-4">
|
||||||
|
<h3 class="text-[12px] font-semibold text-[#3E4C63]">۲. مبلغ برداشت</h3>
|
||||||
|
<div class="flex flex-col gap-2 max-w-md">
|
||||||
|
<UiInput
|
||||||
|
v-model="amount"
|
||||||
|
placeholder="مبلغ مورد نظر را به تومان وارد کنید"
|
||||||
|
prefixEnd="تومان"
|
||||||
|
prefixClasses="!border-dominant-50"
|
||||||
|
class="!w-full"
|
||||||
|
customClass="!border-dominant-50 !font-normal !text-[12px]"
|
||||||
|
:error="amountError"
|
||||||
|
/>
|
||||||
|
<div class="flex items-center justify-between gap-2 flex-wrap">
|
||||||
|
<span class="text-[11px] font-normal text-[#576989]">حداقل مبلغ برداشت {{ formatPrice(MIN_WITHDRAW_AMOUNT) }} تومان است.</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="text-[11px] font-normal text-[#576989] bg-[#F5F7FA] border border-[#DEE3EB] rounded-lg px-3 py-1.5 hover:bg-[#EEF2F8] disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
:disabled="walletBalance < MIN_WITHDRAW_AMOUNT"
|
||||||
|
@click="selectFullBalance"
|
||||||
|
>
|
||||||
|
برداشت کل موجودی
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-between gap-4 max-md:flex-col max-md:items-stretch border-t border-[#EBEBEB] pt-4">
|
||||||
|
<p v-if="canSubmit" class="text-[11px] font-normal text-[#576989] leading-6">
|
||||||
|
<span class="font-[600] text-[#7A1419]">{{ formatPrice(numericAmount) }} تومان</span>
|
||||||
|
به کارت
|
||||||
|
<span class="font-[600] text-[#3E4C63]" dir="ltr">{{ formatCardNumber(selectedCard.card_number) }}</span>
|
||||||
|
واریز میشود.
|
||||||
|
</p>
|
||||||
|
<p v-else class="text-[11px] font-normal text-[#576989] leading-6">
|
||||||
|
برای ثبت درخواست، کارت مقصد را انتخاب و مبلغ را وارد کنید.
|
||||||
|
</p>
|
||||||
|
<UiButton
|
||||||
|
label="ثبت درخواست برداشت"
|
||||||
|
severity="success"
|
||||||
|
size="small"
|
||||||
|
class="shrink-0"
|
||||||
|
:loading="withdrawForm.processing"
|
||||||
|
:disabled="withdrawForm.processing || !canSubmit"
|
||||||
|
@click="submitWithdraw"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Dialog v-model:visible="cardModalVisible" :modal="true" :closable="true" :dismissableMask="false" :style="{ width: '420px' }" class="fast-request-dialog" :pt="{ header: '!bg-client-primaryBlue' }">
|
<Dialog v-model:visible="cardModalVisible" :modal="true" :closable="true" :dismissableMask="false" :style="{ width: '420px' }" class="fast-request-dialog" :pt="{ header: '!bg-client-primaryBlue' }">
|
||||||
|
|||||||
@@ -11,3 +11,14 @@ export const formatFileSize = (bytes) => {
|
|||||||
|
|
||||||
return `${formattedSize} ${sizes[i]}`
|
return `${formattedSize} ${sizes[i]}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const downloadFiles = (files) => {
|
||||||
|
files.forEach((file) => {
|
||||||
|
const link = document.createElement('a')
|
||||||
|
link.href = file.url
|
||||||
|
link.download = file.filename || ''
|
||||||
|
document.body.appendChild(link)
|
||||||
|
link.click()
|
||||||
|
link.remove()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ use Inertia\Inertia;
|
|||||||
use App\Http\Controllers\Client\AuthController;
|
use App\Http\Controllers\Client\AuthController;
|
||||||
use App\Http\Controllers\Client\FastRequestController;
|
use App\Http\Controllers\Client\FastRequestController;
|
||||||
use App\Http\Controllers\Client\InvoiceController;
|
use App\Http\Controllers\Client\InvoiceController;
|
||||||
|
use App\Http\Controllers\Client\MediaController;
|
||||||
use App\Http\Controllers\Client\PaymentController;
|
use App\Http\Controllers\Client\PaymentController;
|
||||||
use App\Http\Controllers\Client\ProfileController;
|
use App\Http\Controllers\Client\ProfileController;
|
||||||
use App\Http\Controllers\Client\WalletController;
|
use App\Http\Controllers\Client\WalletController;
|
||||||
@@ -65,6 +66,8 @@ Route::group(['prefix' => 'dashboard' , 'middleware' => 'auth' , 'as' => 'dashbo
|
|||||||
Route::post('wallet/bank-cards', [WalletController::class, 'storeBankCard'])->name('wallet.bankCards.store');
|
Route::post('wallet/bank-cards', [WalletController::class, 'storeBankCard'])->name('wallet.bankCards.store');
|
||||||
Route::delete('wallet/bank-cards/{bankCard}', [WalletController::class, 'destroyBankCard'])->name('wallet.bankCards.destroy');
|
Route::delete('wallet/bank-cards/{bankCard}', [WalletController::class, 'destroyBankCard'])->name('wallet.bankCards.destroy');
|
||||||
|
|
||||||
|
Route::get('media/{medium}/download', [MediaController::class, 'download'])->name('media.download');
|
||||||
|
|
||||||
Route::get('invoices', [InvoiceController::class, 'index'])->name('invoices.index');
|
Route::get('invoices', [InvoiceController::class, 'index'])->name('invoices.index');
|
||||||
Route::post('invoices/{invoice}/pay-wallet', [InvoiceController::class, 'payWithWallet'])->name('invoices.payWallet');
|
Route::post('invoices/{invoice}/pay-wallet', [InvoiceController::class, 'payWithWallet'])->name('invoices.payWallet');
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user