どうも、業務改善が趣味な筆者です。
これまでに、MFクラウド請求書APIとExmentを組み合わせて、簡易SFA/CRM的なことをやってみました。
今回は、さらに発展させて、簡易原価計算的なことをやってみたいと思います。
今回の記事で想定する原価計算
- 筆者の業務はサービス業(人件費=原価)
- 時給×3を原価とする
- 受注した見積書の金額が、業務にかけた時間×時給×3を超えると赤字
- 超えなければ黒字
非常にシンプルですが、サービス業なんてどこもこんな感じではないでしょうか。
これをベースに、各案件が赤字になっていないかどうか、黒字であれば、粗利率はどうなのか、というのがわかる原価計算システムを作ってみたいと思います。
toggl is 何?
togglは、工数管理のSaaSサービスです。Qiitaにもタグがあるぐらいには、日本のIT業界で知名度があると思います。
筆者が以前勤めていた企業では、togglを日報の代わりに利用しており、togglさえつけていれば、手書き(やメール・チャット等の)日報報告は不要とされていました。
togglには日報以外の側面もあり、部署やプロジェクト単位での総勤務時間が明らかになります。また、課金プランのみですが、各ユーザーごとに給与単価を設定することもできます。したがって、togglのダッシュボードを見ているだけで、どのプロジェクトがどれぐらい原価使っているかがひと目で分かるわけです。
余談ですが、筆者はこのtogglの各作業に対してタグづけ(企画立案・進行管理・デザイン・静的コーディング・システム組み込み)をし、各工程がどれぐらい時間をとっているのか、どれぐらい給与もらっている人がどれぐらいの時間で仕事を片付けているのかを見る業務をしていました。
ただ、その時はダッシュボードの内容をCSVで書き出してスプレッドシートで加工するという、完全には自動化されていない作業でした。また、会計システムとも連動していないため、最終的には会計システムから抽出した見積書データを手で入力するという残念なものでもありました。
今回は、APIとtogglの対となる集計システムにExmentを採用し、さらにその先にはMFクラウド請求書があるという前提で、原価計算をしてみたいと思います。
Exmentで下準備をする
まず、自分の給与単価を決めます。筆者は副業をしているので、そのプロジェクトを前提に考えますが、もし複数の社員がいても応用できる仕組みを前提として説明します。
自分(社員)の給与単価を設定する
今回はtoggl無課金で利用することを前提としますので、給与単価はExment側に持たせます。どこに持たせようかという話ですが、取りあえずユーザーカスタムテーブルに「給与単価」という列を追加しました。
ここで入力する値は、給与の時給単価×3の値としています。給与額そのまま入れるか、今回のように係数をかけた値を入れるのかどうかは、各自の判断で決めてください。
工数(タイムレコード)が入るカスタムテーブルを作成する
今回は「工数管理」という名前でカスタムテーブルを作成しました。
列名 | 列のタイプ |
---|---|
作業者 | ユーザー |
作業開始時刻 | 日付と時刻 |
作業終了時刻 | 日付と時刻 |
作業時間 | 整数 |
案件名 | 選択肢 (他のテーブルの値一覧から選択) |
クライアント | 選択肢 (他のテーブルの値一覧から選択) |
案件名とクライアントは、それぞれ別のカスタムテーブルから選択することとし、リレーションを結んでおきます。
まっさらなtogglアカウントを用意する
今回は記事を書く上でわかりやすいように、新しいtogglアカウントを取得するところから始めます。
togglに初回ログインするとこんな感じです。
ここから、APIの設定を行います。
画面左下にある自分のプロフィール画面にアクセスします。開いた画面の下のほうに、APIトークンが表示されているはずです。
このAPIトークンを控えておきます。
API経由でのアクセスをテストしておく
togglはcurlが好きなのか、APIドキュメントでやたらcurlの例を出してくるのですが、個人的にはcurlのオプションが覚えきれないので、Postmanを使用します。
toggl APIの認証はちょっと変わっていて、Basic認証です。HeaderのAuthorizationに、Basic "base64エンコーディングされたtoken文字列:api_token"というヘンテコな認証を掛ける必要があります。そのトークンの使い方合ってんの? っていう。
まあ、いいです。base64エンコーディングが必要なので、こちらのサービスなどを利用して、エンコーディングしましょう。
再度言いますが、「xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx:api_token」(xはトークン文字列)で、base64エンコーディングです。
エンコーディングができたら、Postmanに設定してテストします。
togglのAPIは2系統に分かれていて、データのCRUDをするToggl APIと、Readを中心にするReport APIに分かれています。今回はとりあえず、Read APIのエンドポイントを叩いています。エンドポイントは、こちらのドキュメントを参照してください。
また、パラメータは2つ必須になっています。user_agent
は、今回作るアプリの名前です。適当につけてOKです。workspace_idは、Togglにログインして設定するWorkspaceの番号です。URLから採取できます。
とりあえず上図のように設定して、レスポンスが以下のように帰ってこれば成功です。
{
"total_grand": null,
"total_billable": null,
"total_currencies": [],
"data": []
}
Toggl API経由で、顧客情報をExmentから登録する
まず、まっさらのTogglに顧客情報を登録します。Togglで登録できるか、一度テストしておきましょう。Postmanもいいですが、実際にコードを書いてテストします。筆者はJavaScriptに慣れ親しんでいるため、今回もNode.jsで書きます。
const axios = require('axios')
//create clients
createClientsEndpoint = 'https://api.track.toggl.com/api/v8/clients'
const payload = {
client: {
name: 'テストクライアント株式会社',
wid: '00000000' //自分のworkspaceidを設定
}
}
axios.post(createClientsEndpoint, payload, {
headers: {
'Authorization': 'Basic xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' //base64エンコーディングされたAPIトークン
}
})
.then(res => {
console.log(res)
})
.catch(err => {
console.log(err)
})
上記node.jsファイルを実行して、以下のレスポンスが帰ってこれば成功です。
{
"id":00000000,
"wid":0000000,
"name":"テストクライアント株式会社"
}
また、togglのClientsページを確認して、クライアントが登録されていることを確認しましょう。
それでは、いよいよ、Exmentから顧客データを全部引っ張り出して、togglに突っ込むプログラムを書きます。
const axios = require('axios')
const fs = require('fs')
const _ = require('lodash')
// get clients data
clientDataEndpoint = 'https://example.com/api/data/clients'
const exmentToken = fs.readFileSync('./exment_tokens.txt')
;(async () => {
let clients = await axios.get(clientDataEndpoint, {
headers: {
'Authorization': 'Bearer ' + JSON.parse(exmentToken).access_token
}
})
.then(res => {
// console.log(res.data.data)
return res.data.data
})
.catch(err => {
console.log(err)
})
clients = _.map(clients, 'value')
clients = _.map(clients, 'name')
//create clients
createClientsEndpoint = 'https://api.track.toggl.com/api/v8/clients'
clients.forEach(item => {
const payload = {
client: {
name: item,
wid: '4660619'
}
}
axios.post(createClientsEndpoint, payload, {
headers: {
'Authorization': 'Basic xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
}
})
.then(res => {
console.log(res)
})
.catch(err => {
console.log(err)
})
})
})()
Exmentのトークンは、外部ファイルから引っ張ってきています。多少冗長な感じもしますが、一旦1回きりの処理ということでお許しください。
無事に、全クライアントが挿入されました。
取引停止はいらなさそうですね。後で削除しておきます。
Toggl API経由で、プロジェクト(案件)情報をExmentから登録する
似た要領で、今度はExmentから案件の情報を抽出してTogglに登録します。受注した案件のみ登録するようにしましょう。
取りあえず、抽出までのロジックはこんな感じです。
const axios = require('axios')
const fs = require('fs')
const projectsDataEndpoint = 'https://example.com/api/data/projects'
const exmentToken = fs.readFileSync('./exment_tokens.txt')
;(async () => {
let clients = await axios.get(projectsDataEndpoint, {
headers: {
'Authorization': 'Bearer ' + JSON.parse(exmentToken).access_token
}
})
.then(res => {
console.log(res.data.data)
return res.data.data
})
.catch(err => {
console.log(err)
})
})()
とりあえず一旦は案件情報全部取れましたね。
後考えないといけないことは、ここから
- Togglに登録する形にオブジェクトを整形する
- Togglのクライアント番号と、Exmentのクライアント番号を紐付ける
の2つのロジックが必要です。
以下、双方のクライアント番号紐づけのロジックです
let exmentClientsArray = []
exmentClients.forEach(item => {
exmentClientsArray.push({
id: item.id,
name: item.value.name
})
})
// console.log(exmentClientsArray)
let togglClients = await axios.get(clientsDataTogglEndpoint, {
headers: {
'Authorization': 'Basic xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
}
})
.then(res => {
return res.data
})
let togglClientsArray = []
togglClients.forEach(item => {
togglClientsArray.push({
id: item.id,
name: item.name
})
})
// console.log(togglClientsArray)
let symmetricClientsId = []
for(item of togglClientsArray) {
const exmentId = _.find(exmentClientsArray, { name: item.name }).id
// console.log(exmentId)
symmetricClientsId.push({
togglId: item.id,
exmentId: exmentId
})
}
console.log(symmetricClientsId)
それぞれ、名前とidのオブジェクトが入った配列を作成し、lodashのfindメソッドで名前を検索して、相対するid同士をオブジェクトにし、さらに配列にしています。(エンドポイント等は冒頭で変数宣言していますので、省略しています。適宜読み替えてください)
console.logの結果はこうです。
[
{ togglId: 49984659, exmentId: 7 },
{ togglId: 49984658, exmentId: 8 },
{ togglId: 49984657, exmentId: 6 },
{ togglId: 49984656, exmentId: 3 },
{ togglId: 49984655, exmentId: 5 },
{ togglId: 49984654, exmentId: 4 },
{ togglId: 49984653, exmentId: 2 },
{ togglId: 49984652, exmentId: 1 }
]
次に、Togglへ突っ込む用の成形です。
projects = _.map(projects, 'value')
projects = _.filter(projects, { reliability: '6' }) // フラグ6、つまり受注のみ抽出
let projectsArray = []
projects.forEach(item => {
const name = item.name
const exmentClientId = item.client
projectsArray.push({
name: name,
client: exmentClientId
})
})
let payload = []
projectsArray.forEach(item => {
const name = item.name
const cid = _.find(symmetricClientsId, { exmentId: Number(item.client) }).togglId
payload.push({
project: {
name: name,
cid: cid,
wid: 0000000
}
})
})
console.log(payload)
これで、console.logの結果は
[
{
name: 'xxxxxx 特設ページ、製品ページ制作',
cid: 49984654,
wid: 0000000
},
{ name: 'xxxxx 公式サイト制作', cid: 49984659, wid: '4660619' },
{
name: 'xxxxxx コーディングデザイン',
cid: 49984659,
wid: 0000000
},
]
こんな感じです。
後は回しておしまいなので、ここでいったん全部の処理を張り付けておきます。
const axios = require('axios')
const fs = require('fs')
const _ = require('lodash')
const clientsDataEndpoint = 'https://example.com/api/data/clients'
const clientsDataTogglEndpoint = 'https://api.track.toggl.com/api/v8/workspaces/0000000/clients'
const projectsDataEndpoint = 'https://example.com/api/data/projects'
const projectsCreateEndpoint = 'https://api.track.toggl.com/api/v8/projects'
const exmentToken = fs.readFileSync('./exment_tokens.txt')
;(async () => {
let projects = await axios.get(projectsDataEndpoint, {
headers: {
'Authorization': 'Bearer ' + JSON.parse(exmentToken).access_token
}
})
.then(res => {
// console.log(res.data.data)
return res.data.data
})
.catch(err => {
console.log(err)
})
let exmentClients = await axios.get(clientsDataEndpoint, {
headers: {
'Authorization': 'Bearer ' + JSON.parse(exmentToken).access_token
}
})
.then(res => {
return res.data.data
})
let exmentClientsArray = []
exmentClients.forEach(item => {
exmentClientsArray.push({
id: item.id,
name: item.value.name
})
})
// console.log(exmentClientsArray)
let togglClients = await axios.get(clientsDataTogglEndpoint, {
headers: {
'Authorization': 'Basic xxxxxxxxxxxxxxxxx'
}
})
.then(res => {
return res.data
})
let togglClientsArray = []
togglClients.forEach(item => {
togglClientsArray.push({
id: item.id,
name: item.name
})
})
// console.log(togglClientsArray)
let symmetricClientsId = []
for(item of togglClientsArray) {
const exmentId = _.find(exmentClientsArray, { name: item.name }).id
// console.log(exmentId)
symmetricClientsId.push({
togglId: item.id,
exmentId: exmentId
})
}
// console.log(symmetricClientsId)
projects = _.map(projects, 'value')
projects = _.filter(projects, { reliability: '6' }) // フラグ6、つまり受注のみ抽出
let projectsArray = []
projects.forEach(item => {
const name = item.name
const exmentClientId = item.client
projectsArray.push({
name: name,
client: exmentClientId
})
})
let payload = []
projectsArray.forEach(item => {
const name = item.name
const cid = _.find(symmetricClientsId, { exmentId: Number(item.client) }).togglId
payload.push({
project: {
name: name,
cid: cid,
wid: 0000000
}
})
})
// console.log(payload)
payload.forEach(item => {
axios.post(projectsCreateEndpoint, item, {
headers: {
'Authorization': 'Basic xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
}
})
.then(res => {
console.log(res)
})
.catch(err => {
console.log(err)
})
})
})()
TogglのProjectsのページを確認して、プロジェクト(案件)情報がきちんと差し込まれたことを確認します。
はい、案件が全部挿入され、クライアントとも紐づいていますね。筆者は副業なのでプロジェクト数もささやかなものですが、実際の企業では大量のデータが挿入され、プログラムで挿入するメリットが生かせると思います。
Togglをつけることを習慣化する
さて、今回はまっさらなTogglからスタートしているので、Togglをつけることを習慣化しないといけませんね。
Togglには、Chrome拡張やGmailとの連動拡張、スマホアプリにデスクトップアプリと、入力の選択肢はたくさんあります。15分以上かかる作業は、必ずTogglにつけるクセをつけるといいでしょう。
Togglは作業開始時にスタートボタンを押し、終了時にストップボタンを押す方式と、後からまとめて作業時間を登録する2方式でデータを登録できます。お好みのほうでどうぞ(筆者は、作業ごとにボタンを押す方式が好きでした)
Togglに工数データ挿入する
とはいえ、この記事も進めないといけないので、Togglにデータを挿入します。覚えている範囲で、過去、何月何日の何時ぐらいから作業したかなあ、ということを思い出しながら入力しましょう。この作業はToggl管理画面から行います。
一般の企業では、複数人で入力することになりますから、一気にデータが溜まっていきます。みんなの作業が一旦可視化されるので、面白いですよ。
ここまでくれば、利益判断できるフェーズまであと1歩です!
Togglに入力した工数データをAPI経由で取得し、ExmentにAPI経由で突っ込む
Togglに管理画面から工数データの入力が出来たら、今度はAPI経由で取得できるか確認してみましょう。
ソースコードは以下です。
const axios = require('axios')
//工数データを取得
const endpoint = 'https://api.track.toggl.com/api/v8/time_entries'
const start_date_param = encodeURIComponent('2017-01-01T00:00:00+09:00')
const end_date_param = encodeURIComponent('2020-09-14T00:00:00+09:00')
const query = `?start_date=${start_date_param}&end_date=${end_date_param}`
axios.get(endpoint + query, {
headers: {
'Authorization': 'Basic xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
}
})
.then(res => {
console.log(res.data)
})
.catch(err => {
console.error(err)
})
正しく取得できれば、レスポンスは以下のような感じになっているはずです。
[
{
id: 1688770474,
guid: '15300aa902507be5d98a685ea9b9c121',
wid: xxxxxxx,
pid: 163437641,
billable: false,
start: '2019-01-22T05:00:00+00:00',
stop: '2019-01-22T07:00:00+00:00',
duration: 7200,
description: 'ホームページ講習会',
duronly: false,
at: '2020-09-11T09:56:05+00:00',
uid: 6121167
},
{
id: 1688769563,
guid: '21ff64bea50aeb0f7c12b4d5178c4ba2',
wid: xxxxxxx,
pid: 163437643,
billable: false,
start: '2019-05-22T12:00:00+00:00',
stop: '2019-05-22T14:00:00+00:00',
duration: 7200,
description: 'コーディング修正',
duronly: false,
at: '2020-09-11T09:55:24+00:00',
uid: 6121167
},
{
id: 1690456743,
guid: '3b297fdb6d01b00efd13ee46306b5121',
wid: xxxxxxx,
pid: 163437646,
billable: false,
start: '2019-09-24T13:30:00+00:00',
stop: '2019-09-24T14:00:00+00:00',
duration: 1800,
description: '入稿作業',
duronly: false,
at: '2020-09-14T02:10:27+00:00',
uid: 6121167
},
]
実際にはもっと工数データが続きましたが、とりあえず3件だけ紹介しました。
このデータをExmentのAPI形式に整形して、登録します。
最終的なソースコードは以下です。
const fs = require('fs')
const axios = require('axios')
const _ = require('lodash')
const moment = require('moment')
//工数データを取得
const exmentToken = JSON.parse(fs.readFileSync('./exment_tokens.txt')).access_token
const togglEndpoint = 'https://api.track.toggl.com/api/v8/time_entries'
const start_date_param = encodeURIComponent('2017-01-01T00:00:00+09:00')
const end_date_param = encodeURIComponent('2020-09-14T00:00:00+09:00')
const query = `?start_date=${start_date_param}&end_date=${end_date_param}`
const exmentEndpoit = 'https://example.com/api/data/manhours'
const togglClientsEndpoint = 'https://api.track.toggl.com/api/v8/workspaces/4660619/clients'
const exmentClientsEndpoint = 'https://example.com/api/data/clients'
const togglProjectsEndpoint = 'https://api.track.toggl.com/api/v8/workspaces/4660619/projects'
const exmentProjectsEndpoint = 'https://example.com/api/data/projects/query-column?q=reliability eq 6'
const exmentManhoursEndpoint = 'https://example.com/api/data/manhours/'
;(async() => {
let manhours = await axios.get(togglEndpoint + query, {
headers: {
'Authorization': 'Basic xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
}
})
.then(res => {
console.log(res.data)
return res.data
})
.catch(err => {
console.error(err)
})
let togglClients = await axios.get(togglClientsEndpoint, {
headers: {
'Authorization': 'Basic xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
}
})
.then(res => {
// console.log(res.data)
return res.data
})
.catch(err => {
console.err(err)
})
let exmentClients = await axios.get(exmentClientsEndpoint, {
headers: {
'Authorization': 'Bearer ' + exmentToken
}
})
.then(res => {
// console.log(res.data)
return res.data.data
})
.catch(err => {
console.err(err)
})
let clientsArray = []
for(item of exmentClients) {
const name = item.value.name
const exmentId = item.id
clientsArray.push({
name: name,
exmentId: exmentId
})
}
for(item of clientsArray) {
_.remove(clientsArray, obj => obj.name === '取引停止')
const togglId = _.find(togglClients, { name: item.name }).id
// console.log(togglId)
if(togglId) {
item.togglId = togglId
}
}
// TogglとExmentの案件情報を取得
let togglProjects = await axios.get(togglProjectsEndpoint, {
headers: {
'Authorization': 'Basic xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
}
})
.then(res => {
// console.log(res.data)
return res.data
})
.catch(err => {
console.error(err)
})
let exmentProjects = await axios.get(exmentProjectsEndpoint, {
headers: {
'Authorization': 'Bearer ' + exmentToken
}
})
.then(res => {
// console.log(res.data.data)
return res.data.data
})
.catch(err => {
console.error(err)
})
let projectsArray = []
for(item of exmentProjects) {
const name = item.value.name
const exmentId = item.id
const togglId = _.find(togglProjects, { name: item.value.name }).id
projectsArray.push({
name: name,
exmentId: exmentId,
togglId: togglId,
exmentClientId: Number(item.value.client)
})
}
let payload = []
manhours.forEach(item => {
payload.push({
value: {
work_title: item.description,
user: 1, // 今回は自分なのでハードコーディング。複数名いる場合は、ユーザーの紐づけデータも作ってください
start_at: String(moment(item.start).format('YYYY-MM-DD HH:mm:ss')),
end_at: String(moment(item.stop).format('YYYY-MM-DD HH:mm:ss')),
duration: item.duration,
project: String(_.find(projectsArray, { togglId: item.pid }).exmentId),
client: String(_.find(projectsArray, { togglId: item.pid }).exmentClientId)
}
})
})
// console.log(payload)
await axios.post(exmentManhoursEndpoint, { data: payload }, {
headers: {
'Authorization': 'Bearer ' + exmentToken
}
})
.then(res => {
// console.log(res.data)
})
.catch(err => {
console.error(err.response.data.errors)
})
})()
これで、無事データを投入することができました。
以下のようになっていれば成功です。
ExmentのAPIからデータを引き出し、フロントでグラフ化する
Exmentの計算機能で予実管理できればいいのですが、残念ながらそこまで高度な計算式はExmentで作れません。
そこで、フロントからAPIを読み出し、各データからグラフ化してみます。
本来であれば、何がしかのフロントフレームワークを使うところで、筆者はVueに慣れているので、Vue-Cliで環境を作ろうかとも思ったのですが、Qiitaの記事のためだけにVueSFCの環境を作るのも負荷大きいなと思ったので、今回はミニマムで行きます。
npm i express pug
HTTPサーバーを立ててHTMLをサーブするために、Expressをインストールします。筆者はpugに慣れていて、viewはpugを使いたいので、pugもインストールします。
フロントをレンダリングするNode.jsは以下です。
const express = require('express')
const app = express()
const fs = require('fs')
const axios = require('axios')
const _ = require('lodash')
const moment = require('moment')
require('moment-duration-format')
app.set('view engine', 'pug') // Pugの設定
let graph = ''
;(async () => {
const exmentToken = JSON.parse(await fs.readFileSync('./exment_tokens.txt')).access_token
let projectsData = await axios.get('https://example.com/api/data/projects/query-column?q=reliability eq 6', {
headers: {
'Authorization': 'Bearer ' + exmentToken
}
})
.then(res => { return res.data.data })
.catch(err => { console.error(err) })
let clientsData = await axios.get('https://example.com/api/data/clients', {
headers: {
'Authorization': 'Bearer ' + exmentToken
}
})
.then(res => { return res.data.data })
.catch(err => { console.error(err) })
let clientsArray = []
clientsData.forEach(item => {
clientsArray.push({
id: item.id,
name: item.value.name
})
})
let manhoursData = await axios.get('https://example.com/api/data/manhours', {
headers: {
'Authorization': 'Bearer ' + exmentToken
}
})
.then(res => { return res.data.data })
.catch(err => { console.error(err) })
const user = await axios.get('https://example.com/api/data/user/query-column?q=id eq 1', {
// 自分の給与単価を抽出するため、IDはハードコーディング
// 複数ユーザーがいる場合は、別途給与テーブルのオブジェクトを作る必要あり
headers: {
'Authorization': 'Bearer ' + exmentToken
}
})
.then(res => { return res.data.data })
.catch(err => { console.error(err) })
const saraly = user[0].value.saraly
// console.log(saraly)
let projectsArray = []
projectsData.forEach(item => {
const id = item.id
const title = item.value.name
const amount = item.value.amount
const client = _.find(clientsArray, { id: Number(item.value.client) }).name
const manhour = () => {
let total = _.filter(_.map(manhoursData, 'value'), { project: String(item.id) })
// console.log(total)
total = _.sumBy(total, 'duration')
return moment.duration(total, 's').asHours() * saraly
}
projectsArray.push({
id: id,
title: title,
client: client,
amount: Number(amount),
manhourTotal: manhour()
})
})
// console.log(manhoursData)
// console.log(projectsArray)
let graphProjectName = ['案件名']
for(item of projectsArray) {
graphProjectName.push(item.title + '/' + item.client)
}
let graphAmounts = ['受注金額']
for(item of projectsArray) {
graphAmounts.push(item.amount)
}
let graphManhours = ['制作原価']
for(item of projectsArray) {
graphManhours.push(item.manhourTotal)
}
const graphData = {
types: {
'受注金額': 'bar',
'制作原価': 'bar'
},
columns: [
graphAmounts, graphManhours, graphProjectName,
],
colors: {
'受注金額': '#f44336',
'制作原価': '#03a9f4'
},
x: '案件名'
}
console.log(graphData)
graph = JSON.stringify(graphData)
})()
app.get('/', (req, res) => {
res.render('index', { graph })
})
app.listen(3000);
案件、クライアント、工数、ユーザーをそれぞれAPIから取得してコネコネしています。フロントでグラフライブラリをc3.jsを使う前提にしているので、c3.jsのデータフォーマットに整形しています。
最後に、フロントのコードです。なんてことはないですね。Expressで、バックエンドでコネコネしたデータを露出させて、c3.jsで読み込んでいます。d3.jsとc3.jsは、ご覧の通りCDNから読み込んでいます。
ちなみに、c3のバージョンが0.7.20は、d3.jsのバージョンが5系でないと通りませんでした。バージョンの相性が悪く、小1位時間ぐらいハマりました… 他のグラフライブラリのほうが良かったかも…
<!DOCTYPE html>
html(lang="ja")
head
meta(charset="UTF-8")
meta(name="viewport", content="width=device-width, initial-scale=1.0")
title 制作原価採算分岐
.
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/c3/0.7.20/c3.min.css" integrity="sha512-cznfNokevSG7QPA5dZepud8taylLdvgr0lDqw/FEZIhluFsSwyvS81CMnRdrNSKwbsmc43LtRd2/WMQV+Z85AQ==" crossorigin="anonymous" />
body
h1 制作原価採算分岐
p: strong 赤字に注意!
#chart
.
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.15.1/d3.min.js" integrity="sha512-VcfmBa1zrzVT5htmBM63lMjDtqe4SAcxAlVLpQmBpUoO9beX5iNTKLGRWDuJ5F37jJZotqq65u00EZSVhJuikw==" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/c3/0.7.20/c3.min.js" integrity="sha512-+IpCthlNahOuERYUSnKFjzjdKXIbJ/7Dd6xvUp+7bEw0Jp2dg6tluyxLs+zq9BMzZgrLv8886T4cBSqnKiVgUw==" crossorigin="anonymous"></script>
script.
var chart = c3.generate({
bindto: '#chart',
size: {
height: 800,
width: 1200,
},
padding: {
top: 20, right: 50, bottom: 20, left: 70
},
data: !{ graph },
axis: {
rotated: true,
x: {
type: 'category',
},
y: {
tick: {
format: function (d) { return new Intl.NumberFormat("ja").format(d); }書式
},
}
},
legend: {
position: 'right'
},
});
結果、このようになりました。
X軸のラベルが不自然ですが、まあ、マウスオーバーすれば案件名が分かるということで、ここはひとつ次回以降の課題としておきます。
このグラフでは、3つ目の案件だけ、わずかに赤字になっています。たかだか500円程度の売上ではありますが、気を付けないといけないですね。
また、これが企業であれば、案件担当者に対してアラートをかけたりすることもできます。日ごろからこのグラフを見て、チームの進捗と予算の進捗が見合っているかを睨めっこすることも大切ですね。
まとめ
いかがでしたでしょうか。Togglの使い方と、Exmentの連携をご理解いただけたかと思います。
APIを叩くので、どうしてもSQLを発行したりする直感的な操作にはならず、lodashのお世話になることも多いのですが、力業でねじ伏せる感じになるのがAPIマッシュアップ流ですね。
今回、最終的なグラフデータを出力するまでNode.jsでやりましたが、React/Vue/Angularなどのフレームワークから直接APIを叩いてグラフを生成するのもいいかもしれません。
また、今回Exmentにインポートをしましたが、この処理だけするのであれば、NoSQLなデータストアを使ったり、Google スプレッドシート + GASでもいいかもしれません。筆者はExmentを採算管理だけでなく、中小企業の基幹システムちっくに使えないかと考えているので、あえてExmentを選びました。もう少しExment側で計算の仕組みがリッチになると良いのですが…
とはいえ、これは試験的なものなので、今回は1回きりのデータインポート前提となりました。これをブラッシュアップして、定期的にデータを取り込んでアップデートしたり新規追加をする前提の処理にすれば、リアルタイムに採算分岐点を追うこともできます。
PMやディレクターから「こんな安い案件に、どんだけ時間かけとるんや!」って怒られる前に、これを見てテキパキ仕事を終わらせたいものですね。