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.

Israeli labor law grants workers an additional bonus on top of their regular pay when they work during evening or night hours. Shiftly models these as two additive segments — a 20% evening bonus and a 50% night bonus — that overlay the regular pay timeline rather than replacing it. A worker who logs hours in the evening receives both their regular rate (100%/125%/150%) and the 20% bonus simultaneously.

Bonus Brackets at a Glance

BonusKeyTime WindowAdditive Rate
Nighthours5000:00 – 06:00+50%
Eveninghours2014:00 – 22:00+20%
Nighthours5022:00 – 24:00+50%
The 22:00–24:00 window and the 00:00–06:00 window both map to the same hours50 key. Hours in either window are summed into the same Segment, giving a single combined total for all night-bonus hours in a day.

Type Definitions

interface Segment {
  percent: number; // additive bonus multiplier (0.2 or 0.5)
  hours: number;   // hours accumulated in this bonus window
}

interface ExtraBreakdown {
  hours20: Segment; // 20% evening bonus — hours in the 14:00–22:00 window
  hours50: Segment; // 50% night bonus  — hours in 00:00–06:00 and 22:00–24:00
}

How Segments Are Labeled: ShiftSegmentResolver

Before ExtraCalculator ever runs, the ShiftSegmentResolver slices the shift’s Point (start/end in minutes from midnight) against a day-type-specific segment map. For a Regular day, the relevant bonus windows in the map are:
// Regular day segment map (extract — bonus windows only)
[
  {
    point: { start: 0,    end: 360  },  // 00:00–06:00
    percent: 0.5,
    key: "hours50",
  },
  {
    point: { start: 840,  end: 1320 },  // 14:00–22:00
    percent: 0.2,
    key: "hours20",
  },
  {
    point: { start: 1320, end: 1800 },  // 22:00–30:00 (next-day 06:00)
    percent: 0.5,
    key: "hours50",
  },
]
The resolver intersects the shift’s time window with each map entry, yielding a list of LabeledSegmentRange objects:
interface Point {
  start: number; // minutes from midnight (beginning of range)
  end: number;   // minutes from midnight (end of range)
}

interface LabeledSegmentRange {
  point: Point;   // the clipped intersection of shift and map entry
  percent: number;
  key: string;    // "hours20" | "hours50" | "hours100" | "shabbat150" | "shabbat200"
}

ExtraCalculator

ExtraCalculator receives the complete LabeledSegmentRange[] list from ShiftSegmentResolver and filters by key, converting minute-ranges to fractional hours:
class ExtraCalculator
  implements
    Calculator<LabeledSegmentRange[], ExtraBreakdown>,
    Reducer<ExtraBreakdown>
{
  private readonly fieldShiftPercent: Record<string, number> = {
    hours20: 0.2,
    hours50: 0.5,
  };

  calculate(labeledSegments: LabeledSegmentRange[]): ExtraBreakdown {
    const sum = (key: string) =>
      labeledSegments
        .filter((s) => s.key === key)
        .reduce((acc, seg) => acc + (seg.point.end - seg.point.start) / 60, 0);

    return {
      hours20: { percent: this.fieldShiftPercent.hours20, hours: sum("hours20") },
      hours50: { percent: this.fieldShiftPercent.hours50, hours: sum("hours50") },
    };
  }
}
The sum helper:
  1. Filters labeledSegments to only those matching the target key.
  2. Sums (end − start) / 60 across all matching segments to convert minutes to hours.
  3. This naturally merges both hours50 windows (00:00–06:00 and 22:00–24:00) into one total.

Additive Nature of Bonuses

The evening and night bonuses are additive, meaning they are added on top of whatever regular-rate bracket the hours fall into. They are never a replacement rate.
Final effective rate for a given hour =
  Regular bracket rate (100% | 125% | 150%)
  + Bonus rate (0% | 20% | 50%)
Example — a shift from 21:00 to 23:00 on a regular weekday:
Time windowRegular bracketBonusEffective rate
21:00–22:00100% (within standard hours)+20% (evening)120%
22:00–23:00100% (within standard hours)+50% (night)150%
The RegularBreakdown and ExtraBreakdown are stored as separate fields in BasePayMap — the UI and the salary calculation engine combine them when rendering the final pay figure.

Accumulation Across Shifts

When multiple shifts are logged on the same day, ExtraCalculator also implements a Reducer interface to accumulate results:
accumulate(base: ExtraBreakdown, add: ExtraBreakdown): ExtraBreakdown {
  return {
    hours20: {
      percent: base.hours20.percent,
      hours: base.hours20.hours + add.hours20.hours,
    },
    hours50: {
      percent: base.hours50.percent,
      hours: base.hours50.hours + add.hours50.hours,
    },
  };
}
Because bonus windows are defined by clock time (not by position within the shift), two separate shifts that both span midnight independently accumulate night-bonus hours. The accumulation step simply adds their hours50 totals together.

Bonus Windows on Special Days

On partial special days (SpecialPartialStart — Fridays and holiday eves), the evening bonus window is shortened: it runs from 14:00 to the special-start time (17:00 or 18:00) instead of continuing to 22:00. After the special-start time, the segment map switches to shabbat150 / shabbat200 keys, which are handled by SpecialCalculator rather than ExtraCalculator. On full special days (SpecialFull — Shabbat and paid holidays), the segment map contains only shabbat150 and shabbat200 entries. No hours20 or hours50 segments exist, so ExtraCalculator returns zero for both bonus fields. See Special Days for the complete breakdown of Shabbat and holiday pay windows.

Build docs developers (and LLMs) love