@@ -17,10 +17,31 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<template v-for="tab in tabs">
|
||||||
|
<div
|
||||||
|
v-if="tab.hasButton"
|
||||||
|
:key="`btn-${tab.name}`"
|
||||||
|
class="tabs-block__action tabs-block__action--desktop"
|
||||||
|
>
|
||||||
|
<BaseButton
|
||||||
|
v-if="tab.hasButton && activeTab === tab.name"
|
||||||
|
:text="tab.textButton"
|
||||||
|
custom-class="tabs-block__action-btn"
|
||||||
|
@click="onActionClick(tab)"
|
||||||
|
>
|
||||||
|
<template #appendIcon>
|
||||||
|
<SvgIcon name="arrow-left" :size="16" color="#fff" />
|
||||||
|
</template>
|
||||||
|
</BaseButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template v-for="tab in tabs">
|
||||||
<div
|
<div
|
||||||
v-for="tab in tabs"
|
v-if="tab.hasButton"
|
||||||
:key="`btn-${tab.name}`"
|
:key="`mbtn-${tab.name}`"
|
||||||
class="tabs-block__action tabs-block__action--desktop"
|
class="tabs-block__action tabs-block__action--mobile"
|
||||||
>
|
>
|
||||||
<BaseButton
|
<BaseButton
|
||||||
v-if="tab.hasButton && activeTab === tab.name"
|
v-if="tab.hasButton && activeTab === tab.name"
|
||||||
@@ -33,24 +54,7 @@
|
|||||||
</template>
|
</template>
|
||||||
</BaseButton>
|
</BaseButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</template>
|
||||||
|
|
||||||
<div
|
|
||||||
v-for="tab in tabs"
|
|
||||||
:key="`mbtn-${tab.name}`"
|
|
||||||
class="tabs-block__action tabs-block__action--mobile"
|
|
||||||
>
|
|
||||||
<BaseButton
|
|
||||||
v-if="tab.hasButton && activeTab === tab.name"
|
|
||||||
:text="tab.textButton"
|
|
||||||
custom-class="tabs-block__action-btn"
|
|
||||||
@click="onActionClick(tab)"
|
|
||||||
>
|
|
||||||
<template #appendIcon>
|
|
||||||
<SvgIcon name="arrow-left" :size="16" color="#fff" />
|
|
||||||
</template>
|
|
||||||
</BaseButton>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="tabs-block__content">
|
<div class="tabs-block__content">
|
||||||
<slot :name="activeTab" />
|
<slot :name="activeTab" />
|
||||||
|
|||||||
@@ -0,0 +1,124 @@
|
|||||||
|
<template>
|
||||||
|
<div class="badge-select">
|
||||||
|
<label v-if="label" class="badge-select__label">{{ label }}</label>
|
||||||
|
<div class="badge-select__chips" :class="`badge-select__chips--${size}`">
|
||||||
|
<button
|
||||||
|
v-for="option in options"
|
||||||
|
:key="option[optionValue]"
|
||||||
|
type="button"
|
||||||
|
class="badge-select__chip"
|
||||||
|
:class="[
|
||||||
|
`badge-select__chip--${variant}`,
|
||||||
|
{ 'badge-select__chip--selected': modelValue === option[optionValue] },
|
||||||
|
]"
|
||||||
|
:disabled="disabled"
|
||||||
|
@click="select(option)"
|
||||||
|
>
|
||||||
|
{{ option[optionLabel] }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div v-if="error" class="badge-select__error">{{ error }}</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: { type: [String, Number], default: null },
|
||||||
|
label: { type: String, default: '' },
|
||||||
|
options: { type: Array, default: () => [] },
|
||||||
|
optionLabel: { type: String, default: 'label' },
|
||||||
|
optionValue: { type: String, default: 'value' },
|
||||||
|
variant: { type: String, default: 'neutral' },
|
||||||
|
size: { type: String, default: 'md' },
|
||||||
|
disabled: { type: Boolean, default: false },
|
||||||
|
error: { type: String, default: '' },
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:modelValue', 'change'])
|
||||||
|
|
||||||
|
const select = (option) => {
|
||||||
|
if (props.disabled) return
|
||||||
|
const value = option[props.optionValue]
|
||||||
|
emit('update:modelValue', value)
|
||||||
|
emit('change', value)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.badge-select {
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
|
||||||
|
&__label {
|
||||||
|
display: flex;
|
||||||
|
margin-bottom: 0.375rem;
|
||||||
|
font-family: var(--font-family-fa);
|
||||||
|
font-weight: 300;
|
||||||
|
line-height: 1.5rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: var(--color-prim-gray);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__chips {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__chip {
|
||||||
|
border: 1px solid #e8e8e8;
|
||||||
|
border-radius: 9999px;
|
||||||
|
background: transparent;
|
||||||
|
font-family: var(--font-family-fa);
|
||||||
|
color: #b5b5b5;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
&:hover:enabled {
|
||||||
|
color: #7a7a7a;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--selected {
|
||||||
|
background: #fff;
|
||||||
|
border-color: #f0f0f0;
|
||||||
|
box-shadow: 0 6.75px 19.425px rgba(0, 0, 0, 6%);
|
||||||
|
color: #4b4b4b;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--primary#{&}--selected {
|
||||||
|
color: var(--color-primary);
|
||||||
|
border-color: var(--color-primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__chips--sm .badge-select__chip {
|
||||||
|
padding: 0.25rem 0.875rem;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__chips--md .badge-select__chip {
|
||||||
|
padding: 0.375rem 1.125rem;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__chips--lg .badge-select__chip {
|
||||||
|
padding: 0.5rem 1.375rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__error {
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
font-family: var(--font-family-fa);
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--color-error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -15,6 +15,15 @@
|
|||||||
@click="toggle"
|
@click="toggle"
|
||||||
>
|
>
|
||||||
<span class="select-field__value">{{ selectedLabel }}</span>
|
<span class="select-field__value">{{ selectedLabel }}</span>
|
||||||
|
<button
|
||||||
|
v-if="showClear"
|
||||||
|
type="button"
|
||||||
|
class="select-field__clear"
|
||||||
|
aria-label="پاک کردن"
|
||||||
|
@click.stop="clearSelection"
|
||||||
|
>
|
||||||
|
<SvgIcon name="close" :size="14" color="currentColor" />
|
||||||
|
</button>
|
||||||
<SvgIcon
|
<SvgIcon
|
||||||
name="caret-down"
|
name="caret-down"
|
||||||
:size="20"
|
:size="20"
|
||||||
@@ -90,6 +99,7 @@ const props = defineProps({
|
|||||||
optionLabel: { type: String, default: 'name' },
|
optionLabel: { type: String, default: 'name' },
|
||||||
optionValue: { type: String, default: 'id' },
|
optionValue: { type: String, default: 'id' },
|
||||||
multiple: { type: Boolean, default: false },
|
multiple: { type: Boolean, default: false },
|
||||||
|
clearable: { type: Boolean, default: true },
|
||||||
disabled: { type: Boolean, default: false },
|
disabled: { type: Boolean, default: false },
|
||||||
error: { type: String, default: '' },
|
error: { type: String, default: '' },
|
||||||
vibration: { type: Boolean, default: false },
|
vibration: { type: Boolean, default: false },
|
||||||
@@ -130,6 +140,20 @@ const selectedLabel = computed(() => {
|
|||||||
return found ? found[props.optionLabel] : props.placeholder
|
return found ? found[props.optionLabel] : props.placeholder
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const hasValue = computed(() =>
|
||||||
|
props.multiple
|
||||||
|
? Array.isArray(props.modelValue) && props.modelValue.length > 0
|
||||||
|
: props.modelValue !== undefined && props.modelValue !== null && props.modelValue !== ''
|
||||||
|
)
|
||||||
|
|
||||||
|
const showClear = computed(() => props.clearable && !props.disabled && hasValue.value)
|
||||||
|
|
||||||
|
const clearSelection = () => {
|
||||||
|
const cleared = props.multiple ? [] : null
|
||||||
|
emit('update:modelValue', cleared)
|
||||||
|
emit('change', cleared)
|
||||||
|
}
|
||||||
|
|
||||||
const select = (opt) => {
|
const select = (opt) => {
|
||||||
if (props.disabled) return
|
if (props.disabled) return
|
||||||
const value = opt[props.optionValue]
|
const value = opt[props.optionValue]
|
||||||
@@ -275,6 +299,24 @@ onBeforeUnmount(() => unmountFloating())
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__clear {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0;
|
||||||
|
margin-inline-end: 0.375rem;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--color-thd-gray);
|
||||||
|
cursor: pointer;
|
||||||
|
flex-shrink: 0;
|
||||||
|
transition: color 0.15s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--color-error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&__error {
|
&__error {
|
||||||
margin-top: 0.25rem;
|
margin-top: 0.25rem;
|
||||||
font-family: var(--font-family-fa);
|
font-family: var(--font-family-fa);
|
||||||
|
|||||||
@@ -0,0 +1,127 @@
|
|||||||
|
<template>
|
||||||
|
<div class="admin-missionary-page">
|
||||||
|
<BoxedIconTitleBlock
|
||||||
|
class="admin-missionary-page__heading"
|
||||||
|
title="مدیریت مبلغین"
|
||||||
|
desc="در این قسمت میتوانید درخواستهای اعزام مبلغین را مشاهده و مدیریت کنید"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<SvgIcon name="user-sound" :size="24" color="var(--color-primary)" />
|
||||||
|
</template>
|
||||||
|
</BoxedIconTitleBlock>
|
||||||
|
|
||||||
|
<SimpleTitleIconBlock title="لیست همه درخواستها" class="admin-missionary-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"
|
||||||
|
:always-actionable="true"
|
||||||
|
@show-details="onShowDetails"
|
||||||
|
@change-status="onAskChangeStatus"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<NoItems v-else title="متاسفیم" desc="درخواستی برای نمایش وجود ندارد." />
|
||||||
|
|
||||||
|
<PaginationBlock :pagination="paginationMeta" @update:page="setPage" />
|
||||||
|
|
||||||
|
<RequestDetailsModal v-if="isModal('RequestDetailsModal')" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed, ref } from 'vue'
|
||||||
|
import useModal from '@/composables/useModal'
|
||||||
|
import { MISSIONARY_REQUEST_STATUS } from '@/enums'
|
||||||
|
import { useQueryClient } from '@tanstack/vue-query'
|
||||||
|
import SvgIcon from '@/components/icons/SvgIcon.vue'
|
||||||
|
import NoItems from '@/components/blocks/NoItems.vue'
|
||||||
|
import { formatJalaaliDate } from '@/utils/date-utils'
|
||||||
|
import { usePagination } from '@/composables/usePagination'
|
||||||
|
import PaginationBlock from '@/components/blocks/PaginationBlock.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 RequestDetailsModal from '@/features/missionary/components/modals/RequestDetailsModal.vue'
|
||||||
|
import {
|
||||||
|
adminMissionaryRequestsKeys,
|
||||||
|
useAdminMissionaryRequestsListQuery,
|
||||||
|
useChangeAdminMissionaryRequestStatusMutation,
|
||||||
|
} from '@/services/query/admin-missionary-requests'
|
||||||
|
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
const { openModal, isModal } = useModal()
|
||||||
|
|
||||||
|
const filters = ref({})
|
||||||
|
const { pagination, setPage } = usePagination({ page: 1, perPage: 10 })
|
||||||
|
|
||||||
|
const { data, isLoading } = useAdminMissionaryRequestsListQuery(filters, pagination, {
|
||||||
|
keepPreviousData: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
const toRequestView = (request) => ({
|
||||||
|
...request,
|
||||||
|
code: request.id,
|
||||||
|
requestNumber: request.id,
|
||||||
|
requestDate: formatJalaaliDate(request.requestedDate),
|
||||||
|
statusLabel: MISSIONARY_REQUEST_STATUS[request.status] || request.status || '—',
|
||||||
|
phone: request.requesterPhone,
|
||||||
|
address: request.location,
|
||||||
|
})
|
||||||
|
|
||||||
|
const requests = computed(() => (data.value?.data ?? []).map((r) => toRequestView(r)))
|
||||||
|
|
||||||
|
const paginationMeta = computed(() => ({
|
||||||
|
page: pagination.value.page,
|
||||||
|
perPage: pagination.value.perPage,
|
||||||
|
...data.value?.meta,
|
||||||
|
}))
|
||||||
|
|
||||||
|
const statusMutation = useChangeAdminMissionaryRequestStatusMutation()
|
||||||
|
|
||||||
|
const invalidate = () =>
|
||||||
|
queryClient.invalidateQueries({ queryKey: adminMissionaryRequestsKeys.all, refetchType: 'all' })
|
||||||
|
|
||||||
|
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: () => statusMutation.mutate({ id, payload: { status } }, { onSuccess: invalidate }),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const onShowDetails = (request) => {
|
||||||
|
openModal('RequestDetailsModal', request)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.admin-missionary-page {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
|
||||||
|
&__heading {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__list-title {
|
||||||
|
margin-bottom: 0.375rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -252,4 +252,15 @@ export default [
|
|||||||
subtitle: 'جلسات مشاوره را پیگیری کرده و پاسخگوی نیازهای کاربران باشید.',
|
subtitle: 'جلسات مشاوره را پیگیری کرده و پاسخگوی نیازهای کاربران باشید.',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/missionary-requests',
|
||||||
|
name: 'admin-missionary-requests',
|
||||||
|
component: () => import('@/features/admin/missionary/pages/AdminMissionaryRequestsPage.vue'),
|
||||||
|
meta: {
|
||||||
|
layout: 'admin',
|
||||||
|
role: 'admin',
|
||||||
|
title: 'مدیریت مبلغین',
|
||||||
|
subtitle: 'درخواستهای اعزام مبلغین را مشاهده و مدیریت نمایید.',
|
||||||
|
},
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ const props = defineProps({
|
|||||||
maxSeconds: { type: Number, default: 120 },
|
maxSeconds: { type: Number, default: 120 },
|
||||||
context: { type: String, default: 'verification' },
|
context: { type: String, default: 'verification' },
|
||||||
subType: { type: String, default: '' },
|
subType: { type: String, default: '' },
|
||||||
purpose: { type: String, default: '' },
|
purpose: { type: String, default: 'video' },
|
||||||
error: { type: String, default: '' },
|
error: { type: String, default: '' },
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
+2
-8
@@ -19,11 +19,6 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mdis__actions" @click.stop>
|
<div class="mdis__actions" @click.stop>
|
||||||
<CircleButton bg-color="rgba(0, 112, 116, 0.08)" size="2rem">
|
|
||||||
<template #icon>
|
|
||||||
<SvgIcon name="pencil" :size="14" color="#007074" />
|
|
||||||
</template>
|
|
||||||
</CircleButton>
|
|
||||||
<BaseButton
|
<BaseButton
|
||||||
text="جزئیات درخواست"
|
text="جزئیات درخواست"
|
||||||
custom-class="mdis__btn"
|
custom-class="mdis__btn"
|
||||||
@@ -40,7 +35,6 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import BaseButton from '@/components/BaseButton.vue'
|
import BaseButton from '@/components/BaseButton.vue'
|
||||||
import SvgIcon from '@/components/icons/SvgIcon.vue'
|
import SvgIcon from '@/components/icons/SvgIcon.vue'
|
||||||
import CircleButton from '@/components/CircleButton.vue'
|
|
||||||
|
|
||||||
defineProps({
|
defineProps({
|
||||||
dispatch: { type: Object, required: true },
|
dispatch: { type: Object, required: true },
|
||||||
@@ -87,7 +81,7 @@ const emit = defineEmits(['show-details'])
|
|||||||
&__title-wrap {
|
&__title-wrap {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: flex-end;
|
align-items: flex-start;
|
||||||
text-align: end;
|
text-align: end;
|
||||||
gap: 0.35rem;
|
gap: 0.35rem;
|
||||||
}
|
}
|
||||||
@@ -167,7 +161,7 @@ const emit = defineEmits(['show-details'])
|
|||||||
|
|
||||||
&__btn {
|
&__btn {
|
||||||
min-width: 8rem;
|
min-width: 8rem;
|
||||||
height: 2rem;
|
height: 2.5rem;
|
||||||
padding: 0 1.25rem;
|
padding: 0 1.25rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+15
-18
@@ -15,20 +15,23 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mnar__actions">
|
<div class="mnar__actions" @click.stop>
|
||||||
<CircleButton bg-color="rgba(0, 112, 116, 0.08)" size="2rem">
|
<CircleButton
|
||||||
|
tooltip="ویرایش روایت"
|
||||||
|
bg-color="rgba(107, 107, 107, 0.04)"
|
||||||
|
@click="emit('edit', narrative)"
|
||||||
|
>
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<SvgIcon name="check" :size="14" color="#007074" />
|
<SvgIcon name="pencil" :size="18" color="#535353" />
|
||||||
</template>
|
</template>
|
||||||
</CircleButton>
|
</CircleButton>
|
||||||
<CircleButton bg-color="rgba(243, 102, 117, 0.06)" size="2rem">
|
<CircleButton
|
||||||
|
tooltip="حذف روایت"
|
||||||
|
bg-color="rgba(243, 102, 117, 0.06)"
|
||||||
|
@click="emit('delete', narrative)"
|
||||||
|
>
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<SvgIcon name="close" :size="14" color="var(--color-error)" />
|
<SvgIcon name="trash" :size="18" color="var(--color-error)" />
|
||||||
</template>
|
|
||||||
</CircleButton>
|
|
||||||
<CircleButton bg-color="rgba(107, 107, 107, 0.04)" size="2rem">
|
|
||||||
<template #icon>
|
|
||||||
<SvgIcon name="pencil" :size="14" color="#535353" />
|
|
||||||
</template>
|
</template>
|
||||||
</CircleButton>
|
</CircleButton>
|
||||||
</div>
|
</div>
|
||||||
@@ -43,7 +46,7 @@ defineProps({
|
|||||||
narrative: { type: Object, required: true },
|
narrative: { type: Object, required: true },
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits(['show-details'])
|
const emit = defineEmits(['show-details', 'edit', 'delete'])
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@@ -78,7 +81,7 @@ const emit = defineEmits(['show-details'])
|
|||||||
&__title-wrap {
|
&__title-wrap {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: flex-end;
|
align-items: flex-start;
|
||||||
text-align: end;
|
text-align: end;
|
||||||
gap: 0.2rem;
|
gap: 0.2rem;
|
||||||
}
|
}
|
||||||
@@ -167,11 +170,5 @@ const emit = defineEmits(['show-details'])
|
|||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__btn {
|
|
||||||
min-width: 7rem;
|
|
||||||
height: 2rem;
|
|
||||||
padding: 0 1.25rem;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -1,56 +1,56 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="missionary-request" @click="emit('show-details', request)">
|
<div class="mreq" @click="emit('show-details', request)">
|
||||||
<div class="missionary-request__heading">
|
<div class="mreq__heading">
|
||||||
<p class="missionary-request__title">{{ request.title }}</p>
|
<span class="mreq__code">{{ request.code }}</span>
|
||||||
<p class="missionary-request__subtitle">{{ request.requesterName }}</p>
|
<div class="mreq__title-wrap">
|
||||||
|
<p class="mreq__title">{{ request.title }}</p>
|
||||||
|
<p class="mreq__subtitle">شماره درخواست : {{ request.requestNumber }}</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="missionary-request__meta">
|
<div class="mreq__meta">
|
||||||
<div class="missionary-request__pill">
|
<div class="mreq__pill mreq__pill--date">
|
||||||
<span class="missionary-request__pill-label">تاریخ درخواست :</span>
|
<span class="mreq__pill-label">تاریخ درخواست :</span>
|
||||||
<span class="missionary-request__pill-value">
|
<span class="mreq__pill-value">{{ request.requestDate }}</span>
|
||||||
{{ formatJalaaliDate(request.requestedDate) || '—' }}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="missionary-request__pill">
|
<div class="mreq__pill mreq__pill--name">
|
||||||
<span class="missionary-request__pill-label">مکان :</span>
|
<span class="mreq__pill-label">نام درخواست کننده :</span>
|
||||||
<span class="missionary-request__pill-value missionary-request__pill-value--fa">
|
<span class="mreq__pill-value">{{ request.requesterName }}</span>
|
||||||
{{ request.location || '—' }}
|
</div>
|
||||||
</span>
|
<div class="mreq__pill mreq__pill--status" :class="`mreq__pill--${tone}`">
|
||||||
|
<span class="mreq__dot" />
|
||||||
|
<span class="mreq__pill-value mreq__pill-value--status">{{ request.statusLabel }}</span>
|
||||||
</div>
|
</div>
|
||||||
<Badge :variant="statusVariant" :dot="true" :value="statusLabel" />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="missionary-request__actions" @click.stop>
|
<div class="mreq__actions" @click.stop>
|
||||||
<CircleButton
|
<CircleButton
|
||||||
v-if="request.status !== 'accepted'"
|
v-if="canAccept"
|
||||||
tooltip="پذیرش درخواست"
|
tooltip="پذیرش درخواست"
|
||||||
bg-color="rgba(0, 154, 18, 0.08)"
|
bg-color="rgba(0, 112, 116, 0.08)"
|
||||||
size="2.5rem"
|
|
||||||
@click="emit('change-status', { id: request.id, status: 'accepted' })"
|
@click="emit('change-status', { id: request.id, status: 'accepted' })"
|
||||||
>
|
>
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<SvgIcon name="check" :size="20" color="var(--color-secondary)" />
|
<SvgIcon name="check" :size="18" color="#007074" />
|
||||||
</template>
|
</template>
|
||||||
</CircleButton>
|
</CircleButton>
|
||||||
<CircleButton
|
<CircleButton
|
||||||
v-if="request.status !== 'rejected'"
|
v-if="canReject"
|
||||||
tooltip="رد درخواست"
|
tooltip="رد درخواست"
|
||||||
bg-color="rgba(204, 40, 49, 0.08)"
|
bg-color="rgba(243, 102, 117, 0.06)"
|
||||||
size="2.5rem"
|
|
||||||
@click="emit('change-status', { id: request.id, status: 'rejected' })"
|
@click="emit('change-status', { id: request.id, status: 'rejected' })"
|
||||||
>
|
>
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<SvgIcon name="close" :size="20" color="var(--color-error)" />
|
<SvgIcon name="close" :size="18" color="var(--color-error)" />
|
||||||
</template>
|
</template>
|
||||||
</CircleButton>
|
</CircleButton>
|
||||||
<BaseButton
|
<BaseButton
|
||||||
text="جزئیات درخواست"
|
text="جزئیات درخواست"
|
||||||
custom-class="missionary-request__details-btn"
|
custom-class="mreq__btn"
|
||||||
@click="emit('show-details', request)"
|
@click="emit('show-details', request)"
|
||||||
>
|
>
|
||||||
<template #appendIcon>
|
<template #appendIcon>
|
||||||
<SvgIcon name="caret-left" :size="16" color="#fff" />
|
<SvgIcon name="arrow-left" :size="16" color="#fff" />
|
||||||
</template>
|
</template>
|
||||||
</BaseButton>
|
</BaseButton>
|
||||||
</div>
|
</div>
|
||||||
@@ -59,39 +59,41 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import Badge from '@/components/Badge.vue'
|
|
||||||
import { MISSIONARY_REQUEST_STATUS } from '@/enums'
|
|
||||||
import BaseButton from '@/components/BaseButton.vue'
|
import BaseButton from '@/components/BaseButton.vue'
|
||||||
import SvgIcon from '@/components/icons/SvgIcon.vue'
|
import SvgIcon from '@/components/icons/SvgIcon.vue'
|
||||||
import { formatJalaaliDate } from '@/utils/date-utils'
|
|
||||||
import CircleButton from '@/components/CircleButton.vue'
|
import CircleButton from '@/components/CircleButton.vue'
|
||||||
|
|
||||||
const STATUS_VARIANTS = {
|
const TONE_MAP = {
|
||||||
pending: 'warning',
|
pending: 'warning',
|
||||||
seen: 'info',
|
seen: 'neutral',
|
||||||
accepted: 'success',
|
accepted: 'success',
|
||||||
rejected: 'danger',
|
rejected: 'danger',
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
request: { type: Object, required: true },
|
request: { type: Object, required: true },
|
||||||
|
alwaysActionable: { type: Boolean, default: false },
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits(['show-details', 'change-status'])
|
const emit = defineEmits(['show-details', 'change-status'])
|
||||||
|
|
||||||
const statusLabel = computed(() => MISSIONARY_REQUEST_STATUS[props.request.status] || '—')
|
const tone = computed(() => TONE_MAP[props.request.status] || 'neutral')
|
||||||
const statusVariant = computed(() => STATUS_VARIANTS[props.request.status] || 'neutral')
|
|
||||||
|
const canAct = computed(() => props.alwaysActionable || props.request.status === 'pending')
|
||||||
|
const canAccept = computed(() => canAct.value && props.request.status !== 'accepted')
|
||||||
|
const canReject = computed(() => canAct.value && props.request.status !== 'rejected')
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.missionary-request {
|
.mreq {
|
||||||
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
padding: 0.75rem 1rem;
|
padding: 1rem 1.25rem;
|
||||||
background: rgba(255, 255, 255, 58%);
|
background: rgba(255, 255, 255, 58%);
|
||||||
box-shadow: 0 6.75px 19.425px rgba(0, 0, 0, 4%);
|
box-shadow: 0 6.75px 19.425px rgba(0, 0, 0, 4%);
|
||||||
border-radius: 0.875rem;
|
border-radius: 0.75rem;
|
||||||
margin-bottom: 0.625rem;
|
margin-bottom: 0.625rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background 0.15s ease;
|
transition: background 0.15s ease;
|
||||||
@@ -103,15 +105,26 @@ const statusVariant = computed(() => STATUS_VARIANTS[props.request.status] || 'n
|
|||||||
@media (min-width: 1024px) {
|
@media (min-width: 1024px) {
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 1rem;
|
gap: 1.5rem;
|
||||||
|
padding: 1rem 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__heading {
|
&__heading {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.625rem;
|
||||||
|
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
min-width: 9rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__title-wrap {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.25rem;
|
align-items: flex-start;
|
||||||
flex: 1 1 33%;
|
text-align: end;
|
||||||
min-width: 0;
|
gap: 0.125rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__title {
|
&__title {
|
||||||
@@ -124,58 +137,136 @@ const statusVariant = computed(() => STATUS_VARIANTS[props.request.status] || 'n
|
|||||||
|
|
||||||
&__subtitle {
|
&__subtitle {
|
||||||
font-family: var(--font-family-fa);
|
font-family: var(--font-family-fa);
|
||||||
font-weight: 300;
|
font-weight: 400;
|
||||||
font-size: 0.75rem;
|
font-size: 0.7rem;
|
||||||
color: #848484;
|
color: #4b4b4b;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__code {
|
||||||
|
font-family: var(--font-family-en);
|
||||||
|
font-weight: 800;
|
||||||
|
font-size: 2.75rem;
|
||||||
|
line-height: 1;
|
||||||
|
color: #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
&__meta {
|
&__meta {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.375rem;
|
justify-content: center;
|
||||||
flex: 1 1 40%;
|
gap: 0.5rem;
|
||||||
|
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
flex: 1;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__pill {
|
&__pill {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.25rem;
|
gap: 0.375rem;
|
||||||
background: rgba(107, 107, 107, 5%);
|
padding: 0.3rem 0.875rem;
|
||||||
padding: 0.25rem 1rem;
|
border-radius: 0.75rem;
|
||||||
border-radius: 0.875rem;
|
background: rgba(107, 107, 107, 4%);
|
||||||
|
font-family: var(--font-family-fa);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
line-height: 1.5;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&__pill-label {
|
&__pill-label {
|
||||||
font-family: var(--font-family-fa);
|
font-weight: 400;
|
||||||
font-weight: 300;
|
font-size: 0.65rem;
|
||||||
font-size: 0.75rem;
|
color: #535353;
|
||||||
color: #848484;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&__pill-value {
|
&__pill-value {
|
||||||
font-family: var(--font-family-en);
|
font-family: var(--font-family-en);
|
||||||
|
font-weight: 400;
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
color: #5d5d5d;
|
color: #5d5d5d;
|
||||||
|
}
|
||||||
|
|
||||||
&--fa {
|
&__pill-value--status {
|
||||||
font-family: var(--font-family-fa);
|
font-family: var(--font-family-fa);
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 0.65rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__dot {
|
||||||
|
width: 0.32rem;
|
||||||
|
height: 0.32rem;
|
||||||
|
border-radius: 9999px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__pill--status {
|
||||||
|
padding: 0.3rem 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__pill--danger {
|
||||||
|
background: rgba(243, 102, 117, 6%);
|
||||||
|
|
||||||
|
.mreq__pill-value {
|
||||||
|
color: #cc2831;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mreq__dot {
|
||||||
|
background: #cc2831;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__pill--success {
|
||||||
|
background: rgba(0, 112, 116, 6%);
|
||||||
|
|
||||||
|
.mreq__pill-value {
|
||||||
|
color: #007074;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mreq__dot {
|
||||||
|
background: #007074;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__pill--warning {
|
||||||
|
background: rgba(182, 132, 45, 7%);
|
||||||
|
|
||||||
|
.mreq__pill-value {
|
||||||
|
color: #b6842d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mreq__dot {
|
||||||
|
background: #b6842d;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__pill--neutral {
|
||||||
|
background: rgba(107, 107, 107, 6%);
|
||||||
|
|
||||||
|
.mreq__pill-value {
|
||||||
|
color: #535353;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mreq__dot {
|
||||||
|
background: #989898;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__actions {
|
&__actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: center;
|
||||||
align-items: center;
|
gap: 6px;
|
||||||
gap: 0.375rem;
|
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__details-btn {
|
&__btn {
|
||||||
min-width: 9rem;
|
min-width: 8rem;
|
||||||
padding: 0 0.625rem;
|
height: 2.5rem;
|
||||||
|
padding: 0 1.25rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
+12
-19
@@ -7,8 +7,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="muc__profile-info">
|
<div class="muc__profile-info">
|
||||||
<div class="muc__level">
|
<div class="muc__level">
|
||||||
<span class="muc__level-label">سطح کاربر :</span>
|
<span class="muc__level-label">شماره تماس :</span>
|
||||||
<span class="muc__level-value">{{ level }}</span>
|
<span class="muc__level-value muc__level-value--numeric">{{ phone }}</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="muc__name">{{ fullName }}</p>
|
<p class="muc__name">{{ fullName }}</p>
|
||||||
<div class="muc__location">
|
<div class="muc__location">
|
||||||
@@ -29,15 +29,6 @@
|
|||||||
|
|
||||||
<span class="muc__divider" />
|
<span class="muc__divider" />
|
||||||
|
|
||||||
<div class="muc__stat">
|
|
||||||
<span class="muc__stat-label">امتیاز کاربر</span>
|
|
||||||
<div class="muc__stat-value">
|
|
||||||
<span class="muc__stat-num">{{ rating }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<span class="muc__divider" />
|
|
||||||
|
|
||||||
<div class="muc__stat">
|
<div class="muc__stat">
|
||||||
<span class="muc__stat-label">تعداد درخواستها</span>
|
<span class="muc__stat-label">تعداد درخواستها</span>
|
||||||
<div class="muc__stat-value">
|
<div class="muc__stat-value">
|
||||||
@@ -48,11 +39,11 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="muc__actions">
|
<div class="muc__actions">
|
||||||
<CircleButton bg-color="rgba(0, 112, 116, 0.08)" size="2.5rem">
|
<BaseButton text="افزودن روایت" custom-class="muc__btn" @click="emit('add-narrative')">
|
||||||
<template #icon>
|
<template #appendIcon>
|
||||||
<SvgIcon name="pencil" :size="20" color="#007074" />
|
<SvgIcon name="plus" :size="16" color="#fff" />
|
||||||
</template>
|
</template>
|
||||||
</CircleButton>
|
</BaseButton>
|
||||||
<BaseButton text="تکمیل اطلاعات" custom-class="muc__btn" @click="emit('complete-info')">
|
<BaseButton text="تکمیل اطلاعات" custom-class="muc__btn" @click="emit('complete-info')">
|
||||||
<template #appendIcon>
|
<template #appendIcon>
|
||||||
<SvgIcon name="arrow-left" :size="16" color="#fff" />
|
<SvgIcon name="arrow-left" :size="16" color="#fff" />
|
||||||
@@ -66,20 +57,18 @@
|
|||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import BaseButton from '@/components/BaseButton.vue'
|
import BaseButton from '@/components/BaseButton.vue'
|
||||||
import SvgIcon from '@/components/icons/SvgIcon.vue'
|
import SvgIcon from '@/components/icons/SvgIcon.vue'
|
||||||
import CircleButton from '@/components/CircleButton.vue'
|
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
profile: { type: Object, default: () => ({}) },
|
profile: { type: Object, default: () => ({}) },
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits(['complete-info', 'settings'])
|
const emit = defineEmits(['complete-info', 'settings', 'add-narrative'])
|
||||||
|
|
||||||
const fullName = computed(() => props.profile.fullName || '—')
|
const fullName = computed(() => props.profile.fullName || '—')
|
||||||
const city = computed(() => props.profile.city || '—')
|
const city = computed(() => props.profile.city || '—')
|
||||||
const level = computed(() => props.profile.level || '—')
|
const phone = computed(() => props.profile.phone || '—')
|
||||||
const avatar = computed(() => props.profile.avatar || '')
|
const avatar = computed(() => props.profile.avatar || '')
|
||||||
const membershipDays = computed(() => props.profile.membershipDays ?? '—')
|
const membershipDays = computed(() => props.profile.membershipDays ?? '—')
|
||||||
const rating = computed(() => props.profile.rating ?? '—')
|
|
||||||
const requestsCount = computed(() => props.profile.requestsCount ?? '—')
|
const requestsCount = computed(() => props.profile.requestsCount ?? '—')
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -234,6 +223,10 @@ const requestsCount = computed(() => props.profile.requestsCount ?? '—')
|
|||||||
&__level-value {
|
&__level-value {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
|
|
||||||
|
&--numeric {
|
||||||
|
font-family: var(--font-family-en);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__avatar {
|
&__avatar {
|
||||||
@@ -0,0 +1,238 @@
|
|||||||
|
<template>
|
||||||
|
<BasicModal
|
||||||
|
:title="isEditMode ? 'ویرایش روایت' : 'ثبت روایت'"
|
||||||
|
:title-en="isEditMode ? 'Edit Story' : 'Create Story'"
|
||||||
|
width="95%"
|
||||||
|
max-width="58rem"
|
||||||
|
min-width="auto"
|
||||||
|
:show-close-button="true"
|
||||||
|
>
|
||||||
|
<template #default="{ close }">
|
||||||
|
<form class="create-narrative" @submit.prevent="onSubmit(close)">
|
||||||
|
<div class="create-narrative__row create-narrative__row--two">
|
||||||
|
<TextField
|
||||||
|
v-model="form.title"
|
||||||
|
name="title"
|
||||||
|
label="عنوان روایت"
|
||||||
|
:error="errors.title"
|
||||||
|
@blur="validateAt('title', form.title)"
|
||||||
|
>
|
||||||
|
<template #appendIcon>
|
||||||
|
<SvgIcon name="pencil" :size="20" color="var(--color-thd-gray)" />
|
||||||
|
</template>
|
||||||
|
</TextField>
|
||||||
|
<SelectField
|
||||||
|
v-model="form.missionaryRequestId"
|
||||||
|
name="missionaryRequestId"
|
||||||
|
label="درخواست مرتبط"
|
||||||
|
placeholder="انتخاب کنید"
|
||||||
|
:options="requestOptions"
|
||||||
|
option-label="title"
|
||||||
|
option-value="id"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="create-narrative__row">
|
||||||
|
<TextareaField
|
||||||
|
v-model="form.description"
|
||||||
|
name="description"
|
||||||
|
label="توضیحات روایت خود را وارد کنید"
|
||||||
|
:row="4"
|
||||||
|
:error="errors.description"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="create-narrative__upload">
|
||||||
|
<FileUploader
|
||||||
|
v-model="files"
|
||||||
|
accept="image/*,video/mp4"
|
||||||
|
:max-files="5"
|
||||||
|
@select="onFilesSelect"
|
||||||
|
@error="onFileError"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="create-narrative__divider" />
|
||||||
|
|
||||||
|
<footer class="create-narrative__footer">
|
||||||
|
<BaseButton
|
||||||
|
type="submit"
|
||||||
|
:text="isEditMode ? 'ذخیره تغییرات' : 'ثبت روایت'"
|
||||||
|
custom-class="create-narrative__submit"
|
||||||
|
:loading="createMutation.isPending.value || updateMutation.isPending.value"
|
||||||
|
:disabled="uploadMutation.isPending.value"
|
||||||
|
>
|
||||||
|
<template #appendIcon>
|
||||||
|
<SvgIcon name="arrow-left" :size="18" color="#fff" />
|
||||||
|
</template>
|
||||||
|
</BaseButton>
|
||||||
|
</footer>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
</BasicModal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import * as yup from 'yup'
|
||||||
|
import { computed, ref } from 'vue'
|
||||||
|
import { toast } from 'vue3-toastify'
|
||||||
|
import useYup from '@/composables/useYup'
|
||||||
|
import useModal from '@/composables/useModal'
|
||||||
|
import BasicModal from '@/components/BasicModal.vue'
|
||||||
|
import BaseButton from '@/components/BaseButton.vue'
|
||||||
|
import SvgIcon from '@/components/icons/SvgIcon.vue'
|
||||||
|
import { useQueryClient } from '@tanstack/vue-query'
|
||||||
|
import TextField from '@/components/form/TextField.vue'
|
||||||
|
import SelectField from '@/components/form/SelectField.vue'
|
||||||
|
import FileUploader from '@/components/form/FileUploader.vue'
|
||||||
|
import { objectToFormData } from '@/utils/object-to-formdata'
|
||||||
|
import { useUploadMediaMutation } from '@/services/query/auth'
|
||||||
|
import TextareaField from '@/components/form/TextareaField.vue'
|
||||||
|
import { useMissionaryRequestsListQuery } from '@/services/query/missionary-requests'
|
||||||
|
import {
|
||||||
|
missionaryMemoriesKeys,
|
||||||
|
useCreateMissionaryMemoryMutation,
|
||||||
|
useUpdateMissionaryMemoryMutation,
|
||||||
|
} from '@/services/query/missionary-memories'
|
||||||
|
|
||||||
|
defineOptions({ name: 'CreateNarrativeModal' })
|
||||||
|
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
const { getModal } = useModal()
|
||||||
|
|
||||||
|
const editingMemory = getModal('CreateNarrativeModal')?.data?.memory ?? null
|
||||||
|
const isEditMode = !!editingMemory
|
||||||
|
|
||||||
|
const form = ref({
|
||||||
|
title: editingMemory?.title || '',
|
||||||
|
description: editingMemory?.description || '',
|
||||||
|
missionaryRequestId: editingMemory?.missionaryRequestId ?? null,
|
||||||
|
})
|
||||||
|
|
||||||
|
const files = ref(
|
||||||
|
(editingMemory?.media ?? []).map((media) => ({
|
||||||
|
id: media.id,
|
||||||
|
name: media.fileName,
|
||||||
|
size: media.fileSize,
|
||||||
|
url: media.url,
|
||||||
|
existing: true,
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
|
||||||
|
const schema = yup.object({
|
||||||
|
title: yup.string().trim().required('عنوان روایت را وارد کنید'),
|
||||||
|
description: yup.string().trim().notRequired(),
|
||||||
|
missionaryRequestId: yup.mixed().nullable().notRequired(),
|
||||||
|
})
|
||||||
|
|
||||||
|
const { validate, validateAt, errors, resetErrors } = useYup(schema)
|
||||||
|
|
||||||
|
const requestsFilters = ref({ status: 'accepted' })
|
||||||
|
const requestsPagination = ref({ page: 1, perPage: 100 })
|
||||||
|
const { data: requestsData } = useMissionaryRequestsListQuery(requestsFilters, requestsPagination)
|
||||||
|
|
||||||
|
const requestOptions = computed(
|
||||||
|
() => requestsData.value?.data?.items ?? requestsData.value?.data ?? []
|
||||||
|
)
|
||||||
|
|
||||||
|
const uploadMutation = useUploadMediaMutation()
|
||||||
|
const createMutation = useCreateMissionaryMemoryMutation()
|
||||||
|
const updateMutation = useUpdateMissionaryMemoryMutation()
|
||||||
|
|
||||||
|
const onFilesSelect = async (picked) => {
|
||||||
|
for (const file of picked) {
|
||||||
|
try {
|
||||||
|
const fd = objectToFormData({ file, purpose: 'memory' })
|
||||||
|
const response = await uploadMutation.mutateAsync(fd)
|
||||||
|
const payload = response?.data ?? response
|
||||||
|
const id = payload?.id ?? payload?.uploadId
|
||||||
|
if (id == null) continue
|
||||||
|
files.value = [
|
||||||
|
...files.value,
|
||||||
|
{ id, name: file.name, size: file.size, url: payload?.url ?? '' },
|
||||||
|
]
|
||||||
|
} catch {
|
||||||
|
toast.error(`بارگذاری فایل "${file.name}" با خطا مواجه شد.`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onFileError = (msg) => toast.warning(msg)
|
||||||
|
|
||||||
|
const onSubmit = async (close) => {
|
||||||
|
const { isValid, payload } = await validate(form.value)
|
||||||
|
if (!isValid) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
const body = {
|
||||||
|
title: payload.title,
|
||||||
|
description: payload.description,
|
||||||
|
mediaIds: files.value
|
||||||
|
.filter((f) => !f.existing)
|
||||||
|
.map((f) => f.id)
|
||||||
|
.filter((id) => id != null),
|
||||||
|
}
|
||||||
|
if (form.value.missionaryRequestId) body.missionaryRequestId = form.value.missionaryRequestId
|
||||||
|
if (isEditMode) {
|
||||||
|
await updateMutation.mutateAsync({ id: editingMemory.id, payload: body })
|
||||||
|
} else {
|
||||||
|
await createMutation.mutateAsync(body)
|
||||||
|
}
|
||||||
|
await queryClient.invalidateQueries({
|
||||||
|
queryKey: missionaryMemoriesKeys.all,
|
||||||
|
refetchType: 'all',
|
||||||
|
})
|
||||||
|
toast.success(isEditMode ? 'روایت با موفقیت بهروزرسانی شد.' : 'روایت با موفقیت ثبت شد.')
|
||||||
|
resetErrors()
|
||||||
|
form.value = { title: '', description: '', missionaryRequestId: null }
|
||||||
|
files.value = []
|
||||||
|
close?.()
|
||||||
|
} catch {
|
||||||
|
toast.error('ثبت روایت با خطا مواجه شد.')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.create-narrative {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
text-align: start;
|
||||||
|
padding-block: 0.25rem 0.5rem;
|
||||||
|
|
||||||
|
&__row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 0.875rem;
|
||||||
|
|
||||||
|
&--two {
|
||||||
|
@media (min-width: 640px) {
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__upload {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__divider {
|
||||||
|
border-block-end: 1px solid #ddd;
|
||||||
|
margin-block: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__footer {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__submit {
|
||||||
|
min-width: 16rem;
|
||||||
|
padding: 0 1.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,123 +0,0 @@
|
|||||||
<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>
|
|
||||||
+24
@@ -25,9 +25,18 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="narrative-details__request">
|
||||||
|
<LineInfoBlock title="درخواست مرتبط" :desc="narrative.missionaryRequest?.title || '—'" />
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="narrative-details__body">
|
<div class="narrative-details__body">
|
||||||
<LineInfoBlock title="نتیجه یا دستاورد مأموریت" :desc="narrative.body || '—'" />
|
<LineInfoBlock title="نتیجه یا دستاورد مأموریت" :desc="narrative.body || '—'" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-if="narrative.media?.length" class="narrative-details__media">
|
||||||
|
<p class="narrative-details__media-title">پیوستها</p>
|
||||||
|
<MessageAttachments :media="narrative.media" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</BasicModal>
|
</BasicModal>
|
||||||
</template>
|
</template>
|
||||||
@@ -38,6 +47,7 @@ import useModal from '@/composables/useModal'
|
|||||||
import BasicModal from '@/components/BasicModal.vue'
|
import BasicModal from '@/components/BasicModal.vue'
|
||||||
import SvgIcon from '@/components/icons/SvgIcon.vue'
|
import SvgIcon from '@/components/icons/SvgIcon.vue'
|
||||||
import LineInfoBlock from '@/components/blocks/LineInfoBlock.vue'
|
import LineInfoBlock from '@/components/blocks/LineInfoBlock.vue'
|
||||||
|
import MessageAttachments from '@/components/blocks/MessageAttachments.vue'
|
||||||
|
|
||||||
defineOptions({ name: 'NarrativeDetailsModal' })
|
defineOptions({ name: 'NarrativeDetailsModal' })
|
||||||
|
|
||||||
@@ -109,6 +119,20 @@ const narrative = computed(() => getModal('NarrativeDetailsModal')?.data ?? {})
|
|||||||
color: #4e4e4e;
|
color: #4e4e4e;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__media {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__media-title {
|
||||||
|
font-family: var(--font-family-fa);
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: #848484;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
&__footer {
|
&__footer {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
+1
-1
@@ -14,7 +14,7 @@
|
|||||||
/>
|
/>
|
||||||
<LineInfoBlock title="تاریخ ثبت درخواست" :numeric-desc="request.requestDate || '—'" />
|
<LineInfoBlock title="تاریخ ثبت درخواست" :numeric-desc="request.requestDate || '—'" />
|
||||||
<LineInfoBlock title="شماره تماس" :numeric-desc="request.phone || '—'" />
|
<LineInfoBlock title="شماره تماس" :numeric-desc="request.phone || '—'" />
|
||||||
<LineInfoBlock title="کد پستی" :numeric-desc="request.postalCode || '—'" />
|
<LineInfoBlock title="وضعیت" :desc="request.statusLabel || '—'" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="request-details__row">
|
<div class="request-details__row">
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
<template>
|
||||||
|
<BasicModal
|
||||||
|
:title="isEditMode ? 'ویرایش گواهی و تقدیرنامه' : 'افزودن گواهی و تقدیرنامه جدید'"
|
||||||
|
title-en="Certificate"
|
||||||
|
width="95%"
|
||||||
|
max-width="48rem"
|
||||||
|
min-width="auto"
|
||||||
|
:show-close-button="true"
|
||||||
|
>
|
||||||
|
<template #default="{ close }">
|
||||||
|
<form class="add-certificate" @submit.prevent="onSubmit(close)">
|
||||||
|
<TextField
|
||||||
|
v-model="form.title"
|
||||||
|
name="title"
|
||||||
|
label="عنوان"
|
||||||
|
:error="errors.title"
|
||||||
|
@blur="validateAt('title', form.title)"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
v-model="form.universityName"
|
||||||
|
name="universityName"
|
||||||
|
label="نام دانشگاه"
|
||||||
|
:error="errors.universityName"
|
||||||
|
@blur="validateAt('universityName', form.universityName)"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="add-certificate__row">
|
||||||
|
<TextField
|
||||||
|
v-model="form.startYear"
|
||||||
|
name="startYear"
|
||||||
|
label="سال شروع"
|
||||||
|
inputmode="numeric"
|
||||||
|
:convert-digits="true"
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
v-model="form.endYear"
|
||||||
|
name="endYear"
|
||||||
|
label="سال پایان"
|
||||||
|
inputmode="numeric"
|
||||||
|
:convert-digits="true"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="add-certificate__divider" />
|
||||||
|
|
||||||
|
<footer class="add-certificate__footer">
|
||||||
|
<BaseButton type="submit" text="ذخیره اطلاعات" custom-class="add-certificate__submit">
|
||||||
|
<template #appendIcon>
|
||||||
|
<SvgIcon name="arrow-left" :size="18" color="#fff" />
|
||||||
|
</template>
|
||||||
|
</BaseButton>
|
||||||
|
</footer>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
</BasicModal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import * as yup from 'yup'
|
||||||
|
import useYup from '@/composables/useYup'
|
||||||
|
import useModal from '@/composables/useModal'
|
||||||
|
import BasicModal from '@/components/BasicModal.vue'
|
||||||
|
import BaseButton from '@/components/BaseButton.vue'
|
||||||
|
import SvgIcon from '@/components/icons/SvgIcon.vue'
|
||||||
|
import TextField from '@/components/form/TextField.vue'
|
||||||
|
import { useCompleteInfoStore } from '@/features/missionary/store/complete-info'
|
||||||
|
|
||||||
|
defineOptions({ name: 'MissionaryAddCertificateModal' })
|
||||||
|
|
||||||
|
const store = useCompleteInfoStore()
|
||||||
|
const { getModal } = useModal()
|
||||||
|
|
||||||
|
const modalData = getModal('MissionaryAddCertificateModal')?.data ?? {}
|
||||||
|
const editingIndex = modalData.index ?? null
|
||||||
|
const isEditMode = editingIndex != null
|
||||||
|
|
||||||
|
const emptyForm = () => ({
|
||||||
|
title: '',
|
||||||
|
universityName: '',
|
||||||
|
startYear: '',
|
||||||
|
endYear: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
const form = ref({ ...emptyForm(), ...modalData.entry })
|
||||||
|
|
||||||
|
const schema = yup.object({
|
||||||
|
title: yup.string().trim().required('عنوان را وارد کنید'),
|
||||||
|
universityName: yup.string().trim().required('نام دانشگاه را وارد کنید'),
|
||||||
|
})
|
||||||
|
|
||||||
|
const { validate, validateAt, errors } = useYup(schema)
|
||||||
|
|
||||||
|
const onSubmit = async (close) => {
|
||||||
|
const { isValid } = await validate(form.value)
|
||||||
|
if (!isValid) return
|
||||||
|
store.upsertCertificate({ ...form.value }, editingIndex)
|
||||||
|
close?.()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.add-certificate {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.25rem;
|
||||||
|
text-align: start;
|
||||||
|
padding-block: 0.25rem 0.5rem;
|
||||||
|
|
||||||
|
&__row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 0 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__divider {
|
||||||
|
border-block-end: 1px solid #ddd;
|
||||||
|
margin-block: 0.5rem 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__submit {
|
||||||
|
min-width: 14rem;
|
||||||
|
padding: 0 1.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
<template>
|
||||||
|
<BasicModal
|
||||||
|
:title="isEditMode ? 'ویرایش سابقه تحصیلی' : 'افزودن سابقه تحصیلی جدید'"
|
||||||
|
title-en="Education History"
|
||||||
|
width="95%"
|
||||||
|
max-width="48rem"
|
||||||
|
min-width="auto"
|
||||||
|
:show-close-button="true"
|
||||||
|
>
|
||||||
|
<template #default="{ close }">
|
||||||
|
<form class="add-education" @submit.prevent="onSubmit(close)">
|
||||||
|
<BadgeSelectField
|
||||||
|
v-model="form.degree"
|
||||||
|
label="مقطع تحصیلی"
|
||||||
|
:options="degreeOptions"
|
||||||
|
:error="errors.degree"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
v-model="form.universityName"
|
||||||
|
name="universityName"
|
||||||
|
label="نام دانشگاه"
|
||||||
|
:error="errors.universityName"
|
||||||
|
@blur="validateAt('universityName', form.universityName)"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
v-model="form.fieldOfStudy"
|
||||||
|
name="fieldOfStudy"
|
||||||
|
label="رشته تحصیلی"
|
||||||
|
:error="errors.fieldOfStudy"
|
||||||
|
@blur="validateAt('fieldOfStudy', form.fieldOfStudy)"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="add-education__row">
|
||||||
|
<TextField
|
||||||
|
v-model="form.startYear"
|
||||||
|
name="startYear"
|
||||||
|
label="سال شروع"
|
||||||
|
inputmode="numeric"
|
||||||
|
:convert-digits="true"
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
v-model="form.endYear"
|
||||||
|
name="endYear"
|
||||||
|
label="سال پایان"
|
||||||
|
inputmode="numeric"
|
||||||
|
:convert-digits="true"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="add-education__divider" />
|
||||||
|
|
||||||
|
<footer class="add-education__footer">
|
||||||
|
<BaseButton type="submit" text="ذخیره اطلاعات" custom-class="add-education__submit">
|
||||||
|
<template #appendIcon>
|
||||||
|
<SvgIcon name="arrow-left" :size="18" color="#fff" />
|
||||||
|
</template>
|
||||||
|
</BaseButton>
|
||||||
|
</footer>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
</BasicModal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import * as yup from 'yup'
|
||||||
|
import useYup from '@/composables/useYup'
|
||||||
|
import useModal from '@/composables/useModal'
|
||||||
|
import BasicModal from '@/components/BasicModal.vue'
|
||||||
|
import BaseButton from '@/components/BaseButton.vue'
|
||||||
|
import SvgIcon from '@/components/icons/SvgIcon.vue'
|
||||||
|
import TextField from '@/components/form/TextField.vue'
|
||||||
|
import BadgeSelectField from '@/components/form/BadgeSelectField.vue'
|
||||||
|
import { useCompleteInfoStore } from '@/features/missionary/store/complete-info'
|
||||||
|
import { EDUCATION_DEGREES } from '@/features/missionary/constants/complete-info'
|
||||||
|
|
||||||
|
defineOptions({ name: 'MissionaryAddEducationModal' })
|
||||||
|
|
||||||
|
const store = useCompleteInfoStore()
|
||||||
|
const { getModal } = useModal()
|
||||||
|
|
||||||
|
const modalData = getModal('MissionaryAddEducationModal')?.data ?? {}
|
||||||
|
const editingIndex = modalData.index ?? null
|
||||||
|
const isEditMode = editingIndex != null
|
||||||
|
|
||||||
|
const degreeOptions = Object.entries(EDUCATION_DEGREES).map(([value, label]) => ({ value, label }))
|
||||||
|
|
||||||
|
const emptyForm = () => ({
|
||||||
|
degree: null,
|
||||||
|
universityName: '',
|
||||||
|
fieldOfStudy: '',
|
||||||
|
startYear: '',
|
||||||
|
endYear: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
const form = ref({ ...emptyForm(), ...modalData.entry })
|
||||||
|
|
||||||
|
const schema = yup.object({
|
||||||
|
degree: yup.string().nullable().required('مقطع تحصیلی را انتخاب کنید'),
|
||||||
|
universityName: yup.string().trim().required('نام دانشگاه را وارد کنید'),
|
||||||
|
fieldOfStudy: yup.string().trim().required('رشته تحصیلی را وارد کنید'),
|
||||||
|
})
|
||||||
|
|
||||||
|
const { validate, validateAt, errors } = useYup(schema)
|
||||||
|
|
||||||
|
const onSubmit = async (close) => {
|
||||||
|
const { isValid } = await validate(form.value)
|
||||||
|
if (!isValid) return
|
||||||
|
store.upsertEducation({ ...form.value }, editingIndex)
|
||||||
|
close?.()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.add-education {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.25rem;
|
||||||
|
text-align: start;
|
||||||
|
padding-block: 0.25rem 0.5rem;
|
||||||
|
|
||||||
|
&__row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 0 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__divider {
|
||||||
|
border-block-end: 1px solid #ddd;
|
||||||
|
margin-block: 0.5rem 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__submit {
|
||||||
|
min-width: 14rem;
|
||||||
|
padding: 0 1.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,241 @@
|
|||||||
|
<template>
|
||||||
|
<BasicModal
|
||||||
|
title="افزودن مهارت"
|
||||||
|
title-en="Add skills"
|
||||||
|
width="95%"
|
||||||
|
max-width="40rem"
|
||||||
|
min-width="auto"
|
||||||
|
:show-close-button="true"
|
||||||
|
>
|
||||||
|
<template #default="{ close }">
|
||||||
|
<div class="add-skill">
|
||||||
|
<div v-if="store.skills.length > 0" class="add-skill__selected">
|
||||||
|
<Badge
|
||||||
|
v-for="skill in store.skills"
|
||||||
|
:key="skill.title"
|
||||||
|
variant="neutral"
|
||||||
|
:label="skill.title"
|
||||||
|
:value="SKILL_LEVELS[skill.level] || skill.level"
|
||||||
|
>
|
||||||
|
<template #append>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="add-skill__remove"
|
||||||
|
aria-label="حذف مهارت"
|
||||||
|
@click="store.removeSkill(skill.title)"
|
||||||
|
>
|
||||||
|
<SvgIcon name="close" :size="12" color="currentColor" />
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label class="add-skill__hint">مهارت های خود را با علامت Enter از هم جدا کنید.</label>
|
||||||
|
<textarea
|
||||||
|
v-model="query"
|
||||||
|
rows="4"
|
||||||
|
class="add-skill__input"
|
||||||
|
@keydown.enter.prevent="onEnter"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div v-if="suggestions.length > 0" class="add-skill__suggestions">
|
||||||
|
<div
|
||||||
|
v-for="suggestion in suggestions"
|
||||||
|
:key="suggestion"
|
||||||
|
class="add-skill__suggestion"
|
||||||
|
:class="{ 'add-skill__suggestion--open': pendingSkill === suggestion }"
|
||||||
|
>
|
||||||
|
<Badge
|
||||||
|
variant="neutral"
|
||||||
|
:value="suggestion"
|
||||||
|
clickable
|
||||||
|
@click="togglePending(suggestion)"
|
||||||
|
>
|
||||||
|
<template #append>
|
||||||
|
<SvgIcon name="plus" :size="12" color="currentColor" />
|
||||||
|
</template>
|
||||||
|
</Badge>
|
||||||
|
<div v-if="pendingSkill === suggestion" class="add-skill__levels">
|
||||||
|
<button
|
||||||
|
v-for="(label, level) in SKILL_LEVELS"
|
||||||
|
:key="level"
|
||||||
|
type="button"
|
||||||
|
class="add-skill__level"
|
||||||
|
@click="addWithLevel(suggestion, level)"
|
||||||
|
>
|
||||||
|
{{ label }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="add-skill__divider" />
|
||||||
|
|
||||||
|
<footer class="add-skill__footer">
|
||||||
|
<BaseButton text="ذخیره اطلاعات" custom-class="add-skill__submit" @click="close">
|
||||||
|
<template #appendIcon>
|
||||||
|
<SvgIcon name="arrow-left" :size="18" color="#fff" />
|
||||||
|
</template>
|
||||||
|
</BaseButton>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</BasicModal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed, ref } from 'vue'
|
||||||
|
import Badge from '@/components/Badge.vue'
|
||||||
|
import BasicModal from '@/components/BasicModal.vue'
|
||||||
|
import BaseButton from '@/components/BaseButton.vue'
|
||||||
|
import SvgIcon from '@/components/icons/SvgIcon.vue'
|
||||||
|
import { useCompleteInfoStore } from '@/features/missionary/store/complete-info'
|
||||||
|
import { SKILL_LEVELS, SKILL_SUGGESTIONS } from '@/features/missionary/constants/complete-info'
|
||||||
|
|
||||||
|
defineOptions({ name: 'MissionaryAddSkillModal' })
|
||||||
|
|
||||||
|
const store = useCompleteInfoStore()
|
||||||
|
|
||||||
|
const query = ref('')
|
||||||
|
const pendingSkill = ref('')
|
||||||
|
|
||||||
|
const suggestions = computed(() => {
|
||||||
|
const term = query.value.trim()
|
||||||
|
if (!term) return []
|
||||||
|
const picked = new Set(store.skills.map((s) => s.title))
|
||||||
|
const matches = SKILL_SUGGESTIONS.filter(
|
||||||
|
(title) => title.includes(term) && !picked.has(title)
|
||||||
|
).slice(0, 8)
|
||||||
|
if (!matches.includes(term) && !picked.has(term)) matches.unshift(term)
|
||||||
|
return matches
|
||||||
|
})
|
||||||
|
|
||||||
|
const togglePending = (title) => {
|
||||||
|
pendingSkill.value = pendingSkill.value === title ? '' : title
|
||||||
|
}
|
||||||
|
|
||||||
|
const onEnter = () => {
|
||||||
|
const term = query.value.trim()
|
||||||
|
if (term) pendingSkill.value = term
|
||||||
|
}
|
||||||
|
|
||||||
|
const addWithLevel = (title, level) => {
|
||||||
|
store.addSkill({ title, level })
|
||||||
|
pendingSkill.value = ''
|
||||||
|
query.value = ''
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.add-skill {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.875rem;
|
||||||
|
text-align: start;
|
||||||
|
padding-block: 0.25rem 0.5rem;
|
||||||
|
|
||||||
|
&__selected {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__remove {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: #b1b1b1;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--color-error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__hint {
|
||||||
|
font-family: var(--font-family-fa);
|
||||||
|
font-weight: 300;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: #b5b5b5;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.875rem 1rem;
|
||||||
|
border: 1px solid var(--color-thd-gray);
|
||||||
|
border-radius: 1rem;
|
||||||
|
font-family: var(--font-family-fa);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: #4b4b4b;
|
||||||
|
outline: none;
|
||||||
|
resize: vertical;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
border-color: var(--color-prim-gray);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__suggestions {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__suggestion {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&--open {
|
||||||
|
z-index: 5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__levels {
|
||||||
|
position: absolute;
|
||||||
|
top: calc(100% + 0.25rem);
|
||||||
|
inset-inline-start: 0;
|
||||||
|
min-width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: #f5f5f5;
|
||||||
|
border-radius: 0.625rem;
|
||||||
|
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 8%);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__level {
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
padding: 0.625rem 1rem;
|
||||||
|
font-family: var(--font-family-fa);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: #7a7a7a;
|
||||||
|
text-align: start;
|
||||||
|
cursor: pointer;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #ececec;
|
||||||
|
color: #4b4b4b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__divider {
|
||||||
|
border-block-end: 1px solid #ddd;
|
||||||
|
margin-top: 4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__submit {
|
||||||
|
min-width: 14rem;
|
||||||
|
padding: 0 1.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,223 @@
|
|||||||
|
<template>
|
||||||
|
<BasicModal
|
||||||
|
:title="isEditMode ? 'ویرایش سابقه کاری' : 'افزودن سابقه کاری'"
|
||||||
|
title-en="Work Experience"
|
||||||
|
width="95%"
|
||||||
|
max-width="58rem"
|
||||||
|
min-width="auto"
|
||||||
|
:show-close-button="true"
|
||||||
|
>
|
||||||
|
<template #default="{ close }">
|
||||||
|
<form class="add-work" @submit.prevent="onSubmit(close)">
|
||||||
|
<TextField
|
||||||
|
v-model="form.jobTitle"
|
||||||
|
name="jobTitle"
|
||||||
|
label="عنوان شغل شما"
|
||||||
|
:error="errors.jobTitle"
|
||||||
|
@blur="validateAt('jobTitle', form.jobTitle)"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="add-work__row">
|
||||||
|
<TextField
|
||||||
|
v-model="form.activityField"
|
||||||
|
name="activityField"
|
||||||
|
label="زمینه فعالیت شما در این شرکت"
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
v-model="form.organizationLevel"
|
||||||
|
name="organizationLevel"
|
||||||
|
label="رده سازمانی"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
v-model="form.organizationName"
|
||||||
|
name="organizationName"
|
||||||
|
label="نام سازمان"
|
||||||
|
:error="errors.organizationName"
|
||||||
|
@blur="validateAt('organizationName', form.organizationName)"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="add-work__row">
|
||||||
|
<SelectField
|
||||||
|
v-model="form.provinceId"
|
||||||
|
name="provinceId"
|
||||||
|
label="استان"
|
||||||
|
:options="provinces"
|
||||||
|
option-label="name"
|
||||||
|
option-value="id"
|
||||||
|
@change="onProvinceChange"
|
||||||
|
/>
|
||||||
|
<SelectField
|
||||||
|
v-model="form.cityId"
|
||||||
|
name="cityId"
|
||||||
|
label="شهر"
|
||||||
|
:options="cities"
|
||||||
|
option-label="name"
|
||||||
|
option-value="id"
|
||||||
|
:disabled="!form.provinceId"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="add-work__row add-work__row--always">
|
||||||
|
<SelectField
|
||||||
|
v-model="form.startMonth"
|
||||||
|
name="startMonth"
|
||||||
|
label="ماه شروع"
|
||||||
|
:options="MONTHS"
|
||||||
|
option-label="label"
|
||||||
|
option-value="value"
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
v-model="form.startYear"
|
||||||
|
name="startYear"
|
||||||
|
label="سال شروع"
|
||||||
|
inputmode="numeric"
|
||||||
|
:convert-digits="true"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="add-work__row add-work__row--always">
|
||||||
|
<SelectField
|
||||||
|
v-model="form.endMonth"
|
||||||
|
name="endMonth"
|
||||||
|
label="ماه پایان"
|
||||||
|
:options="MONTHS"
|
||||||
|
option-label="label"
|
||||||
|
option-value="value"
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
v-model="form.endYear"
|
||||||
|
name="endYear"
|
||||||
|
label="سال پایان"
|
||||||
|
inputmode="numeric"
|
||||||
|
:convert-digits="true"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<TextareaField
|
||||||
|
v-model="form.achievements"
|
||||||
|
name="achievements"
|
||||||
|
label="دستاوردها و وظایف کلیدی"
|
||||||
|
:row="4"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="add-work__divider" />
|
||||||
|
|
||||||
|
<footer class="add-work__footer">
|
||||||
|
<BaseButton type="submit" text="ذخیره اطلاعات" custom-class="add-work__submit">
|
||||||
|
<template #appendIcon>
|
||||||
|
<SvgIcon name="arrow-left" :size="18" color="#fff" />
|
||||||
|
</template>
|
||||||
|
</BaseButton>
|
||||||
|
</footer>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
</BasicModal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import * as yup from 'yup'
|
||||||
|
import useYup from '@/composables/useYup'
|
||||||
|
import useModal from '@/composables/useModal'
|
||||||
|
import BasicModal from '@/components/BasicModal.vue'
|
||||||
|
import BaseButton from '@/components/BaseButton.vue'
|
||||||
|
import SvgIcon from '@/components/icons/SvgIcon.vue'
|
||||||
|
import TextField from '@/components/form/TextField.vue'
|
||||||
|
import SelectField from '@/components/form/SelectField.vue'
|
||||||
|
import TextareaField from '@/components/form/TextareaField.vue'
|
||||||
|
import { MONTHS } from '@/features/missionary/constants/complete-info'
|
||||||
|
import { useCompleteInfoStore } from '@/features/missionary/store/complete-info'
|
||||||
|
import { useGetCitiesOfProvinceQuery, useGetProvincesQuery } from '@/services/query/common'
|
||||||
|
|
||||||
|
defineOptions({ name: 'MissionaryAddWorkExperienceModal' })
|
||||||
|
|
||||||
|
const store = useCompleteInfoStore()
|
||||||
|
const { getModal } = useModal()
|
||||||
|
|
||||||
|
const modalData = getModal('MissionaryAddWorkExperienceModal')?.data ?? {}
|
||||||
|
const editingIndex = modalData.index ?? null
|
||||||
|
const isEditMode = editingIndex != null
|
||||||
|
|
||||||
|
const emptyForm = () => ({
|
||||||
|
jobTitle: '',
|
||||||
|
activityField: '',
|
||||||
|
organizationLevel: '',
|
||||||
|
organizationName: '',
|
||||||
|
provinceId: null,
|
||||||
|
cityId: null,
|
||||||
|
startMonth: null,
|
||||||
|
startYear: '',
|
||||||
|
endMonth: null,
|
||||||
|
endYear: '',
|
||||||
|
achievements: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
const form = ref({ ...emptyForm(), ...modalData.entry })
|
||||||
|
|
||||||
|
const schema = yup.object({
|
||||||
|
jobTitle: yup.string().trim().required('عنوان شغل را وارد کنید'),
|
||||||
|
organizationName: yup.string().trim().required('نام سازمان را وارد کنید'),
|
||||||
|
})
|
||||||
|
|
||||||
|
const { validate, validateAt, errors } = useYup(schema)
|
||||||
|
|
||||||
|
const { data: provinces = ref([]) } = useGetProvincesQuery()
|
||||||
|
|
||||||
|
const provinceIdRef = ref(form.value.provinceId)
|
||||||
|
const { data: cities = ref([]) } = useGetCitiesOfProvinceQuery(provinceIdRef, {
|
||||||
|
enabled: () => !!provinceIdRef.value,
|
||||||
|
})
|
||||||
|
|
||||||
|
const onProvinceChange = () => {
|
||||||
|
provinceIdRef.value = form.value.provinceId
|
||||||
|
form.value.cityId = null
|
||||||
|
}
|
||||||
|
|
||||||
|
const onSubmit = async (close) => {
|
||||||
|
const { isValid } = await validate(form.value)
|
||||||
|
if (!isValid) return
|
||||||
|
store.upsertWorkExperience({ ...form.value }, editingIndex)
|
||||||
|
close?.()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.add-work {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.25rem;
|
||||||
|
text-align: start;
|
||||||
|
padding-block: 0.25rem 0.5rem;
|
||||||
|
|
||||||
|
&__row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 0 0.875rem;
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--always {
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__divider {
|
||||||
|
border-block-end: 1px solid #ddd;
|
||||||
|
margin-block: 0.5rem 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__submit {
|
||||||
|
min-width: 14rem;
|
||||||
|
padding: 0 1.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,259 @@
|
|||||||
|
<template>
|
||||||
|
<BasicModal
|
||||||
|
title="بخش معرفی"
|
||||||
|
title-en="Introduction"
|
||||||
|
width="95%"
|
||||||
|
max-width="64rem"
|
||||||
|
min-width="auto"
|
||||||
|
:show-close-button="true"
|
||||||
|
>
|
||||||
|
<template #default="{ close }">
|
||||||
|
<div class="complete-info">
|
||||||
|
<TextareaField
|
||||||
|
v-model="store.introduction"
|
||||||
|
name="introduction"
|
||||||
|
label="لطفا خودتان را به صورت کامل معرفی نمایید."
|
||||||
|
:row="5"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<section class="complete-info__section">
|
||||||
|
<div class="complete-info__section-head">
|
||||||
|
<LineTitleBlock title="مهارت های شما" title-en="Your Skills" />
|
||||||
|
<BaseButton
|
||||||
|
text="افزودن مهارت"
|
||||||
|
custom-class="complete-info__add-btn"
|
||||||
|
@click="openModal('MissionaryAddSkillModal')"
|
||||||
|
>
|
||||||
|
<template #appendIcon>
|
||||||
|
<SvgIcon name="caret-left" :size="14" color="#fff" />
|
||||||
|
</template>
|
||||||
|
</BaseButton>
|
||||||
|
</div>
|
||||||
|
<div v-if="store.skills.length > 0" class="complete-info__badges">
|
||||||
|
<Badge
|
||||||
|
v-for="skill in store.skills"
|
||||||
|
:key="skill.title"
|
||||||
|
variant="neutral"
|
||||||
|
:label="skill.title"
|
||||||
|
:value="SKILL_LEVELS[skill.level] || skill.level"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="complete-info__section">
|
||||||
|
<div class="complete-info__section-head">
|
||||||
|
<LineTitleBlock title="سوابق کاری" title-en="Work Experience" />
|
||||||
|
<BaseButton
|
||||||
|
text="افـــــــــــزودن"
|
||||||
|
custom-class="complete-info__add-btn"
|
||||||
|
@click="openModal('MissionaryAddWorkExperienceModal')"
|
||||||
|
>
|
||||||
|
<template #appendIcon>
|
||||||
|
<SvgIcon name="caret-left" :size="14" color="#fff" />
|
||||||
|
</template>
|
||||||
|
</BaseButton>
|
||||||
|
</div>
|
||||||
|
<div v-if="store.workExperiences.length > 0" class="complete-info__entries">
|
||||||
|
<EntryCard
|
||||||
|
v-for="(item, index) in store.workExperiences"
|
||||||
|
:key="index"
|
||||||
|
:title="item.jobTitle"
|
||||||
|
:subtitle="rangeSubtitle(item.organizationName, item.startYear, item.endYear)"
|
||||||
|
@edit="openModal('MissionaryAddWorkExperienceModal', { entry: item, index })"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="complete-info__section">
|
||||||
|
<div class="complete-info__section-head">
|
||||||
|
<LineTitleBlock title="سوابق علمی" title-en="Education History" />
|
||||||
|
<BaseButton
|
||||||
|
text="افـــــــــــزودن"
|
||||||
|
custom-class="complete-info__add-btn"
|
||||||
|
@click="openModal('MissionaryAddEducationModal')"
|
||||||
|
>
|
||||||
|
<template #appendIcon>
|
||||||
|
<SvgIcon name="caret-left" :size="14" color="#fff" />
|
||||||
|
</template>
|
||||||
|
</BaseButton>
|
||||||
|
</div>
|
||||||
|
<div v-if="store.educationHistory.length > 0" class="complete-info__entries">
|
||||||
|
<EntryCard
|
||||||
|
v-for="(item, index) in store.educationHistory"
|
||||||
|
:key="index"
|
||||||
|
:title="educationTitle(item)"
|
||||||
|
:subtitle="rangeSubtitle(item.universityName, item.startYear, item.endYear)"
|
||||||
|
@edit="openModal('MissionaryAddEducationModal', { entry: item, index })"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="complete-info__section">
|
||||||
|
<div class="complete-info__section-head">
|
||||||
|
<LineTitleBlock title="گواهی ها و تقدیرنامه ها" title-en="Certificates" />
|
||||||
|
<BaseButton
|
||||||
|
text="افـــــــــــزودن"
|
||||||
|
custom-class="complete-info__add-btn"
|
||||||
|
@click="openModal('MissionaryAddCertificateModal')"
|
||||||
|
>
|
||||||
|
<template #appendIcon>
|
||||||
|
<SvgIcon name="caret-left" :size="14" color="#fff" />
|
||||||
|
</template>
|
||||||
|
</BaseButton>
|
||||||
|
</div>
|
||||||
|
<div v-if="store.certificates.length > 0" class="complete-info__entries">
|
||||||
|
<EntryCard
|
||||||
|
v-for="(item, index) in store.certificates"
|
||||||
|
:key="index"
|
||||||
|
:title="item.title"
|
||||||
|
:subtitle="rangeSubtitle(item.universityName, item.startYear, item.endYear)"
|
||||||
|
@edit="openModal('MissionaryAddCertificateModal', { entry: item, index })"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div class="complete-info__divider" />
|
||||||
|
|
||||||
|
<footer class="complete-info__footer">
|
||||||
|
<BaseButton
|
||||||
|
text="ذخیره اطلاعات"
|
||||||
|
custom-class="complete-info__submit"
|
||||||
|
:loading="saveMutation.isPending.value"
|
||||||
|
@click="onSave(close)"
|
||||||
|
>
|
||||||
|
<template #appendIcon>
|
||||||
|
<SvgIcon name="arrow-left" :size="18" color="#fff" />
|
||||||
|
</template>
|
||||||
|
</BaseButton>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</BasicModal>
|
||||||
|
|
||||||
|
<AddSkillModal v-if="isModal('MissionaryAddSkillModal')" />
|
||||||
|
<AddWorkExperienceModal v-if="isModal('MissionaryAddWorkExperienceModal')" />
|
||||||
|
<AddEducationModal v-if="isModal('MissionaryAddEducationModal')" />
|
||||||
|
<AddCertificateModal v-if="isModal('MissionaryAddCertificateModal')" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { watch } from 'vue'
|
||||||
|
import { toast } from 'vue3-toastify'
|
||||||
|
import Badge from '@/components/Badge.vue'
|
||||||
|
import useModal from '@/composables/useModal'
|
||||||
|
import BasicModal from '@/components/BasicModal.vue'
|
||||||
|
import BaseButton from '@/components/BaseButton.vue'
|
||||||
|
import SvgIcon from '@/components/icons/SvgIcon.vue'
|
||||||
|
import { useQueryClient } from '@tanstack/vue-query'
|
||||||
|
import LineTitleBlock from '@/components/LineTitleBlock.vue'
|
||||||
|
import TextareaField from '@/components/form/TextareaField.vue'
|
||||||
|
import { useCompleteInfoStore } from '@/features/missionary/store/complete-info'
|
||||||
|
import EntryCard from '@/features/missionary/components/modals/completeInfo/EntryCard.vue'
|
||||||
|
import { EDUCATION_DEGREES, SKILL_LEVELS } from '@/features/missionary/constants/complete-info'
|
||||||
|
import AddSkillModal from '@/features/missionary/components/modals/completeInfo/AddSkillModal.vue'
|
||||||
|
import AddEducationModal from '@/features/missionary/components/modals/completeInfo/AddEducationModal.vue'
|
||||||
|
import {
|
||||||
|
authKeys,
|
||||||
|
useGetRegisterDataQuery,
|
||||||
|
useSaveRegisterDataMutation,
|
||||||
|
} from '@/services/query/auth'
|
||||||
|
import AddCertificateModal from '@/features/missionary/components/modals/completeInfo/AddCertificateModal.vue'
|
||||||
|
import AddWorkExperienceModal from '@/features/missionary/components/modals/completeInfo/AddWorkExperienceModal.vue'
|
||||||
|
|
||||||
|
defineOptions({ name: 'MissionaryCompleteInfoModal' })
|
||||||
|
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
const { openModal, isModal } = useModal()
|
||||||
|
|
||||||
|
const store = useCompleteInfoStore()
|
||||||
|
store.reset()
|
||||||
|
|
||||||
|
const { data: registerData } = useGetRegisterDataQuery()
|
||||||
|
watch(
|
||||||
|
registerData,
|
||||||
|
(value) => {
|
||||||
|
if (value && Object.keys(value).length > 0) store.hydrateFromRegisterData(value)
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
const rangeSubtitle = (place, startYear, endYear) => {
|
||||||
|
const parts = []
|
||||||
|
if (place) parts.push(place)
|
||||||
|
if (startYear) parts.push(`از ${startYear} تا ${endYear || 'حالا'}`)
|
||||||
|
return parts.join(' | ')
|
||||||
|
}
|
||||||
|
|
||||||
|
const educationTitle = (item) => {
|
||||||
|
const degree = EDUCATION_DEGREES[item.degree] || item.degree || ''
|
||||||
|
return [degree, item.fieldOfStudy].filter(Boolean).join(': ')
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveMutation = useSaveRegisterDataMutation()
|
||||||
|
|
||||||
|
const onSave = async (close) => {
|
||||||
|
try {
|
||||||
|
await saveMutation.mutateAsync(store.buildPayload())
|
||||||
|
await queryClient.invalidateQueries({ queryKey: authKeys.registerData() })
|
||||||
|
toast.success('اطلاعات با موفقیت ذخیره شد.')
|
||||||
|
close?.()
|
||||||
|
} catch {
|
||||||
|
toast.error('ذخیره اطلاعات با خطا مواجه شد.')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.complete-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1.25rem;
|
||||||
|
text-align: start;
|
||||||
|
padding-block: 0.25rem 0.5rem;
|
||||||
|
|
||||||
|
&__section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__section-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__add-btn {
|
||||||
|
min-width: 8rem;
|
||||||
|
height: 2.25rem;
|
||||||
|
padding: 0 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__badges {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__entries {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.625rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__divider {
|
||||||
|
border-block-end: 1px solid #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__submit {
|
||||||
|
min-width: 14rem;
|
||||||
|
padding: 0 1.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
<template>
|
||||||
|
<div class="entry-card">
|
||||||
|
<div class="entry-card__info">
|
||||||
|
<p class="entry-card__title">{{ title }}</p>
|
||||||
|
<p class="entry-card__subtitle">{{ subtitle }}</p>
|
||||||
|
</div>
|
||||||
|
<CircleButton tooltip="ویرایش" bg-color="rgba(0, 112, 116, 0.06)" @click="emit('edit')">
|
||||||
|
<template #icon>
|
||||||
|
<SvgIcon name="pencil" :size="16" color="#007074" />
|
||||||
|
</template>
|
||||||
|
</CircleButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import SvgIcon from '@/components/icons/SvgIcon.vue'
|
||||||
|
import CircleButton from '@/components/CircleButton.vue'
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
title: { type: String, default: '' },
|
||||||
|
subtitle: { type: String, default: '' },
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['edit'])
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.entry-card {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 0.75rem;
|
||||||
|
padding: 0.875rem 1.25rem;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 0.875rem;
|
||||||
|
box-shadow: 0 6.75px 19.425px rgba(0, 0, 0, 4%);
|
||||||
|
|
||||||
|
&__info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.25rem;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
font-family: var(--font-family-fa);
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: #4b4b4b;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__subtitle {
|
||||||
|
font-family: var(--font-family-fa);
|
||||||
|
font-weight: 300;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
color: #9a9a9a;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
export const SKILL_LEVELS = Object.freeze({
|
||||||
|
beginner: 'مقدماتی',
|
||||||
|
intermediate: 'متوسط',
|
||||||
|
advanced: 'پیشرفته',
|
||||||
|
})
|
||||||
|
|
||||||
|
export const EDUCATION_DEGREES = Object.freeze({
|
||||||
|
associate: 'کاردانی',
|
||||||
|
bachelor: 'کارشناسی',
|
||||||
|
master: 'کارشناسی ارشد',
|
||||||
|
phd: 'دکترا',
|
||||||
|
})
|
||||||
|
|
||||||
|
export const MONTHS = Object.freeze([
|
||||||
|
{ value: 1, label: 'فروردین' },
|
||||||
|
{ value: 2, label: 'اردیبهشت' },
|
||||||
|
{ value: 3, label: 'خرداد' },
|
||||||
|
{ value: 4, label: 'تیر' },
|
||||||
|
{ value: 5, label: 'مرداد' },
|
||||||
|
{ value: 6, label: 'شهریور' },
|
||||||
|
{ value: 7, label: 'مهر' },
|
||||||
|
{ value: 8, label: 'آبان' },
|
||||||
|
{ value: 9, label: 'آذر' },
|
||||||
|
{ value: 10, label: 'دی' },
|
||||||
|
{ value: 11, label: 'بهمن' },
|
||||||
|
{ value: 12, label: 'اسفند' },
|
||||||
|
])
|
||||||
|
|
||||||
|
export const SKILL_SUGGESTIONS = Object.freeze([
|
||||||
|
'زبان انگلیسی',
|
||||||
|
'زبان عربی',
|
||||||
|
'تدریس قرآن',
|
||||||
|
'تدریس احکام',
|
||||||
|
'روضهخوانی',
|
||||||
|
'مداحی',
|
||||||
|
'سخنرانی',
|
||||||
|
'مشاوره خانواده',
|
||||||
|
'مشاوره تحصیلی',
|
||||||
|
'قصهگویی',
|
||||||
|
'نویسندگی',
|
||||||
|
'خطاطی',
|
||||||
|
'نقاشی',
|
||||||
|
'طراحی دکوراسیون',
|
||||||
|
'طراحی گرافیک',
|
||||||
|
'تولید محتوا',
|
||||||
|
'تدوین ویدئو',
|
||||||
|
'عکاسی',
|
||||||
|
'مدیریت فضای مجازی',
|
||||||
|
'کار با کامپیوتر',
|
||||||
|
])
|
||||||
@@ -1 +1,7 @@
|
|||||||
export { default as MissionaryRequestDetailsModal } from './components/modals/MissionaryRequestDetailsModal.vue'
|
export { default as DispatchDetailsModal } from './components/modals/DispatchDetailsModal.vue'
|
||||||
|
export { default as MissionaryDispatchItem } from './components/MissionaryDispatchItem.vue'
|
||||||
|
export { default as MissionaryNarrativeItem } from './components/MissionaryNarrativeItem.vue'
|
||||||
|
export { default as MissionaryRequestItem } from './components/MissionaryRequestItem.vue'
|
||||||
|
export { default as MissionaryUserCard } from './components/MissionaryUserCard.vue'
|
||||||
|
export { default as NarrativeDetailsModal } from './components/modals/NarrativeDetailsModal.vue'
|
||||||
|
export { default as RequestDetailsModal } from './components/modals/RequestDetailsModal.vue'
|
||||||
|
|||||||
@@ -1,73 +1,277 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="missionary-requests-page">
|
<div class="student-missionary">
|
||||||
<BoxedIconTitleBlock
|
<BoxedIconTitleBlock
|
||||||
class="missionary-requests-page__heading"
|
class="student-missionary__heading"
|
||||||
title="لیست درخواست ها"
|
title="سامانه اعزام"
|
||||||
desc="در این قسمت میتوانید درخواستهای اعزام خود را مدیریت کنید"
|
desc="در این قسمت شما میتوانید درخواست های اعزام، تاریخچه اعزام و... را مشاهده کنید."
|
||||||
>
|
>
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<SvgIcon name="user-sound" :size="24" color="var(--color-primary)" />
|
<SvgIcon name="paper-plane-right" :size="24" color="var(--color-primary)" />
|
||||||
</template>
|
</template>
|
||||||
</BoxedIconTitleBlock>
|
</BoxedIconTitleBlock>
|
||||||
|
|
||||||
<SimpleTitleIconBlock title="لیست همه درخواستها" class="missionary-requests-page__list-title">
|
<MissionaryUserCard
|
||||||
<template #header-icon>
|
:profile="profile"
|
||||||
<SvgIcon name="list-bullets" :size="16" color="#bcbcbc" />
|
@complete-info="onCompleteInfo"
|
||||||
|
@settings="onProfileSettings"
|
||||||
|
@add-narrative="onAddNarrative"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TabsBlock v-model="activeTab" :tabs="tabs">
|
||||||
|
<template #my-requests>
|
||||||
|
<SkeletonLoaderBlock v-if="requestsLoading" :rows="3" :cols-per-row="1" />
|
||||||
|
<div v-else-if="requests.length > 0">
|
||||||
|
<MissionaryRequestItem
|
||||||
|
v-for="request in requests"
|
||||||
|
:key="request.id"
|
||||||
|
:request="request"
|
||||||
|
@show-details="onRequestDetails"
|
||||||
|
@change-status="onAskChangeStatus"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<NoItems v-else title="موردی نیست" desc="هنوز درخواستی ثبت نکردهاید." />
|
||||||
|
<PaginationBlock :pagination="requestsMeta" @update:page="setRequestsPage" />
|
||||||
</template>
|
</template>
|
||||||
</SimpleTitleIconBlock>
|
|
||||||
|
|
||||||
<SkeletonLoaderBlock v-if="isLoading" :rows="6" :cols-per-row="1" />
|
<template #history>
|
||||||
<div v-else-if="requests.length > 0">
|
<SkeletonLoaderBlock v-if="dispatchesLoading" :rows="3" :cols-per-row="1" />
|
||||||
<MissionaryRequestItem
|
<div v-else-if="dispatches.length > 0">
|
||||||
v-for="request in requests"
|
<MissionaryDispatchItem
|
||||||
:key="request.id"
|
v-for="dispatch in dispatches"
|
||||||
:request="request"
|
:key="dispatch.id"
|
||||||
@show-details="onShowDetails"
|
:dispatch="dispatch"
|
||||||
@change-status="onAskChangeStatus"
|
@show-details="onDispatchDetails"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<NoItems v-else title="متاسفیم" desc="درخواستی برای نمایش وجود ندارد." />
|
<NoItems v-else title="موردی نیست" desc="تاریخچه اعزامی موجود نیست." />
|
||||||
|
<PaginationBlock :pagination="dispatchesMeta" @update:page="setDispatchesPage" />
|
||||||
|
</template>
|
||||||
|
|
||||||
<MissionaryRequestDetailsModal v-if="isModal('MissionaryRequestDetailsModal')" />
|
<template #narratives>
|
||||||
|
<SkeletonLoaderBlock v-if="narrativesLoading" :rows="3" :cols-per-row="1" />
|
||||||
|
<div v-else-if="narratives.length > 0">
|
||||||
|
<MissionaryNarrativeItem
|
||||||
|
v-for="narrative in narratives"
|
||||||
|
:key="narrative.id"
|
||||||
|
:narrative="narrative"
|
||||||
|
@show-details="onNarrativeDetails"
|
||||||
|
@edit="onEditNarrative"
|
||||||
|
@delete="onAskDeleteNarrative"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<NoItems v-else title="موردی نیست" desc="هنوز روایتی ثبت نکردهاید." />
|
||||||
|
<PaginationBlock :pagination="narrativesMeta" @update:page="setNarrativesPage" />
|
||||||
|
</template>
|
||||||
|
</TabsBlock>
|
||||||
|
|
||||||
|
<RequestDetailsModal v-if="isModal('RequestDetailsModal')" />
|
||||||
|
<DispatchDetailsModal v-if="isModal('DispatchDetailsModal')" />
|
||||||
|
<NarrativeDetailsModal v-if="isModal('NarrativeDetailsModal')" />
|
||||||
|
<CreateNarrativeModal v-if="isModal('CreateNarrativeModal')" />
|
||||||
|
<CompleteInfoModal v-if="isModal('MissionaryCompleteInfoModal')" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed } from 'vue'
|
import { computed, ref, watch } from 'vue'
|
||||||
|
import useAuth from '@/composables/useAuth'
|
||||||
import useModal from '@/composables/useModal'
|
import useModal from '@/composables/useModal'
|
||||||
import { useQueryClient } from '@tanstack/vue-query'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
import { MISSIONARY_REQUEST_STATUS } from '@/enums'
|
||||||
import SvgIcon from '@/components/icons/SvgIcon.vue'
|
import SvgIcon from '@/components/icons/SvgIcon.vue'
|
||||||
|
import { useQueryClient } from '@tanstack/vue-query'
|
||||||
import NoItems from '@/components/blocks/NoItems.vue'
|
import NoItems from '@/components/blocks/NoItems.vue'
|
||||||
|
import TabsBlock from '@/components/blocks/TabsBlock.vue'
|
||||||
|
import { usePagination } from '@/composables/usePagination'
|
||||||
|
import PaginationBlock from '@/components/blocks/PaginationBlock.vue'
|
||||||
|
import { formatJalaaliDate, formatJalaaliDateTime } from '@/utils/date-utils'
|
||||||
import BoxedIconTitleBlock from '@/components/blocks/BoxedIconTitleBlock.vue'
|
import BoxedIconTitleBlock from '@/components/blocks/BoxedIconTitleBlock.vue'
|
||||||
import SkeletonLoaderBlock from '@/components/blocks/SkeletonLoaderBlock.vue'
|
import SkeletonLoaderBlock from '@/components/blocks/SkeletonLoaderBlock.vue'
|
||||||
import SimpleTitleIconBlock from '@/components/blocks/SimpleTitleIconBlock.vue'
|
import MissionaryUserCard from '@/features/missionary/components/MissionaryUserCard.vue'
|
||||||
import MissionaryRequestItem from '@/features/missionary/components/MissionaryRequestItem.vue'
|
import MissionaryRequestItem from '@/features/missionary/components/MissionaryRequestItem.vue'
|
||||||
import MissionaryRequestDetailsModal from '@/features/missionary/components/modals/MissionaryRequestDetailsModal.vue'
|
import MissionaryDispatchItem from '@/features/missionary/components/MissionaryDispatchItem.vue'
|
||||||
|
import RequestDetailsModal from '@/features/missionary/components/modals/RequestDetailsModal.vue'
|
||||||
|
import MissionaryNarrativeItem from '@/features/missionary/components/MissionaryNarrativeItem.vue'
|
||||||
|
import DispatchDetailsModal from '@/features/missionary/components/modals/DispatchDetailsModal.vue'
|
||||||
|
import CreateNarrativeModal from '@/features/missionary/components/modals/CreateNarrativeModal.vue'
|
||||||
|
import NarrativeDetailsModal from '@/features/missionary/components/modals/NarrativeDetailsModal.vue'
|
||||||
|
import CompleteInfoModal from '@/features/missionary/components/modals/completeInfo/CompleteInfoModal.vue'
|
||||||
|
import {
|
||||||
|
missionaryMemoriesKeys,
|
||||||
|
useDeleteMissionaryMemoryMutation,
|
||||||
|
useMissionaryMemoriesListQuery,
|
||||||
|
} from '@/services/query/missionary-memories'
|
||||||
import {
|
import {
|
||||||
missionaryRequestsKeys,
|
missionaryRequestsKeys,
|
||||||
useChangeMissionaryRequestStatusMutation,
|
useChangeMissionaryRequestStatusMutation,
|
||||||
useMissionaryRequestsListQuery,
|
useMissionaryRequestsListQuery,
|
||||||
} from '@/services/query/missionary-requests'
|
} from '@/services/query/missionary-requests'
|
||||||
|
|
||||||
const queryClient = useQueryClient()
|
const { user } = useAuth()
|
||||||
const { openModal, isModal } = useModal()
|
const { openModal, isModal } = useModal()
|
||||||
|
|
||||||
const { data, isLoading } = useMissionaryRequestsListQuery()
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
const requests = computed(() => data.value ?? [])
|
const tabs = [
|
||||||
|
{ name: 'my-requests', label: 'درخواست های من', icon: 'list-bullets' },
|
||||||
|
{ name: 'history', label: 'تاریخچه اعزام ها', icon: 'paper-plane-right' },
|
||||||
|
{ name: 'narratives', label: 'روایت های من', icon: 'pencil' },
|
||||||
|
]
|
||||||
|
|
||||||
const invalidate = () =>
|
const isValidTab = (name) => tabs.some((tab) => tab.name === name)
|
||||||
queryClient.invalidateQueries({ queryKey: missionaryRequestsKeys.all, refetchType: 'all' })
|
const activeTab = ref(isValidTab(route.query.tab) ? route.query.tab : 'my-requests')
|
||||||
|
|
||||||
|
watch(activeTab, (tab) => {
|
||||||
|
if (route.query.tab === tab) return
|
||||||
|
router.replace({ query: { ...route.query, tab } }).catch(() => {})
|
||||||
|
})
|
||||||
|
|
||||||
|
const requestsFilters = ref({})
|
||||||
|
const dispatchesFilters = ref({ status: 'accepted' })
|
||||||
|
const narrativesFilters = ref({})
|
||||||
|
|
||||||
|
const { pagination: requestsPagination, setPage: setRequestsPage } = usePagination({
|
||||||
|
page: 1,
|
||||||
|
perPage: 10,
|
||||||
|
})
|
||||||
|
const { pagination: dispatchesPagination, setPage: setDispatchesPage } = usePagination({
|
||||||
|
page: 1,
|
||||||
|
perPage: 10,
|
||||||
|
})
|
||||||
|
const { pagination: narrativesPagination, setPage: setNarrativesPage } = usePagination({
|
||||||
|
page: 1,
|
||||||
|
perPage: 10,
|
||||||
|
})
|
||||||
|
|
||||||
|
const { data: requestsData, isLoading: requestsLoading } = useMissionaryRequestsListQuery(
|
||||||
|
requestsFilters,
|
||||||
|
requestsPagination,
|
||||||
|
{
|
||||||
|
enabled: () => activeTab.value === 'my-requests',
|
||||||
|
keepPreviousData: true,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const { data: dispatchesData, isLoading: dispatchesLoading } = useMissionaryRequestsListQuery(
|
||||||
|
dispatchesFilters,
|
||||||
|
dispatchesPagination,
|
||||||
|
{
|
||||||
|
enabled: () => activeTab.value === 'history',
|
||||||
|
keepPreviousData: true,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const { data: narrativesData, isLoading: narrativesLoading } = useMissionaryMemoriesListQuery(
|
||||||
|
narrativesFilters,
|
||||||
|
narrativesPagination,
|
||||||
|
{
|
||||||
|
enabled: () => activeTab.value === 'narratives',
|
||||||
|
keepPreviousData: true,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const requestStatusLabel = (status) => MISSIONARY_REQUEST_STATUS[status] || status || '—'
|
||||||
|
|
||||||
|
const toRequestView = (request) => ({
|
||||||
|
...request,
|
||||||
|
code: request.id,
|
||||||
|
requestNumber: request.id,
|
||||||
|
requestDate: formatJalaaliDate(request.requestedDate),
|
||||||
|
statusLabel: requestStatusLabel(request.status),
|
||||||
|
phone: request.requesterPhone,
|
||||||
|
address: request.location,
|
||||||
|
})
|
||||||
|
|
||||||
|
const toDispatchView = (request) => ({
|
||||||
|
id: request.id,
|
||||||
|
code: request.id,
|
||||||
|
title: request.title,
|
||||||
|
missionName: request.title,
|
||||||
|
location: request.location,
|
||||||
|
dispatchDate: formatJalaaliDate(request.requestedDate),
|
||||||
|
statusLabel: requestStatusLabel(request.status),
|
||||||
|
requesterName: request.requesterName,
|
||||||
|
address: request.location,
|
||||||
|
result: request.description,
|
||||||
|
})
|
||||||
|
|
||||||
|
const NARRATIVE_KIND_META = {
|
||||||
|
manuscript: { label: 'دست نوشته', tone: 'pink' },
|
||||||
|
dispatched: { label: 'اعزام شــــده', tone: 'gold' },
|
||||||
|
}
|
||||||
|
|
||||||
|
const toNarrativeView = (narrative) => {
|
||||||
|
const linkedKind =
|
||||||
|
narrative.missionaryRequestId || narrative.missionaryRequest ? 'dispatched' : 'manuscript'
|
||||||
|
const kind = narrative.type || narrative.kind || linkedKind
|
||||||
|
const kindMeta = NARRATIVE_KIND_META[kind] || NARRATIVE_KIND_META.manuscript
|
||||||
|
return {
|
||||||
|
...narrative,
|
||||||
|
code: narrative.id,
|
||||||
|
submittedAt: formatJalaaliDateTime(narrative.createdAt),
|
||||||
|
kind,
|
||||||
|
kindLabel: kindMeta.label,
|
||||||
|
kindTone: kindMeta.tone,
|
||||||
|
body: narrative.description || narrative.content || narrative.body || '',
|
||||||
|
likes: narrative.likesCount ?? narrative.likes ?? 0,
|
||||||
|
comments: narrative.commentsCount ?? narrative.comments ?? 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const requests = computed(() =>
|
||||||
|
(requestsData.value?.data?.items ?? []).map((r) => toRequestView(r))
|
||||||
|
)
|
||||||
|
const dispatches = computed(() =>
|
||||||
|
(dispatchesData.value?.data?.items ?? []).map((r) => toDispatchView(r))
|
||||||
|
)
|
||||||
|
const narratives = computed(() =>
|
||||||
|
(narrativesData.value?.data?.items ?? []).map((n) => toNarrativeView(n))
|
||||||
|
)
|
||||||
|
|
||||||
|
const membershipDays = computed(() => {
|
||||||
|
const created = new Date(user.value?.createdAt)
|
||||||
|
if (Number.isNaN(created.getTime())) return '—'
|
||||||
|
return Math.max(0, Math.floor((Date.now() - created.getTime()) / 86_400_000))
|
||||||
|
})
|
||||||
|
|
||||||
|
const profile = computed(() => ({
|
||||||
|
fullName: user.value?.name || '—',
|
||||||
|
city: user.value?.city?.name || '',
|
||||||
|
avatar: user.value?.avatarUrl || '',
|
||||||
|
phone: user.value?.phone || '',
|
||||||
|
rating: user.value?.rating ?? '',
|
||||||
|
membershipDays: membershipDays.value,
|
||||||
|
requestsCount: requestsData.value?.meta?.total ?? '',
|
||||||
|
}))
|
||||||
|
|
||||||
|
const requestsMeta = computed(() => ({
|
||||||
|
page: requestsPagination.value.page,
|
||||||
|
perPage: requestsPagination.value.perPage,
|
||||||
|
...requestsData.value?.meta,
|
||||||
|
}))
|
||||||
|
const dispatchesMeta = computed(() => ({
|
||||||
|
page: dispatchesPagination.value.page,
|
||||||
|
perPage: dispatchesPagination.value.perPage,
|
||||||
|
...dispatchesData.value?.meta,
|
||||||
|
}))
|
||||||
|
const narrativesMeta = computed(() => ({
|
||||||
|
page: narrativesPagination.value.page,
|
||||||
|
perPage: narrativesPagination.value.perPage,
|
||||||
|
...narrativesData.value?.meta,
|
||||||
|
}))
|
||||||
|
|
||||||
|
const onCompleteInfo = () => {
|
||||||
|
openModal('MissionaryCompleteInfoModal')
|
||||||
|
}
|
||||||
|
|
||||||
|
const onProfileSettings = () => {}
|
||||||
|
|
||||||
|
const queryClient = useQueryClient()
|
||||||
const statusMutation = useChangeMissionaryRequestStatusMutation()
|
const statusMutation = useChangeMissionaryRequestStatusMutation()
|
||||||
|
|
||||||
const changeStatus = (id, status) =>
|
const invalidateRequests = () =>
|
||||||
statusMutation.mutate({ id, payload: { status } }, { onSuccess: invalidate })
|
queryClient.invalidateQueries({ queryKey: missionaryRequestsKeys.all, refetchType: 'all' })
|
||||||
|
|
||||||
const onShowDetails = (request) => {
|
|
||||||
openModal('MissionaryRequestDetailsModal', { id: request.id })
|
|
||||||
if (request.status === 'pending') changeStatus(request.id, 'seen')
|
|
||||||
}
|
|
||||||
|
|
||||||
const STATUS_CONFIRMS = {
|
const STATUS_CONFIRMS = {
|
||||||
accepted: { title: 'پذیرش درخواست', verb: 'پذیرش' },
|
accepted: { title: 'پذیرش درخواست', verb: 'پذیرش' },
|
||||||
@@ -81,23 +285,53 @@ const onAskChangeStatus = ({ id, status }) => {
|
|||||||
openModal('ConfirmModal', {
|
openModal('ConfirmModal', {
|
||||||
title: config.title,
|
title: config.title,
|
||||||
message: `آیا از ${config.verb} درخواست <strong>${request?.title || ''}</strong> مطمئن هستید؟`,
|
message: `آیا از ${config.verb} درخواست <strong>${request?.title || ''}</strong> مطمئن هستید؟`,
|
||||||
onConfirm: () => changeStatus(id, status),
|
onConfirm: () =>
|
||||||
|
statusMutation.mutate({ id, payload: { status } }, { onSuccess: invalidateRequests }),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const deleteMemoryMutation = useDeleteMissionaryMemoryMutation()
|
||||||
|
|
||||||
|
const invalidateMemories = () =>
|
||||||
|
queryClient.invalidateQueries({ queryKey: missionaryMemoriesKeys.all, refetchType: 'all' })
|
||||||
|
|
||||||
|
const onAddNarrative = () => {
|
||||||
|
openModal('CreateNarrativeModal')
|
||||||
|
}
|
||||||
|
|
||||||
|
const onEditNarrative = (narrative) => {
|
||||||
|
openModal('CreateNarrativeModal', { memory: narrative })
|
||||||
|
}
|
||||||
|
|
||||||
|
const onAskDeleteNarrative = (narrative) => {
|
||||||
|
openModal('ConfirmModal', {
|
||||||
|
title: 'حذف روایت',
|
||||||
|
message: `بعد از حذف امکان بازگشت وجود ندارد، آیا مطمئن هستید که میخواهید <strong>${narrative.title}</strong> را حذف کنید؟`,
|
||||||
|
onConfirm: () => deleteMemoryMutation.mutate(narrative.id, { onSuccess: invalidateMemories }),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const onRequestDetails = (request) => {
|
||||||
|
openModal('RequestDetailsModal', request)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onDispatchDetails = (dispatch) => {
|
||||||
|
openModal('DispatchDetailsModal', dispatch)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onNarrativeDetails = (narrative) => {
|
||||||
|
openModal('NarrativeDetailsModal', narrative)
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.missionary-requests-page {
|
.student-missionary {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
|
|
||||||
&__heading {
|
&__heading {
|
||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.75rem;
|
||||||
}
|
|
||||||
|
|
||||||
&__list-title {
|
|
||||||
margin-bottom: 0.375rem;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
import { ref } from 'vue'
|
||||||
|
import { defineStore } from 'pinia'
|
||||||
|
|
||||||
|
const upsert =
|
||||||
|
(listRef) =>
|
||||||
|
(entry, index = null) => {
|
||||||
|
if (index == null || index < 0) {
|
||||||
|
listRef.value = [...listRef.value, entry]
|
||||||
|
} else {
|
||||||
|
listRef.value = listRef.value.map((item, i) => (i === index ? entry : item))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useCompleteInfoStore = defineStore('missionaryCompleteInfo', () => {
|
||||||
|
const introduction = ref('')
|
||||||
|
const skills = ref([])
|
||||||
|
const workExperiences = ref([])
|
||||||
|
const educationHistory = ref([])
|
||||||
|
const certificates = ref([])
|
||||||
|
|
||||||
|
const reset = () => {
|
||||||
|
introduction.value = ''
|
||||||
|
skills.value = []
|
||||||
|
workExperiences.value = []
|
||||||
|
educationHistory.value = []
|
||||||
|
certificates.value = []
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keys mirror what POST /me/register-data persists — flat `{ key: value }`
|
||||||
|
// where array values hold the section entries.
|
||||||
|
const hydrateFromRegisterData = (data = {}) => {
|
||||||
|
if (typeof data.introduction === 'string') introduction.value = data.introduction
|
||||||
|
if (Array.isArray(data.skills)) skills.value = data.skills
|
||||||
|
if (Array.isArray(data.workExperiences)) workExperiences.value = data.workExperiences
|
||||||
|
if (Array.isArray(data.educationHistory)) educationHistory.value = data.educationHistory
|
||||||
|
if (Array.isArray(data.certificates)) certificates.value = data.certificates
|
||||||
|
}
|
||||||
|
|
||||||
|
const addSkill = (skill) => {
|
||||||
|
if (skills.value.some((s) => s.title === skill.title)) return
|
||||||
|
skills.value = [...skills.value, skill]
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeSkill = (title) => {
|
||||||
|
skills.value = skills.value.filter((s) => s.title !== title)
|
||||||
|
}
|
||||||
|
|
||||||
|
const upsertWorkExperience = upsert(workExperiences)
|
||||||
|
const upsertEducation = upsert(educationHistory)
|
||||||
|
const upsertCertificate = upsert(certificates)
|
||||||
|
|
||||||
|
const buildPayload = () => ({
|
||||||
|
introduction: introduction.value,
|
||||||
|
skills: skills.value,
|
||||||
|
workExperiences: workExperiences.value,
|
||||||
|
educationHistory: educationHistory.value,
|
||||||
|
certificates: certificates.value,
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
introduction,
|
||||||
|
skills,
|
||||||
|
workExperiences,
|
||||||
|
educationHistory,
|
||||||
|
certificates,
|
||||||
|
reset,
|
||||||
|
hydrateFromRegisterData,
|
||||||
|
addSkill,
|
||||||
|
removeSkill,
|
||||||
|
upsertWorkExperience,
|
||||||
|
upsertEducation,
|
||||||
|
upsertCertificate,
|
||||||
|
buildPayload,
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -1,257 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="mreq" @click="emit('show-details', request)">
|
|
||||||
<div class="mreq__heading">
|
|
||||||
<span class="mreq__code">{{ request.code }}</span>
|
|
||||||
<div class="mreq__title-wrap">
|
|
||||||
<p class="mreq__title">{{ request.title }}</p>
|
|
||||||
<p class="mreq__subtitle">شماره درخواست : {{ request.requestNumber }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mreq__meta">
|
|
||||||
<div class="mreq__pill mreq__pill--date">
|
|
||||||
<span class="mreq__pill-label">تاریخ درخواست :</span>
|
|
||||||
<span class="mreq__pill-value">{{ request.requestDate }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="mreq__pill mreq__pill--name">
|
|
||||||
<span class="mreq__pill-label">نام درخواست کننده :</span>
|
|
||||||
<span class="mreq__pill-value">{{ request.requesterName }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="mreq__pill mreq__pill--status" :class="`mreq__pill--${tone}`">
|
|
||||||
<span class="mreq__dot" />
|
|
||||||
<span class="mreq__pill-value mreq__pill-value--status">{{ request.statusLabel }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mreq__actions" @click.stop>
|
|
||||||
<CircleButton bg-color="rgba(0, 112, 116, 0.08)" size="2rem">
|
|
||||||
<template #icon>
|
|
||||||
<SvgIcon name="check" :size="14" color="#007074" />
|
|
||||||
</template>
|
|
||||||
</CircleButton>
|
|
||||||
<CircleButton bg-color="rgba(243, 102, 117, 0.06)" size="2rem">
|
|
||||||
<template #icon>
|
|
||||||
<SvgIcon name="close" :size="14" color="var(--color-error)" />
|
|
||||||
</template>
|
|
||||||
</CircleButton>
|
|
||||||
<BaseButton
|
|
||||||
text="جزئیات درخواست"
|
|
||||||
custom-class="mreq__btn"
|
|
||||||
@click="emit('show-details', request)"
|
|
||||||
>
|
|
||||||
<template #appendIcon>
|
|
||||||
<SvgIcon name="arrow-left" :size="16" color="#fff" />
|
|
||||||
</template>
|
|
||||||
</BaseButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { computed } from 'vue'
|
|
||||||
import BaseButton from '@/components/BaseButton.vue'
|
|
||||||
import SvgIcon from '@/components/icons/SvgIcon.vue'
|
|
||||||
import CircleButton from '@/components/CircleButton.vue'
|
|
||||||
|
|
||||||
const TONE_MAP = {
|
|
||||||
rejected: 'danger',
|
|
||||||
approved: 'success',
|
|
||||||
pending: 'warning',
|
|
||||||
cancelled: 'neutral',
|
|
||||||
}
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
request: { type: Object, required: true },
|
|
||||||
})
|
|
||||||
|
|
||||||
const emit = defineEmits(['show-details'])
|
|
||||||
|
|
||||||
const tone = computed(() => TONE_MAP[props.request.status] || 'neutral')
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.mreq {
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.75rem;
|
|
||||||
padding: 1rem 1.25rem;
|
|
||||||
background: rgba(255, 255, 255, 58%);
|
|
||||||
box-shadow: 0 6.75px 19.425px rgba(0, 0, 0, 4%);
|
|
||||||
border-radius: 0.75rem;
|
|
||||||
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: 1.5rem;
|
|
||||||
padding: 1rem 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__heading {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.625rem;
|
|
||||||
|
|
||||||
@media (min-width: 1024px) {
|
|
||||||
min-width: 9rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__title-wrap {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-end;
|
|
||||||
text-align: end;
|
|
||||||
gap: 0.125rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__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: 400;
|
|
||||||
font-size: 0.7rem;
|
|
||||||
color: #4b4b4b;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__code {
|
|
||||||
font-family: var(--font-family-en);
|
|
||||||
font-weight: 800;
|
|
||||||
font-size: 2.75rem;
|
|
||||||
line-height: 1;
|
|
||||||
color: #f0f0f0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__meta {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
|
|
||||||
@media (min-width: 1024px) {
|
|
||||||
flex: 1;
|
|
||||||
flex-wrap: nowrap;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__pill {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.375rem;
|
|
||||||
padding: 0.3rem 0.875rem;
|
|
||||||
border-radius: 0.75rem;
|
|
||||||
background: rgba(107, 107, 107, 4%);
|
|
||||||
font-family: var(--font-family-fa);
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__pill-label {
|
|
||||||
font-weight: 400;
|
|
||||||
font-size: 0.65rem;
|
|
||||||
color: #535353;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__pill-value {
|
|
||||||
font-family: var(--font-family-en);
|
|
||||||
font-weight: 400;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
color: #5d5d5d;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__pill-value--status {
|
|
||||||
font-family: var(--font-family-fa);
|
|
||||||
font-weight: 400;
|
|
||||||
font-size: 0.65rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__dot {
|
|
||||||
width: 0.32rem;
|
|
||||||
height: 0.32rem;
|
|
||||||
border-radius: 9999px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__pill--status {
|
|
||||||
padding: 0.3rem 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__pill--danger {
|
|
||||||
background: rgba(243, 102, 117, 6%);
|
|
||||||
|
|
||||||
.mreq__pill-value {
|
|
||||||
color: #cc2831;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mreq__dot {
|
|
||||||
background: #cc2831;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__pill--success {
|
|
||||||
background: rgba(0, 112, 116, 6%);
|
|
||||||
|
|
||||||
.mreq__pill-value {
|
|
||||||
color: #007074;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mreq__dot {
|
|
||||||
background: #007074;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__pill--warning {
|
|
||||||
background: rgba(182, 132, 45, 7%);
|
|
||||||
|
|
||||||
.mreq__pill-value {
|
|
||||||
color: #b6842d;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mreq__dot {
|
|
||||||
background: #b6842d;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__pill--neutral {
|
|
||||||
background: rgba(107, 107, 107, 6%);
|
|
||||||
|
|
||||||
.mreq__pill-value {
|
|
||||||
color: #535353;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mreq__dot {
|
|
||||||
background: #989898;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__actions {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 6px;
|
|
||||||
|
|
||||||
@media (min-width: 1024px) {
|
|
||||||
justify-content: flex-start;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__btn {
|
|
||||||
min-width: 8rem;
|
|
||||||
height: 2rem;
|
|
||||||
padding: 0 1.25rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
export { default as DispatchDetailsModal } from './components/modals/DispatchDetailsModal.vue'
|
|
||||||
export { default as MissionaryDispatchItem } from './components/MissionaryDispatchItem.vue'
|
|
||||||
export { default as MissionaryNarrativeItem } from './components/MissionaryNarrativeItem.vue'
|
|
||||||
export { default as MissionaryRequestItem } from './components/MissionaryRequestItem.vue'
|
|
||||||
export { default as MissionaryUserCard } from './components/MissionaryUserCard.vue'
|
|
||||||
export { default as NarrativeDetailsModal } from './components/modals/NarrativeDetailsModal.vue'
|
|
||||||
export { default as RequestDetailsModal } from './components/modals/RequestDetailsModal.vue'
|
|
||||||
@@ -1,203 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="student-missionary">
|
|
||||||
<BoxedIconTitleBlock
|
|
||||||
class="student-missionary__heading"
|
|
||||||
title="سامانه اعزام"
|
|
||||||
desc="در این قسمت شما میتوانید درخواست های اعزام، تاریخچه اعزام و... را مشاهده کنید."
|
|
||||||
>
|
|
||||||
<template #icon>
|
|
||||||
<SvgIcon name="paper-plane-right" :size="24" color="var(--color-primary)" />
|
|
||||||
</template>
|
|
||||||
</BoxedIconTitleBlock>
|
|
||||||
|
|
||||||
<SkeletonLoaderBlock v-if="profileLoading && !profile" :rows="1" :cols-per-row="1" />
|
|
||||||
<MissionaryUserCard
|
|
||||||
v-else
|
|
||||||
:profile="profile || {}"
|
|
||||||
@complete-info="onCompleteInfo"
|
|
||||||
@settings="onProfileSettings"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<TabsBlock v-model="activeTab" :tabs="tabs">
|
|
||||||
<template #my-requests>
|
|
||||||
<SkeletonLoaderBlock v-if="requestsLoading" :rows="3" :cols-per-row="1" />
|
|
||||||
<div v-else-if="requests.length > 0">
|
|
||||||
<MissionaryRequestItem
|
|
||||||
v-for="request in requests"
|
|
||||||
:key="request.id"
|
|
||||||
:request="request"
|
|
||||||
@show-details="onRequestDetails"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<NoItems v-else title="موردی نیست" desc="هنوز درخواستی ثبت نکردهاید." />
|
|
||||||
<PaginationBlock :pagination="requestsMeta" @update:page="setRequestsPage" />
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #history>
|
|
||||||
<SkeletonLoaderBlock v-if="dispatchesLoading" :rows="3" :cols-per-row="1" />
|
|
||||||
<div v-else-if="dispatches.length > 0">
|
|
||||||
<MissionaryDispatchItem
|
|
||||||
v-for="dispatch in dispatches"
|
|
||||||
:key="dispatch.id"
|
|
||||||
:dispatch="dispatch"
|
|
||||||
@show-details="onDispatchDetails"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<NoItems v-else title="موردی نیست" desc="تاریخچه اعزامی موجود نیست." />
|
|
||||||
<PaginationBlock :pagination="dispatchesMeta" @update:page="setDispatchesPage" />
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #narratives>
|
|
||||||
<SkeletonLoaderBlock v-if="narrativesLoading" :rows="3" :cols-per-row="1" />
|
|
||||||
<div v-else-if="narratives.length > 0">
|
|
||||||
<MissionaryNarrativeItem
|
|
||||||
v-for="narrative in narratives"
|
|
||||||
:key="narrative.id"
|
|
||||||
:narrative="narrative"
|
|
||||||
@show-details="onNarrativeDetails"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<NoItems v-else title="موردی نیست" desc="هنوز روایتی ثبت نکردهاید." />
|
|
||||||
<PaginationBlock :pagination="narrativesMeta" @update:page="setNarrativesPage" />
|
|
||||||
</template>
|
|
||||||
</TabsBlock>
|
|
||||||
|
|
||||||
<RequestDetailsModal v-if="isModal('RequestDetailsModal')" />
|
|
||||||
<DispatchDetailsModal v-if="isModal('DispatchDetailsModal')" />
|
|
||||||
<NarrativeDetailsModal v-if="isModal('NarrativeDetailsModal')" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { computed, ref } from 'vue'
|
|
||||||
import { useRouter } from 'vue-router'
|
|
||||||
import useModal from '@/composables/useModal'
|
|
||||||
import SvgIcon from '@/components/icons/SvgIcon.vue'
|
|
||||||
import NoItems from '@/components/blocks/NoItems.vue'
|
|
||||||
import TabsBlock from '@/components/blocks/TabsBlock.vue'
|
|
||||||
import { usePagination } from '@/composables/usePagination'
|
|
||||||
import PaginationBlock from '@/components/blocks/PaginationBlock.vue'
|
|
||||||
import BoxedIconTitleBlock from '@/components/blocks/BoxedIconTitleBlock.vue'
|
|
||||||
import SkeletonLoaderBlock from '@/components/blocks/SkeletonLoaderBlock.vue'
|
|
||||||
import MissionaryUserCard from '@/features/student/missionary/components/MissionaryUserCard.vue'
|
|
||||||
import MissionaryRequestItem from '@/features/student/missionary/components/MissionaryRequestItem.vue'
|
|
||||||
import MissionaryDispatchItem from '@/features/student/missionary/components/MissionaryDispatchItem.vue'
|
|
||||||
import RequestDetailsModal from '@/features/student/missionary/components/modals/RequestDetailsModal.vue'
|
|
||||||
import MissionaryNarrativeItem from '@/features/student/missionary/components/MissionaryNarrativeItem.vue'
|
|
||||||
import DispatchDetailsModal from '@/features/student/missionary/components/modals/DispatchDetailsModal.vue'
|
|
||||||
import NarrativeDetailsModal from '@/features/student/missionary/components/modals/NarrativeDetailsModal.vue'
|
|
||||||
import {
|
|
||||||
useMissionaryProfileQuery,
|
|
||||||
useMissionaryDispatchesQuery,
|
|
||||||
useMissionaryNarrativesQuery,
|
|
||||||
useMissionaryRequestsQuery,
|
|
||||||
} from '@/services/query/student-missionary'
|
|
||||||
|
|
||||||
const router = useRouter()
|
|
||||||
const { openModal, isModal } = useModal()
|
|
||||||
|
|
||||||
const tabs = [
|
|
||||||
{ name: 'my-requests', label: 'درخواست های من', icon: 'list-bullets' },
|
|
||||||
{ name: 'history', label: 'تاریخچه اعزام ها', icon: 'paper-plane-right' },
|
|
||||||
{ name: 'narratives', label: 'روایت های من', icon: 'pencil' },
|
|
||||||
]
|
|
||||||
const activeTab = ref('my-requests')
|
|
||||||
|
|
||||||
const { data: profile, isLoading: profileLoading } = useMissionaryProfileQuery()
|
|
||||||
|
|
||||||
const requestsFilters = ref({})
|
|
||||||
const dispatchesFilters = ref({})
|
|
||||||
const narrativesFilters = ref({})
|
|
||||||
|
|
||||||
const { pagination: requestsPagination, setPage: setRequestsPage } = usePagination({
|
|
||||||
page: 1,
|
|
||||||
perPage: 10,
|
|
||||||
})
|
|
||||||
const { pagination: dispatchesPagination, setPage: setDispatchesPage } = usePagination({
|
|
||||||
page: 1,
|
|
||||||
perPage: 10,
|
|
||||||
})
|
|
||||||
const { pagination: narrativesPagination, setPage: setNarrativesPage } = usePagination({
|
|
||||||
page: 1,
|
|
||||||
perPage: 10,
|
|
||||||
})
|
|
||||||
|
|
||||||
const { data: requestsData, isLoading: requestsLoading } = useMissionaryRequestsQuery(
|
|
||||||
requestsFilters,
|
|
||||||
requestsPagination,
|
|
||||||
{
|
|
||||||
enabled: () => activeTab.value === 'my-requests',
|
|
||||||
keepPreviousData: true,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const { data: dispatchesData, isLoading: dispatchesLoading } = useMissionaryDispatchesQuery(
|
|
||||||
dispatchesFilters,
|
|
||||||
dispatchesPagination,
|
|
||||||
{
|
|
||||||
enabled: () => activeTab.value === 'history',
|
|
||||||
keepPreviousData: true,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const { data: narrativesData, isLoading: narrativesLoading } = useMissionaryNarrativesQuery(
|
|
||||||
narrativesFilters,
|
|
||||||
narrativesPagination,
|
|
||||||
{
|
|
||||||
enabled: () => activeTab.value === 'narratives',
|
|
||||||
keepPreviousData: true,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const requests = computed(() => requestsData.value?.data ?? [])
|
|
||||||
const dispatches = computed(() => dispatchesData.value?.data ?? [])
|
|
||||||
const narratives = computed(() => narrativesData.value?.data ?? [])
|
|
||||||
|
|
||||||
const requestsMeta = computed(() => ({
|
|
||||||
page: requestsPagination.value.page,
|
|
||||||
perPage: requestsPagination.value.perPage,
|
|
||||||
...requestsData.value?.meta,
|
|
||||||
}))
|
|
||||||
const dispatchesMeta = computed(() => ({
|
|
||||||
page: dispatchesPagination.value.page,
|
|
||||||
perPage: dispatchesPagination.value.perPage,
|
|
||||||
...dispatchesData.value?.meta,
|
|
||||||
}))
|
|
||||||
const narrativesMeta = computed(() => ({
|
|
||||||
page: narrativesPagination.value.page,
|
|
||||||
perPage: narrativesPagination.value.perPage,
|
|
||||||
...narrativesData.value?.meta,
|
|
||||||
}))
|
|
||||||
|
|
||||||
const onCompleteInfo = () => {
|
|
||||||
router.push({ name: 'student-profile' }).catch(() => {})
|
|
||||||
}
|
|
||||||
|
|
||||||
const onProfileSettings = () => {
|
|
||||||
router.push({ name: 'student-profile' }).catch(() => {})
|
|
||||||
}
|
|
||||||
|
|
||||||
const onRequestDetails = (request) => {
|
|
||||||
openModal('RequestDetailsModal', request)
|
|
||||||
}
|
|
||||||
|
|
||||||
const onDispatchDetails = (dispatch) => {
|
|
||||||
openModal('DispatchDetailsModal', dispatch)
|
|
||||||
}
|
|
||||||
|
|
||||||
const onNarrativeDetails = (narrative) => {
|
|
||||||
openModal('NarrativeDetailsModal', narrative)
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.student-missionary {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.5rem;
|
|
||||||
|
|
||||||
&__heading {
|
|
||||||
margin-bottom: 0.75rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -109,17 +109,6 @@ export default [
|
|||||||
subtitle: 'مهارتها و دورههای خود را دنبال کنید.',
|
subtitle: 'مهارتها و دورههای خود را دنبال کنید.',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: '/missionary',
|
|
||||||
name: 'student-missionary',
|
|
||||||
component: () => import('@/features/student/missionary/pages/StudentMissionaryPage.vue'),
|
|
||||||
meta: {
|
|
||||||
layout: 'student',
|
|
||||||
role: 'student',
|
|
||||||
title: 'سیستم مدیریت اعزام',
|
|
||||||
subtitle: 'سادهترین راه برای برنامهریزی و پایش مأموریتها',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: '/user/services',
|
path: '/user/services',
|
||||||
name: 'student-services',
|
name: 'student-services',
|
||||||
|
|||||||
@@ -31,7 +31,10 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="contentKind === 'audio'" class="ssd__audio">
|
<div v-else-if="contentKind === 'audio'" class="ssd__audio">
|
||||||
<VoiceRecorder :model-value="session.audioUrl" disabled />
|
<VoiceRecorder
|
||||||
|
model-value="http://api.tripwisedaily.ir/storage/pending/3/test2-AmQiegjc.mp3?expires=1783677165&signature=38104395400100f1bd74594bfc64ac462c14634a8c2bc4541c988766a08ea2bd"
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="contentKind === 'text'" class="ssd__text">
|
<div v-else-if="contentKind === 'text'" class="ssd__text">
|
||||||
<SvgIcon name="warning" :size="22" color="#b8b8b8" class="ssd__text-icon" />
|
<SvgIcon name="warning" :size="22" color="#b8b8b8" class="ssd__text-icon" />
|
||||||
|
|||||||
@@ -156,6 +156,12 @@ const menuItems = computed(() => [
|
|||||||
to: { name: 'admin-consultations' },
|
to: { name: 'admin-consultations' },
|
||||||
active: route.name === 'admin-consultations',
|
active: route.name === 'admin-consultations',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: 'مدیریت مبلغین',
|
||||||
|
icon: 'user-sound',
|
||||||
|
to: { name: 'admin-missionary-requests' },
|
||||||
|
active: route.name === 'admin-missionary-requests',
|
||||||
|
},
|
||||||
])
|
])
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ const pageSubtitle = computed(() => route.meta?.subtitle || '')
|
|||||||
|
|
||||||
const menuItems = computed(() => [
|
const menuItems = computed(() => [
|
||||||
{
|
{
|
||||||
title: 'لیست درخواست ها',
|
title: 'سامانه اعزام',
|
||||||
icon: 'user-sound',
|
icon: 'user-sound',
|
||||||
to: { name: 'missionary-requests' },
|
to: { name: 'missionary-requests' },
|
||||||
active: route.name === 'missionary-requests',
|
active: route.name === 'missionary-requests',
|
||||||
|
|||||||
@@ -78,12 +78,6 @@ const menuItems = computed(() => [
|
|||||||
'student-course-details',
|
'student-course-details',
|
||||||
].includes(route.name),
|
].includes(route.name),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
title: 'سامانه اعزام',
|
|
||||||
icon: 'user-sound',
|
|
||||||
to: { name: 'student-missionary' },
|
|
||||||
active: route.name === 'student-missionary',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
title: 'خدمات',
|
title: 'خدمات',
|
||||||
icon: 'folder-simple-star',
|
icon: 'folder-simple-star',
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { http } from '@/services/api/http'
|
||||||
|
import { buildUrl, endpoints } from '@/services/api/endpoints'
|
||||||
|
|
||||||
|
export const apiListAdminMissionaryRequests = (params) =>
|
||||||
|
http.get(endpoints.listAdminMissionaryRequests, { params })
|
||||||
|
|
||||||
|
export const apiShowAdminMissionaryRequest = (id) =>
|
||||||
|
http.get(buildUrl(endpoints.showAdminMissionaryRequest, { id }))
|
||||||
|
|
||||||
|
export const apiChangeAdminMissionaryRequestStatus = (id, payload) =>
|
||||||
|
http.patch(buildUrl(endpoints.changeAdminMissionaryRequestStatus, { id }), payload)
|
||||||
@@ -35,11 +35,6 @@ export const endpoints = {
|
|||||||
showHomework: '/homeworks/:id',
|
showHomework: '/homeworks/:id',
|
||||||
startExam: '/exams/:examId/start',
|
startExam: '/exams/:examId/start',
|
||||||
|
|
||||||
getMissionaryProfile: '/student/missionary/profile',
|
|
||||||
getMissionaryRequests: '/student/missionary/requests',
|
|
||||||
getMissionaryDispatches: '/student/missionary/dispatches',
|
|
||||||
getMissionaryNarratives: '/student/missionary/narratives',
|
|
||||||
|
|
||||||
// Unified student tickets — handles services (type=service), consultants
|
// Unified student tickets — handles services (type=service), consultants
|
||||||
// (type=advise) and inbox (type=ticket) via the same endpoint with a filter.
|
// (type=advise) and inbox (type=ticket) via the same endpoint with a filter.
|
||||||
getStudentTickets: '/student/tickets',
|
getStudentTickets: '/student/tickets',
|
||||||
@@ -168,6 +163,15 @@ export const endpoints = {
|
|||||||
listMissionaryRequests: '/missionary/requests',
|
listMissionaryRequests: '/missionary/requests',
|
||||||
showMissionaryRequest: '/missionary/requests/:id',
|
showMissionaryRequest: '/missionary/requests/:id',
|
||||||
changeMissionaryRequestStatus: '/missionary/requests/:id/status',
|
changeMissionaryRequestStatus: '/missionary/requests/:id/status',
|
||||||
|
|
||||||
|
listMissionaryMemories: '/missionary/memories',
|
||||||
|
createMissionaryMemory: '/missionary/memories',
|
||||||
|
updateMissionaryMemory: '/missionary/memories/:id',
|
||||||
|
deleteMissionaryMemory: '/missionary/memories/:id',
|
||||||
|
|
||||||
|
listAdminMissionaryRequests: '/admin/missionary-requests',
|
||||||
|
showAdminMissionaryRequest: '/admin/missionary-requests/:id',
|
||||||
|
changeAdminMissionaryRequestStatus: '/admin/missionary-requests/:id/status',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const buildUrl = (template, params = {}) =>
|
export const buildUrl = (template, params = {}) =>
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { http } from '@/services/api/http'
|
||||||
|
import { buildUrl, endpoints } from '@/services/api/endpoints'
|
||||||
|
|
||||||
|
export const apiListMissionaryMemories = (params) =>
|
||||||
|
http.get(endpoints.listMissionaryMemories, { params })
|
||||||
|
|
||||||
|
export const apiCreateMissionaryMemory = (payload) =>
|
||||||
|
http.post(endpoints.createMissionaryMemory, payload)
|
||||||
|
|
||||||
|
export const apiUpdateMissionaryMemory = (id, payload) =>
|
||||||
|
http.patch(buildUrl(endpoints.updateMissionaryMemory, { id }), payload)
|
||||||
|
|
||||||
|
export const apiDeleteMissionaryMemory = (id) =>
|
||||||
|
http.delete(buildUrl(endpoints.deleteMissionaryMemory, { id }))
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
import { http } from '@/services/api/http'
|
|
||||||
import { endpoints } from '@/services/api/endpoints'
|
|
||||||
|
|
||||||
export const apiGetMissionaryProfile = () => http.get(endpoints.getMissionaryProfile)
|
|
||||||
|
|
||||||
export const apiGetMissionaryRequests = (params) =>
|
|
||||||
http.get(endpoints.getMissionaryRequests, { params })
|
|
||||||
|
|
||||||
export const apiGetMissionaryDispatches = (params) =>
|
|
||||||
http.get(endpoints.getMissionaryDispatches, { params })
|
|
||||||
|
|
||||||
export const apiGetMissionaryNarratives = (params) =>
|
|
||||||
http.get(endpoints.getMissionaryNarratives, { params })
|
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
const SAMPLE_BODY = `لورم ایپسوم متن ساختگی با تولید سادگی نامفهوم از صنعت چاپ و با استفاده از طراحان گرافیک است. چاپگرها و متون بلکه روزنامه و مجله در ستون و سطرآنچنان که لازم است و برای شرایط فعلی تکنولوژی مورد نیاز و کاربردهای متنوع با هدف بهبود ابزارهای کاربردی می باشد.`
|
||||||
|
|
||||||
|
export const missionaryMemories = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
missionaryId: 8,
|
||||||
|
title: 'سفر به شهر زیبای مشهد مقدس، شهر علم و ادب',
|
||||||
|
description: SAMPLE_BODY,
|
||||||
|
media: [],
|
||||||
|
createdAt: '2026-06-10T08:30:00+00:00',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
missionaryId: 8,
|
||||||
|
title: 'سفر به اصفهان نصف جهان',
|
||||||
|
description: SAMPLE_BODY,
|
||||||
|
media: [
|
||||||
|
{
|
||||||
|
id: 63,
|
||||||
|
collectionName: 'memory',
|
||||||
|
fileName: 'trip-photo.png',
|
||||||
|
mimeType: 'image/png',
|
||||||
|
fileSize: 20_727,
|
||||||
|
url: '',
|
||||||
|
downloadUrl: '',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
missionaryRequestId: 3,
|
||||||
|
missionaryRequest: { id: 3, title: 'جلسه پرسش و پاسخ اعتقادی دانشگاه' },
|
||||||
|
createdAt: '2026-06-18T09:15:00+00:00',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
missionaryId: 8,
|
||||||
|
title: 'روایت جلسات هفتگی قم',
|
||||||
|
description: SAMPLE_BODY,
|
||||||
|
media: [],
|
||||||
|
createdAt: '2026-06-25T14:45:00+00:00',
|
||||||
|
},
|
||||||
|
]
|
||||||
@@ -1,157 +0,0 @@
|
|||||||
export const missionaryProfile = {
|
|
||||||
id: 'me',
|
|
||||||
fullName: 'آقای علیرضا احمدی',
|
|
||||||
city: 'قم، ایران',
|
|
||||||
level: 'پیشرفته',
|
|
||||||
membershipDays: 23,
|
|
||||||
rating: 4.8,
|
|
||||||
requestsCount: 451,
|
|
||||||
avatar: '',
|
|
||||||
}
|
|
||||||
|
|
||||||
const STATUS_LABELS = {
|
|
||||||
pending: 'در انتظار بررسی',
|
|
||||||
approved: 'تایید شده',
|
|
||||||
rejected: 'رد شده توسط موسسه',
|
|
||||||
cancelled: 'لغو شده',
|
|
||||||
}
|
|
||||||
|
|
||||||
const REQUEST_DESCRIPTION = `لورم ایپسوم متن ساختگی با تولید سادگی نامفهوم از صنعت چاپ و با استفاده از طراحان گرافیک است. چاپگرها و متون بلکه روزنامه و مجله در ستون و سطرآنچنان که لازم است و برای شرایط فعلی تکنولوژی مورد نیاز و کاربردهای متنوع با هدف بهبود ابزارهای کاربردی می باشد. کتابهای زیادی در شصت و سه درصد گذشته، حال و آینده شناخت فراوان جامعه و متخصصان را می طلبد.`
|
|
||||||
|
|
||||||
export const missionaryRequests = [
|
|
||||||
{
|
|
||||||
id: 'r-1',
|
|
||||||
code: 1,
|
|
||||||
title: 'درخواست شماره یک',
|
|
||||||
requestNumber: 387,
|
|
||||||
requesterName: 'حسین رضایی',
|
|
||||||
requestDate: '1404/05/12',
|
|
||||||
status: 'rejected',
|
|
||||||
statusLabel: STATUS_LABELS.rejected,
|
|
||||||
phone: '0912 152 1413',
|
|
||||||
postalCode: '1232146890345',
|
|
||||||
address:
|
|
||||||
'قم، سالاریه، میدان پیچک، به سمت فرعی اول، میدان الهیه، ساختمان الیزیوم، طبقه منفی 1 واحد 152',
|
|
||||||
description: REQUEST_DESCRIPTION,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'r-2',
|
|
||||||
code: 2,
|
|
||||||
title: 'درخواست شماره دو',
|
|
||||||
requestNumber: 401,
|
|
||||||
requesterName: 'مریم اکبری',
|
|
||||||
requestDate: '1404/06/04',
|
|
||||||
status: 'pending',
|
|
||||||
statusLabel: STATUS_LABELS.pending,
|
|
||||||
phone: '0912 884 2210',
|
|
||||||
postalCode: '8136987710332',
|
|
||||||
address: 'اصفهان، خیابان چهارباغ بالا، کوچه گلستان، پلاک 12',
|
|
||||||
description: REQUEST_DESCRIPTION,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'r-3',
|
|
||||||
code: 3,
|
|
||||||
title: 'درخواست شماره سه',
|
|
||||||
requestNumber: 412,
|
|
||||||
requesterName: 'فاطمه حسینی',
|
|
||||||
requestDate: '1404/06/18',
|
|
||||||
status: 'approved',
|
|
||||||
statusLabel: STATUS_LABELS.approved,
|
|
||||||
phone: '0935 410 0091',
|
|
||||||
postalCode: '9176548132201',
|
|
||||||
address: 'مشهد، خیابان امام رضا، کوچه 14، پلاک 5',
|
|
||||||
description: REQUEST_DESCRIPTION,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
const DISPATCH_RESULT = `لورم ایپسوم متن ساختگی با تولید سادگی نامفهوم از صنعت چاپ و با استفاده از طراحان گرافیک است. چاپگرها و متون بلکه روزنامه و مجله در ستون و سطرآنچنان که لازم است و برای شرایط فعلی تکنولوژی مورد نیاز و کاربردهای متنوع با هدف بهبود ابزارهای کاربردی می باشد. کتابهای زیادی در شصت و سه درصد گذشته، حال و آینده شناخت فراوان جامعه و متخصصان را می طلبد.`
|
|
||||||
|
|
||||||
export const missionaryDispatches = [
|
|
||||||
{
|
|
||||||
id: 'd-1',
|
|
||||||
code: 1_547_890,
|
|
||||||
title: 'اعزام شماره یک',
|
|
||||||
missionName: 'سفر به مشهد مقدس',
|
|
||||||
location: 'زنجان، ایران',
|
|
||||||
dispatchDate: '1404/12/12',
|
|
||||||
statusLabel: 'انجام شده',
|
|
||||||
requesterName: 'حسین مولایی',
|
|
||||||
address:
|
|
||||||
'قم، سالاریه، میدان پیچک، به سمت فرعی اول، میدان الهیه، ساختمان الیزیوم، طبقه منفی 1 واحد 152',
|
|
||||||
result: DISPATCH_RESULT,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'd-2',
|
|
||||||
code: 1_547_891,
|
|
||||||
title: 'اعزام شماره دو',
|
|
||||||
missionName: 'سفر به اصفهان',
|
|
||||||
location: 'اصفهان، ایران',
|
|
||||||
dispatchDate: '1404/04/09',
|
|
||||||
statusLabel: 'در حال انجام',
|
|
||||||
requesterName: 'مریم اکبری',
|
|
||||||
address: 'اصفهان، خیابان چهارباغ، کوچه گلستان، پلاک 12',
|
|
||||||
result: DISPATCH_RESULT,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'd-3',
|
|
||||||
code: 1_547_892,
|
|
||||||
title: 'اعزام شماره سه',
|
|
||||||
missionName: 'سفر به مشهد مقدس',
|
|
||||||
location: 'مشهد، ایران',
|
|
||||||
dispatchDate: '1404/03/21',
|
|
||||||
statusLabel: 'انجام شده',
|
|
||||||
requesterName: 'فاطمه حسینی',
|
|
||||||
address: 'مشهد، خیابان امام رضا، کوچه 14، پلاک 5',
|
|
||||||
result: DISPATCH_RESULT,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
const NARRATIVE_KIND = {
|
|
||||||
manuscript: { key: 'manuscript', label: 'دست نوشته', tone: 'pink' },
|
|
||||||
dispatched: { key: 'dispatched', label: 'اعزام شــــده', tone: 'gold' },
|
|
||||||
}
|
|
||||||
|
|
||||||
const SAMPLE_BODY = `لورم ایپسوم متن ساختگی با تولید سادگی نامفهوم از صنعت چاپ و با استفاده از طراحان گرافیک است. چاپگرها و متون بلکه روزنامه و مجله در ستون و سطرآنچنان که لازم است و برای شرایط فعلی تکنولوژی مورد نیاز و کاربردهای متنوع با هدف بهبود ابزارهای کاربردی می باشد. کتابهای زیادی در شصت و سه درصد گذشته، حال و آینده شناخت فراوان جامعه و متخصصان را می طلبد.
|
|
||||||
|
|
||||||
لورم ایپسوم متن ساختگی با تولید سادگی نامفهوم از صنعت چاپ و با استفاده از طراحان گرافیک است. چاپگرها و متون بلکه روزنامه و مجله در ستون و سطرآنچنان که لازم است و برای شرایط فعلی تکنولوژی مورد نیاز و کاربردهای متنوع با هدف بهبود ابزارهای کاربردی می باشد.
|
|
||||||
|
|
||||||
لورم ایپسوم متن ساختگی با تولید سادگی نامفهوم از صنعت چاپ و با استفاده از طراحان گرافیک است.`
|
|
||||||
|
|
||||||
export const missionaryNarratives = [
|
|
||||||
{
|
|
||||||
id: 'n-1',
|
|
||||||
code: 1,
|
|
||||||
title: 'سفر به شهر زیبای مشهد مقدس، شهر علم و ادب',
|
|
||||||
submittedAt: '۱۴۰۰/۷/۱۹, ۸:۳۰',
|
|
||||||
kind: NARRATIVE_KIND.manuscript.key,
|
|
||||||
kindLabel: NARRATIVE_KIND.manuscript.label,
|
|
||||||
kindTone: NARRATIVE_KIND.manuscript.tone,
|
|
||||||
body: SAMPLE_BODY,
|
|
||||||
likes: 12,
|
|
||||||
comments: 56,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'n-2',
|
|
||||||
code: 2,
|
|
||||||
title: 'سفر به اصفهان نصف جهان',
|
|
||||||
submittedAt: '۱۴۰۰/۷/۱۹, ۸:۳۰',
|
|
||||||
kind: NARRATIVE_KIND.dispatched.key,
|
|
||||||
kindLabel: NARRATIVE_KIND.dispatched.label,
|
|
||||||
kindTone: NARRATIVE_KIND.dispatched.tone,
|
|
||||||
body: SAMPLE_BODY,
|
|
||||||
likes: 24,
|
|
||||||
comments: 9,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'n-3',
|
|
||||||
code: 3,
|
|
||||||
title: 'روایت شماره سه',
|
|
||||||
submittedAt: '۱۴۰۰/۸/۲, ۹:۱۵',
|
|
||||||
kind: NARRATIVE_KIND.manuscript.key,
|
|
||||||
kindLabel: NARRATIVE_KIND.manuscript.label,
|
|
||||||
kindTone: NARRATIVE_KIND.manuscript.tone,
|
|
||||||
body: SAMPLE_BODY,
|
|
||||||
likes: 3,
|
|
||||||
comments: 1,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
@@ -1,13 +1,24 @@
|
|||||||
import { register } from '@/services/mock/registry'
|
import { register } from '@/services/mock/registry'
|
||||||
import { endpoints } from '@/services/api/endpoints'
|
import { endpoints } from '@/services/api/endpoints'
|
||||||
import { findOrThrow, updateById } from '@/services/mock/helpers'
|
|
||||||
import { missionaryRequests } from '@/services/mock/fixtures/missionary-requests'
|
import { missionaryRequests } from '@/services/mock/fixtures/missionary-requests'
|
||||||
|
import { missionaryMemories } from '@/services/mock/fixtures/missionary-memories'
|
||||||
|
import {
|
||||||
|
filterItems,
|
||||||
|
findOrThrow,
|
||||||
|
isoNow,
|
||||||
|
makeId,
|
||||||
|
paginate,
|
||||||
|
updateById,
|
||||||
|
} from '@/services/mock/helpers'
|
||||||
|
|
||||||
register('GET', endpoints.listMissionaryRequests, () => ({
|
const paginatedItems = (items, query) => {
|
||||||
success: true,
|
const { data, meta } = paginate(items, query)
|
||||||
message: 'OK',
|
return { success: true, message: 'OK', data: { items: data, meta } }
|
||||||
data: missionaryRequests,
|
}
|
||||||
}))
|
|
||||||
|
register('GET', endpoints.listMissionaryRequests, ({ query }) =>
|
||||||
|
paginatedItems(filterItems(missionaryRequests, query, { status: 'eq' }), query)
|
||||||
|
)
|
||||||
|
|
||||||
register('GET', endpoints.showMissionaryRequest, ({ params }) => ({
|
register('GET', endpoints.showMissionaryRequest, ({ params }) => ({
|
||||||
success: true,
|
success: true,
|
||||||
@@ -20,3 +31,68 @@ register('PATCH', endpoints.changeMissionaryRequestStatus, ({ params, data }) =>
|
|||||||
message: 'Status updated.',
|
message: 'Status updated.',
|
||||||
data: updateById(missionaryRequests, params.id, { status: data.status }),
|
data: updateById(missionaryRequests, params.id, { status: data.status }),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
register('GET', endpoints.listMissionaryMemories, ({ query }) =>
|
||||||
|
paginatedItems(missionaryMemories, query)
|
||||||
|
)
|
||||||
|
|
||||||
|
register('POST', endpoints.createMissionaryMemory, ({ data }) => {
|
||||||
|
const memory = {
|
||||||
|
id: makeId(),
|
||||||
|
missionaryId: 8,
|
||||||
|
title: data.title || '',
|
||||||
|
description: data.description || '',
|
||||||
|
missionaryRequestId: data.missionaryRequestId ?? null,
|
||||||
|
media: (data.mediaIds ?? []).map((id) => ({
|
||||||
|
id,
|
||||||
|
collectionName: 'memory',
|
||||||
|
fileName: `memory-${id}`,
|
||||||
|
mimeType: 'image/png',
|
||||||
|
fileSize: 0,
|
||||||
|
url: '',
|
||||||
|
downloadUrl: '',
|
||||||
|
})),
|
||||||
|
createdAt: isoNow(),
|
||||||
|
}
|
||||||
|
missionaryMemories.unshift(memory)
|
||||||
|
return { success: true, message: 'Memory created.', data: memory }
|
||||||
|
})
|
||||||
|
|
||||||
|
register('PATCH', endpoints.updateMissionaryMemory, ({ params, data }) => {
|
||||||
|
const memory = findOrThrow(missionaryMemories, params.id)
|
||||||
|
if (data.title != null) memory.title = data.title
|
||||||
|
if (data.description != null) memory.description = data.description
|
||||||
|
const appended = (data.mediaIds ?? []).map((id) => ({
|
||||||
|
id,
|
||||||
|
collectionName: 'memory',
|
||||||
|
fileName: `memory-${id}`,
|
||||||
|
mimeType: 'image/png',
|
||||||
|
fileSize: 0,
|
||||||
|
url: '',
|
||||||
|
downloadUrl: '',
|
||||||
|
}))
|
||||||
|
memory.media = [...(memory.media ?? []), ...appended]
|
||||||
|
return { success: true, message: 'Memory updated.', data: memory }
|
||||||
|
})
|
||||||
|
|
||||||
|
register('DELETE', endpoints.deleteMissionaryMemory, ({ params }) => {
|
||||||
|
const index = missionaryMemories.findIndex((m) => String(m.id) === String(params.id))
|
||||||
|
if (index >= 0) missionaryMemories.splice(index, 1)
|
||||||
|
return { success: true, message: 'Memory deleted.', data: null }
|
||||||
|
})
|
||||||
|
|
||||||
|
register('GET', endpoints.listAdminMissionaryRequests, ({ query }) =>
|
||||||
|
paginatedItems(filterItems(missionaryRequests, query, { status: 'eq' }), query)
|
||||||
|
)
|
||||||
|
|
||||||
|
register('GET', endpoints.showAdminMissionaryRequest, ({ params }) => ({
|
||||||
|
success: true,
|
||||||
|
message: 'OK',
|
||||||
|
data: findOrThrow(missionaryRequests, params.id),
|
||||||
|
}))
|
||||||
|
|
||||||
|
register('PATCH', endpoints.changeAdminMissionaryRequestStatus, ({ params, data }) => ({
|
||||||
|
success: true,
|
||||||
|
message: 'Status updated.',
|
||||||
|
data: updateById(missionaryRequests, params.id, { status: data.status }),
|
||||||
|
}))
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
import { paginate } from '@/services/mock/helpers'
|
|
||||||
import { register } from '@/services/mock/registry'
|
|
||||||
import { endpoints } from '@/services/api/endpoints'
|
|
||||||
import {
|
|
||||||
missionaryDispatches,
|
|
||||||
missionaryNarratives,
|
|
||||||
missionaryProfile,
|
|
||||||
missionaryRequests,
|
|
||||||
} from '@/services/mock/fixtures/student-missionary'
|
|
||||||
|
|
||||||
register('GET', endpoints.getMissionaryProfile, () => ({ data: missionaryProfile }))
|
|
||||||
|
|
||||||
register('GET', endpoints.getMissionaryRequests, ({ query }) => paginate(missionaryRequests, query))
|
|
||||||
|
|
||||||
register('GET', endpoints.getMissionaryDispatches, ({ query }) =>
|
|
||||||
paginate(missionaryDispatches, query)
|
|
||||||
)
|
|
||||||
|
|
||||||
register('GET', endpoints.getMissionaryNarratives, ({ query }) =>
|
|
||||||
paginate(missionaryNarratives, query)
|
|
||||||
)
|
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { cleanFilters } from '@/utils/clean-filters'
|
||||||
|
import { useMutation, useQuery } from '@tanstack/vue-query'
|
||||||
|
import {
|
||||||
|
apiChangeAdminMissionaryRequestStatus,
|
||||||
|
apiListAdminMissionaryRequests,
|
||||||
|
} from '@/services/api/admin-missionary-requests'
|
||||||
|
|
||||||
|
export const adminMissionaryRequestsKeys = {
|
||||||
|
all: ['admin', 'missionary-requests'],
|
||||||
|
list: (filters, pagination) => ['admin', 'missionary-requests', 'list', filters, pagination],
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useAdminMissionaryRequestsListQuery = (filtersRef, paginationRef, options = {}) =>
|
||||||
|
useQuery({
|
||||||
|
queryKey: ['admin', 'missionary-requests', 'list', filtersRef, paginationRef],
|
||||||
|
queryFn: () =>
|
||||||
|
apiListAdminMissionaryRequests({ ...cleanFilters(filtersRef.value), ...paginationRef.value }),
|
||||||
|
select: (response) => ({
|
||||||
|
data: response?.data?.items ?? response?.data ?? [],
|
||||||
|
meta: response?.data?.meta ?? response?.meta,
|
||||||
|
}),
|
||||||
|
...options,
|
||||||
|
})
|
||||||
|
|
||||||
|
export const useChangeAdminMissionaryRequestStatusMutation = () =>
|
||||||
|
useMutation({
|
||||||
|
mutationFn: ({ id, payload }) => apiChangeAdminMissionaryRequestStatus(id, payload),
|
||||||
|
})
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { cleanFilters } from '@/utils/clean-filters'
|
||||||
|
import { useMutation, useQuery } from '@tanstack/vue-query'
|
||||||
|
import {
|
||||||
|
apiCreateMissionaryMemory,
|
||||||
|
apiDeleteMissionaryMemory,
|
||||||
|
apiListMissionaryMemories,
|
||||||
|
apiUpdateMissionaryMemory,
|
||||||
|
} from '@/services/api/missionary-memories'
|
||||||
|
|
||||||
|
export const missionaryMemoriesKeys = {
|
||||||
|
all: ['missionary', 'memories'],
|
||||||
|
list: (filters, pagination) => ['missionary', 'memories', 'list', filters, pagination],
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useMissionaryMemoriesListQuery = (filtersRef, paginationRef, options = {}) =>
|
||||||
|
useQuery({
|
||||||
|
queryKey: ['missionary', 'memories', 'list', filtersRef, paginationRef],
|
||||||
|
queryFn: () =>
|
||||||
|
apiListMissionaryMemories({ ...cleanFilters(filtersRef.value), ...paginationRef.value }),
|
||||||
|
select: (response) => ({
|
||||||
|
data: response?.data ?? [],
|
||||||
|
meta: response?.data?.meta ?? response?.meta,
|
||||||
|
}),
|
||||||
|
...options,
|
||||||
|
})
|
||||||
|
|
||||||
|
export const useCreateMissionaryMemoryMutation = () =>
|
||||||
|
useMutation({ mutationFn: apiCreateMissionaryMemory })
|
||||||
|
|
||||||
|
export const useUpdateMissionaryMemoryMutation = () =>
|
||||||
|
useMutation({ mutationFn: ({ id, payload }) => apiUpdateMissionaryMemory(id, payload) })
|
||||||
|
|
||||||
|
export const useDeleteMissionaryMemoryMutation = () =>
|
||||||
|
useMutation({ mutationFn: apiDeleteMissionaryMemory })
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { cleanFilters } from '@/utils/clean-filters'
|
||||||
import { useMutation, useQuery } from '@tanstack/vue-query'
|
import { useMutation, useQuery } from '@tanstack/vue-query'
|
||||||
import {
|
import {
|
||||||
apiChangeMissionaryRequestStatus,
|
apiChangeMissionaryRequestStatus,
|
||||||
@@ -7,15 +8,19 @@ import {
|
|||||||
|
|
||||||
export const missionaryRequestsKeys = {
|
export const missionaryRequestsKeys = {
|
||||||
all: ['missionary', 'requests'],
|
all: ['missionary', 'requests'],
|
||||||
list: () => ['missionary', 'requests', 'list'],
|
list: (filters, pagination) => ['missionary', 'requests', 'list', filters, pagination],
|
||||||
detail: (id) => ['missionary', 'requests', 'detail', id],
|
detail: (id) => ['missionary', 'requests', 'detail', id],
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useMissionaryRequestsListQuery = (options = {}) =>
|
export const useMissionaryRequestsListQuery = (filtersRef, paginationRef, options = {}) =>
|
||||||
useQuery({
|
useQuery({
|
||||||
queryKey: missionaryRequestsKeys.list(),
|
queryKey: ['missionary', 'requests', 'list', filtersRef, paginationRef],
|
||||||
queryFn: () => apiListMissionaryRequests(),
|
queryFn: () =>
|
||||||
select: (response) => response?.data ?? [],
|
apiListMissionaryRequests({ ...cleanFilters(filtersRef.value), ...paginationRef.value }),
|
||||||
|
select: (response) => ({
|
||||||
|
data: response?.data ?? [],
|
||||||
|
meta: response?.data?.meta ?? response?.meta,
|
||||||
|
}),
|
||||||
...options,
|
...options,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,56 +0,0 @@
|
|||||||
import { useQuery } from '@tanstack/vue-query'
|
|
||||||
import { cleanFilters } from '@/utils/clean-filters'
|
|
||||||
import {
|
|
||||||
apiGetMissionaryDispatches,
|
|
||||||
apiGetMissionaryNarratives,
|
|
||||||
apiGetMissionaryProfile,
|
|
||||||
apiGetMissionaryRequests,
|
|
||||||
} from '@/services/api/student-missionary'
|
|
||||||
|
|
||||||
export const missionaryKeys = {
|
|
||||||
profile: ['student', 'missionary', 'profile'],
|
|
||||||
requests: (filters, pagination) => ['student', 'missionary', 'requests', filters, pagination],
|
|
||||||
dispatches: (filters, pagination) => ['student', 'missionary', 'dispatches', filters, pagination],
|
|
||||||
narratives: (filters, pagination) => ['student', 'missionary', 'narratives', filters, pagination],
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useMissionaryProfileQuery = (options = {}) =>
|
|
||||||
useQuery({
|
|
||||||
queryKey: missionaryKeys.profile,
|
|
||||||
queryFn: () => apiGetMissionaryProfile(),
|
|
||||||
select: (response) => response?.data ?? response,
|
|
||||||
...options,
|
|
||||||
})
|
|
||||||
|
|
||||||
export const useMissionaryRequestsQuery = (filtersRef, paginationRef, options = {}) =>
|
|
||||||
useQuery({
|
|
||||||
queryKey: ['student', 'missionary', 'requests', filtersRef, paginationRef],
|
|
||||||
queryFn: () =>
|
|
||||||
apiGetMissionaryRequests({
|
|
||||||
...cleanFilters(filtersRef.value),
|
|
||||||
...paginationRef.value,
|
|
||||||
}),
|
|
||||||
...options,
|
|
||||||
})
|
|
||||||
|
|
||||||
export const useMissionaryDispatchesQuery = (filtersRef, paginationRef, options = {}) =>
|
|
||||||
useQuery({
|
|
||||||
queryKey: ['student', 'missionary', 'dispatches', filtersRef, paginationRef],
|
|
||||||
queryFn: () =>
|
|
||||||
apiGetMissionaryDispatches({
|
|
||||||
...cleanFilters(filtersRef.value),
|
|
||||||
...paginationRef.value,
|
|
||||||
}),
|
|
||||||
...options,
|
|
||||||
})
|
|
||||||
|
|
||||||
export const useMissionaryNarrativesQuery = (filtersRef, paginationRef, options = {}) =>
|
|
||||||
useQuery({
|
|
||||||
queryKey: ['student', 'missionary', 'narratives', filtersRef, paginationRef],
|
|
||||||
queryFn: () =>
|
|
||||||
apiGetMissionaryNarratives({
|
|
||||||
...cleanFilters(filtersRef.value),
|
|
||||||
...paginationRef.value,
|
|
||||||
}),
|
|
||||||
...options,
|
|
||||||
})
|
|
||||||
Reference in New Issue
Block a user