feat: add shift management functionality

This commit is contained in:
2026-08-26 18:53:15 +03:30
parent 2d96978c38
commit aeef95339d
34 changed files with 1953 additions and 15 deletions
@@ -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: "خروج از سامانه",