使用字段保存临时数据 ~考勤管理应用为例~

cybozu发表于:2017年02月23日 14:52:43更新于:2021年10月28日 13:59:09

概要

大家好(本文作者为才望子的技术销售负责人)。

如果会写kintone 的 JavaScript 程序的话,还可以给销售产品带来方便。客户在犹豫要不要购买的时候,如果您能够立即拿出一个范例程序,或者可以熟练地展示一下演示环境,自然会给客户带来很好的印象。说不准还是“JavaScript是决定商务谈判成功与否的一个关键!”(笑)

完成后的样子

00158db56605a32e1b0339baeb1355f

         图1. 简单的打卡应用。

在上班时间和下班时间之间,能够持续高效率投入工作的人相当厉害,但是我认为这种人才应该非常稀少^^; 人类集中力的上限值为90分钟,所以能够较好的使用休息时间显得尤为重要。考勤管理应用,除了上下班时间的打卡外,也需要管理总的休息时间。但是,本篇的前提是不对每次的休息开始以及结束时间进行管理。

 00158dc9ddb231ecb7e2fbf8f1d4bf2

         图2. 休息时间为当天所有的休息时间的合计

关键点是「如何管理休息开始的时间?」。在点击休息结束为止,需要临时保存休息开始的时间,休息结束后,根据开始时间和结束时间算出休息时间后,开始时间就可以不要了。(参考 图3)。

00158db5d6fcabba8067527b49ae3e8

         图3. 需要保存休息开始的时间,一直到点击 “返回” 按钮为止

 也就是说,休息结束后 “休息开始” 时间就不再需要了。临时保存“休息开始” 时间,按图3算出返回为止的 “休息时间”之后,就可以扔掉了!

创建kintone应用

创建应用

新建打卡应用,字段的设置如下。
关于应用的创建方法,请参考<<从头开始新建应用>>。

员工信息

字段类型字段名称字段代码备注
单行文本框员工号员工号
  • 勾选“值为唯一”选项

  • 此字段的值将被复制到[考勤卡]应用中

单行文本框姓名姓名
  •  此字段的值将被复制到[考勤卡]应用中

单行文本框拼音拼音
单行文本框 账号名称账号名称 
  • 输入kintone的登录ID
    在[考勤卡]应用中进行考勤时,将获取和登录用户的用户ID一直的“账号名称”来作为员工的信息保存到记录。

考勤卡

字段类型字段名称字段代码备注
lookup员工号员工号
  • “要关联的应用”里选中刚才创建的“员工信息”应用。

  • “其他要复制的字段”里选择“姓名”、“账号名称”2个字段。

  • “设置要在搜选列表中显示的字段”里,选择“姓名”和“账号名称”字段。

单行文本框姓名姓名
单行文本框账号名称账号名称
日期日期日期
时间上班上班
  • 取消勾选“将登记记录时的时间作为初始值”

时间下班下班
  • 取消勾选“将登记记录时的时间作为初始值”

数值休息[分]休息
复选框正在休息正在休息
  • 勾选“隐藏字段名称”

  • “项目与顺序”中设置“正在休息”项

日期与时间休息开始时间(用于计算)休息开始
  • 勾选“隐藏字段名称”

  • 取消勾选“将登记记录时的时间作为初始值”

单行文本框备注(流程管理 URL)备注

创建自定义列表

在考勤卡应用里创建一个自定义列表。

设置自定义列表的目的是为了在列表页面放置上下班以及休息等考勤按钮。
关于自定义列表的创建方法请参考“设置列表”。

自定义列表的“HTML”中请输入以下内容。取消勾选“分页显示”。

<!--    
 * 考勤管理 考勤卡    
 * Copyright (c) 2014 Cybozu    
 *    
 * Licensed under the MIT License    
-->    
<title>范例</title>    
<meta charset='utf-8' />

<style type="text/css">    
  body, ul, li {margin:0; padding:0; list-style:none;}
/* 显示日期与时间 */
  div.date-time {
    height:50px;
    padding:10px;
    background:#44810F;
    border-bottom:1px solid #fff;
    color:#fff;
  }
  .year {width:150px; text-align:center; virtical-align:bottom; }
  .date {width:150px; text-align:center;}
  .time {font-size:200%;}
  /* 按钮 */
  div.buttons {
    /*color:#fff;*/
    height:170px;
    padding-top:30px;
    background:#77C434;
  }
  .buttons li {
    float:left;
    width:120px;
    text-align:center;
  }
  .buttons li span{
    display:block;
    background:#3BB111;
    margin:auto;
    font-size:150%;
    color:#ffffff;
    opacity:0.3;
    width:100px;
    height:50px;
    line-height:50px;
    border:1px solid #BDE7A2;
    border-radius:7px;
    box-shadow:1px 1px 4px 0 #4CAA2A, 0px 1px 1px 0 #91D877 inset;
  }
  .buttons li input{
    display:block;
    background:#3BB111;
    margin:auto;
    font-size:150%;
    color:#ffffff;
    opacity:1.0;
    width:100px;
    height:50px;
    line-height:50px;
    border:1px solid #BDE7A2;
    border-radius:7px;
    box-shadow:1px 1px 4px 0 #4CAA2A, 0px 1px 1px 0 #91D877 inset;
  }
</style>
 
<div class='date-time'>    
  <table>    
    <tr>    
      <td class='year' id='year'>2014</td>    
      <td rowspan='2' class='time' id='time'>16:42:03</td>    
    </tr>    
    <tr>    
      <td class='date' id='date'>4/15 Tue</td>    
    </tr>    
  </table>    
</div>    
<div class='buttons'>    
  <ul>    
    <li><span id='syukkin'>上班</span></li>    
    <li><span id='taikin'>下班</span></li>    
    <li><span id='kyuukei'>休息</span></li>    
    <li><span id='fukki'>回归</span></li>    
  </ul>    
</div>

JavaScript自定义

TimeCard.js

是用于显示考勤卡的JavaScript文件。

  • 第48行的CUSTOM_VIEW_ID请根据您自身环境里的自定义列表ID做相应的修改。

/*
 * 考勤管理 考勤卡
 * Copyright (c) 2014 Cybozu
 *
 * Licensed under the MIT License
 */
(function() {
  // 常量、变量的声明、函数的声明
  'use strict';

  // 获取服务器的时间
  function getUTCDateByServer() {
    var r;
    return (r = new XMLHttpRequest())
      ? (r.open('HEAD', '#', false), r.send(null), new Date(r.getResponseHeader('Date'))) : null;
  }

  // 获取指定了长度的字符串的消零位数
  function zero_suppress(s, len) {
    var str = '';
    for (var i = 0; i < len; i += 1) {
      str += '0';
    }
    str += s;
    return str.substr(str.length - len, len);
  }

  // 时钟显示函数
  function set_digitTime() {
    var dt = new Date(getUTCDateByServer());
    var yyyy, MM, dd, hh, mm, ss;
    var DAY = ['SUN', 'MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT'];
    yyyy = dt.getFullYear();
    MM = zero_suppress(dt.getMonth() + 1, 2);
    dd = zero_suppress(dt.getDate(), 2);
    hh = zero_suppress(dt.getHours(), 2);
    mm = zero_suppress(dt.getMinutes(), 2);
    ss = zero_suppress(dt.getSeconds(), 2);

    $('#year')[0].innerHTML = yyyy;
    $('#date')[0].innerHTML = MM + '/' + dd + ' ' + DAY[dt.getDay()];
    $('#time')[0].innerHTML = hh + ':' + mm + ':' + ss;
  }

  // 执行事件句柄
  var events = ['app.record.index.show'];
  kintone.events.on(events, function(event) {
    if (event.viewId !== CUSTOM_VIEW_ID) {
      return;
    }
    // 考勤卡
    // 获取[账号=登录用户]、[日期=今日]的所有记录
    var appId = kintone.app.getId();
    var user = kintone.getLoginUser();
    var qryInfo = '账号名称 = "' + user.code + '" and 日期 = TODAY() order by 上班 desc limit 1';
    var params = {
      app: appId,
      query: qryInfo,
      fields: [
        '员工号',
        '账号名称',
        '日期',
        '上班',
        '下班',
        '正在休息'
      ]
    };
    kintone.api(kintone.api.url('/k/v1/records', true), 'GET', params, function(resp) {

      // resp['records']的长度为0 or 大于等于1时,更改状态
      // 今天的记录条数 = 0
      if (resp.records.length === 0) {
        $('#syukkin').replaceWith("");
        $('#syukkin').on('click', clickFunction);
      } else if (resp.records.length === 1) {
        // 今天的记录条数 >= 1
        if (resp.records[0]['下班'].value === null) {
          if (resp.records[0]['正在休息'].value[0] === '正在休息') {
            $('#fukki').replaceWith("");
            $('#fukki').on('click', clickFunction);
          } else {
            $('#kyuukei').replaceWith("");
            $('#kyuukei').on('click', clickFunction);

            $('#taikin').replaceWith("");
            $('#taikin').on('click', clickFunction);
          }
        } else {
          $('#syukkin').replaceWith("");
          $('#syukkin').on('click', clickFunction);
        }
      }

    });

    // 点击函数
    function clickFunction() {
      var status = $(this).val();
      var id = $(this).attr('id');
      switch (id) {
        case 'syukkin': syukkin_func(); break;
        case 'taikin': taikin_func(); break;
        case 'kyuukei': kyuukei_func(); break;
        case 'fukki': fukki_func(); break;
      }
    }

    // 上班函数
    function syukkin_func() {
      // 获取lookup的复制来源应用中的ID和员工信息
      var applookId = kintone.app.getLookupTargetAppId('员工号');
      var qryInfo = '账号名称= "' + user.code + '" order by 记录编号 desc limit 1';
      var params = {
        app: applookId,
        query: qryInfo,
        fields: [
          '员工号',
          '姓名',
          '账号名称'
        ]
      };
      kintone.api(kintone.api.url('/k/v1/records', true), 'GET', params, function(resp) {
        // 获取员工信息
        var employeeNo = resp.records[0]['员工号'].value;
        var employeeName = resp.records[0]['姓名'].value;
        var employeeAccount = resp.records[0]['账号名称'].value;
        // 获取日期与时间
        var dt = new Date(getUTCDateByServer());
        var yyyy = dt.getFullYear();
        var MM = zero_suppress(dt.getMonth() + 1, 2);
        var dd = zero_suppress(dt.getDate(), 2);
        var hh = zero_suppress(dt.getHours(), 2);
        var mm = zero_suppress(dt.getMinutes(), 2);
        var syukkin_date = yyyy + '-' + MM + '-' + dd;
        var syukkin_time = hh + ':' + mm;

        var body = {
          app: appId,
          record: {
            '员工号': {
              value: employeeNo
            },
            '姓名': {
              value: employeeName
            },
            '账号名称': {
              value: employeeAccount
            },
            '日期': {
              value: syukkin_date
            },
            '上班': {
              value: syukkin_time
            }
          }
        };
        // 在考勤应用中添加记录
        kintone.api(kintone.api.url('/k/v1/record', true), 'POST', body).then(location.reload());
      });
    }

    // 休息函数
    function kyuukei_func() {
      // 获取记录
      var appId = kintone.app.getId();
      var qryInfo = '账号名称 = "' + user.code + '" order by 记录编号 desc limit 1';
      var params = {
        app: appId,
        query: qryInfo,
        fields: ['记录编号']
      };
      kintone.api(kintone.api.url('/k/v1/records', true), 'GET', params, function(resp) {
        // 获取员工信息
        var employeeRecId = resp.records[0]['记录编号'].value;
        // 获取日期与时间
        var dt = new Date(getUTCDateByServer());
        var yyyy = dt.getFullYear();
        var MM = zero_suppress(dt.getMonth() + 1, 2);
        var dd = zero_suppress(dt.getDate(), 2);
        var hh = zero_suppress(dt.getHours(), 2);
        var mm = zero_suppress(dt.getMinutes(), 2);
        var kyuukei_starttime = yyyy + '-' + MM + '-' + dd + 'T' + hh + ':' + mm + ':00+09:00';

        var body = {
          app: appId,
          id: employeeRecId,
          record: {
            '正在休息': {
              value: ['正在休息']
            },
            '休息开始': {
              value: kyuukei_starttime
            }
          }
        };
        // 更新记录
        kintone.api(kintone.api.url('/k/v1/record', true), 'PUT', body).then(location.reload());
      });
    }

    // 回归函数
    function fukki_func() {
      // 获取记录
      var appId = kintone.app.getId();
      var qryInfo = '账号名称 = "' + user.code + '" order by 记录编号 desc limit 1';
      var params = {
        app: appId,
        query: qryInfo,
        fields: [
          '记录编号',
          '休息开始',
          '休息'
        ]
      };

      kintone.api(kintone.api.url('/k/v1/records', true), 'GET', params, function(resp) {
        // 获取员工信息
        var employeeRecId = resp.records[0]['记录编号'].value;
        var kyuukei_starttime = resp.records[0]['休息开始'].value;
        var kyuukei_time_total = resp.records[0]['休息'].value;

        // 获取日期与时间
        var dt = new Date(getUTCDateByServer());
        var yyyy = dt.getFullYear();
        var MM = zero_suppress(dt.getMonth() + 1, 2);
        var dd = zero_suppress(dt.getDate(), 2);
        var hh = zero_suppress(dt.getHours(), 2);
        var mm = zero_suppress(dt.getMinutes(), 2);
        var kyuukei_endtime = yyyy + '-' + MM + '-' + dd + 'T' + hh + ':' + mm + ':00+09:00';

        var ST = new Date(kyuukei_starttime);
        var ET = new Date(kyuukei_endtime);
        var kyuukei_time;
        if (ET - ST !== 0) {
          kyuukei_time = Math.round((ET - ST) / (1000 * 60));
        } else {
          kyuukei_time = 0;
        } // 表示“分”

        kyuukei_time_total = String(Number(kyuukei_time_total) + Number(kyuukei_time)); // 总的休息时间

        var body = {
          app: appId,
          id: employeeRecId,
          record: {
            '正在休息中': {
              value: []
            },
            '休息开始': {
              value: null
            },
            '休息': {
              value: kyuukei_time_total
            }
          }
        };

        // 更新记录
        kintone.api(kintone.api.url('/k/v1/record', true), 'PUT', body).then(location.reload());
      });
    }

    // 下班函数
    function taikin_func() {
      // 获取记录
      var appId = kintone.app.getId();
      var qryInfo = '账号名称 = "' + user.code + '" order by 记录编号 desc limit 1';
      var params = {
        app: appId,
        query: qryInfo,
        fields: [
          '记录编号',
          '日期',
          '上班',
          '休息'
        ]
      };

      kintone.api(kintone.api.url('/k/v1/records', true), 'GET', params, function(resp) {
        // 获取员工信息
        var employeeRecId = resp.records[0]['记录编号'].value;
        var syukkin_day = resp.records[0]['日期'].value;
        var syukkin_time = resp.records[0]['上班'].value;
        var kyuukei_time_total = resp.records[0]['休息'].value;
        // 获取日期与时间
        var dt = new Date(getUTCDateByServer());
        var yyyy = dt.getFullYear();
        var MM = zero_suppress(dt.getMonth() + 1, 2);
        var dd = zero_suppress(dt.getDate(), 2);
        var hh = zero_suppress(dt.getHours(), 2);
        var mm = zero_suppress(dt.getMinutes(), 2);
        var taikin_time = hh + ':' + mm;

        var syukkin_daytime = syukkin_day + 'T' + syukkin_time + ':00+09:00';
        var taikin_daytime = yyyy + '-' + MM + '-' + dd + 'T' + taikin_time + ':00+09:00';
        var ST = new Date(syukkin_daytime);
        var TT = new Date(taikin_daytime);
        var kousoku_time;
        if (TT - ST !== 0) {
          kousoku_time = Math.round((TT - ST) / (1000 * 60));
        } else {
          kousoku_time = 0;
        }
        var jitsudou_time = Number(kousoku_time) - Number(kyuukei_time_total);

        var body = {
          app: appId,
          id: employeeRecId,
          record: {
            '下班': {
              value: taikin_time
            },
            '总上班时间': {
              value: kousoku_time
            },
            '实际上班时间': {
              value: jitsudou_time
            }
          }
        };

        // 更新记录
        console.log('出勤' + syukkin_time);
        console.log('退勤' + taikin_time);
        console.log('总上班时间' + String(kousoku_time));
        console.log('实际上班实际' + String(jitsudou_time));
        kintone.api(kintone.api.url('/k/v1/record', true), 'PUT', body).then(location.reload());
      });
    }

    // 按每1000毫秒重复执行一次set_digitTime()
    set_digitTime();
    setInterval(set_digitTime, 1000);
  });

  // 打开记录编辑页面时,时间字段变为仅可查看
  // 执行时间句柄
  kintone.events.on('app.record.edit.show', function(resp) {
    resp.record['休息开始'].disabled = true;
    return resp;
  });

})();

在“考勤卡”应用里设置自定义。

请将上面代码保存为TimeCard.js,上传到应用设置的“通过JavaScript / CSS自定义”页面。另外还用到了JavaScript库,也需要将其如下链接一起添加到“JavaScript文件(电脑专用)里。

  • jQuery 2.2.0
    https://js.cybozu.com/jquery/2.2.0/jquery.min.js

  • jQuery UI 1.11.4
    https://js.cybozu.com/jqueryui/1.11.4/jquery-ui.min.js

※ jQuery以及jQuery UI 要放在TimeCard.js的前面。

0015ef5882822c14f80d594d0aadcc9

计算“休息时间”的流程

将“休息开始” 时间暂时保存在“日期与时间”字段里,计算 “休息时间”。以下是具体的操作流程。

1. 点击 “休息” 按钮

 → 变成 “正在休息” 状态,“休息开始” 时间保存在日期与时间字段里。

00158dca1c5e29fdfab0853cb028e71

2. 点击 “返回” 按钮

 → 解除 “正在休息” 状态,获取按钮点击时的 “休息结束” 时间和 “休息开始” 时间的时间差,作为 “休息(分)”填到记录里。清除 “休息开始” 时间的值。

00158dca4fbb4ce2a83f2c45caf3a18

补充)不可编辑状态

 → 将编辑页面的“日期与时间” 字段设成不可编辑。

00158dca50a07a78976ddf25cf1e1d2

范例代码解说

a) 点击 “休息” 【获取休息开始时间 → 保存到日期与时间字段里】

以下是点击 “休息” 按钮触发事件后处理的相关代码。

1. 创建用于获取cybozu环境的服务器时间的函数

如果时间以各客户端为准,客户端之间可能会有差异,况且用户可以修改客户端的时间,造成打卡时间不准确。因此我们使用cybozu 环境的服务器时间。

 ~获取服务器时间的函数~

function getUTCDateByServer() {
   var r;
   return (r = new XMLHttpRequest)
   ? (r.open ('HEAD', '#', false), r.send (null), new Date (r.getResponseHeader ('Date'))) : null;
}

2. 点击 “休息” 按钮时执行的函数

点击自定义列表的 “休息” 按钮时,使用登录用户的ID和现在的日期,筛选出记录。然后根据应用ID(appId)和筛选出的记录ID(employeeRecId)进行以下的处理。

 1) 选择 “正在休息” 复选框
 2) 将服务器的日期与时间转换到ISO8601标准的"yyyy-MM-ddThh:mm:00+09:00" 格式,并填到日期与时间字段里。
 ※另外,还会判断前阶段是 “上班之后没有休息”的状态。

 ~ 点击 “休息” 按钮时执行的函数~

    function kyuukei_func() {
      // 获取记录
      var appId = kintone.app.getId();
      var qryInfo = '账号名称 = "' + user.code + '" order by 记录编号 desc limit 1';
      var params = {
        app: appId,
        query: qryInfo,
        fields: ['记录编号']
      };
      kintone.api(kintone.api.url('/k/v1/records', true), 'GET', params, function(resp) {
        // 获取员工信息
        var employeeRecId = resp.records[0]['记录编号'].value;
        // 获取日期与时间
        var dt = new Date(getUTCDateByServer());
        var yyyy = dt.getFullYear();
        var MM = zero_suppress(dt.getMonth() + 1, 2);
        var dd = zero_suppress(dt.getDate(), 2);
        var hh = zero_suppress(dt.getHours(), 2);
        var mm = zero_suppress(dt.getMinutes(), 2);
        var kyuukei_starttime = yyyy + '-' + MM + '-' + dd + 'T' + hh + ':' + mm + ':00+09:00';

        var body = {
          app: appId,
          id: employeeRecId,
          record: {
            '正在休息': {
              value: ['正在休息']
            },
            '休息开始': {
              value: kyuukei_starttime
            }
          }
        };
        // 更新记录
        kintone.api(kintone.api.url('/k/v1/record', true), 'PUT', body).then(location.reload());
      });
    }

}

b) 点击 “回归”  【获取休息结束时间 → 算出「休息结束时间 - 休息开始时间」 → 保存休息时间[分]】

以下是点击 “回归” 按钮触发的事件时执行的处理的代码。比点击 “休息” 按钮时处理的内容更多。

  1) 取消选择 “正在休息” 复选框
  2) 获取现在的 “休息(分)” 的值
  3) 获取 “休息开始” 日期与时间字段的值
  4) 获取现在的服务器时间
  5) 算出这次的休息时间( 4)的值 - 3)的值 )
  6) 2) 的值加上5) 的值,获得的值作为新的值填入到 “休息(分)”里
  7) 清除 “休息开始” 日期与时间字段的值

 function fukki_func() {
      // 获取记录
      var appId = kintone.app.getId();
      var qryInfo = '账号名称 = "' + user.code + '" order by 记录编号 desc limit 1';
      var params = {
        app: appId,
        query: qryInfo,
        fields: [
          '记录编号',
          '休息开始',
          '休息'
        ]
      };

      kintone.api(kintone.api.url('/k/v1/records', true), 'GET', params, function(resp) {
        // 获取员工信息
        var employeeRecId = resp.records[0]['记录编号'].value;
        var kyuukei_starttime = resp.records[0]['休息开始'].value;
        var kyuukei_time_total = resp.records[0]['休息'].value;

        // 获取日期与时间
        var dt = new Date(getUTCDateByServer());
        var yyyy = dt.getFullYear();
        var MM = zero_suppress(dt.getMonth() + 1, 2);
        var dd = zero_suppress(dt.getDate(), 2);
        var hh = zero_suppress(dt.getHours(), 2);
        var mm = zero_suppress(dt.getMinutes(), 2);
        var kyuukei_endtime = yyyy + '-' + MM + '-' + dd + 'T' + hh + ':' + mm + ':00+09:00';

        var ST = new Date(kyuukei_starttime);
        var ET = new Date(kyuukei_endtime);
        var kyuukei_time;
        if (ET - ST !== 0) {
          kyuukei_time = Math.round((ET - ST) / (1000 * 60));
        } else {
          kyuukei_time = 0;
        } // 表示“分”

        kyuukei_time_total = String(Number(kyuukei_time_total) + Number(kyuukei_time)); // 总休息时间

        var body = {
          app: appId,
          id: employeeRecId,
          record: {
            '正在休息': {
              value: []
            },
            '休息开始': {
              value: null
            },
            '休息': {
              value: kyuukei_time_total
            }
          }
        };

        // 更新记录
        kintone.api(kintone.api.url('/k/v1/record', true), 'PUT', body).then(location.reload());
      });
    }

补充) 将 “休息开始”日期与时间字段设置成不可直接编辑

使用JavaScriptAPI 的话,使用登录用户的账号,可以通过 JavaScript API 来操作字段的值。但是,这次不少用户反映希望在点击 “休息” 按钮时能够自动输入日期与时间,而GUI页面上又不可编辑。然而,当前 kintone 标准功能里API操作和GUI操作只能赋予相同的访问权限。因此,需要像下面这样通过修改字段的属性,使得无法编辑GUI上的字段。

  ~ 将 “休息开始” 日期与时间字段的 "disabled" 属性置成 true ~

kintone.events.on('app.record.edit.show', function(resp){    
    resp['record']['休息开始']['disabled'] = true;    
    return resp;    
});

假如结合BPM,还可以应对类似于当状态从“ 申请者 ”→ “上司”时,更加表单中字段可编辑的情况。
就像这样的,只要稍稍自定义以下,就可以弥补标准功能无法实现的功能,可以灵活变通也是kintone的魅力所在啊!

最后

这次,站在销售的立场,发表了这个Tips。正是因为是边学习JavaScript边做销售,所以更加了解实际业务上的需求。光是这个打卡应用,就包含了各种kintoneAPI的知识。在享受 kintone 的编程过程中,也非常享受跟大家的交流,让我们一起活跃在kintone界。

今后也请多多关照。