Skip to content

Workforce: field service (#1225, PR C) - #1252

Open
MikeAlhayek wants to merge 33 commits into
claude/workforce-contact-center-wfmfrom
claude/workforce-field-service
Open

MikeAlhayek wants to merge 33 commits into
claude/workforce-contact-center-wfmfrom
claude/workforce-field-service

Conversation

@MikeAlhayek

@MikeAlhayek MikeAlhayek commented Nov 1, 2026 •

Copy link
Copy Markdown
Member

PR C of the Workforce Management plan (#1225): field service. It is stacked on #1249 (PR B, contact center workforce management), which is stacked on #1229 (PR A, the workforce core), so its base is PR B's branch; as each PR below it merges, this PR's base moves down.

The plan is at docs/engineering/workforce/workforce-management-plan.md; its Progress section lists every change from the design, slice by slice.

Slices (one commit each)

  • C1 Geo: a new Geo module (CrestApps.OrchardCore.Geo, with Geo.Abstractions and Geo.Core) that references no vertical, so any module can use it:
    • GeoPoint and the geocoding, travel-time and route provider seams behind IGeoService, which picks providers from Settings > Maps and Geocoding, caches travel times, and falls back to the built-in haversine estimate (road factor and average speed) when a provider is missing or fails.
    • Service territories drawn on a map or listed by postal code (with 021* prefixes), with a parent, time zone and travel speed, and ITerritoryResolver: the smallest shape a point is in, else the exact postal code, else the longest prefix.
    • Geocoding in the background (IGeocodeQueue, IGeocodeTarget): one request per record, retried after 1, 5, 30 and 120 minutes, then given up; waiting while no provider is set up. Workforce geocodes team locations and home bases when Geo is on.
    • Address gains optional coordinates. CrestApps Resources vendors Leaflet, Leaflet.markercluster and Leaflet-Geoman (MIT) with a small crestapps-map wrapper.
    • Screens: the territory list with an overview map and a "find the territory of a place" tool, the editor with polygon and rectangle drawing (or pasted GeoJSON), and the settings with the geocoding status.
  • C2 Geo providers: Azure Maps, Google Maps and OSRM modules, each adding its section to the Maps and Geocoding settings with its key encrypted. Requests are split to each service's matrix limits, refusals fall back to the built-in estimate (or are retried, for geocoding), Google's travel times are never cached as its terms require, and choosing a geocoding provider wakes the addresses that waited for one. All three pass one contract test suite against responses written from each service's documented format.
  • C3 Assets and Fleet: a new Assets module with asset types (category, attributes, meters), numbered assets (owner, a hierarchy without cycles, address, warranty, documents) and append-only meter readings, where a cumulative meter can't go back. Fleet gives a bookable vehicle or piece of equipment its own Equipment calendar, which refuses double-booking, and a van that carries stock its own Inventory location. Inventory stock locations move from the settings to a StockLocation store, keeping their ids and the order orders take units from them; owned locations never fulfill orders; stock movements gain a reference and the Consumed kind with ConsumeAsync.
  • C4 Work Orders: a new Work Orders module (CrestApps.OrchardCore.WorkOrders, with WorkOrders.Abstractions and WorkOrders.Core):
    • Settings under Field Service: work order types (duration, crew, priority, checklists, starting lines, service level, billing, signature and photos), checklists (sections and ten answer kinds), service levels (response and resolution targets per priority, counted in weekly opening hours in a time zone or around the clock, pause statuses, warning share) and sub-statuses.
    • Work orders move only along the usual statuses. The service level clock stops while one waits and moves its due times, and a background task raises a warning and a breach once each.
    • Parts used come off a stock location such as a van's, in whole units, with the work order as the reference. Completing refuses missing required answers, signature or photos, and a billed work order gets an outstanding Transactions invoice that refers to it. Follow-ups copy the customer, place and assets.
    • Geo gives the service address its territory and geocodes it; the list filters by asset, linked from the asset page; five workflow events can be limited to some types.
  • C5 Service Agreements: a feature of Work Orders for recurring visits, such as preventive maintenance. An agreement has a customer, place, assets, period, time zone, notice, and included or per-visit pricing. Each schedule's RRULE (expanded with the Calendars recurrence engine) makes the work order of every visit ahead of time, once, for the visit day in the agreement's time zone, across daylight saving changes. The agreed price replaces the type's billing, agreements expire after their end, and the Service Agreement Expiring workflow event starts once within the notice.
  • C6 Dispatch I: a new Dispatch module (CrestApps.OrchardCore.Dispatch, with Dispatch.Abstractions and Dispatch.Core):
    • A visit for each work order, and for older work orders on request.
    • The candidate finder checks skills at a level, certifications valid on the day, the territory, published shifts less breaks and time off, and room for the trip from the visit before and to the one after. It ranks by weighted parts and lists everyone ruled out with the reason.
    • Assignment takes sorted distributed locks per technician and checks again inside them, saving before the locks go: two dispatchers can't double-book one technician. It adds an arrival window and a busy Job event on the technician's team calendar, and schedules the work order.
    • Field statuses move the visit along: en route and on site start the work order; incomplete makes a return visit.
    • The Dispatch Board: a timeline per technician, the unscheduled queue and a map; drag to assign, candidates with one-click assign, live over SignalR.
    • Also: settings, two workflow events, and a Visits panel on the work order's page (a new WorkOrderPanels display model).
    • Deferred pieces from earlier slices: types edit their skills and certifications, the work order editor has a pin on the map, and territories list who serves them (ITerritoryMemberSource).
  • C7 Dispatch II:
    • Optimizer: the built-in optimizer proposes a better day, and the board applies the chosen moves through the locked assignment. Pinned and started visits stay put.
    • Crews and equipment: crew sizes are enforced, and required equipment is booked on Fleet's Equipment calendars, never double-booked.
    • Customer notices: by SMS or email when a visit is scheduled, the day before, on the way and when done. The on-the-way notice carries an ETA and a signed tracking link that stops working when the visit is over.
    • At risk: visits running over, not started or arriving late are flagged, with a workflow event.
    • The Scheduling arrival-window slot provider moves to C10.
  • Review redesign (D1-D6), recorded in docs/engineering/workforce/time-tracker-people-teams-review.md:
    • D1 Time Tracker: a new Time Tracker module (CrestApps.OrchardCore.TimeTracker) for any user, not only workers:
      • stages, such as Work, Break or Lunch, that each count toward any number of categories, such as Payable or Billable, with a factor;
      • My Time, where people clock in, switch stage and clock out with no gaps, and see the day and the week;
      • Time Entries, where managers add, change or remove time, always with a reason that is kept on the entry and recorded in the Audit Trail;
      • Reports > Time Tracker > Hours by category, with a row per person (or per day) and a column per category;
      • recipe and deployment steps with their CrestApps Recipes schemas.
    • AGENTS.md now says that toolbar filters are bootstrap-select pickers, as on Manage Content. It also covers how admin forms and menus are built (ocat-* fields in cards, icons, reports under Reports) and that every stored record has recipe, deployment and schema support.
    • D2 Time Clock in Time Tracker: punches stay the clock's record, and each worker with a login has their day kept in Time Tracker from them.
      • After every punch, correction or removal, the worker's entries around it are rebuilt, paired as timesheets pair them, in the stage of each activity code.
      • Time Tracker shows those entries read-only, refuses to switch or change time the clock keeps, and links to the timesheets and My Work.
      • Activity codes get a stage (a migration, a handler and a Time Tracker card on the editor), and earlier punches are brought in by a background task.
      • The time clock's pages and Predictability Pay moved under the Time Tracker menu.
      • The review explains why punches weren't replaced: kiosk-only workers, the original punch as the payroll record, and rules written against punches.
    • Shift planning under Calendars: the pages moved under the Calendars menu, after Scheduling's: Shift Schedules (renamed from Schedules), Open Shifts, Shift Trades, Rotations, Coverage and Shift Templates. Each team's Workforce calendar links to its schedules from Manage Calendars. The planner stays its own page, because drafts must not reach the live calendar.
    • D6 consistency (started):
      • every list filter in the plan's modules is a bootstrap-select picker, as on Manage Content (Coverage's team filter among them);
      • the Workforce menu shows its icon (Skills adds Workforce first, so the icon sits there too);
      • Get Started left the menu and opens from Teams.
    • D3 Contact Center states: the new Contact Center Time Tracker feature keeps every agent's states in Time Tracker, whether or not the agent is a worker.
      • Each state change rebuilds the last ten minutes of the agent's state log, each state (or a break's reason) in the stage chosen on Time Tracker > Contact Center Stages. The usual states get a stage when the feature is turned on.
      • For a worker on the time clock, the states replace their clocked work while they are signed in. To do this, Time Tracker's sync gained two options: rebuild only a range (ReplacesRangeOnly), and cut another clock's entries around these (Overrides).
    • D4 Time off:
      • Grants with a last day, such as 40 bonus hours to use by 30 December. Time off comes out of grants first, the soonest to end first, and canceled time goes back to the grant. What is left expires the day after the last day (on the accrual run). Balances, on the admin page and in My Work, show what is left of each grant.
      • Approved time off in Time Tracker: each day is kept in the stage of its type once the day comes, read-only. A new ITimeOffRequestObserver, called after the request is saved, keeps the days in step, and an hourly task adds days as they come.
    • D5 People in one place: the user editor has a Workforce card. It makes the user a worker, or changes their worker's employee number, job title, employment, status, hire date, main team and contracted hours. A linked worker takes the user's name and email, which are read-only on the worker page, and Time Tracker records people under their display name.
    • D6 records travel, forms in cards:
      • Geo territories, asset types and assets, work order settings (checklists, service levels, sub-statuses, types, and optionally agreements), adherence rule sets, and planning groups, forecast events and capacity plans each have a recipe step, a deployment step and a CrestApps Recipes schema. Records keep their ids, so links between them survive, and parents come before children. Running a step twice changes nothing.
      • Dispatch settings and Maps and Geocoding settings have site settings deployment steps. Time Tracker categories and stages now keep their ids too.
      • Records that are the business's own activity stay out: work orders, visits, meter readings, punches, timesheets, ledgers, requests and agent states. The review explains why.
      • The field service and Contact Center workforce editors now use ocat rows in cards.
  • C8 Technician Mobile: a new feature of Dispatch that gives technicians a Field page in My Work, which works offline.
    • On the phone: their visits from today, with each work order's address, contact, checklists, parts and notes, are kept in IndexedDB.
    • The outbox: status changes, answers, parts used, notes, photos (made smaller on the phone) and the customer's drawn signature go into an outbox of keyed commands. Commands are sent in order once online and applied once each (IFieldSyncService, with receipts), so sending them again changes nothing.
    • The server wins: a visit given to someone else, or canceled, refuses its commands with the reason. A later answer to a question is kept over an earlier one. Finishing the visit completes the work order, or says what is missing.
    • Location: shared only after the technician agrees to a versioned notice, and only while they work. It refreshes the ETA on the way and shows as a dot on today's board. Positions are removed after 30 days, or at once when the technician withdraws.
    • Endpoints and settings: the same endpoints are under api/dispatch/v1/mobile, and the settings come from CrestApps:Dispatch:Mobile, described in the module's ConfigurationSchema.json. The My Work service worker can now keep any portal page for offline use (MyWorkOfflineOptions). The work order's page shows its photos and signature images.
  • C9 OR-Tools: a new add-on module (CrestApps.OrchardCore.OrTools, Google OR-Tools 9.15, Apache 2.0) with two features.
    • OR-Tools Shift Scheduling: a CP-SAT model that keeps the built-in solver's hard limits and weighs coverage minimum first, then target. It becomes the default for Auto-schedule.
    • OR-Tools Dispatch: the routing solver, with routes from home bases, five-minute starts inside open stretches and windows, breaks, allowed technicians, fixed visits and a penalty per visit left out. Dispatch uses it for Optimize the day.
    • Both start from the built-in solver's answer as a hint and keep the better of the two, so turning them on never makes a schedule or a day worse.
    • The module isn't in the CMS targets, because of its native libraries; the sample host references it.
    • The auto-schedule message and the board's proposal now name the solver.
  • C10 API, AI tools, reports, sample and setup:
    • Field Service API: a new Dispatch feature for other systems and apps, using the API authentication scheme and each endpoint's admin permission:
      • work orders at api/work-orders/v1: list, get, create once per Idempotency-Key, status, notes, answers, parts, signatures, attachments and completing;
      • visits at api/dispatch/v1: list, get, candidates, assign, unassign, status, optimize and apply, and technicians' positions;
      • geocoding and territories at api/geo/v1, and assets with their meter readings at api/assets/v1.
    • Seven AI tools under Field Service:
      • create, follow and summarize work orders;
      • find a technician;
      • optimize and explain a day, applying only the moves the dispatcher approves;
      • reschedule a visit;
      • Where is my technician, for AI agents that talk to customers. It needs no sign-in, answers only for the caller's own phone number or tracking link, and gives only the status, the arrival window, the ETA and the technician's first name.
    • Reports > Field Service > KPIs: first-time fix, on-time arrival, jobs per technician a day, return visits, travel against time on site, response and resolution compliance, and the mean time to repair. A Field Service report data source offers a work orders data set and a visits data set.
    • Field service sample recipe: a team, a shift template, territories, assets and work order settings, all with fixed ids.
    • A Get started checklist on the dispatch board, kept out of the menu.
  • C10 integrations:
    • Bookings: a work order type lists, under Bookings, the Scheduling appointment types that make it. A confirmed booking makes the work order once, a reschedule moves its window, and canceling the booking cancels it, with the reason.
    • Dispositions: the Interaction Center Create Work Order subject action makes a work order of the chosen type and priority for the activity's contact, with the agent's notes, once per activity.
    • Contacts: a Work orders card on CRM contact pages, whose New work order keeps the contact.
  • E1-E2 (follow-ups):
    • Owners picked by name: an asset is owned by a customer, company or CRM contact found by name (IAssetOwnerSource), with an Assets card on contact pages and an owner filter on the asset list. The work order editor picks its customer, company and contact the same way.
    • Booking capacity: a new Scheduling seam, IAppointmentSlotFilter. Dispatch uses it to offer a booking time only while a technician can take the job, less the waiting bookings that overlap.
    • Fix: a booking's work order window ended at the booking's end, so a job longer than the booking could never be dispatched. The booked time is now the arrival time.
    • Reports and API:
      • the Field Service data source gains Assets, Meter readings and Service history data sets;
      • the KPIs add revenue per work order, repeat work on the same asset and parts used by stock location;
      • the API adds checklists, a day's board, and agreements.
  • E3 (follow-ups): the technician Field page shows the job's equipment with the latest work orders on it and how many of each part each van has free. It can also ask for a follow-up work order, applied once even when sent offline.
  • E4 (follow-ups): a checklist question can be shown only after a given answer to an earlier question, and each agreement schedule can cover only some of the agreement's assets.
  • E5 (follow-ups):
    • A published shift can be changed on its own from its page: new hours, another worker, or taken off (IShiftService.RemoveAsync). Each change is checked, logged and paid like a republish.
    • Labor rule sets pick their own workers in the editor.
  • E6 (follow-ups): canceling approved time off puts back the shifts it took off, with their drafts. New workflow events: Shift Assigned, Shift Changed and Shift Removed for every published schedule change, and Time Off Waitlist Promoted.
  • E7 (follow-ups): My Work reminds workers of their shifts as long before as each chooses, and keeps push and email quiet during each worker's quiet hours.
    • Fix: saving those settings used to turn off timesheet and attendance notifications.
  • E8-E9 (follow-ups):
    • The Workforce report data source gains Workers and Schedule changes data sets.
    • New Open Shift Posted workflow event; a filled open shift now raises Shift Assigned.
  • E10 (follow-up): agents see their own adherence on a new Adherence page in My Work.
  • Still not built (in the plan's progress notes):
    • companies have no detail page, so they get no card;
    • agreements aren't billed through Subscriptions;
    • the Field page has no payment link or masked calling.

Testing

  • Test-first unit tests: distances and GeoJSON shapes (holes, multi-polygons, bad input), the territory matcher and store, the built-in estimate, caching and fallback of the geo service, the geocoding queue (written back, retries with growing waits, giving up, waiting for a provider, asking again), and team locations geocoded end to end.
  • Release build with -warnaserror; the Geo, Workforce, architecture, PublicApi and address-using commerce suites; npm run check:manuals, eslint and vitest pass.
  • Checked in a browser: turning the feature on, a territory drawn with the rectangle tool and one pasted as GeoJSON inside it, the finder by point, postal code and nowhere with the place marked on the map, the editor reopening with its shape and the others in grey, the phone layout, the settings and their checks, and a home base waiting for a geocoding provider.
  • C2 checked in a browser: the three features, their settings sections with display names, the OSRM address check, a saved key shown only as saved, and a waiting home base woken by choosing Azure Maps and retried, with the refused connection shown as the latest problem. Live provider calls weren't possible from the test site (no keys, no outbound access).
  • C3 checked in a browser: Assets, Fleet and Inventory turned on; a van type with an odometer and a bookable van that carries stock (its calendar under Calendars, its location kept off the Inventory settings); odometer readings with a lower one refused; a customer's boiler with its owner check, warranty and a part inside it; the list's search and filters; and the phone layout. The test gate covered the commerce suites that sit on Inventory.
  • C4 checked in a browser: a business-hours service level, a checklist, a sub-status that stops the clock, and a time-and-materials type with labor and a counted filter. A High work order created on a Sunday evening was due to be responded to by Monday 1 PM. Two filters came off Van 12 (5 to 3) and a fraction was refused. Completing was refused until the answers and signature were in, then made invoice INV-1001 for 216.25 USD. Also checked: a follow-up, the list filters, an asset's work orders from its page, the phone layout, and the Work Order Created event in the workflow editor. Tests cover the clock (opening hours and time zones), the status rules, pausing, completion checks, billing, and the Inventory, Transactions, Geo and Workflows integrations.
  • C5 checked in a browser: a refused custom rule; an active weekly agreement with its next visits listed; two work orders made for the next two Mondays, and none on a second run; the notice given within 30 days; and a made work order showing its visit day, agreement and "Included". Tests cover the visit days, the end date, daylight saving windows, making each visit once, expiry, the expiring event and the agreed price.
  • C6 checked in a browser:
    • Dispatch and SignalR turned on.
    • A type needing a skill and a certification: everyone ruled out with the reason, then a candidate once the certification was dropped.
    • A territory served by a team, a work order pinned on the map, and visits made for four older work orders.
    • Assigning from the candidates: 09:00 to 10:30, arrival window 08:00 to 10:00, and a Job event on the team calendar.
    • Drags onto the same technician refused at 09:30 (naming the visit in the way) and at 13:00 (a break).
    • Dispatched, en route (the work order in progress) and on site, with a second board updating by itself.
    • The work order's Visits panel, the settings page, and the workflow events in the toolbox.
    • Tests cover the race, with a test that fails without the locks; the expired certification; skills, shifts, time off and travel; the job events; the work order's status; the return visit; and canceling.
  • C7 checked in a browser:
    • A proposal applied on the board.
    • The reason when a waiting visit fits nobody: its skill needs a certificate that isn't valid that day.
    • The customer's tracking link, and an expired link (404).
    • The settings page in cards.
    • Tests cover:
      • an optimizer that beats a naive plan and keeps fixed visits;
      • a proposal that swaps two days and applies once;
      • crew size and equipment that can't be double-booked;
      • the ETA, a link that expires and a tampered link;
      • notices sent once for each start;
      • a visit at risk that is flagged once and cleared when it starts.
  • D1 checked in a browser:
    • stages and categories made, and the day switched between Work and Lunch;
    • a manager's edit, refused without a reason and kept with "Changed" once it had one;
    • the stage filter as a bootstrap-select picker;
    • the hours report opened from Reports.
    • Tests cover switching without gaps, overlaps refused, edits needing a reason, totals split at local midnight with category factors, and the lock per person.
  • D2 checked in a browser:
    • the upgrade gave every activity code a stage, and Work shared the existing Work stage;
    • the Time Tracker menu with the time clock's pages and Predictability Pay;
    • My Time for a worker, still able to clock out of an entry started before;
    • the stage card on an activity code;
    • earlier punches brought in, shown "From the Time Clock" and read-only, linking to Timesheets.
    • Tests cover a day of punches in stages and categories, a corrected and a removed punch, a worker without a login left out, the backfill once, a sync that keeps unchanged ids and works around a manager's time, and an open entry ended where the clock's time starts.
  • D3 checked in a browser: turning the feature on seeded each state's stage, and the stages page saved a reason's own stage (Lunch). A live agent sign-in wasn't possible on the test site, which has no telephony, so the sync is covered by tests: states and reasons in their stages with the current state open, a ten-minute rebuild that continues the entry before it, states replacing a worker's clocked work, and the seeder run twice.
  • D4 checked in a browser:
    • a 40-hour grant paid for 23.5 already-approved hours, leaving 16.5 to use by 30 December, shown on the balance and in the ledger;
    • the upgrade gave the Vacation type a stage, and the type shows the Time Tracker card;
    • an approved morning off appeared in Time Tracker at the hours it counts (10:00 to 12:00, because the shift starts at 10:00), and canceling took it away.
    • Tests cover grants used first, expiry once, a request after expiry refused, canceled time back in the grant, the grant's rules, days kept once they come, a part day, cancel saved, and the type seeding.
  • C10 integrations checked in a browser:
    • a type's Bookings card, saved;
    • a booking made in the admin, which made WO-000007, then canceled, which canceled WO-000007 with the reason;
    • the Create Work Order action's editor in a subject flow;
    • a contact's Work orders card, and a work order started from it keeping the contact.
    • Tests cover the action (once per activity, notes, phone, priority) and the booking observer (made once on confirmation, moved, canceled).
  • E1-E4 checked in a browser:
    • an asset given a contact through the picker, then the contact's Assets card, and Add an asset from it with the owner filled in;
    • a work order given a customer, with its page linking the CRM contact;
    • the booking page offering times only while a technician is on shift and free, a 10:30 booking (WO-000009) taking the overlapping times away, and dispatch then fitting it 10:45 to 12:15;
    • the KPI page with the new tiles and the parts table;
    • on a phone width, the Field page's equipment card, a van's free count, and a follow-up asked offline and sent once;
    • a checklist question shown when an earlier answer is No, and an agreement schedule given its own asset.
    • Tests cover the owner directory, the asset data sets, the slot filter and the Scheduling seam, the booking window, the new KPIs and API reads, the pack's history and the follow-up, and a schedule's own assets. The full suite passed: 13,433 tests.
  • E5-E7 checked in a browser:
    • a rule set given a worker;
    • a published shift moved to 13:00, given to Cy Park and taken off, with all three in the change log;
    • the four new events in the workflow toolbox;
    • My Work settings on a phone width, saving a 30-minute reminder and quiet hours.
    • Tests cover removal and republishing, a canceled time off putting shifts back, the shift events, reminders sent once, and quiet hours. The Workforce suite passed (183 tests).
  • E8-E9 checked: tests of both data sets and of the open shift events; in a browser, both data sets in the report builder and the event in the workflow toolbox.
  • Final browser pass: 31 pages this PR adds or changes all answer 200 without errors, and the server logged no failures.
  • C10 checked in a browser:
    • each API group answered 404 before the feature was on, and 401 without a token after;
    • the AI tools listed in a new AI profile;
    • the KPI page over 30 days of the site's data;
    • the Field Service data source and both data sets in the Report Builder;
    • the Get started checklist, every link opening its page;
    • the sample recipe run from Configuration > Recipes, making the territories, technicians, types and assets.
  • C10 tests cover:
    • a work order made once per key, with its visit;
    • a viewer refused assignment, and a dispatcher assigning a visit and moving it along;
    • a refused status change, with the reason;
    • a day optimized through the API;
    • each AI tool run as a user, and the customer tool for the caller's phone against a stranger's;
    • the KPIs and the visits data set over a day with a first-time fix and a return visit;
    • the sample recipe run twice without errors or duplicates.
  • C10 fixed on the way: the recipe import keeps an incoming ItemId only when it is a valid 26-character id. Readable ids in a hand-written recipe therefore broke the links between records and duplicated them on a second run. The sample uses valid ids, and the docs say so.
  • C9 checked against the built-ins' fixtures:
    • shifts: 97.87% of the target on the two-week clinic sample (the built-in reached 97.71%), every minimum met, proved optimal in 1.2 s;
    • dispatch: the same travel on the naive plan, and on a 20-visit day with a lunch break, windows and a skill, a cost of 2,205 against 2,288, with every route checked independently;
    • the built-in solvers' rule fixtures (skills, availability, fixed shifts, maximums) give the same answers.
  • C9 checked in a browser: both features turned on; Auto-schedule on a schedule, made by the OR-Tools solver in 1.3 s; Optimize the day naming the OR-Tools solver. The Linux container smoke test wasn't run.
  • C9 fixed on the way: the auto-schedule message passed the shift count twice, so it read "2,000% of the people wanted"; DispatchHub was missing from the hub convention test's list; and the Customers recipe schema didn't describe the address coordinates added in C1. These last two were found by the full suite, 13,413 tests.
  • C8 checked in a browser, on a phone-sized window:
    • turning the feature on, and the Field page with a visit for tomorrow;
    • offline: on the way, arrived, two answers, a part, a note and a drawn signature, then a reload offline that showed the cached page with the changes kept on the phone;
    • back online, everything sent once. The part was refused because there was no stock at that location, which is correct;
    • a visit given to another technician while the first was offline: both of their changes were refused, with the reason shown;
    • a photo, then finishing the visit, which completed the work order. The photo and the signature image show on the work order's page;
    • location consent, and the position on today's board with its time;
    • the API refusing a call without a token.
    • Tests cover the outbox applied once and in order, a reassigned visit refusing commands, finishing that waits for the signature, a later answer winning, two offline answers to one question, positions only with consent and while working (with the ETA refreshed), retention, and the pack.
  • C8 fixed on the way:
    • an answer changed twice offline was refused as stale, because the server stamped answers with its own clock. Answers now keep the time they were given on the phone;
    • a change made during a sync vanished from the page until the next sync. The outbox is now applied on top of every new pack;
    • each sync redrew the page, which could drop a tap or a half-typed note. It now redraws only when something changed, and never while the technician is typing or signing.
  • D6 checked in a browser: every converted form loads with its fields in cards; a deployment plan lists the new steps, with work order settings (agreements included) and territories added to it; and a JSON import made a territory and a sub-status. Tests export from one site and import twice into another, for territories with their parent, work order settings still linked, and assets with their type and parent.
  • D5 checked in a browser: the card on the user editor; a job title and a new main team saved from it, with the old team kept; the worker page showing the name from the account, read-only. Tests cover making a worker from a user, saving again, and the name and email following the user.
  • D4 fixed on the way: the first version synced Time Tracker from inside the approval change, and the sync's own save committed a cancel half-way (the hours given back, the request still approved). The sync now runs after the save, and a test reads the request back.
  • C4 fixed on the way: the four settings records shared one index table, so listing one kind read the others and failed; list defaults (weekdays, the on-hold pause) were read back twice; the stock picker showed titles in upper case; statuses read as OnHold; and the location to take parts from silently defaulted to the first one.
  • Fixed on the way: the stock location store would have reordered locations by name, changing which location orders take units from; the OSRM settings didn't render (a sealed view model); addresses kept waiting an hour after a provider was chosen; the Geo YesSql collection wasn't registered, so its tables didn't exist; OpenStreetMap refused tiles because admin pages send no referrer (tiles now send the site's origin); and a hint with a literal {z} broke the settings page.

🤖 Generated with Claude Code

A new Geo module (Geo.Abstractions, Geo.Core and the module) gives every module a shared way to work with places:
GeoPoint; geocoding, travel-time and route providers behind IGeoService, chosen in Settings > Maps and Geocoding,
with travel times cached and the built-in haversine estimate (road factor and average speed) when a provider is
missing or fails; service territories drawn on a map or listed by postal code (with prefixes), with a resolver that
takes the smallest shape a point is in, then the exact postal code, then the longest prefix; and geocoding in the
background, one request per record, retried with growing waits and written back through IGeocodeTarget. The
territory list has an overview map and a finder, the editor draws with Leaflet-Geoman, and the settings show the
geocoding status. CrestApps Resources vendors Leaflet, Leaflet.markercluster and Leaflet-Geoman with a crestapps-map
wrapper that sends the site's origin as referrer for tiles. Address gains optional coordinates, and Workforce
geocodes team locations and home bases when Geo is on, clearing a home base's coordinates when its address changes.
Three provider modules plug into Geo, each adding its section to Settings > Maps and Geocoding: Azure Maps
(geocoding, the route matrix with traffic split to 100 cells a request, and directions), Google Maps (geocoding, the
Distance Matrix split to its 25 by 25 and 100-element limits, directions with the encoded polyline decoded, and travel
times never cached, as its terms require) and OSRM (travel times and routes from a self-hosted server, with its steps
written as sentences). Keys are kept encrypted; refusals throw GeoProviderException, so travel times fall back to the
built-in estimate and geocoding retries. Providers gain a cache cap and a display name, choosing a geocoding provider
wakes the addresses that waited for one, and the settings show the latest geocoding problem. Every provider passes the
same contract tests against responses written from its service's documented format.
…tore

A new Assets module (Assets.Abstractions, Assets.Core and the module) keeps equipment, vehicles, tools and units
installed at customers: asset types with a category, attributes and meters; numbered assets with an owner, a hierarchy
that refuses cycles, an address, warranty, attribute values and documents; and append-only meter readings where a
cumulative meter can't go back. The list searches by name, number or serial number and filters by type and state, and
an asset's page shows its parts, warranty and readings. Its Fleet feature gives a schedulable asset its own Equipment
calendar, which refuses double-booking, and a van that carries stock its own Inventory location owned by it.

Inventory stock locations move from the settings to a StockLocation store, keeping their ids and the order orders
take units from them (a deferred copy after the migration, with the settings still read until then); the settings
page edits the store, and owned locations never fulfill orders. Stock movements gain a reference and the Consumed kind,
with IInventoryService.ConsumeAsync. The Workforce, Assets and Territories menus get icons.
…ls, parts and invoices

New CrestApps.OrchardCore.WorkOrders module (with WorkOrders.Abstractions and
WorkOrders.Core): work order types, checklist templates, service level policies
and sub-statuses, each with its own index table; work orders with the status
rules, a service level clock that runs in weekly opening hours and stops while
waiting, a once-only warning and breach from a background task, parts taken off
an Inventory location with the work order as the reference, checklist answers,
signatures, notes, completion checks, invoices through Transactions and
follow-ups. Geo gives the service address its territory and geocodes it, the
work order list filters by asset (linked from the asset page), and five
workflow events can be limited to some types.
… orders once each

New Service Agreements feature of the Work Orders module: agreements with a
customer, place, assets, period, time zone, included or per-visit pricing and
notice, and schedules whose RRULE (expanded with the Calendars recurrence
engine) makes the work order of each visit ahead of time, once, for the visit
day in the agreement's time zone. The agreed price replaces the type's billing,
agreements expire after their end, and the Service Agreement Expiring workflow
event starts once within the notice. Includes the list, editor and page with
the next visits and the work orders made, and a run-now button; a work order
made for a whole day shows its visit day.
… a live board

A visit for each work order; the candidate finder checks skills at a level,
certifications valid on the day, territory, published shifts less breaks and
time off, and the trip from the visit before, and ranks by weighted parts.
Assignment takes sorted locks per technician and checks again inside them,
adds a Job event on the team calendar and schedules the work order. Field
statuses move the visit along. The Dispatch Board shows a timeline per
technician, the unscheduled queue and a map, with drag to assign and live
updates over SignalR. Work order types edit skills and certifications, the
editor places the job with a pin, territories list who serves them, and the
work order page takes panels from other modules.
…es, tracking and visits at risk

The built-in optimizer proposes a better day (two starts, cheapest insertion,
relocate, swap and 2-opt; fixed visits stay) that the board applies move by
move through the locked assignment. Crew sizes are enforced and required
equipment is booked on Fleet Equipment calendars. Customers are told by SMS
or email when a visit is scheduled, the day before, on the way (with an ETA
and a signed tracking link that stops when the visit is over) and done.
Visits running over, not started or arriving late are flagged at risk with a
workflow event. Dispatch settings are grouped in cards.
…y Time and the hours report

A new CrestApps.OrchardCore.TimeTracker module tracks working time for any user:
stages that count toward any number of categories, clocking in, switching stage
and clocking out without gaps, manager edits and removals with a reason kept on
the entry and in the Audit Trail, and an hours-by-category report under Reports.
It has recipe and deployment steps with their CrestApps Recipes schemas.

AGENTS.md now says toolbar filters are bootstrap-select pickers as on Manage
Content, how admin forms and menus are built, and that every stored record is
portable through recipes and deployments.
Punches stay the clock's record. After every punch, correction and removal,
the worker's Time Tracker entries around it are rebuilt from the punches,
paired as timesheets pair them, in the stage of each activity code. Time
Tracker gets SyncAsync for entries another clock keeps (ManagedBy), and
IExternalTimeClock so it refuses to switch, change or remove that time and
links My Time to My Work. An entry left open there ends where the clock's
time starts. Every activity code gets a stage when the feature starts, and the
editor has a Time Tracker card; a background task brings earlier punches in.

The Time Clock feature depends on Time Tracker, and its pages and Predictability
Pay move under the Time Tracker menu. The review records why punches stay:
kiosk-only workers, the original punch as the payroll record, and the rounding,
meal and overtime rules written against punches.
…team calendar

Shift planning publishes to each team's Workforce calendar, so its pages sit
under Calendars after Scheduling's, as Scheduling's do: Shift Schedules
(renamed from Schedules), Open Shifts, Shift Trades, Rotations, Coverage and
Shift Templates. A team's Workforce calendar has Shift schedules in its actions
on Manage Calendars. The review records why the planner stays its own page:
drafts must not reach the live calendar, and the grid has no room for
coverage, weekly hours or auto-schedule.
… Content

Every toolbar filter of the plan's modules (Workforce, Workforce Contact
Center, Skills, Assets, Work Orders and Dispatch) is now its own btn-group
filter with a selectpicker, a header and the bootstrap-select resources,
instead of a plain form-select. Coverage's team filter, for one, now matches
Orchard Core's Manage Content list.
…s from Teams

The first provider to add a top-level menu item gives it its classes, and
Skills adds Workforce before Workforce does, so the icon now sits there too.
Get Started is a one-time setup page: it left the menu and opens from a
button on the Teams list.
…k's work

The new Contact Center Time Tracker feature keeps every agent's states in Time
Tracker, workers or not. Each AgentStateChanged event rebuilds the last ten
minutes of the agent state log into entries in the stage of each state, or of a
break's reason first, set on Time Tracker > Contact Center Stages (a site
setting with a deployment step, seeded for the usual states). Back-to-back
states in one stage make one entry, and the state the agent is in stays open.

Time Tracker's sync gained ReplacesRangeOnly, to rebuild only a range while
keeping and continuing what came before, and Overrides, to cut another clock's
entries around these. Agents' states thus replace a worker's clocked work while
they are signed in, and the time clock's next sync goes around them.
… after

A grant gives hours or days to use by a date, such as year-end bonus time. Time
off comes out of grants first, the one that ends soonest first, then out of the
rest of the balance, oldest first, and canceled time off goes back to the grant.
Balances count what a grant will have lost by a day against what is available,
and list what is left of each grant, on the admin page and in My Work. The
accrual run expires what is left of a grant the day after its last day, once.
The balance form moved to the ocat layout.
… its type

Every time-off type gets a Time Tracker stage, in Time off and Paid when paid,
with a Time Tracker card on the type. Each day of approved time off of a worker
with a login is kept in it once the day comes: a full day from 9:00, a part day
ending at its end, read-only in Time Tracker. A new ITimeOffRequestObserver,
called after a request is saved, rebuilds the worker's days when time off is
submitted, approved, declined, canceled or given a place from the waitlist, and
an hourly task adds days as they come. Syncing from inside the approval change
committed a cancel half-way, so the sync runs after the save; a test covers it.
Time Tracker shows entry sources by name.
…user

The user editor has a Workforce card for people with Manage workers: it makes the
user a worker, or changes their worker's employee number, job title,
employment, status, hire date, main team (joined from today, keeping the teams
they are in) and contracted hours. WorkerAccounts does the work without saving
on its own, so it runs inside the user's save. A linked worker takes the user's
display name and email after each user update and on the worker page, where they
are read-only. Time Tracker records people under their display name too, so a
person shows under one name.
…enter setup, forms in cards

Geo territories, asset types and assets, work order settings and agreements,
adherence rule sets and forecasting setup travel by recipe and deployment step
with their schemas, keeping their ids; Dispatch and Geo settings get site
settings deployment steps. Time Tracker categories and stages keep their ids.
The field service and Contact Center workforce editors use ocat rows in cards.
Technicians keep their visits on the phone and work offline: status, answers,
parts, notes, photos and the customer's signature go into an outbox of keyed
commands, applied once and in order when online. The server wins on
assignment, answers merge by the device's time, and finishing completes the
work order. Location is shared only with consent and while working, refreshes
the ETA and shows on today's board. The same endpoints are under
api/dispatch/v1/mobile, settings come from CrestApps:Dispatch:Mobile, and the
My Work service worker keeps pages for offline use.
…g for dispatch

A new OR-Tools module (Google.OrTools, Apache 2.0) with two features. OR-Tools
Shift Scheduling auto-schedules shifts with a CP-SAT model under the built-in
solver's hard limits; OR-Tools Dispatch optimizes a day with the routing
solver, with open stretches, breaks, windows, allowed technicians, fixed
visits and a penalty per visit left out. Both start from the built-in answer
and keep the better one. The module isn't in the CMS targets because of its
native libraries. The auto-schedule message and the board's proposal name the
solver, and the auto-schedule shares no longer read 2,000%. Also pins
DispatchHub in the hub convention test and describes address coordinates in
the Customers schema.
…recipe and Get started

The Field Service API feature maps work orders (api/work-orders/v1, created once
per Idempotency-Key), visits, candidates, assignment and optimizing
(api/dispatch/v1), geocoding and territories (api/geo/v1) and assets
(api/assets/v1), each with the admin's permission. Seven AI tools create, follow
and summarize work orders, find technicians, optimize a day (applying only the
approved moves), reschedule visits, and tell customers where their technician
is by their own phone number. Reports > Field Service > KPIs and a Field
Service report data source with work orders and visits. The Field service
sample recipe and a Get started checklist on the dispatch board.
…ispatch, more field service KPIs and API reads

- Assets are owned by a customer, company or CRM contact picked by name (IAssetOwnerSource), with an
  Assets card on contact pages and an owner filter on the list; the work order editor picks its
  customer, company and contact the same way.
- The Field Service data source gains Assets, Meter readings and Service history data sets.
- Scheduling gains IAppointmentSlotFilter; Dispatch keeps a booking slot only while a technician can
  take the job, less the waiting bookings that overlap.
- Fix: a booking's work order window ended at the booking's end, so a longer job could never be
  dispatched; the booked time is now the arrival.
- KPIs add revenue per work order, repeat work on the same asset and parts by stock location; the API
  adds checklists, a day's board and service agreements.
…y, free parts by van, and asks for follow-ups

- The pack carries each work order's assets (IFieldAssetSource) with the latest five other work orders
  on them, and the units each stock location has free of the job's parts (IFieldStockSource.AvailableAsync).
- FieldCommandKind.FollowUp makes a follow-up work order once, with a note naming it.
- Fix: the sync badge said "1 changes waiting"; locations without a part now show none free.
…eement schedules with their own assets

- The checklist editor's Shown when column picks an earlier saved question and the answer; a question
  can't depend on itself or a later one.
- Each service agreement schedule picks its own assets; none means the agreement's.
…le sets for named workers

- IShiftService.RemoveAsync cancels a published shift, deletes its draft and logs the change.
- The shift page's On the published schedule card changes the hours, gives the shift to someone else
  or takes it off now, with a reason and whether the worker asked.
- Labor rule sets pick their own workers in the editor.
- Fix: the card's time read 'A to 5:00 PM'.
…low events

- Shifts approved time off took off the schedule are kept on the request with their drafts and put
  back when it is canceled, if still to come and the worker has no other shift then.
- Shift Assigned, Shift Changed and Shift Removed events from every published schedule change, and
  Time Off Waitlist Promoted.
- Fix: a time-off test compared worker ids sorted on one side only.
- ShiftReminderService reminds each worker of a shift as long before as they chose, once per shift
  and start, every five minutes.
- During a worker's quiet hours only the inbox gets notifications.
- Fix: saving My Work's notification settings turned off timesheet and attendance notifications.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant