Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/dmaman86/shiftly/llms.txt

Use this file to discover all available pages before exploring further.

Shiftly manages all shared application state through Redux Toolkit. The store is composed of two slices: globalSlice, which owns the user’s pay configuration, the per-day pay map cache, and the running monthly aggregate; and workDaysSlice, which holds the generated list of work-day metadata for the currently selected month. React components interact with the store exclusively through three custom hooks — useGlobalState, useWorkDays, and useDomain — keeping dispatch and selector logic out of component bodies.

Store Setup

The Redux store is created in src/redux/store.ts using configureStore from Redux Toolkit:
import { configureStore } from "@reduxjs/toolkit";
import workDaysReducer from "./states/workDaysSlice";
import globalReducer from "./states/globalSlice";

export const store = configureStore({
  reducer: {
    workDays: workDaysReducer,
    global: globalReducer,
  },
});

export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
The RootState and AppDispatch types are derived directly from the store instance so that all selectors and dispatch calls remain fully type-safe without manual maintenance. The store is provided to the React tree by the AppProviders component (src/app/providers/AppProviders.tsx), which wraps the entire application in a Redux <Provider> alongside the MUI theme, Emotion cache (with RTL support via stylis-plugin-rtl), snackbar, domain, and date-picker providers:
return (
  <DirectionContext.Provider value={{ direction, setDirection }}>
    <Provider store={store}>
      <CacheProvider value={direction === "rtl" ? rtlCache : ltrCache}>
        <ThemeProvider theme={theme}>
          <AppSnackbarProvider>
            <DomainProvider>
              <LocalizationProvider dateAdapter={AdapterDateFns}>
                <CssBaseline />
                {children}
              </LocalizationProvider>
            </DomainProvider>
          </AppSnackbarProvider>
        </ThemeProvider>
      </CacheProvider>
    </Provider>
  </DirectionContext.Provider>
);

globalSlice

globalSlice is the primary slice. It tracks the user’s pay configuration, a dictionary of per-day pay maps keyed by date string, and the accumulated monthly breakdown.

State shape

export interface GlobalState {
  config: {
    standardHours: number;  // daily standard-hour threshold (default: 6.67)
    baseRate: number;       // hourly base rate in currency units (default: 0)
    year: number;           // currently selected year
    month: number;          // currently selected month (1–12)
  };
  dailyPayMaps: Record<string, WorkDayMap>;  // dateKey → WorkDayMap
  globalBreakdown: MonthPayMap;              // running monthly aggregate
}

Initial state

The slice initialises with standardHours: 6.67 (the Israeli labor law default for a six-day work week), baseRate: 0 (the user must enter their rate), and the current calendar year and month derived from new Date() at module load time. The globalBreakdown is seeded with an empty MonthPayMap created by the domain’s monthPayMapCalculator.createEmpty().

Actions

ActionPayloadDescription
setYear(year)numberUpdates config.year and clears all daily pay maps and the monthly aggregate via resetMonthData
setMonth(month)numberUpdates config.month and clears all daily pay maps and the monthly aggregate via resetMonthData
setStandardHours(hours)numberUpdates the daily standard-hour threshold used in overtime tier calculations
setBaseRate(rate)numberUpdates the hourly base rate used to compute pay amounts
addDayPayMap({ dateKey, dayPayMap }){ dateKey: string; dayPayMap: WorkDayMap }Subtracts the previous contribution for that date (if any) from globalBreakdown, then accumulates the new dayPayMap into globalBreakdown and stores it in dailyPayMaps
removeDayPayMap(dateKey)stringSubtracts the stored day’s contribution from globalBreakdown and removes it from dailyPayMaps
resetGlobal()Clears all entries in dailyPayMaps and resets globalBreakdown to an empty MonthPayMap
Dispatching setYear or setMonth automatically calls the internal resetMonthData helper, which clears dailyPayMaps and resets globalBreakdown to an empty structure. This prevents stale pay data from a previous month or year from persisting after the user changes the calendar period.

The addDayPayMap subtract-then-accumulate pattern

When a user saves or updates the shifts for a given day, addDayPayMap is dispatched. The reducer first checks whether a WorkDayMap already exists for that date key. If one does, it subtracts the previous contribution from the running monthly total before accumulating the new one. This ensures globalBreakdown always reflects exactly the current set of saved days, with no double-counting when a day is edited:
addDayPayMap: (
  state,
  action: PayloadAction<{ dateKey: string; dayPayMap: WorkDayMap }>
) => {
  const { dateKey, dayPayMap } = action.payload;
  const prev = state.dailyPayMaps[dateKey];

  if (prev) {
    state.globalBreakdown = payMap.monthPayMapCalculator.subtract(
      state.globalBreakdown,
      prev,
    );
  }

  state.globalBreakdown = payMap.monthPayMapCalculator.accumulate(
    state.globalBreakdown,
    dayPayMap,
  );

  state.dailyPayMaps[dateKey] = dayPayMap;
},
The subtract and accumulate methods are pure functions from the domain’s monthPayMapCalculator — Redux Toolkit’s Immer integration allows the reassignment of state.globalBreakdown to a new value returned by those functions without violating immutability constraints.

resetMonthData helper

The private resetMonthData function is used by both setYear and setMonth to keep the reset logic in one place:
const resetMonthData = (state: GlobalState) => {
  state.globalBreakdown = payMap.monthPayMapCalculator.createEmpty();
  state.dailyPayMaps = {};
};

workDaysSlice

workDaysSlice stores the list of WorkDayInfo objects generated for the selected month. Each WorkDayInfo carries a meta object (date string, day type, cross-day continuation flag) and a Hebrew day label used in the UI calendar.
export type WorkDaysState = {
  year: number | null;
  month: number | null;
  workDays: WorkDayInfo[];
};
The slice exposes a single action, setWorkDays, which accepts a year, month, and eventMap (a map of date strings to event labels for public holidays), then delegates to payMap.workDaysMonthBuilder.build(...) to produce the full array of work days for that month:
setWorkDays: (
  state,
  actions: PayloadAction<{
    year: number;
    month: number;
    eventMap: Record<string, string[]>;
  }>
) => {
  const { year, month, eventMap } = actions.payload;
  state.year = year;
  state.month = month;
  state.workDays = payMap.workDaysMonthBuilder.build({ year, month, eventMap });
},

React Hooks

Three custom hooks encapsulate all Redux interactions. Components import these hooks and never call useSelector or useDispatch directly.

useGlobalState()

useGlobalState selects every field from state.global and provides typed action dispatchers for all seven mutations. It is the primary hook for the salary configuration panel and the monthly summary view.
const {
  // state
  year,
  month,
  standardHours,
  baseRate,
  globalBreakdown,
  dailyPayMaps,

  // actions
  updateYear,
  updateMonth,
  updateStandardHours,
  updateBaseRate,
  addDay,
  removeDay,
  reset,
} = useGlobalState();

useWorkDays()

useWorkDays selects state.workDays.workDays and wraps the setWorkDays action in a generate(year, month, eventMap) function. It also exposes four derived read helpers built on top of the raw workDays array:
const {
  workDays,        // WorkDayInfo[]
  generate,        // (year, month, eventMap) => void
  getDayInfo,      // (date: string) => WorkDayInfo | undefined
  isSpecialFullDay,     // (date: string) => boolean
  isPartialHolidayDay,  // (date: string) => boolean
  isCrossDaySpecial,    // (date: string) => boolean
} = useWorkDays();
isSpecialFullDay returns true when the day’s typeDay is WorkDayType.SpecialFull (Shabbat or a full public holiday). isPartialHolidayDay returns true for WorkDayType.SpecialPartialStart (e.g., Friday evening or a holiday eve). isCrossDaySpecial returns true when crossDayContinuation is set, indicating the day continues a special-day classification that started the previous evening.

useDomain()

useDomain reads the domain pipeline instance from DomainContext, which is provided by DomainProvider inside AppProviders. It throws a clear error if called outside the provider tree:
import { useContext } from "react";
import { DomainContext } from "@/app";

export const useDomain = () => {
  const ctx = useContext(DomainContext);
  if (!ctx) throw new Error("useDomain must be used within DomainProvider");
  return ctx;
};
The context value exposes the same PayMapPipeline instance used by the Redux slices, giving components direct access to domain builders, calculators, and resolvers when they need to perform calculations outside of a dispatch cycle (for example, previewing shift pay before saving).

Build docs developers (and LLMs) love