// SchedulesPage — mirrors the Tasks 3-pane shell: list | detail | history // Data model derives from ui/src/pages/SchedulesPage.tsx. const DAYS = ['日', '月', '火', '水', '木', '金', '土']; function parseCronToDisplay(cron) { if (cron === 'once') return '一回のみ'; const parts = (cron || '').split(' '); if (parts.length !== 5) return cron; const [min, hour, dom, , dow] = parts; const hhmm = `${hour}:${String(min).padStart(2, '0')}`; if (dom !== '*' && dow === '*') return `毎月${dom}日 ${hhmm}`; if (dow !== '*' && dom === '*') return `毎週${DAYS[Number(dow)] ?? dow}曜 ${hhmm}`; if (dom === '*' && dow === '*') return `毎日 ${hhmm}`; return cron; } function formatDateShort(iso) { if (!iso) return '—'; const d = new Date(iso); return d.toLocaleString('ja-JP', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }); } function relativeFromNow(iso) { if (!iso) return '—'; const diff = new Date(iso).getTime() - Date.now(); const abs = Math.abs(diff); const mins = Math.round(abs / 60000); const hrs = Math.round(mins / 60); const days = Math.round(hrs / 24); const unit = mins < 60 ? `${mins}分` : hrs < 24 ? `${hrs}時間` : `${days}日`; return diff >= 0 ? `${unit}後` : `${unit}前`; } // ── Left: list of schedules ────────────────────────────────────────────── function ScheduleListItem({ sch, active, onClick }) { return ( ); } function ScheduleListPane({ items, activeId, onSelect, filter, setFilter, search, setSearch, onOpenCreate }) { const filtered = items.filter(s => { if (filter === 'active' && !s.isActive) return false; if (filter === 'paused' && s.isActive) return false; if (filter === 'event' && s.triggerKind !== 'event') return false; if (search && !(s.title + s.body).toLowerCase().includes(search.toLowerCase())) return false; return true; }); const counts = { all: items.length, active: items.filter(s => s.isActive).length, paused: items.filter(s => !s.isActive).length, event: items.filter(s => s.triggerKind === 'event').length, }; const chipStyle = (on) => ({ flexShrink: 0, padding: '6px 10px', borderRadius: 9999, fontSize: 11, fontWeight: 700, whiteSpace: 'nowrap', cursor: 'pointer', border: '1px solid ' + (on ? '#2563eb' : '#e2e8f0'), background: on ? '#eff6ff' : '#fff', color: on ? '#1d4ed8' : '#64748b', fontFamily: 'inherit', }); return (
{counts.all} · {counts.active} 有効 {counts.paused > 0 && {counts.paused} 停止}
setSearch(e.target.value)} placeholder="検索..." style={{ flex: 1, border: 'none', outline: 'none', background: 'transparent', fontSize: 13, fontFamily: 'inherit', color: '#0f172a', minWidth: 0 }} />
{filtered.map(s => onSelect(s.id)} />)} {filtered.length === 0 && ( (search || filter !== 'all') ? ( } title="該当するスケジュールはありません" hint="検索やフィルタを変えてみてください。" action={ } /> ) : ( } title="スケジュールがありません" hint="定期実行やイベントトリガーを登録するとここに表示されます。" /> ) )}
); } // ── Center: schedule detail editor ─────────────────────────────────────── function FormRow({ label, help, children }) { return ( ); } function TextInput(props) { return ; } function SelectInput({ children, ...props }) { return ; } function ScheduleDetail({ sch, onPatch, onTrigger, onDelete }) { if (!sch) { return (
} title="スケジュールを選択してください" hint="左のリストから編集したいスケジュールを開きます。" />
); } const isEvent = sch.triggerKind === 'event'; return (
{/* Header */}
SCHEDULE #{sch.id}
{sch.title || 'タイトルなし'}
{/* Body */}
{/* Summary strip */}
{sch.isActive ? : }
{/* Form card */}
基本情報
onPatch(sch.id, { title: e.target.value })} placeholder="週次ニュースまとめ" />