delivered.vue 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458
  1. <template>
  2. <div>
  3. <div class="inner--headers">
  4. <h2>{{ pageId }}</h2>
  5. <div class="bread--crumbs--wrap">
  6. <span>홈</span>
  7. <span>{{ pageId }}</span>
  8. </div>
  9. </div>
  10. <div class="search--modules type2">
  11. <div class="search--inner">
  12. <div class="form--cont--filter">
  13. <v-select
  14. v-model="filter"
  15. :items="filderArr"
  16. variant="outlined"
  17. class="custom-select"
  18. >
  19. </v-select>
  20. </div>
  21. <div class="form--cont--text">
  22. <v-text-field
  23. v-model="searchModel"
  24. class="custom-input mini"
  25. style="width: 100%"
  26. placeholder="검색어를 입력하세요"
  27. ></v-text-field>
  28. </div>
  29. </div>
  30. <div class="search--inner">
  31. <div class="calendar-wrap ml--0">
  32. <div class="calendar">
  33. <VueDatePicker
  34. :format="datePickerFormat"
  35. v-model="searchStartDate"
  36. placeholder="날짜를 선택하세요"
  37. :auto-apply="true"
  38. week-start="0"
  39. ></VueDatePicker>
  40. </div>
  41. <span class="text">~</span>
  42. <div class="calendar">
  43. <VueDatePicker
  44. v-model="searchEndDate"
  45. :format="datePickerFormat"
  46. placeholder="날짜를 선택하세요"
  47. :auto-apply="true"
  48. week-start="0"
  49. ></VueDatePicker>
  50. </div>
  51. <div class="month--selector">
  52. <v-btn
  53. v-for="option in dateOptions"
  54. :key="option.value"
  55. :class="{ actv: selectedRange === option.value }"
  56. @click="setDateRange(option.value)"
  57. elevation="0"
  58. >
  59. {{ option.label }}
  60. </v-btn>
  61. </div>
  62. </div>
  63. </div>
  64. <v-btn
  65. class="custom-btn btn-blue mini sch--btn"
  66. @click="fnSearch(searchModel, filter)"
  67. >검색</v-btn
  68. >
  69. </div>
  70. <div class="data--list--wrap">
  71. <div class="btn--actions--wrap">
  72. <div class="left--sections">
  73. <v-btn class="custom-btn bdrs--10" :class="itemType == 'E' ? 'btn-pink' : 'btn-white'" @click="itemType = 'E'"
  74. ><i class="ico"></i>개별 배송</v-btn
  75. >
  76. <v-btn class="custom-btn bdrs--10" :class="itemType == 'E' ? 'btn-white' : 'btn-pink'" @click="itemType = 'G'"
  77. ><i class="ico"></i>공동구매 배송</v-btn
  78. >
  79. </div>
  80. <div class="right--sections">
  81. <!-- <div class="status-filter">
  82. <v-select
  83. v-model="statusFilter"
  84. :items="statusOptions"
  85. variant="outlined"
  86. class="custom-select mini"
  87. style="width: 120px; margin-right: 8px;"
  88. @update:modelValue="applyStatusFilter"
  89. >
  90. </v-select>
  91. </div> -->
  92. </div>
  93. </div>
  94. <div class="btn--actions--wrap">
  95. <div class="left--sections">
  96. <v-btn
  97. v-if="memberType === 'VENDOR'"
  98. class="custom-btn btn-blue mini"
  99. @click="markAsSettled"
  100. :disabled="selectedItems.length === 0"
  101. >
  102. <i class="ico"></i>정산완료 처리
  103. </v-btn>
  104. </div>
  105. <div class="right--sections">
  106. <span class="total-count">총 {{ tblItems.length }}건</span>
  107. </div>
  108. </div>
  109. <div class="tbl-wrapper">
  110. <div class="tbl-wrap">
  111. <!-- ag grid -->
  112. <ag-grid-vue
  113. style="width: 100%; height: calc(10 * 2.94rem)"
  114. class="ag-theme-quartz"
  115. :gridOptions="gridOptions"
  116. :rowData="tblItems"
  117. :rowSelection="memberType === 'VENDOR' ? 'multiple' : 'single'"
  118. :paginationPageSize="pageObj.pageSize"
  119. :suppressPaginationPanel="true"
  120. @grid-ready="onGridReady"
  121. @selection-changed="onSelectionChanged"
  122. >
  123. </ag-grid-vue>
  124. <!-- 페이징 -->
  125. <div class="ag-grid-custom-pagenations">
  126. <pagination @chg_page="chgPage" :pageObj="pageObj"></pagination>
  127. </div>
  128. </div>
  129. </div>
  130. </div>
  131. </div>
  132. </template>
  133. <script setup>
  134. import VueDatePicker from "@vuepic/vue-datepicker";
  135. import "@vuepic/vue-datepicker/dist/main.css";
  136. import { AgGridVue } from "ag-grid-vue3";
  137. import dayjs from 'dayjs';
  138. import pagination from "../components/common/pagination.vue";
  139. definePageMeta({
  140. layout: "default",
  141. });
  142. const props = defineProps({
  143. propsData: {
  144. type: Object,
  145. default: () => {},
  146. },
  147. });
  148. const useDtStore = useDetailStore();
  149. const useAtStore = useAuthStore();
  150. const itemType = ref("E");
  151. const memberType = useAtStore.auth.memberType;
  152. const memberSeq = useAtStore.auth.seq;
  153. const searchModel = ref("");
  154. const selectedRange = ref('all');
  155. const searchStartDate = ref("");
  156. const searchEndDate = ref("");
  157. const datePickerFormat = "yyyy-MM-dd";
  158. const dateOptions = [
  159. { label: '오늘', value: 'today' },
  160. { label: '7일', value: '7d' },
  161. { label: '1개월', value: '1m' },
  162. { label: '3개월', value: '3m' },
  163. { label: '전체', value: 'all' },
  164. ]
  165. const filter = ref("");
  166. const filderArr = ref([
  167. { title: "전체", value: "" },
  168. { title: "제품명", value: "name" },
  169. { title: "구매자명", value: "buyer" },
  170. ]);
  171. const selectedItems = ref([]);
  172. const { $toast, $log, $dayjs, $eventBus } = useNuxtApp();
  173. const router = useRouter();
  174. const pageId = ref("배송완료 관리");
  175. let pageObj = ref({
  176. page: 1,
  177. pageMaxNumSize: 10,
  178. pageSize: 10,
  179. totalCnt: 0,
  180. });
  181. const tblItems = ref([]);
  182. const remToPx = () => parseFloat(getComputedStyle(document.documentElement).fontSize);
  183. const rowHeightRem = 2.65;
  184. const rowHeightPx = rowHeightRem * remToPx();
  185. const gridApi = shallowRef();
  186. // gridOption
  187. const gridOptions = {
  188. columnDefs: [
  189. ...(memberType === 'VENDOR' ? [{ checkboxSelection: true, headerCheckboxSelection: true, width: 50 }] : []),
  190. {
  191. headerName: "No",
  192. valueGetter: (params) => params.api.getDisplayedRowCount() - params.node.rowIndex,
  193. sortable: false,
  194. width: 70,
  195. },
  196. {
  197. headerName: "제품명",
  198. field: "ITEM_NAME",
  199. width: 200,
  200. },
  201. {
  202. headerName: "구매자명",
  203. field: "BUYER_NAME",
  204. width: 120,
  205. },
  206. {
  207. headerName: "연락처",
  208. field: "PHONE",
  209. width: 140,
  210. },
  211. {
  212. headerName: "주소",
  213. field: "ADDRESS",
  214. width: 250,
  215. },
  216. {
  217. headerName: "수량",
  218. field: "QTY",
  219. width: 80,
  220. cellRenderer: (params) => {
  221. return Number(params.value).toLocaleString();
  222. },
  223. },
  224. {
  225. headerName: "금액",
  226. field: "TOTAL",
  227. width: 120,
  228. cellRenderer: (params) => {
  229. return Number(params.value).toLocaleString() + '원';
  230. },
  231. },
  232. {
  233. headerName: "배송업체",
  234. field: "DELI_COMP",
  235. width: 120,
  236. },
  237. {
  238. headerName: "송장번호",
  239. field: "DELI_NUMB",
  240. width: 150,
  241. },
  242. {
  243. headerName: "배송완료일",
  244. field: "DELIVERED_DATE",
  245. width: 140,
  246. cellRenderer: (params) => {
  247. return params.value ? dayjs(params.value).format('YYYY-MM-DD') : '';
  248. },
  249. },
  250. {
  251. headerName: "정산상태",
  252. field: "SETTLEMENT_STATUS",
  253. width: 100,
  254. cellRenderer: (params) => {
  255. const status = params.value || 'PENDING';
  256. const statusMap = {
  257. 'PENDING': { text: '대기', color: 'warning' },
  258. 'COMPLETED': { text: '완료', color: 'success' }
  259. };
  260. const config = statusMap[status] || statusMap['PENDING'];
  261. return `<span class="v-chip v-chip--density-default v-chip--size-default v-chip--variant-elevated bg-${config.color}" style="font-size: 12px; padding: 4px 8px;">${config.text}</span>`;
  262. }
  263. },
  264. ],
  265. rowData: tblItems.value,
  266. autoSizeStrategy: {
  267. type: "fitGridWidth",
  268. },
  269. suppressMovableColumns: true,
  270. headerHeight: rowHeightPx,
  271. rowHeight: rowHeightPx,
  272. pagination: true,
  273. suppressPaginationPanel: true,
  274. rowMultiSelectWithClick: true,
  275. ...(memberType === 'VENDOR' ? {
  276. rowSelection: {
  277. checkboxes: true,
  278. headerCheckbox: true,
  279. enableClickSelection: false,
  280. mode: "multiRow",
  281. }
  282. } : {}),
  283. };
  284. const setDateRange = (range) => {
  285. const today = dayjs();
  286. switch(range) {
  287. case 'today' :
  288. searchStartDate.value = today.format('YYYY-MM-DD');
  289. searchEndDate.value = today.format('YYYY-MM-DD');
  290. selectedRange.value = 'today';
  291. break;
  292. case '7d':
  293. searchStartDate.value = today.subtract(7, 'day').format('YYYY-MM-DD');
  294. searchEndDate.value = today.format('YYYY-MM-DD');
  295. selectedRange.value = '7d';
  296. break;
  297. case '1m':
  298. searchStartDate.value = today.subtract(1, 'month').format('YYYY-MM-DD');
  299. searchEndDate.value = today.format('YYYY-MM-DD');
  300. selectedRange.value = '1m';
  301. break;
  302. case '3m':
  303. searchStartDate.value = today.subtract(3, 'month').format('YYYY-MM-DD');
  304. searchEndDate.value = today.format('YYYY-MM-DD');
  305. selectedRange.value = '3m';
  306. break;
  307. case 'all':
  308. searchStartDate.value = "";
  309. searchEndDate.value = today.format('YYYY-MM-DD');
  310. selectedRange.value = 'all';
  311. break
  312. }
  313. }
  314. const onGridReady = (__PARAMS) => {
  315. gridApi.value = __PARAMS.api;
  316. };
  317. const onSelectionChanged = () => {
  318. if (memberType === 'VENDOR') {
  319. selectedItems.value = gridApi.value.getSelectedRows();
  320. }
  321. };
  322. const chgPage = (__PAGE) => {
  323. pageObj.value.page = __PAGE;
  324. gridApi.value.paginationGoToPage(__PAGE - 1);
  325. };
  326. const getDeliveredList = async () => {
  327. let _req = {
  328. MEMBER_TYPE: memberType,
  329. TYPE: itemType.value
  330. };
  331. if (memberType === "INFLUENCER") {
  332. _req.INF_SEQ = memberSeq;
  333. } else if (memberType === "VENDOR") {
  334. _req.COMPANY_NUMBER = useAtStore.auth.companyNumber || "1";
  335. }
  336. await useAxios()
  337. .post("/deli/delivered", _req)
  338. .then((res) => {
  339. tblItems.value = res.data;
  340. pageObj.value.totalCnt = res.data.length;
  341. // ag-grid 데이터 갱신
  342. if (gridApi.value) {
  343. gridApi.value.setGridOption('rowData', tblItems.value);
  344. }
  345. });
  346. };
  347. const fnSearch = (__KEYWORD, __FILTER) => {
  348. // 검색 로직 구현
  349. let filteredData = tblItems.value;
  350. if (__KEYWORD && __KEYWORD.trim() !== '') {
  351. filteredData = tblItems.value.filter(item => {
  352. if (__FILTER === 'name') {
  353. return item.ITEM_NAME && item.ITEM_NAME.toLowerCase().includes(__KEYWORD.toLowerCase());
  354. } else if (__FILTER === 'buyer') {
  355. return item.BUYER_NAME && item.BUYER_NAME.toLowerCase().includes(__KEYWORD.toLowerCase());
  356. } else {
  357. // 전체 검색
  358. return (item.ITEM_NAME && item.ITEM_NAME.toLowerCase().includes(__KEYWORD.toLowerCase())) ||
  359. (item.BUYER_NAME && item.BUYER_NAME.toLowerCase().includes(__KEYWORD.toLowerCase()));
  360. }
  361. });
  362. }
  363. // 날짜 범위 검색 적용 (배송완료일 기준)
  364. if (searchStartDate.value || searchEndDate.value) {
  365. filteredData = filteredData.filter(item => {
  366. if (!item.DELIVERED_DATE) return false;
  367. const deliveredDate = dayjs(item.DELIVERED_DATE).format('YYYY-MM-DD');
  368. // 시작날짜와 끝날짜가 모두 있는 경우
  369. if (searchStartDate.value && searchEndDate.value) {
  370. const startDate = dayjs(searchStartDate.value).format('YYYY-MM-DD');
  371. const endDate = dayjs(searchEndDate.value).format('YYYY-MM-DD');
  372. return deliveredDate >= startDate && deliveredDate <= endDate;
  373. }
  374. // 끝날짜만 있는 경우
  375. else if (searchEndDate.value) {
  376. const endDate = dayjs(searchEndDate.value).format('YYYY-MM-DD');
  377. return deliveredDate <= endDate;
  378. }
  379. return true;
  380. });
  381. }
  382. if (gridApi.value) {
  383. gridApi.value.setGridOption('rowData', filteredData);
  384. }
  385. };
  386. const markAsSettled = async () => {
  387. if (selectedItems.value.length === 0) {
  388. $toast.error('정산완료 처리할 항목을 선택해주세요.');
  389. return;
  390. }
  391. const orderIds = selectedItems.value.map(item => item.SEQ);
  392. await useAxios()
  393. .post("/deli/markSettled", { order_ids: orderIds })
  394. .then((res) => {
  395. $toast.success('정산완료 처리되었습니다.');
  396. getDeliveredList(); // 리스트 새로고침
  397. selectedItems.value = []; // 선택 초기화
  398. })
  399. .catch((error) => {
  400. $toast.error('정산완료 처리 중 오류가 발생했습니다.');
  401. });
  402. };
  403. watch(itemType, () => {
  404. getDeliveredList();
  405. });
  406. onMounted(() => {
  407. getDeliveredList();
  408. // 날짜 초기화
  409. const today = dayjs();
  410. searchEndDate.value = today.format('YYYY-MM-DD');
  411. });
  412. </script>
  413. <style scoped>
  414. .total-count {
  415. font-size: 14px;
  416. color: #666;
  417. margin-left: 10px;
  418. }
  419. .btn--actions--wrap .right--sections {
  420. display: flex;
  421. align-items: center;
  422. }
  423. </style>