fix: design
This commit is contained in:
@@ -1,2 +1,2 @@
|
|||||||
VITE_API_BASE_URL=
|
VITE_API_BASE_URL=https://tripwisedaily.ir/api
|
||||||
VITE_USE_MOCKS=true
|
VITE_USE_MOCKS=false
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Generated
+1
@@ -13,6 +13,7 @@
|
|||||||
"@tanstack/vue-query-devtools": "^5.62.2",
|
"@tanstack/vue-query-devtools": "^5.62.2",
|
||||||
"@tinymce/tinymce-vue": "^4.0.7",
|
"@tinymce/tinymce-vue": "^4.0.7",
|
||||||
"axios": "^1.12.2",
|
"axios": "^1.12.2",
|
||||||
|
"jalaali-js": "^1.2.8",
|
||||||
"lodash": "^4.18.1",
|
"lodash": "^4.18.1",
|
||||||
"pinia": "^3.0.3",
|
"pinia": "^3.0.3",
|
||||||
"tinymce": "^8.5.0",
|
"tinymce": "^8.5.0",
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
"@tanstack/vue-query-devtools": "^5.62.2",
|
"@tanstack/vue-query-devtools": "^5.62.2",
|
||||||
"@tinymce/tinymce-vue": "^4.0.7",
|
"@tinymce/tinymce-vue": "^4.0.7",
|
||||||
"axios": "^1.12.2",
|
"axios": "^1.12.2",
|
||||||
|
"jalaali-js": "^1.2.8",
|
||||||
"lodash": "^4.18.1",
|
"lodash": "^4.18.1",
|
||||||
"pinia": "^3.0.3",
|
"pinia": "^3.0.3",
|
||||||
"tinymce": "^8.5.0",
|
"tinymce": "^8.5.0",
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
:id="inputId"
|
:id="inputId"
|
||||||
ref="referenceEl"
|
ref="referenceRef"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
class="datepicker-field__trigger"
|
class="datepicker-field__trigger"
|
||||||
:class="{
|
:class="{
|
||||||
@@ -22,10 +22,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Teleport to="body">
|
<Teleport to="body">
|
||||||
<Transition name="fade">
|
<transition name="fade">
|
||||||
<div
|
<div
|
||||||
v-show="isOpen"
|
v-show="isOpen"
|
||||||
ref="floatingEl"
|
ref="floatingRef"
|
||||||
class="date-picker-dropdown datepicker-field__dropdown"
|
class="date-picker-dropdown datepicker-field__dropdown"
|
||||||
:style="floatingStyles"
|
:style="floatingStyles"
|
||||||
>
|
>
|
||||||
@@ -34,37 +34,45 @@
|
|||||||
locale="fa"
|
locale="fa"
|
||||||
inline
|
inline
|
||||||
editable
|
editable
|
||||||
:min="min"
|
compact-time
|
||||||
:max="max"
|
:min="jalaliMin"
|
||||||
|
:max="jalaliMax"
|
||||||
:type="type"
|
:type="type"
|
||||||
:simple="simple"
|
:simple="simple"
|
||||||
:auto-submit="type === 'date'"
|
:range="range"
|
||||||
|
:auto-submit="false"
|
||||||
|
:time-picker="type === 'datetime'"
|
||||||
|
:compact-time="type === 'datetime'"
|
||||||
:format="type === 'datetime' ? 'jYYYY/jMM/jDD HH:mm' : 'jYYYY/jMM/jDD'"
|
:format="type === 'datetime' ? 'jYYYY/jMM/jDD HH:mm' : 'jYYYY/jMM/jDD'"
|
||||||
:display-format="type === 'datetime' ? 'jYYYY/jMM/jDD HH:mm' : 'jYYYY/jMM/jDD'"
|
:display-format="type === 'datetime' ? 'jYYYY/jMM/jDD HH:mm' : 'jYYYY/jMM/jDD'"
|
||||||
|
:confirm="type === 'datetime'"
|
||||||
@update:model-value="onSelect"
|
@update:model-value="onSelect"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Transition>
|
</transition>
|
||||||
</Teleport>
|
</Teleport>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script>
|
||||||
import SvgIcon from '@/components/icons/SvgIcon.vue'
|
import SvgIcon from '@/components/icons/SvgIcon.vue'
|
||||||
import DatePicker from 'vue3-persian-datetime-picker'
|
import DatePicker from 'vue3-persian-datetime-picker'
|
||||||
import { computed, onBeforeUnmount, ref, watch } from 'vue'
|
import { computePosition, autoUpdate, offset, shift, flip, size } from '@floating-ui/dom'
|
||||||
import { autoUpdate, computePosition, flip, offset, shift, size } from '@floating-ui/dom'
|
|
||||||
import {
|
import {
|
||||||
formatJalaaliDate,
|
JalaliToGregorianString,
|
||||||
formatJalaaliDateTime,
|
gregorianToJalaliString,
|
||||||
jalaaliStringToIsoDate,
|
gregorianToJalaliStringLong,
|
||||||
jalaaliStringToIsoDateTime,
|
JalaliToGregorianStringWithTime,
|
||||||
} from '@/utils/date-utils'
|
} from '@/utils/date-convertor'
|
||||||
|
|
||||||
let uid = 0
|
let uid = 0
|
||||||
const nextUid = () => `datepicker-field-${++uid}`
|
const nextUid = () => `datepicker-field-${++uid}`
|
||||||
|
|
||||||
const props = defineProps({
|
export default {
|
||||||
|
name: 'DatePickerField',
|
||||||
|
components: { DatePicker, SvgIcon },
|
||||||
|
|
||||||
|
props: {
|
||||||
modelValue: { type: String, default: '' },
|
modelValue: { type: String, default: '' },
|
||||||
name: { type: String, default: '' },
|
name: { type: String, default: '' },
|
||||||
label: { type: String, default: '' },
|
label: { type: String, default: '' },
|
||||||
@@ -76,49 +84,119 @@ const props = defineProps({
|
|||||||
max: { type: String, default: undefined },
|
max: { type: String, default: undefined },
|
||||||
type: { type: String, default: 'date' },
|
type: { type: String, default: 'date' },
|
||||||
simple: { type: Boolean, default: false },
|
simple: { type: Boolean, default: false },
|
||||||
})
|
range: { type: [Boolean, Array], default: false },
|
||||||
|
autoSubmit: { type: Boolean, default: true },
|
||||||
|
},
|
||||||
|
|
||||||
const emit = defineEmits(['update:modelValue', 'change'])
|
emits: ['update:modelValue', 'change'],
|
||||||
|
|
||||||
const inputId = computed(() => props.name || nextUid())
|
data() {
|
||||||
const referenceEl = ref(null)
|
return {
|
||||||
const floatingEl = ref(null)
|
inputId: this.name || nextUid(),
|
||||||
const isOpen = ref(false)
|
isOpen: false,
|
||||||
const internalValue = ref(null)
|
internalValue: null,
|
||||||
const floatingStyles = ref({ position: 'absolute', top: '0px', left: '0px' })
|
cleanup: null,
|
||||||
let cleanup = null
|
floatingStyles: {
|
||||||
|
position: 'absolute',
|
||||||
const displayValue = computed(() => internalValue.value || props.placeholder)
|
top: '0px',
|
||||||
|
left: '0px',
|
||||||
const syncFromProp = () => {
|
},
|
||||||
const v = props.modelValue
|
confirmHandler: null,
|
||||||
if (v && typeof v === 'string') {
|
mutationObserver: null,
|
||||||
internalValue.value =
|
|
||||||
props.type === 'datetime' ? formatJalaaliDateTime(v) : formatJalaaliDate(v)
|
|
||||||
} else {
|
|
||||||
internalValue.value = null
|
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
|
||||||
watch(() => props.modelValue, syncFromProp, { immediate: true })
|
computed: {
|
||||||
|
displayValue() {
|
||||||
|
return this.internalValue || this.placeholder
|
||||||
|
},
|
||||||
|
jalaliMin() {
|
||||||
|
if (!this.min) return
|
||||||
|
if (this.type === 'datetime') {
|
||||||
|
return gregorianToJalaliStringLong(this.min, '/', false).trim().replace('، ', ' ')
|
||||||
|
}
|
||||||
|
return gregorianToJalaliString(this.min, '/', false)
|
||||||
|
},
|
||||||
|
jalaliMax() {
|
||||||
|
if (!this.max) return
|
||||||
|
if (this.type === 'datetime') {
|
||||||
|
return gregorianToJalaliStringLong(this.max, '/', false).trim().replace('، ', ' ')
|
||||||
|
}
|
||||||
|
return gregorianToJalaliString(this.max, '/', false)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
const onSelect = (jalaliValue) => {
|
watch: {
|
||||||
if (!jalaliValue) return
|
modelValue: {
|
||||||
internalValue.value = jalaliValue
|
immediate: true,
|
||||||
const gregorian =
|
handler(val) {
|
||||||
props.type === 'datetime'
|
this.syncFromModel(val)
|
||||||
? jalaaliStringToIsoDateTime(jalaliValue)
|
},
|
||||||
: jalaaliStringToIsoDate(jalaliValue)
|
},
|
||||||
emit('update:modelValue', gregorian)
|
},
|
||||||
emit('change', gregorian)
|
|
||||||
if (props.type === 'date') close()
|
|
||||||
}
|
|
||||||
|
|
||||||
const mountFloating = () => {
|
beforeUnmount() {
|
||||||
const reference = referenceEl.value
|
this.cleanup?.()
|
||||||
const floating = floatingEl.value
|
this.removeConfirmButtonListener()
|
||||||
|
document.removeEventListener('mousedown', this.handleOutside)
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
syncFromModel(val) {
|
||||||
|
if (val && typeof val === 'string') {
|
||||||
|
this.internalValue =
|
||||||
|
this.type === 'datetime'
|
||||||
|
? gregorianToJalaliStringLong(val, '/', false)
|
||||||
|
: gregorianToJalaliString(val, '/', false)
|
||||||
|
} else {
|
||||||
|
this.internalValue = null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
toggle() {
|
||||||
|
if (this.disabled) return
|
||||||
|
this.isOpen ? this.close() : this.open()
|
||||||
|
},
|
||||||
|
|
||||||
|
open() {
|
||||||
|
this.isOpen = true
|
||||||
|
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.initFloating()
|
||||||
|
if (this.type === 'datetime') {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.attachConfirmButtonListener()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
close() {
|
||||||
|
this.isOpen = false
|
||||||
|
this.cleanup?.()
|
||||||
|
this.cleanup = null
|
||||||
|
this.removeConfirmButtonListener()
|
||||||
|
},
|
||||||
|
|
||||||
|
removeConfirmButtonListener() {
|
||||||
|
if (this.confirmHandler) {
|
||||||
|
this.confirmHandler.element.removeEventListener('click', this.confirmHandler.handler)
|
||||||
|
delete this.confirmHandler.element.dataset.confirmListener
|
||||||
|
this.confirmHandler = null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.mutationObserver) {
|
||||||
|
this.mutationObserver.disconnect()
|
||||||
|
this.mutationObserver = null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
initFloating() {
|
||||||
|
const reference = this.$refs.referenceRef
|
||||||
|
const floating = this.$refs.floatingRef
|
||||||
if (!reference || !floating) return
|
if (!reference || !floating) return
|
||||||
cleanup = autoUpdate(reference, floating, () => {
|
|
||||||
|
this.cleanup = autoUpdate(reference, floating, () => {
|
||||||
computePosition(reference, floating, {
|
computePosition(reference, floating, {
|
||||||
placement: 'bottom-start',
|
placement: 'bottom-start',
|
||||||
middleware: [
|
middleware: [
|
||||||
@@ -127,52 +205,183 @@ const mountFloating = () => {
|
|||||||
shift({ padding: 8 }),
|
shift({ padding: 8 }),
|
||||||
size({
|
size({
|
||||||
apply({ rects }) {
|
apply({ rects }) {
|
||||||
Object.assign(floating.style, { minWidth: `${rects.reference.width}px` })
|
Object.assign(floating.style, {
|
||||||
|
minWidth: `${rects.reference.width}px`,
|
||||||
|
})
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
}).then(({ x, y }) => {
|
}).then(({ x, y }) => {
|
||||||
floatingStyles.value = { position: 'absolute', left: `${x}px`, top: `${y}px` }
|
Object.assign(this.floatingStyles, {
|
||||||
|
left: `${x}px`,
|
||||||
|
top: `${y}px`,
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
document.addEventListener('mousedown', handleOutsideClick)
|
|
||||||
}
|
|
||||||
|
|
||||||
const unmountFloating = () => {
|
document.addEventListener('mousedown', this.handleOutside)
|
||||||
cleanup?.()
|
},
|
||||||
cleanup = null
|
|
||||||
document.removeEventListener('mousedown', handleOutsideClick)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleOutsideClick = (event) => {
|
handleOutside(e) {
|
||||||
const reference = referenceEl.value
|
const r = this.$refs.referenceRef
|
||||||
const floating = floatingEl.value
|
const f = this.$refs.floatingRef
|
||||||
if (
|
if (r?.contains(e.target) || f?.contains(e.target)) return
|
||||||
reference &&
|
this.close()
|
||||||
!reference.contains(event.target) &&
|
document.removeEventListener('mousedown', this.handleOutside)
|
||||||
floating &&
|
},
|
||||||
!floating.contains(event.target)
|
|
||||||
) {
|
onSelect(jalaliValue) {
|
||||||
close()
|
if (!jalaliValue) return
|
||||||
|
|
||||||
|
if (this.type === 'datetime') {
|
||||||
|
const gregorian = JalaliToGregorianStringWithTime(jalaliValue)
|
||||||
|
this.$emit('update:modelValue', gregorian)
|
||||||
|
this.$emit('change', gregorian)
|
||||||
|
this.internalValue = jalaliValue
|
||||||
|
} else {
|
||||||
|
const gregorian = JalaliToGregorianString(jalaliValue)
|
||||||
|
this.$emit('update:modelValue', gregorian)
|
||||||
|
this.$emit('change', gregorian)
|
||||||
|
this.internalValue = jalaliValue
|
||||||
|
this.close()
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
|
||||||
const open = () => {
|
attachConfirmButtonListener() {
|
||||||
isOpen.value = true
|
const findAndAttach = () => {
|
||||||
setTimeout(mountFloating, 0)
|
const dropdown = this.$refs.floatingRef
|
||||||
}
|
if (!dropdown) return false
|
||||||
|
let confirmBtn = dropdown.querySelector('.vpd-actions')
|
||||||
|
if (confirmBtn) {
|
||||||
|
const allButtons = confirmBtn.querySelectorAll('button')
|
||||||
|
for (const btn of allButtons) {
|
||||||
|
if (btn.innerHTML.includes('svg') || btn.innerText.includes('تایید')) {
|
||||||
|
confirmBtn = btn
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const close = () => {
|
if (confirmBtn && !Object.hasOwn(confirmBtn.dataset, 'confirmListener')) {
|
||||||
isOpen.value = false
|
const handler = (e) => {
|
||||||
unmountFloating()
|
e.preventDefault()
|
||||||
}
|
e.stopPropagation()
|
||||||
|
this.handleConfirm()
|
||||||
|
}
|
||||||
|
|
||||||
const toggle = () => {
|
confirmBtn.addEventListener('click', handler, true)
|
||||||
if (props.disabled) return
|
confirmBtn.dataset.confirmListener = 'true'
|
||||||
isOpen.value ? close() : open()
|
this.confirmHandler = { element: confirmBtn, handler }
|
||||||
}
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
onBeforeUnmount(() => unmountFloating())
|
if (!findAndAttach()) {
|
||||||
|
const timeouts = [100, 300, 500, 1000]
|
||||||
|
timeouts.forEach((delay) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
if (this.isOpen) {
|
||||||
|
findAndAttach()
|
||||||
|
}
|
||||||
|
}, delay)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setupMutationObserver()
|
||||||
|
},
|
||||||
|
|
||||||
|
setupMutationObserver() {
|
||||||
|
const dropdown = this.$refs.floatingRef
|
||||||
|
if (!dropdown) return
|
||||||
|
|
||||||
|
this.mutationObserver = new MutationObserver((mutations) => {
|
||||||
|
for (const mutation of mutations) {
|
||||||
|
if (mutation.addedNodes.length > 0) {
|
||||||
|
setTimeout(() => {
|
||||||
|
for (const node of mutation.addedNodes) {
|
||||||
|
if (node.nodeType === 1) {
|
||||||
|
if (this.isConfirmButton(node)) {
|
||||||
|
this.attachListenerToButton(node)
|
||||||
|
}
|
||||||
|
|
||||||
|
const btns = node.querySelectorAll ? node.querySelectorAll('button') : []
|
||||||
|
for (const btn of btns) {
|
||||||
|
if (this.isConfirmButton(btn)) {
|
||||||
|
this.attachListenerToButton(btn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 50)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
this.mutationObserver.observe(dropdown, {
|
||||||
|
childList: true,
|
||||||
|
subtree: true,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
isConfirmButton(element) {
|
||||||
|
if (!element || !element.tagName) return false
|
||||||
|
|
||||||
|
if (
|
||||||
|
element.classList?.contains('vpd-confirm-btn') ||
|
||||||
|
element.classList?.contains('vpd-action-btn')
|
||||||
|
) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (element.innerText?.includes('تایید')) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (element.innerHTML?.includes('svg')) {
|
||||||
|
const svgPaths = element.innerHTML.match(/d="[^"]*"/g) || []
|
||||||
|
|
||||||
|
const hasCheckIcon = svgPaths.some(
|
||||||
|
(path) =>
|
||||||
|
path.includes('M20 6L9 17l-5-5') ||
|
||||||
|
path.includes('M9 16.17L4.83 12') ||
|
||||||
|
path.includes('M5 13l4 4L19 7')
|
||||||
|
)
|
||||||
|
|
||||||
|
return hasCheckIcon
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
|
||||||
|
attachListenerToButton(btn) {
|
||||||
|
if (Object.hasOwn(btn.dataset, 'confirmListener')) return
|
||||||
|
|
||||||
|
const handler = (e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
this.handleConfirm()
|
||||||
|
}
|
||||||
|
|
||||||
|
btn.addEventListener('click', handler, true)
|
||||||
|
btn.dataset.confirmListener = 'true'
|
||||||
|
this.confirmHandler = { element: btn, handler }
|
||||||
|
},
|
||||||
|
|
||||||
|
handleConfirm() {
|
||||||
|
if (!this.internalValue) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
const gregorian = JalaliToGregorianStringWithTime(this.internalValue)
|
||||||
|
this.$emit('update:modelValue', gregorian)
|
||||||
|
this.$emit('change', gregorian)
|
||||||
|
this.close()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in handleConfirm:', error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@@ -254,3 +463,143 @@ onBeforeUnmount(() => unmountFloating())
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.fade-enter-active,
|
||||||
|
.fade-leave-active {
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-enter-from,
|
||||||
|
.fade-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-picker-dropdown .vpd-main .vpd-input-group {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-picker-dropdown .vpd-wrapper .vpd-container {
|
||||||
|
margin: 0 !important;
|
||||||
|
font-family: iran-yekan;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-picker-dropdown .vpd-wrapper .vpd-content {
|
||||||
|
width: auto;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-picker-dropdown .vpd-wrapper .vpd-header .vpd-date {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-picker-dropdown .vpd-wrapper .vpd-header {
|
||||||
|
background-color: transparent !important;
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-picker-dropdown .vpd-wrapper .vpd-header .vpd-year-label {
|
||||||
|
color: #5f5f5f;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-picker-dropdown .vpd-wrapper .vpd-body {
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-picker-dropdown .vpd-wrapper .vpd-body .vpd-controls button {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-picker-dropdown .vpd-wrapper .vpd-body .vpd-clearfix.vpd-month .vpd-clearfix.vpd-week {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-picker-dropdown
|
||||||
|
.vpd-wrapper
|
||||||
|
.vpd-body
|
||||||
|
.vpd-clearfix.vpd-month
|
||||||
|
.vpd-clearfix.vpd-week
|
||||||
|
.vpd-weekday {
|
||||||
|
width: auto;
|
||||||
|
float: none;
|
||||||
|
clear: both;
|
||||||
|
padding: 0 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 400px) {
|
||||||
|
.date-picker-dropdown .vpd-wrapper .vpd-body .vpd-clearfix.vpd-month .vpd-clearfix.vpd-week,
|
||||||
|
.date-picker-dropdown .vpd-wrapper .vpd-body .vpd-clearfix.vpd-month .vpd-days {
|
||||||
|
padding: 0 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-picker-dropdown
|
||||||
|
.vpd-wrapper
|
||||||
|
.vpd-body
|
||||||
|
.vpd-clearfix.vpd-month
|
||||||
|
.vpd-days
|
||||||
|
.direction-prev
|
||||||
|
.vpd-clearfix {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-picker-dropdown .vpd-wrapper .vpd-body .vpd-clearfix.vpd-month .vpd-days {
|
||||||
|
height: fit-content !important;
|
||||||
|
padding: 0 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-picker-dropdown .vpd-wrapper .vpd-body .vpd-clearfix.vpd-month .vpd-days .vpd-day {
|
||||||
|
width: 35px;
|
||||||
|
height: 35px;
|
||||||
|
float: right;
|
||||||
|
line-height: 35px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-picker-dropdown
|
||||||
|
.vpd-wrapper
|
||||||
|
.vpd-body
|
||||||
|
.vpd-clearfix.vpd-month
|
||||||
|
.vpd-days
|
||||||
|
.vpd-day:not([disabled='true']).vpd-selected
|
||||||
|
.vpd-day-effect,
|
||||||
|
.date-picker-dropdown
|
||||||
|
.vpd-wrapper
|
||||||
|
.vpd-body
|
||||||
|
.vpd-clearfix.vpd-month
|
||||||
|
.vpd-days
|
||||||
|
.vpd-day:not([disabled='true']):hover
|
||||||
|
.vpd-day-effect {
|
||||||
|
background-color: #f36675 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-picker-dropdown
|
||||||
|
.vpd-wrapper
|
||||||
|
.vpd-body
|
||||||
|
.vpd-clearfix.vpd-month
|
||||||
|
.vpd-days
|
||||||
|
.vpd-day
|
||||||
|
.vpd-day-effect {
|
||||||
|
width: 35px;
|
||||||
|
height: 35px;
|
||||||
|
top: -2px;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-picker-dropdown .vpd-wrapper .vpd-body .vpd-close-addon {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-picker-dropdown .vpd-wrapper .vpd-body .vpd-month-label {
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
class="image-cropper__upload-btn"
|
class="image-cropper__upload-btn"
|
||||||
@click="triggerUpload"
|
@click="triggerUpload"
|
||||||
>
|
>
|
||||||
<SvgIcon name="upload" :size="64" />
|
<SvgIcon name="upload" color="" :size="64" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div v-if="isCropping && image" class="image-cropper__crop-stage">
|
<div v-if="isCropping && image" class="image-cropper__crop-stage">
|
||||||
|
|||||||
@@ -133,6 +133,18 @@ export const VERIFICATION_MEDIA_TYPE = Object.freeze({
|
|||||||
LEADER_MESSAGE: 'leader_message',
|
LEADER_MESSAGE: 'leader_message',
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const COURSE_CONTENT_TYPE = Object.freeze({
|
||||||
|
video: 'ویدئو',
|
||||||
|
voice: 'صوت',
|
||||||
|
text: 'متن',
|
||||||
|
})
|
||||||
|
|
||||||
|
export const COURSE_CONTENT_TYPE_ACCEPT = Object.freeze({
|
||||||
|
video: 'video/*',
|
||||||
|
voice: 'audio/*',
|
||||||
|
text: '.pdf,.doc,.docx,.txt',
|
||||||
|
})
|
||||||
|
|
||||||
export const SESSION_TYPE = Object.freeze({
|
export const SESSION_TYPE = Object.freeze({
|
||||||
in_person: 'حضوری',
|
in_person: 'حضوری',
|
||||||
online: 'آنلاین',
|
online: 'آنلاین',
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ import LineTitleBlock from '@/components/LineTitleBlock.vue'
|
|||||||
import ToggleSwitch from '@/components/form/ToggleSwitch.vue'
|
import ToggleSwitch from '@/components/form/ToggleSwitch.vue'
|
||||||
import ImageCropper from '@/components/form/ImageCropper.vue'
|
import ImageCropper from '@/components/form/ImageCropper.vue'
|
||||||
import { objectToFormData } from '@/utils/object-to-formdata'
|
import { objectToFormData } from '@/utils/object-to-formdata'
|
||||||
import { useUploadTemporaryMutation } from '@/services/query/auth'
|
import { useUploadMediaMutation } from '@/services/query/auth'
|
||||||
import { useAdminTermsListQuery } from '@/services/query/admin-terms'
|
import { useAdminTermsListQuery } from '@/services/query/admin-terms'
|
||||||
import { offeredCourseSchema } from '@/features/admin/courses/schema'
|
import { offeredCourseSchema } from '@/features/admin/courses/schema'
|
||||||
import { useAdminCourseTemplatesListQuery } from '@/services/query/admin-course-templates'
|
import { useAdminCourseTemplatesListQuery } from '@/services/query/admin-course-templates'
|
||||||
@@ -213,11 +213,11 @@ watch(existingCourse, (course) => {
|
|||||||
if (course.image) image.value = { url: course.image }
|
if (course.image) image.value = { url: course.image }
|
||||||
})
|
})
|
||||||
|
|
||||||
const uploadMutation = useUploadTemporaryMutation()
|
const uploadMutation = useUploadMediaMutation()
|
||||||
|
|
||||||
const onImageCropped = async (file) => {
|
const onImageCropped = async (file) => {
|
||||||
try {
|
try {
|
||||||
const formData = objectToFormData({ file, subType: 'avatar', context: 'course' })
|
const formData = objectToFormData({ file, purpose: 'cover', context: 'course' })
|
||||||
const response = await uploadMutation.mutateAsync(formData)
|
const response = await uploadMutation.mutateAsync(formData)
|
||||||
const payload = response?.data ?? response
|
const payload = response?.data ?? response
|
||||||
image.value = { url: payload?.url, uploadId: payload?.uploadId, ...payload }
|
image.value = { url: payload?.url, uploadId: payload?.uploadId, ...payload }
|
||||||
|
|||||||
@@ -48,13 +48,35 @@
|
|||||||
name="defaultTeacherId"
|
name="defaultTeacherId"
|
||||||
label="استاد"
|
label="استاد"
|
||||||
:options="teacherOptions"
|
:options="teacherOptions"
|
||||||
option-label="label"
|
option-label="name"
|
||||||
option-value="id"
|
option-value="id"
|
||||||
:searchable="true"
|
:searchable="true"
|
||||||
:on-search="searchTeachers"
|
:on-search="searchTeachers"
|
||||||
:error="errors.defaultTeacherId"
|
:error="errors.defaultTeacherId"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="course-form__cell course-form__cell--third">
|
||||||
|
<TextField
|
||||||
|
v-model="form.defaultCapacity"
|
||||||
|
name="defaultCapacity"
|
||||||
|
label="ظرفیت (نفر)"
|
||||||
|
inputmode="numeric"
|
||||||
|
:convert-digits="true"
|
||||||
|
:error="errors.defaultCapacity"
|
||||||
|
@blur="validateAt('defaultCapacity', form.defaultCapacity)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="course-form__cell course-form__cell--third">
|
||||||
|
<TextField
|
||||||
|
v-model="form.sessionsCount"
|
||||||
|
name="sessionsCount"
|
||||||
|
label="تعداد جلسه"
|
||||||
|
inputmode="numeric"
|
||||||
|
:convert-digits="true"
|
||||||
|
:error="errors.sessionsCount"
|
||||||
|
@blur="validateAt('sessionsCount', form.sessionsCount)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div class="course-form__cell course-form__cell--third">
|
<div class="course-form__cell course-form__cell--third">
|
||||||
<SelectField
|
<SelectField
|
||||||
v-model="form.prerequisites"
|
v-model="form.prerequisites"
|
||||||
@@ -69,25 +91,36 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="course-form__cell course-form__cell--third">
|
<div class="course-form__cell course-form__cell--third">
|
||||||
<TextField
|
<SelectField
|
||||||
v-model="form.defaultCapacity"
|
v-model="form.contentType"
|
||||||
name="defaultCapacity"
|
name="contentType"
|
||||||
label="ظرفیت (نفر)"
|
label="نوع فایل دوره"
|
||||||
inputmode="numeric"
|
:options="contentTypeOptions"
|
||||||
:convert-digits="true"
|
option-label="label"
|
||||||
:error="errors.defaultCapacity"
|
option-value="value"
|
||||||
@blur="validateAt('defaultCapacity', form.defaultCapacity)"
|
:error="errors.contentType"
|
||||||
|
@change="onContentTypeChange"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="course-form__cell course-form__cell--third course-form__toggle-cell">
|
|
||||||
<ToggleSwitch v-model="form.isActiveByDefault" label="دوره فعال باشد" />
|
|
||||||
</div>
|
|
||||||
<div class="course-form__cell course-form__cell--full">
|
<div class="course-form__cell course-form__cell--full">
|
||||||
<TextareaField
|
<TextareaField
|
||||||
v-model="form.description"
|
v-model="form.description"
|
||||||
name="description"
|
name="description"
|
||||||
label="توضیحات"
|
label="توضیحات دوره"
|
||||||
:row="5"
|
:row="5"
|
||||||
|
:error="errors.description"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="course-form__cell course-form__cell--full">
|
||||||
|
<label class="course-form__uploader-label">فایل دوره</label>
|
||||||
|
<FileUploader
|
||||||
|
v-model="contentFiles"
|
||||||
|
:accept="contentAccept"
|
||||||
|
:multiple="false"
|
||||||
|
:max-files="1"
|
||||||
|
@select="onContentSelect"
|
||||||
|
@remove="onContentRemove"
|
||||||
|
@error="onContentError"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -134,13 +167,14 @@ import SvgIcon from '@/components/icons/SvgIcon.vue'
|
|||||||
import TextField from '@/components/form/TextField.vue'
|
import TextField from '@/components/form/TextField.vue'
|
||||||
import SelectField from '@/components/form/SelectField.vue'
|
import SelectField from '@/components/form/SelectField.vue'
|
||||||
import LineTitleBlock from '@/components/LineTitleBlock.vue'
|
import LineTitleBlock from '@/components/LineTitleBlock.vue'
|
||||||
import ToggleSwitch from '@/components/form/ToggleSwitch.vue'
|
import FileUploader from '@/components/form/FileUploader.vue'
|
||||||
import ImageCropper from '@/components/form/ImageCropper.vue'
|
import ImageCropper from '@/components/form/ImageCropper.vue'
|
||||||
import { objectToFormData } from '@/utils/object-to-formdata'
|
import { objectToFormData } from '@/utils/object-to-formdata'
|
||||||
|
import { useUploadMediaMutation } from '@/services/query/auth'
|
||||||
import TextareaField from '@/components/form/TextareaField.vue'
|
import TextareaField from '@/components/form/TextareaField.vue'
|
||||||
import { useUploadTemporaryMutation } from '@/services/query/auth'
|
|
||||||
import { useAdminUsersListQuery } from '@/services/query/admin-users'
|
import { useAdminUsersListQuery } from '@/services/query/admin-users'
|
||||||
import { courseTemplateSchema } from '@/features/admin/courses/schema'
|
import { courseTemplateSchema } from '@/features/admin/courses/schema'
|
||||||
|
import { COURSE_CONTENT_TYPE, COURSE_CONTENT_TYPE_ACCEPT } from '@/enums'
|
||||||
import BoxedIconTitleBlock from '@/components/blocks/BoxedIconTitleBlock.vue'
|
import BoxedIconTitleBlock from '@/components/blocks/BoxedIconTitleBlock.vue'
|
||||||
import {
|
import {
|
||||||
adminCourseTemplatesKeys,
|
adminCourseTemplatesKeys,
|
||||||
@@ -155,23 +189,33 @@ const router = useRouter()
|
|||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
|
|
||||||
const courseId = computed(() => (route.params.id ? Number(route.params.id) : null))
|
const courseId = computed(() => (route.params.id ? Number(route.params.id) : null))
|
||||||
|
const termId = computed(() => (route.query.termId ? Number(route.query.termId) : null))
|
||||||
const isEditMode = computed(() => !!courseId.value)
|
const isEditMode = computed(() => !!courseId.value)
|
||||||
|
|
||||||
const form = ref({
|
const form = ref({
|
||||||
title: '',
|
title: '',
|
||||||
defaultTeacherId: '',
|
defaultTeacherId: '',
|
||||||
prerequisites: [],
|
|
||||||
defaultCapacity: '',
|
defaultCapacity: '',
|
||||||
|
sessionsCount: '',
|
||||||
|
prerequisites: [],
|
||||||
|
contentType: '',
|
||||||
|
contentMediaId: null,
|
||||||
description: '',
|
description: '',
|
||||||
imageId: null,
|
coverMediaId: null,
|
||||||
isActiveByDefault: false,
|
termId: termId.value,
|
||||||
})
|
})
|
||||||
|
|
||||||
const image = ref(null)
|
const image = ref(null)
|
||||||
|
const contentFiles = ref([])
|
||||||
|
|
||||||
const schema = courseTemplateSchema
|
const contentTypeOptions = Object.entries(COURSE_CONTENT_TYPE).map(([value, label]) => ({
|
||||||
|
value,
|
||||||
|
label,
|
||||||
|
}))
|
||||||
|
|
||||||
const { validate, validateAt, errors } = useYup(schema)
|
const contentAccept = computed(() => COURSE_CONTENT_TYPE_ACCEPT[form.value.contentType] || '*')
|
||||||
|
|
||||||
|
const { validate, validateAt, errors } = useYup(courseTemplateSchema)
|
||||||
|
|
||||||
const teacherSearch = ref('')
|
const teacherSearch = ref('')
|
||||||
const teacherFilters = computed(() => ({ name: teacherSearch.value }))
|
const teacherFilters = computed(() => ({ name: teacherSearch.value }))
|
||||||
@@ -222,24 +266,37 @@ watch(existingCourse, (course) => {
|
|||||||
form.value = {
|
form.value = {
|
||||||
title: course.title || '',
|
title: course.title || '',
|
||||||
defaultTeacherId: teacher?.id || course.defaultTeacherId || '',
|
defaultTeacherId: teacher?.id || course.defaultTeacherId || '',
|
||||||
prerequisites: prereqs.map((p) => p.courseId ?? p.id).filter(Boolean),
|
|
||||||
defaultCapacity: course.defaultCapacity ?? course.capacity ?? '',
|
defaultCapacity: course.defaultCapacity ?? course.capacity ?? '',
|
||||||
|
sessionsCount: course.sessionsCount ?? '',
|
||||||
|
prerequisites: prereqs.map((p) => p.courseId ?? p.id).filter(Boolean),
|
||||||
|
contentType: course.contentType || '',
|
||||||
|
contentMediaId: course.contentMediaId || null,
|
||||||
description: course.description || '',
|
description: course.description || '',
|
||||||
imageId: course.imageId || null,
|
coverMediaId: course.coverMediaId || null,
|
||||||
isActiveByDefault: course.isActiveByDefault ?? course.isActive ?? false,
|
termId: termId.value,
|
||||||
|
}
|
||||||
|
if (course.coverUrl) image.value = { url: course.coverUrl }
|
||||||
|
if (course.contentMedia) {
|
||||||
|
contentFiles.value = [
|
||||||
|
{
|
||||||
|
id: course.contentMedia.id,
|
||||||
|
name: course.contentMedia.name || course.contentMedia.fileName || 'file',
|
||||||
|
size: course.contentMedia.size ?? 0,
|
||||||
|
url: course.contentMedia.url,
|
||||||
|
},
|
||||||
|
]
|
||||||
}
|
}
|
||||||
if (course.image) image.value = { url: course.image }
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const uploadMutation = useUploadTemporaryMutation()
|
const uploadMutation = useUploadMediaMutation()
|
||||||
|
|
||||||
const onImageCropped = async (file) => {
|
const onImageCropped = async (file) => {
|
||||||
try {
|
try {
|
||||||
const formData = objectToFormData({ file, subType: 'avatar', context: 'course' })
|
const formData = objectToFormData({ file, purpose: 'cover', context: 'course' })
|
||||||
const response = await uploadMutation.mutateAsync(formData)
|
const response = await uploadMutation.mutateAsync(formData)
|
||||||
const payload = response?.data ?? response
|
const payload = response?.data ?? response
|
||||||
image.value = { url: payload?.url, uploadId: payload?.uploadId, ...payload }
|
image.value = { url: payload?.url, ...payload }
|
||||||
form.value.imageId = payload?.uploadId || payload?.id
|
form.value.coverMediaId = payload?.id
|
||||||
} catch {
|
} catch {
|
||||||
/* handled globally */
|
/* handled globally */
|
||||||
}
|
}
|
||||||
@@ -247,6 +304,38 @@ const onImageCropped = async (file) => {
|
|||||||
|
|
||||||
const onImageError = (msg) => toast.error(msg)
|
const onImageError = (msg) => toast.error(msg)
|
||||||
|
|
||||||
|
const onContentTypeChange = () => {
|
||||||
|
contentFiles.value = []
|
||||||
|
form.value.contentMediaId = null
|
||||||
|
}
|
||||||
|
|
||||||
|
const onContentSelect = async (files) => {
|
||||||
|
const file = files?.[0]
|
||||||
|
if (!file) return
|
||||||
|
try {
|
||||||
|
const formData = objectToFormData({
|
||||||
|
file,
|
||||||
|
purpose: 'content',
|
||||||
|
context: 'course',
|
||||||
|
type: form.value.contentType,
|
||||||
|
})
|
||||||
|
const response = await uploadMutation.mutateAsync(formData)
|
||||||
|
const payload = response?.data ?? response
|
||||||
|
const id = payload?.id ?? payload?.uploadId
|
||||||
|
contentFiles.value = [{ id, name: file.name, size: file.size, url: payload?.url }]
|
||||||
|
form.value.contentMediaId = id
|
||||||
|
} catch {
|
||||||
|
contentFiles.value = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onContentRemove = () => {
|
||||||
|
contentFiles.value = []
|
||||||
|
form.value.contentMediaId = null
|
||||||
|
}
|
||||||
|
|
||||||
|
const onContentError = (msg) => toast.error(msg)
|
||||||
|
|
||||||
const addMutation = useAddAdminCourseTemplateMutation()
|
const addMutation = useAddAdminCourseTemplateMutation()
|
||||||
const updateMutation = useUpdateAdminCourseTemplateMutation()
|
const updateMutation = useUpdateAdminCourseTemplateMutation()
|
||||||
|
|
||||||
@@ -345,9 +434,14 @@ const onCancel = () => router.push({ name: 'admin-courses' })
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__toggle-cell {
|
&__uploader-label {
|
||||||
display: flex;
|
display: block;
|
||||||
align-items: center;
|
margin-bottom: 0.5rem;
|
||||||
|
font-family: var(--font-family-fa);
|
||||||
|
font-weight: 300;
|
||||||
|
line-height: 1.5rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: var(--color-prim-gray);
|
||||||
}
|
}
|
||||||
|
|
||||||
&__divider {
|
&__divider {
|
||||||
|
|||||||
@@ -164,7 +164,7 @@ const { data: offeredData, isLoading: offeredPending } = useAdminCoursesListQuer
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
const templates = computed(() => templatesData.value?.data ?? [])
|
const templates = computed(() => templatesData.value?.data?.items ?? [])
|
||||||
const templatesPaginationMeta = computed(() => ({
|
const templatesPaginationMeta = computed(() => ({
|
||||||
page: templatesPagination.value.page,
|
page: templatesPagination.value.page,
|
||||||
perPage: templatesPagination.value.perPage,
|
perPage: templatesPagination.value.perPage,
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
import { array, boolean, object, string } from 'yup'
|
import { array, boolean, mixed, number, object, string } from 'yup'
|
||||||
|
|
||||||
export const courseTemplateSchema = object().shape({
|
export const courseTemplateSchema = object().shape({
|
||||||
title: string().required().min(3).max(255),
|
title: string().required().min(3).max(255),
|
||||||
defaultTeacherId: string().required(),
|
defaultTeacherId: mixed().required(),
|
||||||
defaultCapacity: string().required(),
|
defaultCapacity: number().required().min(1),
|
||||||
|
sessionsCount: number().required().min(1),
|
||||||
prerequisites: array().nullable().default([]),
|
prerequisites: array().nullable().default([]),
|
||||||
|
contentType: string().oneOf(['video', 'voice', 'text']).required(),
|
||||||
|
contentMediaId: number().nullable().notRequired(),
|
||||||
description: string().nullable().notRequired(),
|
description: string().nullable().notRequired(),
|
||||||
|
termId: string().nullable(),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const offeredCourseSchema = object().shape({
|
export const offeredCourseSchema = object().shape({
|
||||||
|
|||||||
@@ -236,7 +236,7 @@ import FileUploader from '@/components/form/FileUploader.vue'
|
|||||||
import { objectToFormData } from '@/utils/object-to-formdata'
|
import { objectToFormData } from '@/utils/object-to-formdata'
|
||||||
import TextareaField from '@/components/form/TextareaField.vue'
|
import TextareaField from '@/components/form/TextareaField.vue'
|
||||||
import { sessionSchema } from '@/features/admin/sessions/schema'
|
import { sessionSchema } from '@/features/admin/sessions/schema'
|
||||||
import { useUploadTemporaryMutation } from '@/services/query/auth'
|
import { useUploadMediaMutation } from '@/services/query/auth'
|
||||||
import DatePickerField from '@/components/form/DatePickerField.vue'
|
import DatePickerField from '@/components/form/DatePickerField.vue'
|
||||||
import BoxedIconTitleBlock from '@/components/blocks/BoxedIconTitleBlock.vue'
|
import BoxedIconTitleBlock from '@/components/blocks/BoxedIconTitleBlock.vue'
|
||||||
import { useAdminCourseTemplatesListQuery } from '@/services/query/admin-course-templates'
|
import { useAdminCourseTemplatesListQuery } from '@/services/query/admin-course-templates'
|
||||||
@@ -341,7 +341,7 @@ watch(existingSession, (session) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const uploadMutation = useUploadTemporaryMutation()
|
const uploadMutation = useUploadMediaMutation()
|
||||||
|
|
||||||
const onImageCropped = async (file) => {
|
const onImageCropped = async (file) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="term-item">
|
<div class="term-item">
|
||||||
<div class="term-item__main">
|
<div class="term-item__main">
|
||||||
<div v-if="term.image" class="term-item__image">
|
<div v-if="term.coverUrl" class="term-item__image">
|
||||||
<img :src="term.image" :alt="term.title" />
|
<img :src="term.coverUrl" :alt="term.title" />
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="term-item__image term-item__image--placeholder">
|
<div v-else class="term-item__image term-item__image--placeholder">
|
||||||
<SvgIcon name="book" :size="24" color="#bcbcbc" />
|
<SvgIcon name="book" :size="24" color="#bcbcbc" />
|
||||||
|
|||||||
@@ -42,20 +42,20 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="term-form__cell term-form__cell--third">
|
<div class="term-form__cell term-form__cell--third">
|
||||||
<DatePickerField
|
<DatePickerField
|
||||||
v-model="form.startDate"
|
v-model="form.startsAt"
|
||||||
name="startDate"
|
name="startsAt"
|
||||||
label="تاریخ شروع"
|
label="تاریخ شروع"
|
||||||
:min="todayIso"
|
:min="todayIso"
|
||||||
:error="errors.startDate"
|
:error="errors.startsAt"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="term-form__cell term-form__cell--third">
|
<div class="term-form__cell term-form__cell--third">
|
||||||
<DatePickerField
|
<DatePickerField
|
||||||
v-model="form.endDate"
|
v-model="form.endsAt"
|
||||||
name="endDate"
|
name="endsAt"
|
||||||
label="تاریخ پایان"
|
label="تاریخ پایان"
|
||||||
:min="todayIso"
|
:min="todayIso"
|
||||||
:error="errors.endDate"
|
:error="errors.endsAt"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="term-form__cell term-form__cell--full">
|
<div class="term-form__cell term-form__cell--full">
|
||||||
@@ -112,8 +112,8 @@ import { termSchema } from '@/features/admin/terms/schema'
|
|||||||
import LineTitleBlock from '@/components/LineTitleBlock.vue'
|
import LineTitleBlock from '@/components/LineTitleBlock.vue'
|
||||||
import ImageCropper from '@/components/form/ImageCropper.vue'
|
import ImageCropper from '@/components/form/ImageCropper.vue'
|
||||||
import { objectToFormData } from '@/utils/object-to-formdata'
|
import { objectToFormData } from '@/utils/object-to-formdata'
|
||||||
|
import { useUploadMediaMutation } from '@/services/query/auth'
|
||||||
import TextareaField from '@/components/form/TextareaField.vue'
|
import TextareaField from '@/components/form/TextareaField.vue'
|
||||||
import { useUploadTemporaryMutation } from '@/services/query/auth'
|
|
||||||
import DatePickerField from '@/components/form/DatePickerField.vue'
|
import DatePickerField from '@/components/form/DatePickerField.vue'
|
||||||
import BoxedIconTitleBlock from '@/components/blocks/BoxedIconTitleBlock.vue'
|
import BoxedIconTitleBlock from '@/components/blocks/BoxedIconTitleBlock.vue'
|
||||||
import {
|
import {
|
||||||
@@ -134,17 +134,16 @@ const todayIso = new Date().toISOString()
|
|||||||
|
|
||||||
const form = ref({
|
const form = ref({
|
||||||
title: '',
|
title: '',
|
||||||
startDate: '',
|
|
||||||
endDate: '',
|
|
||||||
description: '',
|
description: '',
|
||||||
imageId: null,
|
isActive: true,
|
||||||
|
startsAt: '',
|
||||||
|
endsAt: '',
|
||||||
|
coverMediaId: null,
|
||||||
})
|
})
|
||||||
|
|
||||||
const image = ref(null)
|
const image = ref(null)
|
||||||
|
|
||||||
const schema = termSchema
|
const { validate, validateAt, errors } = useYup(termSchema)
|
||||||
|
|
||||||
const { validate, validateAt, errors } = useYup(schema)
|
|
||||||
|
|
||||||
const { data: existingTerm } = useAdminTermQuery(termId, {
|
const { data: existingTerm } = useAdminTermQuery(termId, {
|
||||||
enabled: () => !!termId.value,
|
enabled: () => !!termId.value,
|
||||||
@@ -154,23 +153,24 @@ watch(existingTerm, (term) => {
|
|||||||
if (!term) return
|
if (!term) return
|
||||||
form.value = {
|
form.value = {
|
||||||
title: term.title || '',
|
title: term.title || '',
|
||||||
startDate: term.startDate || '',
|
|
||||||
endDate: term.endDate || '',
|
|
||||||
description: term.description || '',
|
description: term.description || '',
|
||||||
imageId: term.imageId || null,
|
isActive: term.isActive ?? true,
|
||||||
|
startsAt: term.startsAt || '',
|
||||||
|
endsAt: term.endsAt || '',
|
||||||
|
coverMediaId: term.coverMediaId || null,
|
||||||
}
|
}
|
||||||
if (term.image) image.value = { url: term.image }
|
if (term.coverUrl) image.value = { url: term.coverUrl }
|
||||||
})
|
})
|
||||||
|
|
||||||
const uploadMutation = useUploadTemporaryMutation()
|
const uploadMutation = useUploadMediaMutation()
|
||||||
|
|
||||||
const onImageCropped = async (file) => {
|
const onImageCropped = async (file) => {
|
||||||
try {
|
try {
|
||||||
const formData = objectToFormData({ file, subType: 'avatar', context: 'term' })
|
const formData = objectToFormData({ file, purpose: 'cover', context: 'term' })
|
||||||
const response = await uploadMutation.mutateAsync(formData)
|
const response = await uploadMutation.mutateAsync(formData)
|
||||||
const payload = response?.data ?? response
|
const payload = response?.data ?? response
|
||||||
image.value = { url: payload?.url, uploadId: payload?.uploadId, ...payload }
|
image.value = { url: payload?.url, ...payload }
|
||||||
form.value.imageId = payload?.uploadId || payload?.id
|
form.value.coverMediaId = payload?.id
|
||||||
} catch {
|
} catch {
|
||||||
/* handled globally */
|
/* handled globally */
|
||||||
}
|
}
|
||||||
@@ -191,7 +191,7 @@ const onSubmit = async () => {
|
|||||||
} else {
|
} else {
|
||||||
await addMutation.mutateAsync(payload)
|
await addMutation.mutateAsync(payload)
|
||||||
}
|
}
|
||||||
await queryClient.invalidateQueries({ queryKey: adminTermsKeys.all })
|
await queryClient.resetQueries({ queryKey: adminTermsKeys.all })
|
||||||
router.push({ name: 'admin-terms' })
|
router.push({ name: 'admin-terms' })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import { array, mixed, object, string } from 'yup'
|
import { array, boolean, mixed, number, object, string } from 'yup'
|
||||||
|
|
||||||
export const termSchema = object().shape({
|
export const termSchema = object().shape({
|
||||||
title: string().required().min(3),
|
title: string().required().min(3),
|
||||||
startDate: string().required(),
|
|
||||||
endDate: string().required(),
|
|
||||||
description: string().nullable().notRequired(),
|
description: string().nullable().notRequired(),
|
||||||
|
isActive: boolean().default(true),
|
||||||
|
startsAt: string().required(),
|
||||||
|
endsAt: string().required(),
|
||||||
|
coverMediaId: number().nullable().notRequired(),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const addTermStudentSchema = object().shape({
|
export const addTermStudentSchema = object().shape({
|
||||||
|
|||||||
@@ -72,11 +72,7 @@ import SvgIcon from '@/components/icons/SvgIcon.vue'
|
|||||||
import BaseButton from '@/components/BaseButton.vue'
|
import BaseButton from '@/components/BaseButton.vue'
|
||||||
import { CHANGEABLE_ROLES, ROLE_LABELS } from '@/enums'
|
import { CHANGEABLE_ROLES, ROLE_LABELS } from '@/enums'
|
||||||
import DropdownMenu from '@/components/DropdownMenu.vue'
|
import DropdownMenu from '@/components/DropdownMenu.vue'
|
||||||
import {
|
import { adminUsersKeys, useUpdateAdminUserRoleMutation } from '@/services/query/admin-users'
|
||||||
adminUsersKeys,
|
|
||||||
useAdminRolesListQuery,
|
|
||||||
useUpdateAdminUserRoleMutation,
|
|
||||||
} from '@/services/query/admin-users'
|
|
||||||
|
|
||||||
defineProps({
|
defineProps({
|
||||||
users: { type: Array, required: true },
|
users: { type: Array, required: true },
|
||||||
@@ -124,15 +120,6 @@ const onMoreClick = (event, user) => {
|
|||||||
const roleMenuOpen = ref(false)
|
const roleMenuOpen = ref(false)
|
||||||
const roleMenuTrigger = ref(null)
|
const roleMenuTrigger = ref(null)
|
||||||
|
|
||||||
const { data: rolesList = ref([]) } = useAdminRolesListQuery()
|
|
||||||
const roleIdByName = computed(() => {
|
|
||||||
const map = {}
|
|
||||||
;(rolesList.value ?? []).forEach((r) => {
|
|
||||||
if (r?.name) map[r.name] = r.id
|
|
||||||
})
|
|
||||||
return map
|
|
||||||
})
|
|
||||||
|
|
||||||
const updateRoleMutation = useUpdateAdminUserRoleMutation()
|
const updateRoleMutation = useUpdateAdminUserRoleMutation()
|
||||||
|
|
||||||
const invalidateUsers = () => queryClient.invalidateQueries({ queryKey: adminUsersKeys.all })
|
const invalidateUsers = () => queryClient.invalidateQueries({ queryKey: adminUsersKeys.all })
|
||||||
@@ -150,10 +137,11 @@ const onRoleClick = (event, user) => {
|
|||||||
|
|
||||||
const onChangeRole = (targetRoleName) => {
|
const onChangeRole = (targetRoleName) => {
|
||||||
const user = activeUser.value
|
const user = activeUser.value
|
||||||
if (!user) return
|
if (!user || !targetRoleName) return
|
||||||
const roleId = roleIdByName.value[targetRoleName]
|
updateRoleMutation.mutate(
|
||||||
const payload = roleId ? { roleId } : { role: targetRoleName }
|
{ id: user.id, payload: { roles: [targetRoleName] } },
|
||||||
updateRoleMutation.mutate({ id: user.id, payload }, { onSuccess: invalidateUsers })
|
{ onSuccess: invalidateUsers }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const roleMenuItems = computed(() => {
|
const roleMenuItems = computed(() => {
|
||||||
|
|||||||
@@ -233,7 +233,7 @@ import { GENDER, MARITAL_STATUS, ROLE_LABELS } from '@/enums'
|
|||||||
import { objectToFormData } from '@/utils/object-to-formdata'
|
import { objectToFormData } from '@/utils/object-to-formdata'
|
||||||
import TextareaField from '@/components/form/TextareaField.vue'
|
import TextareaField from '@/components/form/TextareaField.vue'
|
||||||
import PasswordField from '@/components/form/PasswordField.vue'
|
import PasswordField from '@/components/form/PasswordField.vue'
|
||||||
import { useUploadTemporaryMutation } from '@/services/query/auth'
|
import { useUploadMediaMutation } from '@/services/query/auth'
|
||||||
import DatePickerField from '@/components/form/DatePickerField.vue'
|
import DatePickerField from '@/components/form/DatePickerField.vue'
|
||||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||||
import BoxedIconTitleBlock from '@/components/blocks/BoxedIconTitleBlock.vue'
|
import BoxedIconTitleBlock from '@/components/blocks/BoxedIconTitleBlock.vue'
|
||||||
@@ -348,7 +348,7 @@ watch(existingUser, (user) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const uploadMutation = useUploadTemporaryMutation()
|
const uploadMutation = useUploadMediaMutation()
|
||||||
|
|
||||||
const onAvatarCropped = async (file) => {
|
const onAvatarCropped = async (file) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ import useModal from '@/composables/useModal'
|
|||||||
import { computed, onBeforeUnmount, ref } from 'vue'
|
import { computed, onBeforeUnmount, ref } from 'vue'
|
||||||
import SvgIcon from '@/components/icons/SvgIcon.vue'
|
import SvgIcon from '@/components/icons/SvgIcon.vue'
|
||||||
import { objectToFormData } from '@/utils/object-to-formdata'
|
import { objectToFormData } from '@/utils/object-to-formdata'
|
||||||
import { useUploadTemporaryMutation } from '@/services/query/auth'
|
import { useUploadMediaMutation } from '@/services/query/auth'
|
||||||
import PermissionModal from '@/features/auth/components/studentRegister/PermissionModal.vue'
|
import PermissionModal from '@/features/auth/components/studentRegister/PermissionModal.vue'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@@ -123,7 +123,7 @@ const formattedTime = computed(() => {
|
|||||||
return `${mm}:${ss}`
|
return `${mm}:${ss}`
|
||||||
})
|
})
|
||||||
|
|
||||||
const uploadMutation = useUploadTemporaryMutation()
|
const uploadMutation = useUploadMediaMutation()
|
||||||
|
|
||||||
const stopTracks = () => {
|
const stopTracks = () => {
|
||||||
if (mediaStream) {
|
if (mediaStream) {
|
||||||
|
|||||||
@@ -167,7 +167,7 @@ import LineTitleBlock from '@/components/LineTitleBlock.vue'
|
|||||||
import ImageCropper from '@/components/form/ImageCropper.vue'
|
import ImageCropper from '@/components/form/ImageCropper.vue'
|
||||||
import { objectToFormData } from '@/utils/object-to-formdata'
|
import { objectToFormData } from '@/utils/object-to-formdata'
|
||||||
import TextareaField from '@/components/form/TextareaField.vue'
|
import TextareaField from '@/components/form/TextareaField.vue'
|
||||||
import { useUploadTemporaryMutation } from '@/services/query/auth'
|
import { useUploadMediaMutation } from '@/services/query/auth'
|
||||||
import DatePickerField from '@/components/form/DatePickerField.vue'
|
import DatePickerField from '@/components/form/DatePickerField.vue'
|
||||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||||
import { GENDER, MARITAL_STATUS, STUDENT_REGISTRATION } from '@/enums'
|
import { GENDER, MARITAL_STATUS, STUDENT_REGISTRATION } from '@/enums'
|
||||||
@@ -242,7 +242,7 @@ watch(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
const uploadMutation = useUploadTemporaryMutation()
|
const uploadMutation = useUploadMediaMutation()
|
||||||
|
|
||||||
const onAvatarCropped = async (file) => {
|
const onAvatarCropped = async (file) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -223,9 +223,9 @@ import BoxedIconTitleBlock from '@/components/blocks/BoxedIconTitleBlock.vue'
|
|||||||
import { useGetCitiesOfProvinceQuery, useGetProvincesQuery } from '@/services/query/common'
|
import { useGetCitiesOfProvinceQuery, useGetProvincesQuery } from '@/services/query/common'
|
||||||
import {
|
import {
|
||||||
authKeys,
|
authKeys,
|
||||||
useGetProfileQuery,
|
useGetMeQuery,
|
||||||
useUpdateProfileMutation,
|
useUpdateProfileMutation,
|
||||||
useUploadTemporaryMutation,
|
useUploadMediaMutation,
|
||||||
} from '@/services/query/auth'
|
} from '@/services/query/auth'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -291,7 +291,7 @@ watch(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
const { data: profile } = useGetProfileQuery()
|
const { data: profile } = useGetMeQuery()
|
||||||
|
|
||||||
watch(profile, (user) => {
|
watch(profile, (user) => {
|
||||||
if (!user) return
|
if (!user) return
|
||||||
@@ -315,7 +315,7 @@ watch(profile, (user) => {
|
|||||||
if (user.avatarUrl) avatar.value = { url: user.avatarUrl }
|
if (user.avatarUrl) avatar.value = { url: user.avatarUrl }
|
||||||
})
|
})
|
||||||
|
|
||||||
const uploadMutation = useUploadTemporaryMutation()
|
const uploadMutation = useUploadMediaMutation()
|
||||||
|
|
||||||
const onAvatarCropped = async (file) => {
|
const onAvatarCropped = async (file) => {
|
||||||
try {
|
try {
|
||||||
@@ -341,7 +341,7 @@ const onSubmit = async () => {
|
|||||||
delete payload.passwordConfirmation
|
delete payload.passwordConfirmation
|
||||||
}
|
}
|
||||||
await updateMutation.mutateAsync(payload)
|
await updateMutation.mutateAsync(payload)
|
||||||
await queryClient.invalidateQueries({ queryKey: authKeys.profile() })
|
await queryClient.invalidateQueries({ queryKey: authKeys.me() })
|
||||||
toast.success('پروفایل با موفقیت بهروز شد')
|
toast.success('پروفایل با موفقیت بهروز شد')
|
||||||
router.push({ name: 'student-dashboard' })
|
router.push({ name: 'student-dashboard' })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,11 @@
|
|||||||
import { http } from '@/services/api/http'
|
import { http } from '@/services/api/http'
|
||||||
import { buildUrl, endpoints } from '@/services/api/endpoints'
|
import { buildUrl, endpoints } from '@/services/api/endpoints'
|
||||||
|
|
||||||
export const apiGetAdminCourseTemplates = (params) =>
|
export const apiGetAdminCourseTemplates = (params) => http.get(endpoints.getCoursesList, { params })
|
||||||
http.get(endpoints.getCourseTemplatesList, { params })
|
|
||||||
|
|
||||||
export const apiShowAdminCourseTemplate = (id) =>
|
export const apiShowAdminCourseTemplate = (id) => http.get(buildUrl(endpoints.showCourse, { id }))
|
||||||
http.get(buildUrl(endpoints.showCourseTemplate, { id }))
|
|
||||||
|
|
||||||
export const apiAddAdminCourseTemplate = (payload) =>
|
export const apiAddAdminCourseTemplate = (payload) => http.post(endpoints.addNewCourse, payload)
|
||||||
http.post(endpoints.addNewCourseTemplate, payload)
|
|
||||||
|
|
||||||
export const apiUpdateAdminCourseTemplate = (id, payload) =>
|
export const apiUpdateAdminCourseTemplate = (id, payload) =>
|
||||||
http.put(buildUrl(endpoints.updateCourseTemplate, { id }), payload)
|
http.put(buildUrl(endpoints.updateCourseTemplate, { id }), payload)
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export const apiShowAdminCourse = (id) => http.get(buildUrl(endpoints.showCourse
|
|||||||
export const apiAddAdminCourse = (payload) => http.post(endpoints.addNewCourse, payload)
|
export const apiAddAdminCourse = (payload) => http.post(endpoints.addNewCourse, payload)
|
||||||
|
|
||||||
export const apiUpdateAdminCourse = (id, payload) =>
|
export const apiUpdateAdminCourse = (id, payload) =>
|
||||||
http.put(buildUrl(endpoints.updateCourse, { id }), payload)
|
http.patch(buildUrl(endpoints.updateCourse, { id }), payload)
|
||||||
|
|
||||||
export const apiDeleteAdminCourse = (id) => http.delete(buildUrl(endpoints.deleteCourse, { id }))
|
export const apiDeleteAdminCourse = (id) => http.delete(buildUrl(endpoints.deleteCourse, { id }))
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export const apiShowAdminSession = (id) => http.get(buildUrl(endpoints.showSessi
|
|||||||
export const apiAddAdminSession = (payload) => http.post(endpoints.addNewSession, payload)
|
export const apiAddAdminSession = (payload) => http.post(endpoints.addNewSession, payload)
|
||||||
|
|
||||||
export const apiUpdateAdminSession = (id, payload) =>
|
export const apiUpdateAdminSession = (id, payload) =>
|
||||||
http.put(buildUrl(endpoints.updateSession, { id }), payload)
|
http.patch(buildUrl(endpoints.updateSession, { id }), payload)
|
||||||
|
|
||||||
export const apiDeleteAdminSession = (id) => http.delete(buildUrl(endpoints.deleteSession, { id }))
|
export const apiDeleteAdminSession = (id) => http.delete(buildUrl(endpoints.deleteSession, { id }))
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export const apiShowAdminTerm = (id) => http.get(buildUrl(endpoints.showTerm, {
|
|||||||
export const apiAddAdminTerm = (payload) => http.post(endpoints.addNewTerm, payload)
|
export const apiAddAdminTerm = (payload) => http.post(endpoints.addNewTerm, payload)
|
||||||
|
|
||||||
export const apiUpdateAdminTerm = (id, payload) =>
|
export const apiUpdateAdminTerm = (id, payload) =>
|
||||||
http.put(buildUrl(endpoints.updateTerm, { id }), payload)
|
http.patch(buildUrl(endpoints.updateTerm, { id }), payload)
|
||||||
|
|
||||||
export const apiDeleteAdminTerm = (id) => http.delete(buildUrl(endpoints.deleteTerm, { id }))
|
export const apiDeleteAdminTerm = (id) => http.delete(buildUrl(endpoints.deleteTerm, { id }))
|
||||||
|
|
||||||
|
|||||||
@@ -8,10 +8,10 @@ export const apiShowAdminUser = (id) => http.get(buildUrl(endpoints.showUserDeta
|
|||||||
export const apiAddAdminUser = (payload) => http.post(endpoints.addNewUser, payload)
|
export const apiAddAdminUser = (payload) => http.post(endpoints.addNewUser, payload)
|
||||||
|
|
||||||
export const apiUpdateAdminUser = (id, payload) =>
|
export const apiUpdateAdminUser = (id, payload) =>
|
||||||
http.put(buildUrl(endpoints.updateUser, { id }), payload)
|
http.patch(buildUrl(endpoints.updateUser, { id }), payload)
|
||||||
|
|
||||||
export const apiUpdateAdminUserRole = (id, payload) =>
|
export const apiUpdateAdminUserRole = (id, payload) =>
|
||||||
http.post(buildUrl(endpoints.updateUserRole, { id }), payload)
|
http.patch(buildUrl(endpoints.updateUserRole, { id }), payload)
|
||||||
|
|
||||||
export const apiChangeAdminUserStatus = (id, payload) =>
|
export const apiChangeAdminUserStatus = (id, payload) =>
|
||||||
http.post(buildUrl(endpoints.changeUserStatus, { id }), payload)
|
http.post(buildUrl(endpoints.changeUserStatus, { id }), payload)
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ export const apiResetPassword = (payload) => http.post(endpoints.resetPassword,
|
|||||||
|
|
||||||
export const apiLogout = () => http.post(endpoints.logout)
|
export const apiLogout = () => http.post(endpoints.logout)
|
||||||
|
|
||||||
export const apiGetProfile = () => http.get(endpoints.getProfile)
|
export const apiGetMe = () => http.get(endpoints.me)
|
||||||
|
|
||||||
export const apiCompleteProfile = (payload) => http.post(endpoints.completeProfile, payload)
|
export const apiCompleteProfile = (payload) => http.post(endpoints.completeProfile, payload)
|
||||||
|
|
||||||
@@ -30,4 +30,4 @@ export const apiUpdateProfile = (payload) => http.put(endpoints.updateProfile, p
|
|||||||
export const apiGetRegistrationQuestionVideo = () =>
|
export const apiGetRegistrationQuestionVideo = () =>
|
||||||
http.get(endpoints.getRegistrationQuestionVideo)
|
http.get(endpoints.getRegistrationQuestionVideo)
|
||||||
|
|
||||||
export const apiUploadTemporary = (formData) => http.post(endpoints.uploadMediaTemporary, formData)
|
export const apiUploadMedia = (formData) => http.post(endpoints.uploadMedia, formData)
|
||||||
|
|||||||
@@ -10,12 +10,12 @@ export const endpoints = {
|
|||||||
verifyForgotPasswordCode: '/verify-forgot-password-code',
|
verifyForgotPasswordCode: '/verify-forgot-password-code',
|
||||||
resetPassword: '/reset-password',
|
resetPassword: '/reset-password',
|
||||||
logout: '/logout',
|
logout: '/logout',
|
||||||
uploadMediaTemporary: '/upload-temp',
|
uploadMedia: '/media',
|
||||||
|
|
||||||
provinceList: '/provinces',
|
provinceList: '/provinces',
|
||||||
citiesList: '/provinces/:provinceId/cities',
|
citiesList: '/provinces/:provinceId/cities',
|
||||||
|
|
||||||
getProfile: '/profile',
|
me: '/auth/me',
|
||||||
updateProfile: '/profile',
|
updateProfile: '/profile',
|
||||||
getStudentCourses: '/student/courses',
|
getStudentCourses: '/student/courses',
|
||||||
showStudentCourse: '/student/courses/:id',
|
showStudentCourse: '/student/courses/:id',
|
||||||
@@ -59,11 +59,11 @@ export const endpoints = {
|
|||||||
changeUserStatus: '/admin/users/:id/status',
|
changeUserStatus: '/admin/users/:id/status',
|
||||||
deleteUser: '/admin/users/:id',
|
deleteUser: '/admin/users/:id',
|
||||||
|
|
||||||
getTermsList: '/admin/terms',
|
getTermsList: '/terms',
|
||||||
addNewTerm: '/admin/terms',
|
addNewTerm: '/terms',
|
||||||
showTerm: '/admin/terms/:id',
|
showTerm: '/terms/:id',
|
||||||
updateTerm: '/admin/terms/:id',
|
updateTerm: '/terms/:id',
|
||||||
deleteTerm: '/admin/terms/:id',
|
deleteTerm: '/terms/:id',
|
||||||
cloneTerm: '/admin/terms/:id/clone',
|
cloneTerm: '/admin/terms/:id/clone',
|
||||||
changeStatusTerm: '/admin/terms/:id/status',
|
changeStatusTerm: '/admin/terms/:id/status',
|
||||||
|
|
||||||
@@ -76,18 +76,18 @@ export const endpoints = {
|
|||||||
addCourseTerm: '/admin/terms/:termId/courses',
|
addCourseTerm: '/admin/terms/:termId/courses',
|
||||||
removeCourseTerm: '/admin/terms/:termId/courses/:courseId',
|
removeCourseTerm: '/admin/terms/:termId/courses/:courseId',
|
||||||
|
|
||||||
getCoursesList: '/admin/courses',
|
getCoursesList: '/courses',
|
||||||
addNewCourse: '/admin/courses',
|
addNewCourse: '/courses',
|
||||||
showCourse: '/admin/courses/:id',
|
showCourse: '/courses/:id',
|
||||||
updateCourse: '/admin/courses/:id',
|
updateCourse: '/courses/:id',
|
||||||
deleteCourse: '/admin/courses/:id',
|
deleteCourse: '/courses/:id',
|
||||||
changeStatusCourse: '/admin/courses/:id/toggle-status',
|
changeStatusCourse: '/admin/courses/:id/toggle-status',
|
||||||
|
|
||||||
getCourseTemplatesList: '/admin/course-templates',
|
getCourseTemplatesList: '/admin/courses',
|
||||||
addNewCourseTemplate: '/admin/course-templates',
|
addNewCourseTemplate: '/admin/courses',
|
||||||
showCourseTemplate: '/admin/course-templates/:id',
|
showCourseTemplate: '/admin/courses/:id',
|
||||||
updateCourseTemplate: '/admin/course-templates/:id',
|
updateCourseTemplate: '/admin/courses/:id',
|
||||||
deleteCourseTemplate: '/admin/course-templates/:id',
|
deleteCourseTemplate: '/admin/courses/:id',
|
||||||
changeStatusCourseTemplate: '/admin/course-templates/:id/status',
|
changeStatusCourseTemplate: '/admin/course-templates/:id/status',
|
||||||
|
|
||||||
listTemplateStudents: '/admin/course-templates/:templateId/students',
|
listTemplateStudents: '/admin/course-templates/:templateId/students',
|
||||||
@@ -98,11 +98,11 @@ export const endpoints = {
|
|||||||
attachTemplateSession: '/admin/course-templates/:templateId/sessions',
|
attachTemplateSession: '/admin/course-templates/:templateId/sessions',
|
||||||
detachTemplateSession: '/admin/course-templates/:templateId/sessions/:sessionId',
|
detachTemplateSession: '/admin/course-templates/:templateId/sessions/:sessionId',
|
||||||
|
|
||||||
getSessionsList: '/admin/sessions',
|
getSessionsList: '/sessions',
|
||||||
addNewSession: '/admin/sessions',
|
addNewSession: '/sessions',
|
||||||
showSession: '/admin/sessions/:id',
|
showSession: '/sessions/:id',
|
||||||
updateSession: '/admin/sessions/:id',
|
updateSession: '/sessions/:id',
|
||||||
deleteSession: '/admin/sessions/:id',
|
deleteSession: '/sessions/:id',
|
||||||
changeStatusSession: '/admin/sessions/:id/toggle-status',
|
changeStatusSession: '/admin/sessions/:id/toggle-status',
|
||||||
|
|
||||||
getSessionsAttendance: '/admin/sessions/:sessionId/attendances',
|
getSessionsAttendance: '/admin/sessions/:sessionId/attendances',
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
const TOKEN = 'user_token'
|
const TOKEN = 'accessToken'
|
||||||
const REFRESH_TOKEN = 'refresh_user_token'
|
const REFRESH_TOKEN = 'refreshToken'
|
||||||
const USER_INFO = 'user_info'
|
const USER_INFO = 'userInfo'
|
||||||
const STUDENT_REGISTRATION_INFO = 'StudentRegistration'
|
const STUDENT_REGISTRATION_INFO = 'StudentRegistration'
|
||||||
|
|
||||||
export const tokenService = {
|
export const tokenService = {
|
||||||
|
|||||||
@@ -37,53 +37,95 @@ export const adminCourseTemplates = [
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
// Offered courses carry both the spec keys (term_id/teacher_id/
|
||||||
|
// description/capacity/is_active/cover_url) and the UI-only keys the
|
||||||
|
// current frontend reads (image, template/templateId, term/teacher
|
||||||
|
// nested objects, prerequisitesCount, startDate, endDate). See
|
||||||
|
// docs/backend-api-todo.md.
|
||||||
|
const makeTeacherSnapshot = (overrides) => ({
|
||||||
|
id: overrides.id,
|
||||||
|
// --- spec ---
|
||||||
|
name: overrides.name ?? `${overrides.firstName ?? ''} ${overrides.lastName ?? ''}`.trim(),
|
||||||
|
email: overrides.email ?? '',
|
||||||
|
phone: overrides.phone ?? null,
|
||||||
|
roles: overrides.roles ?? ['teacher'],
|
||||||
|
avatarUrl: overrides.avatarUrl ?? null,
|
||||||
|
avatarDownloadUrl: overrides.avatarDownloadUrl ?? null,
|
||||||
|
createdAt: overrides.createdAt ?? '',
|
||||||
|
// --- ui-only ---
|
||||||
|
firstName: overrides.firstName ?? '',
|
||||||
|
lastName: overrides.lastName ?? '',
|
||||||
|
fullName:
|
||||||
|
overrides.fullName ?? `${overrides.firstName ?? ''} ${overrides.lastName ?? ''}`.trim(),
|
||||||
|
})
|
||||||
|
|
||||||
export const adminOfferedCourses = [
|
export const adminOfferedCourses = [
|
||||||
{
|
{
|
||||||
|
// --- spec ---
|
||||||
id: 11,
|
id: 11,
|
||||||
|
termId: 1,
|
||||||
|
teacherId: 5,
|
||||||
title: 'اصول اخلاق اسلامی - پاییز',
|
title: 'اصول اخلاق اسلامی - پاییز',
|
||||||
|
description: 'دوره مقدماتی اخلاق اسلامی برای ترم پاییز.',
|
||||||
|
capacity: 30,
|
||||||
|
isActive: true,
|
||||||
|
coverUrl: 'https://picsum.photos/seed/offered1/200/200',
|
||||||
|
|
||||||
|
// --- ui-only ---
|
||||||
image: 'https://picsum.photos/seed/offered1/200/200',
|
image: 'https://picsum.photos/seed/offered1/200/200',
|
||||||
template: { id: 1, title: 'اصول اخلاق اسلامی' },
|
template: { id: 1, title: 'اصول اخلاق اسلامی' },
|
||||||
templateId: 1,
|
templateId: 1,
|
||||||
term: { id: 1, title: 'ترم پاییز ۱۴۰۴' },
|
term: { id: 1, title: 'ترم پاییز ۱۴۰۴' },
|
||||||
termId: 1,
|
teacher: makeTeacherSnapshot({ id: 5, firstName: 'علی', lastName: 'حسنی' }),
|
||||||
teacher: { id: 5, firstName: 'علی', lastName: 'حسنی', fullName: 'علی حسنی' },
|
|
||||||
capacity: 30,
|
|
||||||
isActive: true,
|
|
||||||
prerequisitesCount: 0,
|
prerequisitesCount: 0,
|
||||||
startDate: '2025-09-23T00:00:00.000Z',
|
startDate: '2025-09-23T00:00:00.000Z',
|
||||||
endDate: '2025-11-20T00:00:00.000Z',
|
endDate: '2025-11-20T00:00:00.000Z',
|
||||||
createdAt: '2025-09-01T08:00:00.000Z',
|
createdAt: '2025-09-01T08:00:00.000Z',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
// --- spec ---
|
||||||
id: 12,
|
id: 12,
|
||||||
|
termId: 1,
|
||||||
|
teacherId: 6,
|
||||||
title: 'مفاهیم قرآنی - پاییز',
|
title: 'مفاهیم قرآنی - پاییز',
|
||||||
|
description: 'مرور مفاهیم قرآنی به همراه تفسیر مختصر.',
|
||||||
|
capacity: 25,
|
||||||
|
isActive: true,
|
||||||
|
coverUrl: 'https://picsum.photos/seed/offered2/200/200',
|
||||||
|
|
||||||
|
// --- ui-only ---
|
||||||
image: 'https://picsum.photos/seed/offered2/200/200',
|
image: 'https://picsum.photos/seed/offered2/200/200',
|
||||||
template: { id: 2, title: 'مفاهیم قرآنی' },
|
template: { id: 2, title: 'مفاهیم قرآنی' },
|
||||||
templateId: 2,
|
templateId: 2,
|
||||||
term: { id: 1, title: 'ترم پاییز ۱۴۰۴' },
|
term: { id: 1, title: 'ترم پاییز ۱۴۰۴' },
|
||||||
termId: 1,
|
teacher: makeTeacherSnapshot({ id: 6, firstName: 'حسین', lastName: 'مرادی' }),
|
||||||
teacher: { id: 6, firstName: 'حسین', lastName: 'مرادی', fullName: 'حسین مرادی' },
|
|
||||||
capacity: 25,
|
|
||||||
isActive: true,
|
|
||||||
prerequisitesCount: 1,
|
prerequisitesCount: 1,
|
||||||
startDate: '2025-10-01T00:00:00.000Z',
|
startDate: '2025-10-01T00:00:00.000Z',
|
||||||
endDate: '2025-12-15T00:00:00.000Z',
|
endDate: '2025-12-15T00:00:00.000Z',
|
||||||
createdAt: '2025-09-10T08:00:00.000Z',
|
createdAt: '2025-09-10T08:00:00.000Z',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
// --- spec ---
|
||||||
id: 13,
|
id: 13,
|
||||||
|
termId: 2,
|
||||||
|
teacherId: 7,
|
||||||
title: 'فقه عبادات - زمستان',
|
title: 'فقه عبادات - زمستان',
|
||||||
|
description: 'مرور احکام عملی نماز و روزه برای ترم زمستان.',
|
||||||
|
capacity: 20,
|
||||||
|
isActive: false,
|
||||||
|
coverUrl: '',
|
||||||
|
|
||||||
|
// --- ui-only ---
|
||||||
image: '',
|
image: '',
|
||||||
template: { id: 3, title: 'فقه عبادات' },
|
template: { id: 3, title: 'فقه عبادات' },
|
||||||
templateId: 3,
|
templateId: 3,
|
||||||
term: { id: 2, title: 'ترم زمستان ۱۴۰۴' },
|
term: { id: 2, title: 'ترم زمستان ۱۴۰۴' },
|
||||||
termId: 2,
|
teacher: makeTeacherSnapshot({ id: 7, firstName: 'مهدی', lastName: 'سهرابی' }),
|
||||||
teacher: { id: 7, firstName: 'مهدی', lastName: 'سهرابی', fullName: 'مهدی سهرابی' },
|
|
||||||
capacity: 20,
|
|
||||||
isActive: false,
|
|
||||||
prerequisitesCount: 0,
|
prerequisitesCount: 0,
|
||||||
startDate: '2026-01-22T00:00:00.000Z',
|
startDate: '2026-01-22T00:00:00.000Z',
|
||||||
endDate: '2026-03-15T00:00:00.000Z',
|
endDate: '2026-03-15T00:00:00.000Z',
|
||||||
createdAt: '2025-12-10T08:00:00.000Z',
|
createdAt: '2025-12-10T08:00:00.000Z',
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
export { makeTeacherSnapshot }
|
||||||
|
|||||||
@@ -1,6 +1,50 @@
|
|||||||
|
// Maps the FE's 7-value `sessionType` to the spec's 3-value `type`.
|
||||||
|
const SESSION_TYPE_TO_SPEC = {
|
||||||
|
in_person: 'offline',
|
||||||
|
online: 'online',
|
||||||
|
video: 'content',
|
||||||
|
audio: 'content',
|
||||||
|
text: 'content',
|
||||||
|
slide: 'content',
|
||||||
|
pdf: 'content',
|
||||||
|
}
|
||||||
|
|
||||||
|
const makeSession = (overrides) => {
|
||||||
|
const sessionType = overrides.sessionType ?? ''
|
||||||
|
const sessionConfig = overrides.sessionConfig ?? {}
|
||||||
|
const startsAt = overrides.startsAt ?? sessionConfig.startTime ?? null
|
||||||
|
const location = overrides.location ?? sessionConfig.location ?? null
|
||||||
|
const link = overrides.link ?? sessionConfig.meetingLink ?? null
|
||||||
|
return {
|
||||||
|
// --- spec ---
|
||||||
|
id: overrides.id,
|
||||||
|
courseId: overrides.courseId ?? null,
|
||||||
|
title: overrides.title ?? '',
|
||||||
|
description: overrides.description ?? '',
|
||||||
|
type: overrides.type ?? SESSION_TYPE_TO_SPEC[sessionType] ?? null,
|
||||||
|
startsAt,
|
||||||
|
location,
|
||||||
|
link,
|
||||||
|
media: overrides.media ?? [],
|
||||||
|
|
||||||
|
// --- ui-only ---
|
||||||
|
image: overrides.image ?? '',
|
||||||
|
courseTemplate: overrides.courseTemplate ?? null,
|
||||||
|
sessionType,
|
||||||
|
sessionTypeFa: overrides.sessionTypeFa ?? '',
|
||||||
|
durationMinutes: overrides.durationMinutes ?? 0,
|
||||||
|
order: overrides.order ?? 1,
|
||||||
|
sessionConfig,
|
||||||
|
materials: overrides.materials ?? [],
|
||||||
|
usedInTerms: overrides.usedInTerms ?? [],
|
||||||
|
createdAt: overrides.createdAt ?? '',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const adminSessions = [
|
export const adminSessions = [
|
||||||
{
|
makeSession({
|
||||||
id: 101,
|
id: 101,
|
||||||
|
courseId: 11,
|
||||||
title: 'مقدمهای بر اخلاق اسلامی',
|
title: 'مقدمهای بر اخلاق اسلامی',
|
||||||
description: 'جلسه نخست؛ تعاریف و چارچوب دوره.',
|
description: 'جلسه نخست؛ تعاریف و چارچوب دوره.',
|
||||||
image: 'https://picsum.photos/seed/session1/200/200',
|
image: 'https://picsum.photos/seed/session1/200/200',
|
||||||
@@ -13,15 +57,14 @@ export const adminSessions = [
|
|||||||
startTime: '2025-09-25T16:00:00.000Z',
|
startTime: '2025-09-25T16:00:00.000Z',
|
||||||
location: 'سالن آمفیتئاتر ۲ - مدرسه قم',
|
location: 'سالن آمفیتئاتر ۲ - مدرسه قم',
|
||||||
},
|
},
|
||||||
materials: [],
|
|
||||||
usedInTerms: [{ termId: 1 }],
|
usedInTerms: [{ termId: 1 }],
|
||||||
createdAt: '2025-09-10T08:00:00.000Z',
|
createdAt: '2025-09-10T08:00:00.000Z',
|
||||||
},
|
}),
|
||||||
{
|
makeSession({
|
||||||
id: 102,
|
id: 102,
|
||||||
|
courseId: 12,
|
||||||
title: 'تفسیر سوره حمد',
|
title: 'تفسیر سوره حمد',
|
||||||
description: 'تحلیل آیات سوره حمد.',
|
description: 'تحلیل آیات سوره حمد.',
|
||||||
image: '',
|
|
||||||
courseTemplate: { id: 2, title: 'مفاهیم قرآنی' },
|
courseTemplate: { id: 2, title: 'مفاهیم قرآنی' },
|
||||||
sessionType: 'online',
|
sessionType: 'online',
|
||||||
sessionTypeFa: 'آنلاین',
|
sessionTypeFa: 'آنلاین',
|
||||||
@@ -32,15 +75,14 @@ export const adminSessions = [
|
|||||||
platform: 'google_meet',
|
platform: 'google_meet',
|
||||||
startTime: '2025-10-04T19:00:00.000Z',
|
startTime: '2025-10-04T19:00:00.000Z',
|
||||||
},
|
},
|
||||||
materials: [],
|
|
||||||
usedInTerms: [{ termId: 1 }],
|
usedInTerms: [{ termId: 1 }],
|
||||||
createdAt: '2025-09-25T08:00:00.000Z',
|
createdAt: '2025-09-25T08:00:00.000Z',
|
||||||
},
|
}),
|
||||||
{
|
makeSession({
|
||||||
id: 103,
|
id: 103,
|
||||||
|
courseId: 13,
|
||||||
title: 'احکام نماز جماعت',
|
title: 'احکام نماز جماعت',
|
||||||
description: 'مرور احکام و شرایط نماز جماعت.',
|
description: 'مرور احکام و شرایط نماز جماعت.',
|
||||||
image: '',
|
|
||||||
courseTemplate: { id: 3, title: 'فقه عبادات' },
|
courseTemplate: { id: 3, title: 'فقه عبادات' },
|
||||||
sessionType: 'video',
|
sessionType: 'video',
|
||||||
sessionTypeFa: 'ویدئو',
|
sessionTypeFa: 'ویدئو',
|
||||||
@@ -50,12 +92,12 @@ export const adminSessions = [
|
|||||||
minWatchedPercent: 80,
|
minWatchedPercent: 80,
|
||||||
mustCompleteBeforeNext: true,
|
mustCompleteBeforeNext: true,
|
||||||
},
|
},
|
||||||
materials: [],
|
|
||||||
usedInTerms: [],
|
|
||||||
createdAt: '2026-01-12T08:00:00.000Z',
|
createdAt: '2026-01-12T08:00:00.000Z',
|
||||||
},
|
}),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
export { makeSession, SESSION_TYPE_TO_SPEC }
|
||||||
|
|
||||||
export const sessionAttendance = new Map([
|
export const sessionAttendance = new Map([
|
||||||
[
|
[
|
||||||
101,
|
101,
|
||||||
|
|||||||
@@ -1,42 +1,72 @@
|
|||||||
|
// Each term carries both the spec keys (title/description/is_active/
|
||||||
|
// starts_at/ends_at/cover_url/created_at — camelized) and the UI-only
|
||||||
|
// keys the current frontend reads (image, startDate, endDate,
|
||||||
|
// studentsCount, coursesCount). See docs/backend-api-todo.md.
|
||||||
|
const makeTerm = (overrides) => {
|
||||||
|
const startsAt = overrides.startsAt ?? overrides.startDate ?? ''
|
||||||
|
const endsAt = overrides.endsAt ?? overrides.endDate ?? ''
|
||||||
|
const coverUrl = overrides.coverUrl ?? overrides.image ?? ''
|
||||||
|
return {
|
||||||
|
// --- spec ---
|
||||||
|
id: overrides.id,
|
||||||
|
title: overrides.title ?? '',
|
||||||
|
description: overrides.description ?? '',
|
||||||
|
isActive: overrides.isActive ?? true,
|
||||||
|
startsAt,
|
||||||
|
endsAt,
|
||||||
|
coverUrl,
|
||||||
|
createdAt: overrides.createdAt ?? '',
|
||||||
|
|
||||||
|
// --- ui-only ---
|
||||||
|
image: coverUrl,
|
||||||
|
startDate: startsAt,
|
||||||
|
endDate: endsAt,
|
||||||
|
studentsCount: overrides.studentsCount ?? 0,
|
||||||
|
coursesCount: overrides.coursesCount ?? 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const adminTerms = [
|
export const adminTerms = [
|
||||||
{
|
makeTerm({
|
||||||
id: 1,
|
id: 1,
|
||||||
title: 'ترم پاییز ۱۴۰۴',
|
title: 'ترم پاییز ۱۴۰۴',
|
||||||
description: 'ترم پاییز با محوریت آموزش معارف اسلامی.',
|
description: 'ترم پاییز با محوریت آموزش معارف اسلامی.',
|
||||||
image: 'https://picsum.photos/seed/term1/200/200',
|
coverUrl: 'https://picsum.photos/seed/term1/200/200',
|
||||||
startDate: '2025-09-23T00:00:00.000Z',
|
startsAt: '2025-09-23T00:00:00.000Z',
|
||||||
endDate: '2026-01-20T00:00:00.000Z',
|
endsAt: '2026-01-20T00:00:00.000Z',
|
||||||
isActive: true,
|
isActive: true,
|
||||||
studentsCount: 18,
|
studentsCount: 18,
|
||||||
coursesCount: 4,
|
coursesCount: 4,
|
||||||
createdAt: '2025-08-01T08:00:00.000Z',
|
createdAt: '2025-08-01T08:00:00.000Z',
|
||||||
},
|
}),
|
||||||
{
|
makeTerm({
|
||||||
id: 2,
|
id: 2,
|
||||||
title: 'ترم زمستان ۱۴۰۴',
|
title: 'ترم زمستان ۱۴۰۴',
|
||||||
description: 'ترم زمستان با تمرکز بر فقه و اصول.',
|
description: 'ترم زمستان با تمرکز بر فقه و اصول.',
|
||||||
image: 'https://picsum.photos/seed/term2/200/200',
|
coverUrl: 'https://picsum.photos/seed/term2/200/200',
|
||||||
startDate: '2026-01-21T00:00:00.000Z',
|
startsAt: '2026-01-21T00:00:00.000Z',
|
||||||
endDate: '2026-04-20T00:00:00.000Z',
|
endsAt: '2026-04-20T00:00:00.000Z',
|
||||||
isActive: true,
|
isActive: true,
|
||||||
studentsCount: 12,
|
studentsCount: 12,
|
||||||
coursesCount: 3,
|
coursesCount: 3,
|
||||||
createdAt: '2025-11-10T08:00:00.000Z',
|
createdAt: '2025-11-10T08:00:00.000Z',
|
||||||
},
|
}),
|
||||||
{
|
makeTerm({
|
||||||
id: 3,
|
id: 3,
|
||||||
title: 'ترم بهار ۱۴۰۵',
|
title: 'ترم بهار ۱۴۰۵',
|
||||||
description: 'ترم بهار ویژه دورههای تخصصی.',
|
description: 'ترم بهار ویژه دورههای تخصصی.',
|
||||||
image: '',
|
coverUrl: '',
|
||||||
startDate: '2026-04-21T00:00:00.000Z',
|
startsAt: '2026-04-21T00:00:00.000Z',
|
||||||
endDate: '2026-08-21T00:00:00.000Z',
|
endsAt: '2026-08-21T00:00:00.000Z',
|
||||||
isActive: false,
|
isActive: false,
|
||||||
studentsCount: 0,
|
studentsCount: 0,
|
||||||
coursesCount: 2,
|
coursesCount: 2,
|
||||||
createdAt: '2026-02-15T08:00:00.000Z',
|
createdAt: '2026-02-15T08:00:00.000Z',
|
||||||
},
|
}),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
export { makeTerm }
|
||||||
|
|
||||||
export const termStudentLinks = new Map([
|
export const termStudentLinks = new Map([
|
||||||
[
|
[
|
||||||
1,
|
1,
|
||||||
|
|||||||
@@ -66,16 +66,26 @@ const makeUser = (i, overrides = {}) => {
|
|||||||
const province = provinceById((i % 4) + 1)
|
const province = provinceById((i % 4) + 1)
|
||||||
const city = cityForProvince(province.id)
|
const city = cityForProvince(province.id)
|
||||||
const role = roles[(i % 4) + 1]?.name || 'student'
|
const role = roles[(i % 4) + 1]?.name || 'student'
|
||||||
|
const id = 100 + i
|
||||||
|
const phoneNumber = `0912${String(1_000_000 + i).padStart(7, '0')}`
|
||||||
return {
|
return {
|
||||||
id: 100 + i,
|
// --- spec (GET /admin/users) ---
|
||||||
|
id,
|
||||||
|
name: `${firstName} ${lastName}`,
|
||||||
|
email: `user${id}@example.com`,
|
||||||
|
phone: phoneNumber.replace(/^0/, '+98'),
|
||||||
|
roles: [role],
|
||||||
|
avatarUrl: '',
|
||||||
|
avatarDownloadUrl: '',
|
||||||
|
createdAt: new Date(Date.now() - i * 86_400_000).toISOString(),
|
||||||
|
|
||||||
|
// --- ui-only (pending backend) ---
|
||||||
firstName,
|
firstName,
|
||||||
lastName,
|
lastName,
|
||||||
fullName: `${firstName} ${lastName}`,
|
fullName: `${firstName} ${lastName}`,
|
||||||
phoneNumber: `0912${String(1_000_000 + i).padStart(7, '0')}`,
|
phoneNumber,
|
||||||
nationalCode: String(1_000_000_000 + i * 7).padStart(10, '0'),
|
nationalCode: String(1_000_000_000 + i * 7).padStart(10, '0'),
|
||||||
avatarUrl: '',
|
|
||||||
status: i % 5 === 0 ? 'pending' : 'approved',
|
status: i % 5 === 0 ? 'pending' : 'approved',
|
||||||
roles: [role],
|
|
||||||
roleId: roles.find((r) => r.name === role)?.id,
|
roleId: roles.find((r) => r.name === role)?.id,
|
||||||
address: {
|
address: {
|
||||||
address: 'آدرس نمونه',
|
address: 'آدرس نمونه',
|
||||||
@@ -88,7 +98,6 @@ const makeUser = (i, overrides = {}) => {
|
|||||||
maritalStatus: i % 2 ? 'single' : 'married',
|
maritalStatus: i % 2 ? 'single' : 'married',
|
||||||
gender: i % 2 ? 'female' : 'male',
|
gender: i % 2 ? 'female' : 'male',
|
||||||
},
|
},
|
||||||
createdAt: new Date(Date.now() - i * 86_400_000).toISOString(),
|
|
||||||
...overrides,
|
...overrides,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,25 @@
|
|||||||
export const currentProfile = {
|
// Backend `/auth/me` currently returns the keys marked "spec" below.
|
||||||
id: 1,
|
// The "ui-only" keys are extras the UI already depends on (edit-profile form,
|
||||||
|
// avatars, address, etc.) — they must be preserved in the mock until the
|
||||||
|
// backend completes the response model. See docs/backend-api-todo.md.
|
||||||
|
export const currentMe = {
|
||||||
|
// --- spec (GET /auth/me) ---
|
||||||
|
id: 5,
|
||||||
|
name: 'سجاد محمدی',
|
||||||
|
email: 'sajjad@example.com',
|
||||||
|
phone: '+989121234567',
|
||||||
|
roles: ['student'],
|
||||||
|
avatarUrl: 'http://localhost:8080/storage/users/5/avatar.jpg',
|
||||||
|
avatarDownloadUrl: 'http://localhost:8080/api/media/9/download',
|
||||||
|
createdAt: '2026-02-01T10:00:00+00:00',
|
||||||
|
|
||||||
|
// --- ui-only (pending backend) ---
|
||||||
firstName: 'سجاد',
|
firstName: 'سجاد',
|
||||||
lastName: 'محمدی',
|
lastName: 'محمدی',
|
||||||
fullName: 'سجاد محمدی',
|
fullName: 'سجاد محمدی',
|
||||||
phoneNumber: '09123456789',
|
phoneNumber: '09123456789',
|
||||||
nationalCode: '0079827498',
|
nationalCode: '0079827498',
|
||||||
avatarUrl: '',
|
|
||||||
status: 'approved',
|
status: 'approved',
|
||||||
roles: ['user'],
|
|
||||||
address: {
|
address: {
|
||||||
address: 'خیابان آزادی، پلاک ۱۲',
|
address: 'خیابان آزادی، پلاک ۱۲',
|
||||||
province: { id: 1, name: 'تهران' },
|
province: { id: 1, name: 'تهران' },
|
||||||
@@ -2,7 +2,11 @@ import { register } from '@/services/mock/registry'
|
|||||||
import { endpoints } from '@/services/api/endpoints'
|
import { endpoints } from '@/services/api/endpoints'
|
||||||
import { adminTerms } from '@/services/mock/fixtures/admin-terms'
|
import { adminTerms } from '@/services/mock/fixtures/admin-terms'
|
||||||
import { adminUsers } from '@/services/mock/fixtures/admin-users'
|
import { adminUsers } from '@/services/mock/fixtures/admin-users'
|
||||||
import { adminCourseTemplates, adminOfferedCourses } from '@/services/mock/fixtures/admin-courses'
|
import {
|
||||||
|
adminCourseTemplates,
|
||||||
|
adminOfferedCourses,
|
||||||
|
makeTeacherSnapshot,
|
||||||
|
} from '@/services/mock/fixtures/admin-courses'
|
||||||
import {
|
import {
|
||||||
filterDateRange,
|
filterDateRange,
|
||||||
filterItems,
|
filterItems,
|
||||||
@@ -99,66 +103,128 @@ register('GET', endpoints.getCoursesList, ({ query }) => {
|
|||||||
status: (item, v) => String(item.isActive ? 1 : 0) === String(v),
|
status: (item, v) => String(item.isActive ? 1 : 0) === String(v),
|
||||||
})
|
})
|
||||||
list = filterDateRange(list, query, 'startDate')
|
list = filterDateRange(list, query, 'startDate')
|
||||||
return paginate(list, query)
|
const { data: items, meta } = paginate(list, query)
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: 'OK',
|
||||||
|
data: { items, meta },
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
register('GET', endpoints.showCourse, ({ params }) => ({
|
register('GET', endpoints.showCourse, ({ params }) => ({
|
||||||
|
success: true,
|
||||||
|
message: 'OK',
|
||||||
data: findOrThrow(adminOfferedCourses, params.id),
|
data: findOrThrow(adminOfferedCourses, params.id),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
register('POST', endpoints.addNewCourse, ({ data }) => {
|
register('POST', endpoints.addNewCourse, ({ data }) => {
|
||||||
const template = adminCourseTemplates.find((t) => t.id === Number(data.templateId))
|
const template = adminCourseTemplates.find((t) => t.id === Number(data.templateId))
|
||||||
const term = adminTerms.find((t) => t.id === Number(data.termId))
|
const term = adminTerms.find((t) => t.id === Number(data.termId))
|
||||||
const teacher = adminUsers.find((u) => u.id === Number(data.teacherId))
|
const teacherSource = adminUsers.find((u) => u.id === Number(data.teacherId))
|
||||||
|
const teacher = teacherSource
|
||||||
|
? makeTeacherSnapshot({
|
||||||
|
id: teacherSource.id,
|
||||||
|
firstName: teacherSource.firstName,
|
||||||
|
lastName: teacherSource.lastName,
|
||||||
|
name: teacherSource.name,
|
||||||
|
email: teacherSource.email,
|
||||||
|
phone: teacherSource.phone,
|
||||||
|
roles: teacherSource.roles,
|
||||||
|
avatarUrl: teacherSource.avatarUrl,
|
||||||
|
avatarDownloadUrl: teacherSource.avatarDownloadUrl,
|
||||||
|
createdAt: teacherSource.createdAt,
|
||||||
|
})
|
||||||
|
: null
|
||||||
|
const coverFromUpload = data.imageId
|
||||||
|
? `https://picsum.photos/seed/offered-${data.imageId}/200/200`
|
||||||
|
: ''
|
||||||
|
const coverUrl = data.coverUrl ?? coverFromUpload
|
||||||
const item = {
|
const item = {
|
||||||
|
// --- spec ---
|
||||||
id: makeId(),
|
id: makeId(),
|
||||||
|
termId: term?.id ?? Number(data.termId) ?? null,
|
||||||
|
teacherId: teacher?.id ?? (Number(data.teacherId) || null),
|
||||||
title: data.title || template?.title || '',
|
title: data.title || template?.title || '',
|
||||||
image: data.imageId ? `https://picsum.photos/seed/offered-${data.imageId}/200/200` : '',
|
description: data.description || '',
|
||||||
|
capacity: Number(data.capacity) || 0,
|
||||||
|
isActive: data.isActive !== undefined ? !!data.isActive : true,
|
||||||
|
coverUrl,
|
||||||
|
|
||||||
|
// --- ui-only ---
|
||||||
|
image: coverUrl,
|
||||||
template: template ? { id: template.id, title: template.title } : null,
|
template: template ? { id: template.id, title: template.title } : null,
|
||||||
templateId: template?.id,
|
templateId: template?.id,
|
||||||
term: term ? { id: term.id, title: term.title } : null,
|
term: term ? { id: term.id, title: term.title } : null,
|
||||||
termId: term?.id,
|
teacher,
|
||||||
teacher: teacher
|
|
||||||
? {
|
|
||||||
id: teacher.id,
|
|
||||||
firstName: teacher.firstName,
|
|
||||||
lastName: teacher.lastName,
|
|
||||||
fullName: `${teacher.firstName} ${teacher.lastName}`,
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
capacity: Number(data.capacity) || 0,
|
|
||||||
isActive: !!data.isActive,
|
|
||||||
prerequisitesCount: 0,
|
prerequisitesCount: 0,
|
||||||
startDate: term?.startDate || '',
|
startDate: term?.startDate || '',
|
||||||
endDate: term?.endDate || '',
|
endDate: term?.endDate || '',
|
||||||
createdAt: isoNow(),
|
createdAt: isoNow(),
|
||||||
}
|
}
|
||||||
adminOfferedCourses.unshift(item)
|
adminOfferedCourses.unshift(item)
|
||||||
return { data: item }
|
return {
|
||||||
|
success: true,
|
||||||
|
message: 'Course created.',
|
||||||
|
data: item,
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
register('PUT', endpoints.updateCourse, ({ params, data }) => {
|
register('PATCH', endpoints.updateCourse, ({ params, data }) => {
|
||||||
const template = adminCourseTemplates.find((t) => t.id === Number(data.templateId))
|
const patch = {}
|
||||||
|
if (data.title !== undefined) patch.title = data.title
|
||||||
|
if (data.description !== undefined) patch.description = data.description
|
||||||
|
if (data.capacity !== undefined) patch.capacity = Number(data.capacity) || 0
|
||||||
|
if (data.isActive !== undefined) patch.isActive = !!data.isActive
|
||||||
|
if (data.termId !== undefined) {
|
||||||
|
patch.termId = Number(data.termId) || null
|
||||||
const term = adminTerms.find((t) => t.id === Number(data.termId))
|
const term = adminTerms.find((t) => t.id === Number(data.termId))
|
||||||
|
if (term) patch.term = { id: term.id, title: term.title }
|
||||||
|
}
|
||||||
|
if (data.teacherId !== undefined) {
|
||||||
|
patch.teacherId = Number(data.teacherId) || null
|
||||||
|
const t = adminUsers.find((u) => u.id === Number(data.teacherId))
|
||||||
|
if (t) {
|
||||||
|
patch.teacher = makeTeacherSnapshot({
|
||||||
|
id: t.id,
|
||||||
|
firstName: t.firstName,
|
||||||
|
lastName: t.lastName,
|
||||||
|
name: t.name,
|
||||||
|
email: t.email,
|
||||||
|
phone: t.phone,
|
||||||
|
roles: t.roles,
|
||||||
|
avatarUrl: t.avatarUrl,
|
||||||
|
avatarDownloadUrl: t.avatarDownloadUrl,
|
||||||
|
createdAt: t.createdAt,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (data.templateId !== undefined) {
|
||||||
|
const template = adminCourseTemplates.find((t) => t.id === Number(data.templateId))
|
||||||
|
patch.templateId = template?.id
|
||||||
|
if (template) patch.template = { id: template.id, title: template.title }
|
||||||
|
}
|
||||||
|
if (data.coverUrl !== undefined || data.imageId !== undefined) {
|
||||||
|
const url =
|
||||||
|
data.coverUrl ??
|
||||||
|
(data.imageId ? `https://picsum.photos/seed/offered-${data.imageId}/200/200` : '')
|
||||||
|
patch.coverUrl = url
|
||||||
|
patch.image = url
|
||||||
|
}
|
||||||
|
const updated = updateById(adminOfferedCourses, params.id, patch)
|
||||||
return {
|
return {
|
||||||
data: updateById(adminOfferedCourses, params.id, {
|
success: true,
|
||||||
title: data.title,
|
message: 'Course updated.',
|
||||||
image: data.imageId
|
data: updated,
|
||||||
? `https://picsum.photos/seed/offered-${data.imageId}/200/200`
|
|
||||||
: undefined,
|
|
||||||
template: template ? { id: template.id, title: template.title } : undefined,
|
|
||||||
templateId: template?.id,
|
|
||||||
term: term ? { id: term.id, title: term.title } : undefined,
|
|
||||||
termId: term?.id,
|
|
||||||
capacity: Number(data.capacity) || 0,
|
|
||||||
isActive: !!data.isActive,
|
|
||||||
}),
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
register('DELETE', endpoints.deleteCourse, ({ params }) => {
|
register('DELETE', endpoints.deleteCourse, ({ params }) => {
|
||||||
removeById(adminOfferedCourses, params.id)
|
removeById(adminOfferedCourses, params.id)
|
||||||
return { data: { message: 'حذف موفق' } }
|
return {
|
||||||
|
success: true,
|
||||||
|
message: 'Course deleted.',
|
||||||
|
data: null,
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
register('POST', endpoints.changeStatusCourse, ({ params, data }) => ({
|
register('POST', endpoints.changeStatusCourse, ({ params, data }) => ({
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
import { SESSION_TYPE } from '@/enums'
|
import { SESSION_TYPE } from '@/enums'
|
||||||
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 { adminCourseTemplates } from '@/services/mock/fixtures/admin-courses'
|
import { adminCourseTemplates, adminOfferedCourses } from '@/services/mock/fixtures/admin-courses'
|
||||||
import { adminSessions, sessionAttendance } from '@/services/mock/fixtures/admin-sessions'
|
import {
|
||||||
|
adminSessions,
|
||||||
|
makeSession,
|
||||||
|
sessionAttendance,
|
||||||
|
} from '@/services/mock/fixtures/admin-sessions'
|
||||||
import {
|
import {
|
||||||
filterDateRange,
|
filterDateRange,
|
||||||
filterItems,
|
filterItems,
|
||||||
@@ -19,27 +23,62 @@ const enrich = (session) => ({
|
|||||||
sessionTypeFa: SESSION_TYPE[session.sessionType] || session.sessionTypeFa || '',
|
sessionTypeFa: SESSION_TYPE[session.sessionType] || session.sessionTypeFa || '',
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const courseSnapshot = (offered) =>
|
||||||
|
offered
|
||||||
|
? {
|
||||||
|
id: offered.id,
|
||||||
|
termId: offered.termId,
|
||||||
|
teacherId: offered.teacherId,
|
||||||
|
title: offered.title,
|
||||||
|
description: offered.description,
|
||||||
|
capacity: offered.capacity,
|
||||||
|
isActive: offered.isActive,
|
||||||
|
coverUrl: offered.coverUrl,
|
||||||
|
}
|
||||||
|
: null
|
||||||
|
|
||||||
register('GET', endpoints.getSessionsList, ({ query }) => {
|
register('GET', endpoints.getSessionsList, ({ query }) => {
|
||||||
let list = filterItems(adminSessions, query, {
|
let list = filterItems(adminSessions, query, {
|
||||||
title: 'includes',
|
title: 'includes',
|
||||||
courseTemplateId: (item, v) => String(item.courseTemplate?.id) === String(v),
|
courseTemplateId: (item, v) => String(item.courseTemplate?.id) === String(v),
|
||||||
|
courseId: 'eq',
|
||||||
sessionType: 'eq',
|
sessionType: 'eq',
|
||||||
|
type: 'eq',
|
||||||
})
|
})
|
||||||
list = filterDateRange(list, query)
|
list = filterDateRange(list, query)
|
||||||
// eslint-disable-next-line unicorn/no-array-callback-reference
|
// eslint-disable-next-line unicorn/no-array-callback-reference
|
||||||
return paginate(list.map(enrich), query)
|
const { data: items, meta } = paginate(list.map(enrich), query)
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: 'OK',
|
||||||
|
data: { items, meta },
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
register('GET', endpoints.showSession, ({ params }) => ({
|
register('GET', endpoints.showSession, ({ params }) => {
|
||||||
data: enrich(findOrThrow(adminSessions, params.id)),
|
const session = enrich(findOrThrow(adminSessions, params.id))
|
||||||
}))
|
const offered = adminOfferedCourses.find((c) => c.id === session.courseId)
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: 'OK',
|
||||||
|
data: { ...session, course: courseSnapshot(offered) },
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
register('POST', endpoints.addNewSession, ({ data }) => {
|
register('POST', endpoints.addNewSession, ({ data }) => {
|
||||||
const tpl = adminCourseTemplates.find((c) => c.id === Number(data.courseTemplateId))
|
const tpl = adminCourseTemplates.find((c) => c.id === Number(data.courseTemplateId))
|
||||||
const item = {
|
const offered =
|
||||||
|
adminOfferedCourses.find((c) => c.id === Number(data.courseId)) ||
|
||||||
|
(tpl ? adminOfferedCourses.find((c) => c.templateId === tpl.id) : null)
|
||||||
|
const item = makeSession({
|
||||||
id: makeId(),
|
id: makeId(),
|
||||||
|
courseId: data.courseId ?? offered?.id ?? null,
|
||||||
title: data.title || '',
|
title: data.title || '',
|
||||||
description: data.description || '',
|
description: data.description || '',
|
||||||
|
type: data.type,
|
||||||
|
startsAt: data.startsAt ?? data.sessionConfig?.startTime ?? null,
|
||||||
|
location: data.location ?? data.sessionConfig?.location ?? null,
|
||||||
|
link: data.link ?? data.sessionConfig?.meetingLink ?? null,
|
||||||
image: data.imageId ? `https://picsum.photos/seed/session-${data.imageId}/200/200` : '',
|
image: data.imageId ? `https://picsum.photos/seed/session-${data.imageId}/200/200` : '',
|
||||||
courseTemplate: tpl ? { id: tpl.id, title: tpl.title } : null,
|
courseTemplate: tpl ? { id: tpl.id, title: tpl.title } : null,
|
||||||
sessionType: data.sessionType || 'in_person',
|
sessionType: data.sessionType || 'in_person',
|
||||||
@@ -47,32 +86,65 @@ register('POST', endpoints.addNewSession, ({ data }) => {
|
|||||||
order: Number(data.order) || 1,
|
order: Number(data.order) || 1,
|
||||||
sessionConfig: data.sessionConfig || {},
|
sessionConfig: data.sessionConfig || {},
|
||||||
materials: data.materials || [],
|
materials: data.materials || [],
|
||||||
usedInTerms: [],
|
|
||||||
createdAt: isoNow(),
|
createdAt: isoNow(),
|
||||||
}
|
})
|
||||||
adminSessions.unshift(item)
|
adminSessions.unshift(item)
|
||||||
return { data: enrich(item) }
|
return {
|
||||||
|
success: true,
|
||||||
|
message: 'Session created.',
|
||||||
|
data: enrich(item),
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
register('PUT', endpoints.updateSession, ({ params, data }) => {
|
register('PATCH', endpoints.updateSession, ({ params, data }) => {
|
||||||
|
const patch = {}
|
||||||
|
if (data.title !== undefined) patch.title = data.title
|
||||||
|
if (data.description !== undefined) patch.description = data.description
|
||||||
|
if (data.type !== undefined) patch.type = data.type
|
||||||
|
if (data.startsAt !== undefined) patch.startsAt = data.startsAt
|
||||||
|
if (data.location !== undefined) patch.location = data.location
|
||||||
|
if (data.link !== undefined) patch.link = data.link
|
||||||
|
if (data.courseId !== undefined) patch.courseId = Number(data.courseId) || null
|
||||||
|
if (data.courseTemplateId !== undefined) {
|
||||||
const tpl = adminCourseTemplates.find((c) => c.id === Number(data.courseTemplateId))
|
const tpl = adminCourseTemplates.find((c) => c.id === Number(data.courseTemplateId))
|
||||||
const updated = updateById(adminSessions, params.id, {
|
if (tpl) patch.courseTemplate = { id: tpl.id, title: tpl.title }
|
||||||
title: data.title,
|
}
|
||||||
description: data.description,
|
if (data.sessionType !== undefined) patch.sessionType = data.sessionType
|
||||||
image: data.imageId ? `https://picsum.photos/seed/session-${data.imageId}/200/200` : undefined,
|
if (data.durationMinutes !== undefined) patch.durationMinutes = Number(data.durationMinutes) || 0
|
||||||
courseTemplate: tpl ? { id: tpl.id, title: tpl.title } : undefined,
|
if (data.order !== undefined) patch.order = Number(data.order) || 1
|
||||||
sessionType: data.sessionType,
|
if (data.sessionConfig !== undefined) {
|
||||||
durationMinutes: Number(data.durationMinutes) || 0,
|
patch.sessionConfig = data.sessionConfig
|
||||||
order: Number(data.order) || 1,
|
if (data.startsAt === undefined && data.sessionConfig.startTime !== undefined) {
|
||||||
sessionConfig: data.sessionConfig ?? undefined,
|
patch.startsAt = data.sessionConfig.startTime
|
||||||
materials: data.materials ?? undefined,
|
}
|
||||||
})
|
if (data.location === undefined && data.sessionConfig.location !== undefined) {
|
||||||
return { data: enrich(updated) }
|
patch.location = data.sessionConfig.location
|
||||||
|
}
|
||||||
|
if (data.link === undefined && data.sessionConfig.meetingLink !== undefined) {
|
||||||
|
patch.link = data.sessionConfig.meetingLink
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (data.materials !== undefined) patch.materials = data.materials
|
||||||
|
if (data.imageId !== undefined) {
|
||||||
|
patch.image = data.imageId
|
||||||
|
? `https://picsum.photos/seed/session-${data.imageId}/200/200`
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
const updated = updateById(adminSessions, params.id, patch)
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: 'Session updated.',
|
||||||
|
data: enrich(updated),
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
register('DELETE', endpoints.deleteSession, ({ params }) => {
|
register('DELETE', endpoints.deleteSession, ({ params }) => {
|
||||||
removeById(adminSessions, params.id)
|
removeById(adminSessions, params.id)
|
||||||
return { data: { message: 'حذف موفق' } }
|
return {
|
||||||
|
success: true,
|
||||||
|
message: 'Session deleted.',
|
||||||
|
data: null,
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
register('POST', endpoints.changeStatusSession, ({ params, data }) => {
|
register('POST', endpoints.changeStatusSession, ({ params, data }) => {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { register } from '@/services/mock/registry'
|
|||||||
import { endpoints } from '@/services/api/endpoints'
|
import { endpoints } from '@/services/api/endpoints'
|
||||||
import { adminUsers } from '@/services/mock/fixtures/admin-users'
|
import { adminUsers } from '@/services/mock/fixtures/admin-users'
|
||||||
import { adminOfferedCourses } from '@/services/mock/fixtures/admin-courses'
|
import { adminOfferedCourses } from '@/services/mock/fixtures/admin-courses'
|
||||||
import { adminTerms, termStudentLinks } from '@/services/mock/fixtures/admin-terms'
|
import { adminTerms, makeTerm, termStudentLinks } from '@/services/mock/fixtures/admin-terms'
|
||||||
import {
|
import {
|
||||||
filterDateRange,
|
filterDateRange,
|
||||||
filterItems,
|
filterItems,
|
||||||
@@ -18,45 +18,85 @@ register('GET', endpoints.getTermsList, ({ query }) => {
|
|||||||
let list = filterItems(adminTerms, query, {
|
let list = filterItems(adminTerms, query, {
|
||||||
title: 'includes',
|
title: 'includes',
|
||||||
status: (item, v) => String(item.isActive ? 1 : 0) === String(v),
|
status: (item, v) => String(item.isActive ? 1 : 0) === String(v),
|
||||||
|
activeOnly: (item, v) => (String(v) === '1' ? !!item.isActive : true),
|
||||||
})
|
})
|
||||||
list = filterDateRange(list, query, 'startDate')
|
list = filterDateRange(list, query, 'startDate')
|
||||||
return paginate(list, query)
|
const { data: items, meta } = paginate(list, query)
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: 'OK',
|
||||||
|
data: { items, meta },
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
register('GET', endpoints.showTerm, ({ params }) => ({
|
register('GET', endpoints.showTerm, ({ params }) => ({
|
||||||
|
success: true,
|
||||||
|
message: 'OK',
|
||||||
data: findOrThrow(adminTerms, params.id),
|
data: findOrThrow(adminTerms, params.id),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
register('POST', endpoints.addNewTerm, ({ data }) => {
|
register('POST', endpoints.addNewTerm, ({ data }) => {
|
||||||
const term = {
|
const coverFromUpload = data.imageId
|
||||||
|
? `https://picsum.photos/seed/term-${data.imageId}/200/200`
|
||||||
|
: ''
|
||||||
|
const term = makeTerm({
|
||||||
id: makeId(),
|
id: makeId(),
|
||||||
title: data.title || '',
|
title: data.title || '',
|
||||||
description: data.description || '',
|
description: data.description || '',
|
||||||
image: data.imageId ? `https://picsum.photos/seed/term-${data.imageId}/200/200` : '',
|
isActive: data.isActive ?? true,
|
||||||
startDate: data.startDate || '',
|
startsAt: data.startsAt ?? data.startDate ?? '',
|
||||||
endDate: data.endDate || '',
|
endsAt: data.endsAt ?? data.endDate ?? '',
|
||||||
isActive: true,
|
coverUrl: data.coverUrl ?? coverFromUpload,
|
||||||
studentsCount: 0,
|
studentsCount: 0,
|
||||||
coursesCount: 0,
|
coursesCount: 0,
|
||||||
createdAt: isoNow(),
|
createdAt: isoNow(),
|
||||||
}
|
})
|
||||||
adminTerms.unshift(term)
|
adminTerms.unshift(term)
|
||||||
return { data: term }
|
return {
|
||||||
|
success: true,
|
||||||
|
message: 'Term created.',
|
||||||
|
data: term,
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
register('PUT', endpoints.updateTerm, ({ params, data }) => ({
|
register('PATCH', endpoints.updateTerm, ({ params, data }) => {
|
||||||
data: updateById(adminTerms, params.id, {
|
const coverFromUpload = data.imageId
|
||||||
title: data.title,
|
? `https://picsum.photos/seed/term-${data.imageId}/200/200`
|
||||||
description: data.description,
|
: undefined
|
||||||
startDate: data.startDate,
|
const startsAt = data.startsAt ?? data.startDate
|
||||||
endDate: data.endDate,
|
const endsAt = data.endsAt ?? data.endDate
|
||||||
image: data.imageId ? `https://picsum.photos/seed/term-${data.imageId}/200/200` : undefined,
|
const coverUrl = data.coverUrl ?? coverFromUpload
|
||||||
}),
|
const patch = {}
|
||||||
}))
|
if (data.title !== undefined) patch.title = data.title
|
||||||
|
if (data.description !== undefined) patch.description = data.description
|
||||||
|
if (data.isActive !== undefined) patch.isActive = data.isActive
|
||||||
|
if (startsAt !== undefined) {
|
||||||
|
patch.startsAt = startsAt
|
||||||
|
patch.startDate = startsAt
|
||||||
|
}
|
||||||
|
if (endsAt !== undefined) {
|
||||||
|
patch.endsAt = endsAt
|
||||||
|
patch.endDate = endsAt
|
||||||
|
}
|
||||||
|
if (coverUrl !== undefined) {
|
||||||
|
patch.coverUrl = coverUrl
|
||||||
|
patch.image = coverUrl
|
||||||
|
}
|
||||||
|
const updated = updateById(adminTerms, params.id, patch)
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: 'Term updated.',
|
||||||
|
data: updated,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
register('DELETE', endpoints.deleteTerm, ({ params }) => {
|
register('DELETE', endpoints.deleteTerm, ({ params }) => {
|
||||||
removeById(adminTerms, params.id)
|
removeById(adminTerms, params.id)
|
||||||
return { data: { message: 'حذف موفق' } }
|
return {
|
||||||
|
success: true,
|
||||||
|
message: 'Term deleted.',
|
||||||
|
data: null,
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
register('POST', endpoints.cloneTerm, ({ params }) => {
|
register('POST', endpoints.cloneTerm, ({ params }) => {
|
||||||
|
|||||||
@@ -21,28 +21,51 @@ register('GET', endpoints.getApprovedUsers, ({ query }) => {
|
|||||||
roleId: (item, v) => String(item.roleId) === String(v),
|
roleId: (item, v) => String(item.roleId) === String(v),
|
||||||
})
|
})
|
||||||
list = filterDateRange(list, query)
|
list = filterDateRange(list, query)
|
||||||
return paginate(list, query)
|
const { data: items, meta } = paginate(list, query)
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: 'OK',
|
||||||
|
data: { items, meta },
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
register('GET', endpoints.showUserDetails, ({ params }) => ({
|
register('GET', endpoints.showUserDetails, ({ params }) => ({
|
||||||
|
success: true,
|
||||||
|
message: 'OK',
|
||||||
data: findOrThrow(adminUsers, params.id),
|
data: findOrThrow(adminUsers, params.id),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
register('POST', endpoints.addNewUser, ({ data }) => {
|
register('POST', endpoints.addNewUser, ({ data }) => {
|
||||||
const id = makeId()
|
const id = makeId()
|
||||||
const role = roles.find((r) => r.id === Number(data.roleId)) || null
|
const roleByName = (n) => roles.find((r) => r.name === n)
|
||||||
|
const role =
|
||||||
|
roles.find((r) => r.id === Number(data.roleId)) ||
|
||||||
|
(Array.isArray(data.roles) && data.roles[0] ? roleByName(data.roles[0]) : null) ||
|
||||||
|
null
|
||||||
const province = data.provinceId ? { id: data.provinceId, name: '' } : null
|
const province = data.provinceId ? { id: data.provinceId, name: '' } : null
|
||||||
const city = data.cityId ? { id: data.cityId, name: '' } : null
|
const city = data.cityId ? { id: data.cityId, name: '' } : null
|
||||||
|
const firstName = data.firstName || ''
|
||||||
|
const lastName = data.lastName || ''
|
||||||
|
const fullName = `${firstName} ${lastName}`.trim()
|
||||||
|
const phoneNumber = data.phoneNumber || ''
|
||||||
const user = {
|
const user = {
|
||||||
|
// --- spec ---
|
||||||
id,
|
id,
|
||||||
firstName: data.firstName || '',
|
name: data.name || fullName,
|
||||||
lastName: data.lastName || '',
|
email: data.email || '',
|
||||||
fullName: `${data.firstName || ''} ${data.lastName || ''}`.trim(),
|
phone: data.phone || (phoneNumber ? phoneNumber.replace(/^0/, '+98') : null),
|
||||||
phoneNumber: data.phoneNumber || '',
|
roles: Array.isArray(data.roles) ? data.roles : role ? [role.name] : [],
|
||||||
nationalCode: data.nationalCode || '',
|
|
||||||
avatarUrl: '',
|
avatarUrl: '',
|
||||||
|
avatarDownloadUrl: null,
|
||||||
|
createdAt: isoNow(),
|
||||||
|
|
||||||
|
// --- ui-only ---
|
||||||
|
firstName,
|
||||||
|
lastName,
|
||||||
|
fullName,
|
||||||
|
phoneNumber,
|
||||||
|
nationalCode: data.nationalCode || '',
|
||||||
status: 'approved',
|
status: 'approved',
|
||||||
roles: role ? [role.name] : [],
|
|
||||||
roleId: role?.id,
|
roleId: role?.id,
|
||||||
address: { address: data.address || '', province, city },
|
address: { address: data.address || '', province, city },
|
||||||
profile: {
|
profile: {
|
||||||
@@ -52,22 +75,39 @@ register('POST', endpoints.addNewUser, ({ data }) => {
|
|||||||
gender: data.gender || '',
|
gender: data.gender || '',
|
||||||
avatarId: data.avatarId || null,
|
avatarId: data.avatarId || null,
|
||||||
},
|
},
|
||||||
createdAt: isoNow(),
|
|
||||||
}
|
}
|
||||||
adminUsers.unshift(user)
|
adminUsers.unshift(user)
|
||||||
return { data: user }
|
return {
|
||||||
|
success: true,
|
||||||
|
message: 'User created.',
|
||||||
|
data: user,
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
register('PUT', endpoints.updateUser, ({ params, data }) => {
|
register('PATCH', endpoints.updateUser, ({ params, data }) => {
|
||||||
const role = roles.find((r) => r.id === Number(data.roleId))
|
const roleByName = (n) => roles.find((r) => r.name === n)
|
||||||
|
const role =
|
||||||
|
roles.find((r) => r.id === Number(data.roleId)) ||
|
||||||
|
(Array.isArray(data.roles) && data.roles[0] ? roleByName(data.roles[0]) : null) ||
|
||||||
|
null
|
||||||
|
const firstName = data.firstName ?? ''
|
||||||
|
const lastName = data.lastName ?? ''
|
||||||
|
const fullName = `${firstName} ${lastName}`.trim()
|
||||||
|
const phoneNumber = data.phoneNumber ?? ''
|
||||||
const patch = {
|
const patch = {
|
||||||
firstName: data.firstName,
|
// --- spec ---
|
||||||
lastName: data.lastName,
|
name: data.name ?? fullName,
|
||||||
fullName: `${data.firstName || ''} ${data.lastName || ''}`.trim(),
|
email: data.email,
|
||||||
phoneNumber: data.phoneNumber,
|
phone: data.phone ?? (phoneNumber ? phoneNumber.replace(/^0/, '+98') : undefined),
|
||||||
|
roles: Array.isArray(data.roles) ? data.roles : role ? [role.name] : [],
|
||||||
|
|
||||||
|
// --- ui-only ---
|
||||||
|
firstName,
|
||||||
|
lastName,
|
||||||
|
fullName,
|
||||||
|
phoneNumber,
|
||||||
nationalCode: data.nationalCode,
|
nationalCode: data.nationalCode,
|
||||||
roleId: role?.id,
|
roleId: role?.id,
|
||||||
roles: role ? [role.name] : [],
|
|
||||||
address: {
|
address: {
|
||||||
address: data.address || '',
|
address: data.address || '',
|
||||||
province: data.provinceId ? { id: data.provinceId, name: '' } : null,
|
province: data.provinceId ? { id: data.provinceId, name: '' } : null,
|
||||||
@@ -82,13 +122,32 @@ register('PUT', endpoints.updateUser, ({ params, data }) => {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
const updated = updateById(adminUsers, params.id, patch)
|
const updated = updateById(adminUsers, params.id, patch)
|
||||||
return { data: updated }
|
return {
|
||||||
|
success: true,
|
||||||
|
message: 'User updated.',
|
||||||
|
data: updated,
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
register('POST', endpoints.updateUserRole, ({ params, data }) => {
|
register('PATCH', endpoints.updateUserRole, ({ params, data }) => {
|
||||||
const role = roles.find((r) => r.id === Number(data.roleId))
|
const byName = (n) => roles.find((r) => r.name === n)
|
||||||
const patch = role ? { roleId: role.id, roles: [role.name] } : {}
|
const nextRoleNames = Array.isArray(data.roles)
|
||||||
return { data: updateById(adminUsers, params.id, patch) }
|
? data.roles.filter(Boolean)
|
||||||
|
: data.role
|
||||||
|
? [data.role]
|
||||||
|
: data.roleId
|
||||||
|
? [roles.find((r) => r.id === Number(data.roleId))?.name].filter(Boolean)
|
||||||
|
: []
|
||||||
|
const primary = nextRoleNames[0] ? byName(nextRoleNames[0]) : null
|
||||||
|
const patch = nextRoleNames.length
|
||||||
|
? { roles: nextRoleNames, roleId: primary?.id }
|
||||||
|
: {}
|
||||||
|
const updated = updateById(adminUsers, params.id, patch)
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: 'Roles updated.',
|
||||||
|
data: updated,
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
register('POST', endpoints.changeUserStatus, ({ params, data }) => ({
|
register('POST', endpoints.changeUserStatus, ({ params, data }) => ({
|
||||||
@@ -97,7 +156,11 @@ register('POST', endpoints.changeUserStatus, ({ params, data }) => ({
|
|||||||
|
|
||||||
register('DELETE', endpoints.deleteUser, ({ params }) => {
|
register('DELETE', endpoints.deleteUser, ({ params }) => {
|
||||||
removeById(adminUsers, params.id)
|
removeById(adminUsers, params.id)
|
||||||
return { data: { message: 'حذف موفق' } }
|
return {
|
||||||
|
success: true,
|
||||||
|
message: 'User deleted.',
|
||||||
|
data: null,
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
register('GET', endpoints.getRolesList, () => ({ data: roles }))
|
register('GET', endpoints.getRolesList, () => ({ data: roles }))
|
||||||
|
|||||||
@@ -1,24 +1,24 @@
|
|||||||
import { makeId } from '@/services/mock/helpers'
|
import { makeId } from '@/services/mock/helpers'
|
||||||
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 { currentProfile } from '@/services/mock/fixtures/profile'
|
import { currentMe } from '@/services/mock/fixtures/me'
|
||||||
|
|
||||||
const fakeToken = 'mock-token-1234567890'
|
const fakeToken = 'mock-token-1234567890'
|
||||||
|
|
||||||
register('POST', endpoints.login, () => ({
|
register('POST', endpoints.login, () => ({
|
||||||
data: { user: currentProfile, token: fakeToken },
|
data: { user: currentMe, token: fakeToken },
|
||||||
}))
|
}))
|
||||||
|
|
||||||
register('POST', endpoints.loginWithCode2Step, () => ({
|
register('POST', endpoints.loginWithCode2Step, () => ({
|
||||||
data: { user: currentProfile, token: fakeToken },
|
data: { user: currentMe, token: fakeToken },
|
||||||
}))
|
}))
|
||||||
|
|
||||||
register('POST', endpoints.verifyCode, () => ({
|
register('POST', endpoints.verifyCode, () => ({
|
||||||
data: { user: currentProfile, token: fakeToken },
|
data: { user: currentMe, token: fakeToken },
|
||||||
}))
|
}))
|
||||||
|
|
||||||
register('POST', endpoints.register, ({ data }) => ({
|
register('POST', endpoints.register, ({ data }) => ({
|
||||||
data: { user: { ...currentProfile, ...data }, token: fakeToken },
|
data: { user: { ...currentMe, ...data }, token: fakeToken },
|
||||||
}))
|
}))
|
||||||
|
|
||||||
register('POST', endpoints.resendVerificationCodeForRegister, () => ({
|
register('POST', endpoints.resendVerificationCodeForRegister, () => ({
|
||||||
@@ -39,47 +39,56 @@ register('POST', endpoints.resetPassword, () => ({
|
|||||||
|
|
||||||
register('POST', endpoints.logout, () => ({ data: { message: 'خروج موفق' } }))
|
register('POST', endpoints.logout, () => ({ data: { message: 'خروج موفق' } }))
|
||||||
|
|
||||||
register('GET', endpoints.getProfile, () => ({ data: currentProfile }))
|
register('GET', endpoints.me, () => ({
|
||||||
|
success: true,
|
||||||
|
message: 'OK',
|
||||||
|
data: currentMe,
|
||||||
|
}))
|
||||||
|
|
||||||
register('PUT', endpoints.updateProfile, ({ data }) => {
|
register('PUT', endpoints.updateProfile, ({ data }) => {
|
||||||
Object.assign(currentProfile, {
|
Object.assign(currentMe, {
|
||||||
firstName: data.firstName ?? currentProfile.firstName,
|
name: data.name ?? currentMe.name,
|
||||||
lastName: data.lastName ?? currentProfile.lastName,
|
email: data.email ?? currentMe.email,
|
||||||
phoneNumber: data.phoneNumber ?? currentProfile.phoneNumber,
|
phone: data.phone ?? currentMe.phone,
|
||||||
nationalCode: data.nationalCode ?? currentProfile.nationalCode,
|
firstName: data.firstName ?? currentMe.firstName,
|
||||||
|
lastName: data.lastName ?? currentMe.lastName,
|
||||||
|
phoneNumber: data.phoneNumber ?? currentMe.phoneNumber,
|
||||||
|
nationalCode: data.nationalCode ?? currentMe.nationalCode,
|
||||||
})
|
})
|
||||||
if (data.provinceId) {
|
if (data.provinceId) {
|
||||||
currentProfile.address = currentProfile.address || {}
|
currentMe.address = currentMe.address || {}
|
||||||
currentProfile.address.province = { id: data.provinceId, name: '' }
|
currentMe.address.province = { id: data.provinceId, name: '' }
|
||||||
}
|
}
|
||||||
if (data.cityId) {
|
if (data.cityId) {
|
||||||
currentProfile.address = currentProfile.address || {}
|
currentMe.address = currentMe.address || {}
|
||||||
currentProfile.address.city = { id: data.cityId, name: '' }
|
currentMe.address.city = { id: data.cityId, name: '' }
|
||||||
}
|
}
|
||||||
if (data.address) {
|
if (data.address) {
|
||||||
currentProfile.address = { ...currentProfile.address, address: data.address }
|
currentMe.address = { ...currentMe.address, address: data.address }
|
||||||
}
|
}
|
||||||
currentProfile.profile = {
|
currentMe.profile = {
|
||||||
...currentProfile.profile,
|
...currentMe.profile,
|
||||||
bio: data.bio ?? currentProfile.profile?.bio,
|
bio: data.bio ?? currentMe.profile?.bio,
|
||||||
birthDate: data.birthDate ?? currentProfile.profile?.birthDate,
|
birthDate: data.birthDate ?? currentMe.profile?.birthDate,
|
||||||
maritalStatus: data.maritalStatus ?? currentProfile.profile?.maritalStatus,
|
maritalStatus: data.maritalStatus ?? currentMe.profile?.maritalStatus,
|
||||||
gender: data.gender ?? currentProfile.profile?.gender,
|
gender: data.gender ?? currentMe.profile?.gender,
|
||||||
}
|
}
|
||||||
return { data: currentProfile }
|
return { data: currentMe }
|
||||||
})
|
})
|
||||||
|
|
||||||
register('POST', endpoints.completeProfile, ({ data }) => ({
|
register('POST', endpoints.completeProfile, ({ data }) => ({
|
||||||
data: { user: { ...currentProfile, ...data } },
|
data: { user: { ...currentMe, ...data } },
|
||||||
}))
|
}))
|
||||||
|
|
||||||
register('GET', endpoints.getRegistrationQuestionVideo, () => ({
|
register('GET', endpoints.getRegistrationQuestionVideo, () => ({
|
||||||
data: { url: '', id: 0 },
|
data: { url: '', id: 0 },
|
||||||
}))
|
}))
|
||||||
|
|
||||||
register('POST', endpoints.uploadMediaTemporary, () => {
|
register('POST', endpoints.uploadMedia, () => {
|
||||||
const id = makeId()
|
const id = makeId()
|
||||||
return {
|
return {
|
||||||
|
success: true,
|
||||||
|
message: 'Media uploaded.',
|
||||||
data: {
|
data: {
|
||||||
id,
|
id,
|
||||||
uploadId: id,
|
uploadId: id,
|
||||||
|
|||||||
@@ -20,6 +20,10 @@ export const useAdminCoursesListQuery = (filtersRef, paginationRef, options = {}
|
|||||||
queryKey: ['admin', 'courses', 'list', filtersRef, paginationRef],
|
queryKey: ['admin', 'courses', 'list', filtersRef, paginationRef],
|
||||||
queryFn: () =>
|
queryFn: () =>
|
||||||
apiGetAdminCourses({ ...cleanFilters(filtersRef.value), ...paginationRef.value }),
|
apiGetAdminCourses({ ...cleanFilters(filtersRef.value), ...paginationRef.value }),
|
||||||
|
select: (response) => ({
|
||||||
|
data: response?.data?.items ?? response?.data ?? [],
|
||||||
|
meta: response?.data?.meta ?? response?.meta,
|
||||||
|
}),
|
||||||
...options,
|
...options,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,10 @@ export const useAdminSessionsListQuery = (filtersRef, paginationRef, options = {
|
|||||||
queryKey: ['admin', 'sessions', 'list', filtersRef, paginationRef],
|
queryKey: ['admin', 'sessions', 'list', filtersRef, paginationRef],
|
||||||
queryFn: () =>
|
queryFn: () =>
|
||||||
apiGetAdminSessions({ ...cleanFilters(filtersRef.value), ...paginationRef.value }),
|
apiGetAdminSessions({ ...cleanFilters(filtersRef.value), ...paginationRef.value }),
|
||||||
|
select: (response) => ({
|
||||||
|
data: response?.data?.items ?? response?.data ?? [],
|
||||||
|
meta: response?.data?.meta ?? response?.meta,
|
||||||
|
}),
|
||||||
...options,
|
...options,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -43,6 +43,10 @@ export const useAdminTermsListQuery = (filtersRef, paginationRef, options = {})
|
|||||||
useQuery({
|
useQuery({
|
||||||
queryKey: ['admin', 'terms', 'list', filtersRef, paginationRef],
|
queryKey: ['admin', 'terms', 'list', filtersRef, paginationRef],
|
||||||
queryFn: () => apiGetAdminTerms({ ...cleanFilters(filtersRef.value), ...paginationRef.value }),
|
queryFn: () => apiGetAdminTerms({ ...cleanFilters(filtersRef.value), ...paginationRef.value }),
|
||||||
|
select: (response) => ({
|
||||||
|
data: response?.data?.items ?? response?.data ?? [],
|
||||||
|
meta: response?.data?.meta ?? response?.meta,
|
||||||
|
}),
|
||||||
...options,
|
...options,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,10 @@ export const useAdminUsersListQuery = (filtersRef, paginationRef, options = {})
|
|||||||
useQuery({
|
useQuery({
|
||||||
queryKey: ['admin', 'users', 'list', filtersRef, paginationRef],
|
queryKey: ['admin', 'users', 'list', filtersRef, paginationRef],
|
||||||
queryFn: () => apiGetAdminUsers({ ...cleanFilters(filtersRef.value), ...paginationRef.value }),
|
queryFn: () => apiGetAdminUsers({ ...cleanFilters(filtersRef.value), ...paginationRef.value }),
|
||||||
|
select: (response) => ({
|
||||||
|
data: response?.data?.items ?? response?.data ?? [],
|
||||||
|
meta: response?.data?.meta ?? response?.meta,
|
||||||
|
}),
|
||||||
...options,
|
...options,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { useMutation, useQuery } from '@tanstack/vue-query'
|
|||||||
import {
|
import {
|
||||||
apiCompleteProfile,
|
apiCompleteProfile,
|
||||||
apiForgotPassword,
|
apiForgotPassword,
|
||||||
apiGetProfile,
|
apiGetMe,
|
||||||
apiGetRegistrationQuestionVideo,
|
apiGetRegistrationQuestionVideo,
|
||||||
apiLogin,
|
apiLogin,
|
||||||
apiLoginWithCode,
|
apiLoginWithCode,
|
||||||
@@ -12,13 +12,13 @@ import {
|
|||||||
apiResendVerificationCode,
|
apiResendVerificationCode,
|
||||||
apiResetPassword,
|
apiResetPassword,
|
||||||
apiUpdateProfile,
|
apiUpdateProfile,
|
||||||
apiUploadTemporary,
|
apiUploadMedia,
|
||||||
apiVerifyCode,
|
apiVerifyCode,
|
||||||
apiVerifyForgotPasswordCode,
|
apiVerifyForgotPasswordCode,
|
||||||
} from '@/services/api/auth'
|
} from '@/services/api/auth'
|
||||||
|
|
||||||
export const authKeys = {
|
export const authKeys = {
|
||||||
profile: () => ['auth', 'profile'],
|
me: () => ['auth', 'me'],
|
||||||
registrationVideo: () => ['auth', 'registration-video'],
|
registrationVideo: () => ['auth', 'registration-video'],
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,10 +42,10 @@ export const useResetPasswordMutation = () => useMutation({ mutationFn: apiReset
|
|||||||
|
|
||||||
export const useLogoutMutation = () => useMutation({ mutationFn: apiLogout })
|
export const useLogoutMutation = () => useMutation({ mutationFn: apiLogout })
|
||||||
|
|
||||||
export const useGetProfileQuery = (options = {}) =>
|
export const useGetMeQuery = (options = {}) =>
|
||||||
useQuery({
|
useQuery({
|
||||||
queryKey: authKeys.profile(),
|
queryKey: authKeys.me(),
|
||||||
queryFn: () => apiGetProfile(),
|
queryFn: () => apiGetMe(),
|
||||||
select: (response) => response?.data?.user ?? response?.data ?? response,
|
select: (response) => response?.data?.user ?? response?.data ?? response,
|
||||||
...options,
|
...options,
|
||||||
})
|
})
|
||||||
@@ -62,4 +62,4 @@ export const useGetRegistrationVideoQuery = (options = {}) =>
|
|||||||
...options,
|
...options,
|
||||||
})
|
})
|
||||||
|
|
||||||
export const useUploadTemporaryMutation = () => useMutation({ mutationFn: apiUploadTemporary })
|
export const useUploadMediaMutation = () => useMutation({ mutationFn: apiUploadMedia })
|
||||||
|
|||||||
@@ -0,0 +1,126 @@
|
|||||||
|
import jalaali from 'jalaali-js'
|
||||||
|
|
||||||
|
export function gregorianToJalaliString(isoString, separator = '/', persianDigits = false) {
|
||||||
|
if (!isoString) return ''
|
||||||
|
|
||||||
|
const match = isoString.match(/^(\d{4})-(\d{2})-(\d{2})/)
|
||||||
|
if (!match) return ''
|
||||||
|
|
||||||
|
const gy = Number.parseInt(match[1], 10)
|
||||||
|
const gm = Number.parseInt(match[2], 10)
|
||||||
|
const gd = Number.parseInt(match[3], 10)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const jalali = jalaali.toJalaali(gy, gm, gd)
|
||||||
|
let jy = String(jalali.jy)
|
||||||
|
let jm = String(jalali.jm).padStart(2, '0')
|
||||||
|
let jd = String(jalali.jd).padStart(2, '0')
|
||||||
|
|
||||||
|
if (persianDigits) {
|
||||||
|
const enToFa = {
|
||||||
|
0: '۰',
|
||||||
|
1: '۱',
|
||||||
|
2: '۲',
|
||||||
|
3: '۳',
|
||||||
|
4: '۴',
|
||||||
|
5: '۵',
|
||||||
|
6: '۶',
|
||||||
|
7: '۷',
|
||||||
|
8: '۸',
|
||||||
|
9: '۹',
|
||||||
|
}
|
||||||
|
|
||||||
|
jy = jy.replace(/\d/g, (d) => enToFa[d])
|
||||||
|
jm = jm.replace(/\d/g, (d) => enToFa[d])
|
||||||
|
jd = jd.replace(/\d/g, (d) => enToFa[d])
|
||||||
|
}
|
||||||
|
return `${jy}${separator}${jm}${separator}${jd}`
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Date conversion error:', error)
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function gregorianToJalaliStringLong(isoString, separator = '/', persianDigits = false) {
|
||||||
|
if (!isoString) return ''
|
||||||
|
|
||||||
|
const match = isoString.match(/^(\d{4})-(\d{2})-(\d{2})[\sT](\d{2}):(\d{2})/)
|
||||||
|
if (!match) return ''
|
||||||
|
|
||||||
|
const gy = Number.parseInt(match[1], 10)
|
||||||
|
const gm = Number.parseInt(match[2], 10)
|
||||||
|
const gd = Number.parseInt(match[3], 10)
|
||||||
|
const hour = match[4]
|
||||||
|
const minute = match[5]
|
||||||
|
|
||||||
|
try {
|
||||||
|
const jalali = jalaali.toJalaali(gy, gm, gd)
|
||||||
|
let jy = String(jalali.jy)
|
||||||
|
let jm = String(jalali.jm).padStart(2, '0')
|
||||||
|
let jd = String(jalali.jd).padStart(2, '0')
|
||||||
|
let hh = hour
|
||||||
|
let mm = minute
|
||||||
|
|
||||||
|
if (persianDigits) {
|
||||||
|
const enToFa = {
|
||||||
|
0: '۰',
|
||||||
|
1: '۱',
|
||||||
|
2: '۲',
|
||||||
|
3: '۳',
|
||||||
|
4: '۴',
|
||||||
|
5: '۵',
|
||||||
|
6: '۶',
|
||||||
|
7: '۷',
|
||||||
|
8: '۸',
|
||||||
|
9: '۹',
|
||||||
|
}
|
||||||
|
|
||||||
|
jy = jy.replace(/\d/g, (d) => enToFa[d])
|
||||||
|
jm = jm.replace(/\d/g, (d) => enToFa[d])
|
||||||
|
jd = jd.replace(/\d/g, (d) => enToFa[d])
|
||||||
|
hh = hh.replace(/\d/g, (d) => enToFa[d])
|
||||||
|
mm = mm.replace(/\d/g, (d) => enToFa[d])
|
||||||
|
}
|
||||||
|
|
||||||
|
return ` ${jy}${separator}${jm}${separator}${jd}، ${hh}:${mm}`
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Date conversion error:', error)
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function JalaliToGregorianString(jalaliStr) {
|
||||||
|
try {
|
||||||
|
if (!jalaliStr) return null
|
||||||
|
const [jy, jm, jd] = jalaliStr.split('/').map(Number)
|
||||||
|
const greg = jalaali.toGregorian(jy, jm, jd)
|
||||||
|
return `${greg.gy}-${String(greg.gm).padStart(2, '0')}-${String(greg.gd).padStart(
|
||||||
|
2,
|
||||||
|
'0'
|
||||||
|
)}T00:00:00.000Z`
|
||||||
|
} catch (error) {
|
||||||
|
console.log('Jalali to Gregorian conversion error:', error)
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function JalaliToGregorianStringWithTime(jalaliDateTimeStr) {
|
||||||
|
try {
|
||||||
|
if (!jalaliDateTimeStr) return null
|
||||||
|
|
||||||
|
const parts = jalaliDateTimeStr.split(' ')
|
||||||
|
const datePart = parts[0]
|
||||||
|
const timePart = parts[1] || '00:00'
|
||||||
|
|
||||||
|
const [jy, jm, jd] = datePart.split('/').map(Number)
|
||||||
|
const greg = jalaali.toGregorian(jy, jm, jd)
|
||||||
|
|
||||||
|
return `${greg.gy}-${String(greg.gm).padStart(2, '0')}-${String(greg.gd).padStart(
|
||||||
|
2,
|
||||||
|
'0'
|
||||||
|
)}T${timePart}:00.000Z`
|
||||||
|
} catch (error) {
|
||||||
|
console.log('Jalali to Gregorian with time conversion error:', error)
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user