Skip to content

実績工数データを他の管理システムに反映する

概要

TimeTracker NXで入力された実績工数は、社内の採算管理や勤怠管理にとっても重要なデータです。Web APIを活用することにより、実績工数データを社内で運用している他のシステムに簡単に取り込むことができます。

利用場面

  • 日単位の実績工数データを勤怠管理システムに反映
  • 週単位/月単位の実績工数データを勤怠管理システムに反映

使用するAPI

以下のAPIを使用します。

  • ユーザーの取得
  • 実績工数の取得
//======================================================================
//  日単位の実績工数を取得する
//======================================================================
//======================================================================
//  外部ファイル(ライブラリ、他ファイル)のインクルード
//======================================================================

const https = require('https'); // HTTPS通信を利用する場合に設定する
const http = require('http'); // HTTP通信を利用する場合に設定する
const axios = require('axios');
const fs = require('fs');
const iconv = require("iconv-lite");


//======================================================================
//  共通部の定義
//======================================================================

// APIを呼び出すURLを設定する
const baseURLString = 'http://yourserver/TimeTrackerNX/api';

// サーバースペックに合わせてウェイト時間を設定する
// 1分間に1500件程度の処理に収まるようするため、
// 目安として推奨スペックでは100ms程度とする 
const requestWaitTime = 100;    //単位:ms

//----------------------------------------------------------------------
//  Web API間の処理待ち関数
//----------------------------------------------------------------------
async function wait(time){
    const d1 = new Date();
    while (true) {
        const d2 = new Date();
        if (d2 - d1 > time) {
            break;
        }
    }
}

//======================================================================
//  メイン処理
//======================================================================
main();

async function main(){

    // 引数チェック    
    if (process.argv.length < 3){
        return 'enter username and password'; 
    }

    const userName = process.argv[2];
    var pass;
    var authString; //ブロック内スコープから外に出すためvarで宣言(固定値)

    // 引数でパスワードが未入力の場合は空白とみなす
    if (process.argv.length > 3){
        pass = process.argv[3];
    } else {
        pass = '';        
    }

    // 認証情報を取得する
    var sendUrl = baseURLString + '/auth/token';

    try{
        const { data } = await axios.post(
            sendUrl,
            {
                loginName : userName,
                password : pass
            },
/*
        // https通信時に使用する
        httpsAgent  : new https.Agent({
            rejectUnauthorized: false
                    })
*/    
        );
        authString = 'Bearer ' + data.token;

    } catch (err) {
        console.log(err);
    }

    // 以降のAPI通信で利用する送信ヘッダを作成する
    const request = axios.create({
        baseURL     : baseURLString,
        headers     :{
                        'Authorization'   : authString
                    },
/*
        // https通信時に使用する
        httpsAgent  : new https.Agent({
            rejectUnauthorized: false
                    })
*/    
                });

    // 実績出力の対象ユーザーを取得
    var sendUrl = '/system/users';

    try {
        // 所属組織を指定してユーザーIDを取得する(リクエストに検索条件の所属組織を設定して送信)
        const { data } = await request.get(
            sendUrl,
            {
                params: {
                    organizations       : '開発2課'
                }
            }
        );

        // 初回のデータ書き込み時に見出しを追加するためのフラグを設定
        var headerFlag = 1;

        // 指定した組織に所属するユーザーの実績工数を一人ずつ取得する
        for (var key in data.data){
            sendUrl = '/system/users/' + data.data[key].id + '/timeEntries';

            // 対象ユーザーの実績工数を取得する(リクエストに取得期間の開始日・終了日を設定して送信)
            try {
                const { data } = await request.get(
                    sendUrl,
                    {
                        params: {
                            startDate   : '2019-03-01',
                            finishDate  : '2019-03-31'
                        }
                    }
                );

                // 取得したデータをCSV形式で出力する
                outputData(headerFlag, data);

                // 初回のデータ書き込みが終わったタイミングでフラグを無効にする
                if(headerFlag == 1){
                    headerFlag = 0;
                }

/*
                // 取得したデータをJSON形式で出力(1ユーザーごとに追加するので"[][][]"と続く出力になる)
                fs.appendFileSync('./output.json',JSON.stringify(data,undefined,4),function(err){
                    if (err) {
                        console.log(err);
                    }
                });
*/
            } catch (e) {
                console.log(e);
            }
            await wait(requestWaitTime);
        }
    } catch (e) {
        console.log(e);
    }
}

function outputData(headerFlag, data){
    var filepath = './output.csv';
    var buffer;

    // 初回の書き込みのみヘッダを追加
    if(headerFlag == 1){

        buffer = 'projectId,projectName,projectCode,workDate,startTime,finishTime,time,memo,workItemId,workItemName,workItemNumber,workItemTypeId,userId,userName,timeEntryCategoryId,timeEntryCategory,processCategoryId,processCategory,isLocked,isDeleted,id,createdAt,createdBy,updatedAt,updatedBy\n';

        fs.writeFileSync(filepath, iconv.encode(buffer, 'Shift_JIS'), function(err){
            if (err) {
                console.log(err);
            }
        });
    }

    // 1行分ごとにデータをファイルへ追記する
    for (var key in data.data){
        var array = [
            data.data[key].projectId,
            data.data[key].projectName,
            data.data[key].projectCode,
            data.data[key].workDate,
            data.data[key].startTime,
            data.data[key].finishTime,
            data.data[key].time,
            data.data[key].memo,
            data.data[key].workItemId,
            data.data[key].workItemName,
            data.data[key].workItemNumber,
            data.data[key].workItemTypeId,
            data.data[key].userId,
            data.data[key].userName,
            data.data[key].timeEntryCategoryId,
            data.data[key].timeEntryCategory,
            data.data[key].processCategoryId,
            data.data[key].processCategory,
            data.data[key].isLocked,
            data.data[key].isDeleted,
            data.data[key].id,
            data.data[key].createdAt,
            data.data[key].createdBy,
            data.data[key].updatedAt,
            data.data[key].updatedBy
        ]
        buffer = array.join(',') + '\n';

        fs.appendFileSync(filepath, iconv.encode(buffer, 'Shift_JIS'), function(err){
            if (err) {
                console.log(err);
            }
        });
    }
}
private async void Execute_Click(object sender, EventArgs e)
        {

            //======================================================//
            //  必要な変数等の定義                                    //
            //======================================================//
            JavaScriptSerializer serializar = new JavaScriptSerializer();
            var httpClient = new HttpClient();

            string serverUrl = "http://WIN-Q1OR68LM6RG.mshome.net/TimeTrackerNX/";
            string loginname = "okamoto";
            string password = "";

            // トークンの取得
            var tokenRequest = new Dictionary<string, string>
            {
                {"loginName",loginname },
                {"password",password }
            };
            var jsonData = serializar.Serialize(tokenRequest);
            var content = new StringContent(jsonData, Encoding.UTF8, "application/json");
            string requestUrl = serverUrl + "/api/auth/token";
            var response = await httpClient.PostAsync(requestUrl, content);
            string responseData = await response.Content.ReadAsStringAsync();
            Token token;

            try
            {
                token = serializar.Deserialize<Token>(responseData);
            }
            catch
            {
                MessageBox.Show("トークン取得に失敗しました。\n接続先のサーバーURL、ログイン名、パスワードを確認してください。");
                return;
            }
            // ヘッダ設定
            httpClient.DefaultRequestHeaders.Add("contentType", "application/json");
            httpClient.DefaultRequestHeaders.Add("Authorization", "Bearer " + token.token);





            //==========================================//
            // 実績工数データの取得                   //
            //==========================================//

            const string OrganizationName = "開発2課";         // 対象組織の設定
            const string StartDate = "2020-10-01";              // 取得期間:開始日
            const string FinishDate = "2020-10-31";             // 取得期間:終了日
            const string ExportFileName = "actual_export.csv";  // エクスポートファイル名

            //------------------------------------------//
            // 組織に対するユーザーの取得                //
            //------------------------------------------//

            requestUrl = serverUrl + "/api/system/users?organizationName=" + OrganizationName;
            response = await httpClient.GetAsync(requestUrl);
            responseData = await response.Content.ReadAsStringAsync();

            var user = serializar.Deserialize<ResponseUsers>(responseData);

            int memberNum = int.Parse(user.totalCount);

            for (int i = 0; i < memberNum; i++)
            {
                //------------------------------------------//
                // ユーザーの実績工数の取得             //
                //------------------------------------------//        
                requestUrl = serverUrl + "/api/system/users/" + user.data[i].id + "/timeEntries?startDate=" + StartDate + "&finishDate=" + FinishDate;

                response = await httpClient.GetAsync(requestUrl);
                responseData = await response.Content.ReadAsStringAsync();

                var actualTimeData = serializar.Deserialize<ActualTimeEntries>(responseData);


                //------------------------------------------//
                // CSVファイルへデータの書き出し         //
                //------------------------------------------//        
                int actualDataNum = int.Parse(actualTimeData.totalCount);

                StreamWriter sr = new StreamWriter(@ExportFileName, true, System.Text.Encoding.GetEncoding("shift_jis"));

                for (int export_cnt = 0; export_cnt < actualDataNum; export_cnt++)
                {
                    sr.WriteLine
                    (
                        string.Format
                        (
                        "{0},{1},{2},{3},{4},{5},{6},{7},{8},{9},{10},{11},{12},{13},{14},{15},{16},{17},{18},{19},{20},{21},{22},{23},{24}",
                        actualTimeData.data[i].id,
                        actualTimeData.data[i].workDate,
                        actualTimeData.data[i].time,
                        actualTimeData.data[i].startTime,
                        actualTimeData.data[i].finishTime,
                        actualTimeData.data[i].memo,
                        actualTimeData.data[i].projectId,
                        actualTimeData.data[i].projectName,
                        actualTimeData.data[i].projectCode,
                        actualTimeData.data[i].userId,
                        actualTimeData.data[i].userName,
                        actualTimeData.data[i].workItemId,
                        actualTimeData.data[i].workItemName,
                        actualTimeData.data[i].workItemNumber,
                        actualTimeData.data[i].workItemTypeId,
                        actualTimeData.data[i].timeEntryCategoryId,
                        actualTimeData.data[i].timeEntryCategory,
                        actualTimeData.data[i].processCategoryId,
                        actualTimeData.data[i].processCategory,
                        actualTimeData.data[i].isLocked,
                        actualTimeData.data[i].isDeleted,
                        actualTimeData.data[i].createdAt,
                        actualTimeData.data[i].createdBy,
                        actualTimeData.data[i].updatedAt,
                        actualTimeData.data[i].updatedBy
                        )
                    );

                }

                sr.Close();                
            }
        }        
    }
}

// Tokenレスポンス用のクラス
public class Token
{
    public string token { get; set; }
}

public class ResponseUsers
{
    public string totalCount;
    public ResponseUser[] data;
}

public class ResponseUser
{
    public string isDeleted;
    public string createdAt;
    public string updatedAt;
    public string createdBy;
    public string updatedBy;
    public string id;

    public ResponseUser()
    {
        this.id = null;
    }
}

public class ActualTimeEntries
{
    public string totalCount;
    public ActualTimeEntry[] data;
}

public class ActualTimeEntry
{    
    public string id;
    public DateTime workDate;
    public int time;
    public DateTime startTime;
    public DateTime finishTime;
    public string memo;
    public string projectId;
    public string projectName;
    public string projectCode;
    public string userId;
    public string userName;
    public string workItemId;
    public string workItemName;
    public string workItemNumber;
    public string workItemTypeId;
    public string timeEntryCategoryId;
    public string timeEntryCategory;
    public string processCategoryId;
    public string processCategory;
    public bool isLocked;
    public bool isDeleted;
    public DateTime createdAt;
    public string createdBy;
    public DateTime updatedAt;
    public string updatedBy;


    public ActualTimeEntry()
    {
        this.workItemId = null;
        this.id = null;
        this.workDate = DateTime.MinValue;
        this.time = 0;
        this.startTime = DateTime.MinValue;
        this.finishTime = DateTime.MaxValue;
        this.memo = null;
        this.projectId = null;
        this.projectName = null;
        this.projectCode = null;
        this.userId = null;
        this.userName = null;
        this.workItemId = null;
        this.workItemName = null;
        this.workItemNumber = null;
        this.workItemTypeId = null;
        this.timeEntryCategoryId = null;
        this.timeEntryCategory = null;
        this.processCategoryId = null;
        this.processCategory = null;
        this.isLocked = false;
        this.isDeleted = false;
        this.createdAt = DateTime.MinValue;
        this.createdBy = null;
        this.updatedAt = DateTime.MinValue;
        this.updatedBy = null;

    }

}