feat: add Discord habit history and dated progress corrections
This commit is contained in:
64
src/discord/history.ts
Normal file
64
src/discord/history.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { addDays } from '../habits/calendar';
|
||||
import { dateSchema, type Schedule } from '../habits/contracts';
|
||||
import { dailyAction, detail } from './daily';
|
||||
import { fields, type Context, type ControlData, type Message, type Reply } from './types';
|
||||
import { button, message, modal, pages, row, select, text } from './ui';
|
||||
|
||||
const weekdays = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
|
||||
function scheduleLabel(schedule: Schedule): string {
|
||||
if (schedule.type === 'daily') return 'Every day';
|
||||
if (schedule.type === 'weekdays') return schedule.days.map(day => weekdays[day]).join(', ');
|
||||
if (schedule.type === 'interval') return `Every ${schedule.every} days from ${schedule.anchor}`;
|
||||
return `Every ${schedule.every} weeks on ${weekdays[schedule.weekday]} from ${schedule.anchor}`;
|
||||
}
|
||||
|
||||
export function history(c: Context, data: ControlData = {}): Reply {
|
||||
const all = c.service.list(true);
|
||||
const page = Math.max(0, Math.min(data.page ?? 0, Math.ceil(all.length / 10) - 1));
|
||||
const state: ControlData = { date: data.date ?? c.service.today, page, mode: 'history', today: false };
|
||||
const visible = all.slice(page * 10, page * 10 + 10);
|
||||
return message(c, `**History** · ${text(c.service.user.timezone)}\nChoose a habit, then enter a calendar date. Archived habits are included.${all.length ? `\nPage ${page + 1} of ${Math.ceil(all.length / 10)}` : '\nNo habits yet. Use /habits to create one.'}`,
|
||||
[...(visible.length ? [select(c, 'Choose a habit', 'history.select', state, visible.map(h => ({ id: h.id, name: h.name, description: `${h.archived ? 'Archived · ' : ''}Tracked since ${h.createdDate}` })))] : []), ...pages(c, 'history', state, all.length)]);
|
||||
}
|
||||
|
||||
function dateForm(c: Context, data: ControlData): Reply {
|
||||
c.service.owned(data.habitId!);
|
||||
return modal(c, 'Choose history date', 'history.date', data, [{ id: 'date', label: 'Calendar date (YYYY-MM-DD)', value: data.date ?? c.service.today, max: 10 }]);
|
||||
}
|
||||
|
||||
// Keep the dated editor and all mutation checks in daily.ts. Its opaque payloads
|
||||
// preserve mode/today, so subsequent saves, forms and task pages return here.
|
||||
function withNavigation(c: Context, data: ControlData, reply: Reply): Reply {
|
||||
if (![4, 7].includes(reply.type)) return reply;
|
||||
const d = c.service.day(data.habitId!, data.date!);
|
||||
const state: ControlData = { habitId: data.habitId, date: data.date, mode: 'history', today: false, page: 0 };
|
||||
const navigation = [
|
||||
...(data.date! > '1970-01-01' ? [button(c, 'Previous date', 'history.detail', { ...state, date: addDays(data.date!, -1) })] : []),
|
||||
...(data.date! < '9998-12-31' ? [button(c, 'Next date', 'history.detail', { ...state, date: addDays(data.date!, 1) })] : []),
|
||||
button(c, 'Change date', 'history.form', state),
|
||||
button(c, 'Choose habit', 'history', { date: data.date }),
|
||||
];
|
||||
const explanation = !d.requirements ? 'Read-only · This habit was not tracked yet.'
|
||||
: d.requirements.archived ? 'Read-only · This habit was archived on this date.'
|
||||
: !d.due ? 'Read-only · Day off; nothing is scheduled for this date.' : '';
|
||||
const body = reply.data as Message;
|
||||
const requirements = d.requirements ? `${d.method === 'manual' ? 'Checkbox' : d.method === 'count' ? 'Count' : 'Tasks'} · ${scheduleLabel(d.requirements.schedule)}\n` : '';
|
||||
return { ...reply, data: { ...body, content: `${requirements}${text(d.timezone)}\n${explanation ? explanation + '\n' : ''}${body.content ?? ''}`.slice(0, 2000), components: [...(body.components ?? []), row(...navigation)] } };
|
||||
}
|
||||
|
||||
export function historyAction(c: Context, action: string, data: ControlData): Reply | undefined {
|
||||
if (action === 'history') return history(c, data);
|
||||
if (action === 'history.select') return dateForm(c, { ...data, habitId: c.interaction.data?.values?.[0], page: 0 });
|
||||
if (action === 'history.form') return dateForm(c, data);
|
||||
if (action === 'history.date') {
|
||||
const parsed = dateSchema.safeParse(fields(c.interaction).date?.trim());
|
||||
if (!parsed.success) return message(c, 'Enter a real calendar date as YYYY-MM-DD, from 1970 through 9998.', [row(button(c, 'Change date', 'history.form', data))]);
|
||||
data = { ...data, date: parsed.data, page: 0 };
|
||||
action = 'history.detail';
|
||||
}
|
||||
if (action === 'history.detail' || (data.mode === 'history' && action.startsWith('daily.'))) {
|
||||
const state = { ...data, date: dateSchema.parse(data.date), mode: 'history', today: false };
|
||||
const reply = action === 'history.detail' ? detail(c, state) : dailyAction(c, action, state);
|
||||
return reply && withNavigation(c, state, reply);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user