feat: add shift management functionality
This commit is contained in:
@@ -1,11 +1,98 @@
|
||||
<script setup>
|
||||
import ClientLayout from '../../../layouts/ClientLayout.vue'
|
||||
import { computed, ref } from 'vue';
|
||||
import Dialog from 'primevue/dialog';
|
||||
import { toast } from 'vue3-toastify';
|
||||
import ClientLayout from '../../../layouts/ClientLayout.vue';
|
||||
import UiCard from '../../../components/ui/UiCard.vue';
|
||||
import UiButton from '../../../components/ui/UiButton.vue';
|
||||
import ShiftCalendar from '../../../components/dashboard/shifts/ShiftCalendar.vue';
|
||||
import { useShiftStore } from '../../../store/Shift';
|
||||
import { convertEnglishToPersian } from '../../../utils/numberUtils';
|
||||
|
||||
const props = defineProps({
|
||||
myShifts: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
openShifts: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
const shiftStore = useShiftStore();
|
||||
|
||||
const selectedShift = ref(null);
|
||||
const confirmVisible = ref(false);
|
||||
|
||||
const isMine = computed(() => {
|
||||
return !!selectedShift.value && props.myShifts.data.some((shift) => shift.id === selectedShift.value.id);
|
||||
});
|
||||
|
||||
const shiftLabel = computed(() => {
|
||||
if (!selectedShift.value) return '';
|
||||
const day = selectedShift.value.date ?? selectedShift.value.day_of_week;
|
||||
const time = `${selectedShift.value.start_time} تا ${selectedShift.value.end_time}`;
|
||||
return convertEnglishToPersian(`${day} - ساعت ${time}`);
|
||||
});
|
||||
|
||||
const openConfirm = (shift) => {
|
||||
selectedShift.value = shift;
|
||||
confirmVisible.value = true;
|
||||
};
|
||||
|
||||
const handleConfirm = async () => {
|
||||
try {
|
||||
if (isMine.value) {
|
||||
await shiftStore.release(selectedShift.value.id);
|
||||
toast.success('شیفت با موفقیت لغو شد.');
|
||||
} else {
|
||||
await shiftStore.claim(selectedShift.value.id);
|
||||
toast.success('شیفت با موفقیت به شما اختصاص یافت.');
|
||||
}
|
||||
confirmVisible.value = false;
|
||||
} catch (error) {
|
||||
toast.error(error.message);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ClientLayout>
|
||||
<h1 class="text-2xl font-bold">
|
||||
Shifts
|
||||
</h1>
|
||||
<UiCard title="برنامه شیفتهای من" variant="default">
|
||||
<ShiftCalendar
|
||||
:my-shifts="myShifts.data"
|
||||
:open-shifts="openShifts.data"
|
||||
@select-shift="openConfirm"
|
||||
/>
|
||||
</UiCard>
|
||||
|
||||
<Dialog v-model:visible="confirmVisible" :modal="true" :closable="true" :dismissableMask="true" :style="{ width: '400px' }" class="fast-request-dialog">
|
||||
<template #header>
|
||||
<div class="w-full flex items-center justify-start">
|
||||
<h2 class="text-[14px] font-[700] text-[#576989]">{{ isMine ? 'لغو شیفت' : 'انتخاب شیفت' }}</h2>
|
||||
</div>
|
||||
</template>
|
||||
<div class="w-full py-4">
|
||||
<p class="text-[12px] font-[600] text-[#3E4C63]">
|
||||
{{ isMine ? 'آیا از لغو این شیفت اطمینان دارید؟' : 'آیا این شیفت به برنامه شما اضافه شود؟' }}
|
||||
</p>
|
||||
<p class="mt-2 text-[11px] font-[600] text-[#7188AD] bg-[#F6F7F9] border border-[#DEE3EB] rounded-lg p-3">
|
||||
{{ shiftLabel }}
|
||||
</p>
|
||||
</div>
|
||||
<template #footer>
|
||||
<div class="w-full flex items-center justify-end gap-2">
|
||||
<UiButton label="انصراف" size="small" outlined @click="confirmVisible = false" />
|
||||
<UiButton
|
||||
:label="isMine ? 'لغو شیفت' : 'انتخاب شیفت'"
|
||||
size="small"
|
||||
:severity="isMine ? 'danger' : 'success'"
|
||||
:loading="shiftStore.processingId === selectedShift?.id"
|
||||
@click="handleConfirm"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</Dialog>
|
||||
</ClientLayout>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import Dialog from 'primevue/dialog';
|
||||
import UiButton from '../../ui/UiButton.vue';
|
||||
import { modalityTitle } from '../../../data/fastRequestOptions';
|
||||
|
||||
const visible = defineModel('visible');
|
||||
|
||||
const props = defineProps({
|
||||
document: {
|
||||
type: Object,
|
||||
default: null
|
||||
}
|
||||
});
|
||||
|
||||
const formattedAmount = computed(() => {
|
||||
return Number(props.document?.invoice?.amount || 0).toLocaleString('fa-IR');
|
||||
});
|
||||
|
||||
const handlePay = () => {
|
||||
visible.value = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Dialog v-model:visible="visible" :modal="true" :closable="true" :dismissableMask="false" :maximizable="false" :style="{ width: '1032px' }">
|
||||
<template #header>
|
||||
<h2 class="text-[14px] font-[700] text-[#576989]">فاکتور پرداخت «{{ document?.invoice?.number }}»</h2>
|
||||
</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]">
|
||||
یک فاکتور به مبلغ « <span class="font-[700]">{{ formattedAmount }} تومان</span> » برای شما ایجاد شد. لطفا جهت ادامه مراحل درخواست نسبت به پرداخت اقدام نمایید.
|
||||
</p>
|
||||
</div>
|
||||
<p class="text-[13px] font-[400] text-[#3E4C63] leading-7">
|
||||
این فاکتور جهت پردازش درخواست
|
||||
<span class="font-[600] text-[#7A1419]">(({{ modalityTitle(document.modality) }}))</span>
|
||||
به شناسه
|
||||
<span class="font-[600] text-[#7A1419]">(({{ document.code }}))</span>
|
||||
برای شما ایجاد شده است. پس از پرداخت ادامه مراحل درخواست شما پردازش و انجام خواهد شد.
|
||||
</p>
|
||||
<p class="text-[13px] font-[400] text-[#3E4C63] leading-7">
|
||||
درخواست شما پس از پرداخت توسط کارشناسان بررسی میگردد و در صورت نیاز به مدارک و یا توضیحات بیشتر با شما در ارتباط خواهند بود. پس از تایید اولیه کارشناسان ما مبلغ نهایی برای شما فاکتور میگردد. تمامی مراحل با
|
||||
<span class="font-[600] text-[#7A1419]">((پیامک))</span>
|
||||
به اطلاع شما خواهد رسید.
|
||||
<br>
|
||||
بدیهیست که در صورت عدم تایید کارشناسان مبلغ مربوطه به کیف پول شما بازگردانده خواهد شد.
|
||||
</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>
|
||||
</Dialog>
|
||||
</template>
|
||||
@@ -0,0 +1,96 @@
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue';
|
||||
import FullCalendar from '@fullcalendar/vue3';
|
||||
import timeGridPlugin from '@fullcalendar/timegrid';
|
||||
import interactionPlugin from '@fullcalendar/interaction';
|
||||
import faLocale from '@fullcalendar/core/locales/fa';
|
||||
import UiButton from '../../ui/UiButton.vue';
|
||||
import { buildShiftEvent, formatJalaliDay, formatJalaliRange } from '../../../../Shared/shiftCalendarEvents';
|
||||
|
||||
const props = defineProps({
|
||||
myShifts: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
openShifts: {
|
||||
type: Array,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['select-shift']);
|
||||
|
||||
const calendarRef = ref(null);
|
||||
const viewTitle = ref('');
|
||||
|
||||
const events = computed(() => [
|
||||
...props.myShifts.map((shift) => buildShiftEvent(shift, {
|
||||
title: 'شیفت من',
|
||||
classNames: [shift.is_active ? 'shift-event--mine' : 'shift-event--mine-inactive'],
|
||||
})),
|
||||
...props.openShifts.map((shift) => buildShiftEvent(shift, {
|
||||
title: 'شیفت آزاد',
|
||||
classNames: ['shift-event--open'],
|
||||
})),
|
||||
])
|
||||
|
||||
const calendarOptions = computed(() => ({
|
||||
plugins: [timeGridPlugin, interactionPlugin],
|
||||
initialView: 'timeGridWeek',
|
||||
locale: faLocale,
|
||||
direction: 'rtl',
|
||||
firstDay: 6,
|
||||
headerToolbar: false,
|
||||
allDaySlot: false,
|
||||
height: 'auto',
|
||||
nowIndicator: true,
|
||||
slotLabelFormat: { hour: '2-digit', minute: '2-digit', hour12: false },
|
||||
eventTimeFormat: { hour: '2-digit', minute: '2-digit', hour12: false },
|
||||
dayHeaderContent: (args) => ({
|
||||
html: `${args.text}<span class="shift-calendar-day-date">${formatJalaliDay(args.date)}</span>`
|
||||
}),
|
||||
datesSet: (info) => {
|
||||
viewTitle.value = formatJalaliRange(info.start, info.end);
|
||||
},
|
||||
events: events.value,
|
||||
eventClick: (info) => {
|
||||
emit('select-shift', info.event.extendedProps.shift);
|
||||
},
|
||||
}))
|
||||
|
||||
const goPrev = () => calendarRef.value?.getApi().prev();
|
||||
const goNext = () => calendarRef.value?.getApi().next();
|
||||
const goToday = () => calendarRef.value?.getApi().today();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="shift-calendar">
|
||||
<div class="flex flex-wrap items-center justify-between gap-3 pb-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<UiButton label="امروز" size="small" outlined @click="goToday" />
|
||||
<button type="button" class="w-8 h-8 flex items-center justify-center rounded-lg border border-[#DEE3EB] text-[#576989] cursor-pointer" @click="goPrev">
|
||||
<i class="fi fi-rr-angle-small-right flex text-sm"></i>
|
||||
</button>
|
||||
<button type="button" class="w-8 h-8 flex items-center justify-center rounded-lg border border-[#DEE3EB] text-[#576989] cursor-pointer" @click="goNext">
|
||||
<i class="fi fi-rr-angle-small-left flex text-sm"></i>
|
||||
</button>
|
||||
<span class="text-[12px] font-[600] text-[#3E4C63]">{{ viewTitle }}</span>
|
||||
</div>
|
||||
<ul class="flex items-center gap-4">
|
||||
<li class="flex items-center gap-1.5">
|
||||
<span class="w-3 h-3 rounded bg-[#576989]"></span>
|
||||
<span class="text-[11px] font-[600] text-[#3E4C63]">شیفت من</span>
|
||||
</li>
|
||||
<li class="flex items-center gap-1.5">
|
||||
<span class="w-3 h-3 rounded border border-dashed border-[#7188AD] bg-[#F6F7F9]"></span>
|
||||
<span class="text-[11px] font-[600] text-[#3E4C63]">شیفت آزاد</span>
|
||||
</li>
|
||||
<li class="flex items-center gap-1.5">
|
||||
<span class="w-3 h-3 rounded bg-[#C8D2E2]"></span>
|
||||
<span class="text-[11px] font-[600] text-[#3E4C63]">غیرفعال</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<FullCalendar ref="calendarRef" :options="calendarOptions" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -18,6 +18,7 @@ interface SidebarItem extends MenuItem {
|
||||
expanded?: boolean;
|
||||
items?: SidebarItem[];
|
||||
permission?: string | null;
|
||||
level?: string | null;
|
||||
}
|
||||
|
||||
const globalStore = useGlobalStore();
|
||||
@@ -31,9 +32,14 @@ function hasPermission(permission) {
|
||||
return userPermissions.value.includes(permission);
|
||||
}
|
||||
|
||||
function hasLevel(level) {
|
||||
if (!level) return true;
|
||||
return currentUser.value?.level === level;
|
||||
}
|
||||
|
||||
function filterSidebarItems(items) {
|
||||
return items
|
||||
.filter(item => hasPermission(item.permission))
|
||||
.filter(item => hasPermission(item.permission) && hasLevel(item.level))
|
||||
.map(item => {
|
||||
if (item.items) {
|
||||
const filteredChildren = filterSidebarItems(item.items);
|
||||
@@ -87,6 +93,7 @@ const items: Ref<SidebarItem[]> = ref([
|
||||
to: "/dashboard/shifts",
|
||||
key: 'shifts',
|
||||
permission: null,
|
||||
level: "پزشک",
|
||||
},
|
||||
// {
|
||||
// label: "خروج از سامانه",
|
||||
|
||||
@@ -27,7 +27,6 @@ onMounted(() => {
|
||||
<Transition
|
||||
name="page-transition"
|
||||
mode="out-in"
|
||||
appear
|
||||
>
|
||||
<section class="grid grid-cols-1">
|
||||
<slot></slot>
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { Inertia } from '@inertiajs/inertia'
|
||||
|
||||
export const useShiftStore = defineStore('ShiftStore', {
|
||||
state: () => ({
|
||||
endpoints: {
|
||||
index: '/dashboard/shifts',
|
||||
claim: (id) => `/dashboard/shifts/${id}`,
|
||||
release: (id) => `/dashboard/shifts/${id}`,
|
||||
},
|
||||
processingId: null,
|
||||
}),
|
||||
actions: {
|
||||
async claim(id) {
|
||||
this.processingId = id
|
||||
try {
|
||||
await window.axios.patch(this.endpoints.claim(id))
|
||||
Inertia.get(this.endpoints.index)
|
||||
} catch (error) {
|
||||
throw new Error(error.response?.data?.errors?.shift?.[0] ?? 'خطا در انتخاب شیفت')
|
||||
} finally {
|
||||
this.processingId = null
|
||||
}
|
||||
},
|
||||
async release(id) {
|
||||
this.processingId = id
|
||||
try {
|
||||
await window.axios.delete(this.endpoints.release(id))
|
||||
Inertia.get(this.endpoints.index)
|
||||
} catch (error) {
|
||||
throw new Error(error.response?.data?.errors?.shift?.[0] ?? 'خطا در لغو شیفت')
|
||||
} finally {
|
||||
this.processingId = null
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user