スケジュールデータ管理
:LiTarget: 用途
ミーティング日程調整・候補日提示・確定管理のロジック。
:LiSparkle: 特徴
- 候補日生成
- 確定管理
- 招待状送信
- リマインダー
:LiCode: 実コード(SCALE Base より自動抽出)
:LiInfo:
lib/scheduling-data.tsの中身そのもの。コピペ即可。
// 日程調整ビルダー(apo-sukeライクな設計)
// コンセプト: URLを送らず、候補日をテキストで生成してコピペ送信
export interface SchedulingSettings {
meetingMin: number; // 会議時間(分)
daysAhead: number; // 何日先まで抽出
startDayOffset: number; // 開始日オフセット(0=今日、1=明日から)
workStart: string; // '09:00'
workEnd: string; // '19:00'
weekdays: number[]; // [1,2,3,4,5] (日=0, 月=1 ... 土=6)
bufferBefore: number; // 前後バッファ(分)
bufferAfter: number;
maxCandidates: number;
slotInterval: number; // 候補の刻み(分)例: 30
fmt: 'simple' | 'polite' | 'business';
lunchStart?: string; // '12:00'
lunchEnd?: string; // '13:00'
excludeLunch: boolean;
}
export interface BusySlot {
id: string;
date: string; // YYYY-MM-DD
start: string; // HH:MM
end: string; // HH:MM
label?: string;
}
export interface Candidate {
date: string; // YYYY-MM-DD
startMin: number;
endMin: number;
}
export const DEFAULT_SETTINGS: SchedulingSettings = {
meetingMin: 30,
daysAhead: 10,
startDayOffset: 1,
workStart: '09:00',
workEnd: '19:00',
weekdays: [1, 2, 3, 4, 5],
bufferBefore: 15,
bufferAfter: 15,
maxCandidates: 5,
slotInterval: 30,
fmt: 'polite',
lunchStart: '12:00',
lunchEnd: '13:00',
excludeLunch: true,
};
export const DAYS_JA = ['日', '月', '火', '水', '木', '金', '土'];
export function hhmmToMin(hhmm: string): number {
const [h, m] = hhmm.split(':').map(Number);
return h * 60 + (m || 0);
}
export function minToHHMM(min: number): string {
const h = Math.floor(min / 60);
const m = min % 60;
return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`;
}
export function ymd(d: Date): string {
const y = d.getFullYear();
const m = String(d.getMonth() + 1).padStart(2, '0');
const day = String(d.getDate()).padStart(2, '0');
return `${y}-${m}-${day}`;
}
// 設定 + 既存の予定から空き時間の候補日を計算
export function generateCandidates(settings: SchedulingSettings, busy: BusySlot[]): Candidate[] {
const candidates: Candidate[] = [];
const today = new Date();
today.setHours(0, 0, 0, 0);
for (let day = settings.startDayOffset; day < settings.startDayOffset + settings.daysAhead; day++) {
const date = new Date(today);
date.setDate(today.getDate() + day);
const dow = date.getDay();
if (!settings.weekdays.includes(dow)) continue;
const dateStr = ymd(date);
const dayBusy = busy.filter(b => b.date === dateStr);
const startMin = hhmmToMin(settings.workStart);
const endMin = hhmmToMin(settings.workEnd);
const duration = settings.meetingMin;
const lunchS = settings.lunchStart ? hhmmToMin(settings.lunchStart) : null;
const lunchE = settings.lunchEnd ? hhmmToMin(settings.lunchEnd) : null;
for (let t = startMin; t + duration <= endMin; t += settings.slotInterval) {
const slotStart = t;
const slotEnd = t + duration;
// ランチタイム除外
if (settings.excludeLunch && lunchS !== null && lunchE !== null) {
if (slotStart < lunchE && slotEnd > lunchS) continue;
}
// 既存予定との重複チェック(バッファ込み)
const hasConflict = dayBusy.some(b => {
const bs = hhmmToMin(b.start) - settings.bufferBefore;
const be = hhmmToMin(b.end) + settings.bufferAfter;
return slotStart < be && slotEnd > bs;
});
if (hasConflict) continue;
candidates.push({ date: dateStr, startMin: slotStart, endMin: slotEnd });
if (candidates.length >= settings.maxCandidates) return candidates;
}
}
return candidates;
}
export function formatCandidate(c: Candidate): string {
const d = new Date(c.date + 'T00:00:00');
const dow = DAYS_JA[d.getDay()];
return `${d.getMonth() + 1}/${d.getDate()}(${dow}) ${minToHHMM(c.startMin)}-${minToHHMM(c.endMin)}`;
}
export function formatCandidatesText(candidates: Candidate[], fmt: SchedulingSettings['fmt']): string {
if (candidates.length === 0) return '候補日が見つかりませんでした。設定を調整してください。';
const lines = candidates.map(c => `・${formatCandidate(c)}`);
switch (fmt) {
case 'polite':
return `お世話になっております。\n下記の日程でお打ち合わせをご調整いただけますと幸いです。\n\n${lines.join('\n')}\n\n上記でご都合の良い時間帯がございましたら、お知らせください。\nよろしくお願いいたします。`;
case 'business':
return `打ち合わせ候補日時、下記の中からご都合の良い時間をお知らせください。\n\n${lines.join('\n')}`;
case 'simple':
default:
return `候補日時:\n${lines.join('\n')}`;
}
}
const KEY_SETTINGS = 'scale-scheduling-settings';
const KEY_BUSY = 'scale-scheduling-busy';
export function loadSettings(): SchedulingSettings {
if (typeof window === 'undefined') return DEFAULT_SETTINGS;
try {
const v = localStorage.getItem(KEY_SETTINGS);
if (!v) return DEFAULT_SETTINGS;
return { ...DEFAULT_SETTINGS, ...JSON.parse(v) };
} catch {
return DEFAULT_SETTINGS;
}
}
export function saveSettings(s: SchedulingSettings) {
if (typeof window === 'undefined') return;
localStorage.setItem(KEY_SETTINGS, JSON.stringify(s));
}
export function loadBusy(): BusySlot[] {
if (typeof window === 'undefined') return [];
try {
const v = localStorage.getItem(KEY_BUSY);
if (!v) return [];
return JSON.parse(v);
} catch {
return [];
}
}
export function saveBusy(b: BusySlot[]) {
if (typeof window === 'undefined') return;
localStorage.setItem(KEY_BUSY, JSON.stringify(b));
}
export function genId(): string {
return Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
}
:LiFolder: ソースファイルのパス
/Users/oogushiyuuki/Library/CloudStorage/GoogleDrive-y-ogushi@scale-group.co.jp/マイドライブ/AI/scale-base/lib/scheduling-data.ts
:LiHandPointer: 使い方
対象プロジェクトに該当ファイルをコピーして、props を流し込むだけ。
:LiAlertCircle: 注意事項
- 依存パッケージを忘れず追加