就業月報

Wijmoのコントロールを使用して、月々の勤怠を管理する就業月報を作成します。このサンプルでは、次のシナリオを確認することができます

  • FlexGridで日々の勤務データを表示しています
    • 休暇事由の列では、cellTemplateを利用して公休および有給データに応じたバッジを設定しています
    • 終業時刻の列では、CollectionViewを利用して申告漏れに対して警告を表示しています
    • 申請列では、formatItemを利用してセルのインライン編集を有効にしています
  • InputDateを利用して、処理対象月を選択できるようにしています
    • 選択可能な月は2023年1月から2023年4月です
    • 1月から3月のデータは編集することができません
  • FlexGridXlsxConverterを利用して、グリッドデータをExcel出力します
import 'bootstrap.css'; import './styles.css'; import '@mescius/wijmo.styles/wijmo.css'; import { FlexGrid, AllowDragging, AllowSorting, SelectionMode, CellType, GroupRow } from '@mescius/wijmo.grid'; import { getData, getMonthlyWorkPlan, getMonthlyWorkAct, getMonthlyOverTime } from './data'; import { InputDate, DateSelectionMode } from '@mescius/wijmo.input'; import { changeType, getType, CollectionView, toggleClass, Tooltip, glbz } from '@mescius/wijmo'; import * as wjGridXlsx from '@mescius/wijmo.grid.xlsx'; import '@mescius/wijmo.cultures/wijmo.culture.ja'; document.readyState === 'complete' ? init() : window.onload = init; function init() { let waitingMessage = '承認待ち'; let waitingValue = '----'; let waitingChar = '△'; let tipMessage = '現在、上長の承認待ちです。<br> 承認完了後、労働時間と残業時間は確定されます。'; const setCollectionVew = (data) => { let item = new CollectionView(data, { getError: (item, prop) => { if (prop == 'workEndAct' && item.workEndAct == '' && item.workStart != '') { return '申告漏れがあります。'; } // no errors return null; } }); return item; }; let currentData = getData(3); const flexGrid = new FlexGrid('#flexGrid', { itemsSource: setCollectionVew(currentData), headersVisibility: 'Column', autoGenerateColumns: false, isReadOnly: true, allowDragging: AllowDragging.None, allowSorting: AllowSorting.None, columns: [ { binding: 'date', header: '日付', width: 80, isReadOnly: true, align: 'center', }, { binding: 'leaveReason', header: '休暇事由', cellTemplate: '<span class="badge bg-secondary bg-${text}">${text}</span>', isReadOnly: true, align: 'center', }, { binding: 'remarks', header: '備考', width: 100, align: 'center' }, { binding: 'workStart', header: '始業予定', width: 110, align: 'center' }, { binding: 'workEnd', header: '終業予定', width: 110, align: 'center' }, { binding: 'workStartAct', header: '始業時刻', width: 110, align: 'center' }, { binding: 'workEndAct', header: '終業時刻', width: 108, align: 'center' }, { binding: 'actualWork', header: '労働時間', width: 110, align: 'center' }, { binding: 'overtime', header: '残業時間', width: 110, align: 'center' }, { binding: 'approval', header: '承認', width: 50, align: 'center' }, { binding: 'apply', header: '申請', width: 180, align: 'center' } ], frozenColumns: 1, selectionMode: SelectionMode.None, formatItem: function (s, e) { if (e.panel == s.cells) { if (e.col !== 0) return; if (s.getCellData(e.row, e.col).indexOf('日') > 0 || s.getCellData(e.row, e.col + 2) !== '' && s.getCellData(e.row, e.col + 1) !== '有給休暇' && s.getCellData(e.row, e.col + 1) !== '午後休') { e.cell.innerHTML = '<div class="sunday">' + e.cell.innerHTML + '</div>'; } if (s.getCellData(e.row, e.col).indexOf('土') > 0) { e.cell.innerHTML = '<div class="saturday">' + e.cell.innerHTML + '</div>'; } } }, }); const setFooterData = (currentData) => { flexGrid.columnFooters.setCellData(0, 4, '計 ' + getMonthlyWorkPlan(currentData)); flexGrid.columnFooters.setCellData(0, 7, '計 ' + getMonthlyWorkAct(currentData)); flexGrid.columnFooters.setCellData(0, 8, '計 ' + getMonthlyOverTime(currentData)); }; flexGrid.columnFooters.rows.push(new GroupRow()); flexGrid.columnFooters.rows.defaultSize = 43; setFooterData(currentData); const inputDateRange = new InputDate('#theInputDateRange', { alwaysShowCalendar: true, //predefinedRanges: getPredefinedRanges(), value: new Date(2023, 3, 1), //rangeEnd:new Date(2023, 3, 31), selectionMode: DateSelectionMode.Month, min: new Date(2023, 0, 1), max: new Date(2023, 3, 1), format: 'yyyy年M月度', valueChanged: (s, e) => { currentData = getData(s.value.getMonth()); flexGrid.itemsSource = setCollectionVew(currentData); setFooterData(currentData); } }); let mod = false; flexGrid.formatItem.addHandler(function (s, e) { if (e.panel == s.cells) { let col = s.columns[e.col], item = s.rows[e.row].dataItem; if (item == currentEditItem && currentEditItem.workEndAct == '' || item == currentEditItem && mod) { switch (col.binding) { case 'apply': e.cell.innerHTML = document.getElementById('tplBtnEditMode').innerHTML; e.cell['dataItem'] = item; break; case 'workEndAct': e.cell.innerHTML = '<input class="form-control" type="time" value="18:00" min="18:00" max="24:00"' + 'id="' + col.binding + '" ' + 'value="' + s.getCellData(e.row, e.col, true) + '"/>'; break; } } else { switch (col.binding) { case 'apply': if (s.getCellData(e.row, e.col - 4, true) == '' && s.getCellData(e.row, e.col - 7, true) != '') { e.cell.innerHTML = document.getElementById('tplBtnViewMode').innerHTML; e.cell['dataItem'] = item; } else if (s.getCellData(e.row, e.col, true) == waitingMessage) { toggleClass(e.cell, 'approval-wait', true); e.cell.innerHTML = document.getElementById('tplBtnModifyMode').innerHTML; } break; case 'workEndAct': if (s.getCellData(e.row, e.col + 4, true) == waitingMessage) { toggleClass(e.cell, 'approval-wait', true); } break; case 'actualWork': if (s.getCellData(e.row, e.col, true) == waitingValue) { toggleClass(e.cell, 'approval-wait', true); } break; case 'overtime': if (s.getCellData(e.row, e.col, true) == waitingValue) { toggleClass(e.cell, 'approval-wait', true); } break; case 'approval': if (s.getCellData(e.row, e.col, true) == waitingChar) { toggleClass(e.cell, 'approval-wait', true); } break; } } } }); // // handle button clicks flexGrid.addEventListener(flexGrid.hostElement, 'click', function (e) { let targetBtn; if (e.target instanceof HTMLButtonElement) { targetBtn = e.target; } else if (e.target instanceof HTMLSpanElement && e.target.classList.contains('glyphicon')) { targetBtn = e.target.parentElement; } if (targetBtn) { let ht = flexGrid.hitTest(e); let item = flexGrid.rows[ht.row].dataItem; switch (targetBtn.id) { case 'btnEdit': editItem(item); break; case 'btnModify': mod = true; editItem(item); break; case 'btnOK': commitEdit(); break; case 'btnCancel': cancelEdit(); break; } } }); document.getElementById('saveXlsx').addEventListener('click', () => { wjGridXlsx.FlexGridXlsxConverter.saveAsync(flexGrid, {}, glbz `FlexGird_${new Date()}:d${new Date()}:T.xlsx`); }); let tip = new Tooltip(); let rng = null; flexGrid.hostElement.addEventListener('mousemove', function (e) { let ht = flexGrid.hitTest(e.pageX, e.pageY); if (!ht.range.equals(rng)) { if (ht.cellType == CellType.Cell && flexGrid.getCellData(ht.row, 10, true) === waitingMessage && ht.col > 5) { rng = ht.range; let cellElement = document.elementFromPoint(e.clientX, e.clientY), cellBounds = flexGrid.getCellBoundingRect(ht.row, ht.col); if (cellElement.className.indexOf('wj-cell') > -1) { tip.show(flexGrid.hostElement, tipMessage, cellBounds); } } } }); flexGrid.hostElement.addEventListener('mouseout', function (e) { tip.hide(); rng = null; }); // flexGrid.rows.defaultSize = 40; // exit edit mode when scrolling the grid or losing focus flexGrid.scrollPositionChanged.addHandler(cancelEdit); flexGrid.lostFocus.addHandler(cancelEdit); // // editing commands let currentEditItem = null; // function editItem(item) { cancelEdit(); currentEditItem = item; flexGrid.invalidate(); } // function commitEdit() { if (currentEditItem) { flexGrid.columns.forEach(function (col) { let input = flexGrid.hostElement.querySelector('#' + col.binding); if (input) { let value = changeType(input.value, col.dataType, col.format); if (getType(value) == col.dataType) { currentEditItem[col.binding] = value; currentEditItem['approval'] = waitingChar; currentEditItem['apply'] = waitingMessage; currentEditItem['actualWork'] = waitingValue; currentEditItem['overtime'] = waitingValue; } } }); } currentEditItem = null; flexGrid.invalidate(); } // function cancelEdit() { if (currentEditItem) { currentEditItem = null; flexGrid.invalidate(); } } }
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <title>MESCIUS Wijmo Monthly Report</title> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <!-- SystemJS --> <script src="https://cdnjs.cloudflare.com/ajax/libs/systemjs/0.21.5/system.src.js" integrity="sha512-skZbMyvYdNoZfLmiGn5ii6KmklM82rYX2uWctBhzaXPxJgiv4XBwJnFGr5k8s+6tE1pcR1nuTKghozJHyzMcoA==" crossorigin="anonymous" referrerpolicy="no-referrer"></script> <script src="systemjs.config.js"></script> <script> System.import('./src/app'); </script> </head> <body> <div class="container-fluid"> <label for="theInputDateRange">処理期間: </label> <input id="theInputDateRange"> <button id="saveXlsx" class="btn btn-default">Excelエクスポート</button> <div id="flexGrid"></div> <!-- template for buttons on items in view mode --> <div id="tplBtnViewMode" style="display:none"> <button id="btnEdit" class="btn btn-default btn-sm"> <span class="glyphicon glyphicon-pencil"></span> 申請漏れを入力する </button> </div> <div id="tplBtnModifyMode" style="display:none"> <button id="btnModify" class="btn btn-default btn-sm"> <span class="glyphicon glyphicon-pencil"></span> 申請時刻を変更する </button> </div> <!-- template for buttons on items in edit mode --> <div id="tplBtnEditMode" style="display:none"> <button id="btnOK" class="btn btn-primary btn-sm"> <span class="glyphicon glyphicon-ok"></span> 申請 </button> <button id="btnCancel" class="btn btn-warning btn-sm"> <span class="glyphicon glyphicon-ban-circle"></span> キャンセル </button> </div> </div> </body> </html>
const dayOfweek = ['日', '月', '火', '水', '木', '金', '土']; export function getData(month) { const data = []; const nextMonth = new Date(2023, month + 1, 0); for (let i = 1; i <= nextMonth.getDate(); i++) { let date = new Date(2023, month, i); let isHoliday = date.getDay() === 0 || date.getDay() === 6; let holiday = '公休'; let workStartAct = '8:' + (30 + Math.floor(Math.random() * 29)); let workEndAct = 18 + Math.floor(Math.random() * 3) + ':' + (10 + Math.floor(Math.random() * 49)); let actualWork = getSubTime(workEndAct); let overTime = getOverTime(actualWork); let remarks = ''; let approval = '◎'; let paid = false; let halfPaid = false; //休日設定 if (i == 2 && month == 0 || i == 9 && month == 0 || i == 23 && month == 1 || i == 21 && month == 2) { isHoliday = true; holiday = '公休'; remarks = getRemarks(month, i); approval = ''; } //任意の有給設定 if (i == 28 && month == 3 || i == 20 && month == 2 || i == 24 && month == 1) { isHoliday = true; holiday = '有給休暇'; remarks = '私用のため'; approval = '◎'; paid = true; } if (i == 12 && month == 3) { halfPaid = true; holiday = '午後休'; remarks = '体調不良'; approval = '◎'; paid = true; workStartAct = '8:45'; workEndAct = '12:08'; actualWork = '3:00'; overTime = '0:00'; } if (i == 7 && month == 3 || i == 13 && month == 3 || i == 25 && month == 3) { overTime = ''; actualWork = ''; workEndAct = ''; } data.push({ date: date.getMonth() + 1 + '/' + date.getDate() + ' (' + dayOfweek[date.getDay()] + ') ', leaveReason: isHoliday || halfPaid ? holiday : '', remarks: remarks, workStart: isHoliday ? '' : '9:00', workEnd: isHoliday ? '' : '18:00', workStartAct: isHoliday ? '' : workStartAct, workEndAct: isHoliday ? '' : workEndAct, actualWork: isHoliday ? '' : actualWork, overtime: isHoliday ? '' : overTime, approval: isHoliday && !paid ? '' : approval }); } return data; } export const getMonthlyWorkPlan = (data) => { let items = data.filter(item => item.leaveReason !== ''); let workDayCount = data.length - items.length; let workMonthlydata = 8 * workDayCount; let halfPaid = data.filter(item => item.leaveReason == '午後休'); if (halfPaid.length > 0) { workMonthlydata = workMonthlydata + 3; } return workMonthlydata + ':00'; }; export const getMonthlyWorkAct = (data) => { let items = data.filter(item => item.actualWork !== ''); let workMonthlyAct = '0:00'; items.forEach((item) => { workMonthlyAct = getActTime(workMonthlyAct, item.actualWork); }); return workMonthlyAct; }; export const getMonthlyOverTime = (data) => { let items = data.filter(item => item.overtime !== ''); let workMonthlyOverTime = '0:00'; items.forEach((item) => { workMonthlyOverTime = getActTime(workMonthlyOverTime, item.overtime); }); //4月限定処理 let aprilData = data.filter(item => item.date.includes('4/1')); if (aprilData.length > 0) { let data = workMonthlyOverTime.split(':'); let subHour = Number(data[0]) - 8; workMonthlyOverTime = subHour + ':' + data[1]; } return workMonthlyOverTime; }; const getSubTime = (time2) => { let time2a = time2.split(':'); let time2s = time2a[0] * 60 * 60 + time2a[1] * 60; let subTime = time2s - 36000; let hour = Math.floor(subTime / 3600); let min = Math.floor((subTime % 3600) / 60); return hour + ':' + min; }; const getActTime = (time1, time2) => { let time1a = time1.split(':'); let time2a = time2.split(':'); let hour = Number(time1a[0]) + Number(time2a[0]); let min = Number(time1a[1]) + Number(time2a[1]); if (min >= 60) { min = min - 60; hour++; } if (min < 10) { min = '0' + min; } return hour + ':' + min; }; const getOverTime = (time) => { let timea = time.split(':'); let overtimeh = Number(timea[0]) - 8; return overtimeh + ':' + timea[1]; }; const getRemarks = (m, d) => { if (d == 2 && m == 0) { return '振替休日'; } else if (d == 9 && m == 0) { return '成人の日'; } else if (d == 23 && m == 1) { return '天皇誕生日'; } else if (d == 21 && m == 2) { return '春分の日'; } };
.wj-flexgrid { height: 500px; } .bg-公休 { background-color: green; } .bg-有給休暇 { background-color: skyblue; } .bg-午後休 { background-color: rgba(27, 161, 56, 0.466); } .badge { padding: 6px 25px; } .saturday { color:blue; } .sunday { color: red; } .wj-cell.wj-align-center.approval-wait{ background-color:rgba(84, 221, 255, 0.808); } .wj-cell{ font-size: 15px; }
(function (global) { System.config({ transpiler: 'plugin-babel', babelOptions: { es2015: true }, meta: { '*.css': { loader: 'css' } }, paths: { // paths serve as alias 'npm:': 'node_modules/' }, // map tells the System loader where to look for things map: { 'jszip': 'npm:jszip/dist/jszip.js', '@mescius/wijmo': 'npm:@mescius/wijmo/index.js', '@mescius/wijmo.input': 'npm:@mescius/wijmo.input/index.js', '@mescius/wijmo.styles': 'npm:@mescius/wijmo.styles', '@mescius/wijmo.cultures': 'npm:@mescius/wijmo.cultures', '@mescius/wijmo.chart': 'npm:@mescius/wijmo.chart/index.js', '@mescius/wijmo.chart.analytics': 'npm:@mescius/wijmo.chart.analytics/index.js', '@mescius/wijmo.chart.animation': 'npm:@mescius/wijmo.chart.animation/index.js', '@mescius/wijmo.chart.annotation': 'npm:@mescius/wijmo.chart.annotation/index.js', '@mescius/wijmo.chart.finance': 'npm:@mescius/wijmo.chart.finance/index.js', '@mescius/wijmo.chart.finance.analytics': 'npm:@mescius/wijmo.chart.finance.analytics/index.js', '@mescius/wijmo.chart.hierarchical': 'npm:@mescius/wijmo.chart.hierarchical/index.js', '@mescius/wijmo.chart.interaction': 'npm:@mescius/wijmo.chart.interaction/index.js', '@mescius/wijmo.chart.radar': 'npm:@mescius/wijmo.chart.radar/index.js', '@mescius/wijmo.chart.render': 'npm:@mescius/wijmo.chart.render/index.js', '@mescius/wijmo.chart.webgl': 'npm:@mescius/wijmo.chart.webgl/index.js', '@mescius/wijmo.chart.map': 'npm:@mescius/wijmo.chart.map/index.js', '@mescius/wijmo.gauge': 'npm:@mescius/wijmo.gauge/index.js', '@mescius/wijmo.grid': 'npm:@mescius/wijmo.grid/index.js', '@mescius/wijmo.grid.detail': 'npm:@mescius/wijmo.grid.detail/index.js', '@mescius/wijmo.grid.filter': 'npm:@mescius/wijmo.grid.filter/index.js', '@mescius/wijmo.grid.search': 'npm:@mescius/wijmo.grid.search/index.js', '@mescius/wijmo.grid.grouppanel': 'npm:@mescius/wijmo.grid.grouppanel/index.js', '@mescius/wijmo.grid.multirow': 'npm:@mescius/wijmo.grid.multirow/index.js', '@mescius/wijmo.grid.transposed': 'npm:@mescius/wijmo.grid.transposed/index.js', '@mescius/wijmo.grid.transposedmultirow': 'npm:@mescius/wijmo.grid.transposedmultirow/index.js', '@mescius/wijmo.grid.pdf': 'npm:@mescius/wijmo.grid.pdf/index.js', '@mescius/wijmo.grid.sheet': 'npm:@mescius/wijmo.grid.sheet/index.js', '@mescius/wijmo.grid.xlsx': 'npm:@mescius/wijmo.grid.xlsx/index.js', '@mescius/wijmo.grid.selector': 'npm:@mescius/wijmo.grid.selector/index.js', '@mescius/wijmo.grid.cellmaker': 'npm:@mescius/wijmo.grid.cellmaker/index.js', '@mescius/wijmo.nav': 'npm:@mescius/wijmo.nav/index.js', '@mescius/wijmo.odata': 'npm:@mescius/wijmo.odata/index.js', '@mescius/wijmo.olap': 'npm:@mescius/wijmo.olap/index.js', '@mescius/wijmo.rest': 'npm:@mescius/wijmo.rest/index.js', '@mescius/wijmo.pdf': 'npm:@mescius/wijmo.pdf/index.js', '@mescius/wijmo.pdf.security': 'npm:@mescius/wijmo.pdf.security/index.js', '@mescius/wijmo.viewer': 'npm:@mescius/wijmo.viewer/index.js', '@mescius/wijmo.xlsx': 'npm:@mescius/wijmo.xlsx/index.js', '@mescius/wijmo.undo': 'npm:@mescius/wijmo.undo/index.js', '@mescius/wijmo.interop.grid': 'npm:@mescius/wijmo.interop.grid/index.js', '@mescius/wijmo.touch': 'npm:@mescius/wijmo.touch/index.js', '@mescius/wijmo.cloud': 'npm:@mescius/wijmo.cloud/index.js', '@mescius/wijmo.barcode': 'npm:@mescius/wijmo.barcode/index.js', '@mescius/wijmo.barcode.common': 'npm:@mescius/wijmo.barcode.common/index.js', '@mescius/wijmo.barcode.composite': 'npm:@mescius/wijmo.barcode.composite/index.js', '@mescius/wijmo.barcode.specialized': 'npm:@mescius/wijmo.barcode.specialized/index.js', 'jszip': 'npm:jszip/dist/jszip.js', 'bootstrap.css': 'npm:bootstrap/dist/css/bootstrap.min.css', 'css': 'npm:systemjs-plugin-css/css.js', 'plugin-babel': 'npm:systemjs-plugin-babel/plugin-babel.js', 'systemjs-babel-build':'npm:systemjs-plugin-babel/systemjs-babel-browser.js' }, // packages tells the System loader how to load when no filename and/or no extension packages: { src: { defaultExtension: 'js' }, "node_modules": { defaultExtension: 'js' }, } }); })(this);