SCALE — Build Lab
開発パターン · TYPESCRIPT LIBRARY

レポート通知システム

CATEGORY開発パターン TYPETypeScript Library EFFORT120〜360分 DIFFICULTY
PRIMARY CODE
ts · lib/report-notify.ts
// 日報・作業報告のSlack自動通知ヘルパー
// チャンネル: #作業報告-日報提出_ligl
// 仕様: 1日1スレッドに集約 — 親メッセージ「◯/◯ 日次報告(ユーザー名)」を作り、
// 朝日報・作業開始・作業終了・夜日報をすべてその親メッセージのスレッド返信として投稿する。

export const REPORT_CHANNEL = 'C08MKSMTPNZ';

const threadKey = (userId: string, date: string) => `scale-slack-daily-thread:${userId}:${date}`;

// その日の親メッセージts を取得(無ければ作成して localStorage に保存)
export async function ensureDailyThread(userId: string, userName: string, date: string): Promise<string | null> {
  if (typeof window === 'undefined') return null;
  const k = threadKey(userId, date);
  const cached = localStorage.getItem(k);
  if (cached) return cached;
  // YYYY-MM-DD → M/D
  const m = date.match(/^\d{4}-(\d{2})-(\d{2})$/);
  const mmdd = m ? `${parseInt(m[1], 10)}/${parseInt(m[2], 10)}` : date;
  const title = `:date: *${mmdd} 日次報告(${userName})*`;
  try {
    const res = await fetch('/api/slack-post', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ channel: REPORT_CHANNEL, text: title }),
    });
    const data = await res.json() as { ok?: boolean; ts?: string };
    if (data.ok && data.ts) {
      try { localStorage.setItem(k, data.ts); } catch {}
      return data.ts;
    }
  } catch {}
  return null;
}

export interface PostReportOptions {
  userId?: string;
  userName?: string;
  date?: string; // YYYY-MM-DD(その日の親スレッドに紐づける)
}

export async function postReport(text: string, opts?: PostReportOptions): Promise<boolean> {
  try {
    let thread_ts: string | null = null;
    if (opts?.userId && opts?.userName && opts?.date) {
      thread_ts = await ensureDailyThread(opts.userId, opts.userName, opts.date);
    }
    const res = await fetch('/api/slack-post', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ channel: REPORT_CHANNEL, text, thread_ts: thread_ts || undefined }),
    });
    const data = await res.json();
    return !!data.ok;
  } catch {
    return false;
  }
}

export function fmtHoursMin(minutes: number): string {
  const h = Math.floor(minutes / 60);
  const m = minutes % 60;
  if (h === 0) return `${m}分`;
  if (m === 0) return `${h}時間`;
  return `${h}時間${m}分`;
}

前提条件
Tailwind CSS v4TypeScript 5
USE CASES
  • 週次レポート
  • 日次KPI通知

レポート通知システム

:LiTarget: 用途

日次/週次レポートを自動生成してSlack/メール送信するパターン。

:LiSparkle: 特徴

  • スケジュール実行
  • Slack送信
  • メール送信
  • PDF添付

:LiCode: 実コード(SCALE Base より自動抽出)

:LiInfo: lib/report-notify.ts の中身そのもの。コピペ即可。

// 日報・作業報告のSlack自動通知ヘルパー
// チャンネル: #作業報告-日報提出_ligl
// 仕様: 1日1スレッドに集約 — 親メッセージ「◯/◯ 日次報告(ユーザー名)」を作り、
// 朝日報・作業開始・作業終了・夜日報をすべてその親メッセージのスレッド返信として投稿する。

export const REPORT_CHANNEL = 'C08MKSMTPNZ';

const threadKey = (userId: string, date: string) => `scale-slack-daily-thread:${userId}:${date}`;

// その日の親メッセージts を取得(無ければ作成して localStorage に保存)
export async function ensureDailyThread(userId: string, userName: string, date: string): Promise<string | null> {
  if (typeof window === 'undefined') return null;
  const k = threadKey(userId, date);
  const cached = localStorage.getItem(k);
  if (cached) return cached;
  // YYYY-MM-DD → M/D
  const m = date.match(/^\d{4}-(\d{2})-(\d{2})$/);
  const mmdd = m ? `${parseInt(m[1], 10)}/${parseInt(m[2], 10)}` : date;
  const title = `:date: *${mmdd} 日次報告(${userName})*`;
  try {
    const res = await fetch('/api/slack-post', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ channel: REPORT_CHANNEL, text: title }),
    });
    const data = await res.json() as { ok?: boolean; ts?: string };
    if (data.ok && data.ts) {
      try { localStorage.setItem(k, data.ts); } catch {}
      return data.ts;
    }
  } catch {}
  return null;
}

export interface PostReportOptions {
  userId?: string;
  userName?: string;
  date?: string; // YYYY-MM-DD(その日の親スレッドに紐づける)
}

export async function postReport(text: string, opts?: PostReportOptions): Promise<boolean> {
  try {
    let thread_ts: string | null = null;
    if (opts?.userId && opts?.userName && opts?.date) {
      thread_ts = await ensureDailyThread(opts.userId, opts.userName, opts.date);
    }
    const res = await fetch('/api/slack-post', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ channel: REPORT_CHANNEL, text, thread_ts: thread_ts || undefined }),
    });
    const data = await res.json();
    return !!data.ok;
  } catch {
    return false;
  }
}

export function fmtHoursMin(minutes: number): string {
  const h = Math.floor(minutes / 60);
  const m = minutes % 60;
  if (h === 0) return `${m}分`;
  if (m === 0) return `${h}時間`;
  return `${h}時間${m}分`;
}

:LiFolder: ソースファイルのパス

/Users/oogushiyuuki/Library/CloudStorage/GoogleDrive-y-ogushi@scale-group.co.jp/マイドライブ/AI/scale-base/lib/report-notify.ts

:LiHandPointer: 使い方

対象プロジェクトに該当ファイルをコピーして、props を流し込むだけ。

:LiAlertCircle: 注意事項

  • 依存パッケージを忘れず追加