Files
sodalive-vuejs-admin/src/views/Gift/GiftList.vue
T

563 lines
18 KiB
Vue

<template>
<div>
<v-toolbar dark>
<v-spacer />
<v-toolbar-title>선물함 리스트</v-toolbar-title>
<v-spacer />
</v-toolbar>
<br>
<v-container fluid>
<v-form @submit.prevent="search">
<v-row align="center">
<v-col
cols="12"
md="2"
>
<v-select
v-model="filters.status"
:items="giftStatuses"
:disabled="is_loading || is_operating"
label="상태"
/>
</v-col>
<v-col
cols="12"
md="3"
>
<v-text-field
v-model="filters.applicationNo"
:disabled="is_loading || is_operating"
label="신청번호"
clearable
/>
</v-col>
<v-col
cols="12"
md="4"
>
<v-text-field
v-model="filters.nickname"
:disabled="is_loading || is_operating"
label="발송인 또는 수취인 닉네임"
clearable
/>
</v-col>
<v-col
cols="12"
md="3"
>
<v-btn
type="submit"
color="#3bb9f1"
:disabled="is_loading || is_operating"
dark
>
검색
</v-btn>
<v-btn
class="ml-2"
:disabled="is_loading || is_operating"
@click="reset"
>
초기화
</v-btn>
</v-col>
</v-row>
</v-form>
<v-row>
<v-col>
<div class="mb-2">
총 {{ totalCount }}건
</div>
<v-progress-linear
v-if="is_loading"
indeterminate
/>
<v-simple-table class="elevation-10">
<thead>
<tr>
<th
v-for="header in headers"
:key="header"
class="text-center"
>
{{ header }}
</th>
</tr>
</thead>
<tbody>
<tr
v-for="item in gifts"
:key="item.applicationNo"
>
<td class="text-center">
{{ item.applicationNo }}
</td>
<td class="text-center">
{{ item.senderNickname }}
</td>
<td class="text-center">
{{ item.recipientNickname }}
</td>
<td class="text-center">
{{ item.sizeName }} / {{ item.sizeCode }}
</td>
<td class="text-center">
{{ item.categoryName }}
</td>
<td class="text-center">
{{ item.classificationNumber }}
</td>
<td class="text-center">
{{ item.courierCompanyName || '-' }}
</td>
<td class="text-center">
{{ item.trackingNumber || '-' }}
</td>
<td class="text-center">
{{ item.statusName }}
</td>
<td class="text-center">
<v-btn
class="ma-1"
small
:disabled="is_operating || is_loading"
@click="openDetail(item.applicationNo)"
>
상세
</v-btn>
<gift-actions
:actions="item.availableActions"
:disabled="is_operating || is_loading"
@select="requestAction(item, $event)"
/>
</td>
</tr>
<tr v-if="!gifts.length">
<td
:colspan="headers.length"
class="text-center"
>
{{ is_loading ? '조회 중입니다.' : list_error ? '목록을 불러오지 못했습니다. 다시 검색해 주세요.' : '조회된 선물 신청이 없습니다.' }}
</td>
</tr>
</tbody>
</v-simple-table>
</v-col>
</v-row>
<v-row class="text-center">
<v-col>
<v-pagination
:value="page"
:length="total_page"
:disabled="is_loading || is_operating"
:total-visible="10"
circle
@input="changePage"
/>
</v-col>
</v-row>
</v-container>
<v-dialog
v-model="show_detail_dialog"
max-width="1000px"
persistent
scrollable
>
<v-card>
<v-card-title>선물 상세</v-card-title>
<v-progress-linear
v-if="is_detail_loading"
indeterminate
/>
<v-card-text>
<template v-if="detail">
<div class="mb-4">
신청번호: {{ detail.applicationNo }} / 현재 상태: {{ detail.statusName }}
</div>
<v-row>
<v-col
v-for="member in memberSections"
:key="member.key"
cols="12"
md="6"
>
<h3 class="mb-2">
{{ member.title }}
</h3>
<v-simple-table>
<tbody>
<tr
v-for="field in memberFields"
:key="field.key"
>
<th>{{ field.label }}</th>
<td>{{ detail[member.key][field.key] || (member.key === 'recipientInfo' && field.key !== 'nickname' ? '미입력' : '-') }}</td>
</tr>
</tbody>
</v-simple-table>
</v-col>
<v-col
cols="12"
md="6"
>
<h3 class="mb-2">
상품 정보
</h3>
<v-simple-table>
<tbody>
<tr><th>사이즈</th><td>{{ detail.productInfo.sizeName }} / {{ detail.productInfo.sizeCode }}</td></tr>
<tr><th>카테고리</th><td>{{ detail.productInfo.categoryName }}</td></tr>
<tr><th>분류번호</th><td>{{ detail.productInfo.classificationNumber }}</td></tr>
</tbody>
</v-simple-table>
</v-col>
<v-col
cols="12"
md="6"
>
<h3 class="mb-2">
입고 배송 정보
</h3>
<v-simple-table>
<tbody>
<tr><th>택배사</th><td>{{ detail.inboundDeliveryInfo.courierCompanyName || '-' }}</td></tr>
<tr><th>운송장번호</th><td>{{ detail.inboundDeliveryInfo.trackingNumber || '-' }}</td></tr>
</tbody>
</v-simple-table>
</v-col>
</v-row>
<div class="mt-4">
<h3 class="mb-2">
가능한 액션
</h3>
<gift-actions
:actions="detail.availableActions"
:disabled="is_operating || is_detail_loading || is_loading"
@select="requestAction(detail, $event)"
/>
<span v-if="!detail.availableActions.length">현재 가능한 액션이 없습니다.</span>
</div>
</template>
<div v-else-if="!is_detail_loading">
상세 정보를 불러오지 못했습니다.
<v-btn
text
color="primary"
@click="getDetail(detailApplicationNo)"
>
다시 시도
</v-btn>
</div>
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn
color="blue darken-1"
text
:disabled="is_operating"
@click="closeDetail"
>
닫기
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<v-dialog
v-model="show_reason_dialog"
max-width="500px"
persistent
>
<v-card>
<v-card-title>전달 불가 처리</v-card-title>
<v-card-text>
<p>신청번호: {{ pendingAction ? pendingAction.applicationNo : '' }}</p>
<v-form @submit.prevent="confirmReason">
<v-textarea
v-model="reason"
label="전달 불가 사유"
:rules="[validateUndeliverableReason]"
:error-messages="reason_error"
:disabled="is_operating"
counter="255"
maxlength="255"
auto-grow
rows="3"
@input="reason_error = ''"
/>
</v-form>
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn
color="blue darken-1"
text
:disabled="is_operating"
@click="cancelAction"
>
취소
</v-btn>
<v-btn
color="blue darken-1"
text
:disabled="is_operating"
@click="confirmReason"
>
확인
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<v-dialog
v-model="show_confirm_dialog"
max-width="400px"
persistent
>
<v-card>
<v-card-title>상태 변경 확인</v-card-title>
<v-card-text v-if="pendingAction">
<p>신청번호: {{ pendingAction.applicationNo }}</p>
<p>{{ pendingAction.label }}하시겠습니까?</p>
<p
v-if="pendingAction.value === 'MARK_UNDELIVERABLE'"
class="gift-reason"
>
사유: {{ reason.trim() }}
</p>
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn
color="blue darken-1"
text
:disabled="is_operating"
@click="cancelConfirmation"
>
취소
</v-btn>
<v-btn
color="blue darken-1"
text
:loading="is_operating"
:disabled="is_operating"
@click="executeAction"
>
확인
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</div>
</template>
<script>
import * as api from '@/api/gift'
import { giftActions, giftStatuses, validateUndeliverableReason } from '@/utils/gift'
import GiftActions from './GiftActions.vue'
export default {
name: 'GiftList',
components: { GiftActions },
data() {
return {
giftStatuses,
headers: ['신청번호', '발송인 닉네임', '수취인 닉네임', '사이즈', '카테고리', '분류번호', '택배사', '운송장번호', '상태', '액션'],
memberSections: [{ key: 'senderInfo', title: '발송인 정보' }, { key: 'recipientInfo', title: '수취인 정보' }],
memberFields: [{ key: 'nickname', label: '닉네임' }, { key: 'name', label: '이름' }, { key: 'phoneNumber', label: '전화번호' }, { key: 'address', label: '주소' }],
filters: { status: '', applicationNo: '', nickname: '' },
appliedFilters: { status: '', applicationNo: '', nickname: '' },
gifts: [],
page: 1,
size: 20,
totalCount: 0,
hasNext: false,
is_loading: false,
list_error: false,
listRequestId: 0,
detail: null,
detailApplicationNo: '',
detailRequestId: 0,
is_detail_loading: false,
show_detail_dialog: false,
is_operating: false,
pendingAction: null,
show_reason_dialog: false,
show_confirm_dialog: false,
reason: '',
reason_error: ''
}
},
computed: {
total_page() {
const count = Math.max(1, Math.ceil(this.totalCount / this.size))
return this.hasNext ? Math.max(count, this.page + 1) : Math.max(1, this.page)
}
},
async created() {
await this.getGifts()
},
beforeDestroy() {
this.listRequestId++
this.detailRequestId++
},
methods: {
validateUndeliverableReason,
notifyError(error) {
const response = error.response && error.response.data
this.$dialog.notify.error((response && response.message) || error.message || '알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.')
},
checkResponse(response) {
if (response.success !== true) throw new Error(response.message || '알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.')
},
async getGifts(page = this.page) {
const requestId = ++this.listRequestId
this.is_loading = true
this.list_error = false
try {
const response = await api.getGifts({ ...this.appliedFilters, page: page - 1, size: this.size })
if (requestId !== this.listRequestId) return
this.checkResponse(response)
this.gifts = response.data.items
this.page = response.data.page + 1
this.size = response.data.size
this.totalCount = response.data.totalCount
this.hasNext = response.data.hasNext
} catch (error) {
if (requestId !== this.listRequestId) return
this.gifts = []
this.totalCount = 0
this.hasNext = false
this.list_error = true
this.notifyError(error)
} finally {
if (requestId === this.listRequestId) this.is_loading = false
}
},
async search() {
if (this.is_loading || this.is_operating) return
this.appliedFilters = {
status: this.filters.status || '',
applicationNo: (this.filters.applicationNo || '').trim(),
nickname: (this.filters.nickname || '').trim()
}
await this.getGifts(1)
},
async reset() {
if (this.is_loading || this.is_operating) return
this.filters = { status: '', applicationNo: '', nickname: '' }
await this.search()
},
async changePage(page) {
if (this.is_loading || this.is_operating) return
await this.getGifts(page)
},
async openDetail(applicationNo) {
if (this.is_operating) return
this.detail = null
this.show_detail_dialog = true
await this.getDetail(applicationNo)
},
async getDetail(applicationNo) {
const requestId = ++this.detailRequestId
this.detailApplicationNo = applicationNo
this.is_detail_loading = true
try {
const response = await api.getGift(applicationNo)
if (requestId !== this.detailRequestId) return
this.checkResponse(response)
this.detail = response.data
} catch (error) {
if (requestId !== this.detailRequestId) return
this.detail = null
this.notifyError(error)
} finally {
if (requestId === this.detailRequestId) this.is_detail_loading = false
}
},
closeDetail() {
if (this.is_operating) return
this.detailRequestId++
this.show_detail_dialog = false
this.is_detail_loading = false
this.detail = null
this.detailApplicationNo = ''
},
requestAction(item, value) {
if (this.is_operating || this.is_loading || this.is_detail_loading || this.pendingAction) return
const action = giftActions.find(action => action.value === value)
if (!action || !item.availableActions.includes(value)) return
this.pendingAction = { ...action, applicationNo: item.applicationNo }
this.reason = ''
this.reason_error = ''
if (value === 'MARK_UNDELIVERABLE') {
this.show_reason_dialog = true
} else {
this.show_confirm_dialog = true
}
},
confirmReason() {
if (this.is_operating || !this.pendingAction || this.pendingAction.value !== 'MARK_UNDELIVERABLE') return
const validation = validateUndeliverableReason(this.reason)
this.reason_error = validation === true ? '' : validation
if (validation === true) this.show_confirm_dialog = true
},
cancelConfirmation() {
if (this.is_operating) return
if (this.pendingAction && this.pendingAction.value === 'MARK_UNDELIVERABLE') {
this.show_confirm_dialog = false
} else {
this.cancelAction()
}
},
cancelAction() {
if (this.is_operating) return
this.show_confirm_dialog = false
this.show_reason_dialog = false
this.pendingAction = null
this.reason = ''
this.reason_error = ''
},
async executeAction() {
if (this.is_operating || !this.pendingAction || !this.show_confirm_dialog) return
const action = this.pendingAction
if (action.value === 'MARK_UNDELIVERABLE' && validateUndeliverableReason(this.reason) !== true) {
this.show_confirm_dialog = false
this.confirmReason()
return
}
this.is_operating = true
try {
const response = action.value === 'MARK_UNDELIVERABLE'
? await api.markUndeliverable(action.applicationNo, this.reason.trim())
: await api[action.method](action.applicationNo)
this.checkResponse(response)
this.show_confirm_dialog = false
this.show_reason_dialog = false
this.pendingAction = null
this.reason = ''
this.$dialog.notify.success(`${action.label}되었습니다.`)
const refreshes = [this.getGifts()]
if (this.show_detail_dialog) refreshes.push(this.getDetail(this.detail ? this.detail.applicationNo : this.detailApplicationNo))
await Promise.all(refreshes)
} catch (error) {
this.notifyError(error)
} finally {
this.is_operating = false
}
}
}
}
</script>
<style scoped>
.gift-reason {
white-space: pre-wrap;
overflow-wrap: anywhere;
}
</style>