diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index c90b0bf1bfb25f88bb4b76598342080ee31dbdf4..4e42c92cf3089453ab7e26dc398fc33d9684f964 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -35,9 +35,10 @@ deployToS3: before_script: - curl "https://d1vvhvl2y92vvt.cloudfront.net/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" - unzip awscliv2.zip - - sudo ./aws/install + - ./aws/install script: - aws2 s3 rm s3://$S3_BUCKET_NAME/ --recursive + - npm run build - aws2 s3 cp dist s3://$S3_BUCKET_NAME/ --recursive --acl public-read when: manual only: diff --git a/src/components/NavigationDrawer.vue b/src/components/NavigationDrawer.vue index c247b9728334a28b0fc29cb34b310d69d183113f..3005d0bc4fd05f2aacd4a98666de490f2dc90587 100644 --- a/src/components/NavigationDrawer.vue +++ b/src/components/NavigationDrawer.vue @@ -5,10 +5,8 @@ :value="isNavDrawerVisible" > <v-list> - <v-subheader>회원 관리</v-subheader> - <v-list-item-group - color="primary" - > + <v-subheader>회원</v-subheader> + <v-list-item-group color="primary"> <v-list-item link :to="{ name: 'TraineeList' }" @@ -17,14 +15,35 @@ <v-icon>mdi-account-group</v-icon> </v-list-item-icon> <v-list-item-content> - <v-list-item-title>회원 목록</v-list-item-title> + <v-list-item-title>전체 회원</v-list-item-title> + </v-list-item-content> + </v-list-item> + <v-list-item + link + :to="{ name: 'DietList' }" + > + <v-list-item-icon> + <v-icon>mdi-pasta</v-icon> + </v-list-item-icon> + <v-list-item-content> + <v-list-item-title>식단 관리</v-list-item-title> + </v-list-item-content> + </v-list-item> + <v-list-item + link + :to="{ name: 'ExerciseList' }" + > + <v-list-item-icon> + <v-icon>mdi-pasta</v-icon> + </v-list-item-icon> + <v-list-item-content> + <v-list-item-title>운동 관리</v-list-item-title> </v-list-item-content> </v-list-item> </v-list-item-group> - <v-subheader>프로그램 관리</v-subheader> - <v-list-item-group - color="primary" - > + + <v-subheader>프로그램</v-subheader> + <v-list-item-group color="primary"> <v-list-item link :to="{ name: 'ProgramList' }" @@ -33,7 +52,7 @@ <v-icon>mdi-shape</v-icon> </v-list-item-icon> <v-list-item-content> - <v-list-item-title>프로그램 목록</v-list-item-title> + <v-list-item-title>전체 프로그램</v-list-item-title> </v-list-item-content> </v-list-item> <v-list-item @@ -44,14 +63,13 @@ <v-icon>mdi-ticket</v-icon> </v-list-item-icon> <v-list-item-content> - <v-list-item-title>이용권 목록</v-list-item-title> + <v-list-item-title>프로그램 이용권 관리</v-list-item-title> </v-list-item-content> </v-list-item> </v-list-item-group> - <v-subheader>일정 관리</v-subheader> - <v-list-item-group - color="primary" - > + + <v-subheader>일정</v-subheader> + <v-list-item-group color="primary"> <v-list-item link :to="{ name: 'ScheduleList' }" @@ -60,14 +78,13 @@ <v-icon>mdi-shape</v-icon> </v-list-item-icon> <v-list-item-content> - <v-list-item-title>일정 목록</v-list-item-title> + <v-list-item-title>전체 일정</v-list-item-title> </v-list-item-content> </v-list-item> </v-list-item-group> - <v-subheader>매장 관리</v-subheader> - <v-list-item-group - color="primary" - > + + <v-subheader>매장</v-subheader> + <v-list-item-group color="primary"> <v-list-item link :to="{ name: 'TrainerList' }" @@ -76,7 +93,7 @@ <v-icon>mdi-account-supervisor</v-icon> </v-list-item-icon> <v-list-item-content> - <v-list-item-title>트레이너 목록</v-list-item-title> + <v-list-item-title>트레이너 관리</v-list-item-title> </v-list-item-content> </v-list-item> </v-list-item-group> diff --git a/src/router/index.js b/src/router/index.js index d87b8ced91c19ba1e33e5eb20ca55438b2e5fb1c..63f34c259dde6430aeac16bc0381f341d1a85156 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -5,6 +5,10 @@ import Store from '@/store/index'; import APISetting from '@/settings/api'; import Login from '@/views/Login.vue'; import Main from '@/views/Main.vue'; +import DietList from '@/views/DietList.vue'; +import DietDetail from '@/views/DietDetail.vue'; +import ExerciseList from '@/views/ExerciseList.vue'; +import ExerciseDetail from '@/views/ExerciseDetail.vue'; import ProgramList from '@/views/ProgramList.vue'; import ProgramCreate from '@/views/ProgramCreate.vue'; import ProgramDetail from '@/views/ProgramDetail.vue'; @@ -35,6 +39,26 @@ const routes = [ name: 'Main', component: Main, }, + { + path: '/diet', + name: 'DietList', + component: DietList, + }, + { + path: '/diet/:id', + name: 'DietDetail', + component: DietDetail, + }, + { + path: '/exercise', + name: 'ExerciseList', + component: ExerciseList, + }, + { + path: '/exercise/:id', + name: 'ExerciseDetail', + component: ExerciseDetail, + }, { path: '/program', name: 'ProgramList', diff --git a/src/settings/api.js b/src/settings/api.js index a0b1dcd9198fb9d0f1aa99aefba8b52cfb708199..7d4efbe230c8147179b58744973369396d03f9c3 100644 --- a/src/settings/api.js +++ b/src/settings/api.js @@ -14,6 +14,10 @@ export default { list: `${host}/diet`, detail: id => (`${host}/diet/${id}`), }, + exercise: { + list: `${host}/exercise`, + detail: id => (`${host}/exercise/${id}`), + }, program: { list: `${host}/program`, detail: id => (`${host}/program/${id}`), @@ -25,6 +29,10 @@ export default { trainee: { list: `${host}/trainee`, detail: id => (`${host}/trainee/${id}`), + booking: id => (`${host}/trainee/${id}/booking`), + diet: id => (`${host}/trainee/${id}/diet`), + exercise: id => (`${host}/trainee/${id}/exercise`), + inventory: id => (`${host}/trainee/${id}/inventory`), }, trainer: { list: `${host}/trainer`, diff --git a/src/views/DietDetail.vue b/src/views/DietDetail.vue new file mode 100644 index 0000000000000000000000000000000000000000..5d027a863ad603439dc4d5a18fd129166df64ed3 --- /dev/null +++ b/src/views/DietDetail.vue @@ -0,0 +1,186 @@ +<template> + <div class="diet-detail"> + <v-row> + <v-col cols="12"> + <v-card :loading="isProcessing"> + <v-card-title>식단 정보</v-card-title> + <v-card-text> + <v-alert + v-if="error.isError" + dense + text + type="error" + > + {{ error.message }} + </v-alert> + <v-text-field + label="등록번호" + readonly + v-model="id" + ></v-text-field> + <v-textarea + auto-grow + label="식단" + readonly + v-model="diet.diet" + ></v-textarea> + <v-textarea + auto-grow + label="피드백" + v-model="diet.feedback" + ></v-textarea> + <v-text-field + label="등록 시각" + readonly + v-model="diet.createdAt" + ></v-text-field> + <v-text-field + label="최종 수정 시각" + readonly + v-model="diet.updatedAt" + ></v-text-field> + </v-card-text> + <v-card-actions> + <v-spacer></v-spacer> + <v-btn + color="error" + outlined + @click="deleteDiet(id)" + > + <v-icon left>mdi-delete</v-icon> + 삭제 + </v-btn> + <v-btn + color="primary" + outlined + @click="updateDiet(id)" + > + <v-icon left>mdi-content-save</v-icon> + 저장 + </v-btn> + </v-card-actions> + </v-card> + </v-col> + </v-row> + </div> +</template> + +<script> +import moment from 'moment'; + +import APISetting from '@/settings/api'; + +export default { + name: 'DietDetail', + + data: () => ({ + isProcessing: false, + error: { + isError: false, + message: '', + }, + showDatePicker: false, + diet: { + diet: '', + feedback: '', + createdAt: '', + updatedAt: '', + }, + }), + + computed: { + id() { + if ('id' in this.$route.params) return this.$route.params.id; + return null; + }, + }, + + methods: { + getDiet(id) { + this.error.isError = false; + this.error.message = ''; + this.isProcessing = true; + + fetch(APISetting.endpoints.diet.detail(id), APISetting.settings.get) + .then((res) => { + if (res.status === 404) return Promise.all([null, res]); + if ([200, 400, 500].includes(res.status)) return Promise.all([res.json(), res]); + throw new Error('알 수 없는 응답입니다.'); + }) + .then((values) => { + const [json, res] = values; + if (res.status === 404) throw new Error('존재하지 않는 데이터입니다.'); + if (res.status !== 200) throw new Error(json.message); + const { diet } = json; + diet.createdAt = moment(diet.createdAt).format('YYYY-MM-DD HH:mm:ss'); + diet.updatedAt = moment(diet.updatedAt).format('YYYY-MM-DD HH:mm:ss'); + this.diet = diet; + }) + .catch((e) => { + this.error.message = e.message; + this.error.isError = true; + }) + .finally(() => { + this.isProcessing = false; + }); + }, + updateDiet(id) { + this.error.isError = false; + this.error.message = ''; + this.isProcessing = true; + + fetch(APISetting.endpoints.diet.detail(id), APISetting.settings.put(this.diet)) + .then((res) => { + if (res.status === 404) return Promise.all([null, res]); + if ([200, 400, 500].includes(res.status)) return Promise.all([res.json(), res]); + throw new Error('알 수 없는 응답입니다.'); + }) + .then((values) => { + const [json, res] = values; + if (res.status === 404) throw new Error('존재하지 않는 데이터입니다.'); + if (res.status !== 200) throw new Error(json.message); + const { diet } = json; + diet.createdAt = moment(diet.createdAt).format('YYYY-MM-DD HH:mm:ss'); + diet.updatedAt = moment(diet.updatedAt).format('YYYY-MM-DD HH:mm:ss'); + this.diet = diet; + }) + .catch((e) => { + this.error.message = e.message; + this.error.isError = true; + }) + .finally(() => { + this.isProcessing = false; + }); + }, + deleteDiet(id) { + this.error.isError = false; + this.error.message = ''; + this.isProcessing = true; + + fetch(APISetting.endpoints.diet.detail(id), APISetting.settings.delete) + .then((res) => { + if ([204, 404].includes(res.status)) return Promise.all([null, res]); + if ([400, 500].includes(res.status)) return Promise.all([res.json(), res]); + throw new Error('알 수 없는 응답입니다.'); + }) + .then((values) => { + const [json, res] = values; + if (res.status === 404) throw new Error('존재하지 않는 데이터입니다.'); + if (res.status !== 204) throw new Error(json.message); + return this.$router.push('/diet'); + }) + .catch((e) => { + this.error.message = e.message; + this.error.isError = true; + }) + .finally(() => { + this.isProcessing = false; + }); + }, + }, + + created() { + this.getDiet(this.id); + }, +}; +</script> diff --git a/src/views/DietList.vue b/src/views/DietList.vue new file mode 100644 index 0000000000000000000000000000000000000000..1b955629830f013410bc090bfd7aab569c7b7084 --- /dev/null +++ b/src/views/DietList.vue @@ -0,0 +1,164 @@ +<template> + <div class="diet-list"> + <v-row> + <v-col cols="12"> + <v-card> + <v-card-title>식단 목록</v-card-title> + <v-card-text> + <v-row justify="end"> + <v-col + cols="12" + sm="4" + md="3" + lg="3" + xl="2" + > + <v-text-field + append-icon="mdi-magnify" + clearable + label="식단 검색" + v-model="dietList.searchKeyword" + ></v-text-field> + </v-col> + </v-row> + <v-data-table + :headers="dietList.headers" + item-key="id" + :items="dietList.data" + :loading="isProcessing" + loading-text="데이터를 불러오는 중입니다." + :search="dietList.searchKeyword" + no-results-text="일치하는 식단을 찾지 못했습니다." + > + <template v-slot:item.action="{ item }"> + <v-icon + small + class="mr-2" + @click="editDiet(item.id)" + > + mdi-pencil + </v-icon> + <v-icon + small + @click="deleteDiet(item.id)" + > + mdi-delete + </v-icon> + </template> + </v-data-table> + </v-card-text> + </v-card> + </v-col> + </v-row> + </div> +</template> + +<script> +import moment from 'moment'; + +import APISetting from '@/settings/api'; + +export default { + name: 'DietList', + + data: () => ({ + isProcessing: false, + error: { + isError: false, + message: '', + }, + dietList: { + headers: [ + { + text: '#', + value: 'id', + }, + { + text: '회원', + value: 'trainee.name', + }, + { + text: '식단', + value: 'diet', + }, + { + text: '피드백 여부', + value: 'hasFeedback', + }, + { + text: '등록 시각', + value: 'createdAt', + }, + { + text: '', + value: 'action', + sortable: false, + }, + ], + data: [], + searchKeyword: '', + }, + }), + + methods: { + getDietList() { + this.error.isError = false; + this.error.message = ''; + this.isProcessing = true; + + fetch(APISetting.endpoints.diet.list, APISetting.settings.get) + .then((res) => { + if (res.status === 200) return res.json(); + throw new Error('알 수 없는 응답입니다.'); + }) + .then((json) => { + this.dietList.data = json.diets; + this.dietList.data.forEach((diet) => { + // eslint-disable-next-line no-param-reassign + diet.hasFeedback = diet.feedback ? '완료' : '미완료'; + // eslint-disable-next-line no-param-reassign + diet.createdAt = moment(diet.createdAt).format('YYYY-MM-DD HH:mm:ss'); + }); + }) + .catch((e) => { + this.error.message = e.message; + this.error.isError = true; + }) + .finally(() => { + this.isProcessing = false; + }); + }, + editDiet(id) { + this.$router.push(`/diet/${id}`); + }, + deleteDiet(id) { + this.error.isError = false; + this.error.message = ''; + this.isProcessing = true; + + fetch(APISetting.endpoints.diet.detail(id), APISetting.settings.delete) + .then((res) => { + if (res.status === 204) return Promise.all([null, res]); + if ([400, 404, 500].includes(res.status)) return Promise.all([res.json(), res]); + throw new Error('알 수 없는 응답입니다.'); + }) + .then((values) => { + const [json, res] = values; + if (res.status === 204) return this.getDietList(); + throw new Error(json.message); + }) + .catch((e) => { + this.error.message = e.message; + this.error.isError = true; + }) + .finally(() => { + this.isProcessing = true; + }); + }, + }, + + created() { + this.getDietList(); + }, +}; +</script> diff --git a/src/views/ExerciseDetail.vue b/src/views/ExerciseDetail.vue new file mode 100644 index 0000000000000000000000000000000000000000..1f7c67da19084324a6d5ae179abd1f7007b8bc91 --- /dev/null +++ b/src/views/ExerciseDetail.vue @@ -0,0 +1,186 @@ +<template> + <div class="exercise-detail"> + <v-row> + <v-col cols="12"> + <v-card :loading="isProcessing"> + <v-card-title>운동 정보</v-card-title> + <v-card-text> + <v-alert + v-if="error.isError" + dense + text + type="error" + > + {{ error.message }} + </v-alert> + <v-text-field + label="등록번호" + readonly + v-model="id" + ></v-text-field> + <v-textarea + auto-grow + label="운동" + readonly + v-model="exercise.exercise" + ></v-textarea> + <v-textarea + auto-grow + label="피드백" + v-model="exercise.feedback" + ></v-textarea> + <v-text-field + label="등록 시각" + readonly + v-model="exercise.createdAt" + ></v-text-field> + <v-text-field + label="최종 수정 시각" + readonly + v-model="exercise.updatedAt" + ></v-text-field> + </v-card-text> + <v-card-actions> + <v-spacer></v-spacer> + <v-btn + color="error" + outlined + @click="deleteExercise(id)" + > + <v-icon left>mdi-delete</v-icon> + 삭제 + </v-btn> + <v-btn + color="primary" + outlined + @click="updateExercise(id)" + > + <v-icon left>mdi-content-save</v-icon> + 저장 + </v-btn> + </v-card-actions> + </v-card> + </v-col> + </v-row> + </div> +</template> + +<script> +import moment from 'moment'; + +import APISetting from '@/settings/api'; + +export default { + name: 'ExerciseDetail', + + data: () => ({ + isProcessing: false, + error: { + isError: false, + message: '', + }, + showDatePicker: false, + exercise: { + exercise: '', + feedback: '', + createdAt: '', + updatedAt: '', + }, + }), + + computed: { + id() { + if ('id' in this.$route.params) return this.$route.params.id; + return null; + }, + }, + + methods: { + getExercise(id) { + this.error.isError = false; + this.error.message = ''; + this.isProcessing = true; + + fetch(APISetting.endpoints.exercise.detail(id), APISetting.settings.get) + .then((res) => { + if (res.status === 404) return Promise.all([null, res]); + if ([200, 400, 500].includes(res.status)) return Promise.all([res.json(), res]); + throw new Error('알 수 없는 응답입니다.'); + }) + .then((values) => { + const [json, res] = values; + if (res.status === 404) throw new Error('존재하지 않는 데이터입니다.'); + if (res.status !== 200) throw new Error(json.message); + const { exercise } = json; + exercise.createdAt = moment(exercise.createdAt).format('YYYY-MM-DD HH:mm:ss'); + exercise.updatedAt = moment(exercise.updatedAt).format('YYYY-MM-DD HH:mm:ss'); + this.exercise = exercise; + }) + .catch((e) => { + this.error.message = e.message; + this.error.isError = true; + }) + .finally(() => { + this.isProcessing = false; + }); + }, + updateExercise(id) { + this.error.isError = false; + this.error.message = ''; + this.isProcessing = true; + + fetch(APISetting.endpoints.exercise.detail(id), APISetting.settings.put(this.exercise)) + .then((res) => { + if (res.status === 404) return Promise.all([null, res]); + if ([200, 400, 500].includes(res.status)) return Promise.all([res.json(), res]); + throw new Error('알 수 없는 응답입니다.'); + }) + .then((values) => { + const [json, res] = values; + if (res.status === 404) throw new Error('존재하지 않는 데이터입니다.'); + if (res.status !== 200) throw new Error(json.message); + const { exercise } = json; + exercise.createdAt = moment(exercise.createdAt).format('YYYY-MM-DD HH:mm:ss'); + exercise.updatedAt = moment(exercise.updatedAt).format('YYYY-MM-DD HH:mm:ss'); + this.exercise = exercise; + }) + .catch((e) => { + this.error.message = e.message; + this.error.isError = true; + }) + .finally(() => { + this.isProcessing = false; + }); + }, + deleteExercise(id) { + this.error.isError = false; + this.error.message = ''; + this.isProcessing = true; + + fetch(APISetting.endpoints.exercise.detail(id), APISetting.settings.delete) + .then((res) => { + if ([204, 404].includes(res.status)) return Promise.all([null, res]); + if ([400, 500].includes(res.status)) return Promise.all([res.json(), res]); + throw new Error('알 수 없는 응답입니다.'); + }) + .then((values) => { + const [json, res] = values; + if (res.status === 404) throw new Error('존재하지 않는 데이터입니다.'); + if (res.status !== 204) throw new Error(json.message); + return this.$router.push('/exercise'); + }) + .catch((e) => { + this.error.message = e.message; + this.error.isError = true; + }) + .finally(() => { + this.isProcessing = false; + }); + }, + }, + + created() { + this.getExercise(this.id); + }, +}; +</script> diff --git a/src/views/ExerciseList.vue b/src/views/ExerciseList.vue new file mode 100644 index 0000000000000000000000000000000000000000..9f35b71d875230914893300f8e23f64913b9900e --- /dev/null +++ b/src/views/ExerciseList.vue @@ -0,0 +1,164 @@ +<template> + <div class="exercise-list"> + <v-row> + <v-col cols="12"> + <v-card> + <v-card-title>운동 목록</v-card-title> + <v-card-text> + <v-row justify="end"> + <v-col + cols="12" + sm="4" + md="3" + lg="3" + xl="2" + > + <v-text-field + append-icon="mdi-magnify" + clearable + label="운동 검색" + v-model="exerciseList.searchKeyword" + ></v-text-field> + </v-col> + </v-row> + <v-data-table + :headers="exerciseList.headers" + item-key="id" + :items="exerciseList.data" + :loading="isProcessing" + loading-text="데이터를 불러오는 중입니다." + :search="exerciseList.searchKeyword" + no-results-text="일치하는 운동을 찾지 못했습니다." + > + <template v-slot:item.action="{ item }"> + <v-icon + small + class="mr-2" + @click="editExercise(item.id)" + > + mdi-pencil + </v-icon> + <v-icon + small + @click="deleteExercise(item.id)" + > + mdi-delete + </v-icon> + </template> + </v-data-table> + </v-card-text> + </v-card> + </v-col> + </v-row> + </div> +</template> + +<script> +import moment from 'moment'; + +import APISetting from '@/settings/api'; + +export default { + name: 'ExerciseList', + + data: () => ({ + isProcessing: false, + error: { + isError: false, + message: '', + }, + exerciseList: { + headers: [ + { + text: '#', + value: 'id', + }, + { + text: '회원', + value: 'trainee.name', + }, + { + text: '운동', + value: 'exercise', + }, + { + text: '피드백 여부', + value: 'hasFeedback', + }, + { + text: '등록 시각', + value: 'createdAt', + }, + { + text: '', + value: 'action', + sortable: false, + }, + ], + data: [], + searchKeyword: '', + }, + }), + + methods: { + getExerciseList() { + this.error.isError = false; + this.error.message = ''; + this.isProcessing = true; + + fetch(APISetting.endpoints.exercise.list, APISetting.settings.get) + .then((res) => { + if (res.status === 200) return res.json(); + throw new Error('알 수 없는 응답입니다.'); + }) + .then((json) => { + this.exerciseList.data = json.exercises; + this.exerciseList.data.forEach((exercise) => { + // eslint-disable-next-line no-param-reassign + exercise.hasFeedback = exercise.feedback ? '완료' : '미완료'; + // eslint-disable-next-line no-param-reassign + exercise.createdAt = moment(exercise.createdAt).format('YYYY-MM-DD HH:mm:ss'); + }); + }) + .catch((e) => { + this.error.message = e.message; + this.error.isError = true; + }) + .finally(() => { + this.isProcessing = false; + }); + }, + editExercise(id) { + this.$router.push(`/exercise/${id}`); + }, + deleteExercise(id) { + this.error.isError = false; + this.error.message = ''; + this.isProcessing = true; + + fetch(APISetting.endpoints.exercise.detail(id), APISetting.settings.delete) + .then((res) => { + if (res.status === 204) return Promise.all([null, res]); + if ([400, 404, 500].includes(res.status)) return Promise.all([res.json(), res]); + throw new Error('알 수 없는 응답입니다.'); + }) + .then((values) => { + const [json, res] = values; + if (res.status === 204) return this.getExerciseList(); + throw new Error(json.message); + }) + .catch((e) => { + this.error.message = e.message; + this.error.isError = true; + }) + .finally(() => { + this.isProcessing = true; + }); + }, + }, + + created() { + this.getExerciseList(); + }, +}; +</script> diff --git a/src/views/ScheduleDetail.vue b/src/views/ScheduleDetail.vue index 4baf94fa8dcbf099358feb86946a4ca658c40db4..50f43518c43b662151e4bb9ef73e4fdd51728781 100644 --- a/src/views/ScheduleDetail.vue +++ b/src/views/ScheduleDetail.vue @@ -226,7 +226,10 @@ export default { const [json, res] = values; if (res.status === 404) throw new Error('존재하지 않는 데이터입니다.'); if (res.status !== 200) throw new Error(json.message); - this.schedule = json.schedule; + const { schedule } = json; + schedule.createdAt = moment(schedule.createdAt).format('YYYY-MM-DD HH:mm:ss'); + schedule.updatedAt = moment(schedule.updatedAt).format('YYYY-MM-DD HH:mm:ss'); + this.schedule = schedule; }) .catch((e) => { this.error.message = e.message; diff --git a/src/views/TraineeDetail.vue b/src/views/TraineeDetail.vue index bfadb67561ce1f43738aaf90eede6ef5b8a4f591..e287fe873b819357e1c83a0eaa9494c588b3ad50 100644 --- a/src/views/TraineeDetail.vue +++ b/src/views/TraineeDetail.vue @@ -82,6 +82,174 @@ </v-card> </v-col> </v-row> + <v-row> + <v-col cols="12"> + <v-card :loading="booking.isProcessing"> + <v-card-title>예약 정보</v-card-title> + <v-card-text> + <v-alert + v-if="booking.error.isError" + dense + text + type="error" + > + {{ booking.error.message }} + </v-alert> + <v-data-table + :headers="booking.list.headers" + item-key="id" + :items="booking.list.data" + :loading="booking.isProcessing" + loading-text="데이터를 불러오는 중입니다." + :search="booking.list.searchKeyword" + no-results-text="일치하는 예약을 찾지 못했습니다." + > + <template v-slot:item.action="{ item }"> + <v-icon + small + class="mr-2" + @click="editBooking(item.id)" + > + mdi-pencil + </v-icon> + <v-icon + small + @click="deleteBooking(item.id)" + > + mdi-delete + </v-icon> + </template> + </v-data-table> + </v-card-text> + </v-card> + </v-col> + </v-row> + <v-row> + <v-col cols="12"> + <v-card :loading="inventory.isProcessing"> + <v-card-title>이용권 정보</v-card-title> + <v-card-text> + <v-alert + v-if="inventory.error.isError" + dense + text + type="error" + > + {{ inventory.error.message }} + </v-alert> + <v-data-table + :headers="inventory.list.headers" + item-key="id" + :items="inventory.list.data" + :loading="inventory.isProcessing" + loading-text="데이터를 불러오는 중입니다." + :search="inventory.list.searchKeyword" + no-results-text="일치하는 이용권을 찾지 못했습니다." + > + <template v-slot:item.action="{ item }"> + <v-icon + small + class="mr-2" + @click="editInventory(item.id)" + > + mdi-pencil + </v-icon> + <v-icon + small + @click="deleteInventory(item.id)" + > + mdi-delete + </v-icon> + </template> + </v-data-table> + </v-card-text> + </v-card> + </v-col> + </v-row> + <v-row> + <v-col cols="12"> + <v-card :loading="diet.isProcessing"> + <v-card-title>식단 정보</v-card-title> + <v-card-text> + <v-alert + v-if="diet.error.isError" + dense + text + type="error" + > + {{ diet.error.message }} + </v-alert> + <v-data-table + :headers="diet.list.headers" + item-key="id" + :items="diet.list.data" + :loading="diet.isProcessing" + loading-text="데이터를 불러오는 중입니다." + :search="diet.list.searchKeyword" + no-results-text="일치하는 식단을 찾지 못했습니다." + > + <template v-slot:item.action="{ item }"> + <v-icon + small + class="mr-2" + @click="editDiet(item.id)" + > + mdi-pencil + </v-icon> + <v-icon + small + @click="deleteDiet(item.id)" + > + mdi-delete + </v-icon> + </template> + </v-data-table> + </v-card-text> + </v-card> + </v-col> + </v-row> + <v-row> + <v-col cols="12"> + <v-card :loading="exercise.isProcessing"> + <v-card-title>운동 정보</v-card-title> + <v-card-text> + <v-alert + v-if="exercise.error.isError" + dense + text + type="error" + > + {{ exercise.error.message }} + </v-alert> + <v-data-table + :headers="exercise.list.headers" + item-key="id" + :items="exercise.list.data" + :loading="exercise.isProcessing" + loading-text="데이터를 불러오는 중입니다." + :search="exercise.list.searchKeyword" + no-results-text="일치하는 운동을 찾지 못했습니다." + > + <template v-slot:item.action="{ item }"> + <v-icon + small + class="mr-2" + @click="editExercise(item.id)" + > + mdi-pencil + </v-icon> + <v-icon + small + @click="deleteExercise(item.id)" + > + mdi-delete + </v-icon> + </template> + </v-data-table> + </v-card-text> + </v-card> + </v-col> + </v-row> </div> </template> @@ -108,6 +276,146 @@ export default { createdAt: '', updatedAt: '', }, + booking: { + isProcessing: false, + error: { + isError: false, + message: '', + }, + list: { + headers: [ + { + text: '프로그램 이름', + value: 'schedule.program.title', + }, + { + text: '시작 시각', + value: 'schedule.startAt', + }, + { + text: '메모', + value: 'memo', + }, + { + text: '등록 시각', + value: 'createdAt', + }, + { + text: '', + value: 'action', + sortable: false, + }, + ], + data: [], + searchKeyword: '', + }, + }, + inventory: { + isProcessing: false, + error: { + isError: false, + message: '', + }, + list: { + headers: [ + { + text: '#', + value: 'id', + }, + { + text: '프로그램 이름', + value: 'program.title', + }, + { + text: '사용 완료 횟수', + value: 'usedCount', + }, + { + text: '사용 가능 횟수', + value: 'maxCount', + }, + { + text: '등록 시각', + value: 'createdAt', + }, + { + text: '', + value: 'action', + sortable: false, + }, + ], + data: [], + searchKeyword: '', + }, + }, + diet: { + isProcessing: false, + error: { + isError: false, + message: '', + }, + list: { + headers: [ + { + text: '#', + value: 'id', + }, + { + text: '식단', + value: 'diet', + }, + { + text: '피드백 여부', + value: 'hasFeedback', + }, + { + text: '등록 시각', + value: 'createdAt', + }, + { + text: '', + value: 'action', + sortable: false, + }, + ], + data: [], + searchKeyword: '', + }, + }, + exercise: { + isProcessing: false, + error: { + isError: false, + message: '', + }, + list: { + headers: [ + { + text: '#', + value: 'id', + }, + { + text: '운동', + value: 'exercise', + }, + { + text: '피드백 여부', + value: 'hasFeedback', + }, + { + text: '등록 시각', + value: 'createdAt', + }, + { + text: '', + value: 'action', + sortable: false, + }, + ], + data: [], + searchKeyword: '', + }, + }, }), computed: { @@ -137,6 +445,10 @@ export default { trainee.createdAt = moment(trainee.createdAt).format('YYYY-MM-DD HH:mm:ss'); trainee.updatedAt = moment(trainee.updatedAt).format('YYYY-MM-DD HH:mm:ss'); this.trainee = trainee; + this.getBookingList(trainee.id); + this.getInventoryList(trainee.id); + this.getDietList(trainee.id); + this.getExerciseList(trainee.id); }) .catch((e) => { this.error.message = e.message; @@ -165,6 +477,10 @@ export default { trainee.createdAt = moment(trainee.createdAt).format('YYYY-MM-DD HH:mm:ss'); trainee.updatedAt = moment(trainee.updatedAt).format('YYYY-MM-DD HH:mm:ss'); this.trainee = trainee; + this.getBookingList(trainee.id); + this.getInventoryList(trainee.id); + this.getDietList(trainee.id); + this.getExerciseList(trainee.id); }) .catch((e) => { this.error.message = e.message; @@ -199,6 +515,220 @@ export default { this.isProcessing = false; }); }, + getBookingList(id) { + this.booking.error.isError = false; + this.booking.error.message = ''; + this.booking.isProcessing = true; + + fetch(APISetting.endpoints.trainee.booking(id), APISetting.settings.get) + .then((res) => { + if (res.status === 200) return res.json(); + throw new Error('알 수 없는 응답입니다.'); + }) + .then((json) => { + this.booking.list.data = json.bookings; + this.booking.list.data.forEach((item) => { + // eslint-disable-next-line no-param-reassign + item.schedule.startAt = moment(item.schedule.startAt).format('YYYY-MM-DD HH:mm:ss'); + // eslint-disable-next-line no-param-reassign + item.createdAt = moment(item.createdAt).format('YYYY-MM-DD HH:mm:ss'); + }); + }) + .catch((e) => { + this.booking.error.message = e.message; + this.booking.error.isError = true; + }) + .finally(() => { + this.booking.isProcessing = false; + }); + }, + editBooking(id) { + this.$router.push(`/booking/${id}`); + }, + deleteBooking(id) { + this.booking.error.isError = false; + this.booking.error.message = ''; + this.booking.isProcessing = true; + + fetch(APISetting.endpoints.booking.detail(id), APISetting.settings.delete) + .then((res) => { + if (res.status === 204) return Promise.all([null, res]); + if ([400, 404, 500].includes(res.status)) return Promise.all([res.json(), res]); + throw new Error('알 수 없는 응답입니다.'); + }) + .then((values) => { + const [json, res] = values; + if (res.status === 204) return this.getBookingList(id); + throw new Error(json.message); + }) + .catch((e) => { + this.error.message = e.message; + this.error.isError = true; + }) + .finally(() => { + this.isProcessing = true; + }); + }, + getDietList() { + this.error.isError = false; + this.error.message = ''; + this.isProcessing = true; + + fetch(APISetting.endpoints.diet.list, APISetting.settings.get) + .then((res) => { + if (res.status === 200) return res.json(); + throw new Error('알 수 없는 응답입니다.'); + }) + .then((json) => { + this.diet.list.data = json.diets; + this.diet.list.data.forEach((diet) => { + // eslint-disable-next-line no-param-reassign + diet.hasFeedback = diet.feedback ? '완료' : '미완료'; + // eslint-disable-next-line no-param-reassign + diet.createdAt = moment(diet.createdAt).format('YYYY-MM-DD HH:mm:ss'); + }); + }) + .catch((e) => { + this.error.message = e.message; + this.error.isError = true; + }) + .finally(() => { + this.isProcessing = false; + }); + }, + editDiet(id) { + this.$router.push(`/diet/${id}`); + }, + deleteDiet(id) { + this.error.isError = false; + this.error.message = ''; + this.isProcessing = true; + + fetch(APISetting.endpoints.diet.detail(id), APISetting.settings.delete) + .then((res) => { + if (res.status === 204) return Promise.all([null, res]); + if ([400, 404, 500].includes(res.status)) return Promise.all([res.json(), res]); + throw new Error('알 수 없는 응답입니다.'); + }) + .then((values) => { + const [json, res] = values; + if (res.status === 204) return this.getDietList(); + throw new Error(json.message); + }) + .catch((e) => { + this.error.message = e.message; + this.error.isError = true; + }) + .finally(() => { + this.isProcessing = true; + }); + }, + getInventoryList(id) { + this.inventory.error.isError = false; + this.inventory.error.message = ''; + this.inventory.isProcessing = true; + + fetch(APISetting.endpoints.trainee.inventory(id), APISetting.settings.get) + .then((res) => { + if (res.status === 200) return res.json(); + throw new Error('알 수 없는 응답입니다.'); + }) + .then((json) => { + this.inventory.list.data = json.inventories; + this.inventory.list.data.forEach((item) => { + // eslint-disable-next-line no-param-reassign + item.createdAt = moment(item.createdAt).format('YYYY-MM-DD HH:mm:ss'); + }); + }) + .catch((e) => { + this.inventory.error.message = e.message; + this.inventory.error.isError = true; + }) + .finally(() => { + this.inventory.isProcessing = false; + }); + }, + editInventory(id) { + this.$router.push(`/inventory/${id}`); + }, + deleteInventory(id) { + this.inventory.error.isError = false; + this.inventory.error.message = ''; + this.inventory.isProcessing = true; + + fetch(APISetting.endpoints.inventory.detail(id), APISetting.settings.delete) + .then((res) => { + if (res.status === 204) return Promise.all([null, res]); + if ([400, 404, 500].includes(res.status)) return Promise.all([res.json(), res]); + throw new Error('알 수 없는 응답입니다.'); + }) + .then((values) => { + const [json, res] = values; + if (res.status === 204) return this.getInventoryList(id); + throw new Error(json.message); + }) + .catch((e) => { + this.error.message = e.message; + this.error.isError = true; + }) + .finally(() => { + this.isProcessing = true; + }); + }, + getExerciseList(id) { + this.exercise.error.isError = false; + this.exercise.error.message = ''; + this.exercise.isProcessing = true; + + fetch(APISetting.endpoints.trainee.exercise(id), APISetting.settings.get) + .then((res) => { + if (res.status === 200) return res.json(); + throw new Error('알 수 없는 응답입니다.'); + }) + .then((json) => { + this.exercise.list.data = json.exercises; + this.exercise.list.data.forEach((item) => { + // eslint-disable-next-line no-param-reassign + item.hasFeedback = item.feedback ? '완료' : '미완료'; + // eslint-disable-next-line no-param-reassign + item.createdAt = moment(item.createdAt).format('YYYY-MM-DD HH:mm:ss'); + }); + }) + .catch((e) => { + this.exercise.error.message = e.message; + this.exercise.error.isError = true; + }) + .finally(() => { + this.exercise.isProcessing = false; + }); + }, + editExercise(id) { + this.$router.push(`/exercise/${id}`); + }, + deleteExercise(id) { + this.exercise.error.isError = false; + this.exercise.error.message = ''; + this.exercise.isProcessing = true; + + fetch(APISetting.endpoints.exercise.detail(id), APISetting.settings.delete) + .then((res) => { + if (res.status === 204) return Promise.all([null, res]); + if ([400, 404, 500].includes(res.status)) return Promise.all([res.json(), res]); + throw new Error('알 수 없는 응답입니다.'); + }) + .then((values) => { + const [json, res] = values; + if (res.status === 204) return this.getExerciseList(id); + throw new Error(json.message); + }) + .catch((e) => { + this.error.message = e.message; + this.error.isError = true; + }) + .finally(() => { + this.isProcessing = true; + }); + }, }, created() {