プロジェクト工数管理

Wijmo のコントロールを使用して、簡易的なプロジェクト工数管理アプリケーションを作成します。

クーポンアプリ開発、在庫管理システム開発、勤怠管理システム開発の3つのプロジェクトを管理するWBSを想定したアプリケーションです。

ここでは、プロジェクト工数管理サンプルを作成する際のポイントを紹介します。 プロジェクト工数管理の詳細 プロジェクト工数管理サンプルでは、次のコントロールを利用しています。 TabPanelを利用してプロジェクトをタブで切り替えができるようにします。 FlexGridXlsxConverter.saveAsyncメソッドを利用して、表示中のグリッドを EXCEL 出力します。 クーポンアプリ開発のタブ FlexGridを利用してクーポンアプリ開発の WBS を表示します。 在庫管理システム開発のタブ FlexGridを利用してMergeManagerを拡張したクラスを作成して 工程/タスク/担当者を2行ごとにセル結合しています。 勤怠管理システム開発のタブ MultiRowを利用して勤怠管理システム開発の WBS を表示します。 各タブ共通の処理 InputNumberを利用して1日の労働時間を設定する数値入力テキストボックスを表示します。 Popupを利用して不正な値の入力行われた場合にアラートを表示します。 InputDateを利用して開発期間を設定する入力ボックスを作成します。 UndoStackを利用して WBS のグリッドに元に戻す/やり直し機能を実現します。 DataMapを利用して 工程列に、マップの値を含むドロップダウンリストを表示します。 FlexGridFilterを利用して工程と担当者の列に EXCEL スタイルのフィルタ追加しています。 CollectionView.calculatedFieldsプロパティを利用して、タスクごとに出来高および工数消化率を計算しています。 Flexgrid.cellEditEndingイベントを利用して、セルの値の入力チェックを行っています。 Flexgrid.pastingCellイベントを利用して、セルの値の入力チェックを行っています。 Flexgrid.cellEditEndedイベントを利用して、行単位の計算や、工程「その他」の行を更新をしています。 Flexgrid.pastedCellイベントを利用して、行単位の計算や、工程「その他」の行を更新をしています。 Flexgrid.beginningEditイベントを利用して、入力禁止をセル単位で制御しています。
import "./css/style.css"; import "bootstrap.css"; import "@mescius/wijmo.styles/wijmo.css"; import { Control } from "@mescius/wijmo"; import { FlexGridXlsxConverter } from "@mescius/wijmo.grid.xlsx"; import { TabPanel } from "@mescius/wijmo.nav"; // document.readyState === "complete" ? init() : (window.onload = init); // let showGrid = null; function init() { let tabPanel = new TabPanel("#theTabPanel"); loadContent("project_flexgrid1", "theFlexGrid1"); tabPanel.selectedIndexChanged.addHandler(function (s, e) { switch (s.selectedIndex) { case 0: loadContent("project_flexgrid1", "theFlexGrid1"); break; case 1: loadContent("project_flexgrid2", "theFlexGrid2"); break; case 2: loadContent("project_multrow", "theMultRow"); break; } }); document.querySelector("#saveXlsx").addEventListener("click", () => { if (!showGrid) return; let projectName = tabPanel.selectedTab.header.textContent; FlexGridXlsxConverter.saveAsync(showGrid, { includeColumnHeaders: true, includeStyles: false, formatItem: null, }, `${projectName}_${dateFns.format(new Date(), "yyyy-MM-dd-HH-mm-ss")}.xlsx`); }); } function loadContent(urlId, gridId) { fetch(`${urlId}.html`) .then((response) => response.text()) .then((data) => { if (document.getElementById(urlId).innerHTML !== "") return; document.getElementById(urlId).innerHTML = data; System.import("./src/" + urlId) // 動的にファイルを読み込む .then(() => { showGrid = Control.getControl(`#${gridId}`); }); }) .catch((error) => console.error("Error loading content:", error)); }
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <title>プロジェクト工数管理システム</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 src="https://cdn.jsdelivr.net/npm/date-fns@3.6.0/cdn.min.js"></script> <script> System.import("./src/app"); </script> <style type="text/css"> .index { padding: 5px 15px 5px; /* 上、左右、下 */ } </style> </head> <body> <div class="index"> <h3 class="title">プロジェクト工数管理システム</h3> <button id="saveXlsx" type="button">Excel出力</button> <div id="theTabPanel"> <div> <a>クーポンアプリ開発</a> <div id="project_flexgrid1"></div> </div> <div> <a>在庫管理システム開発</a> <div id="project_flexgrid2"></div> </div> <div> <a>勤怠管理システム開発</a> <div id="project_multrow"></div> </div> </div> </div> </body> </html>
import { CellType, DataMap, MergeManager, CellRange, } from "@mescius/wijmo.grid"; import { DataType, addClass, removeClass, Globalize } from "@mescius/wijmo"; import { Column } from "@mescius/wijmo.grid"; import { Popup } from "@mescius/wijmo.input"; import { FlexGridFilter, FilterType } from "@mescius/wijmo.grid.filter"; import { InputNumber, InputDate } from "@mescius/wijmo.input"; import { UndoStack } from "@mescius/wijmo.undo"; import "./../css/style.css"; import "@mescius/wijmo.cultures/wijmo.culture.ja"; import { holidays } from "./holidays"; import { isEmpty } from "../util/number-string"; export const process = [ { id: 0, name: "RD" }, { id: 1, name: "UI・SS" }, { id: 2, name: "PG・PT" }, { id: 3, name: "IT" }, { id: 4, name: "ST" }, ]; const AllText = "全体"; const CommonText = "その他"; export const dateCellBinding = "date_"; const dateValueCellBinding = `${dateCellBinding}value_`; const dateBindingFormat = "yyyy-MM-dd"; const MMddFormat = "MM/dd"; const processMap = new DataMap(process, "id", "name"); export let WorkingHours = 8.0; let previousStartDate; let previousEndDate; const Process = "process"; const Task = "task"; const ProgressRate = "progressRate"; // 進捗率 const Personnel = "personnel"; const Yield = "yield"; const ActualTime = "actualTime"; const ScheduledTime = "scheduledTime"; const ManHourDigestionRate = "manHourDigestionRate"; const grayTextCellNames = [Process, Task, Personnel]; let PersonnelList = []; // 名前のリスト const baseHeaders = [ { binding: Process, header: "工程", dataMap: processMap }, { binding: Task, header: "タスク", width: 250, align: "left", }, { binding: Personnel, header: "担当者", width: 100, align: "left", }, { binding: ScheduledTime, header: "予定(d)", align: "left", dataType: "Number", width: 70, format: "n2", }, { binding: ActualTime, header: "実績(d)", align: "right", dataType: "Number", width: 70, format: "n2", isReadOnly: true, }, { binding: ProgressRate, header: "進捗率", align: "left", dataType: "Number", width: 70, format: "p0", }, { binding: Yield, header: "出来高", align: "left", dataType: "Number", width: 70, format: "n2", isReadOnly: true, }, { binding: ManHourDigestionRate, header: "工数消化率", align: "left", width: 95, format: "p0", isReadOnly: true, }, ]; let headers; // コピー function isHoliday(date) { return (dateFns.isWeekend(date) || holidays.some((holiday) => new Date(holiday).getTime() === date.getTime())); } function getDateHeader(currentDate) { return { binding: `${dateCellBinding}${dateFns.format(currentDate, dateBindingFormat)}`, header: dateFns.format(currentDate, MMddFormat), align: "left", width: 56, dataType: "String", cssClass: isHoliday(currentDate) ? "holiday-cell" : "", }; } function getDateValueHeader(currentDate) { return { binding: `${dateValueCellBinding}${dateFns.format(currentDate, dateBindingFormat)}`, header: dateFns.format(currentDate, MMddFormat), align: "left", width: 56, dataType: "String", cssClass: isHoliday(currentDate) ? "holiday-cell" : "", }; } export function createHeaders(startDate = new Date(), days = 30, useMultiRow = false) { headers = !useMultiRow ? (headers = [...baseHeaders]) : baseHeaders.map((header) => ({ header: header.header, cells: [header] })); for (let i = 0; i < days; i++) { const currentDate = dateFns.addDays(startDate, i); headers.push(!useMultiRow ? getDateHeader(currentDate) : { header: dateFns.format(currentDate, MMddFormat), cells: [ getDateHeader(currentDate), getDateValueHeader(currentDate), ], }); } return headers; } export function getFrozenColumns() { return (baseHeaders.findIndex((header) => header.binding === ManHourDigestionRate) + 1); } export function getData(data, useSample = true) { // 工程がその他の行を追加 const tmpPersonnelList = [ ...new Set(data .filter((item) => !isEmpty(item.personnel)) .map((item) => item.personnel)), ]; PersonnelList = tmpPersonnelList; let commonCols = PersonnelList.map((personnel) => ({ process: CommonText, personnel: personnel, scheduledTime: 3, })); data = commonCols.concat(data); // その他の行追加 // 工程が全体の行を追加 data = [ { process: AllText, }, ].concat(data); // その他の行追加 //作業日(例) let sanpleRow = { process: `(例)${process[1].name}`, task: "(例)設計書レビュー", personnel: "(例)田中", scheduledTime: 3, isSample: true, progressRate: 0.8, }; let sampleStartDateStr = isFlexGrid() ? headers.find((header) => header.binding.startsWith(dateCellBinding)) .binding : headers.find((header) => header.cells[0].binding.startsWith(dateCellBinding)).cells[0].binding; sampleStartDateStr = sampleStartDateStr.replace(dateCellBinding, ""); let sampleStartDate = new Date(sampleStartDateStr); let sampleWorkDateStr = []; while (sampleWorkDateStr.length < 5) { if (!isHoliday(sampleStartDate)) { sampleWorkDateStr.push(dateFns.format(sampleStartDate, dateBindingFormat)); } sampleStartDate = dateFns.addDays(sampleStartDate, 1); } sampleWorkDateStr.forEach((sampleWork) => { sanpleRow[`${dateCellBinding}${sampleWork}`] = "□"; }); if (useSample) { //サンプルを使用する場合のみ追加 data = [sanpleRow].concat(data); // サンプルの行追加 } // "date_" を含む binding 名を抽出 const dateBindings = headers .filter((col) => getBinding(col).includes(dateCellBinding)) .map((col) => getBinding(col)); if (!isFlexGrid()) { let actualTime = 0; if (useSample) { sampleWorkDateStr.forEach((sampleWork) => { let randHours = Math.round((Math.random() * (WorkingHours - 1) + 1) / 0.25) * 0.25; // 1~8 (0.25刻み) data[0][`${dateValueCellBinding}${sampleWork}`] = Globalize.formatNumber(Number(randHours), "n2"); actualTime += randHours; }); data[0].actualTime = Globalize.formatNumber(Number(actualTime / WorkingHours), "n2"); } return data; // multrowの場合 } let doubledData = []; data.forEach((item) => { let copy = JSON.parse(JSON.stringify(item)); if (copy.scheduledTime !== undefined) { copy.scheduledTime = ""; copy.actualTime = ""; } copy.progressRate = ""; copy.isCopy = true; // dateBindings 内の文字列すべてを初期化 dateBindings.forEach((binding) => { copy[binding] = ""; }); // サンプル行に適当に実施工数を入れる let actualTime = 0; if (copy.isSample) { sampleWorkDateStr.forEach((sampleWork) => { let randHours = Math.round((Math.random() * (WorkingHours - 1) + 1) / 0.25) * 0.25; // 1~8 (0.25刻み) copy[`${dateCellBinding}${sampleWork}`] = Globalize.formatNumber(Number(randHours), "n2"); actualTime += randHours; }); item.actualTime = Globalize.formatNumber(Number(actualTime / WorkingHours), "n2"); } doubledData.push(item); doubledData.push(copy); }); return doubledData; } export function getUpdatedView(s, e) { // FlexGridが描画された後に実行する recalculateActualTime(s); // 実績(d)の計算 // 全体実績の計算 calcAllCell(s.columns.getColumn(ActualTime).index, s.columns.getColumn(ActualTime).index, s); // 全体予定の計算 calcAllCell(s.columns.getColumn(ScheduledTime).index, s.columns.getColumn(ScheduledTime).index, s); // 各々のその他の進捗率を計算 PersonnelList.forEach((personnel) => { calcCommonProgressRate(0, s.columns.getColumn(Personnel).index, personnel, s); }); calcProgressRate(0, s.columns.getColumn(Personnel).index, s); // 全体進捗率を計算 // イベントハンドラーを解除 s.updatedView.removeHandler(getUpdatedView); } export function getCalculatedFields() { return { yield: ($) => { if ($.isCopy) return ""; else if ($.scheduledTime === undefined || $.progressRate === undefined) return 0; else return $.scheduledTime * $.progressRate; }, manHourDigestionRate: ($) => { if ($.isCopy) return ""; else if ($.scheduledTime === 0 || $.actualTime === undefined || $.scheduledTime === undefined || $.actualTime === "" || $.scheduledTime === "") return 0; else return $.actualTime / $.scheduledTime; }, }; } export function getFormatItem(s, e) { // 入力するセルのヘッダー背景色を変更 if (e.panel.cellType === CellType.ColumnHeader) { // 進捗率 addClass(e.cell, "secondary-color-cell"); } // 数字セルの右寄せ if (e.panel.cellType === CellType.Cell) { const col = s.columns[e.col]; const row = s.rows[e.row]; // 数値は右寄せ if (col.dataType === DataType.Number) { removeClass(e.cell, "wj-align-left"); addClass(e.cell, "wj-align-right"); } if (e.row % 2 === 0) { if (!grayTextCellNames.includes(col.binding)) { addClass(e.cell, "custom-color-cell"); } } else if (grayTextCellNames.includes(col.binding)) { // 工程~担当のセルの文字を薄色に addClass(e.cell, "gray-text"); } // サンプル行をグレーアウト if (row.dataItem && row.dataItem.isSample) { removeClass(e.cell, "custom-color-cell"); addClass(e.cell, "inactive"); } // glyphグリフを非表示にする if ((row.dataItem && row.dataItem.isSample) || [CommonText, AllText].includes(s.getCellData(e.row, e.col, false))) { let glyph = e.cell.querySelector(".wj-glyph-down"); if (glyph) { glyph.style.display = "none"; } } // データセルの中央寄せ if (!baseHeaders.map((header) => header.binding).includes(col.binding)) { removeClass(e.cell, "wj-align-left"); addClass(e.cell, "wj-align-center"); } } } export function getBeginningEdit(s, e) { const col = s.columns[e.col]; const row = s.rows[e.row]; // セルごとに編集禁止 if ((isFlexGrid() && // multrowの場合はスキップ e.row % 2 !== 0 && baseHeaders.map((header) => header.binding).includes(col.binding)) || (row.dataItem && row.dataItem.isSample)) { e.cancel = true; } // その他・全体セル行の工程タスク担当および進捗率の編集禁止 const processIndex = s.columns.findIndex((c) => c.binding === Process // 工程のindexを取得 ); const processCellData = s.getCellData(e.row, processIndex, true); if (([CommonText, AllText].includes(processCellData) && (grayTextCellNames.includes(col.binding) || col.binding === ProgressRate)) || // その他の進捗率は自動計算のため編集禁止 (col.binding === ScheduledTime && processCellData === AllText)) { e.cancel = true; } } export function getPastingCell(s, e) { getCellEditEnding(s, e); } // CellEditEndingイベントのハンドラーを追加 export function getCellEditEnding(s, e) { // 編集されているセルの値を取得 var editedValue = s.activeEditor ? s.activeEditor.value : e.data; // 入力チェックを行う if (e.row % 2 === 0) { if (!isNaN(editedValue) && (editedValue > 24 || editedValue < 0)) { e.cancel = true; } } else { if (isNaN(editedValue) || editedValue > 24 || editedValue < 0) { e.cancel = true; } } } // セルに値が貼り付け完了前のイベント export function getPastedCell(s, e) { getCellEditEnded(s, e); } export function getCellEditEnded(s, e) { const col = s.columns[e.col]; // コピーセルの工程・タスク担当者を一致させる処理 // multrowの場合はスキップ if (isFlexGrid() && e.row % 2 === 0) { let grayTextIndex = grayTextCellNames.indexOf(col.binding); if (grayTextIndex !== -1) { s.setCellData(e.row + 1, s.columns.findIndex((c) => c.binding === grayTextCellNames[grayTextIndex]), s.getCellData(e.row, col.index, false)); } } // 実績(d)の計算 if (e.row % 2 !== 0 && col.binding.startsWith(dateCellBinding)) { calcActualTime(s, e.row); calcAllCell(s.columns.getColumn(ActualTime).index, s.columns.getColumn(ActualTime).index, s); // 全体実績の計算 } // 全体予定の計算 if (e.row % 2 === 0 && col.binding === ScheduledTime) { calcAllCell(e.col, e.col, s); } // 編集されたセルの担当を取得 const editedPersonnel = s.getCellData(e.row, s.columns.getColumn(Personnel).index, true); setTimeout(() => { // その他行の進捗率の計算(出来高/予定時間) 出来高=予定時間x進捗率, 予定時間 // 編集後の担当者の進捗率を更新 calcCommonProgressRate(e.row, e.col, editedPersonnel, s); // 編集前の担当者の進捗率を更新(削除された担当者は除く) if (e.col === s.columns.getColumn(Personnel).index && editedPersonnel !== e.data && PersonnelList.includes(e.data) && !(e.data === undefined || e.data === null || e.data === "")) { calcCommonProgressRate(e.row, e.col, e.data, s); } calcProgressRate(e.row, e.col, s); // 全体の進捗率を計算 }, 0); if (e.col === s.columns.getColumn(Personnel).index) { // 新しい担当者がリストに存在しない場合、「その他」セルの行を追加 // 既存の担当者リストを取得 if (!PersonnelList.includes(editedPersonnel) && !isEmpty(editedPersonnel)) { let newRow = { process: CommonText, personnel: editedPersonnel, scheduledTime: 3, // progressRate: 0, }; // 追加するIndexを取得 let insertIndex = s.collectionView.sourceCollection.findIndex((item) => ![CommonText, AllText].includes(item.process) && (item.isSample === undefined || !item.isSample)); s.collectionView.sourceCollection.splice(insertIndex, 0, newRow); if (isFlexGrid()) { // コピー行 let newCopyRow = { process: CommonText, personnel: editedPersonnel, isCopy: true, scheduledTime: "", }; s.collectionView.sourceCollection.splice(insertIndex + 1, 0, newCopyRow); } PersonnelList.push(editedPersonnel); } // 担当者が消えた場合 if (e.col === s.columns.getColumn(Personnel).index && editedPersonnel !== e.data) { let personnelList = [ ...new Set(s.collectionView.sourceCollection .filter((item) => item.process !== CommonText) .map((item) => item.personnel)), ]; if (!personnelList.includes(e.data)) { let removeIndex = s.collectionView.sourceCollection.findIndex((item) => item.process === CommonText && item.personnel === e.data); // FlexGridの場合はコピー行が格納されているが、MultRowにはコピー行がない点を考慮して要素を削除 let removeCount = isFlexGrid() ? 2 : 1; s.collectionView.sourceCollection.splice(removeIndex, removeCount); PersonnelList = PersonnelList.filter((item) => item !== e.data); } } s.collectionView.refresh(); } // Number型のセルでないときに数値のフォーマットを適用させる let cellData = s.getCellData(e.row, col.index, false); if (!(col.dataType === DataType.Number) && !isNaN(cellData) && cellData !== "") { s.setCellData(e.row, e.col, Globalize.formatNumber(Number(cellData), "n2")); } // 新規行を追加 if (e.row >= s.rows.length - 2) { addRow(s); } } export function createUndoStack(parentClassName, view) { // parentClassNameと子要素の間にformElementを追加する const parentElement = document.querySelector(`.${parentClassName}`); // 新しい要素を作成 const formElement = document.createElement("form"); formElement.id = `${parentClassName}-undoable-form`; // parentElementの子要素をnewElementに移動 while (parentElement.firstChild) { formElement.appendChild(parentElement.firstChild); } // 新しい要素を追加 parentElement.appendChild(formElement); let undoStack = new UndoStack(`#${formElement.id}`, { undoneAction: (s, e) => { getCellEditEnded(view, e.action); }, redoneAction: (s, e) => { getCellEditEnded(view, e.action); }, }); } export function createWorkingHoursInputNumber(id, grid) { const alertId = "alertPopup"; return new InputNumber(`#${id}`, { value: WorkingHours, lostFocus: (sender) => { //労働時間の変更により実績を再計算 if (sender.value > 0 && sender.value < 24) { WorkingHours = sender.value; recalculateActualTime(grid); } else { showAlert(alertId, "有効な数値を入力してください。"); sender.text = WorkingHours.toString(); // 無効な入力の場合、元の値に戻す } }, }); } export function createTermInputeDate(startId, endId, startDate, endDate, view) { previousStartDate = startDate; // 変更前の期間を保存しておく previousEndDate = endDate; // 変更前の期間を保存しておく let theStartDate = new InputDate(`#${startId}`, { value: previousStartDate, valueChanged: (sender) => { if (dateFns.isSameDay(previousStartDate, sender.value)) return; // WBSの日付の範囲を更新 // 変更した日付が終了日を過ぎていないかチェック if (dateFns.isAfter(sender.value, previousEndDate)) { showAlert("startTermInvalidPopup", "開始日時は終了日時以前の日付を選択して下さい"); sender.value = previousStartDate; // 無効な入力の場合、元の値に戻す return; } if (view) { if (dateFns.isAfter(previousStartDate, sender.value)) { let currentDate = previousStartDate; let startDate = sender.value; while (dateFns.isAfter(currentDate, startDate)) { let columnBinding = `${dateCellBinding}${dateFns.format(currentDate, dateBindingFormat)}`; let columnIndex = isFlexGrid() ? view.columns.findIndex((col) => getBinding(col) === columnBinding) : view.layoutDefinition .slice() .findIndex((col) => getBinding(col) === columnBinding); if (columnIndex !== -1) { currentDate = dateFns.subDays(currentDate, 1); if (isFlexGrid()) { view.columns.splice(columnIndex, 0, new Column(getDateHeader(currentDate))); // 列を追加 } else { // MultRow let newLayout = view.layoutDefinition.slice(); newLayout.splice(columnIndex, 0, { header: dateFns.format(currentDate, MMddFormat), cells: [ getDateHeader(currentDate), getDateValueHeader(currentDate), ], }); // 列を追加 view.layoutDefinition = newLayout; // 新しいレイアウトを設定 let headerLayout = newLayout.map((column) => { return Object.assign(Object.assign({}, column), { colspan: column.cells.length }); }); view.headerLayoutDefinition = headerLayout; } } } } else { let currentDate = previousStartDate; let startDate = sender.value; while (dateFns.isBefore(currentDate, startDate)) { let columnBinding = `${dateCellBinding}${dateFns.format(currentDate, dateBindingFormat)}`; let columnIndex = isFlexGrid() ? view.columns.findIndex((col) => getBinding(col) === columnBinding) : view.layoutDefinition .slice() .findIndex((col) => getBinding(col) === columnBinding); if (columnIndex !== -1) { if (isFlexGrid()) { view.columns.splice(columnIndex, 1); // 列を削除 } else { //MultRow let newLayout = view.layoutDefinition.slice(); newLayout.splice(columnIndex, 1); // 列を削除 view.layoutDefinition = newLayout; } } currentDate = dateFns.addDays(currentDate, 1); } } previousStartDate = sender.value; // 変更前の期間を保存しておく // 期間を変更した後に実績が更新されないので自動で呼び出す必要がある recalculateActualTime(view); // 実績の再計算 } }, }); let theEndDate = new InputDate(`#${endId}`, { value: previousEndDate, valueChanged: (sender) => { if (dateFns.isSameDay(previousEndDate, sender.value)) return; // WBSの日付の範囲を更新 // 変更した日付が開始日より前でないかチェック if (dateFns.isBefore(sender.value, previousStartDate)) { showAlert("endTermInvalidPopup", "終了日時は開始日時以後の日付を選択して下さい"); sender.value = previousEndDate; // 無効な入力の場合、元の値に戻す return; } if (view) { if (dateFns.isBefore(previousEndDate, sender.value)) { let currentDate = previousEndDate; let endDate = sender.value; while (dateFns.isBefore(currentDate, endDate)) { let columnBinding = `${dateCellBinding}${dateFns.format(currentDate, dateBindingFormat)}`; let columnIndex = isFlexGrid() ? view.columns.findIndex((col) => getBinding(col) === columnBinding) : view.layoutDefinition .slice() .findIndex((col) => getBinding(col) === columnBinding); if (columnIndex !== -1) { currentDate = dateFns.addDays(currentDate, 1); if (isFlexGrid()) { view.columns.splice(columnIndex + 1, 0, new Column(getDateHeader(currentDate))); // 列を追加 } else { // MultRow let newLayout = view.layoutDefinition.slice(); newLayout.splice(columnIndex + 1, 0, { header: dateFns.format(currentDate, MMddFormat), cells: [ getDateHeader(currentDate), getDateValueHeader(currentDate), ], }); // 列を追加 view.layoutDefinition = newLayout; // 新しいレイアウトを設定 let headerLayout = newLayout.map((column) => { return Object.assign(Object.assign({}, column), { colspan: column.cells.length }); }); view.headerLayoutDefinition = headerLayout; } } } } else { let currentDate = previousEndDate; let endDate = sender.value; while (dateFns.isAfter(currentDate, endDate)) { let columnBinding = `${dateCellBinding}${dateFns.format(currentDate, dateBindingFormat)}`; let columnIndex = isFlexGrid() ? view.columns.findIndex((col) => getBinding(col) === columnBinding) : view.layoutDefinition .slice() .findIndex((col) => getBinding(col) === columnBinding); if (columnIndex !== -1) { if (isFlexGrid()) { view.columns.splice(columnIndex, 1); // 列を削除 } else { //MultRow let newLayout = view.layoutDefinition.slice(); newLayout.splice(columnIndex, 1); // 列を削除 view.layoutDefinition = newLayout; // 新しいレイアウトを設定 } } currentDate = dateFns.subDays(currentDate, 1); } } previousEndDate = sender.value; // 変更前の期間を保存しておく // 期間を変更した後に実績が更新されないので自動で呼び出す必要がある recalculateActualTime(view); // 実績の再計算 } }, }); } // 再計算関数 function recalculateActualTime(grid) { if (grid) { grid.collectionView.items.forEach((item, index) => { if (index % 2 !== 0) { calcActualTime(grid, index); } }); grid.collectionView.refresh(); } } function calcActualTime(s, rowIndex) { let actualTime = s.columns.reduce((sum, col) => { if (col.binding.startsWith(dateCellBinding)) { let cellData = s.getCellData(rowIndex, col.index, false); return !isNaN(cellData) ? Number(sum) + Number(cellData) : sum; } return sum; }, 0); if (actualTime > 0 || !isEmpty( // 実績が入力されている場合は実績が0でも入力(期間が変更されたときなどに起こる) s.getCellData(rowIndex - 1, s.columns.findIndex((c) => c.binding === ActualTime), false))) { s.setCellData(rowIndex - 1, s.columns.findIndex((c) => c.binding === ActualTime), !isNaN(WorkingHours) ? actualTime / WorkingHours : 0); } } // 全体の進捗率を計算 function calcProgressRate(rowIndex, colIndex, grid) { const col = grid.columns[colIndex]; if (rowIndex % 2 === 0 && (col.binding === ProgressRate || col.binding === ScheduledTime || col.binding === Personnel)) { // 担当ごとの進捗率の平均を計算(出来高/予定時間) let sumYielda = 0; let sumScheduledTime = 0; for (let i = 0; i < grid.rows.length; i++) { if (i % 2 !== 0) continue; // 工程 let processValue = grid.getCellData(i, grid.columns.getColumn(Process).index, false); // 出来高 let yielda = grid.getCellData(i, grid.columns.getColumn(Yield).index, false); // 予定時間 let scheduledTime = grid.getCellData(i, grid.columns.getColumn(ScheduledTime).index, false); if (processValue !== AllText && !isEmpty(grid.rows[i].dataItem) && (grid.rows[i].dataItem.isSample === undefined || !grid.rows[i].dataItem.isSample)) { if (isNumber(yielda)) { sumYielda += yielda; } if (isNumber(scheduledTime)) { sumScheduledTime += scheduledTime; } } } // 進捗をその他セルに設定 grid.rows.some((row, i) => { if (row.dataItem.process === AllText) { // その他セル以外に入力があればその他セルの進捗は0%でも表示 if (sumYielda !== 0 && sumScheduledTime !== 0) { grid.setCellData(i, grid.columns.getColumn(ProgressRate).index, sumYielda / sumScheduledTime, true); } return true; // ループを終了する } return false; // ループを続行する }); } } // その他の進捗率を計算 function calcCommonProgressRate(rowIndex, colIndex, editedPersonnel, grid) { if (isEmpty(editedPersonnel)) return; const col = grid.columns[colIndex]; if (rowIndex % 2 === 0 && (col.binding === ProgressRate || col.binding === ScheduledTime || col.binding === Personnel)) { // 担当ごとの進捗率の平均を計算(出来高/予定時間) let sumYielda = 0; let sumScheduledTime = 0; let targetCommonRow = 0; for (let i = 0; i < grid.rows.length; i++) { if (i % 2 !== 0) continue; // 工程 let processValue = grid.getCellData(i, grid.columns.getColumn(Process).index, false); // 担当者 let personnelValue = grid.getCellData(i, grid.columns.getColumn(Personnel).index, false); // 出来高 let yielda = grid.getCellData(i, grid.columns.getColumn(Yield).index, false); // 予定時間 let scheduledTime = grid.getCellData(i, grid.columns.getColumn(ScheduledTime).index, false); if (personnelValue === editedPersonnel) { if (processValue !== CommonText && !isEmpty(grid.rows[i].dataItem) && (grid.rows[i].dataItem.isSample === undefined || !grid.rows[i].dataItem.isSample)) { if (isNumber(yielda)) { sumYielda += yielda; } if (isNumber(scheduledTime)) { sumScheduledTime += scheduledTime; } } else { targetCommonRow = i; } } } // 進捗をその他セルに設定 if (sumYielda !== 0 && sumScheduledTime !== 0 && sumYielda / sumScheduledTime !== 0) { grid.setCellData(targetCommonRow, grid.columns.getColumn(ProgressRate).index, sumYielda / sumScheduledTime, true); } } } //全体セルのうち任意の集計セルの計算 function calcAllCell(colIndex, bindingX, s) { let sumBindingName = s.rows.reduce((sum, row, i) => { if (i % 2 === 0 && s.getCellData(i, s.columns.getColumn(Process).index, false) !== AllText && row.dataItem && (row.dataItem.isSample === undefined || !row.dataItem.isSample)) { let cellData = s.getCellData(i, colIndex, false); return !isNaN(cellData) ? Number(sum) + Number(cellData) : sum; } return sum; }, 0); if (sumBindingName > 0) { if (sumBindingName > 0) { s.rows.some((row, i) => { if (row.dataItem.process === AllText) { s.setCellData(i, bindingX, sumBindingName); return true; // ループを終了する } return false; // ループを続行する }); } } } export function setFilter(grid) { // EXCELスタイルのフィルタ追加 const filter = new FlexGridFilter(grid); filter.defaultFilterType = FilterType.None; filter.getColumnFilter(Process).filterType = FilterType.Value; filter.getColumnFilter(Personnel).filterType = FilterType.Value; } export function addRow(grid) { grid.itemsSource.items.push({ scheduledTime: "", progressRate: "" }); if (isFlexGrid()) { grid.itemsSource.items.push({ scheduledTime: "", progressRate: "", isCopy: true, }); } grid.collectionView.refresh(); // データの変更を反映 } // アラートの要素を追加 function createAlertPopup(alertPopupId) { const popupDiv = document.createElement("div"); popupDiv.id = alertPopupId; popupDiv.className = "wj-popup"; popupDiv.innerHTML = ` <div class="wj-dialog-header">アラート</div> <div class="wj-dialog-body"> <p id="${alertPopupId}Message"></p> </div> <div class="wj-dialog-footer"> <button id="${alertPopupId}OkButton" class="wj-btn wj-btn-default">OK</button> </div> `; document.body.appendChild(popupDiv); // Popupの初期化 let alertPopup = new Popup(`#${alertPopupId}`); // OKボタンのクリックイベント document .getElementById(`${alertPopupId}OkButton`) .addEventListener("click", () => { alertPopup.hide(); }); return alertPopup; } // アラートを表示する関数 function showAlert(alertId, message) { const alertPopup = createAlertPopup(alertId); document.getElementById(`${alertId}Message`).textContent = message; alertPopup.show(true); // trueを渡すとモーダル表示になります } export function isNumber(number) { if (number !== null && number !== undefined && number !== "" && !isNaN(number)) return true; else false; } export class RestrictedMergeManager extends MergeManager { getMergedRange(p, r, c, clip = true) { if (grayTextCellNames.includes(p.columns[c].binding)) { // create basic cell range var rng = null; // start with single cell rng = new CellRange(r, c); // expand up while (rng.row > 0 && rng.row % 2 !== 0) { rng.row--; } // expand down while (rng.row2 < p.rows.length - 1 && (rng.row2 + 1) % 2 !== 0) { rng.row2++; } // don't bother with single-cell ranges if (rng.isSingleCell) { rng = null; } // done return rng; } } } function isFlexGrid() { return headers[0].binding !== undefined; } function getBinding(col) { return col.binding !== undefined ? col.binding : col.cells[0].binding; }
import "bootstrap.css"; import "@mescius/wijmo.styles/wijmo.css"; import * as DataProjectAService from "./data/data_project_flexgrid1"; import "./css/style_project_flexgrid1.css"; document.readyState === "complete" ? init() : (window.onload = init); function init() { let grid = DataProjectAService.getFlexGrid(); }
import * as DataCommonService from "./data_common"; import { CollectionView, Globalize } from "@mescius/wijmo"; import { FlexGrid } from "@mescius/wijmo.grid"; export function getFlexGrid() { const process = DataCommonService.process; const baseData = [ { process: process[0].name, task: "要件定義作成", personnel: "田中", scheduledTime: 3, // actualTime: 3, }, { process: process[0].name, task: "要件定義レビュー", personnel: "鈴木", scheduledTime: 1.5, }, { process: process[1].name, task: "設計書作成", personnel: "斎藤", scheduledTime: 3.5, }, { process: process[1].name, task: "設計書レビュー", personnel: "田中", scheduledTime: 1, }, { process: process[2].name, task: "お知らせ一覧取得API", personnel: "斎藤", scheduledTime: 2, }, { process: process[2].name, task: "お知らせ取得API", personnel: "斎藤", scheduledTime: 2, }, { process: process[2].name, task: "ホーム画面", personnel: "高橋", scheduledTime: 5, }, { process: process[2].name, task: "クーポン一覧取得API", personnel: "斎藤", scheduledTime: 3, }, { process: process[2].name, task: "クーポン取得API", personnel: "斎藤", scheduledTime: 3, }, { process: process[2].name, task: "クーポン一覧画面", personnel: "斎藤", scheduledTime: 3, }, { process: process[2].name, task: "クーポン詳細画面", personnel: "斎藤", scheduledTime: 4, }, { process: process[2].name, task: "クーポン利用API", personnel: "斎藤", scheduledTime: 3, }, { process: process[2].name, task: "クーポン一括利用画面", personnel: "斎藤", scheduledTime: 3, }, { process: process[2].name, task: "設定画面", scheduledTime: 3, }, ]; const columns = DataCommonService.createHeaders(new Date("2024-11-01"), 25); const data = DataCommonService.getData(baseData); // TODO 実データにも適当に実データを入れる const idx1 = data.findIndex((item) => item.task === "要件定義作成"); const prefix = DataCommonService.dateCellBinding; data[idx1][prefix + "2024-11-01"] = "□"; data[idx1 + 1][prefix + "2024-11-01"] = Globalize.formatNumber(8, "n2"); data[idx1][prefix + "2024-11-04"] = "□"; data[idx1 + 1][prefix + "2024-11-04"] = Globalize.formatNumber(4, "n2"); data[idx1][prefix + "2024-11-05"] = "□"; data[idx1 + 1][prefix + "2024-11-05"] = Globalize.formatNumber(4, "n2"); data[idx1][prefix + "2024-11-06"] = "□"; data[idx1].progressRate = 0.7; const idx2 = data.findIndex((item) => item.task === "要件定義レビュー"); data[idx2][prefix + "2024-11-04"] = "□"; data[idx2][prefix + "2024-11-05"] = "□"; data[idx2 + 1][prefix + "2024-11-04"] = Globalize.formatNumber(4, "n2"); data[idx2 + 1][prefix + "2024-11-05"] = Globalize.formatNumber(3, "n2"); data[idx2][prefix + "2024-11-06"] = "□"; data[idx2].progressRate = 0.6; const grid = new FlexGrid("#theFlexGrid1", { autoGenerateColumns: false, columns: columns, frozenColumns: DataCommonService.getFrozenColumns(), showMarquee: true, allowSorting: false, headersVisibility: "Column", updatedView: DataCommonService.getUpdatedView, formatItem: DataCommonService.getFormatItem, beginningEdit: DataCommonService.getBeginningEdit, cellEditEnding: DataCommonService.getCellEditEnding, cellEditEnded: DataCommonService.getCellEditEnded, pastingCell: DataCommonService.getPastingCell, pastedCell: DataCommonService.getPastedCell, itemsSource: new CollectionView(data, { calculatedFields: DataCommonService.getCalculatedFields(), }), }); DataCommonService.addRow(grid); DataCommonService.createWorkingHoursInputNumber("pj1-theInputNoSrc", grid); DataCommonService.createUndoStack("project_flexgrid1", grid); DataCommonService.setFilter(grid); grid.autoSizeColumns(); return grid; }
import "bootstrap.css"; import "@mescius/wijmo.styles/wijmo.css"; import * as DataProjectBService from "./data/data_project_flexgrid2"; import "./css/style_project_flexgrid2.css"; document.readyState === "complete" ? init() : (window.onload = init); function init() { let grid = DataProjectBService.getFlexGrid(); }
// data used to generate random items import * as DataCommonService from "./data_common"; import { CollectionView } from "@mescius/wijmo"; import { FlexGrid } from "@mescius/wijmo.grid"; export function getFlexGrid() { const startDate = new Date(); const term = 10; const endDate = dateFns.addDays(startDate, term); const process = DataCommonService.process; const data = [ { process: process[0].name, task: "要件定義作成", personnel: "田中", scheduledTime: 7, }, { process: process[1].name, task: "要件定義レビュー", personnel: "鈴木", scheduledTime: 4, }, { process: process[1].name, task: "設計書作成", personnel: "斎藤", scheduledTime: 7, }, { process: process[1].name, task: "設計書レビュー", scheduledTime: 2.5, }, { process: process[2].name, task: "商品登録/編集/削除/検索API", scheduledTime: 5, }, { process: process[2].name, task: "商品登録/編集/削除/検索画面", scheduledTime: 7, }, { process: process[2].name, task: "在庫追加/削減/確認API", personnel: "斎藤", scheduledTime: 3, }, ]; let grid = new FlexGrid("#theFlexGrid2", { autoGenerateColumns: false, columns: DataCommonService.createHeaders(startDate, term + 1), frozenColumns: DataCommonService.getFrozenColumns(), showMarquee: true, allowSorting: false, allowMerging: "Cells", headersVisibility: "Column", formatItem: DataCommonService.getFormatItem, beginningEdit: DataCommonService.getBeginningEdit, cellEditEnding: DataCommonService.getCellEditEnding, cellEditEnded: DataCommonService.getCellEditEnded, pastingCell: DataCommonService.getPastingCell, pastedCell: DataCommonService.getPastedCell, itemsSource: new CollectionView(DataCommonService.getData(data), { calculatedFields: DataCommonService.getCalculatedFields(), }), }); DataCommonService.addRow(grid); grid.mergeManager = new DataCommonService.RestrictedMergeManager(); DataCommonService.createWorkingHoursInputNumber("pj2-theInputNoSrc", grid); DataCommonService.createTermInputeDate("pj2-TermStartDate", "pj2-TermEndDate", startDate, endDate, grid); DataCommonService.createUndoStack("project_flexgrid2", grid); DataCommonService.setFilter(grid); grid.autoSizeColumns(); return grid; }
import "bootstrap.css"; import "@mescius/wijmo.styles/wijmo.css"; import * as DataProjectMultRowService from "./data/data_project_multrow"; import "./css/style_project_multrow.css"; document.readyState === "complete" ? init() : (window.onload = init); function init() { let grid = DataProjectMultRowService.getMultRow(); }
import * as DataCommonService from "./data_common"; import { CollectionView } from "@mescius/wijmo"; import { MultiRow } from "@mescius/wijmo.grid.multirow"; export function getMultRow() { const startDate = new Date(); const term = 10; const endDate = dateFns.addDays(startDate, term); let columns = DataCommonService.createHeaders(startDate, term + 1, true); let headerLayout = columns.map((column) => { return Object.assign(Object.assign({}, column), { colspan: column.cells.length }); }); const process = DataCommonService.process; const data = [ { process: process[0].name, task: "要件定義作成", personnel: "田中", scheduledTime: 7, }, { process: process[1].name, task: "要件定義レビュー", personnel: "鈴木", scheduledTime: 4, }, { process: process[1].name, task: "設計書作成", personnel: "斎藤", scheduledTime: 7, }, { process: process[1].name, task: "設計書レビュー", scheduledTime: 2.5, }, { process: process[2].name, task: "出勤登録API", scheduledTime: 3, }, { process: process[2].name, task: "退勤登録API", scheduledTime: 3, }, ]; let grid = new MultiRow("#theMultRow", { frozenColumns: DataCommonService.getFrozenColumns(), layoutDefinition: columns, headerLayoutDefinition: headerLayout, collapsedHeaders: true, showMarquee: true, allowSorting: false, headersVisibility: "Column", formatItem: DataCommonService.getFormatItem, beginningEdit: DataCommonService.getBeginningEdit, cellEditEnding: DataCommonService.getCellEditEnding, cellEditEnded: DataCommonService.getCellEditEnded, pastingCell: DataCommonService.getPastingCell, pastedCell: DataCommonService.getPastedCell, itemsSource: new CollectionView(DataCommonService.getData(data), { calculatedFields: DataCommonService.getCalculatedFields(), }), }); DataCommonService.addRow(grid); DataCommonService.createWorkingHoursInputNumber("pj3-theInputNoSrc", grid); DataCommonService.createTermInputeDate("pj3-TermStartDate", "pj3-TermEndDate", startDate, endDate, grid); DataCommonService.createUndoStack("project_multrow", grid); DataCommonService.setFilter(grid); grid.autoSizeColumns(); return grid; }
(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);