Skip to main content

Settings — Working Hours

Audience: Sales · Support · QA · Management · Engineering · Where in app: Settings → Working Hours (/settings/working-hours) · Plan availability: All plans (no permission gate on the personal screen)

This is the personal Working Hours screen — a team member's own availability for calls and chats. A user can either inherit the organization's hours (Default) or define their own per-day schedule (Custom). Working hours feed the widget's online/offline state, call/chat routing, and AI fallback. The company-wide default schedule and timezone live on a separate Organization Working Hours screen.

What it does

  • Lets a user pick Default (use the organization's working hours) or Custom (define their own).
  • In Custom mode, per weekday: a toggle for whether that day is worked, plus start and end times.
  • Saves to the user's working_hours array (empty array = "use organization hours").
  • Validates that, for each enabled day, the end time is after the start time.

How it works

The form (settings/WorkingHours.vue) is a vee-validate + zod form. Default vs Custom is a radio (hours_type).

Default mode

  • Stores an empty working_hours array. The UI shows: "Your availability follows your organization's working hours."
  • On save: PUT /user/<id> with { working_hours: [] }.

Custom mode

  • Seven weekday rows (Monday–Sunday). Default seed: weekdays enabled, weekends disabled, 09:00–17:00.
  • Each enabled day uses FormTimePicker for start/end (time objects { hours, minutes }).
  • On save, only enabled days are sent, formatted as HH:MM:
{
"working_hours": [{ "day": "Monday", "start_time": "09:00", "end_time": "17:00" }]
}
  • Validation (superRefine, custom mode only): each enabled day's start/end must be valid times and end must be after start ("End time must be after start time").

Loading existing data

When the user record loads: an empty/absent working_hours → Default mode; otherwise Custom mode with each stored day enabled and its times parsed (parseTimeFromApi tolerates both {hours,minutes} objects and "HH:MM" strings).

How working hours affect the product

Per the working-hours knowledge base, online/offline status drives widget and routing behavior. There are two levels:

LevelStored onControls
Organizationcompany.working_hours[] + company.timezone (+ timezone_offset)Company default; the Organization Working Hours screen.
User (this screen)user.working_hours (+ user.timezone)Personal availability. An agent shows online only when both org AND personal hours match.

When online: the widget shows online copy, chat can connect directly to a human (per chat routing), calls ring active agents, and auto-triggers fire per URL rules. When offline: the widget shows offline copy, chat falls back to capturing an email/question, calls fall back to a callback request or inbound AI call (if enabled), and a missed-call email can trigger. The widget computes isOnline client-side using the company timezone, and the backend re-checks server-side for any decision that matters (routing, AI takeover). See Widget Working Hours.

Configuration & options

ControlFieldNotes
Default / Customhours_type (default | custom)Default writes an empty working_hours.
Per-day enabled<day>_enabledCustom mode only; disabled days are excluded from the payload (= "Closed").
Start / end time<day>_start, <day>_endstart_time, end_timeHH:MM; end must be after start.

Saved via PUT /user/<id>.

Behaviors & edge cases

  • Empty array means inherit: working_hours: [] is the signal to use the organization schedule — not "unavailable always."
  • A day with no entry = closed that day (it's simply omitted).
  • Validation is custom-mode only: in Default mode time fields aren't validated.
  • Both levels must agree: per KB, an agent is online only when org hours AND personal hours are both within range — a user with custom hours can be offline even during company hours, and vice-versa.
  • Timezone: the personal screen edits the schedule; the company timezone (and timezone_offset) governs interpretation. Whether a separate user.timezone is edited here is verify (this screen edits working_hours; timezone is set elsewhere/onboarding).
  • Unsaved-changes guard: leaving dirty prompts to save (onBeforeRouteLeave + swal).
  • Relationship to the widget doc: Widget → Working Hours describes the runtime/online-offline behavior and the org-level schema; this page is the personal settings screen that writes user.working_hours. They describe the same system at different levels.

Plan & limits

Available on all plans; the personal screen has no permission gate. The Organization Working Hours screen (company default) is gated by settings.organization. No per-plan limits on number of days/hours in the code reviewed.

Technical implementation

  • View: frontend/src/views/settings/WorkingHours.vue (vee-validate + zod; days Monday–Sunday)
  • Route: name settings-working-hours, path /settings/working-hours, meta { title: 'Working Hours' } (no permission) — frontend/src/router/index.js
  • Org counterpart: frontend/src/views/settings/organization/WorkingHours.vue, route /settings/organization/working-hours, gated by settings.organization
  • Save: PUT /user/<id> { working_hours: [{ day, start_time, end_time }] } (or [] for default)
  • Schema/fields: user.working_hours, company.working_hours[], company.timezone, company.timezone_offset
  • Runtime use: widget isOnline computation (client) + server-side re-check for routing/AI takeover. See services/widget and services/ms-communication.