勤務シフト表

Wijmoのコントロールを使用して、勤務シフト表を作成します。このサンプルでは、次のシナリオを確認することができます。

  • Calendarコントロールで勤務日の範囲を選択します
    • 選択できる日数は7日から14日間です
  • TransposedGridでシフトのデータと出勤区分別人数を表示します
    • セルはDataMapを活用します
    • 土、日セルは色付けします
  • 勤務日数を別FlexGridで管理します
  • CollectionViewでシフトの人数に応じたメッセージを出力します
import 'bootstrap.css'; import '@mescius/wijmo.styles/wijmo.css'; import './styles.css'; import { FlexGrid, Row, HeadersVisibility, SelectionMode, AllowSorting } from '@mescius/wijmo.grid'; import { TransposedGrid } from '@mescius/wijmo.grid.transposed'; import { CollectionView } from '@mescius/wijmo'; import { ComboBox, InputTime, Calendar, DateSelectionMode } from '@mescius/wijmo.input'; import { getMonthData, getDataColumns, emptyData, getShiftData } from './data'; document.readyState === 'complete' ? init() : window.onload = init; function init() { let theCalendar = new Calendar('#theCalendar', { selectionMode: DateSelectionMode.Range, rangeMin: 7, rangeMax: 14, rangeChanged: () => { tg.itemsSource = getItems(); tg.refreshCells(true, false); } }); let gridView = new CollectionView(emptyData(), { getError: (item, prop, parsing) => { if (prop == 'count' && item.count > 11) { return '出勤日が多すぎます。'; } else if (prop == 'count' && item.count < 5) { return 'シフトを追加できそうです。'; } return null; } }); let tg = new TransposedGrid('#theTransposedGrid', { selectionMode: 'CellRange', rows: getDataColumns(), itemsSource: getItems(), alternatingRowStep: 0, loadedRows: (s) => { s.columns.defaultSize = 70; }, formatItem: function (s, e) { if (e.panel == s.cells) { let binding = s.rows[e.row].binding || s.columns[e.col].binding; switch (binding) { case 'countA': countData(e.row, e.col, 'A'); break; case 'countB': countData(e.row, e.col, 'B'); break; case 'countC': countData(e.row, e.col, 'C'); break; } if (e.row > 9) return; if (s.getCellData(e.row, e.col) === '日' || s.getCellData(e.row + 1, e.col) === '日') { e.cell.innerHTML = '<div class="sunday">' + e.cell.innerHTML + '</div>'; } if (s.getCellData(e.row, e.col) === '土' || s.getCellData(e.row + 1, e.col) === '土') { e.cell.innerHTML = '<div class="saturday">' + e.cell.innerHTML + '</div>'; } else { e.cell.classList.remove('sunday'); e.cell.classList.remove('saturday'); } } }, updatedView: (s, e) => setGridData(), selectionChanging: (s, e) => { if (e.row == 0 || e.row == 1) e.cancel = true; } }); let fg = new FlexGrid('#grid', { autoGenerateColumns: false, allowMerging: 'ColumnHeaders', columns: [ { binding: 'count', header: '出勤日数', align: 'center', width: '*' } ], itemsSource: gridView, isReadOnly: true, allowSorting: AllowSorting.None, headersVisibility: HeadersVisibility.Column, selectionMode: SelectionMode.None }); let extraRow = new Row(); extraRow.allowMerging = true; let panel = fg.columnHeaders; panel.rows.splice(0, 0, extraRow); let col = fg.getColumn('count'); col.allowMerging = true; panel.setCellData(0, col.index, col.header); let sf = new FlexGrid('#grid2', { autoGenerateColumns: false, columns: [ { header: '勤務区分', binding: 'type', editor: new ComboBox(document.createElement('div'), { itemsSource: ['A', 'B', 'C'], }), isReadOnly: true, width: '*' }, { header: '開始時刻', binding: 'startTime', format: 't', width: '*', editor: new InputTime(document.createElement('div'), { format: 't', step: 30, isEditable: true, }) }, { header: '終了時刻', binding: 'endTime', format: 't', width: '*', editor: new InputTime(document.createElement('div'), { format: 't', step: 30, isEditable: true, }) }, ], itemsSource: getShiftData(), allowDragging: 0, headersVisibility: HeadersVisibility.Column, }); function countData(row, col, type) { let counter = 0; for (let i = 0; i < 9; i++) { if (tg.getCellData(i, col, false) === type) counter++; } if (tg.getCellData(row, col, false) != counter) { tg.setCellData(row, col, counter); } } function setGridData() { let counter = 0; let gridRow = 0; for (let row = 2; row < 9; row++) { for (let i = 0; i < tg.columns.length; i++) { if (tg.getCellData(row, i, false) !== '休') counter++; } fg.setCellData(gridRow, 0, counter); counter = 0; gridRow++; } } function getItems() { let view = new CollectionView(getMonthData(theCalendar.value, theCalendar.rangeEnd), { getError: (item, prop, parsing) => { if (prop == 'countA' && item.countA == 0 || prop == 'countB' && item.countB == 0 || prop == 'countC' && item.countC == 0) { return `${item.day} シフトが誰もいません`; } else if (prop == 'countB' && item.countB < 2 && item.dayw == '土' || prop == 'countB' && item.countB < 2 && item.dayw == '日') { return `${item.day} ${item.dayw}曜日のこの時間帯は2人以上のシフトが必要です`; } return null; } }); return view; } }
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <title>MESCIUS Wijmo Shift application</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"> <div class="setting"> <input id="theCalendar" class="calendar"> <div id="grid2"></div> </div> <br> <div class="shift"> <div id="theTransposedGrid" class="product-grid"></div> <div id="grid" class="workday-grid"></div> </div> </div> </body> </html>
let items = ['A', 'B', 'C', '休']; //let datamapItem = new DataMap(items); let dayOfweek = ['日', '月', '火', '水', '木', '金', '土']; export function getMonthData(firstDate, lastDate) { let data = []; let days = selectedDatOfWeek(firstDate.getDay()); //同月内の範囲 if (firstDate.getMonth() === lastDate.getMonth()) { data = setData(firstDate.getDate(), lastDate.getDate(), firstDate.getMonth() + 1, data); //月を跨ぐ場合 } else { let firstlastDate = new Date(firstDate.getFullYear(), firstDate.getMonth() + 1, 0); //最初の月 data = setData(firstDate.getDate(), firstlastDate.getDate(), firstDate.getMonth() + 1, data); //次の月 data = setData(1, lastDate.getDate(), lastDate.getMonth() + 1, data); } for (let i = 0; i < data.length; i++) { data[i].dayw = days[i % days.length]; } return data; } export function getShiftData() { return [ { type: 'A', startTime: '8:00', endTime: '11:00', }, { type: 'B', startTime: '11:00', endTime: '17:30' }, { type: 'C', startTime: '17:30', endTime: '20:00' }, ]; } export function getDataColumns() { return [ // { binding: 'year', header: '2019', align: 'center' }, { binding: 'day', header: '月/日', align: 'center', isReadOnly: true, cssClassAll: 'cell-day', width: 100 }, { binding: 'dayw', header: '曜日', align: 'center', isReadOnly: true, cssClassAll: 'cell-day' }, { binding: 'p1', header: '山岡', align: 'center', dataMap: items, isRequired: false }, { binding: 'p2', header: '野口', align: 'center', dataMap: items, isRequired: false }, { binding: 'p3', header: '田中', align: 'center', dataMap: items, isRequired: false }, { binding: 'p4', header: '太田', align: 'center', dataMap: items, isRequired: false }, { binding: 'p5', header: '長野', align: 'center', dataMap: items, isRequired: false }, { binding: 'p6', header: '加藤', align: 'center', dataMap: items, isRequired: false }, { binding: 'p7', header: '山田', align: 'center', dataMap: items, isRequired: false }, { binding: 'countA', header: 'A出勤の人数', align: 'center', isReadOnly: true, cssClassAll: 'cell-typeA' }, { binding: 'countB', header: 'B出勤の人数', align: 'center', isReadOnly: true, cssClassAll: 'cell-typeB' }, { binding: 'countC', header: 'C出勤の人数', align: 'center', isReadOnly: true, cssClassAll: 'cell-typeC' }, ]; } export function emptyData() { var data = []; for (let i = 0; i < 7; i++) { data.push({ count: 0 }); } return data; } function selectedDatOfWeek(index) { let selectedDatOfWeek = []; //indexより前 for (let i = index; i < dayOfweek.length; i++) { selectedDatOfWeek.push(dayOfweek[i]); } //indexより後 for (let i = 0; i < index; i++) { selectedDatOfWeek.push(dayOfweek[i]); } return selectedDatOfWeek; } function setData(start, end, month, data) { for (let i = start; i <= end; i++) { data.push({ day: `${month}/${i}`, dayw: '', p1: items[Math.floor(Math.random() * items.length)], p2: items[Math.floor(Math.random() * items.length)], p3: items[Math.floor(Math.random() * items.length)], p4: items[Math.floor(Math.random() * items.length)], p5: items[Math.floor(Math.random() * items.length)], p6: items[Math.floor(Math.random() * items.length)], p7: items[Math.floor(Math.random() * items.length)], countA: 0, countB: 0, countC: 0 }); } return data; }
.product-grid { width: 84%; } .workday-grid { width: 16%; height: 255px; } #grid2{ height: 115px; } .cell-day:not(.wj-state-selected):not(.wj-state-multi-selected) { background-color: #eee ; } .shift { display: flex; } .wj-listbox-item { height: 27px; } .wj-cell.wj-align-center.cell-day:has(.sunday) { background-color:rgba(255,229,229,0.49) ; } .wj-cell.wj-align-center.cell-day:has(.saturday) { background-color: #e5f4ff ; } .wj-cell:has(.holiday) { background-color: #f2f2f2 ; } .wj-calendar { width: 60em; } .setting{ display: flex; } .wj-header.cell-day{ font-weight: normal; }
(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);