From 226853ac37340181a59d3eac2fbe9069dd6537a5 Mon Sep 17 00:00:00 2001 From: xue jiahao Date: Mon, 17 Feb 2025 15:00:16 +0800 Subject: [PATCH] =?UTF-8?q?=E5=87=BA=E6=AC=A0=E9=9B=86=E8=A8=88=E8=A1=A8?= =?UTF-8?q?=E5=87=BA=E5=8A=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/env.js | 3 + src/utils.js | 14 ++ src/園児別出欠簿入力/ExtractHandler.js | 319 ++++++++++++++++++++++++- 3 files changed, 333 insertions(+), 3 deletions(-) diff --git a/src/env.js b/src/env.js index a164e30..e6876de 100644 --- a/src/env.js +++ b/src/env.js @@ -24,6 +24,9 @@ const env = { "園児台帳": { appId: 16, }, + "保育・教育日数マスタ": { + appId: 41, + }, "Excelテンプレート": { appId: 46 } diff --git a/src/utils.js b/src/utils.js index 5b2d877..39dfc50 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,5 +1,7 @@ const Kuc = Kucs['1.19.0']; +const WEEK = ['日', '月', '火', '水', '木', '金', '土'] + const monthItems = Array.from({ length: 12 }, (_, i) => { const month = "" + (i + 1); return { label: month, value: month.padStart(2, '0') }; @@ -170,6 +172,7 @@ function checkInputData(map, { btnLabel, yearElId, monthElId, dateElId, classElI } function getLastDate(year, month) { + // month は 1-12 の数値で入力する return new Date(year, month, 0); } @@ -486,3 +489,14 @@ function fillApproveArea(baseCells, worksheet, record) { worksheet.getCell(signRow, baseCells['園長'][0].col + i).value = (record[signLabels[i]]?.value || ''); } } + +function groupingBySex(list) { + return list.reduce(([male, female], record) => { + if (record['性別'].value === '男') { + return [male.concat(record), female]; + } else if (record['性別'].value === '女') { + return [male, female.concat(record)]; + } + return [male, female]; + }, [[], []]); +} \ No newline at end of file diff --git a/src/園児別出欠簿入力/ExtractHandler.js b/src/園児別出欠簿入力/ExtractHandler.js index a83076f..ff2b61c 100644 --- a/src/園児別出欠簿入力/ExtractHandler.js +++ b/src/園児別出欠簿入力/ExtractHandler.js @@ -1,4 +1,5 @@ class ExtractHandler { + constructor(headerSpace) { const elements = createBtnGroupArea('extract-action-area', '出欠集計表出力', this.handleExtractData, { btnElId: 'extract-btn', @@ -12,9 +13,321 @@ class ExtractHandler { headerSpace.appendChild(elements['extract-action-area']); } - handleExtractData(e, { year, month, className }) { - const fileName = getExcelName(env["園児別出欠簿入力"], year + month + '_' + className + '組'); - console.log(fileName); + handleExtractData = async (e, { year, month, className }) => { + loading(true, '帳票出力中...'); + showError(false); + const api = new KintoneRestAPIClient(); + // 本アプリからデータを読み取る + const records = await this.getRecords(api, year, month, className); + if (!records) { + // エラー + loading(false); + return e; + } + const recordMap = this.buildRecordMap(records); + + const childMaster = await this.getChildMstRecords(api, year, month, className, Object.keys(recordMap.byId)); + if (!childMaster) { + // エラー + loading(false); + return e; + } + + const dayMaster = await this.getDayMstRecords(api, year, month); + if (!dayMaster) { + // エラー + loading(false); + return e; + } + + const excelName = env["園児別出欠簿入力"].excelName; + await createExcelAndDownload({ + api, + excelName, + exportName: getExcelName(excelName, year + month + '_' + className + '組'), + bizLogic: this.writeExcel({ records, recordMap, childMaster, dayMaster }, className, getJapaneseEraDate(new Date(year, month - 1, 1))), + }); + loading(false); + } + + getRecords = async (api, year, month, className) => { + const firstDate = getFormatDateString(year, month, 1) + const lastDate = getFormatDateString(getLastDate(year, month)); + try { + return await api.record.getAllRecordsWithId({ + app: env["園児別出欠簿入力"].appId, + condition: `登園日 >= "${firstDate}" and 登園日 <= "${lastDate}" and クラス in ("${className}")` + }); + } catch (e) { + showError(true, '本アプリのデータ読み取りエラー\n - ' + e); + } + } + + getChildMstRecords = async (api, year, month, className, uniqueKeys) => { + const date = getJapaneseEraDate(new Date(year, month - 1, 1)) + const prevMonth = getJapaneseEraDate(getLastDate(year, month - 1)); + const result = {}; + try { + result['入園'] = await api.record.getAllRecordsWithId({ + app: env["園児台帳"].appId, + fields: ['性別'], + condition: `和暦_入園年月日 in ("${date.era}") and 年_入園年月日 = "${date.year}" and 月_入園年月日 = "${date.month}" and クラス in ("${className}")` + }); + result['退園'] = await api.record.getAllRecordsWithId({ + app: env["園児台帳"].appId, + fields: ['性別'], + condition: `和暦_退園年月日 in ("${prevMonth.era}") and 年_退園年月日 = "${prevMonth.year}" and 月_退園年月日 = "${prevMonth.month}" and クラス in ("${className}")` + }); + if (uniqueKeys?.length) { + result['在籍'] = await api.record.getAllRecordsWithId({ + app: env["園児台帳"].appId, + fields: ['ユニークキー', '性別'], + condition: `ユニークキー in ("${uniqueKeys.join('", "')}")` + }); + } + return result; + } catch (e) { + showError(true, '園児台帳のデータ読み取りエラー\n - ' + e); + } + } + + getDayMstRecords = async (api, yearStr, monthStr) => { + let year = Number(yearStr); + const month = Number(monthStr); + // ※年度=4月~翌年3月までの区切りとする(例:2025年3月→2024年度) + if (month < 4) { + year--; + } + try { + const data = await api.record.getAllRecordsWithId({ + app: env["保育・教育日数マスタ"].appId, + fields: ['教育日数' + month, '保育日数' + month], + condition: `年度 = "${year}"` + }); + return data && data[0]; + } catch (e) { + showError(true, '保育・教育日数マスタのデータ読み取りエラー\n - ' + e); + } + } + + writeExcel = ({ records, recordMap, childMaster, dayMaster }, className, { era, year, westernYear, month }) => { + const teachDays = Number(dayMaster['教育日数' + month].value); + const careDays = Number(dayMaster['保育日数' + month].value); + + return async (api, worksheet) => { + const baseCells = findCellsInfo(worksheet, ['13', '16', '19', '25', '(担 任)', '1', '番号']); + + // header + updateCell(worksheet, { base: baseCells['13'][0], up: 1 }, era); + updateCell(worksheet, { base: baseCells['16'][0], up: 1 }, year + '年'); + updateCell(worksheet, { base: baseCells['19'][0], up: 1 }, month + '月'); + updateCell(worksheet, { base: baseCells['25'][0], up: 1 }, className + '組'); + + const weekRow = worksheet.getRow(baseCells['1'][0].row + 1); + const startCol = baseCells['1'][0].col; + const weekStart = new Date(westernYear, month - 1, 1).getDay(); + const lastDate = getLastDate(westernYear, month).getDate(); + for (let i = 0; i < lastDate; i++) { + updateCell(weekRow, { base: { col: startCol + i } }, WEEK[(weekStart + i) % 7]); + } + + if (!records.length) { + return; + } + updateCell(worksheet, { base: baseCells['(担 任)'][0], down: 1 }, records[0]['担任']?.value || ''); // TODO force use records[0]? + + fillMainPage(baseCells, worksheet, recordMap); + fillFooter(worksheet, recordMap); + } + + function fillMainPage(baseCells, worksheet, recordMap) { + baseCells['番号'] = baseCells['番号'].filter((_, index) => index % 2 !== 0); + const sortedRecords = Object.values(recordMap.byId).sort((a, b) => a['idKey'].localeCompare(b['idKey'])); + + const lastPage = 2; + const baseForTemplate = baseCells['番号'][lastPage - 1]; // 番号 merged 2 rows + const pageSize = 15; + const totalPages = Math.ceil(sortedRecords.length / pageSize); + + // make new copy + if (totalPages > 2) { + const copyPageRowStart = baseForTemplate.row - 2; + const copyPageRowEnd = baseForTemplate.row + (pageSize); + + createCopyFromTemplate(worksheet, { + startPage: lastPage + 1, + totalPages, + copyPageRowStart, + copyPageRowEnd, + callback: (newPage, rowCount) => { + ['番号'].forEach((label) => { + const last = baseCells[label][newPage - 2]; + baseCells[label].push({ + col: last.col, + row: last.row + rowCount + }); + }); + } + }); + } + + for (let i = 0; i < totalPages; i++) { + const childLabelCell = baseCells['番号'][i]; + let currentRow = childLabelCell.row + 1; + + for (let j = 0; j < pageSize; j++) { + const index = i * pageSize + j; + const recordWrapper = sortedRecords[index]; + if (!recordWrapper) { + break; + } + const row = worksheet.getRow(currentRow); + const base = { row: currentRow, col: 1 }; + // 番号 + updateCell(row, { base }, recordWrapper['id']); + // 児 童 氏 名 + updateCell(row, { base, right: 1 }, recordWrapper['name']); + + const reasons = []; + const sum = { + '出席': 0, + '出席停止': 0, + '病欠': 0, + '自欠': 0, + } + // 日 + recordWrapper.list.forEach((record, i) => { + const res = record["出欠"].value; + sum[res]++; + if (res === '出席') { + return; + } + if (res === '出席停止' && record["出席停止理由"].value) { + reasons.push(record["出席停止理由"].value); + } + updateCell(row, { base, right: 2 + i }, res === '出席停止' ? '×' : '/'); + }) + + // 出 席 + updateCell(row, { base, right: 33 }, sum['出席']); + updateCell(row, { base, right: 34 }, sum['出席停止']); + // 欠 席 + updateCell(row, { base, right: 35 }, sum['病欠']); + updateCell(row, { base, right: 36 }, sum['自欠']); + // 教育日数 + // updateCell(row, { base, right: 37 }, sum['出席'] + sum['出席停止'] - sum['病欠'] - sum['自欠']); + updateCell(row, { base, right: 37 }, teachDays - sum['病欠'] - sum['自欠']); + + // 備考 + updateCell(row, { base, right: 38 }, reasons.join("\n")); + + currentRow += 1; + } + + worksheet.getRow(childLabelCell.row + pageSize + 1).addPageBreak(); + } + } + + + function fillFooter(worksheet, recordMap) { + const baseCells = findCellsInfo(worksheet, ['合 計', '保 育 日 数', '男', '女', '欠 席 総 数', '出 席 率', '%']); + // 合 計 + const totalAreaRow = worksheet.getRow(baseCells['合 計'][0].row); + const totalAreaRow2 = worksheet.getRow(baseCells['合 計'][1].row); + const totalAreaRow3 = worksheet.getRow(baseCells['合 計'][2].row); + const base = { col: 3 }; + const dateList = recordMap.sum.list; + for (let i = 0; i < dateList.length; i++) { + if (dateList[i]) { + updateCell(totalAreaRow, { base, right: i }, dateList[i]['出席']); + updateCell(totalAreaRow2, { base, right: i }, dateList[i]['出停']); + updateCell(totalAreaRow3, { base, right: i }, dateList[i]['欠席']); + } + } + + // 保 育 日 数 + updateCell(worksheet, { base: baseCells['保 育 日 数'][0], down: 1 }, careDays); + + // 入 園 数 + let list = childMaster['入園']; + let [male, female] = groupingBySex(list); + updateCell(worksheet, { base: baseCells['男'][0], right: 1 }, male.length); + updateCell(worksheet, { base: baseCells['女'][0], right: 1 }, female.length); + // 退 園 数 + list = childMaster['退園']; + [male, female] = groupingBySex(list); + updateCell(worksheet, { base: baseCells['男'][1], right: 1 }, male.length); + updateCell(worksheet, { base: baseCells['女'][1], right: 1 }, female.length); + // 在 籍 数 + list = childMaster['在籍']; + [male, female] = groupingBySex(list); + updateCell(worksheet, { base: baseCells['男'][2], right: 1 }, male.length); + updateCell(worksheet, { base: baseCells['女'][2], right: 1 }, female.length); + + const total = recordMap.sum['欠席'] + recordMap.sum['出席']; + if (total) { + // 出 席 総 数 + updateCell(worksheet, { base: baseCells['欠 席 総 数'][0], left: 1 }, recordMap.sum['出席']); + // 欠 席 総 数 + updateCell(worksheet, { base: baseCells['出 席 率'][0], left: 1 }, recordMap.sum['欠席']); + // 出 席 率 + updateCell(worksheet, { base: baseCells['%'][0], right: 1 }, Math.round(recordMap.sum['出席'] / total * 1000) / 10 + '%'); + } + + } + + } + + buildRecordMap = (records) => { + const recordMap = { + sum: { + '出席': 0, + '欠席': 0, + list: [] + }, + byId: {} + } + + records.forEach((record) => { + const dateIndex = Number(record['登園日'].value.split('-')[2]) - 1; + const idKey = record['園児ユニークキー'].value; + let attendance = recordMap.byId[idKey]; + if (!attendance) { + attendance = { + idKey, + id: record['出席番号'].value, + name: record['園児名'].value, + list: [], + }; + recordMap.byId[idKey] = attendance + } + attendance.list[dateIndex] = record; + + const status = record["出欠"].value; + if (status === '出席' || status === '出席停止') { + recordMap.sum['出席']++; + } else if (status === '病欠' || status === '自欠') { + recordMap.sum['欠席']++; + } + + let dateSum = recordMap.sum.list[dateIndex]; + if (!dateSum) { + dateSum = { + '出席': 0, + '出停': 0, + '欠席': 0, + }; + recordMap.sum.list[dateIndex] = dateSum + } + if (status === '出席') { + dateSum['出席']++; + } else if (status === '出席停止') { + dateSum['出停']++; + } else if (status === '病欠' || status === '自欠') { + dateSum['欠席']++; + } + }); + return recordMap; } static getInstance(headerSpace) {