import { FASTING_PLANS, FastingPlan, getPlanById } from '@/constants/Fasting'; import { calculateFastingWindow, getFastingPhase } from '@/utils/fasting'; import { createSlice, PayloadAction } from '@reduxjs/toolkit'; import dayjs from 'dayjs'; import type { RootState } from './index'; export type FastingScheduleOrigin = 'manual' | 'recommended' | 'quick-start' | 'auto'; export type FastingSchedule = { planId: string; startISO: string; endISO: string; createdAtISO: string; updatedAtISO: string; origin: FastingScheduleOrigin; }; type FastingState = { activeSchedule: FastingSchedule | null; history: FastingSchedule[]; }; const initialState: FastingState = { activeSchedule: null, history: [], }; const fastingSlice = createSlice({ name: 'fasting', initialState, reducers: { hydrateActiveSchedule: ( state, action: PayloadAction ) => { const incoming = action.payload; if (!incoming) { state.activeSchedule = null; return; } if (state.activeSchedule) { const currentUpdated = dayjs(state.activeSchedule.updatedAtISO ?? state.activeSchedule.startISO); const incomingUpdated = dayjs(incoming.updatedAtISO ?? incoming.startISO); if (currentUpdated.isSame(incomingUpdated) || currentUpdated.isAfter(incomingUpdated)) { return; } } state.activeSchedule = incoming; }, scheduleFastingPlan: ( state, action: PayloadAction<{ planId: string; start: string; origin?: FastingScheduleOrigin }> ) => { const plan = getPlanById(action.payload.planId); if (!plan) return; const startDate = new Date(action.payload.start); const { start, end } = calculateFastingWindow(startDate, plan.fastingHours); const nowISO = new Date().toISOString(); state.activeSchedule = { planId: plan.id, startISO: start.toISOString(), endISO: end.toISOString(), createdAtISO: nowISO, updatedAtISO: nowISO, origin: action.payload.origin ?? 'manual', }; }, rescheduleActivePlan: ( state, action: PayloadAction<{ start: string; origin?: FastingScheduleOrigin }> ) => { if (!state.activeSchedule) return; const plan = getPlanById(state.activeSchedule.planId); if (!plan) return; const startDate = new Date(action.payload.start); const { start, end } = calculateFastingWindow(startDate, plan.fastingHours); state.activeSchedule = { ...state.activeSchedule, startISO: start.toISOString(), endISO: end.toISOString(), updatedAtISO: new Date().toISOString(), origin: action.payload.origin ?? state.activeSchedule.origin, }; }, setRecommendedSchedule: ( state, action: PayloadAction<{ planId: string; recommendedStart: string }> ) => { const plan = getPlanById(action.payload.planId); if (!plan) return; const startDate = new Date(action.payload.recommendedStart); const { start, end } = calculateFastingWindow(startDate, plan.fastingHours); const nowISO = new Date().toISOString(); state.activeSchedule = { planId: plan.id, startISO: start.toISOString(), endISO: end.toISOString(), createdAtISO: nowISO, updatedAtISO: nowISO, origin: 'recommended', }; }, completeActiveSchedule: (state) => { if (!state.activeSchedule) return; const phase = getFastingPhase( new Date(state.activeSchedule.startISO), new Date(state.activeSchedule.endISO) ); if (phase === 'fasting') { // Allow manual completion only when fasting window已经结束 state.activeSchedule = { ...state.activeSchedule, endISO: dayjs().toISOString(), updatedAtISO: new Date().toISOString(), }; } state.history.unshift(state.activeSchedule); state.activeSchedule = null; }, clearActiveSchedule: (state) => { state.activeSchedule = null; }, }, }); export const { hydrateActiveSchedule, scheduleFastingPlan, rescheduleActivePlan, setRecommendedSchedule, completeActiveSchedule, clearActiveSchedule, } = fastingSlice.actions; export default fastingSlice.reducer; export const selectFastingRoot = (state: RootState) => state.fasting; export const selectActiveFastingSchedule = (state: RootState) => state.fasting.activeSchedule; export const selectFastingPlans = () => FASTING_PLANS; export const selectActiveFastingPlan = (state: RootState): FastingPlan | undefined => { const schedule = state.fasting.activeSchedule; if (!schedule) return undefined; return getPlanById(schedule.planId); };