The Staff class itself — granting a Member staff access via a Role — is documented in Company. This page covers what attaches to a Staff member: pay rates, bonuses, clock-in time tracking (company/staff/), and the pay-period/run layer that batches all of that into paychecks (company/payroll/).
company/staff/rate.ts. A Staffmember's pay rate. payrollScheduleID determines which PayrollSchedulethis rate's hours/bonuses get batched into when a run is processed — the pay-period recurrence itself lives on that schedule, not here.
| Field | Type | Notes |
|---|---|---|
id, staffID, companyID | string | |
payrollScheduleID | string | required |
rate | number | |
rateUnit | StaffRateUnit = "minute" | "hourly" | "daily" | "falt_rate" | "falt_rate" is a backend typo, kept as-is since it's the literal wire value — not a documentation typo, don't "fix" it client-side |
overtimeRate | number | full data only |
overtimeOver | number | full data only |
StaffRateUnitOptions — display labels: "Per Minute", "Hourly", "Daily", "Flat Rate" (the label is spelled correctly even though the underlying value is "falt_rate").
static create(company: Company, rates: CreateStaffRateData[]): Promise<EventData<string[]>>
async save(): Promise<SimpleResult<EventData>>
async delete(): Promise<SimpleResult<EventData>>
static query(company: Company, data: QueryRequest): Promise<SimpleResult<StaffRate[]>>CreateStaffRateData = { staffID: string; payrollScheduleID: string; rate?; rateUnit?; overtimeRate?; overtimeOver? }. The backend enforces one rate per (staffID, companyID).
Endpoint: /company/{cID}/staff/rates. viewHref: /company/{cID}/staff/rates/{id}.
Related-entity getters, each cached in a private field:
rate.staff // Staff | undefined, from staffID
rate.payrollSchedule // PayrollSchedule | undefined, from payrollScheduleIDParent → children convenience wrapper:
rate.bonuses(opts: QueryRequest) // StaffPayrollBonus.query filtered by rateIDcompany/staff/payrollBonus.ts. A bonus tied to a StaffRate — added into a PayrollRunLine's bonusAmountwhenever that rate's schedule is processed.
| Field | Type | Notes |
|---|---|---|
id, rateID, companyID | string | |
bonusType | StaffPayrollBonusType = "per_class" | "per_service" | "per_class_attendee" | "per_class_attendee_over_amount" | |
amount | number |
StaffPayrollBonusTypeOptions — display labels: "Per Class", "Per Service", "Per Class Attendee", "Per Class Attendee Over Amount".
static create(company: Company, bonuses: CreateStaffPayrollBonusData[]): Promise<EventData<string[]>>
async save(): Promise<SimpleResult<EventData>>
async delete(): Promise<SimpleResult<EventData>>
static query(company: Company, data: QueryRequest): Promise<SimpleResult<StaffPayrollBonus[]>>Static creator is create (lowercase), matching every other class in this doc. The backend enforces one bonus per (rateID, companyID).
Endpoint: /company/{cID}/staff/rates/bonuses. viewHref: /company/{cID}/staff/bonuses/{id}.
bonus.rate // StaffRate | undefined, from rateID — cached in a private fieldcompany/staff/timeBlock.ts. A staff clock-in/clock-out record. Open (not yet clocked out) while endedAt is unset.
| Field | Type | Notes |
|---|---|---|
id, companyID, staffID | string | |
locationID | string | null? | optional — not every clock-in needs to be tied to a location |
startedAt | Date | |
endedAt | Date | null? | unset means the block is still open |
static create(company: Company, blocks: CreateStaffTimeBlockData[]): Promise<EventData<string[]>>
async save(): Promise<SimpleResult<EventData>>
async delete(): Promise<SimpleResult<EventData>>
static query(company: Company, data: QueryRequest): Promise<SimpleResult<StaffTimeBlock[]>>Endpoint: /company/{cID}/staff/timeBlocks. viewHref: /company/{cID}/staff/timeBlocks/{id}.
timeBlock.IsActive: boolean // !this._data?.endedAtasync clockOut(): Promise<SimpleResult<EventData>>clockOut(), lowercase-c (not ClockOut() as its counterpart on the display layer is sometimes referred to) — sets endedAt to new Date() via setData({ ...this._data, endedAt: new Date() }) and calls save().Related-entity getters, each cached in a private field:
timeBlock.staff // Staff | undefined, from staffID
timeBlock.location // Location | undefined, from locationIDParent → children convenience wrapper:
timeBlock.breaks(opts: QueryRequest) // TimeBlockBreak.query filtered by timeBlockIDcompany/staff/timeBlockBreak.ts. A break within a StaffTimeBlock. Mirrors StaffTimeBlock's own active/clock-out pattern via EndBreak().
| Field | Type | Notes |
|---|---|---|
id, companyID, timeBlockID | string | |
locationID | string | null? | |
startedAt | Date | |
endedAt | Date | null? | unset means the break is still open |
static create(company: Company, breaks: CreateTimeBlockBreakData[]): Promise<EventData<string[]>>
async save(): Promise<SimpleResult<EventData>>
async delete(): Promise<SimpleResult<EventData>>
static query(company: Company, data: QueryRequest): Promise<SimpleResult<TimeBlockBreak[]>>Endpoint: /company/{cID}/staff/timeBlocks/breaks. viewHref: /company/{cID}/staff/timeBlockBreaks/{id}.
timeBlockBreak.IsActive: boolean // !this._data?.endedAt
async EndBreak(): Promise<SimpleResult<EventData>> // sets endedAt to now, saves — mirrors StaffTimeBlock.clockOut()Related-entity getters, each cached in a private field:
timeBlockBreak.timeBlock // StaffTimeBlock | undefined, from timeBlockID
timeBlockBreak.location // Location | undefined, from locationIDcompany/payroll/schedule.ts. The recurring pay period that StaffRates attach to (via payrollScheduleID) and PayrollRuns are processed against.
| Field | Type | Notes |
|---|---|---|
id, companyID | string | |
name | string | |
everyAmount | number | |
everyUnit | BillingUnit | reuses BillingUnit from Feature ("day" | "week" | "month" | "year") — the same recurrence domain used for pricing/resolve-after fields, now living here instead of on each StaffRate |
static create(company: Company, schedules: CreatePayrollScheduleData[]): Promise<EventData<string[]>>
async save(): Promise<SimpleResult<EventData>>
async delete(): Promise<SimpleResult<EventData>>
static query(company: Company, data: QueryRequest): Promise<SimpleResult<PayrollSchedule[]>>CreatePayrollScheduleData = { name: string; everyAmount?; everyUnit? }.
Endpoint: /company/{cID}/payroll/schedules. Has icon (lucide Calendar) and viewHref: /company/{cID}/payroll/schedules/{id}.
Parent → children convenience wrappers:
schedule.rates(opts: QueryRequest) // StaffRate.query filtered by payrollScheduleID — read-only from here; a rate can only be
// created from a Staff's own Rates tab, where staffID is known
schedule.runs(opts: QueryRequest) // PayrollRun.query filtered by payrollScheduleIDcompany/payroll/run.ts. One processing run over a PayrollSchedule's pay period — batches its StaffRates' worked hours/bonuses into PayrollRunLines and a payout total.
| Field | Type | Notes |
|---|---|---|
id, companyID, payrollScheduleID | string | |
periodStart / periodEnd | Date | |
status | PayrollRunStatus = "draft" | "processing" | "processed" | "paid" | |
totalAmount | number | computed by Process(), not hand-entered |
processedAt | Date | null? | full data only — only ever set by Process() |
paidAt | Date | null? | full data only — only ever set by MarkPaid() |
PayrollRunStatusOptions — display labels in lifecycle order: "Draft", "Processing", "Processed", "Paid".
static create(company: Company, runs: CreatePayrollRunData[]): Promise<EventData<string[]>>
async save(): Promise<SimpleResult<EventData>>
async delete(): Promise<SimpleResult<EventData>>
static query(company: Company, data: QueryRequest): Promise<SimpleResult<PayrollRun[]>>CreatePayrollRunData = { payrollScheduleID: string; periodStart: Date; periodEnd: Date } — a run is created as draft with just a schedule + period; totals/lines come later from Process().
Endpoint: /company/{cID}/payroll/runs. viewHref: /company/{cID}/payroll/runs/{id}.
run.schedule // PayrollSchedule | undefined, from payrollScheduleID — cached in a private fieldrun.lines(opts: QueryRequest) // PayrollRunLine.query filtered by payrollRunID — rows are system-generated by Process(), not hand-createdStatus helpers over the raw status string:
run.IsDraft: boolean
run.IsProcessing: boolean
run.IsProcessed: boolean
run.IsPaid: booleanActions:
async Process(): Promise<SimpleResult<EventData>> // POST /company/{cID}/payroll/runs/process, body { payrollRunID: this.id }
async MarkPaid(): Promise<SimpleResult<EventData>> // POST /company/{cID}/payroll/runs/pay, body { payrollRunID: this.id }Process() asks the backend to compute regular/overtime/bonus totals for every staff rate on the schedule and generate the PayrollRunLine rows — the frontend never computes these itself, it just triggers the calculation and re-reads the result. MarkPaid() marks a processed run as paid.company/payroll/runLine.ts. One staff member's computed line within a PayrollRun — worked units and their dollar amounts, split into regular/overtime/bonus. Normally system-generated by run.Process() rather than hand-created — mirrors Discount's "read-only-ish record with full CRUD methods" pattern.
| Field | Type | Notes |
|---|---|---|
id, companyID, payrollRunID, staffID, rateID | string | |
regularUnits | number | |
overtimeUnits | number | |
regularAmount | number | |
overtimeAmount | number | |
bonusAmount | number | |
totalAmount | number |
static create(company: Company, lines: CreatePayrollRunLineData[]): Promise<EventData<string[]>> // rarely called directly — see PayrollRun.Process()
async save(): Promise<SimpleResult<EventData>> // rarely called directly — lines are normally regenerated wholesale by Process()
async delete(): Promise<SimpleResult<EventData>>
static query(company: Company, data: QueryRequest): Promise<SimpleResult<PayrollRunLine[]>>Endpoint: /company/{cID}/payroll/runs/lines. viewHref: /company/{cID}/payroll/lines/{id}.
Related-entity getters, each cached in a private field:
line.run // PayrollRun | undefined, from payrollRunID
line.staff // Staff | undefined, from staffID
line.rate // StaffRate | undefined, from rateIDStaff ──has one──> StaffRate ──attached to──> PayrollSchedule
│ │
├─ bonuses ──> StaffPayrollBonus
│ │
StaffTimeBlock ─(via staffID)──────────────────────┘
(worked hours, breaks tracked via TimeBlockBreak)
│
▼
PayrollRun.Process() batches worked
StaffTimeBlocks + StaffRate + bonuses
for every rate on the schedule
│
▼
PayrollRunLine (per staff member)
│
▼
PayrollRun.MarkPaid() → status "paid"A Staff member gets one StaffRate (enforced 1:1 per (staffID, companyID)), which attaches to a PayrollSchedule and may carry a StaffPayrollBonus (also 1:1 per (rateID, companyID)). The staff member clocks in/out via StaffTimeBlock/TimeBlockBreak rows. When a PayrollRun is opened for the schedule's period and Process()d, the backend reads all of the above and writes one PayrollRunLineper staff member with the computed regular/overtime/bonus amounts — the client only ever triggers this and re-fetches, it doesn't do the math. MarkPaid() then closes out the run.