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
@@ -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';
}
}
})