Files
digital-pilates/store/fastingSlice.ts
richarjiang d39a32c0d8 feat(fasting): add auto-renewal and reset functionality for fasting plans
- Implement auto-renewal logic for completed fasting cycles using dayjs
- Add reset button with information modal in FastingOverviewCard
- Configure iOS push notifications for production environment
- Add expo-media-library and react-native-view-shot dependencies
- Update FastingScheduleOrigin type to include 'auto' origin
2025-10-15 19:06:18 +08:00

151 lines
4.7 KiB
TypeScript

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<FastingSchedule | null>
) => {
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);
};