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
+50 -2
View File
@@ -1,3 +1,51 @@
.p-formfield .p-select{
@apply !py-0.5;
.p-inputtext,
.p-select,
.p-multiselect,
.p-password,
.vpd-input-group {
@apply h-10;
}
.p-multiselect {
@apply h-auto min-h-10;
}
.p-select,
.p-multiselect {
@apply items-center;
}
.p-password .p-inputtext {
@apply !h-full;
}
.p-inputtext,
.p-textarea,
.p-select-label,
.p-multiselect-label,
.p-select-option,
.p-multiselect-option {
@apply !text-sm;
}
.p-inputtext::placeholder,
.p-textarea::placeholder {
@apply !text-sm font-light;
}
.p-select-label,
.p-multiselect-label {
@apply flex items-center !py-0;
}
.p-textarea {
@apply min-h-28;
}
.p-formfield select {
@apply w-full rounded-md border border-gray-300 bg-white p-2 text-sm text-client-Fontcolor;
}
.vpd-input-group input.form-control {
@apply !h-full !w-full !rounded-md !border !border-gray-300 !bg-white !px-3 !text-sm !text-client-Fontcolor;
}
+16
View File
@@ -40,6 +40,22 @@
.ui-button--success.p-button {
@apply !bg-success !border-success font-normal !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 {
box-shadow: 0 0 0 2px rgb(80 205 137 / 0.25);
}
.ui-button-icon {
@apply transition-all duration-200;
}
-15
View File
@@ -1,19 +1,4 @@
/* Flag Icons using Emoji */
.fi {
font-style: normal;
font-weight: normal;
font-variant: normal;
text-transform: none;
line-height: 1;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.fi:before {
content: attr(data-flag);
font-size: 1.2em;
}
/* Country Flags */
.fi.fi-af:before { content: "🇦🇫"; }
.fi.fi-al:before { content: "🇦🇱"; }
+5
View File
@@ -99,3 +99,8 @@
.shift-calendar .shift-event--inactive {
opacity: 0.55;
}
.shift-calendar .fc-highlight {
background: rgba(87, 105, 137, 0.15);
border-radius: 8px;
}
-3
View File
@@ -13,9 +13,6 @@ body{
@apply font-dana font-normal bg-client-gray/5;
}
.vpd-input-group{
@apply !h-9;
}
.ql-container {
direction: rtl;
text-align: right;
@@ -118,17 +118,17 @@ const removeHistoryFile = (index) => {
</FormField>
<FormField v-slot="$field" name="submitted_at" initialValue="" class="flex flex-col gap-1">
<label>تاریخ ثبت</label>
<CustomDatePicker v-model="documentStore.createForm.submitted_at" class="!text-xs" type="datetime" compact-time></CustomDatePicker>
<CustomDatePicker v-model="documentStore.createForm.submitted_at" type="datetime" compact-time></CustomDatePicker>
<Message v-if="documentStore.errorsCreateForm?.submitted_at" severity="error" size="small" variant="simple">{{ documentStore.errorsCreateForm?.submitted_at[0] }}</Message>
</FormField>
<FormField v-slot="$field" name="processing_deadline" initialValue="" class="flex flex-col gap-1">
<label>مهلت پردازش</label>
<CustomDatePicker v-model="documentStore.createForm.processing_deadline" class="!text-xs" type="datetime" compact-time></CustomDatePicker>
<CustomDatePicker v-model="documentStore.createForm.processing_deadline" type="datetime" compact-time></CustomDatePicker>
<Message v-if="documentStore.errorsCreateForm?.processing_deadline" severity="error" size="small" variant="simple">{{ documentStore.errorsCreateForm?.processing_deadline[0] }}</Message>
</FormField>
<FormField v-slot="$field" name="note" initialValue="" class="flex flex-col col-span-2 gap-1">
<label>متن درخواست</label>
<Textarea type="text" class="placeholder:!text-sm" v-model="documentStore.createForm.note" placeholder="متن درخواست را وارد کنید" />
<Textarea type="text" v-model="documentStore.createForm.note" placeholder="متن درخواست را وارد کنید" />
</FormField>
<div class="grid grid-cols-2 col-span-2 gap-4">
<Card class="mt-4">
@@ -155,7 +155,7 @@ const removeHistoryFile = (index) => {
<div class="p-4 grid grid-cols-1 gap-4 rounded-2xl">
<div class="grid grid-cols-1 justify-between items-center">
<span>زمان انتشار:</span>
<CustomDatePicker v-model="documentStore.createForm.datetime" class="!text-xs mt-2" type="datetime" compact-time></CustomDatePicker>
<CustomDatePicker v-model="documentStore.createForm.datetime" class="mt-2" type="datetime" compact-time></CustomDatePicker>
</div>
<div class="grid grid-cols-1 justify-start">
<Button :loading="documentStore.loaderCreateForm" @click="documentStore.update_and_store" type="button" severity="secondary" :label="documentStore.buttonLabel" />
@@ -166,29 +166,3 @@ const removeHistoryFile = (index) => {
</section>
</master>
</template>
<style>
.p-multiselect {
height: auto;
min-height: 40px;
}
.p-multiselect-label {
line-height: 24px;
}
.p-multiselect .p-multiselect-header {
line-height: 24px;
}
.p-multiselect .p-multiselect-filter {
max-height: calc(100px);
overflow-y: auto;
}
.p-multiselect .p-multiselect-item {
font-size: 0.875rem;
line-height: 44px;
}
</style>
@@ -56,24 +56,18 @@ onUnmounted(() => {
<Form ref="formRef" v-slot="$form" class="flex flex-col gap-4 !w-full">
<FormField v-slot="$field" name="name" initialValue="" class="flex flex-col gap-1">
<label>نام سطح</label>
<InputText type="text" :class="[
'placeholder:!text-sm',
permissionStore.errorsCreateForm?.name && !permissionStore.createForm.name ? 'p-invalid' : ''
]" v-model="permissionStore.createForm.name" placeholder="نام سطح را وارد کنید" />
<InputText type="text" :class="permissionStore.errorsCreateForm?.name && !permissionStore.createForm.name ? 'p-invalid' : ''" v-model="permissionStore.createForm.name" placeholder="نام سطح را وارد کنید" />
<Message v-if="permissionStore.errorsCreateForm?.name"
severity="error" size="small" variant="simple">{{ permissionStore.errorsCreateForm?.name[0] }}</Message>
</FormField>
<FormField v-slot="$field" name="label" initialValue="" class="flex flex-col gap-1">
<label>نام فارسی سطح</label>
<InputText type="text" :class="[
'placeholder:!text-sm',
permissionStore.errorsCreateForm?.label && !permissionStore.createForm.label ? 'p-invalid' : ''
]" v-model="permissionStore.createForm.label" placeholder="نام فارسی سطح را وارد کنید" />
<InputText type="text" :class="permissionStore.errorsCreateForm?.label && !permissionStore.createForm.label ? 'p-invalid' : ''" v-model="permissionStore.createForm.label" placeholder="نام فارسی سطح را وارد کنید" />
<Message v-if="permissionStore.errorsCreateForm?.label" severity="error" size="small" variant="simple">{{ permissionStore.errorsCreateForm?.label[0] }}</Message>
</FormField>
<FormField v-slot="$field" name="parent_id" initialValue="" class="flex flex-col gap-1">
<label> والد (مرتبط با) </label>
<Select v-model="permissionStore.createForm.parent_id" optionLabel="label" optionValue="id" :pt="{option : '!text-sm'}"
<Select v-model="permissionStore.createForm.parent_id" optionLabel="label" optionValue="id"
:options="permissions.data" filter
placeholder="انتخاب سطح" class="w-full">
<template #empty>
@@ -54,41 +54,38 @@ onUnmounted(() => {
<Form ref="formRef" v-slot="$form" class=" p-4 grid grid-cols-2 gap-4 !w-full">
<FormField v-slot="$field" name="name" initialValue="" class="flex flex-col gap-1">
<label>نام کیس</label>
<InputText type="text" :class="[
'placeholder:!text-sm',
petStore.errorsCreateForm?.name && !petStore.createForm.name ? 'p-invalid' : ''
]" v-model="petStore.createForm.name" placeholder="نام کیس خود را وارد کنید" />
<InputText type="text" :class="petStore.errorsCreateForm?.name && !petStore.createForm.name ? 'p-invalid' : ''" v-model="petStore.createForm.name" placeholder="نام کیس خود را وارد کنید" />
<Message v-if="petStore.errorsCreateForm?.name"
severity="error" size="small" variant="simple">{{ petStore.errorsCreateForm?.name[0] }}</Message>
</FormField>
<FormField v-slot="$field" name="email" initialValue="" class="flex flex-col gap-1">
<label>نوع کیس</label>
<Select class="!text-xs !h-full" placeholder="انتخاب نوع کیس" v-model="petStore.createForm.type" :options="petStore.typeOptions" optionValue="value" optionLabel="label" :pt="{ option : '!text-sm' , placeholder : '!text-sm' , label : '!pt-3' }" />
<Select placeholder="انتخاب نوع کیس" v-model="petStore.createForm.type" :options="petStore.typeOptions" optionValue="value" optionLabel="label" />
<Message v-if="petStore.errorsCreateForm?.type" severity="error" size="small" variant="simple">{{ petStore.errorsCreateForm?.type[0] }}</Message>
</FormField>
<FormField v-slot="$field" name="breed" initialValue="" class="flex flex-col gap-1">
<label>نژاد</label>
<InputText type="text" class="placeholder:!text-sm" v-model="petStore.createForm.breed" placeholder="نژاد را وارد کنید" />
<InputText type="text" v-model="petStore.createForm.breed" placeholder="نژاد را وارد کنید" />
</FormField>
<FormField v-slot="$field" name="weight" initialValue="" class="flex flex-col gap-1">
<label>وزن</label>
<InputText type="text" class="placeholder:!text-sm" v-model="petStore.createForm.weight" placeholder="وزن را وارد کنید" />
<InputText type="text" v-model="petStore.createForm.weight" placeholder="وزن را وارد کنید" />
<Message v-if="$field?.invalid" severity="error" size="small" variant="simple">{{ $field.error?.message }}</Message>
</FormField>
<FormField v-slot="$field" name="sterilized" initialValue="" class="flex flex-col gap-1">
<label>عقیم سازی</label>
<Select class="!text-xs !h-full" placeholder="انتخاب عقیم سازی" v-model="petStore.createForm.sterilized" optionLabel="label" optionValue="value" :options="petStore.sterilizedOptions" :pt="{ option : '!text-sm' , placeholder : '!text-sm' , label : '!pt-3' }" />
<Select placeholder="انتخاب عقیم سازی" v-model="petStore.createForm.sterilized" optionLabel="label" optionValue="value" :options="petStore.sterilizedOptions" />
<Message v-if="petStore.errorsCreateForm?.sterilized"
severity="error" size="small" variant="simple">{{ petStore.errorsCreateForm?.sterilized[0] }}</Message>
</FormField>
<FormField v-slot="$field" name="gender" initialValue="" class="flex flex-col gap-1">
<label>جنسیت</label>
<Select class="!text-xs !h-full" placeholder="انتخاب جنسیت" v-model="petStore.createForm.gender" optionLabel="label" optionValue="value" :options="petStore.genderOptions" :pt="{ option : '!text-sm' , placeholder : '!text-sm' , label : '!pt-3' }" />
<Select placeholder="انتخاب جنسیت" v-model="petStore.createForm.gender" optionLabel="label" optionValue="value" :options="petStore.genderOptions" />
<Message v-if="petStore.errorsCreateForm?.gender" severity="error" size="small" variant="simple">{{ petStore.errorsCreateForm?.gender[0] }}</Message>
</FormField>
<FormField v-slot="$field" name="birthday" initialValue="" class="flex flex-col gap-1">
<label>تاریخ تولد</label>
<CustomDatePicker v-model="petStore.createForm.birthday" class="!text-xs mt-2" type="datetime" compact-time></CustomDatePicker>
<CustomDatePicker v-model="petStore.createForm.birthday" type="datetime" compact-time></CustomDatePicker>
<Message v-if="petStore.errorsCreateForm?.birthday" severity="error" size="small" variant="simple">{{ petStore.errorsCreateForm?.birthday[0] }}</Message>
</FormField>
</Form>
@@ -105,7 +102,7 @@ onUnmounted(() => {
<div class="p-4 grid grid-cols-1 gap-4 rounded-2xl">
<div class="grid grid-cols-1 justify-between items-center">
<span>زمان انتشار:</span>
<CustomDatePicker v-model="petStore.createForm.datetime" class="!text-xs mt-2" type="datetime" compact-time></CustomDatePicker>
<CustomDatePicker v-model="petStore.createForm.datetime" class="mt-2" type="datetime" compact-time></CustomDatePicker>
</div>
<div class="grid grid-cols-1 justify-start">
<Button :loading="petStore.loaderCreateForm" @click="petStore.update_and_store" type="button" severity="secondary" :label="petStore.buttonLabel" />
@@ -116,29 +113,3 @@ onUnmounted(() => {
</section>
</master>
</template>
<style>
.p-multiselect {
height: auto;
min-height: 40px;
}
.p-multiselect-label {
line-height: 24px;
}
.p-multiselect .p-multiselect-header {
line-height: 24px;
}
.p-multiselect .p-multiselect-filter {
max-height: calc(100px);
overflow-y: auto;
}
.p-multiselect .p-multiselect-item {
font-size: 0.875rem;
line-height: 44px;
}
</style>
@@ -74,19 +74,13 @@ const selectAllWithOwn = () => {
<Form ref="formRef" v-slot="$form" class="bg-white p-4 rounded-2xl flex flex-col gap-4 !w-full">
<FormField v-slot="$field" name="name" initialValue="" class="flex flex-col gap-1">
<label>نام مقام</label>
<InputText type="text" :class="[
'placeholder:!text-sm',
roleStore.errorsCreateForm?.name && !roleStore.createForm.name ? 'p-invalid' : ''
]" v-model="roleStore.createForm.name" placeholder="نام مقام را وارد کنید" />
<InputText type="text" :class="roleStore.errorsCreateForm?.name && !roleStore.createForm.name ? 'p-invalid' : ''" v-model="roleStore.createForm.name" placeholder="نام مقام را وارد کنید" />
<Message v-if="roleStore.errorsCreateForm?.name"
severity="error" size="small" variant="simple">{{ roleStore.errorsCreateForm?.name[0] }}</Message>
</FormField>
<FormField v-slot="$field" name="label" initialValue="" class="flex flex-col gap-1">
<label>نام فارسی مقام</label>
<InputText type="text" :class="[
'placeholder:!text-sm',
roleStore.errorsCreateForm?.label && !roleStore.createForm.label ? 'p-invalid' : ''
]" v-model="roleStore.createForm.label" placeholder="نام فارسی مقام را وارد کنید" />
<InputText type="text" :class="roleStore.errorsCreateForm?.label && !roleStore.createForm.label ? 'p-invalid' : ''" v-model="roleStore.createForm.label" placeholder="نام فارسی مقام را وارد کنید" />
<Message v-if="roleStore.errorsCreateForm?.label" severity="error" size="small" variant="simple">{{ roleStore.errorsCreateForm?.label[0] }}</Message>
</FormField>
<FormField v-slot="$field" name="permissions_parent" initialValue="" class="flex flex-col gap-1">
@@ -109,7 +103,7 @@ const selectAllWithOwn = () => {
<div class="p-4 bg-white grid grid-cols-1 gap-4 rounded-2xl">
<div class="grid grid-cols-1 justify-between items-center">
<span>زمان انتشار:</span>
<CustomDatePicker v-model="roleStore.createForm.datetime" class="!text-xs mt-2" type="datetime" compact-time></CustomDatePicker>
<CustomDatePicker v-model="roleStore.createForm.datetime" class="mt-2" type="datetime" compact-time></CustomDatePicker>
</div>
<div class="grid grid-cols-2 gap-x-3 mt-1 justify-between items-center">
<span class="col-span-2 mb-2">عملیات سریع:</span>
@@ -98,34 +98,25 @@ const uploadProfile = () => {
<Form @submit="onFormSubmit" class="grid grid-cols-1 gap-4 w-full !border-0 !text-sm">
<FormField v-slot="$field" name="name" initialValue="" class="flex flex-col gap-1">
<label>نام کاربری</label>
<InputText type="text" :class="[
'placeholder:!text-sm !text-sm',
errors?.name && !errors.name ? 'p-invalid' : ''
]" v-model="user_form.name" placeholder="نام کاربری را وارد کنید" />
<InputText type="text" :class="errors?.name && !errors.name ? 'p-invalid' : ''" v-model="user_form.name" placeholder="نام کاربری را وارد کنید" />
<Message v-if="errors?.name"
severity="error" size="small" variant="simple">{{ errors?.name[0] }}</Message>
</FormField>
<FormField v-slot="$field" name="first_name" initialValue="" class="flex flex-col gap-1">
<label>نام کوچک</label>
<InputText type="text" :class="[
'placeholder:!text-sm !text-sm',
errors?.first_name && !errors.first_name ? 'p-invalid' : ''
]" v-model="user_form.first_name" placeholder="نام کوچک خود را وارد کنید" />
<InputText type="text" :class="errors?.first_name && !errors.first_name ? 'p-invalid' : ''" v-model="user_form.first_name" placeholder="نام کوچک خود را وارد کنید" />
<Message v-if="errors?.first_name"
severity="error" size="small" variant="simple">{{ errors?.first_name[0] }}</Message>
</FormField>
<FormField v-slot="$field" name="last_name" initialValue="" class="flex flex-col gap-1">
<label>نام خانوادگی</label>
<InputText type="text" :class="[
'placeholder:!text-sm !text-sm',
errors?.last_name && !errors.last_name ? 'p-invalid' : ''
]" v-model="user_form.last_name" placeholder="نام کوچک خود را وارد کنید" />
<InputText type="text" :class="errors?.last_name && !errors.last_name ? 'p-invalid' : ''" v-model="user_form.last_name" placeholder="نام کوچک خود را وارد کنید" />
<Message v-if="errors?.last_name"
severity="error" size="small" variant="simple">{{ errors?.last_name[0] }}</Message>
</FormField>
<FormField v-slot="$field" asChild name="email" :initialValue="globalStore.current_user.email">
<section class="flex flex-col gap-2">
<InputText type="email" class="flex pt-2 !text-sm !font-light" disabled id="email" fluid />
<InputText type="email" class="!font-light" disabled id="email" fluid />
</section>
</FormField>
<Button type="submit" :loading="loading" severity="secondary" label="ذخیره اطلاعات" />
@@ -60,7 +60,7 @@ const ChangePasswordHandler = () => {
<Password id="password" type="password" :feedback="false"
appendTo="self" toggleMask
v-model="ChangePasswordForm.password"
class="placeholder:!font-light !text-sm !bg-white h-12 !border-gray-500/20 !text-client-Fontcolor"/>
class="placeholder:!font-light !bg-white !border-gray-500/20 !text-client-Fontcolor"/>
<label for="on_label" class="!text-client-Fontcolor !bg-white !text-sm font-light">کلمه عبور فعلی</label>
</FloatLabel>
<Message
@@ -74,7 +74,7 @@ const ChangePasswordHandler = () => {
<Password id="password" type="password" :feedback="false"
appendTo="self" toggleMask
v-model="ChangePasswordForm.newPassword"
class="placeholder:!font-light !text-sm !bg-white h-12 !border-gray-500/20 !text-client-Fontcolor"/>
class="placeholder:!font-light !bg-white !border-gray-500/20 !text-client-Fontcolor"/>
<label for="on_label" class="!text-client-Fontcolor !bg-white !text-sm font-light"> کلمه عبور جدید</label>
</FloatLabel>
<Message
@@ -88,7 +88,7 @@ const ChangePasswordHandler = () => {
<Password id="password" type="password" :feedback="false"
appendTo="self" toggleMask
v-model="ChangePasswordForm.newPassword_confirmation"
class="placeholder:!font-light !text-sm !bg-white h-12 !border-gray-500/20 !text-client-Fontcolor"/>
class="placeholder:!font-light !bg-white !border-gray-500/20 !text-client-Fontcolor"/>
<label for="on_label" class="!text-client-Fontcolor !bg-white !text-sm font-light">تکرار کلمه عبور جدید</label>
</FloatLabel>
<Message
@@ -4,6 +4,8 @@ import {onMounted , onUnmounted} from 'vue';
import {useShiftStore} from "../../store/Shift.js";
import {Link} from "@inertiajs/inertia-vue3";
import Card from "../../components/ui/Card.vue";
import ShiftCalendar from "../../components/shifts/ShiftCalendar.vue";
import { weekdayName } from "../../../Shared/shiftCalendarEvents";
const shiftStore = useShiftStore();
@@ -18,8 +20,22 @@ const props = defineProps({
},
});
const pad = (value) => String(value).padStart(2, '0');
const onRangeSelect = ({ start, end }) => {
shiftStore.createForm.start_time = `${pad(start.getHours())}:${pad(start.getMinutes())}`;
shiftStore.createForm.end_time = `${pad(end.getHours())}:${pad(end.getMinutes())}`;
if (shiftStore.createForm.schedule_type === 'date') {
shiftStore.createForm.date = `${start.getFullYear()}-${pad(start.getMonth() + 1)}-${pad(start.getDate())}`;
} else {
shiftStore.createForm.day_of_week = weekdayName(start.getDay());
}
};
onMounted(() => {
shiftStore.fetchDoctorOptions();
shiftStore.fetchCalendarData();
if (props.is_edit) {
shiftStore.setEditData(props.shift);
}
@@ -52,36 +68,36 @@ onUnmounted(() => {
<Form ref="formRef" v-slot="$form" class=" p-4 grid grid-cols-2 gap-4 !w-full">
<FormField v-slot="$field" name="user_id" initialValue="" class="flex flex-col gap-1">
<label>پزشک</label>
<Select class="!text-xs !h-full" placeholder="انتخاب پزشک (خالی = شیفت آزاد)" v-model="shiftStore.createForm.user_id" :options="shiftStore.doctorOptions" optionValue="value" optionLabel="label" showClear :pt="{ option : '!text-sm' , placeholder : '!text-sm' , label : '!pt-3' }" />
<Select placeholder="انتخاب پزشک (خالی = شیفت آزاد)" v-model="shiftStore.createForm.user_id" :options="shiftStore.doctorOptions" optionValue="value" optionLabel="label" showClear />
<Message v-if="shiftStore.errorsCreateForm?.user_id" severity="error" size="small" variant="simple">{{ shiftStore.errorsCreateForm?.user_id[0] }}</Message>
</FormField>
<FormField v-slot="$field" name="schedule_type" initialValue="" class="flex flex-col gap-1">
<label>نوع زمان‌بندی</label>
<Select class="!text-xs !h-full" placeholder="انتخاب نوع زمان‌بندی" v-model="shiftStore.createForm.schedule_type" :options="shiftStore.scheduleTypeOptions" optionValue="value" optionLabel="label" :pt="{ option : '!text-sm' , placeholder : '!text-sm' , label : '!pt-3' }" />
<Select placeholder="انتخاب نوع زمان‌بندی" v-model="shiftStore.createForm.schedule_type" :options="shiftStore.scheduleTypeOptions" optionValue="value" optionLabel="label" />
</FormField>
<FormField v-if="shiftStore.createForm.schedule_type === 'weekly'" v-slot="$field" name="day_of_week" initialValue="" class="flex flex-col gap-1">
<label>روز هفته</label>
<Select class="!text-xs !h-full" placeholder="انتخاب روز هفته" v-model="shiftStore.createForm.day_of_week" :options="shiftStore.dayOptions" optionValue="value" optionLabel="label" :pt="{ option : '!text-sm' , placeholder : '!text-sm' , label : '!pt-3' }" />
<Select placeholder="انتخاب روز هفته" v-model="shiftStore.createForm.day_of_week" :options="shiftStore.dayOptions" optionValue="value" optionLabel="label" />
<Message v-if="shiftStore.errorsCreateForm?.day_of_week" severity="error" size="small" variant="simple">{{ shiftStore.errorsCreateForm?.day_of_week[0] }}</Message>
</FormField>
<FormField v-else v-slot="$field" name="date" initialValue="" class="flex flex-col gap-1">
<label>تاریخ</label>
<CustomDatePicker v-model="shiftStore.createForm.date" class="!text-xs mt-2" type="date" format="YYYY-MM-DD" placeholder="تاریخ شیفت را انتخاب کنید"></CustomDatePicker>
<CustomDatePicker v-model="shiftStore.createForm.date" type="date" format="YYYY-MM-DD" placeholder="تاریخ شیفت را انتخاب کنید"></CustomDatePicker>
<Message v-if="shiftStore.errorsCreateForm?.date" severity="error" size="small" variant="simple">{{ shiftStore.errorsCreateForm?.date[0] }}</Message>
</FormField>
<FormField v-slot="$field" name="start_time" initialValue="" class="flex flex-col gap-1">
<label>ساعت شروع</label>
<CustomDatePicker v-model="shiftStore.createForm.start_time" class="!text-xs mt-2" type="time" format="HH:mm" display-format="HH:mm" placeholder="ساعت شروع را انتخاب کنید"></CustomDatePicker>
<CustomDatePicker v-model="shiftStore.createForm.start_time" type="time" format="HH:mm" display-format="HH:mm" placeholder="ساعت شروع را انتخاب کنید"></CustomDatePicker>
<Message v-if="shiftStore.errorsCreateForm?.start_time" severity="error" size="small" variant="simple">{{ shiftStore.errorsCreateForm?.start_time[0] }}</Message>
</FormField>
<FormField v-slot="$field" name="end_time" initialValue="" class="flex flex-col gap-1">
<label>ساعت پایان</label>
<CustomDatePicker v-model="shiftStore.createForm.end_time" class="!text-xs mt-2" type="time" format="HH:mm" display-format="HH:mm" placeholder="ساعت پایان را انتخاب کنید"></CustomDatePicker>
<CustomDatePicker v-model="shiftStore.createForm.end_time" type="time" format="HH:mm" display-format="HH:mm" placeholder="ساعت پایان را انتخاب کنید"></CustomDatePicker>
<Message v-if="shiftStore.errorsCreateForm?.end_time" severity="error" size="small" variant="simple">{{ shiftStore.errorsCreateForm?.end_time[0] }}</Message>
</FormField>
<FormField v-slot="$field" name="is_active" initialValue="" class="flex flex-col gap-1">
<label>وضعیت</label>
<Select class="!text-xs !h-full" placeholder="انتخاب وضعیت" v-model="shiftStore.createForm.is_active" :options="shiftStore.statusOptions" optionValue="value" optionLabel="label" :pt="{ option : '!text-sm' , placeholder : '!text-sm' , label : '!pt-3' }" />
<Select placeholder="انتخاب وضعیت" v-model="shiftStore.createForm.is_active" :options="shiftStore.statusOptions" optionValue="value" optionLabel="label" />
<Message v-if="shiftStore.errorsCreateForm?.is_active" severity="error" size="small" variant="simple">{{ shiftStore.errorsCreateForm?.is_active[0] }}</Message>
</FormField>
</Form>
@@ -97,5 +113,18 @@ onUnmounted(() => {
</Card>
</div>
</section>
<Card class="mt-4">
<div class="p-4 rounded-2xl">
<div class="flex items-center justify-between pb-2">
<h5 class="font-bold text-client-Fontcolor">برنامه فعلی شیفت‌ها</h5>
<span class="text-xs text-client-Fontcolor/60">برای تنظیم سریع زمان شیفت، بازه مورد نظر را روی تقویم بکشید</span>
</div>
<ShiftCalendar
:shifts="shiftStore.calendarShifts"
selectable
@select-range="onRangeSelect"
/>
</div>
</Card>
</master>
</template>
@@ -58,41 +58,32 @@ onUnmounted(() => {
<Form ref="formRef" v-slot="$form" class=" p-4 flex flex-col gap-4 !w-full">
<FormField v-slot="$field" name="username" initialValue="" class="flex flex-col gap-1">
<label>نام کاربری</label>
<InputText type="text" :class="[
'placeholder:!text-sm',
userStore.errorsCreateForm?.username && !userStore.createForm.username ? 'p-invalid' : ''
]" v-model="userStore.createForm.username" placeholder="نام کاربری خود را وارد کنید" />
<InputText type="text" :class="userStore.errorsCreateForm?.username && !userStore.createForm.username ? 'p-invalid' : ''" v-model="userStore.createForm.username" placeholder="نام کاربری خود را وارد کنید" />
<Message v-if="userStore.errorsCreateForm?.username"
severity="error" size="small" variant="simple">{{ userStore.errorsCreateForm?.username[0] }}</Message>
</FormField>
<FormField v-slot="$field" name="email" initialValue="" class="flex flex-col gap-1">
<label>ایمیل کاربری</label>
<InputText type="text" :class="[
'placeholder:!text-sm',
userStore.errorsCreateForm?.email && !userStore.createForm.email ? 'p-invalid' : ''
]" v-model="userStore.createForm.email" placeholder="ایمیل خود را وارد کنید" />
<InputText type="text" :class="userStore.errorsCreateForm?.email && !userStore.createForm.email ? 'p-invalid' : ''" v-model="userStore.createForm.email" placeholder="ایمیل خود را وارد کنید" />
<Message v-if="userStore.errorsCreateForm?.email" severity="error" size="small" variant="simple">{{ userStore.errorsCreateForm?.email[0] }}</Message>
</FormField>
<FormField v-slot="$field" name="national_code" initialValue="" class="flex flex-col gap-1">
<label>کد ملی</label>
<InputText type="text" :class="[
'placeholder:!text-sm',
userStore.errorsCreateForm?.national_code && !userStore.createForm.national_code ? 'p-invalid' : ''
]" v-model="userStore.createForm.national_code" placeholder="کد ملی خود را وارد کنید" />
<InputText type="text" :class="userStore.errorsCreateForm?.national_code && !userStore.createForm.national_code ? 'p-invalid' : ''" v-model="userStore.createForm.national_code" placeholder="کد ملی خود را وارد کنید" />
<Message v-if="userStore.errorsCreateForm?.national_code" severity="error" size="small" variant="simple">{{ userStore.errorsCreateForm?.national_code[0] }}</Message>
</FormField>
<FormField v-slot="$field" name="phone" initialValue="" class="flex flex-col gap-1">
<label>شماره تماس</label>
<InputText type="text" class="placeholder:!text-sm" v-model="userStore.createForm.phone" placeholder="شماره تماس خود را وارد کنید" />
<InputText type="text" v-model="userStore.createForm.phone" placeholder="شماره تماس خود را وارد کنید" />
</FormField>
<FormField v-slot="$field" name="fullname" initialValue="" class="flex flex-col gap-1">
<label>نام خانوادگی</label>
<InputText type="text" class="placeholder:!text-sm" v-model="userStore.createForm.fullname" placeholder="نام خانوادگی خود را وارد کنید" />
<InputText type="text" v-model="userStore.createForm.fullname" placeholder="نام خانوادگی خود را وارد کنید" />
<Message v-if="$field?.invalid" severity="error" size="small" variant="simple">{{ $field.error?.message }}</Message>
</FormField>
<FormField v-slot="$field" name="password" initialValue="" class="flex flex-col gap-1">
<label>کلمه عبور</label>
<Password type="text" :inputClass="['placeholder:!text-sm' , { 'p-invalid' : userStore.errorsCreateForm?.password && !userStore.createForm.password}]"
<Password type="text" :inputClass="{ 'p-invalid' : userStore.errorsCreateForm?.password && !userStore.createForm.password}"
v-model="userStore.createForm.password" placeholder="کلمه عبور خود را وارد کنید" :feedback="false" toggleMask fluid />
<Message v-if="userStore.errorsCreateForm?.password"
severity="error" size="small" variant="simple">{{ userStore.errorsCreateForm?.password[0] }}</Message>
@@ -100,7 +91,7 @@ onUnmounted(() => {
</FormField>
<FormField v-slot="$field" name="password_confirmation" initialValue="" class="flex flex-col gap-1">
<label>تکرار کلمه عبور</label>
<Password type="text" inputClass="placeholder:!text-sm" v-model="userStore.createForm.password_confirmation" placeholder="تکرار کلمه عبور خود را وارد کنید" :feedback="false" toggleMask fluid />
<Password type="text" v-model="userStore.createForm.password_confirmation" placeholder="تکرار کلمه عبور خود را وارد کنید" :feedback="false" toggleMask fluid />
<Message v-if="$field?.invalid" severity="error" size="small" variant="simple">{{ $field.error?.message }}</Message>
</FormField>
</Form>
@@ -117,7 +108,7 @@ onUnmounted(() => {
<div class="mt-4 p-4 grid grid-cols-1 gap-4 rounded-2xl">
<div class="grid grid-cols-1 justify-between items-center">
<span>زمان انتشار:</span>
<CustomDatePicker v-model="userStore.createForm.datetime" class="!text-xs mt-2" type="datetime" compact-time></CustomDatePicker>
<CustomDatePicker v-model="userStore.createForm.datetime" class="mt-2" type="datetime" compact-time></CustomDatePicker>
</div>
<div class="grid grid-cols-2a justify-between items-center">
<span class="flex">سطح:</span>
@@ -125,9 +116,7 @@ onUnmounted(() => {
</div>
<div class="grid grid-cols-2a justify-between items-center">
<span class="flex">مقام:</span>
<MultiSelect class="!text-xs"
v-model="userStore.createForm.role"
:pt="{ option : '!text-sm' }"
<MultiSelect v-model="userStore.createForm.role"
placeholder="انتخاب مقام"
optionLabel="label"
optionValue="id"
@@ -142,29 +131,3 @@ onUnmounted(() => {
</section>
</master>
</template>
<style>
.p-multiselect {
height: auto;
min-height: 40px;
}
.p-multiselect-label {
line-height: 24px;
}
.p-multiselect .p-multiselect-header {
line-height: 24px;
}
.p-multiselect .p-multiselect-filter {
max-height: calc(100px);
overflow-y: auto;
}
.p-multiselect .p-multiselect-item {
font-size: 0.875rem;
line-height: 44px;
}
</style>
@@ -0,0 +1,267 @@
<script setup>
import Master from "../../layouts/master.vue";
import { ref, reactive, watch } from 'vue';
import TableGeneric from "../../components/TableGeneric.vue";
import { useConfirm } from 'primevue/useconfirm';
import { useToast } from 'primevue/usetoast';
import { useWithdrawRequestStore } from "../../store/WithdrawRequest.js";
import UIModal from "../../components/ui/UIModal.vue";
const withdrawRequestStore = useWithdrawRequestStore();
const toast = useToast();
const props = defineProps({
withdrawRequests: {
type: Object,
},
stats: {
type: Object,
},
search: {
type: String
},
status: {
type: String
},
})
const withdrawRequests = ref(props.withdrawRequests ?? []);
const searchLoader = ref(false);
const actionLoader = ref(false);
const columns = ref(withdrawRequestStore.columns)
const statusOptions = [
'همه وضعیت ها',
'در انتظار بررسی',
'تایید شده',
'رد شده',
];
const queryParams = reactive({
search: props.search ?? '',
status: props.status ?? 'همه وضعیت ها',
})
const confirm = useConfirm();
watch(() => props.withdrawRequests, (newRequests) => {
if (newRequests) {
withdrawRequests.value = newRequests;
}
}, { deep: true, immediate: true });
const onPageChange = (event) => {
withdrawRequestStore.navigateToPage(event.page + 1);
};
const searchData = () => {
searchLoader.value = true;
withdrawRequestStore.search(queryParams).then(() => {
searchLoader.value = false;
});
}
const onSearchChange = (value) => {
queryParams.search = value;
searchData();
};
const formatCardNumber = (cardNumber) => cardNumber.replace(/(\d{4})(?=\d)/g, '$1-');
const showApproveConfirm = (data) => {
confirm.require({
group: 'deleteTemplate',
header: 'تایید درخواست برداشت',
message: `مبلغ ${Number(data.amount).toLocaleString('fa-IR')} تومان از کیف پول «${data.user?.fullname}» کسر و به ولت سایت منتقل می‌شود.`,
icon: 'fi fi-rr-info',
rejectProps: {
label: 'انصراف',
icon: 'fi fi-rr-ban',
outlined: true,
size: 'small',
class: '!text-xs !flex pt-1 !item-center',
iconClass: '!flex !text-xs',
},
acceptProps: {
label: 'تایید برداشت',
icon: 'fi fi-rr-check',
size: 'small',
class: '!text-xs !flex pt-1 !item-center',
iconClass: '!flex !text-xs',
},
accept: () => {
actionLoader.value = true;
withdrawRequestStore.approve(data.id)
.catch((error) => {
toast.add({ severity: 'error', summary: 'خطا', detail: error.response?.data?.message || 'خطا در تایید درخواست', life: 4000 });
})
.finally(() => {
actionLoader.value = false;
});
},
reject: () => {
}
});
}
const rejectModalVisible = ref(false);
const rejectingRequest = ref(null);
const rejectNote = ref('');
const openRejectModal = (data) => {
rejectingRequest.value = data;
rejectNote.value = '';
rejectModalVisible.value = true;
}
const submitReject = () => {
actionLoader.value = true;
withdrawRequestStore.reject(rejectingRequest.value.id, rejectNote.value)
.then(() => {
rejectModalVisible.value = false;
})
.catch((error) => {
toast.add({ severity: 'error', summary: 'خطا', detail: error.response?.data?.message || 'خطا در رد درخواست', life: 4000 });
})
.finally(() => {
actionLoader.value = false;
});
}
</script>
<template>
<master>
<div class="mb-4 grid grid-cols-2a justify-between items-center bg-white border-[1px] border-client-gray/20 py-4 px-4 rounded-2xl">
<h2 class="font-bold text-client-Fontcolor">{{ withdrawRequestStore.indexPageTitle }}</h2>
</div>
<div class="mb-4 grid grid-cols-3 max-lg:grid-cols-1 gap-4">
<div class="bg-white border-[1px] border-client-gray/20 rounded-2xl p-4 flex flex-col gap-2">
<span class="text-xs text-gray-500">درخواست‌های در انتظار بررسی</span>
<strong class="text-lg text-client-Fontcolor">{{ Number(stats.pending_count).toLocaleString('fa-IR') }} درخواست</strong>
</div>
<div class="bg-white border-[1px] border-client-gray/20 rounded-2xl p-4 flex flex-col gap-2">
<span class="text-xs text-gray-500">مجموع مبلغ در انتظار برداشت</span>
<strong class="text-lg text-client-Fontcolor">{{ Number(stats.pending_amount).toLocaleString('fa-IR') }} تومان</strong>
</div>
<div class="bg-white border-[1px] border-client-gray/20 rounded-2xl p-4 flex flex-col gap-2">
<span class="text-xs text-gray-500">موجودی ولت سایت</span>
<strong class="text-lg text-client-Fontcolor">{{ Number(stats.system_wallet_balance).toLocaleString('fa-IR') }} تومان</strong>
</div>
</div>
<div class="card border-[1px] border-client-gray/20 bg-white rounded-2xl">
<TableGeneric
:data="withdrawRequests.data"
:columns="columns"
:selectable="false"
:pagination="withdrawRequests.meta"
:searchable="true"
:searchValue="queryParams.search"
:model-value="[]"
@page-change="onPageChange"
@search-change="onSearchChange"
>
<template #toolbar-actions>
<div class="grid grid-cols-a gap-4 justify-start">
<Dropdown
v-model="queryParams.status"
:options="statusOptions"
placeholder="انتخاب وضعیت..."
class="!text-xs"
@change="searchData"
/>
</div>
</template>
<template #column-user="{ data }">
<div class="flex flex-col gap-1">
<span class="text-sm">{{ data.user?.fullname }}</span>
<span class="text-xs text-gray-500">{{ data.user?.level }} - {{ data.user?.phone }}</span>
</div>
</template>
<template #column-bank_card="{ data }">
<div v-if="data.bank_card" class="flex flex-col gap-1">
<pre dir="ltr" class="text-right text-xs">{{ formatCardNumber(data.bank_card.card_number) }}</pre>
<span class="text-xs text-gray-500">{{ data.bank_card.holder_name }}</span>
</div>
<span v-else class="text-xs text-gray-500">حذف شده</span>
</template>
<template #column-status="{ data }">
<div class="flex flex-col gap-1 items-start">
<Tag
class="!text-xs"
:value="data.status"
:severity="withdrawRequestStore.getStatusSeverity(data.status)"
/>
<span v-if="data.admin_note" class="text-xs text-gray-500">{{ data.admin_note }}</span>
</div>
</template>
<template #column-actions="{ data }">
<div v-if="data.status === 'در انتظار بررسی'" class="grid grid-cols-2a justify-start gap-2">
<Button
label="تایید"
size="small"
severity="success"
class="!text-xs"
:disabled="actionLoader"
@click="showApproveConfirm(data)"
/>
<Button
label="رد"
size="small"
severity="danger"
variant="outlined"
class="!text-xs"
:disabled="actionLoader"
@click="openRejectModal(data)"
/>
</div>
</template>
</TableGeneric>
</div>
<UIModal
v-model:visible="rejectModalVisible"
title="رد درخواست برداشت"
size="small"
>
<div class="flex flex-col gap-3">
<p class="text-sm text-gray-600">
درخواست برداشت «{{ rejectingRequest ? Number(rejectingRequest.amount).toLocaleString('fa-IR') : '' }} تومان» کاربر «{{ rejectingRequest?.user?.fullname }}» رد می‌شود.
</p>
<textarea
v-model="rejectNote"
rows="4"
placeholder="توضیح برای کاربر (اختیاری)"
class="w-full border border-gray-200 rounded-xl p-3 text-sm outline-none focus:border-gray-400 resize-y"
></textarea>
<div class="flex items-center justify-start gap-2">
<Button
label="رد درخواست"
size="small"
severity="danger"
class="!text-xs"
:loading="actionLoader"
:disabled="actionLoader"
@click="submitReject"
/>
<Button
label="انصراف"
size="small"
severity="secondary"
variant="outlined"
class="!text-xs"
:disabled="actionLoader"
@click="rejectModalVisible = false"
/>
</div>
</div>
</UIModal>
</master>
</template>
@@ -86,6 +86,14 @@ const items: Ref<SidebarItem[]> = ref([
permission: "reports.index",
expanded: false,
},
{
label: "درخواست‌های برداشت",
icon: "fi fi-rr-wallet",
to: "/admin/withdraw-requests",
key: 'withdrawals',
permission: "withdrawals.index",
expanded: false,
},
{
label: "مدیریت شیفت ها",
icon: "fi fi-rr-calendar",
@@ -251,7 +251,6 @@ const getColumnStyle = (column) => {
<div class="grid grid-cols-1 items-center justify-start">
<InputText
size="small"
class="placeholder:!font-light placeholder:!text-xs !h-10"
placeholder="جستجو..."
type="text"
v-model="localSearchValue"
@@ -274,8 +273,7 @@ const getColumnStyle = (column) => {
optionLabel="label"
optionValue="value"
size="small"
:pt="{label : '!text-[12px] !pt-2.5' , option : '!text-[12px] !font-normal'}"
class="min-w-[130px] max-sm:!w-full max-w-[200px] !text-[12px] !h-10"
class="min-w-[130px] max-sm:!w-full max-w-[200px]"
:loading="renderFilter(column).loading"
:disabled="renderFilter(column).loading"
:filter="renderFilter(column).search"
@@ -297,8 +295,7 @@ const getColumnStyle = (column) => {
optionLabel="label"
optionValue="value"
size="small"
:pt="{label : '!text-[12px] !pt-2.5' , option : '!text-[12px] !font-normal'}"
class="min-w-[150px] max-sm:!w-full max-w-[200px] !text-[12px] !h-10"
class="min-w-[150px] max-sm:!w-full max-w-[200px]"
:loading="renderFilter(column).loading"
:disabled="renderFilter(column).loading"
:filter="renderFilter(column).search"
@@ -11,10 +11,14 @@ const props = defineProps({
shifts: {
type: Array,
required: true
},
selectable: {
type: Boolean,
default: false
}
})
const emit = defineEmits(['select-shift']);
const emit = defineEmits(['select-shift', 'select-range']);
const doctorPalette = ['#576989', '#800E13', '#50CD89', '#7239EA', '#FFC700', '#F1416C', '#009EF7', '#965457'];
@@ -71,6 +75,11 @@ const calendarOptions = computed(() => ({
eventClick: (info) => {
emit('select-shift', info.event.extendedProps.shift);
},
selectable: props.selectable,
selectMirror: true,
select: (info) => {
emit('select-range', { start: info.start, end: info.end });
},
}))
const goPrev = () => calendarRef.value?.getApi().prev();
@@ -45,15 +45,11 @@ const props = defineProps({
},
class: {
type: String,
default: '!text-xs !h-full'
default: 'w-full'
},
pt: {
type: Object,
default: () => ({
option: '!text-sm',
placeholder: '!text-sm',
label: '!pt-3'
})
default: () => ({})
}
})
+110
View File
@@ -0,0 +1,110 @@
import { defineStore } from 'pinia'
import axios from "axios";
import { Inertia } from '@inertiajs/inertia'
export const useWithdrawRequestStore = defineStore('WithdrawRequestStore', {
state: () => ({
columns: [
{
key: 'id',
title: 'شناسه',
width: 100,
},
{
key: 'user',
title: 'درخواست‌کننده',
slot: 'user',
},
{
key: 'amount',
title: 'مبلغ',
render: (data) => `${Number(data.amount).toLocaleString('fa-IR')} تومان`,
},
{
key: 'bank_card',
title: 'کارت مقصد',
slot: 'bank_card',
width: 220,
},
{
key: 'status',
title: 'وضعیت',
slot: 'status',
},
{
key: 'created_at',
title: 'تاریخ درخواست',
width: 180,
},
{
key: 'reviewed_by',
title: 'بررسی‌کننده',
render: (data) => data.reviewed_by ? `${data.reviewed_by.fullname} - ${data.reviewed_at}` : 'بررسی نشده',
},
{
key: 'actions',
title: 'عملیات',
hasSort: false,
slot: true,
},
],
endpoints: {
index: '/admin/withdraw-requests',
approve: (id) => `/admin/withdraw-requests/${id}/approve`,
reject: (id) => `/admin/withdraw-requests/${id}/reject`,
}
}),
getters: {
indexPageTitle: () => {
return 'درخواست‌های برداشت';
}
},
actions: {
approve(id) {
return new Promise((resolve, reject) => {
axios.post(this.endpoints.approve(id))
.then(response => {
Inertia.get(this.endpoints.index);
resolve(response);
})
.catch(error => {
reject(error);
});
});
},
reject(id, note) {
return new Promise((resolve, rejectPromise) => {
axios.post(this.endpoints.reject(id), { note })
.then(response => {
Inertia.get(this.endpoints.index);
resolve(response);
})
.catch(error => {
rejectPromise(error);
});
});
},
search(params) {
return Inertia.get(this.endpoints.index, params, {
preserveState: false
});
},
navigateToPage(page) {
return Inertia.get(this.endpoints.index, { page }, { preserveState: true });
},
getStatusSeverity(status) {
const statusSeverities = {
'در انتظار بررسی': 'warning',
'تایید شده': 'success',
'رد شده': 'danger',
};
return statusSeverities[status] || 'secondary';
}
}
})
@@ -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>
@@ -7,9 +7,10 @@ import UiButton from '../../components/ui/UiButton.vue';
import EyeSvg from '../../components/svg/eye.svg';
import UiCard from '../../components/ui/UiCard.vue';
import RequestCard from '../../components/dashboard/RequestCard.vue';
import { ref } from 'vue';
import { Link } from '@inertiajs/inertia-vue3';
import { onMounted, ref } from 'vue';
import { Link, usePage } from '@inertiajs/inertia-vue3';
import FastRequestModal from '../../components/dashboard/FastRequestModal.vue';
import PaymentResultModal from '../../components/dashboard/PaymentResultModal.vue';
import { useFastRequestStore } from '../../store/FastRequest';
import { modalities } from '../../data/fastRequestOptions';
import { caseColumns } from '../../data/casesTableColumns';
@@ -22,6 +23,17 @@ const openFastRequestModal = (type) => {
fastRequestStore.requestType = type;
}
const paymentResultVisible = ref(false);
const paymentResult = ref(null);
onMounted(() => {
const result = usePage().props.value.flash?.payment_result;
if (result) {
paymentResult.value = result;
paymentResultVisible.value = true;
}
})
defineProps({
user: {
type: Object,
@@ -30,13 +42,17 @@ defineProps({
cases: {
type: Object,
required: true
},
stats: {
type: Object,
required: true
}
})
</script>
<template>
<ClientLayout>
<ActivitiesWidget />
<ActivitiesWidget :stats="stats" />
<UiCard title="ثبت آنی درخواست جدید" variant="default" class="mt-4">
<p class="w-full flex items-center justify-start font-[400] text-[11px]">فقط چند قدم ساده برای ثبت درخواست کافیه.</p>
<div class="w-full h-[179px] grid grid-cols-5 gap-3 justify-start pt-[10px]">
@@ -77,5 +93,6 @@ defineProps({
<ActivitiesChart />
</UiCard>
<fast-request-modal v-model:visible="fastRequestModalVisible"></fast-request-modal>
<payment-result-modal v-model:visible="paymentResultVisible" :result="paymentResult"></payment-result-modal>
</ClientLayout>
</template>
@@ -1,26 +0,0 @@
<script setup>
import ClientLayout from '../../../layouts/ClientLayout.vue';
const props = defineProps({
document: {
type: Object,
required: true,
}
});
</script>
<template>
<ClientLayout>
<h1>{{ document.title }}</h1>
<p>{{ document.description }}</p>
<p>{{ document.created_at }}</p>
<p>{{ document.updated_at }}</p>
<p>{{ document.status }}</p>
<p>{{ document.priority }}</p>
<p>{{ document.note }}</p>
<p>{{ document.modality }}</p>
<p>{{ document.pet_id }}</p>
<p>{{ document.requester_id }}</p>
</ClientLayout>
</template>
@@ -0,0 +1,55 @@
<script setup>
import { ref } from 'vue';
import ClientLayout from '../../../layouts/ClientLayout.vue';
import TableGeneric from '../../../components/ui/TableGeneric.vue';
import UiCard from '../../../components/ui/UiCard.vue';
import UiButton from '../../../components/ui/UiButton.vue';
import InvoiceModal from '../../../components/dashboard/fastRequest/InvoiceModal.vue';
import { invoiceColumns } from '../../../data/invoicesTableColumns';
defineProps({
invoices: {
type: Object,
required: true
}
});
const selectedInvoice = ref(null);
const invoiceModalVisible = ref(false);
const openInvoice = (invoice) => {
selectedInvoice.value = invoice;
invoiceModalVisible.value = true;
};
</script>
<template>
<ClientLayout>
<UiCard title="لیست فاکتورها" variant="default">
<TableGeneric
class="bg-white !rounded-2xl"
:data="invoices.data"
customStyle="!text-[11px] !font-normal"
:searchable="false"
:columns="invoiceColumns"
:selectable="false"
:pagination="invoices.meta"
:model-value="[]"
>
<template #column-actions="{ data }">
<div class="flex items-center justify-end">
<UiButton
v-if="data.status === 'در انتظار پرداخت'"
label="پرداخت"
severity="success"
size="small"
@click="openInvoice(data)"
/>
</div>
</template>
</TableGeneric>
</UiCard>
<InvoiceModal v-model:visible="invoiceModalVisible" :invoice="selectedInvoice" />
</ClientLayout>
</template>
@@ -1,11 +1,42 @@
<script setup>
import ClientLayout from '../../../layouts/ClientLayout.vue'
import ClientLayout from '../../../layouts/ClientLayout.vue';
import TableGeneric from '../../../components/ui/TableGeneric.vue';
import UiCard from '../../../components/ui/UiCard.vue';
import EyeSvg from '../../../components/svg/eye.svg';
import { Link } from '@inertiajs/inertia-vue3';
import { reportColumns } from '../../../data/reportsTableColumns';
defineProps({
reports: {
type: Object,
required: true
}
})
</script>
<template>
<ClientLayout>
<h1 class="text-2xl font-bold">
Reports
</h1>
<UiCard title="گزارشات" variant="default">
<TableGeneric
class="bg-white !rounded-2xl"
:data="reports.data"
customStyle="!text-[11px] !font-normal"
:searchable="false"
:columns="reportColumns"
:selectable="false"
:pagination="reports.meta"
:model-value="[]"
>
<template #column-actions="{ data }">
<div class="flex items-center justify-end">
<Link :href="`/dashboard/cases/${data.document?.id}`"
class="text-primary-500 bg-complement-100 flex items-center justify-end p-1.5 rounded-md"
>
<EyeSvg class="w-4 h-4 text-primary-500" />
</Link>
</div>
</template>
</TableGeneric>
</UiCard>
</ClientLayout>
</template>
@@ -0,0 +1,189 @@
<script setup>
import { computed, ref } from 'vue';
import { usePage } from '@inertiajs/inertia-vue3';
import { toast } from 'vue3-toastify';
import ClientLayout from '../../../layouts/ClientLayout.vue';
import TableGeneric from '../../../components/ui/TableGeneric.vue';
import UiCard from '../../../components/ui/UiCard.vue';
import UiInput from '../../../components/ui/UiInput.vue';
import UiButton from '../../../components/ui/UiButton.vue';
import WalletSvg from '../../../components/svg/wallet.svg';
import WithdrawRequestCard from '../../../components/dashboard/wallet/WithdrawRequestCard.vue';
import { walletTransactionColumns } from '../../../data/walletTransactionsTableColumns';
import { withdrawRequestColumns } from '../../../data/withdrawRequestsTableColumns';
import { convertEnglishToPersian, convertPersianToEnglish, formatPrice } from '../../../utils/numberUtils';
const props = defineProps({
transactions: {
type: Object,
required: true
},
withdrawRequests: {
type: Array,
required: true
},
bankCards: {
type: Array,
required: true
}
});
const { props: pageProps } = usePage();
const quickAmounts = [100000, 200000, 500000];
const amount = ref('');
const depositing = ref(false);
const numericAmount = computed(() => Number(convertPersianToEnglish(amount.value).replace(/\D/g, '')) || 0);
const formattedBalance = computed(() => formatPrice(pageProps.value.auth?.wallet_balance || 0));
const pendingWithdrawAmount = computed(() => props.withdrawRequests
.filter((request) => request.status === 'در انتظار بررسی')
.reduce((sum, request) => sum + request.amount, 0));
const withdrawStatusClasses = (status) => {
if (status === 'تایید شده') {
return 'text-[#1F7A4D] bg-[#E6F4EC]';
}
if (status === 'رد شده') {
return 'text-[#7A1419] bg-[#F8E9EA]';
}
return 'text-[#576989] bg-[#EEF2F8]';
};
const formatCardNumber = (cardNumber) => convertEnglishToPersian(cardNumber.replace(/(\d{4})(?=\d)/g, '$1 - '));
const selectQuickAmount = (value) => {
amount.value = String(value);
};
const handleDeposit = async () => {
if (numericAmount.value < 10000) {
toast.error('حداقل مبلغ شارژ ۱۰٬۰۰۰ تومان است.');
return;
}
depositing.value = true;
try {
const { data } = await window.axios.post('/dashboard/wallet/deposit', { amount: numericAmount.value });
window.location.href = data.url;
} catch (error) {
depositing.value = false;
toast.error(error.response?.data?.message || 'خطا در اتصال به درگاه پرداخت. لطفا دوباره تلاش کنید.');
}
};
</script>
<template>
<ClientLayout>
<div class="flex flex-col gap-4">
<UiCard title="کیف پول" variant="default">
<div class="grid grid-cols-2 max-sm:grid-cols-1 gap-4">
<div class="flex items-center gap-3 bg-[#EEF2F8] border border-[#DEE3EB] rounded-[12px] px-5 py-4">
<WalletSvg class="w-6 h-6" />
<div class="flex flex-col gap-1">
<span class="text-[11px] font-normal text-[#576989]">موجودی کیف پول</span>
<strong class="text-[16px] font-bold text-[#3E4C63]">{{ formattedBalance }} تومان</strong>
</div>
</div>
<div class="flex items-center gap-3 bg-[#F8FAFC] border border-[#DEE3EB] rounded-[12px] px-5 py-4">
<div class="flex flex-col gap-1">
<span class="text-[11px] font-normal text-[#576989]">مبلغ در انتظار برداشت</span>
<strong class="text-[16px] font-bold text-[#3E4C63]">{{ formatPrice(pendingWithdrawAmount) }} تومان</strong>
</div>
</div>
</div>
</UiCard>
<UiCard title="شارژ کیف پول" variant="default">
<form class="flex flex-col gap-4" @submit.prevent="handleDeposit">
<p class="text-[11px] font-normal text-[#576989] leading-6">
مبلغ مورد نظر را وارد کنید؛ پس از پرداخت از طریق درگاه بانکی، موجودی کیف پول شما بلافاصله افزایش می‌یابد.
</p>
<div class="flex items-end gap-3 max-sm:flex-col max-sm:items-stretch">
<UiInput
v-model="amount"
label="مبلغ شارژ"
placeholder="مبلغ مورد نظر را به تومان وارد کنید"
prefixEnd="تومان"
prefixClasses="!border-dominant-50"
class="!w-[22rem] max-sm:!w-full"
customClass="!border-dominant-50 !font-normal !text-[12px]"
/>
<UiButton
label="افزایش موجودی"
severity="success"
size="small"
:loading="depositing"
:disabled="depositing"
@click="handleDeposit"
/>
</div>
<div class="flex items-center gap-2 flex-wrap">
<button
v-for="value in quickAmounts"
:key="value"
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="selectQuickAmount(value)"
>
{{ formatPrice(value) }} تومان
</button>
</div>
</form>
</UiCard>
<WithdrawRequestCard :bank-cards="bankCards" />
<UiCard title="درخواست‌های برداشت" variant="default">
<TableGeneric
class="bg-white !rounded-2xl"
:data="withdrawRequests"
customStyle="!text-[11px] !font-normal"
:searchable="false"
:columns="withdrawRequestColumns"
:selectable="false"
:pagination="null"
:model-value="[]"
>
<template #column-bank_card="{ data }">
<span v-if="data.bank_card" dir="ltr">{{ formatCardNumber(data.bank_card.card_number) }}</span>
<span v-else>-</span>
</template>
<template #column-status="{ data }">
<span class="text-[11px] font-normal rounded-md px-2 py-1" :class="withdrawStatusClasses(data.status)">
{{ data.status }}
</span>
</template>
</TableGeneric>
</UiCard>
<UiCard title="تراکنش‌ها" variant="default">
<TableGeneric
class="bg-white !rounded-2xl"
:data="transactions.data"
customStyle="!text-[11px] !font-normal"
:searchable="false"
:columns="walletTransactionColumns"
:selectable="false"
:pagination="transactions.meta"
:model-value="[]"
>
<template #column-type="{ data }">
<span
class="text-[11px] font-normal rounded-md px-2 py-1"
:class="data.type === 'واریز' ? 'text-[#1F7A4D] bg-[#E6F4EC]' : 'text-[#7A1419] bg-[#F8E9EA]'"
>
{{ data.type }}
</span>
</template>
</TableGeneric>
</UiCard>
</div>
</ClientLayout>
</template>
@@ -1,18 +1,24 @@
<script setup>
import { Link } from '@inertiajs/inertia-vue3'
import ActivityCard from './ActivityCard.vue'
import { formatPrice } from '../../utils/numberUtils'
const props = defineProps({
stats: {
type: Object,
required: true
}
})
</script>
<template>
<section class="grid grid-cols-4 max-lg:grid-cols-2 max-sm:grid-cols-1 gap-4">
<ActivityCard title="کیس در انتظار بررسی"
:count="5" link="/dashboard/doctor/cases" label-link="مشاهده و بررسی" />
:count="stats.pending_cases" link="/dashboard/cases" label-link="مشاهده و بررسی" />
<ActivityCard title="کیس بررسی شده در ماه"
:count="999" link="/dashboard/doctor/cases" label-link="مشاهده گزارشات کیس‌ها" />
<ActivityCard title="تومان درآمد"
:count="formatPrice(50000000)" link="/dashboard/doctor/cases" label-link="مشاهده گزارشات مالی " />
<ActivityCard title="ساعت تا شیت بعدی"
:count="4" link="/dashboard/doctor/cases" label-link="مشاهده شیفت‌ها" />
:count="stats.monthly_reviewed" link="/dashboard/reports" label-link="مشاهده گزارشات کیس‌ها" />
<ActivityCard :title="stats.is_doctor ? 'تومان درآمد' : 'تومان هزینه در ماه'"
:count="formatPrice(stats.monthly_amount)" :link="stats.is_doctor ? '/dashboard/wallet' : '/dashboard/invoices'" label-link="مشاهده گزارشات مالی" />
<ActivityCard v-if="stats.next_shift_hours !== null" title="ساعت تا شیفت بعدی"
:count="stats.next_shift_hours" link="/dashboard/shifts" label-link="مشاهده شیفت‌ها" />
</section>
</template>
@@ -0,0 +1,132 @@
<script setup>
import { computed, ref, watch } from 'vue';
import { Inertia } from '@inertiajs/inertia';
import { usePage } from '@inertiajs/inertia-vue3';
import Dialog from 'primevue/dialog';
import UiButton from '../ui/UiButton.vue';
import UiInput from '../ui/UiInput.vue';
import { useGlobalStore } from '../../store/Global';
const visible = defineModel('visible');
const globalStore = useGlobalStore();
const { props } = usePage();
const form = ref({
fullname: '',
birthdate: '',
national_code: '',
email: '',
province: '',
city: '',
address: '',
});
const avatarFile = ref(null);
const avatarPreview = ref('');
const avatarInput = ref(null);
const errors = ref({});
const submitting = ref(false);
const avatarUrl = computed(() => avatarPreview.value || globalStore.current_user.profile_url || '');
watch(visible, (isOpen) => {
if (!isOpen) return;
const user = props.value.auth?.User || {};
form.value = {
fullname: user.fullname || '',
birthdate: user.birthdate || '',
national_code: user.national_code || '',
email: user.email || '',
province: user.province || '',
city: user.city || '',
address: user.address || '',
};
avatarFile.value = null;
avatarPreview.value = '';
errors.value = {};
});
const errorFor = (field) => {
const error = errors.value[field];
return Array.isArray(error) ? error[0] : (error || '');
};
const openAvatarPicker = () => {
avatarInput.value?.click();
};
const handleAvatarChange = (event) => {
const file = event.target.files?.[0];
if (!file) return;
avatarFile.value = file;
avatarPreview.value = URL.createObjectURL(file);
};
const submit = () => {
submitting.value = true;
Inertia.post('/dashboard/profile', {
...form.value,
avatar: avatarFile.value,
}, {
forceFormData: true,
preserveScroll: true,
onSuccess: () => {
globalStore.current_user = props.value.auth.User;
visible.value = false;
},
onError: (formErrors) => {
errors.value = formErrors;
},
onFinish: () => {
submitting.value = false;
},
});
};
</script>
<template>
<Dialog v-model:visible="visible" :modal="true" :closable="true" :dismissableMask="false" :maximizable="false" :style="{ width: '688px' , minHeight: '376px' }" 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>
<form class="w-full flex flex-col gap-5 border-t border-[#EBEBEB] pt-5" @submit.prevent="submit">
<div class="flex items-center gap-4">
<div class="relative">
<span class="flex items-center justify-center w-20 h-20 rounded-full bg-[#F6F7F9] border border-[#DEE3EB] overflow-hidden">
<img v-if="avatarUrl" :src="avatarUrl" alt="تصویر پروفایل" class="w-full h-full object-cover" />
<i v-else class="fi fi-rr-user flex text-2xl text-[#576989]"></i>
</span>
<button type="button" class="absolute -bottom-1 -left-1 flex items-center justify-center w-7 h-7 rounded-full bg-dominant text-white border-2 border-white cursor-pointer" @click="openAvatarPicker">
<i class="fi fi-rr-camera flex text-[11px]"></i>
</button>
<input ref="avatarInput" type="file" accept="image/png,image/jpeg" class="hidden" @change="handleAvatarChange" />
</div>
<div class="flex flex-col gap-1">
<span class="text-[13px] font-[700] text-[#3E4C63]">تصویر پروفایل</span>
<span class="text-[11px] font-[400] text-[#576989]">فرمت‌های مجاز: png ،jpg — حداکثر ۱۰ مگابایت</span>
<span v-if="errorFor('avatar')" class="text-[11px] text-[#7A1419]">{{ errorFor('avatar') }}</span>
</div>
</div>
<div class="grid grid-cols-2 max-sm:grid-cols-1 gap-4">
<UiInput v-model="form.fullname" label="نام و نام خانوادگی" placeholder="نام و نام خانوادگی خود را وارد کنید" :error="errorFor('fullname')" />
<div class="flex flex-col gap-1">
<label class="ui-input-label">تاریخ تولد</label>
<CustomDatePicker v-model="form.birthdate" type="date" format="YYYY-MM-DD" display-format="jYYYY/jMM/jDD" class="w-full" />
<span v-if="errorFor('birthdate')" class="text-[11px] text-[#7A1419]">{{ errorFor('birthdate') }}</span>
</div>
<UiInput v-model="form.national_code" label="کد ملی" placeholder="کد ملی خود را وارد کنید" convertPersianNumbers :error="errorFor('national_code')" />
<UiInput v-model="form.email" label="ایمیل" type="email" placeholder="ایمیل خود را وارد کنید" direction="ltr" :error="errorFor('email')" />
<UiInput v-model="form.province" label="استان" placeholder="استان محل سکونت" :error="errorFor('province')" />
<UiInput v-model="form.city" label="شهر" placeholder="شهر محل سکونت" :error="errorFor('city')" />
</div>
<UiInput v-model="form.address" label="آدرس" placeholder="آدرس خود را وارد کنید" :error="errorFor('address')" />
<div class="w-full pt-3 flex items-center justify-start gap-2 border-t border-[#EBEBEB]">
<UiButton label="ذخیره تغییرات" severity="success" size="small" :loading="submitting" :disabled="submitting" @click="submit" />
<UiButton label="انصراف" severity="secondary" size="small" :disabled="submitting" @click="visible = false" />
</div>
</form>
</Dialog>
</template>
@@ -1,6 +1,6 @@
<script setup>
import UiButton from '../../components/ui/UiButton.vue';
import { computed, watch } from 'vue';
import { computed, ref, watch } from 'vue';
import Dialog from 'primevue/dialog';
import ProgressBar from 'primevue/progressbar';
import { toast } from 'vue3-toastify';
@@ -9,11 +9,15 @@ import RequestCase from './fastRequest/RequestCase.vue';
import RequestDetails from './fastRequest/RequestDetails.vue';
import RequestDoctor from './fastRequest/RequestDoctor.vue';
import RequestReview from './fastRequest/RequestReview.vue';
import InvoiceModal from './fastRequest/InvoiceModal.vue';
import { useFastRequestStore } from '../../store/FastRequest';
const visible = defineModel('visible');
const fastRequestStore = useFastRequestStore();
const invoiceVisible = ref(false);
const invoiceDocument = ref(null);
const steps = [
{ label: 'نوع درخواست', component: RequestType },
{ label: 'انتخاب کیس', component: RequestCase },
@@ -49,8 +53,9 @@ const handleNext = async () => {
try {
const document = await fastRequestStore.submit();
toast.success(`درخواست شما با شماره پرونده ${document.code} با موفقیت ثبت شد.`);
invoiceDocument.value = document;
visible.value = false;
invoiceVisible.value = true;
} catch (error) {
toast.error(error.response?.data?.message || 'خطا در ثبت درخواست. لطفا دوباره تلاش کنید.');
}
@@ -93,4 +98,5 @@ const handleNext = async () => {
/>
</div>
</Dialog>
<invoice-modal v-model:visible="invoiceVisible" :document="invoiceDocument"></invoice-modal>
</template>
@@ -0,0 +1,46 @@
<script setup>
import { computed } from 'vue';
import Dialog from 'primevue/dialog';
import UiButton from '../ui/UiButton.vue';
import PaymentSuccessSvg from '../svg/paymentSuccess.svg';
import PaymentFailedSvg from '../svg/paymentFailed.svg';
const visible = defineModel('visible');
const props = defineProps({
result: {
type: Object,
default: null
}
});
const isPaid = computed(() => props.result?.status === 'paid');
const isWalletTopUp = computed(() => props.result?.purpose === 'WalletTopUp');
const closeModal = () => {
visible.value = false;
}
</script>
<template>
<Dialog v-model:visible="visible" :modal="true" :closable="true" :dismissableMask="false" :maximizable="false" :style="{ width: '688px' , minHeight: '376px' }" class="fast-request-dialog">
<div v-if="result" class="w-full flex flex-col items-center justify-center gap-2 py-6">
<component :is="isPaid ? PaymentSuccessSvg : PaymentFailedSvg" class="w-[180px] h-[150px]" />
<h2 class="text-[15px] font-[700] text-[#3E4C63] mt-2">
<template v-if="isPaid">{{ isWalletTopUp ? 'کیف پول شما با موفقیت شارژ شد.' : 'پرداخت شما با موفقیت انجام شد.' }}</template>
<template v-else>متاسفانه پرداخت شما با خطا روبه‌رو شده است.</template>
</h2>
<p class="max-w-[480px] text-center text-[13px] font-[400] text-[#576989] leading-7">
<template v-if="isPaid && isWalletTopUp">مبلغ پرداختی به موجودی کیف پول شما اضافه شد و می‌توانید از آن برای پرداخت فاکتورها استفاده کنید.</template>
<template v-else-if="isPaid">کارشناسان پرداخت و درخواست شما را به زودی بررسی خواهند کرد.</template>
<template v-else>لطفا دوباره نسبت به پرداخت اقدام نمایید. در صورتیکه مبلغ از حساب شما کسر شده و یا فکر می‌کنید اشتباهی رخ داده است، با کارشناسان ما در تماس باشید.</template>
</p>
<p class="text-[13px] font-[700] text-[#3E4C63] mt-2">شماره پیگیری: {{ result.track_id }}</p>
<div class="flex items-center justify-center gap-2 mt-4">
<ui-button label="بازگشت به داشبورد" severity="success" size="small" @click="closeModal" />
<ui-button v-if="!isPaid" label="تماس با پشتیبانی" severity="secondary" size="small" @click="closeModal" />
</div>
</div>
</Dialog>
</template>
@@ -0,0 +1,27 @@
<script setup>
defineProps({
title: {
type: String,
required: true
},
rows: {
type: Array,
required: true
}
})
</script>
<template>
<section class="bg-white border border-complement-50 rounded-2xl p-4">
<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>
{{ title }}
</h2>
<ul class="mt-3 flex flex-col gap-2">
<li v-for="row in rows" :key="row.label" class="flex items-start gap-2 text-[11px]">
<span class="font-normal text-[#576989]">{{ row.label }}:</span>
<span class="font-semibold text-[#3E4C63]">{{ row.value ?? '-' }}</span>
</li>
</ul>
</section>
</template>
@@ -0,0 +1,42 @@
<script setup>
const value = defineModel({ type: String, default: '' })
defineProps({
title: {
type: String,
required: true
},
editable: {
type: Boolean,
default: false
},
placeholder: {
type: String,
default: ''
},
error: {
type: String,
default: null
}
})
</script>
<template>
<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>
{{ title }}
</h2>
<textarea
v-if="editable"
v-model="value"
rows="6"
:placeholder="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 v-else class="w-full min-h-[7rem] bg-[#F8FAFC] border border-[#DEE3EB] rounded-xl p-4 text-[12px] font-normal text-[#3E4C63] leading-6 whitespace-pre-line">
{{ value || 'موردی ثبت نشده است.' }}
</div>
<p v-if="error" class="text-[11px] text-[#7A1419]">{{ error }}</p>
</section>
</template>
@@ -1,8 +1,12 @@
<script setup>
import { computed } from 'vue';
import { computed, ref } from 'vue';
import Dialog from 'primevue/dialog';
import { usePage } from '@inertiajs/inertia-vue3';
import { Inertia } from '@inertiajs/inertia';
import UiButton from '../../ui/UiButton.vue';
import { toast } from 'vue3-toastify';
import { modalityTitle } from '../../../data/fastRequestOptions';
import { formatPrice } from '../../../utils/numberUtils';
const visible = defineModel('visible');
@@ -10,31 +14,69 @@ const props = defineProps({
document: {
type: Object,
default: null
},
invoice: {
type: Object,
default: null
}
});
const formattedAmount = computed(() => {
return Number(props.document?.invoice?.amount || 0).toLocaleString('fa-IR');
});
const { props: pageProps } = usePage();
const handlePay = () => {
visible.value = false;
const paying = ref(false);
const payingWithWallet = ref(false);
const currentInvoice = computed(() => props.invoice ?? props.document?.invoice ?? null);
const formattedAmount = computed(() => formatPrice(currentInvoice.value?.amount || 0));
const walletBalance = computed(() => Number(pageProps.value.auth?.wallet_balance || 0));
const formattedWalletBalance = computed(() => formatPrice(walletBalance.value));
const hasEnoughBalance = computed(() => walletBalance.value >= Number(currentInvoice.value?.amount || 0));
const handlePay = async () => {
paying.value = true;
try {
const { data } = await window.axios.post('/dashboard/payments', { invoice_id: currentInvoice.value.id });
window.location.href = data.url;
} catch (error) {
paying.value = false;
toast.error(error.response?.data?.message || 'خطا در اتصال به درگاه پرداخت. لطفا دوباره تلاش کنید.');
}
}
const handleWalletPay = async () => {
payingWithWallet.value = true;
try {
await window.axios.post(`/dashboard/invoices/${currentInvoice.value.id}/pay-wallet`);
toast.success('فاکتور با موفقیت از کیف پول پرداخت شد.');
visible.value = false;
Inertia.reload();
} catch (error) {
toast.error(error.response?.data?.message || 'خطا در پرداخت از کیف پول. لطفا دوباره تلاش کنید.');
} finally {
payingWithWallet.value = false;
}
}
</script>
<template>
<Dialog v-model:visible="visible" :modal="true" :closable="true" :dismissableMask="false" :maximizable="false" :style="{ width: '1032px' }">
<Dialog v-model:visible="visible" :modal="true" :closable="true" :dismissableMask="false" :maximizable="false" :style="{ width: '688px' , minHeight: '376px' }" class="fast-request-dialog" :pt="{header : '!bg-client-primaryBlue'}">
<template #header>
<h2 class="text-[14px] font-[700] text-[#576989]">فاکتور پرداخت «{{ document?.invoice?.number }}»</h2>
<div class="w-full flex items-center justify-start">
<h2 class="text-[14px] font-[700] text-[#576989]">فاکتور پرداخت «{{ currentInvoice?.number }}»</h2>
</div>
</template>
<div v-if="document" class="w-full flex flex-col gap-4 border-t border-[#EBEBEB] pt-4">
<div class="w-full flex items-center gap-3 bg-[#EEF2F8] border border-[#DEE3EB] rounded-[12px] px-4 py-4">
<i class="fi-rr-info text-[20px] text-[#576989]"></i>
<p class="text-[13px] font-[400] text-[#3E4C63]">
<div v-if="currentInvoice" class="w-full flex flex-col gap-4 border-t border-[#EBEBEB] pt-4">
<div class="w-full flex items-center gap-3 bg-[#EEF2F8] border border-[#DEE3EB] rounded-[12px] px-4 py-3">
<i class="fi-rr-info text-[18px] text-[#576989]"></i>
<p class="text-[12px] font-[400] text-[#3E4C63] leading-6">
یک فاکتور به مبلغ « <span class="font-[700]">{{ formattedAmount }} تومان</span> » برای شما ایجاد شد. لطفا جهت ادامه مراحل درخواست نسبت به پرداخت اقدام نمایید.
</p>
</div>
<p class="text-[13px] font-[400] text-[#3E4C63] leading-7">
<p v-if="document" class="text-[13px] font-[400] text-[#3E4C63] leading-7">
این فاکتور جهت پردازش درخواست
<span class="font-[600] text-[#7A1419]">(({{ modalityTitle(document.modality) }}))</span>
به شناسه
@@ -48,9 +90,14 @@ const handlePay = () => {
<br>
بدیهیست که در صورت عدم تایید کارشناسان مبلغ مربوطه به کیف پول شما بازگردانده خواهد شد.
</p>
<p class="text-[12px] font-[400] text-[#576989]">
موجودی کیف پول شما: <span class="font-[700] text-[#3E4C63]">{{ formattedWalletBalance }} تومان</span>
<span v-if="!hasEnoughBalance" class="text-[#7A1419]"> (موجودی کیف پول کافی نیست)</span>
</p>
</div>
<div class="w-full pt-3 flex items-center justify-start border-t border-[#EBEBEB]">
<ui-button label="تایید و پرداخت" severity="success" size="small" @click="handlePay" />
<div class="w-full pt-3 flex items-center justify-start gap-2 border-t border-[#EBEBEB]">
<ui-button label="پرداخت با کیف پول" severity="primary" size="small" :loading="payingWithWallet" :disabled="payingWithWallet || paying || !hasEnoughBalance" @click="handleWalletPay" />
<ui-button label="پرداخت از طریق درگاه" severity="success" size="small" :loading="paying" :disabled="paying || payingWithWallet" @click="handlePay" />
</div>
</Dialog>
</template>
@@ -0,0 +1,193 @@
<script setup>
import { computed, ref } from 'vue';
import { useForm, usePage } from '@inertiajs/inertia-vue3';
import { Inertia } from '@inertiajs/inertia';
import Dialog from 'primevue/dialog';
import { toast } from 'vue3-toastify';
import UiCard from '../../ui/UiCard.vue';
import UiInput from '../../ui/UiInput.vue';
import UiButton from '../../ui/UiButton.vue';
import { convertEnglishToPersian, convertPersianToEnglish, formatPrice } from '../../../utils/numberUtils';
const props = defineProps({
bankCards: {
type: Array,
required: true
}
});
const { props: pageProps } = usePage();
const walletBalance = computed(() => Number(pageProps.value.auth?.wallet_balance || 0));
const amount = ref('');
const selectedCardId = ref(props.bankCards[0]?.id ?? null);
const numericAmount = computed(() => Number(convertPersianToEnglish(amount.value).replace(/\D/g, '')) || 0);
const formatCardNumber = (cardNumber) => convertEnglishToPersian(cardNumber.replace(/(\d{4})(?=\d)/g, '$1 - '));
const selectFullBalance = () => {
amount.value = String(walletBalance.value);
};
const withdrawForm = useForm({
amount: null,
bank_card_id: null,
});
const submitWithdraw = () => {
if (numericAmount.value < 10000) {
toast.error('حداقل مبلغ برداشت ۱۰٬۰۰۰ تومان است.');
return;
}
if (!selectedCardId.value) {
toast.error('لطفا کارت بانکی مقصد را انتخاب کنید.');
return;
}
withdrawForm.amount = numericAmount.value;
withdrawForm.bank_card_id = selectedCardId.value;
withdrawForm.post('/dashboard/wallet/withdraw-requests', {
preserveScroll: true,
onSuccess: () => {
amount.value = '';
toast.success('درخواست برداشت شما ثبت شد و در انتظار بررسی است.');
},
onError: (errors) => toast.error(Object.values(errors)[0] || 'خطا در ثبت درخواست برداشت.'),
});
};
const cardModalVisible = ref(false);
const cardForm = useForm({
card_number: '',
holder_name: '',
});
const submitCard = () => {
cardForm.card_number = convertPersianToEnglish(cardForm.card_number).replace(/\D/g, '');
cardForm.post('/dashboard/wallet/bank-cards', {
preserveScroll: true,
onSuccess: () => {
cardForm.reset();
cardModalVisible.value = false;
toast.success('کارت بانکی با موفقیت ثبت شد.');
},
onError: (errors) => toast.error(Object.values(errors)[0] || 'خطا در ثبت کارت بانکی.'),
});
};
const deleteCard = (card) => {
Inertia.delete(`/dashboard/wallet/bank-cards/${card.id}`, {
preserveScroll: true,
onSuccess: () => {
if (selectedCardId.value === card.id) {
selectedCardId.value = null;
}
},
});
};
</script>
<template>
<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-4">
<p class="text-[11px] font-normal text-[#576989] leading-6">
مبلغ مورد نظر پس از بررسی و تایید توسط کارشناسان، از کیف پول شما کسر و به کارت انتخابی واریز می‌شود.
</p>
<div class="flex items-end gap-3 max-sm:flex-col max-sm:items-stretch">
<UiInput
v-model="amount"
label="مبلغ برداشت"
placeholder="مبلغ مورد نظر را به تومان وارد کنید"
prefixEnd="تومان"
prefixClasses="!border-dominant-50"
class="!w-full"
customClass="!border-dominant-50 !font-normal !text-[12px]"
/>
</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 class="flex flex-col gap-3">
<div class="flex items-center justify-between">
<span class="text-[11px] font-semibold text-[#3E4C63]">کارت بانکی مقصد</span>
<button type="button" class="text-[11px] font-semibold text-[#365891]" @click="cardModalVisible = true">
+ افزودن کارت
</button>
</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">
هنوز کارتی ثبت نکرده‌اید. برای ثبت درخواست برداشت ابتدا یک کارت بانکی اضافه کنید.
</p>
<label
v-for="card in bankCards"
:key="card.id"
class="flex items-center gap-3 border rounded-xl px-4 py-3 cursor-pointer transition-colors"
:class="selectedCardId === card.id ? 'border-[#365891] bg-[#EEF2F8]' : 'border-[#DEE3EB] bg-white hover:bg-[#F8FAFC]'"
>
<input
v-model="selectedCardId"
type="radio"
name="bank_card"
:value="card.id"
class="accent-[#365891]"
/>
<span class="flex flex-col gap-1 flex-1">
<span class="text-[12px] font-semibold text-[#3E4C63]" dir="ltr">{{ formatCardNumber(card.card_number) }}</span>
<span class="text-[11px] font-normal text-[#576989]">{{ card.holder_name }}</span>
</span>
<button type="button" class="text-[11px] font-semibold text-[#7A1419]" @click.prevent="deleteCard(card)">حذف</button>
</label>
</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' }">
<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-4 border-t border-[#EBEBEB] pt-4">
<UiInput
v-model="cardForm.card_number"
label="شماره کارت"
placeholder="شماره کارت ۱۶ رقمی"
maxlength="16"
customClass="!border-dominant-50 !font-normal !text-[12px]"
/>
<UiInput
v-model="cardForm.holder_name"
label="نام صاحب حساب"
placeholder="نام و نام خانوادگی صاحب کارت"
customClass="!border-dominant-50 !font-normal !text-[12px]"
/>
</div>
<div class="w-full pt-4 flex items-center justify-start gap-2 border-t border-[#EBEBEB] mt-4">
<UiButton label="ثبت کارت" severity="success" size="small" :loading="cardForm.processing" :disabled="cardForm.processing" @click="submitCard" />
<UiButton label="انصراف" severity="secondary" size="small" :disabled="cardForm.processing" @click="cardModalVisible = false" />
</div>
</Dialog>
</UiCard>
</template>
@@ -1,14 +1,47 @@
<script setup>
import { ref } from 'vue';
import { computed, onBeforeUnmount, onMounted, ref } from 'vue';
import { Link, usePage } from '@inertiajs/inertia-vue3';
import { useGlobalStore } from '../../store/Global';
import UiInput from '../ui/UiInput.vue';
import ArrowDownSvg from '../svg/arrowDown.svg';
import GlobalSvg from '../svg/global.svg';
import EditProfileModal from '../dashboard/EditProfileModal.vue';
import { formatPrice } from '../../utils/numberUtils';
const globalStore = useGlobalStore();
const { props } = usePage();
const walletBalance = computed(() => formatPrice(props.value.auth?.wallet_balance || 0));
const toggleSidebar = () => {
globalStore.sidebarOpen = !globalStore.sidebarOpen;
};
const userMenuOpen = ref(false);
const userMenuRef = ref(null);
const editProfileVisible = ref(false);
const toggleUserMenu = () => {
userMenuOpen.value = !userMenuOpen.value;
};
const openEditProfile = () => {
userMenuOpen.value = false;
editProfileVisible.value = true;
};
const closeOnClickOutside = (event) => {
if (userMenuRef.value && !userMenuRef.value.contains(event.target)) {
userMenuOpen.value = false;
}
};
onMounted(() => document.addEventListener('click', closeOnClickOutside));
onBeforeUnmount(() => document.removeEventListener('click', closeOnClickOutside));
const menuItems = [
{ label: 'کیف پول', icon: 'fi-rr-wallet', href: '/dashboard/wallet' },
{ label: 'فاکتورها', icon: 'fi-rr-receipt', href: '/dashboard/invoices' },
{ label: 'برنامه شیفت‌ها', icon: 'fi-rr-calendar-clock', href: '/dashboard/shifts' },
];
</script>
<template>
@@ -39,17 +72,63 @@ const toggleSidebar = () => {
/>
</div>
</div>
<div class="flex gap-4">
<div class="flex items-center gap-4">
<Link href="/dashboard/wallet" class="flex items-center gap-2.5 rounded-lg bg-dominant-light border border-dominant-50 ps-1.5 pe-3 py-1.5 transition-colors hover:border-dominant-100">
<span class="flex items-center justify-center w-8 h-8 rounded-lg bg-white border border-dominant-50 text-dominant-400">
<i class="fi fi-rr-wallet flex text-[15px]"></i>
</span>
<span class="flex flex-col max-sm:hidden">
<span class="text-[10px] font-normal text-dominant-200">موجودی کیف پول</span>
<span class="text-[12px] font-bold text-dominant-500">{{ walletBalance }} <span class="text-[10px] font-normal text-dominant-300">تومان</span></span>
</span>
</Link>
<div class="flex items-center justify-center border-l-[1px] !border-dominant-50 pl-4">
<GlobalSvg />
</div>
<div class="grid grid-cols-2a justify-start items-center gap-2">
<span class="text-dominant-300 text-[12px] font-normal">
{{ globalStore.current_user.fullname }}
</span>
<ArrowDownSvg class="!w-4 !h-4 flex -mt-0.5"/>
<div ref="userMenuRef" class="relative">
<button type="button" class="flex items-center gap-2 cursor-pointer" @click="toggleUserMenu">
<span class="flex items-center justify-center w-9 h-9 rounded-full bg-dominant-light border border-dominant-50 overflow-hidden">
<img v-if="globalStore.current_user.profile_url" :src="globalStore.current_user.profile_url" alt="تصویر پروفایل" class="w-full h-full object-cover" />
<i v-else class="fi fi-rr-user flex text-sm text-dominant-300"></i>
</span>
<span class="text-dominant-300 text-[12px] font-normal max-sm:hidden">
{{ globalStore.current_user.fullname }}
</span>
<i class="fi fi-rr-angle-small-down flex text-sm text-dominant-300 transition-transform duration-200" :class="{ 'rotate-180': userMenuOpen }"></i>
</button>
<Transition
enter-active-class="transition duration-150 ease-out"
enter-from-class="opacity-0 -translate-y-1"
enter-to-class="opacity-100 translate-y-0"
leave-active-class="transition duration-100 ease-in"
leave-from-class="opacity-100 translate-y-0"
leave-to-class="opacity-0 -translate-y-1"
>
<nav v-if="userMenuOpen" class="absolute left-0 top-full mt-2 w-52 rounded-xl bg-white border border-complement-100 shadow-all py-2 z-30">
<ul class="flex flex-col">
<li>
<button type="button" class="w-full flex items-center gap-2.5 px-4 py-2.5 text-[12px] font-normal text-complement-900 cursor-pointer transition-colors hover:bg-complement-50" @click="openEditProfile">
<i class="fi fi-rr-user-pen flex text-[14px] text-complement-700"></i>
ویرایش حساب کاربری
</button>
</li>
<li v-for="item in menuItems" :key="item.href">
<Link :href="item.href" class="flex items-center gap-2.5 px-4 py-2.5 text-[12px] font-normal text-complement-900 transition-colors hover:bg-complement-50" @click="userMenuOpen = false">
<i class="fi flex text-[14px] text-complement-700" :class="item.icon"></i>
{{ item.label }}
</Link>
</li>
<li class="border-t border-complement-100 mt-1 pt-1">
<Link href="/dashboard/logout" class="flex items-center gap-2.5 px-4 py-2.5 text-[12px] font-normal text-dominant-400 transition-colors hover:bg-dominant-light">
<i class="fi fi-rr-sign-out-alt flex text-[14px]"></i>
خروج از حساب
</Link>
</li>
</ul>
</nav>
</Transition>
</div>
</div>
<EditProfileModal v-model:visible="editProfileVisible" />
</header>
</template>
@@ -10,6 +10,8 @@ import LogoutSvg from "../svg/logout.svg"
import CalendarSvg from "../svg/calendar.svg"
import StatusUpSvg from "../svg/statusUp.svg"
import PetSvg from "../svg/pet.svg"
import WalletSvg from "../svg/wallet.svg"
import InvoiceSvg from "../svg/invoice.svg"
interface SidebarItem extends MenuItem {
label: string;
@@ -87,6 +89,20 @@ const items: Ref<SidebarItem[]> = ref([
key: 'reports',
permission: null,
},
{
label: "کیف پول",
icon: WalletSvg,
to: "/dashboard/wallet",
key: 'wallet',
permission: null,
},
{
label: "فاکتورها",
icon: InvoiceSvg,
to: "/dashboard/invoices",
key: 'invoices',
permission: null,
},
{
label: "شیفت ها",
icon: CalendarSvg,
@@ -0,0 +1,6 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M16.5 5.25V12.75C16.5 15 15.375 16.5 12.75 16.5H5.25C2.625 16.5 1.5 15 1.5 12.75V5.25C1.5 3 2.625 1.5 5.25 1.5H12.75C15.375 1.5 16.5 3 16.5 5.25Z" stroke="#965457" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M5.4375 5.8125H12.5625" stroke="#965457" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M5.4375 9H12.5625" stroke="#965457" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M5.4375 12.1875H9.75" stroke="#965457" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 691 B

@@ -0,0 +1,9 @@
<svg width="220" height="180" viewBox="0 0 220 180" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="46" cy="34" r="4" fill="#F5A9AB"/>
<circle cx="178" cy="28" r="3" fill="#F5A9AB"/>
<circle cx="188" cy="120" r="4" fill="#F5A9AB"/>
<circle cx="34" cy="128" r="3" fill="#F5A9AB"/>
<circle cx="110" cy="164" r="3" fill="#F5A9AB"/>
<circle cx="110" cy="90" r="52" fill="#E5484D"/>
<path d="M92 72L128 108M128 72L92 108" stroke="white" stroke-width="9" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 493 B

@@ -0,0 +1,19 @@
<svg width="220" height="180" viewBox="0 0 220 180" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="38" y="24" width="4" height="14" rx="2" transform="rotate(-24 38 24)" fill="#3E6D9C"/>
<rect x="176" y="18" width="4" height="12" rx="2" transform="rotate(32 176 18)" fill="#E5484D"/>
<rect x="24" y="96" width="4" height="12" rx="2" transform="rotate(58 24 96)" fill="#E5484D"/>
<rect x="188" y="92" width="4" height="14" rx="2" transform="rotate(-40 188 92)" fill="#3E6D9C"/>
<rect x="66" y="150" width="4" height="12" rx="2" transform="rotate(20 66 150)" fill="#8BC34A"/>
<rect x="150" y="152" width="4" height="12" rx="2" transform="rotate(-28 150 152)" fill="#E5484D"/>
<circle cx="30" cy="58" r="3" fill="#8BC34A"/>
<circle cx="58" cy="12" r="3" fill="#E5484D"/>
<circle cx="118" cy="8" r="3" fill="#64B5F6"/>
<circle cx="196" cy="52" r="3" fill="#8BC34A"/>
<circle cx="200" cy="136" r="3" fill="#64B5F6"/>
<circle cx="96" cy="168" r="3" fill="#E5484D"/>
<circle cx="20" cy="140" r="3" fill="#64B5F6"/>
<circle cx="164" cy="120" r="2.5" fill="#E5484D"/>
<circle cx="52" cy="118" r="2.5" fill="#8BC34A"/>
<circle cx="110" cy="90" r="52" fill="#5384AD"/>
<path d="M88 91L103 106L133 76" stroke="white" stroke-width="9" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

@@ -0,0 +1,8 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.75 9.9075H10.995C10.29 9.9075 9.7125 9.33 9.7125 8.625C9.7125 7.92 10.29 7.3425 10.995 7.3425H12.75" stroke="#965457" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M11.235 8.61H11.145" stroke="#965457" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6.1875 2.25H10.6875C12.5325 2.25 12.9375 2.7 12.9375 4.005V7.3425" stroke="#965457" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M4.65 11.055C4.65 11.4675 4.3125 11.805 3.9 11.805C3.4875 11.805 3.15 11.4675 3.15 11.055C3.15 10.6425 3.4875 10.305 3.9 10.305C4.3125 10.305 4.65 10.6425 4.65 11.055Z" fill="#965457"/>
<path d="M1.5 8.8125V12.375C1.5 14.445 3.18 16.125 5.25 16.125H12.75C14.82 16.125 16.5 14.445 16.5 12.375V6.375C16.5 4.305 14.82 2.625 12.75 2.625" stroke="#965457" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M1.5 8.8125V5.25C1.5 4.0575 2.235 2.9925 3.3525 2.5725" stroke="#965457" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

@@ -7,50 +7,39 @@ const props = defineProps([
'perPage',
'links',
])
const pageItemClass = 'flex items-center justify-center w-8 h-8 rounded-lg border text-[11px] font-semibold transition-colors';
</script>
<template>
<section class="pagination" v-if="lastPage > 1">
<nav class="flex items-center gap-1.5 p-4" v-if="lastPage > 1">
<template v-for="(link , index) in links" :key="index">
<div class="paginate" v-if="link.label == 'قبلی'">
<Link v-if="link.url" :href="link.url">
<i class="fi fi-rr-angle-small-right flex text-xl"></i>
<template v-if="link.label == 'قبلی'">
<Link v-if="link.url" :href="link.url" :class="pageItemClass" class="border-complement-100 text-complement-700 hover:bg-complement-50">
<i class="fi fi-rr-angle-small-right flex text-base"></i>
</Link>
</div>
<div class="paginate" v-else-if="link.label == 'بعدی'">
<Link v-if="link.url" :href="link.url">
<i class="fi fi-rr-angle-small-left flex text-xl"></i>
<span v-else :class="pageItemClass" class="border-complement-100 text-complement-200">
<i class="fi fi-rr-angle-small-right flex text-base"></i>
</span>
</template>
<template v-else-if="link.label == 'بعدی'">
<Link v-if="link.url" :href="link.url" :class="pageItemClass" class="border-complement-100 text-complement-700 hover:bg-complement-50">
<i class="fi fi-rr-angle-small-left flex text-base"></i>
</Link>
</div>
<div class="paginate flex items-center" :class="{'!text-white !bg-client-primary' : currentPage == link.label }" v-else>
<Link v-if="link.url" :href="link.url" class="flex pt-1">{{link.label}}</Link>
<Link v-else href="" class="flex">{{link.label}}</Link>
</div>
<span v-else :class="pageItemClass" class="border-complement-100 text-complement-200">
<i class="fi fi-rr-angle-small-left flex text-base"></i>
</span>
</template>
<template v-else>
<Link
v-if="link.url"
:href="link.url"
:class="[pageItemClass, currentPage == link.label ? 'bg-dominant border-dominant text-white' : 'border-complement-100 text-complement-900 hover:bg-complement-50']"
>
{{ link.label }}
</Link>
<span v-else :class="pageItemClass" class="border-transparent text-complement-700">{{ link.label }}</span>
</template>
</template>
</section>
</nav>
</template>
<style lang="postcss">
section.pagination{
padding: 1rem;
display: flex;
align-items: center;
}
section.pagination div.paginate:first-child a{
border-top-right-radius: .4rem;
border-bottom-right-radius: .4rem;
}
section.pagination div.paginate:last-child a{
border-top-left-radius: .4rem;
border-bottom-left-radius: .4rem;
}
section.pagination div.paginate a{
display: flex;
height: 2.2rem;
width: 2.2rem;
align-items: center;
justify-content: center;
border: 1px solid #e6e6e6;
@apply text-client-Fontcolor
}
</style>
@@ -133,6 +133,12 @@ const buttonClasses = computed(() => {
if (props.severity === 'secondary') {
classes.push('!font-[600] !text-[11px] !rounded-[6px]')
}
if (props.severity === 'success') {
classes.push('ui-button--success !font-[600] !text-[11px] !rounded-[6px]')
}
if (props.severity === 'danger') {
classes.push('!font-[600] !text-[11px] !rounded-[6px]')
}
return classes.join(' ')
})
@@ -0,0 +1,46 @@
import { modalityTitle } from './fastRequestOptions'
import { convertEnglishToPersian, formatPrice } from '../utils/numberUtils'
export const invoiceColumns = [
{
title: 'شماره فاکتور',
key: 'number',
render: (row) => convertEnglishToPersian(row.number)
},
{
title: 'شماره پرونده',
key: 'document',
render: (row) => row.document?.code ?? '-'
},
{
title: 'نوع درخواست',
key: 'modality',
render: (row) => row.document ? modalityTitle(row.document.modality) : '-'
},
{
title: 'مبلغ',
key: 'amount',
render: (row) => `${formatPrice(row.amount)} تومان`
},
{
title: 'وضعیت',
key: 'status'
},
{
title: 'روش پرداخت',
key: 'payment_method',
render: (row) => row.payment_method ?? '-'
},
{
title: 'تاریخ ثبت',
key: 'created_at',
render: (row) => row.created_at ? convertEnglishToPersian(new Date(row.created_at).toLocaleDateString('fa-IR')) : '-'
},
{
title: '',
key: 'actions',
width: 100,
style: 'grid grid-cols-1',
slot: true,
}
]
@@ -0,0 +1,36 @@
import { modalityTitle } from './fastRequestOptions'
import { convertEnglishToPersian } from '../utils/numberUtils'
export const reportColumns = [
{
title: 'شماره پرونده',
key: 'code',
render: (row) => row.document?.code || '-'
},
{
title: 'نوع درخواست',
key: 'modality',
render: (row) => row.document ? modalityTitle(row.document.modality) : '-'
},
{
title: 'وضعیت گزارش',
key: 'status'
},
{
title: 'پزشک',
key: 'doctor',
render: (row) => row.doctor || '-'
},
{
title: 'تاریخ ثبت گزارش',
key: 'submitted_at',
render: (row) => row.submitted_at ? convertEnglishToPersian(row.submitted_at) : '-'
},
{
title: '',
key: 'actions',
width: 80,
style: 'grid grid-cols-1',
slot: true,
}
]
@@ -0,0 +1,38 @@
import { convertEnglishToPersian, formatPrice } from '../utils/numberUtils'
export const walletTransactionColumns = [
{
title: 'نوع',
key: 'type',
slot: true,
},
{
title: 'بابت',
key: 'reason'
},
{
title: 'مبلغ',
key: 'amount',
render: (row) => `${formatPrice(row.amount)} تومان`
},
{
title: 'روش',
key: 'via_gateway',
render: (row) => row.via_gateway ? 'درگاه پرداخت' : 'کیف پول'
},
{
title: 'شماره فاکتور',
key: 'invoice_number',
render: (row) => row.invoice_number ? convertEnglishToPersian(row.invoice_number) : '-'
},
{
title: 'موجودی پس از تراکنش',
key: 'balance_after',
render: (row) => `${formatPrice(row.balance_after)} تومان`
},
{
title: 'تاریخ',
key: 'created_at',
render: (row) => row.created_at ? convertEnglishToPersian(row.created_at) : '-'
}
]
@@ -0,0 +1,34 @@
import { convertEnglishToPersian, formatPrice } from '../utils/numberUtils'
export const withdrawRequestColumns = [
{
title: 'مبلغ',
key: 'amount',
render: (row) => `${formatPrice(row.amount)} تومان`
},
{
title: 'کارت مقصد',
key: 'bank_card',
slot: true,
},
{
title: 'وضعیت',
key: 'status',
slot: true,
},
{
title: 'تاریخ درخواست',
key: 'created_at',
render: (row) => convertEnglishToPersian(row.created_at)
},
{
title: 'تاریخ بررسی',
key: 'reviewed_at',
render: (row) => row.reviewed_at ? convertEnglishToPersian(row.reviewed_at) : '-'
},
{
title: 'توضیح',
key: 'admin_note',
render: (row) => row.admin_note || '-'
},
]
@@ -35,6 +35,20 @@ export function buildShiftEvent(shift, overrides = {}) {
}
}
const WEEKDAY_NAMES = {
6: 'شنبه',
0: 'یکشنبه',
1: 'دوشنبه',
2: 'سه‌شنبه',
3: 'چهارشنبه',
4: 'پنج‌شنبه',
5: 'جمعه',
}
export function weekdayName(dayIndex) {
return WEEKDAY_NAMES[dayIndex] ?? null
}
export function formatJalaliDay(date) {
return new Intl.DateTimeFormat('fa-IR', { day: 'numeric', month: 'short' }).format(date)
}