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

カレンダーデータ管理

CATEGORY開発パターン TYPETypeScript Library EFFORT120〜360分 DIFFICULTY
PRIMARY CODE
ts · lib/calendar-data.ts
// SCALE Base カレンダー連携データ層
// 目的: 大串・細川の毎朝のGoogleカレンダー記入漏れをなくし、SCALE Base内で予定を見える化する

export interface CalendarMember {
  id: string;
  name: string;
  avatar: string;
  color: string;
  googleCalendarEmbedUrl?: string; // 埋め込み用(将来)
  googleCalendarEmail?: string;
}

export interface CalendarEntry {
  id: string;
  memberId: string;
  memberName: string;
  date: string;      // YYYY-MM-DD
  startTime: string; // HH:MM
  endTime: string;   // HH:MM
  title: string;
  category: 'work'|'meeting'|'break'|'travel'|'personal'|'focus';
  note: string;
  location: string;
  syncedToGoogle: boolean;
  createdAt: string;
}

const KEY_ENTRIES = 'scale-calendar-entries';

// SCALE Baseメンバー(カレンダー対象)
export const CALENDAR_MEMBERS: CalendarMember[] = [
  { id: '1', name: '大串', avatar: '大', color: '#6485b8', googleCalendarEmail: 'y-ogushi@scale-group.co.jp' },
  { id: '4', name: '細川',     avatar: '細', color: '#6aa57a', googleCalendarEmail: 'hosokawa@scale-group.co.jp' },
];

export const CATEGORY_INFO: Record<CalendarEntry['category'], { label: string; color: string; icon: string }> = {
  work:     { label: '作業',   color: '#6485b8', icon: '💻' },
  meeting:  { label: 'MTG',    color: '#9078ad', icon: '🤝' },
  break:    { label: '休憩',   color: '#63636e', icon: '☕' },
  travel:   { label: '移動',   color: '#a89352', icon: '🚗' },
  personal: { label: '個人',   color: '#6395a3', icon: '👤' },
  focus:    { label: '集中',   color: '#6aa57a', icon: '🎯' },
};

export function loadEntries(): CalendarEntry[] {
  if (typeof window === 'undefined') return [];
  try { const v = localStorage.getItem(KEY_ENTRIES); return v ? JSON.parse(v) : []; } catch { return []; }
}
export function saveEntries(d: CalendarEntry[]) {
  if (typeof window === 'undefined') return;
  localStorage.setItem(KEY_ENTRIES, JSON.stringify(d));
}
export function addEntry(e: CalendarEntry) {
  const all = loadEntries();
  saveEntries([...all, e]);
}
export function updateEntry(id: string, patch: Partial<CalendarEntry>) {
  const all = loadEntries();
  saveEntries(all.map(e => e.id === id ? { ...e, ...patch } : e));
}
export function deleteEntry(id: string) {
  const all = loadEntries();
  saveEntries(all.filter(e => e.id !== id));
}

export function genId(p: string): string {
  return p + '-' + Date.now() + '-' + Math.random().toString(36).slice(2, 8);
}

// YYYY-MM-DD + HH:MM → Googleカレンダー形式 YYYYMMDDTHHMMSS
function toGoogleDateTime(date: string, time: string): string {
  const d = date.replace(/-/g, '');
  const t = time.replace(':', '') + '00';
  return `${d}T${t}`;
}

/**
 * Googleカレンダーの「予定作成」URLを生成。
 * 新規タブで開けば、そのままGoogleカレンダーの作成画面に飛べる(代理入力)。
 */
export function buildGoogleCalendarUrl(entry: {
  title: string;
  date: string;
  startTime: string;
  endTime: string;
  note?: string;
  location?: string;
  attendeeEmail?: string; // 他メンバーを招待
}): string {
  const params = new URLSearchParams();
  params.set('action', 'TEMPLATE');
  params.set('text', entry.title);
  params.set('dates', `${toGoogleDateTime(entry.date, entry.startTime)}/${toGoogleDateTime(entry.date, entry.endTime)}`);
  if (entry.note) params.set('details', entry.note);
  if (entry.location) params.set('location', entry.location);
  if (entry.attendeeEmail) params.set('add', entry.attendeeEmail);
  params.set('ctz', 'Asia/Tokyo');
  return `https://calendar.google.com/calendar/u/0/r/eventedit?${params.toString()}`;
}

// Googleカレンダー本体への直リンク(本日ビュー)
export const GOOGLE_CALENDAR_URL = 'https://calendar.google.com/calendar/u/0/r/day';
export const GOOGLE_CALENDAR_WEEK_URL = 'https://calendar.google.com/calendar/u/0/r/week';

export function getTodayDate(): string {
  return new Date().toISOString().slice(0, 10);
}
export function getWeekDates(baseDate?: string): string[] {
  const base = baseDate ? new Date(baseDate) : new Date();
  const day = base.getDay();
  const monday = new Date(base);
  monday.setDate(base.getDate() - ((day + 6) % 7));
  return Array.from({ length: 7 }, (_, i) => {
    const d = new Date(monday);
    d.setDate(monday.getDate() + i);
    return d.toISOString().slice(0, 10);
  });
}
export function fmtDateJP(date: string): string {
  const d = new Date(date);
  return `${d.getMonth() + 1}/${d.getDate()}(${'日月火水木金土'[d.getDay()]})`;
}

// 時間をminuteに変換(グリッド配置用)
export function timeToMinutes(time: string): number {
  const [h, m] = time.split(':').map(Number);
  return h * 60 + m;
}

// 同じ日のエントリを開始時刻順にソート
export function sortByStart(entries: CalendarEntry[]): CalendarEntry[] {
  return [...entries].sort((a, b) => timeToMinutes(a.startTime) - timeToMinutes(b.startTime));
}

export function seedIfEmpty() {
  // ダミーデータは削除済み(本番運用開始)
  return;
  // eslint-disable-next-line no-unreachable
  if (typeof window === 'undefined') return;
  if (loadEntries().length > 0) return;

  const today = getTodayDate();
  const tomorrow = new Date(Date.now() + 86400000).toISOString().slice(0, 10);
  const yesterday = new Date(Date.now() - 86400000).toISOString().slice(0, 10);

  const entries: CalendarEntry[] = [
    // 大串 本日
    { id: genId('ce'), memberId: '1', memberName: '大串', date: today, startTime: '09:00', endTime: '10:00', title: 'X運用施策ミーティング', category: 'meeting', note: 'ハヤテと月次計画', location: 'Zoom', syncedToGoogle: true, createdAt: new Date().toISOString() },
    { id: genId('ce'), memberId: '1', memberName: '大串', date: today, startTime: '10:00', endTime: '12:00', title: 'SCALE Base開発', category: 'focus', note: '新機能実装', location: '', syncedToGoogle: true, createdAt: new Date().toISOString() },
    { id: genId('ce'), memberId: '1', memberName: '大串', date: today, startTime: '13:00', endTime: '14:00', title: 'テックフロンティア定例', category: 'meeting', note: '', location: 'Google Meet', syncedToGoogle: true, createdAt: new Date().toISOString() },
    { id: genId('ce'), memberId: '1', memberName: '大串', date: today, startTime: '14:30', endTime: '16:30', title: 'FS提案書作成', category: 'work', note: 'グロウメソッド向け', location: '', syncedToGoogle: true, createdAt: new Date().toISOString() },
    { id: genId('ce'), memberId: '1', memberName: '大串', date: today, startTime: '17:00', endTime: '18:00', title: '採用面談', category: 'meeting', note: 'FSメンバー候補', location: 'Zoom', syncedToGoogle: true, createdAt: new Date().toISOString() },
    // 細川 本日
    { id: genId('ce'), memberId: '4', memberName: '細川',     date: today, startTime: '09:00', endTime: '10:00', title: 'PMチーム定例', category: 'meeting', note: '', location: 'Slack Huddle', syncedToGoogle: true, createdAt: new Date().toISOString() },
    { id: genId('ce'), memberId: '4', memberName: '細川',     date: today, startTime: '10:30', endTime: '12:00', title: '案件ステータス整理', category: 'work', note: '品質チェック', location: '', syncedToGoogle: true, createdAt: new Date().toISOString() },
    { id: genId('ce'), memberId: '4', memberName: '細川',     date: today, startTime: '13:00', endTime: '14:00', title: 'ブライトリードMTG', category: 'meeting', note: '', location: 'Zoom', syncedToGoogle: true, createdAt: new Date().toISOString() },
    { id: genId('ce'), memberId: '4', memberName: '細川',     date: today, startTime: '14:00', endTime: '17:00', title: 'レポート作成', category: 'focus', note: '週次レポート', location: '', syncedToGoogle: true, createdAt: new Date().toISOString() },
    // 明日
    { id: genId('ce'), memberId: '1', memberName: '大串', date: tomorrow, startTime: '09:30', endTime: '11:00', title: 'ファインエデュ 四半期レビュー', category: 'meeting', note: '', location: '訪問', syncedToGoogle: true, createdAt: new Date().toISOString() },
    { id: genId('ce'), memberId: '1', memberName: '大串', date: tomorrow, startTime: '11:00', endTime: '12:00', title: '移動', category: 'travel', note: '', location: '', syncedToGoogle: true, createdAt: new Date().toISOString() },
    { id: genId('ce'), memberId: '4', memberName: '細川',     date: tomorrow, startTime: '10:00', endTime: '12:00', title: '新規案件キックオフ', category: 'meeting', note: '', location: 'Zoom', syncedToGoogle: true, createdAt: new Date().toISOString() },
    // 昨日
    { id: genId('ce'), memberId: '1', memberName: '大串', date: yesterday, startTime: '10:00', endTime: '12:00', title: 'プロダクト企画', category: 'focus', note: '', location: '', syncedToGoogle: true, createdAt: new Date().toISOString() },
    { id: genId('ce'), memberId: '4', memberName: '細川',     date: yesterday, startTime: '14:00', endTime: '16:00', title: 'クライアントMTG 2件', category: 'meeting', note: '', location: 'Zoom', syncedToGoogle: true, createdAt: new Date().toISOString() },
  ];

  saveEntries(entries);
}

前提条件
Tailwind CSS v4TypeScript 5
USE CASES
  • スケジュール管理
  • 予約システム
  • 日程調整

カレンダーデータ管理

:LiTarget: 用途

Googleカレンダー連携・イベント・予定をTypeScript型で扱うパターン。

:LiSparkle: 特徴

  • イベント取得
  • 予定登録
  • 空き時間検索
  • Googleカレンダー API 連携

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

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

// SCALE Base カレンダー連携データ層
// 目的: 大串・細川の毎朝のGoogleカレンダー記入漏れをなくし、SCALE Base内で予定を見える化する

export interface CalendarMember {
  id: string;
  name: string;
  avatar: string;
  color: string;
  googleCalendarEmbedUrl?: string; // 埋め込み用(将来)
  googleCalendarEmail?: string;
}

export interface CalendarEntry {
  id: string;
  memberId: string;
  memberName: string;
  date: string;      // YYYY-MM-DD
  startTime: string; // HH:MM
  endTime: string;   // HH:MM
  title: string;
  category: 'work'|'meeting'|'break'|'travel'|'personal'|'focus';
  note: string;
  location: string;
  syncedToGoogle: boolean;
  createdAt: string;
}

const KEY_ENTRIES = 'scale-calendar-entries';

// SCALE Baseメンバー(カレンダー対象)
export const CALENDAR_MEMBERS: CalendarMember[] = [
  { id: '1', name: '大串', avatar: '大', color: '#6485b8', googleCalendarEmail: 'y-ogushi@scale-group.co.jp' },
  { id: '4', name: '細川',     avatar: '細', color: '#6aa57a', googleCalendarEmail: 'hosokawa@scale-group.co.jp' },
];

export const CATEGORY_INFO: Record<CalendarEntry['category'], { label: string; color: string; icon: string }> = {
  work:     { label: '作業',   color: '#6485b8', icon: '💻' },
  meeting:  { label: 'MTG',    color: '#9078ad', icon: '🤝' },
  break:    { label: '休憩',   color: '#63636e', icon: '☕' },
  travel:   { label: '移動',   color: '#a89352', icon: '🚗' },
  personal: { label: '個人',   color: '#6395a3', icon: '👤' },
  focus:    { label: '集中',   color: '#6aa57a', icon: '🎯' },
};

export function loadEntries(): CalendarEntry[] {
  if (typeof window === 'undefined') return [];
  try { const v = localStorage.getItem(KEY_ENTRIES); return v ? JSON.parse(v) : []; } catch { return []; }
}
export function saveEntries(d: CalendarEntry[]) {
  if (typeof window === 'undefined') return;
  localStorage.setItem(KEY_ENTRIES, JSON.stringify(d));
}
export function addEntry(e: CalendarEntry) {
  const all = loadEntries();
  saveEntries([...all, e]);
}
export function updateEntry(id: string, patch: Partial<CalendarEntry>) {
  const all = loadEntries();
  saveEntries(all.map(e => e.id === id ? { ...e, ...patch } : e));
}
export function deleteEntry(id: string) {
  const all = loadEntries();
  saveEntries(all.filter(e => e.id !== id));
}

export function genId(p: string): string {
  return p + '-' + Date.now() + '-' + Math.random().toString(36).slice(2, 8);
}

// YYYY-MM-DD + HH:MM → Googleカレンダー形式 YYYYMMDDTHHMMSS
function toGoogleDateTime(date: string, time: string): string {
  const d = date.replace(/-/g, '');
  const t = time.replace(':', '') + '00';
  return `${d}T${t}`;
}

/**
 * Googleカレンダーの「予定作成」URLを生成。
 * 新規タブで開けば、そのままGoogleカレンダーの作成画面に飛べる(代理入力)。
 */
export function buildGoogleCalendarUrl(entry: {
  title: string;
  date: string;
  startTime: string;
  endTime: string;
  note?: string;
  location?: string;
  attendeeEmail?: string; // 他メンバーを招待
}): string {
  const params = new URLSearchParams();
  params.set('action', 'TEMPLATE');
  params.set('text', entry.title);
  params.set('dates', `${toGoogleDateTime(entry.date, entry.startTime)}/${toGoogleDateTime(entry.date, entry.endTime)}`);
  if (entry.note) params.set('details', entry.note);
  if (entry.location) params.set('location', entry.location);
  if (entry.attendeeEmail) params.set('add', entry.attendeeEmail);
  params.set('ctz', 'Asia/Tokyo');
  return `https://calendar.google.com/calendar/u/0/r/eventedit?${params.toString()}`;
}

// Googleカレンダー本体への直リンク(本日ビュー)
export const GOOGLE_CALENDAR_URL = 'https://calendar.google.com/calendar/u/0/r/day';
export const GOOGLE_CALENDAR_WEEK_URL = 'https://calendar.google.com/calendar/u/0/r/week';

export function getTodayDate(): string {
  return new Date().toISOString().slice(0, 10);
}
export function getWeekDates(baseDate?: string): string[] {
  const base = baseDate ? new Date(baseDate) : new Date();
  const day = base.getDay();
  const monday = new Date(base);
  monday.setDate(base.getDate() - ((day + 6) % 7));
  return Array.from({ length: 7 }, (_, i) => {
    const d = new Date(monday);
    d.setDate(monday.getDate() + i);
    return d.toISOString().slice(0, 10);
  });
}
export function fmtDateJP(date: string): string {
  const d = new Date(date);
  return `${d.getMonth() + 1}/${d.getDate()}(${'日月火水木金土'[d.getDay()]})`;
}

// 時間をminuteに変換(グリッド配置用)
export function timeToMinutes(time: string): number {
  const [h, m] = time.split(':').map(Number);
  return h * 60 + m;
}

// 同じ日のエントリを開始時刻順にソート
export function sortByStart(entries: CalendarEntry[]): CalendarEntry[] {
  return [...entries].sort((a, b) => timeToMinutes(a.startTime) - timeToMinutes(b.startTime));
}

export function seedIfEmpty() {
  // ダミーデータは削除済み(本番運用開始)
  return;
  // eslint-disable-next-line no-unreachable
  if (typeof window === 'undefined') return;
  if (loadEntries().length > 0) return;

  const today = getTodayDate();
  const tomorrow = new Date(Date.now() + 86400000).toISOString().slice(0, 10);
  const yesterday = new Date(Date.now() - 86400000).toISOString().slice(0, 10);

  const entries: CalendarEntry[] = [
    // 大串 本日
    { id: genId('ce'), memberId: '1', memberName: '大串', date: today, startTime: '09:00', endTime: '10:00', title: 'X運用施策ミーティング', category: 'meeting', note: 'ハヤテと月次計画', location: 'Zoom', syncedToGoogle: true, createdAt: new Date().toISOString() },
    { id: genId('ce'), memberId: '1', memberName: '大串', date: today, startTime: '10:00', endTime: '12:00', title: 'SCALE Base開発', category: 'focus', note: '新機能実装', location: '', syncedToGoogle: true, createdAt: new Date().toISOString() },
    { id: genId('ce'), memberId: '1', memberName: '大串', date: today, startTime: '13:00', endTime: '14:00', title: 'テックフロンティア定例', category: 'meeting', note: '', location: 'Google Meet', syncedToGoogle: true, createdAt: new Date().toISOString() },
    { id: genId('ce'), memberId: '1', memberName: '大串', date: today, startTime: '14:30', endTime: '16:30', title: 'FS提案書作成', category: 'work', note: 'グロウメソッド向け', location: '', syncedToGoogle: true, createdAt: new Date().toISOString() },
    { id: genId('ce'), memberId: '1', memberName: '大串', date: today, startTime: '17:00', endTime: '18:00', title: '採用面談', category: 'meeting', note: 'FSメンバー候補', location: 'Zoom', syncedToGoogle: true, createdAt: new Date().toISOString() },
    // 細川 本日
    { id: genId('ce'), memberId: '4', memberName: '細川',     date: today, startTime: '09:00', endTime: '10:00', title: 'PMチーム定例', category: 'meeting', note: '', location: 'Slack Huddle', syncedToGoogle: true, createdAt: new Date().toISOString() },
    { id: genId('ce'), memberId: '4', memberName: '細川',     date: today, startTime: '10:30', endTime: '12:00', title: '案件ステータス整理', category: 'work', note: '品質チェック', location: '', syncedToGoogle: true, createdAt: new Date().toISOString() },
    { id: genId('ce'), memberId: '4', memberName: '細川',     date: today, startTime: '13:00', endTime: '14:00', title: 'ブライトリードMTG', category: 'meeting', note: '', location: 'Zoom', syncedToGoogle: true, createdAt: new Date().toISOString() },
    { id: genId('ce'), memberId: '4', memberName: '細川',     date: today, startTime: '14:00', endTime: '17:00', title: 'レポート作成', category: 'focus', note: '週次レポート', location: '', syncedToGoogle: true, createdAt: new Date().toISOString() },
    // 明日
    { id: genId('ce'), memberId: '1', memberName: '大串', date: tomorrow, startTime: '09:30', endTime: '11:00', title: 'ファインエデュ 四半期レビュー', category: 'meeting', note: '', location: '訪問', syncedToGoogle: true, createdAt: new Date().toISOString() },
    { id: genId('ce'), memberId: '1', memberName: '大串', date: tomorrow, startTime: '11:00', endTime: '12:00', title: '移動', category: 'travel', note: '', location: '', syncedToGoogle: true, createdAt: new Date().toISOString() },
    { id: genId('ce'), memberId: '4', memberName: '細川',     date: tomorrow, startTime: '10:00', endTime: '12:00', title: '新規案件キックオフ', category: 'meeting', note: '', location: 'Zoom', syncedToGoogle: true, createdAt: new Date().toISOString() },
    // 昨日
    { id: genId('ce'), memberId: '1', memberName: '大串', date: yesterday, startTime: '10:00', endTime: '12:00', title: 'プロダクト企画', category: 'focus', note: '', location: '', syncedToGoogle: true, createdAt: new Date().toISOString() },
    { id: genId('ce'), memberId: '4', memberName: '細川',     date: yesterday, startTime: '14:00', endTime: '16:00', title: 'クライアントMTG 2件', category: 'meeting', note: '', location: 'Zoom', syncedToGoogle: true, createdAt: new Date().toISOString() },
  ];

  saveEntries(entries);
}

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

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

:LiHandPointer: 使い方

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

:LiAlertCircle: 注意事項

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