Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 15 additions & 5 deletions web/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -694,10 +694,13 @@ function MonthDashboard({ attributes, plans, planRecords, goals, projects, proje
</div>;
}

function CalendarDrawerPlan({ plan, mode, onEditPlan }: { plan: PlanItem; mode: "week" | "month"; onEditPlan: (id: string) => void }) {
function CalendarDrawerPlan({ plan, mode, readOnly = false, onEditPlan }: { plan: PlanItem; mode: "week" | "month"; readOnly?: boolean; onEditPlan: (id: string) => void }) {
const unscheduled = mode === "week" && plan.timeStatus === "unscheduled";
const timeLabel = mode === "month" ? "✓ 已完成" : unscheduled ? "时间未配置" : (plan.timeSegments?.length ?? 0) > 1 ? `${plan.timeSegments!.length} 个时段` : `${plan.start}—${plan.end}`;
return <div className={`drawer-plan${unscheduled ? " drawer-plan--unscheduled" : ""}`}>
<button className={`drawer-plan__time${unscheduled ? " is-unscheduled" : ""}`} onClick={() => onEditPlan(plan.calendarSourceId ?? plan.id)}>{mode === "month" ? "✓ 已完成" : unscheduled ? "时间待配置" : (plan.timeSegments?.length ?? 0) > 1 ? `${plan.timeSegments!.length} 个时段` : `${plan.start}—${plan.end}`}</button>
{readOnly
? <span className={`drawer-plan__time${unscheduled ? " is-unscheduled" : ""}`}>{timeLabel}</span>
: <button className={`drawer-plan__time${unscheduled ? " is-unscheduled" : ""}`} onClick={() => onEditPlan(plan.calendarSourceId ?? plan.id)}>{timeLabel}</button>}
<div className="drawer-plan__copy"><b>{plan.title}</b><span>{plan.category}{plan.source === "learning-more" ? " · Learning MORE" : ""}</span></div>
</div>;
}
Expand Down Expand Up @@ -751,7 +754,14 @@ function CalendarView({ plans, initialMode, content, onEditPlan }: { plans: Plan
<div className="page-title"><div><span className="eyebrow">{content === "timeline" ? "TIMELINE" : "CALENDAR"}</span><h1>{content === "timeline" ? "时间轨迹" : "日程表"}</h1><p>{mode === "week" ? rangeLabel : monthLabel} · Asia/Shanghai</p></div><div className="segmented"><button className={mode === "week" ? "active" : ""} onClick={() => { setMode("week"); setSelectedDate(null); }}>周</button><button className={mode === "month" ? "active" : ""} onClick={() => { setMode("month"); setSelectedDate(null); }}>月</button></div></div>
{mode === "week" ? <section className="calendar-card pixel-card">
<div className="calendar-head"><span>时间</span>{dayDates.map((date, index) => <div className={index === todayIndex ? "today" : ""} key={date.key}>{date.weekday}<b>{date.day}</b></div>)}</div>
{content === "schedule" && <div className="unscheduled-dock"><div className="unscheduled-dock__label"><span>POCKET</span><b>待安排</b></div>{dayDates.map((date, dayIndex) => { const count = plansForDate(date.key).filter((plan) => plan.timeStatus === "unscheduled").length; return <button className={`${dayIndex === todayIndex ? "is-today" : ""}${count > 0 ? " has-items" : ""}`} key={date.key} disabled={count === 0} onClick={() => setSelectedDate(date.key)} aria-label={`${date.weekday}有${count}项待安排计划`}>{count > 0 ? <><strong>+{count}</strong><small>展开待安排</small></> : <span>—</span>}</button>; })}</div>}
<div className="unscheduled-dock">
<div className="unscheduled-dock__label"><span>{content === "timeline" ? "UNTIMED" : "POCKET"}</span><b>{content === "timeline" ? "未配置时间" : "待安排"}</b></div>
{dayDates.map((date, dayIndex) => {
const count = plansForDate(date.key).filter((plan) => plan.timeStatus === "unscheduled").length;
const countDescription = content === "timeline" ? "条完成记录未配置实际时间" : "项待安排计划";
return <button className={`${dayIndex === todayIndex ? "is-today" : ""}${count > 0 ? " has-items" : ""}`} key={date.key} disabled={count === 0} onClick={() => setSelectedDate(date.key)} aria-label={`${date.weekday}有${count}${countDescription}`}>{count > 0 ? <><strong>+{count}</strong><small>{content === "timeline" ? "展开完成记录" : "展开待安排"}</small></> : <span>—</span>}</button>;
})}
</div>
<div className="calendar-body"><div className="time-axis">{["06:00", "08:00", "10:00", "12:00", "14:00", "16:00", "18:00", "20:00", "22:00"].map((time) => <span key={time}>{time}</span>)}</div>{dayDates.map((date, dayIndex) => {
const allDayPlans = plansForDate(date.key);
const timedPlans = expandCalendarSegments(allDayPlans.filter((plan) => plan.timeStatus !== "unscheduled"));
Expand All @@ -771,14 +781,14 @@ function CalendarView({ plans, initialMode, content, onEditPlan }: { plans: Plan
const timedPlans = expandCalendarSegments(dayPlans.filter((plan) => plan.timeStatus !== "unscheduled"));
const expanded = expandedDays.includes(dayIndex);
const shown = expanded ? timedPlans : timedPlans.slice(0, 6);
return <section className="agenda-day" key={date.key}><header><b>{date.weekday} {date.day}</b><span>{timedPlans.length} 已排 · {unscheduledPlans.length} 待安排</span></header>{unscheduledPlans.length > 0 && <button className="agenda-unscheduled" onClick={() => setSelectedDate(date.key)}><strong>+{unscheduledPlans.length}</strong><span>展开待安排计划</span><em>→</em></button>}{shown.map((plan) => <div className="agenda-row" key={plan.id}><button className="agenda-row__time" onClick={() => onEditPlan(plan.calendarSourceId ?? plan.id)}>{plan.start}—{plan.end}</button><span>{plan.title}</span><small>{plan.completed ? "已完成" : plan.source === "learning-more" ? "LM 同步" : plan.category}</small></div>)}{dayPlans.length === 0 && <p>没有安排</p>}{timedPlans.length > 6 && <button className="load-more" onClick={() => setExpandedDays((current) => expanded ? current.filter((item) => item !== dayIndex) : [...current, dayIndex])}>{expanded ? "收起" : `再显示 ${timedPlans.length - 6} 项`}</button>}</section>;
return <section className="agenda-day" key={date.key}><header><b>{date.weekday} {date.day}</b><span>{timedPlans.length} {content === "timeline" ? "已记录" : "已排"} · {unscheduledPlans.length} {content === "timeline" ? "未配置时间" : "待安排"}</span></header>{unscheduledPlans.length > 0 && <button className="agenda-unscheduled" onClick={() => setSelectedDate(date.key)}><strong>+{unscheduledPlans.length}</strong><span>{content === "timeline" ? "展开未配置时间完成记录" : "展开待安排计划"}</span><em>→</em></button>}{shown.map((plan) => <div className="agenda-row" key={plan.id}>{content === "timeline" ? <span className="agenda-row__time">{plan.start}—{plan.end}</span> : <button className="agenda-row__time" onClick={() => onEditPlan(plan.calendarSourceId ?? plan.id)}>{plan.start}—{plan.end}</button>}<span>{plan.title}</span><small>{plan.completed ? "已完成" : plan.source === "learning-more" ? "LM 同步" : plan.category}</small></div>)}{dayPlans.length === 0 && <p>{content === "timeline" ? "没有完成记录" : "没有安排"}</p>}{timedPlans.length > 6 && <button className="load-more" onClick={() => setExpandedDays((current) => expanded ? current.filter((item) => item !== dayIndex) : [...current, dayIndex])}>{expanded ? "收起" : `再显示 ${timedPlans.length - 6} 项`}</button>}</section>;
})}</div>
</section> : <section className="month-calendar pixel-card">
<div className="month-calendar__toolbar"><button onClick={() => shiftMonth(-1)} aria-label="上个月">←</button><b>{monthLabel}</b><button onClick={() => shiftMonth(1)} aria-label="下个月">→</button><button className="month-calendar__today" onClick={() => setMonthAnchor(todayKey.slice(0, 7))}>回到本月</button></div>
<div className="month-calendar__weekdays">{weekdays.map((day) => <b key={day}>{day}</b>)}</div>
<div className="month-calendar__grid">{monthCells.map((cell) => <div className={`month-day${cell.inMonth ? "" : " is-outside"}${cell.today ? " is-today" : ""}`} key={cell.key}><button className="month-day__number" onClick={() => setSelectedDate(cell.key)} aria-label={`查看${cell.key}完成内容`}><span>{cell.day}</span>{cell.plans.length > 0 ? <small>{cell.plans.length} 项完成</small> : cell.today && <small>今天</small>}</button><div className="month-day__plans">{cell.plans.slice(0, 3).map((plan) => <button className="month-plan-chip" style={{ "--category-color": plan.categoryColor, "--category-text": plan.categoryTextColor } as CSSProperties} key={plan.id} onClick={() => setSelectedDate(cell.key)}><i>✓</i><span>{plan.title}</span></button>)}</div>{cell.plans.length > 3 && <button className="month-day__more" onClick={() => setSelectedDate(cell.key)}>另有 {cell.plans.length - 3} 项完成</button>}</div>)}</div>
</section>}
{selectedDate !== null && <div className="agenda-drawer pixel-card"><div className="section-heading"><div><span className="eyebrow">{mode === "month" ? "DONE TODAY" : "DAY AGENDA"}</span><h2>{selectedDateLabel} · {mode === "month" ? "完成内容" : "计划口袋"}</h2></div><button className="modal-close" onClick={() => setSelectedDate(null)}>×</button></div>{selectedPlans.length > 0 ? <>{mode === "week" && selectedUnscheduled.length > 0 && <section className="drawer-plan-group drawer-plan-group--unscheduled"><div className="drawer-plan-group__title"><b>待安排</b><span>{selectedUnscheduled.length} 项 · 点击时间票券直接设置</span></div>{selectedUnscheduled.map((plan) => <CalendarDrawerPlan key={plan.id} plan={plan} mode={mode} onEditPlan={onEditPlan} />)}</section>}{selectedTimed.length > 0 && <section className="drawer-plan-group"><div className="drawer-plan-group__title"><b>{mode === "month" ? "完成内容" : "已安排"}</b><span>{selectedTimed.length} 项 · 点击时间可再次修改</span></div>{selectedTimed.map((plan) => <CalendarDrawerPlan key={plan.id} plan={plan} mode={mode} onEditPlan={onEditPlan} />)}</section>}</> : <div className="mini-empty">{mode === "month" ? "这一天还没有完成记录。" : "这一天还没有计划。"}</div>}</div>}
{selectedDate !== null && <div className="agenda-drawer pixel-card"><div className="section-heading"><div><span className="eyebrow">{mode === "month" ? "DONE TODAY" : content === "timeline" ? "COMPLETION FACTS" : "DAY AGENDA"}</span><h2>{selectedDateLabel} · {mode === "month" || content === "timeline" ? "完成记录" : "计划口袋"}</h2></div><button className="modal-close" onClick={() => setSelectedDate(null)}>×</button></div>{selectedPlans.length > 0 ? <>{mode === "week" && selectedUnscheduled.length > 0 && <section className="drawer-plan-group drawer-plan-group--unscheduled"><div className="drawer-plan-group__title"><b>{content === "timeline" ? "未配置时间" : "待安排"}</b><span>{selectedUnscheduled.length} 项 · {content === "timeline" ? "已完成但未记录实际时段" : "点击时间票券直接设置"}</span></div>{selectedUnscheduled.map((plan) => <CalendarDrawerPlan key={plan.id} plan={plan} mode={mode} readOnly={content === "timeline"} onEditPlan={onEditPlan} />)}</section>}{selectedTimed.length > 0 && <section className="drawer-plan-group"><div className="drawer-plan-group__title"><b>{mode === "month" ? "完成内容" : content === "timeline" ? "实际执行" : "已安排"}</b><span>{selectedTimed.length} 项{content === "schedule" ? " · 点击时间可再次修改" : ""}</span></div>{selectedTimed.map((plan) => <CalendarDrawerPlan key={plan.id} plan={plan} mode={mode} readOnly={content === "timeline"} onEditPlan={onEditPlan} />)}</section>}</> : <div className="mini-empty">{mode === "month" || content === "timeline" ? "这一天还没有完成记录。" : "这一天还没有计划。"}</div>}</div>}
</div>
);
}
Expand Down
2 changes: 1 addition & 1 deletion web/lib/state-patch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { WeekUpState } from "./week-up-domain.ts";
const COLLECTION_KEYS = [
"attributeCategories", "projectCategories", "attributes", "projects",
"learningMoreCourses", "learningMoreLessons", "goals", "plans",
"completionFacts", "executionRecords", "xpTransactions", "weightRevisions", "dailySettlements", "settlements", "skillbooks",
"completionFacts", "xpTransactions", "weightRevisions", "dailySettlements", "settlements", "skillbooks",
] as const;

type CollectionKey = typeof COLLECTION_KEYS[number];
Expand Down
43 changes: 23 additions & 20 deletions web/lib/use-week-up.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,35 +129,37 @@ export function projectWeekUpView(state: WeekUpState, at = new Date()): WeekUpVi
};
});
const activePlans = new Map(state.plans.filter((item) => item.removedAt === undefined).map((item) => [item.id, item]));
const timelinePlans: PlanItem[] = state.executionRecords
.filter((record) => record.revertedAt === undefined)
.flatMap((record): PlanItem[] => {
const item = activePlans.get(record.planId);
const fact = activeFacts.get(record.planId);
if (!item || !fact) return [];
const timelinePlans: PlanItem[] = state.completionFacts
.filter((fact) => fact.revertedAt === undefined)
.flatMap((fact): PlanItem[] => {
const item = activePlans.get(fact.planId);
if (!item) return [];
const currentProject = item.projectId
? state.projects.find((project) => project.id === item.projectId)
: item.sourceCourseId
? state.projects.find((project) => project.sourceCourseId === item.sourceCourseId)
: undefined;
const category = currentProject?.category ?? item.category;
const categoryColor = colorForCategory(category, state.projectCategories.find((entry) => entry.name === category)?.color);
return [{
id: record.id,
const segments = fact.actualSegments.length > 0 ? fact.actualSegments : [undefined];
return segments.map((segment, index): PlanItem => {
const anchor = segment?.startAt ?? fact.completedAt;
return {
id: segment ? `${fact.id}:actual:${index}` : fact.id,
calendarSourceId: item.id,
...(item.projectId ? { projectId: item.projectId } : {}),
scheduledDate: dateInShanghai(record.startAt),
scheduledDate: dateInShanghai(anchor),
title: item.title,
detail: item.detail,
start: timeInShanghai(record.startAt),
end: timeInShanghai(record.endAt),
timeSegments: [{
id: record.planSegmentId ?? record.id,
start: timeInShanghai(record.startAt),
end: timeInShanghai(record.endAt),
start: segment ? timeInShanghai(segment.startAt) : "时间未配置",
end: segment ? timeInShanghai(segment.endAt) : "",
timeSegments: segment ? [{
id: `${fact.id}:actual:${index}`,
start: timeInShanghai(segment.startAt),
end: timeInShanghai(segment.endAt),
completed: true,
}],
timeStatus: "scheduled",
}] : [],
timeStatus: segment ? "scheduled" : "unscheduled",
category,
categoryColor,
categoryTextColor: readableTextColor(categoryColor),
Expand All @@ -167,9 +169,9 @@ export function projectWeekUpView(state: WeekUpState, at = new Date()): WeekUpVi
completedEarly: completedBeforeSchedule(fact.completedAt, item),
rewards: item.rewards.map((reward) => ({ ...reward })),
source: item.source,
executionSource: record.source,
executionSource: fact.source,
syncState: "completed",
dayIndex: dayIndexFor(record.startAt),
dayIndex: dayIndexFor(anchor),
scheduleGroup: "completed",
rewardMode: item.rewardMode,
templateLabel: item.projectId
Expand All @@ -180,7 +182,8 @@ export function projectWeekUpView(state: WeekUpState, at = new Date()): WeekUpVi
unitLabel: item.unitKind && item.unitQuantity !== undefined
? `${item.unitQuantity} ${item.unitKind === "hour" ? "时" : item.unitKind === "lesson" ? "节" : "次"}`
: undefined,
}];
};
});
})
.sort((left, right) =>
(left.scheduledDate ?? "").localeCompare(right.scheduledDate ?? "")
Expand Down
Loading
Loading