カレンダーデータ管理
: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: 注意事項
- 依存パッケージを忘れず追加