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.

Reducers are the incremental state management layer of Shiftly’s domain. When a user edits a shift, only the affected day needs to be recalculated — not the entire month. Reducers make this efficient by supporting three operations: creating an empty baseline state, accumulating a new contribution into the running total, and subtracting a prior contribution when it is replaced or removed. The result is a MonthPayMap that always reflects the sum of all days recorded so far, updated in O(1) relative to the number of days in the month.

The Reducer Interface

export interface Reducer<State, Input = State> {
  createEmpty(): State;
  accumulate(base: State, add: Input): State;
  subtract(base: State, sub: Input): State;
}
State is the type being maintained (e.g. MonthPayMap, MealAllowance). Input defaults to State but can differ — MonthPayMapReducer uses WorkDayMap as its input type while maintaining MonthPayMap as its state.

Why Reducers Exist

When a shift is edited:
  1. The day’s old WorkDayMap (stored in Redux) is subtracted from the running MonthPayMap.
  2. The day’s newly computed WorkDayMap is accumulated into the same MonthPayMap.
This subtract → accumulate pattern avoids iterating over all 30+ days whenever a single shift changes. All subtract operations clamp at 0 to prevent negative hour counts from floating-point rounding.

Reducers Overview

MonthPayMapReducer

The top-level orchestrator: accumulates and subtracts a full WorkDayMap into the running MonthPayMap by delegating to sub-reducers.

RegularByMonthAccumulator

Accumulates RegularBreakdown values across shifts within a day, and across days within the month.

FixedSegmentMonthReducer

Tracks sick-day hours, vacation hours, and extra-Shabbat hours across all days of the month.

MealAllowanceMonthReducer

Accumulates meal allowance points and monetary amounts (large and small) across the month.

WorkDayMonthReducer

Accumulates regular, extra, and special pay breakdowns from each day’s workMap into the monthly totals.

MonthPayMapReducer

MonthPayMapReducer is the root reducer for the monthly payslip state. It implements Reducer<MonthPayMap, WorkDayMap> and coordinates four sub-reducers, each responsible for a distinct slice of MonthPayMap.
class MonthPayMapReducer implements Reducer<MonthPayMap, WorkDayMap> {
  constructor(
    private readonly workPay: WorkDayMonthReducer,
    private readonly fixed: FixedSegmentMonthReducer,
    private readonly allowances: MealAllowanceMonthReducer,
    private readonly perDiem: PerDiemMonthReducer,
  ) {}
}

State Shape

export interface MonthPayMap {
  // from WorkDayMonthReducer
  regular: RegularBreakdown;
  extra: ExtraBreakdown;
  special: SpecialBreakdown;

  // from FixedSegmentMonthReducer
  hours100Sick: Segment;
  hours100Vacation: Segment;
  extra100Shabbat: Segment;

  // from MealAllowanceMonthReducer
  mealAllowance: MealAllowance;

  // from DefaultPerDiemMonthCalculator
  perDiem: PerDiemInfo;

  totalHours: number;
}

Methods

createEmpty(): MonthPayMap
// Delegates createEmpty() to each sub-reducer and assembles the full zero state.

accumulate(base: MonthPayMap, add: WorkDayMap): MonthPayMap
// Spreads results from all sub-reducers' accumulate() calls.
// totalHours: base.totalHours + add.totalHours

subtract(base: MonthPayMap, sub: WorkDayMap): MonthPayMap
// Spreads results from all sub-reducers' subtract() calls.
// totalHours: Math.max(base.totalHours - sub.totalHours, 0)

RegularByMonthAccumulator

RegularByMonthAccumulator extends BaseRegularCalculator and inherits its accumulate and subtract implementations. It is used as the month-level reducer for regular hours, accumulating each day’s RegularBreakdown contribution into the running MonthPayMap.
class RegularByMonthAccumulator extends BaseRegularCalculator {}
// Instantiated via: RegularFactory.monthReducer()
The underlying accumulation logic:
accumulate(base: RegularBreakdown, add: RegularBreakdown): RegularBreakdown {
  return {
    hours100: { percent: base.hours100.percent, hours: base.hours100.hours + add.hours100.hours },
    hours125: { percent: base.hours125.percent, hours: base.hours125.hours + add.hours125.hours },
    hours150: { percent: base.hours150.percent, hours: base.hours150.hours + add.hours150.hours },
  };
}

subtract(base: RegularBreakdown, sub: RegularBreakdown): RegularBreakdown {
  return {
    hours100: { percent: base.hours100.percent, hours: Math.max(base.hours100.hours - sub.hours100.hours, 0) },
    hours125: { percent: base.hours125.percent, hours: Math.max(base.hours125.hours - sub.hours125.hours, 0) },
    hours150: { percent: base.hours150.percent, hours: Math.max(base.hours150.hours - sub.hours150.hours, 0) },
  };
}

FixedSegmentMonthReducer

FixedSegmentMonthReducer accumulates the three fixed-rate segments that appear as separate payslip line items rather than as part of the regular overtime calculation.
class FixedSegmentMonthReducer {
  constructor(private readonly fixed: FixedSegmentBundle) {}

  createEmpty(): {
    hours100Sick: Segment;
    hours100Vacation: Segment;
    extra100Shabbat: Segment;
  }

  accumulate(base: MonthPayMap, add: WorkDayMap): { ... }
  subtract(base: MonthPayMap, sub: WorkDayMap): { ... }
}
Each field uses FixedSegmentFactory.create(hours) to construct the Segment — ensuring the percent field is always 1.0 regardless of the hours accumulated.
FieldSourceMeaning
hours100SickWorkDayMap.hours100SickSick-day hours paid at standard rate
hours100VacationWorkDayMap.hours100VacationVacation-day hours paid at standard rate
extra100ShabbatWorkDayMap.extra100ShabbatShabbat/holiday shift hours for separate line tracking

MealAllowanceMonthReducer

MealAllowanceMonthReducer accumulates meal allowance across all days of the month, maintaining separate running totals for large and small allowances.
class MealAllowanceMonthReducer implements Reducer<MealAllowance> {
  createEmpty(): MealAllowance
  accumulate(base: MealAllowance, add: MealAllowance): MealAllowance
  subtract(base: MealAllowance, sub: MealAllowance): MealAllowance
}
export interface MealAllowance {
  small: MealAllowanceEntry;
  large: MealAllowanceEntry;
}

export interface MealAllowanceEntry {
  points: number;
  amount: number; // ILS
}
Both points and amount are accumulated independently, so the month-level total reflects the sum of each day’s entitlement. Subtraction clamps both fields at 0.

WorkDayMonthReducer

WorkDayMonthReducer accumulates the three core pay breakdowns — regular, extra, and special — from each day’s workMap into the running monthly state.
class WorkDayMonthReducer implements Reducer<WorkPayPart, WorkDayMap> {
  constructor(private readonly workDay: WorkDayReducerBundle) {}

  createEmpty(): { regular: RegularBreakdown; extra: ExtraBreakdown; special: SpecialBreakdown }
  accumulate(base: MonthPayMap, add: WorkDayMap): { ... }
  subtract(base: MonthPayMap, sub: WorkDayMap): { ... }
}

// WorkDayReducerBundle is:
export type WorkDayReducerBundle = {
  regular: Reducer<RegularBreakdown>;
  extra: Reducer<ExtraBreakdown>;
  special: Reducer<SpecialBreakdown>;
};
It reads from add.workMap.regular, add.workMap.extra, and add.workMap.special — the shift-level computation results embedded in the day’s WorkDayMap.

The Subtract + Accumulate Pattern

The Redux action addDayPayMap in globalSlice.ts demonstrates the canonical update pattern that all reducers are designed for:
addDayPayMap: (
  state,
  action: PayloadAction<{ dateKey: string; dayPayMap: WorkDayMap }>
) => {
  const { dateKey, dayPayMap } = action.payload;
  const prev = state.dailyPayMaps[dateKey];

  // 1. If a previous result exists for this date, remove its contribution
  if (prev) {
    state.globalBreakdown = payMap.monthPayMapCalculator.subtract(
      state.globalBreakdown,
      prev,
    );
  }

  // 2. Add the new computation result
  state.globalBreakdown = payMap.monthPayMapCalculator.accumulate(
    state.globalBreakdown,
    dayPayMap,
  );

  // 3. Store the new result for future subtract operations
  state.dailyPayMaps[dateKey] = dayPayMap;
},
This means MonthPayMap is always the exact sum of all stored WorkDayMap values — no full month recomputation is ever needed, regardless of how many shifts have been recorded.
Because all subtract operations clamp at 0, the order of operations is safe even if floating-point rounding causes minor discrepancies between accumulated and subtracted hour values. The monthly total will never go negative.

Build docs developers (and LLMs) love