@@ -0,0 +1,181 @@
|
||||
<template>
|
||||
<div class="missionary-request" @click="emit('show-details', request)">
|
||||
<div class="missionary-request__heading">
|
||||
<p class="missionary-request__title">{{ request.title }}</p>
|
||||
<p class="missionary-request__subtitle">{{ request.requesterName }}</p>
|
||||
</div>
|
||||
|
||||
<div class="missionary-request__meta">
|
||||
<div class="missionary-request__pill">
|
||||
<span class="missionary-request__pill-label">تاریخ درخواست :</span>
|
||||
<span class="missionary-request__pill-value">
|
||||
{{ formatJalaaliDate(request.requestedDate) || '—' }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="missionary-request__pill">
|
||||
<span class="missionary-request__pill-label">مکان :</span>
|
||||
<span class="missionary-request__pill-value missionary-request__pill-value--fa">
|
||||
{{ request.location || '—' }}
|
||||
</span>
|
||||
</div>
|
||||
<Badge :variant="statusVariant" :dot="true" :value="statusLabel" />
|
||||
</div>
|
||||
|
||||
<div class="missionary-request__actions" @click.stop>
|
||||
<CircleButton
|
||||
v-if="request.status !== 'accepted'"
|
||||
tooltip="پذیرش درخواست"
|
||||
bg-color="rgba(0, 154, 18, 0.08)"
|
||||
size="2.5rem"
|
||||
@click="emit('change-status', { id: request.id, status: 'accepted' })"
|
||||
>
|
||||
<template #icon>
|
||||
<SvgIcon name="check" :size="20" color="var(--color-secondary)" />
|
||||
</template>
|
||||
</CircleButton>
|
||||
<CircleButton
|
||||
v-if="request.status !== 'rejected'"
|
||||
tooltip="رد درخواست"
|
||||
bg-color="rgba(204, 40, 49, 0.08)"
|
||||
size="2.5rem"
|
||||
@click="emit('change-status', { id: request.id, status: 'rejected' })"
|
||||
>
|
||||
<template #icon>
|
||||
<SvgIcon name="close" :size="20" color="var(--color-error)" />
|
||||
</template>
|
||||
</CircleButton>
|
||||
<BaseButton
|
||||
text="جزئیات درخواست"
|
||||
custom-class="missionary-request__details-btn"
|
||||
@click="emit('show-details', request)"
|
||||
>
|
||||
<template #appendIcon>
|
||||
<SvgIcon name="caret-left" :size="16" color="#fff" />
|
||||
</template>
|
||||
</BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import Badge from '@/components/Badge.vue'
|
||||
import { MISSIONARY_REQUEST_STATUS } from '@/enums'
|
||||
import BaseButton from '@/components/BaseButton.vue'
|
||||
import SvgIcon from '@/components/icons/SvgIcon.vue'
|
||||
import { formatJalaaliDate } from '@/utils/date-utils'
|
||||
import CircleButton from '@/components/CircleButton.vue'
|
||||
|
||||
const STATUS_VARIANTS = {
|
||||
pending: 'warning',
|
||||
seen: 'info',
|
||||
accepted: 'success',
|
||||
rejected: 'danger',
|
||||
}
|
||||
|
||||
const props = defineProps({
|
||||
request: { type: Object, required: true },
|
||||
})
|
||||
|
||||
const emit = defineEmits(['show-details', 'change-status'])
|
||||
|
||||
const statusLabel = computed(() => MISSIONARY_REQUEST_STATUS[props.request.status] || '—')
|
||||
const statusVariant = computed(() => STATUS_VARIANTS[props.request.status] || 'neutral')
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.missionary-request {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
padding: 0.75rem 1rem;
|
||||
background: rgba(255, 255, 255, 58%);
|
||||
box-shadow: 0 6.75px 19.425px rgba(0, 0, 0, 4%);
|
||||
border-radius: 0.875rem;
|
||||
margin-bottom: 0.625rem;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 75%);
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
&__heading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
flex: 1 1 33%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-family: var(--font-family-fa);
|
||||
font-weight: 400;
|
||||
font-size: 0.95rem;
|
||||
color: #4b4b4b;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&__subtitle {
|
||||
font-family: var(--font-family-fa);
|
||||
font-weight: 300;
|
||||
font-size: 0.75rem;
|
||||
color: #848484;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&__meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
flex: 1 1 40%;
|
||||
}
|
||||
|
||||
&__pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
background: rgba(107, 107, 107, 5%);
|
||||
padding: 0.25rem 1rem;
|
||||
border-radius: 0.875rem;
|
||||
white-space: nowrap;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
&__pill-label {
|
||||
font-family: var(--font-family-fa);
|
||||
font-weight: 300;
|
||||
font-size: 0.75rem;
|
||||
color: #848484;
|
||||
}
|
||||
|
||||
&__pill-value {
|
||||
font-family: var(--font-family-en);
|
||||
font-size: 0.75rem;
|
||||
color: #5d5d5d;
|
||||
|
||||
&--fa {
|
||||
font-family: var(--font-family-fa);
|
||||
}
|
||||
}
|
||||
|
||||
&__actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
}
|
||||
|
||||
&__details-btn {
|
||||
min-width: 9rem;
|
||||
padding: 0 0.625rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,123 @@
|
||||
<template>
|
||||
<BasicModal
|
||||
title="جزئیات درخواست"
|
||||
title-en="Request Details"
|
||||
width="95%"
|
||||
max-width="50rem"
|
||||
min-width="auto"
|
||||
:show-close-button="true"
|
||||
>
|
||||
<template #default="{ close }">
|
||||
<div class="request-details">
|
||||
<SkeletonLoaderBlock v-if="isLoading" :rows="3" :cols-per-row="1" />
|
||||
|
||||
<template v-else-if="request">
|
||||
<div class="request-details__status">
|
||||
<Badge :variant="statusVariant" :dot="true" :value="statusLabel" />
|
||||
</div>
|
||||
|
||||
<div class="request-details__grid">
|
||||
<LineInfoBlock title="عنوان درخواست" :desc="request.title || '—'" />
|
||||
<LineInfoBlock title="نام درخواست کننده" :desc="request.requesterName || '—'" />
|
||||
<LineInfoBlock title="شماره تماس" :numeric-desc="request.requesterPhone || '—'" />
|
||||
<LineInfoBlock title="ایمیل" :numeric-desc="request.requesterEmail || '—'" />
|
||||
<LineInfoBlock title="مکان" :desc="request.location || '—'" />
|
||||
<LineInfoBlock
|
||||
title="تاریخ درخواست"
|
||||
:numeric-desc="formatJalaaliDate(request.requestedDate) || '—'"
|
||||
/>
|
||||
<LineInfoBlock
|
||||
title="تاریخ ثبت"
|
||||
:numeric-desc="formatJalaaliDateTime(request.createdAt) || '—'"
|
||||
/>
|
||||
<LineInfoBlock title="منبع درخواست" :desc="request.externalSource || '—'" />
|
||||
<div class="request-details__cell--full">
|
||||
<LineInfoBlock title="توضیحات" :desc="request.description || '—'" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<NoItems v-else title="یافت نشد" desc="اطلاعات این درخواست در دسترس نیست." />
|
||||
|
||||
<div class="request-details__actions">
|
||||
<BaseButton text="بسیار خب" custom-class="request-details__btn" @click="close">
|
||||
<template #appendIcon>
|
||||
<SvgIcon name="arrow-left" :size="18" color="#fff" />
|
||||
</template>
|
||||
</BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</BasicModal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import Badge from '@/components/Badge.vue'
|
||||
import useModal from '@/composables/useModal'
|
||||
import { MISSIONARY_REQUEST_STATUS } from '@/enums'
|
||||
import BasicModal from '@/components/BasicModal.vue'
|
||||
import BaseButton from '@/components/BaseButton.vue'
|
||||
import SvgIcon from '@/components/icons/SvgIcon.vue'
|
||||
import NoItems from '@/components/blocks/NoItems.vue'
|
||||
import LineInfoBlock from '@/components/blocks/LineInfoBlock.vue'
|
||||
import { formatJalaaliDate, formatJalaaliDateTime } from '@/utils/date-utils'
|
||||
import SkeletonLoaderBlock from '@/components/blocks/SkeletonLoaderBlock.vue'
|
||||
import { useMissionaryRequestQuery } from '@/services/query/missionary-requests'
|
||||
|
||||
defineOptions({ name: 'MissionaryRequestDetailsModal' })
|
||||
|
||||
const STATUS_VARIANTS = {
|
||||
pending: 'warning',
|
||||
seen: 'info',
|
||||
accepted: 'success',
|
||||
rejected: 'danger',
|
||||
}
|
||||
|
||||
const { getModal } = useModal()
|
||||
|
||||
const modalData = computed(() => getModal('MissionaryRequestDetailsModal')?.data ?? {})
|
||||
const requestId = computed(() => modalData.value.id ?? null)
|
||||
|
||||
const { data: request, isLoading } = useMissionaryRequestQuery(requestId, {
|
||||
enabled: () => !!requestId.value,
|
||||
})
|
||||
|
||||
const statusLabel = computed(() => MISSIONARY_REQUEST_STATUS[request.value?.status] || '—')
|
||||
const statusVariant = computed(() => STATUS_VARIANTS[request.value?.status] || 'neutral')
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.request-details {
|
||||
width: 100%;
|
||||
text-align: start;
|
||||
|
||||
&__status {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
&__grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 0.25rem;
|
||||
|
||||
@media (min-width: 640px) {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
&__cell--full {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
&__actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
&__btn {
|
||||
min-width: 12rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1 @@
|
||||
export { default as MissionaryRequestDetailsModal } from './components/modals/MissionaryRequestDetailsModal.vue'
|
||||
@@ -0,0 +1,103 @@
|
||||
<template>
|
||||
<div class="missionary-requests-page">
|
||||
<BoxedIconTitleBlock
|
||||
class="missionary-requests-page__heading"
|
||||
title="لیست درخواست ها"
|
||||
desc="در این قسمت میتوانید درخواستهای اعزام خود را مدیریت کنید"
|
||||
>
|
||||
<template #icon>
|
||||
<SvgIcon name="user-sound" :size="24" color="var(--color-primary)" />
|
||||
</template>
|
||||
</BoxedIconTitleBlock>
|
||||
|
||||
<SimpleTitleIconBlock title="لیست همه درخواستها" class="missionary-requests-page__list-title">
|
||||
<template #header-icon>
|
||||
<SvgIcon name="list-bullets" :size="16" color="#bcbcbc" />
|
||||
</template>
|
||||
</SimpleTitleIconBlock>
|
||||
|
||||
<SkeletonLoaderBlock v-if="isLoading" :rows="6" :cols-per-row="1" />
|
||||
<div v-else-if="requests.length > 0">
|
||||
<MissionaryRequestItem
|
||||
v-for="request in requests"
|
||||
:key="request.id"
|
||||
:request="request"
|
||||
@show-details="onShowDetails"
|
||||
@change-status="onAskChangeStatus"
|
||||
/>
|
||||
</div>
|
||||
<NoItems v-else title="متاسفیم" desc="درخواستی برای نمایش وجود ندارد." />
|
||||
|
||||
<MissionaryRequestDetailsModal v-if="isModal('MissionaryRequestDetailsModal')" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import useModal from '@/composables/useModal'
|
||||
import { useQueryClient } from '@tanstack/vue-query'
|
||||
import SvgIcon from '@/components/icons/SvgIcon.vue'
|
||||
import NoItems from '@/components/blocks/NoItems.vue'
|
||||
import BoxedIconTitleBlock from '@/components/blocks/BoxedIconTitleBlock.vue'
|
||||
import SkeletonLoaderBlock from '@/components/blocks/SkeletonLoaderBlock.vue'
|
||||
import SimpleTitleIconBlock from '@/components/blocks/SimpleTitleIconBlock.vue'
|
||||
import MissionaryRequestItem from '@/features/missionary/components/MissionaryRequestItem.vue'
|
||||
import MissionaryRequestDetailsModal from '@/features/missionary/components/modals/MissionaryRequestDetailsModal.vue'
|
||||
import {
|
||||
missionaryRequestsKeys,
|
||||
useChangeMissionaryRequestStatusMutation,
|
||||
useMissionaryRequestsListQuery,
|
||||
} from '@/services/query/missionary-requests'
|
||||
|
||||
const queryClient = useQueryClient()
|
||||
const { openModal, isModal } = useModal()
|
||||
|
||||
const { data, isLoading } = useMissionaryRequestsListQuery()
|
||||
|
||||
const requests = computed(() => data.value ?? [])
|
||||
|
||||
const invalidate = () =>
|
||||
queryClient.invalidateQueries({ queryKey: missionaryRequestsKeys.all, refetchType: 'all' })
|
||||
|
||||
const statusMutation = useChangeMissionaryRequestStatusMutation()
|
||||
|
||||
const changeStatus = (id, status) =>
|
||||
statusMutation.mutate({ id, payload: { status } }, { onSuccess: invalidate })
|
||||
|
||||
const onShowDetails = (request) => {
|
||||
openModal('MissionaryRequestDetailsModal', { id: request.id })
|
||||
if (request.status === 'pending') changeStatus(request.id, 'seen')
|
||||
}
|
||||
|
||||
const STATUS_CONFIRMS = {
|
||||
accepted: { title: 'پذیرش درخواست', verb: 'پذیرش' },
|
||||
rejected: { title: 'رد درخواست', verb: 'رد' },
|
||||
}
|
||||
|
||||
const onAskChangeStatus = ({ id, status }) => {
|
||||
const config = STATUS_CONFIRMS[status]
|
||||
if (!config) return
|
||||
const request = requests.value.find((r) => r.id === id)
|
||||
openModal('ConfirmModal', {
|
||||
title: config.title,
|
||||
message: `آیا از ${config.verb} درخواست <strong>${request?.title || ''}</strong> مطمئن هستید؟`,
|
||||
onConfirm: () => changeStatus(id, status),
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.missionary-requests-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
|
||||
&__heading {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
&__list-title {
|
||||
margin-bottom: 0.375rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,24 @@
|
||||
export default [
|
||||
{
|
||||
path: '/missionary-panel/requests',
|
||||
name: 'missionary-requests',
|
||||
component: () => import('@/features/missionary/pages/MissionaryRequestsPage.vue'),
|
||||
meta: {
|
||||
layout: 'missionary',
|
||||
role: 'missionary',
|
||||
title: 'سیستم مدیریت اعزام',
|
||||
subtitle: 'سادهترین راه برای برنامهریزی و پایش مأموریتها',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/missionary-panel/tickets',
|
||||
name: 'missionary-tickets',
|
||||
component: () => import('@/features/student/tickets/pages/StudentTicketsPage.vue'),
|
||||
meta: {
|
||||
layout: 'missionary',
|
||||
role: 'missionary',
|
||||
title: 'تیکتهای شما',
|
||||
subtitle: 'اینجا پل ارتباطی شما با پشتیبانی است.',
|
||||
},
|
||||
},
|
||||
]
|
||||
Reference in New Issue
Block a user