Files
jsc-anima/hooks/todo.ts
T

89 lines
3.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 工具四(待辦事項)的純邏輯:解析、渲染、到點判斷。讀寫與指令在 register.tsx。
export const TODO_COMMAND = 'anima-todo'
export const TODO_KEY = 'todos'
export const TYPES_KEY = 'todoTypes'
export const DEFAULT_TYPES = ['提醒', '開發', '研究', '維護']
export type Status = 'pending' | 'reminded' | 'done'
export type Todo = { id: number; type: string; at: number; task: string; project: string; status: Status; result?: string }
export const USAGE =
'用法:\n' +
'/jsc-anima:anima-todo add <類型> <YYYY-MM-DD> <HH:MM> <任務> 建立\n' +
'/jsc-anima:anima-todo update <編號> <類型> <YYYY-MM-DD> <HH:MM> <任務> 更新\n' +
'/jsc-anima:anima-todo list 列出全部\n' +
'/jsc-anima:anima-todo type <名稱> 新增自訂類型\n' +
'/jsc-anima:anima-todo run <編號> 確認後執行\n' +
'/jsc-anima:anima-todo done <編號> 標記完成(不執行)'
const pad = (n: number) => String(n).padStart(2, '0')
export const fmtTime = (ms: number): string => {
const d = new Date(ms)
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`
}
// 排程時間以本機時區解讀。
export const parseTime = (date: string | undefined, time: string | undefined): number | undefined => {
const d = /^(\d{4})-(\d{2})-(\d{2})$/.exec(date ?? '')
const t = /^(\d{1,2}):(\d{2})$/.exec(time ?? '')
if (!d || !t) return undefined
const [y, mo, da, h, mi] = [d[1], d[2], d[3], t[1], t[2]].map(Number) as [number, number, number, number, number]
const out = new Date(y, mo - 1, da, h, mi)
if (out.getMonth() !== mo - 1 || out.getDate() !== da || h > 23 || mi > 59) return undefined
return out.getTime()
}
export type Parsed =
| { op: 'add'; type: string; at: number; task: string }
| { op: 'update'; id: number; type: string; at: number; task: string }
| { op: 'list' }
| { op: 'type'; name: string }
| { op: 'run' | 'done'; id: number }
const spec = (w: string[]): { type: string; at: number; task: string } | undefined => {
const [type, date, time, ...rest] = w
const at = parseTime(date, time)
const task = rest.join(' ').trim()
return type && at !== undefined && task ? { type, at, task } : undefined
}
export const parseArgs = (args: string): Parsed | undefined => {
const w = args.trim().split(/\s+/).filter(Boolean)
const [op, ...rest] = w
const id = Number(rest[0])
if (op === 'list' && rest.length === 0) return { op }
if (op === 'type' && rest.length === 1) return { op, name: rest[0]! }
if ((op === 'run' || op === 'done') && rest.length === 1 && Number.isInteger(id) && id > 0) return { op, id }
if (op === 'add') {
const s = spec(rest)
return s ? { op, ...s } : undefined
}
if (op === 'update' && Number.isInteger(id) && id > 0) {
const s = spec(rest.slice(1))
return s ? { op, id, ...s } : undefined
}
return undefined
}
export const dueNow = (todos: Todo[], now: number): Todo[] => todos.filter(t => t.status === 'pending' && t.at <= now)
export const nextId = (todos: Todo[]): number => todos.reduce((m, t) => Math.max(m, t.id), 0) + 1
const STATUS_TEXT: Record<Status, string> = { pending: '待辦', reminded: '已提醒', done: '完成' }
export const renderList = (todos: Todo[]): string =>
todos.length === 0
? '目前沒有待辦。'
: todos
.slice()
.sort((a, b) => a.at - b.at)
.map(t => `#${t.id} [${t.type}] ${fmtTime(t.at)} ${t.task}(${STATUS_TEXT[t.status]},專案:${t.project || '無'})`)
.join('\n')
export const reminderText = (t: Todo): string =>
`到點了:[${t.type}] ${t.task}(#${t.id},專案:${t.project || '無'})。要我執行的話請輸入 /jsc-anima:${TODO_COMMAND} run ${t.id},確認前我不會動手。`
export const RUN_SYSTEM = '你要執行使用者確認過的待辦,簡短回報你做了什麼與結果;沒做到的事如實說明。'