1. 初めに
勤怠管理システムの全貌としては以下の3つである。3本に分けて残していく。今回はその3、とうとう最終回!
その1:名前を選択して「出勤」or「退勤」ボタンを押し、記録する
その2:勤怠記録のデータを可視化する
その3:過去の勤怠を編集できるようにする <-MOUKOKO
本Qiitaの内容
- LambdaでDynamoDBに過去の勤怠を記録
- LambdaでDynamoDBの直近の記録を削除
- 入力のUI試行錯誤
2. LambdaでDynamoDBに記録
その1で書いたLambdaは開始時にその時間のTimestampを記録して、終了時にその時のTimestampを記録するものだった。今回のコードは、名前と過去働いた日時を入力し、記録するというものなので、大きく異なる。Lambda関数はそこまで変わらないけど。eventの中のjsonは以下のようなものを想定している。
{"body": {
"Name": "Namae",
"Date": "2024-05-02",
"StartTime": "09:12",
"EndTime": "10:00"}
}
このイベントJSONが与えられた時に動くようにLambda関数を書く必要がある。まずはイベントJSONの中にあるものをそれぞれ受け取り、以前のDynamoDBへの記録関数と同じ形で記録できるようにデータを加工する。そこの辺は自由にできると思う。そして、DynamoDBへ書き込む。ロールの付与を忘れずに。
import boto3
import json
from datetime import datetime, timedelta
from decimal import Decimal
def lambda_handler(event, context):
dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('TableName')
# APIからJSON形式でデータを受け取る
body = json.loads(event['body'])
name = body['Name']
date = body['Date']
start_time = body['StartTime']
end_time = body['EndTime']
# 日付と時刻をISO 8601形式に変換する
start_datetime = datetime.strptime(date + ' ' + start_time, '%Y-%m-%d %H:%M')
end_datetime = datetime.strptime(date + ' ' + end_time, '%Y-%m-%d %H:%M')
if end_datetime < start_datetime:
end_datetime += timedelta(days=1)
work_hours = Decimal(str((end_datetime - start_datetime).total_seconds() / 3600.00))
# DynamoDBに書き込むデータを設定
response = table.put_item(
Item={
'Name': name,
'Timestamp': start_datetime.isoformat(),
'EndTime': end_datetime.isoformat(),
'WorkHours': work_hours # Decimal 型で保存
}
)
return {
'statusCode': 200,
'body': json.dumps('Item successfully written to DynamoDB.')
}
3. Lambdaで直近の記録を消す
最後のLambda関数は直近の記録を消すというものである。使う場面は間違えて入力した場合や、退勤し忘れた場合とかに使えると思う。イベントJSONには名前だけがあり、名前を受け取ったLambda関数がその人のDynamoDBにある直近の記録を消す。
responseがその人の最新の項目を探すもので、いろんな制限ができる。
ちなみにDynamoDBのreadとかは結構早く数ミリ秒とかでできるらしい。(Chat GPTによる)全然関係ないけど。
Table.delete _item
で消せる。
import boto3
def lambda_handler(event, context):
dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('TableName')
target_name = event["name"]
# 最新の項目を検索
response = table.query(
KeyConditionExpression=boto3.dynamodb.conditions.Key('Name').eq(target_name),
ScanIndexForward=False, # これにより最新の項目が最初に来る
Limit=1 # 最新の1項目のみを取得
)
if response['Items']:
latest_item = response['Items'][0]
# 項目を削除
delete_response = table.delete_item(
Key={
'Name': latest_item['Name'],
'Timestamp': latest_item['Timestamp']
}
)
return delete_response
else:
return "記録がありません"
4. UI
日付選択とかアラートとかのデフォルトのUIが嫌いなので、その部分も凝りたいと思って色々を探した。結局使うことにしたのが
- 日付選択UI: Flatpickr (
flatpickr.min.css
とflatpickr.js
) - 時間選択UI: ClockPicker (
bootstrap-clockpicker.min.css
とbootstrap-clockpicker.min.js
) - アラートUI: SweetAlert2 (
sweetalert2@11
とsweetalert.min.js
)
たちである。それぞれちょっとだけ説明していく。
全てのUIをimportするために以下のコードをhtmlに書く。
<!-- for FlatPickr -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css">
<script src="https://cdn.jsdelivr.net/npm/flatpickr"></script>
<!-- for ClockPicker -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/clockpicker/0.0.7/bootstrap-clockpicker.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/clockpicker/0.0.7/bootstrap-clockpicker.min.js"></script>
<!-- for SweetAlert2 -->
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<script src="https://unpkg.com/sweetalert/dist/sweetalert.min.js"></script>
これでインポートできる。あとは、それぞれ使うためのJSを書かねばならない。
日付選択UI
flatpickr("#datePicker", {
altInput: true,
altFormat: "F j, Y",
dateFormat: "Y-m-d",
});
時間選択UI
おーーいい!これはすごいいい!使ってみたらわかるけど、ヌルって動く感じがいい!
$('.clockpicker').clockpicker({
donetext: '完了',
twelvehour: false,
autoclose: true,
vibrate: true,
fromnow: 0,
placement: 'bottom',
align: 'left'
});
});
アラートUI
Sweetalert2ってのを使ってるんだけど、すごい面白いアラート。
詳しくはこのHPを見てほしんだけど、注意しないといけないのが、Sweeralert1とSweetalert2があるらしく、若干ネットにことがってる情報では混在しているので、エラーが出たらちゃんとHPで確認しよう。これに関してはChat GPTに聞いてもうまく直らなかった。
使い方としては、JSの中でアラートを出したいタイミングで、showToast(message, type)
としてアラートを実行させる。mesageにはアラートに表示したいメッセージをtypeにはどのアラートの種類で出したいをかく。
結構いけてるアラートだから全HPのアラートこれにするべきだと思う。
function showToast(message, type) {
let icon;
let title;
switch (type) {
case 'success':
icon = 'success';
title = 'Success';
break;
case 'error':
icon = 'error';
title = 'Error';
break;
case 'info':
icon = 'info';
title = 'Information';
break;
default:
icon = 'error';
title = 'error';
break;
}
swal({
title: title,
text: message,
icon: icon,
buttons: {
confirm: {
text: 'OK',
className: 'btn btn-success'
}
},
buttonsStyling: true,
timer: 5000,
timerProgressBar: true
});
}
5. 最後に
流石にこれでできたでしょ!っと思って、メンバーに見せたところ導入された!やったー!
またここで、一つ問題が、実はチームの半分が日本にいない。Timestampとかの管理が若干めんどくさくなるかもと思った。特にその2で実装したカレンダー形式の表示の時差をどこに合わせるかとか問題が出てきた。
ただ、あんまり機能として重要じゃないかつ流石に勤怠管理作るのに飽きてきたので、また気が向いたらこの部分も対処していこうと思ふ。
次回:何しようかな