Skip to content

Timesheets and time cards (Vendor)

Vendor Timesheets (/dashboard/hr/timesheet) lists clocked time by day or pay period for the selected store. Managers with the employee module can add a time card from the day view. That create flow calls POST /vendor/attendance and now persists Clover-style details: clock in/out, paid/unpaid breaks, cash/credit tips, an optional role override, punch events, and an edit-history row.

TablePurpose
vendor_attendance_daysOne row per employee per calendar day (store timezone). New columns: employee_role_id, cash_tips_cents, credit_tips_cents, paid_break_minutes, unpaid_break_minutes. break_minutes remains the unpaid total used for production-hour math.
vendor_attendance_punchesCheck-in / check-out events. Manual time cards write device_label = manual_timecard.
vendor_attendance_breaksPaid or unpaid intervals (kind, start_at, end_at, duration_minutes).
vendor_attendance_validationsPhoto / GPS evidence (empty on create from the dashboard modal).
vendor_attendance_edit_logsCreate/update audit (action, snapshot JSON, actor_user_id as public char(13) JWT subject — not a legacy bigint)

Unpaid break minutes are subtracted from worked time. Paid breaks do not reduce production minutes.

The Filters button on the day timesheet opens a right-hand drawer (same pattern as the employee list). The drawer holds Team members, Roles, and Issues. Edits stay local until Show results; Reset filters clears them. Applied values are stored in the URL (q, roles, issues) so they are shareable.

Needs review uses those same Filters as All, plus a required Issues constraint. All lists every time card (and leave) for the day. Needs review is a subset of that list — only rows that already have an issue (late, stored absent, uninformed, overtime, missed clock-out, or pending leave). Employees with no time card are not treated as “absent” issues, so Needs review is never larger than All.

Choosing specific Issues in the drawer further narrows both tabs the same way.

GET /api/v1/vendor/attendance accepts:

  • employee_role_id — one id or a comma-separated list
  • issuelate, absent, uninformed, overtime, missed_clock_out (derived from the day row; not a stored column)

pending_leave is applied in the UI after merging leave cells. Pass timezone with date so the calendar day matches the store.

Day-view Actions on a time-card row:

  • Resolve issue (check icon) — shown when the row has Issues; calls POST /vendor/attendance/:id/resolve-issues to acknowledge open issues without opening the edit modal. Any active note is moved into edit history.
  • Delete time card (trash icon) — shown when a stored attendance day exists; hover tooltip, then a confirm modal, then DELETE /vendor/attendance/:id

Hovering a row tints the background so the active row is obvious.

POST /api/v1/vendor/attendance (employee module). Body includes existing clock fields plus:

  • employee_role_id (optional; the add/edit role field can be cleared. Omitted values default to the employee’s current role)
  • cash_tips_cents / credit_tips_cents (integer cents)
  • breaks[] with kind (paid | unpaid), start_at, end_at (ISO-8601)

Breaks must fall inside the clock-in / clock-out window. Duplicate employee+date rows still return 400.

Pass timezone (store dashboard_prefs.timezone) so the calendar day matches the timesheet UI.

Clicking a day-view row opens the Clover edit time card modal (40rem, date + name header, Delete, overtime warning, break duration + remove, tips, notes, photo/GPS, edit history). Save changes stays disabled until the form differs from the loaded card.

  • GET /api/v1/vendor/attendance/days/:id?store_id=&timezone= returns the day plus breaks[], edit_logs[], and validation_count.
  • PATCH /api/v1/vendor/attendance/:id (employee module) updates clock in/out, role, tips, and replaces breaks[]. It recalculates late/overtime/production and appends an updated edit-log row.
  • DELETE /api/v1/vendor/attendance/:id (employee module) removes the day (punches, breaks, validations, and logs cascade).

POST /api/v1/vendor/attendance/:id/resolve-issues (employee module) acknowledges the currently open derived issues (late, absent, uninformed, overtime, missed clock-out) for that day. Resolved keys are stored on the attendance day as resolved_issue_keys so the row no longer appears under Needs review until clock data changes. If the day has an active note, it is appended to edit history (action: issue_resolved) and cleared from notes.

The header Time Clock settings control opens a full-screen form matching the Clover layout. Settings are stored on the store as dashboard_prefs.time_clock (all flags default to yes / recommended):

FlagPersistRuntime behavior
clock_in_reminderYesPref only today — no reminder push/UI yet
prevent_open_order_clock_outsYesEnforced — punch-out returns 400 open_orders while a pending order exists
server_bankingYesPref only — not shown in Time Clock settings modal (still stored/API)
import_credit_tipsYesPref only — not shown in Time Clock settings modal (still stored/API)
auto_clock_in_on_work_shiftYesEnforced — open / join work shift calls ensureClockInForEmployees when on
auto_clock_out_on_work_shiftYesEnforced — close work shift clocks out only when the person has no other open work shift at the store
auto_clock_in_on_scheduleYesEnforced — cron converts published schedule → clock-in at schedule start (store timezone)
auto_clock_out_on_scheduleYesEnforced — cron clocks out at schedule end; skipped while a POS work shift is still open

GET / PATCH /api/v1/vendor/attendance/time-clock-settings (employee module) read and update that slice. PATCH merges into existing dashboard_prefs so timezone / POS prefs are left intact.

Every 5 minutes (CRON_SCHEDULE_AUTO_ATTENDANCE_*; disable with CRON_SCHEDULE_AUTO_ATTENDANCE_ENABLED=0):

  1. Materialize active shift-template assignments into vendor_attendance_schedule_shifts for today → +13 days (store TZ). New rows use note = auto_template:{templateId} when none exist yet. Existing auto-template rows are updated so start_time / end_time track current vendor_availability_slots (stale template time_start/time_end windows are rewritten). Manual / POS-published rows are left alone. Days that already have a timecard do not get a new auto row, but an existing auto row on that day is still reconciled. Template chips / materialize skip calendar days before the template’s effective_on (set on the shift-template detail Effective on picker).
  2. Clock-in when now >= start and now < end and auto_clock_in_on_schedule is on (device_label = schedule_auto_in, punch time = schedule start), unless the employee has approved time off overlapping that shift date. Planned breaks from the matching availability slot (by shift-template id + weekday, falling back when start/end differ) are copied onto the timecard when it has none yet. Re-running Refresh on a schedule-auto-only day realigns check-in to the corrected window and seeds breaks if still empty.
  3. Clock-out when now >= end and auto_clock_out_on_schedule is on and the employee is not on an open POS work shift (device_label = schedule_auto_out, punch time = schedule end) — also skipped on approved time-off days.

POS open/close still uses the work-shift flags independently of the schedule cron.

The timesheet Day toolbar Refresh control calls POST /api/v1/vendor/attendance/schedule-auto/run (employee module, body { store_id }) with createTimecardsForAllPublished. That path materializes like cron, then creates (or seeds) a timecard for every published scheduled/open schedule row for yesterday–today when auto_clock_in_on_schedule is on — even if wall-clock is outside the shift window (punch time still = schedule start). Clock-out still only runs when now >= end. Cron keeps the in-window gate. Response includes materialized, clockedIn, and clockedOut counts.

  • POST /api/v1/vendor/attendance/batch/resolve-issues (employee module) — body { ids: string[] } (max 200). Acknowledges open derived issues on each day the same way as the single-id resolve route. Returns { resolved, skipped, total }.
  • POST /api/v1/vendor/attendance/batch/delete (employee module) — body { ids: string[] } (max 200). Deletes each attendance day the caller can access. Returns { deleted, skipped, total }.

The day table supports row checkboxes; when any rows are selected, the toolbar Refresh / Filters controls swap to a bulk actions group (Resolve issues / Delete) while the date and review tabs stay visible.

The header Set pay schedule control opens a centered Clover card (37.5rem wide, fields inset 13.75rem). Settings are stored on the store as dashboard_prefs.pay_schedule:

  • frequencyweek, two_weeks, twice_month, or month (default every week)
  • period_start — weekday 06 (Sunday–Saturday) for weekly / two-week; 1_15 or 16_last for twice a month; 128 or last for monthly (default Sunday)

GET / PATCH /api/v1/vendor/attendance/pay-schedule (employee module) read and update that slice. PATCH merges into existing dashboard_prefs so timezone / time-clock / POS prefs stay intact.

The pay-period View control opens a Clover-style picker: search team members, sort (first name / last name / role), group (team member / role), visible columns (14/23 by default), show members without hours, and collapse grouped rows. Columns without stored hours or wages render until that data exists.

Add time card on both the day and pay-period views opens the same create modal (POST /vendor/attendance). Pay period defaults the date to today when the current month is selected, or the 1st of the selected month otherwise.

The attendance Schedule add-item panel can create time off for a team member. That flow calls POST /vendor/leaves (same create contract as the Time Off page): store_id, vendor_employee_id, leave_type (mapped from category), date_from / date_to (store calendar day), session (full_day or first_half), optional start_time / end_time (HH:mm) when Whole Day is off, free-text reason, and timezone from store dashboard_prefs.timezone.

The Time Off page Add / Edit time off modal (dashboard-leaves-add-time-off-modal) uses the same contract: Edit (or turning off Whole Day) shows Start Time / End Time pickers; Total Hours is derived from the clock window (or from the date range for whole-day leave).

On the Time Off requests table, Total hours prefers start_time / end_time duration when both are set (instead of always showing 4h / 8h from session / day_count). The Dates column also shows the clock window under the calendar day. Row actions include Edit (opens the same modal) plus Approve / Decline when the actor can approve.

The reason list is the Homebase set (vacation paid/unpaid, holiday, jury duty, sick leave, personal/family medical emergency, other). The UI label plus optional notes are concatenated into reason.

Vendor Schedule (/dashboard/attendance / timesheet) loads team members, overlapping time off, published shifts, attendance, and template-generated shifts for the visible range via GET /api/v1/vendor/attendance/schedule. Pass store_id, date_from, date_to, and timezone (store dashboard_prefs.timezone). The response is capped (200 employees, 500 leave rows) and excludes declined time off.

When an employee is a member of an active shift template (vendor_shift_template_assignees) and the day has no published shift, schedule-visible time off (pending / approved / new), or attendance row, the API synthesizes chips from that template:

  1. Use weekday windows on vendor_availability_slots where shift_template_id matches the template (not template_id, which is for employee availability). Times always come from those slots — not the template’s time_start / time_end.
  2. If the template has slots but none for that weekday, that day stays empty (day off).
  3. If the template has no slots yet, the day stays empty until slots are configured on the shift schedule.

Each item includes shift_template_id, shift_template_name, times as HH:mm, and optional is_no_working when the slot kind is unavailable.

When a stored timecard already exists for emp|date on today or in the past, getSchedule omits published shifts and template_shifts for that day so the grid shows the clocked card (Worked / Working) instead of the schedule chip. Future attendance rows do not hide shifts — the grid keeps showing Shift / Off until that calendar day arrives. The same omit applies to auto-materialized published rows (note prefix auto_template:) on days with schedule-visible time off, so leave does not stack under a stale auto Shift. Materialize / Refresh also deletes those stale auto rows under leave.

Materialize persists template Off (unavailable / is_no_working) windows into published rows (and flips stale auto Working rows to Off) so the schedule chip shows Off / No working after Refresh. Auto clock-in/out still skips Off rows.

Saving an already-published shift from the edit modal calls PATCH /api/v1/vendor/attendance/schedule/shifts/:id with start_time, end_time, note, is_no_working (Working ↔ Off), and optional vendor_employee_id (Trade — reassign to another store employee). Publish (POST …/schedule/shifts) accepts the same is_no_working flag. Editing a template-generated chip publishes an override row so the Off/Working choice persists. Save also applies the same times / Working|Off (and traded employee when set) to other weekdays selected under Apply to (patch published, publish override for template chips, or pending drafts). The edit modal keeps a fixed header (actions + employee) and footer (Cancel / Save) while the body scrolls; Trade opens an employee picker. Breaks always show in the modal; when the shift is tied to a shift template (template chip or auto_template:{id} note), Save updates breaks on vendor_availability_slot_breaks via the availability bulk upsert. Auto clock-in/out skips published rows marked Off. GET …/schedule returns is_no_working (and shift_template_id when materialized from a template) on published shifts.

When the user picks a different employee through the Trade picker and saves, the patch body adds is_trade: true alongside vendor_employee_id. The backend keeps the original shift’s is_no_working flag and reassigns it to the new employee (B), then auto-creates the inverse side on the same shift_date so the original owner (A) does not lose their day:

  • If the original shift is Working (is_no_working = false), A receives a new Off shift with the same start_time / end_time — unless A already has any shift on that date (Working or Off), in which case the inverse is skipped to avoid duplicates.
  • If the original shift is Off (is_no_working = true), B receives a matching Working shift with the same times — unless B already has any shift on that date, in which case the inverse is skipped.

If the save throws after the inverse row is inserted, the backend compensates with a follow-up delete on the inverse row so partial writes never linger. is_trade without a different vendor_employee_id returns 400 (code: 'is_trade').

The schedule toolbar Show conflicts toggle filters to employees who have overlapping schedule items on the same calendar day (for example a planned shift + approved time off, or two overlapping shifts). It does not flag leave + auto/manual timecard (future leave should show as time off, not as a Working conflict), and it does not use attendance issue keys (late / absent / overtime / missed clock-out) — those belong on the Timesheets Needs review tab.

When the toggle is on, non-conflict chips are hidden so only conflicting items remain visible. Conflicting chips use the same soft-red fill as attendance issue chips (not a full cell fill) and stack so leave no longer hides a shift or Working card underneath. Hover a conflict or issue chip for a tooltip with the reason (for example shift overlaps time off, or late / overtime issue labels).

Add/edit shift modals use the same Working / Off toggle as availability add-slot shifts (is_no_working when Off). Both modals show the Breaks section (paid/unpaid). On edit, breaks persist to vendor_availability_slot_breaks when the shift is tied to a shift template. Clicking empty space in a schedule cell (outside chips) opens the Add shift modal; chips still open edit / timecard / leave actions.

On Refresh / cron auto clock-in from a published schedule window, the backend copies matching vendor_availability_slot_breaks (paid/unpaid HH:mm on the shift-template slot) into vendor_attendance_breaks when the day has no breaks yet. Matching prefers the auto_template:{id} note + weekday; exact start/end is preferred but not required so stale published times still seed breaks after materialize reconciles the slot. Auto clock-in/out skips emp|date pairs with approved time off so leave days do not create a Working card that reappears after the leave is deleted.

After migration VendorShiftTemplateAssigneesPublicIds (0174), re-save Members on each shift template if assignee rows were cleared during the id-type cutover.

The toolbar Today button snaps the grid to this week, this month, or today according to the current range type. Previous / next shift that period. Date-picker presets This week / Next week / Next 2 weeks switch the range type to week; This month / Next month switch it to month. In month range type, hovering a calendar cell highlights the whole month and clicking it selects that month the same way a week click selects a week.

The add-time-card modal matches the Clover layout: date + employee + role header, worked times, paid/unpaid breaks, tips, photo/GPS count (0 on create), and collapsible edit history (empty until save). Paid and unpaid break blocks share the same row chrome (start/end, duration dropdown, trash). Unpaid only adds the section divider before Tips. Add a break appends a start/end row; removing a row drops it from the create payload (POST /vendor/attendance breaks[]). Unpaid minutes still subtract from worked time. Lists stay independent and are capped at 20 rows each.

Clock-out stays disabled until clock-in is set, and it must be after clock-in (same calendar day). Break start must be at or after clock-in; break end must be after start and, when clock-out is set, at or before clock-out. Add a break is disabled until clock-in is set.

The role combobox lists roles for the selected store only. GET /vendor/vendor-employees/roles?store_id= returns that store’s roles so the same name (for example Cashier) is not duplicated once per vendor store.

Clicking a day-view team-member row opens the edit modal: weekday date + employee name, role combobox, Delete, overtime warning under worked hours, break duration dropdowns with per-row remove, All notes, Photo & GPS, and Edit history with a count.

Date and time controls wrap the icon and the value in one outline field. Click, hover, or keep the picker open to show the thicker border and focus shadow around both, not only the input. Clock-in / clock-out / break times use the same chrome as the header date picker.