
Obsidian Thino x Dataview らくらく工数管理
はじめに
Obsidianのコミュニティプラグイン、ThinoとDataviewを活用した工数管理が便利だったので記事にしました。
Obsidian - Obsidian 日本語ヘルプ - Obsidian Publish
Obsidianはマークダウンエディタであり、ナレッジベースアプリでもあります。
運用イメージ
- ThinoでX風の作業ログを記録
プロジェクト毎にタグを打つのがポイント!
- Dataviewで月別に集計した工数を確認
打ったタグ毎の工数と全体の工数を確認可能
設定方法
- デイリーノートの設定
設定 > オプション > コアプラグイン > デイリーノート > 設定 から 新規ファイルの場所 を指定
例)20_daily ※後述するdataviewjsでフォルダを指定しています。
- Thinoをインストール
Githubリポジトリ:Thino
設定 > オプション > コミュニティプラグイン から Thino を検索しインストール
- Dataviewをインストール
Githubリポジトリ:Dataview
設定 > オプション > コミュニティプラグイン から Dataview を検索しインストール
設定 > コミュニティプラグイン > Dataview から JavaScript queries を有効化
- 月別工数サマリーを作成
適当なファイルにコードブロック(「```dataviewjs ```」
)を用意し、その中に以下全文をコピペ
※自分専用に作成したものなので悪しからず🙏
/**
* 月別工数サマリー生成スクリプト
* 指定した月の日次ファイルから工数を集計し、プロジェクト別にテーブル表示する
*/
// ===== 設定 =====
const TARGET_MONTH = "2025-07"; // 対象月(YYYY-MM形式)
// ===== ユーティリティ関数 =====
/**
* 時刻文字列(HH:mm)をミリ秒に変換
* @param {string} time - 時刻文字列(例: "14:30")
* @return {number} ミリ秒
*/
function parseTime(time) {
return Date.parse(`01/01/2000 ${time}`);
}
/**
* ミリ秒を時間文字列(HH:mm)に変換
* @param {number} duration - ミリ秒
* @return {string} 時間文字列(例: "2:30")
*/
function formatDuration(duration) {
const hours = Math.floor(duration / 1000 / 60 / 60);
const minutes = Math.floor(duration / 1000 / 60) % 60;
return `${hours}:${String(minutes).padStart(2, "0")}`;
}
/**
* テキストからハッシュタグを抽出
* @param {string} text - 検索対象のテキスト
* @return {string[]} ハッシュタグの配列
*/
function extractTags(text) {
const tagMatches = text.match(/#[\w/-]+/g);
return tagMatches || [];
}
// ===== タスク処理関数 =====
/**
* タスクテキストから開始時刻と終了時刻を抽出
* @param {Object} task - タスクオブジェクト
* @return {Object} {startTime: number, endTime: number|null}
*/
function extractTaskTimes(task) {
if (!task?.text) return { startTime: null, endTime: null };
// 時刻パターン: "14:30" または "14:30-16:00"
const timePattern = /^(\d+:\d+)(\s*-(\d+:\d+))?/;
const match = task.text.match(timePattern);
if (!match) return { startTime: null, endTime: null };
return {
startTime: parseTime(match[1]),
endTime: match[3] ? parseTime(match[3]) : null
};
}
/**
* 現在のタスクの工数を計算し、durations に追加
* @param {Object} durations - プロジェクト別工数を格納するオブジェクト
* @param {Object} currentTask - 現在のタスク
* @param {Object|null} nextTask - 次のタスク(終了時刻計算用)
*/
function calculateTaskDuration(durations, currentTask, nextTask) {
// タスクの開始時刻・終了時刻を取得
let { startTime, endTime } = extractTaskTimes(currentTask);
// 終了時刻が未指定の場合、次のタスクの開始時刻または24:00を使用
if (!endTime) {
const nextTaskTimes = extractTaskTimes(nextTask);
endTime = nextTaskTimes.startTime || parseTime("24:00");
}
// ハッシュタグ(プロジェクトタグ)を抽出
const tags = extractTags(currentTask.text);
if (tags.length === 0) return; // タグがない場合はスキップ
// 最初のタグをプロジェクトとして使用
const projectTag = tags[0];
// 工数をプロジェクト別に集計
if (!durations[projectTag]) {
durations[projectTag] = 0;
}
durations[projectTag] += endTime - startTime;
}
/**
* 1つの日次ファイルから工数を集計
* @param {Object} file - Dataviewのファイルオブジェクト
* @return {Object} プロジェクト別工数
*/
function processFile(file) {
// 時刻パターンにマッチするタスクのみを抽出
const timePattern = /^(\d+:\d+)(\s*-(\d+:\d+))?/;
const timeTasks = file.file.tasks.filter(task => timePattern.test(task.text));
const fileDurations = {};
// 各タスクの工数を計算
for (let i = 0; i < timeTasks.length; i++) {
const currentTask = timeTasks[i];
const nextTask = i < timeTasks.length - 1 ? timeTasks[i + 1] : null;
calculateTaskDuration(fileDurations, currentTask, nextTask);
}
return fileDurations;
}
// ===== メイン処理 =====
// 指定月の日次ファイルを取得
const dailyFiles = dv.pages('"20_daily"')
.where(file => file.file.name.startsWith(TARGET_MONTH));
// 全プロジェクトの工数を格納するオブジェクト
const allProjectDurations = {};
// 各日次ファイルから工数を集計
dailyFiles.forEach(file => {
const fileDurations = processFile(file);
// ファイル別の工数を全体の集計に追加
Object.entries(fileDurations).forEach(([project, duration]) => {
if (!allProjectDurations[project]) {
allProjectDurations[project] = 0;
}
allProjectDurations[project] += duration;
});
});
// ===== 結果表示 =====
// ページヘッダーを表示
dv.header(2, `${TARGET_MONTH} の工数サマリー`);
// データが存在しない場合の処理
if (Object.keys(allProjectDurations).length === 0) {
dv.paragraph("指定月のデータが見つかりません");
} else {
// プロジェクトを工数の多い順でソート
const sortedProjects = Object.entries(allProjectDurations)
.sort((a, b) => b[1] - a[1]);
// テーブル用のデータを作成(プロジェクト名と工数)
const tableData = sortedProjects.map(([project, duration]) => [
project,
formatDuration(duration)
]);
// 総工数を計算
const totalDuration = Object.values(allProjectDurations)
.reduce((sum, duration) => sum + duration, 0);
// 総工数をテーブルの最下行に追加
tableData.push(["**Total**", `**${formatDuration(totalDuration)}**`]);
// テーブルを表示(ヘッダーにプロジェクト数を表示)
dv.table([`プロジェクト (${sortedProjects.length})`, "工数"], tableData);
}
使用方法
-
Thinoで作業を記録
例)Project1 作業 #man-hour/test/pro1 -
月別工数サマリーを確認
-
タグ毎に工数が表示されていることを確認🎉
まとめ
複数プロジェクトに参画したときは割り込みタスクがあったり、
どの作業にどのくらい時間をかけているかわからなくなりがちだと思います。
Thino x Dataview を活用すれば、Xにポストする感覚で作業ログを残せます。
mdファイルのため、Cursorで読み込んで日報の自動生成にも使えるなぁと思ったり。
余談..
本記事の内容と合わせCustom Framesプラグインを使い、打刻を行うWebページをObsidian内に表示することで、
Thinoの出勤ポストと出勤打刻を同時に実施できるようにしています。
打刻忘れが減ったのでおすすめです👍