Have something to say?

Tell us how we could make the product more useful to you.

Planned

Radon chart out of sync after inspector reassignment

Objective Keep the radon equipment chart (/tools/radon) accurate when radon-related calendar events are moved to a different inspector or corrected after the fact. Align chart totals per inspector with the underlying radon events (and with the calendar radon overlay where both are used). Reduce manual rework (e.g. resetting many events) when inspector assignment or historical baselines drift. Background Operations reported that the radon chart does not reflect events moved from one inspector row to another on the calendar; after correcting ~15 events manually, aggregate counts still did not match the event list—suggesting a disconnect between chart math and stored events. The chart is not directly editable: cell values are computed from (1) yesterday’s ending counts per inspector (RadonEnds), plus (2) radon events in the date window (event.radon.number, including equipment-derived radon), bucketed by current event._inspectorId. Calendar save (ChangedEventIndicator → PATCH event/:id) should persist a new _inspectorId, but the radon chart page does not auto-refresh after calendar saves (only after Equipment Transfer on the radon page). Radon data (/tools/radon/data) lists the same events grouped by job/inspection, not by inspector row—so it is normal for it to look different from the chart even when data is correct. Job-level inspector changes with “reassign events” only move incomplete future events (endtime > now in attik-backend/src/routes/inspection.ts); past radon pickups stay on the original inspector until moved on the calendar. Until a fix ships, teams can correct totals via Settings → Radon ending numbers, Equipment Transfer, and calendar drag + save + chart refresh (see internal ops note in team channel). Original report: Slack message Scope Backend Chart data is built in attik-backend/src/routes/radon.ts (GET /radon/radon-chart-data): loads legacy radon events and equipment-derived radon via findRadonEquipmentEventsForDateRange / mergeLegacyAndEquipmentRadonEvents in attik-backend/src/util/functions/radon/radonEquipmentBridge.ts, seeds each inspector from RadonEnds, then buckets by strict data._inspectorId === insp._id.toString() and dayjs time slots. Ending-number CRUD lives on the same router (GET/PATCH /radon/ends/:id); nightly rollup uses updateRadonEndsFromYesterday in radon.ts. Event updates use attik-backend/src/routes/event.ts (PATCH /event/:id); _inspectorId on attik-backend/src/models/eventSchema.ts is a string. Calendar API radon overlay uses attik-backend/src/routes/calendar.ts with enrichCalendarEventsWithEquipmentRadon and synthetic radonEnds for future dates. Decision needed: Normalize inspector matching and time-slot generation (chart uses strict equality and server dayjs() for slots while dateCheck uses company TZDayjs for the query window). Frontend Radon chart UI: attik-frontend/src/app/tools/radon/RadonChartBase.tsx (fetches radon/radon-chart-data), RadonContext.tsx (refreshChart / setRefreshChart), read-only cells in RadonCell.tsx. Equipment Transfer (creates transfer events, refreshes chart): attik-frontend/src/app/tools/radon/CalibrationModel.tsx, opened from CornerMenuWrapper.tsx (?calibration=new). Ending number edits (baseline per inspector/date): attik-frontend/src/app/tools/settings/radon/EndEditting.tsx → PATCH radon/ends/:id, embedded in RadonSettingsForm.tsx at /settings/radon. Event audit / transfer cleanup: attik-frontend/src/app/tools/radon/data/page.tsx, EventGroup.tsx (delete some transfer rows). Calendar reassignment: attik-frontend/src/components/calendar/handleChangeEventFn.tsx, EnhancedDragHandler.tsx, replaceEventsWithChangedEvents.ts, save via ChangedEventIndicator.tsx; radon row counts in CalendarRowGrid.tsx use cal._inspectorId.includes(employeeId) (looser than chart ===). Decision needed: Refresh chart after calendar event save; ensure calendar PATCH always sends _inspectorId as a string (drag handlers can produce a single-element array). Out of scope (unless expanded) Client portal, mobile. Replacing the cumulative chart model with direct cell editing (product change). Acceptance criteria After moving a radon event to another inspector on the calendar and saving, the event appears on the correct inspector row on /tools/radon without requiring a full browser cache clear (refresh or automatic re-fetch is acceptable). Chart totals for a test inspector match the sum of that inspector’s radon events (plus configured transfers/ends) for the same window used by /tools/radon/data. Inspector matching and time buckets behave consistently in company timezone. Documented ops path remains valid until release (ending numbers + equipment transfer + calendar reassignment). References Slack — original report attik-backend/src/routes/radon.ts attik-backend/src/util/functions/radon/radonEquipmentBridge.ts attik-backend/src/routes/event.ts attik-frontend/src/app/tools/radon/RadonChartBase.tsx attik-frontend/src/components/calendar/CalendarRowGrid.tsx attik-frontend/src/components/calendar/ChangedEventIndicator.tsx attik-frontend/src/app/tools/settings/radon/EndEditting.tsx

Linear 1 day ago

🏠

Main App

Planned

Online scheduler “+N more” doesn’t expand hidden time slots (Top Choice)

Objective Make the online scheduler calendar show all bookable times on busy days, not only the first five. Fix the “+N more” overflow control on the Choose Your Time Slot step so it actually expands (or is replaced with working UX). Unblock Top Choice Inspection Services testing and launch: 1:00 PM (and other times) are available on inspector schedules but hidden behind a non-functional “+2 more” label. Background Neil reported on the Top Choice online scheduler: at the time-picking step, a control that looks like an expander (“+2” / “+1”) does not respond when clicked. 1:00 PM availability is missing from the calendar even though Sean and David both have 1:00 on the internal schedule. Top Choice uses many custom start times, so a single day often has more than five distinct slot times from schedule/optimal-slots. Slots are returned by the API; the calendar truncates display to five per day and shows static “+N more” text with no click handler—users interpret it as broken. Repro walkthrough: Loom — Top Choice Online Scheduler Time Slot Not Showing Transcript summary (Neil): On Top Choice online scheduler, reached time selection. “Plus 2” / “plus 1” expander GUI does not work. 1:00 not shown; Sean and David both have 1:00 on the schedule. Expectation: clicking +2 / +1 should expand to show those times. Scope Frontend (attik-frontend) Public flow: /scheduler → Choose Your Time Slot in src/app/scheduler/slots/SlotsStep.tsx, rendered by src/app/scheduler/slots/CalendarView.tsx. src/app/scheduler/constants.ts sets MAX_VISIBLE_SLOTS_PER_DAY = 5; renderDayCell only maps slotsForDay.slice(0, MAX_VISIBLE_SLOTS_PER_DAY). When slotsForDay.length > MAX_VISIBLE_SLOTS_PER_DAY, UI renders a non-interactive label (+{count} more) around lines 694–697 in CalendarView.tsx—no expand state, no onClick / onPress. Slot times are grouped by exact datetime in slotsByDate; each custom start time counts toward the five-slot cap. SlotsStep passes autoSelectTopInspector={true}—separate from this bug: affects inspector picker on multi-inspector rows, not revealing hidden times. In scope: working expand/collapse per day (or equivalent) so overflow times (e.g. 1:00 PM) are selectable; accessible button semantics for overflow. Decision needed: scroll within day cell vs. expand-in-place vs. higher cap for companies with dense slot grids. Backend (attik-backend) GET schedule/optimal-slots in src/routes/schedule.ts supplies slots; no change required for this specific UI bug unless investigation shows 1:00 absent from API (unlikely per report). Out of scope (confirmed not the root cause) Missing API availability / service-area filtering as primary cause. Pay at Close and manual workorder payments. References Loom recording Calendar UI: attik-frontend/src/app/scheduler/slots/CalendarView.tsx Slot step: attik-frontend/src/app/scheduler/slots/SlotsStep.tsx Visible cap: attik-frontend/src/app/scheduler/constants.ts Slot API: attik-backend/src/routes/schedule.ts (/optimal-slots)

Linear 1 day ago

🏠

Main App

Planned

Reorder contacts in Attik mobile by role order

Objective On inspection detail in Attik mobile, the Contacts section should list people in the same order inspectors see on Attik web—driven by each role’s configured order (e.g. Clients → Client’s Agent → Listing Agent), not by how rows were added or synced into inspection.people. Align mobile with existing company contact-role settings so field staff can scan contacts in a predictable, role-based order. Background Product feedback (Christopher Scott): under Contacts on mobile, users expect contacts top-to-bottom as Clients, then Client’s Agent, then Listing Agent. The requirement is that display order matches the order field on populated roles in the people array—the same sort key companies configure under contact roles. Attik web already sorts inspection people by _roleId.order (with a high fallback when order is missing) in PeopleBox before rendering. Attik mobile currently passes inspection.people through unchanged; ContactsList maps the array in API/storage order, so Listing Agent or other roles can appear above Client or Client’s Agent even when role settings say otherwise. The inspection API already returns order on populated people._roleId; mobile types omit it but the value is available at runtime when people is populated. Scope Backend Contact roles define order on contactRolesSchema (attik-backend/src/models/contactRolesSchema.ts, default 9999). Inspection reads with populate: people include role order via people._roleId select in attik-backend/src/routes/inspection.ts (name, icon, description, portalPermissions, order). Aggregation population in attik-backend/src/util/functions/aggregation/inspectionPopulation.ts merges full role objects into people—no change required unless mobile populate paths differ. Frontend (reference behavior) attik-frontend/src/components/ui/PeopleBox.tsx is the parity target: roleDisplayOrder reads Number(role.order) with fallback 9999, comparePeopleForDisplay sorts people before grouping by role. Mobile should behave consistently with this pattern; exact reuse vs. local helper is a dev choice. Mobile Inspection detail in attik-mobile/app/(app)/inspection/[id].tsx builds people from inspection?.people and renders with no sort. attik-mobile/components/inspection/ContactsList.tsx maps people in array order. attik-mobile/types/index.ts — ContactRole currently has _id, name, type only; extending types for order may be needed for clarity. attik-mobile/hooks/useInspectionDetails.ts and useInspections.ts request populated people—data should include order when populated. Also in scope: the deduped uniquePeople list and client-portal dropdown options in [id].tsx are built from the same unsorted people loop; sorting for the Contacts section should keep portal option order aligned with the visible contact list. Out of scope unless discovered in QA: changing company role order values in settings; reordering rows inside inspection.people on the server. Decision needed Whether to sort in ContactsList only vs. a shared sorted people memo in [id].tsx (portal dropdown and any other consumers). Tie-break when two people share the same role order (web uses contact id string compare after role order). References Mobile: attik-mobile/app/(app)/inspection/[id].tsx, attik-mobile/components/inspection/ContactsList.tsx Web parity: attik-frontend/src/components/ui/PeopleBox.tsx API populate: attik-backend/src/routes/inspection.ts, attik-backend/src/models/contactRolesSchema.ts

Linear 2 days ago

🏠

Main App

Planned

Warn when client and agent contact details match

The scheduler should warn users when the client and agent appear to be the same person, so staff do not accidentally create orders where the agent is entered as the client. Transcript Context On the May 27, 2026 SE Attik call, the team described repeated cases where agents entered themselves as both the client and the agent. This led to bad downstream data on agreements and reports and created confusion about who the legal purchaser actually was. The suggested direction was to detect when the client and agent contact details match and prompt the user to confirm that they are actually the purchaser, rather than allowing the workflow to proceed silently. The group emphasized that agents may think they are protecting the client from emails, but the result is legally and operationally incorrect data.

Linear 3 days ago

🏠

Main App

Planned

PDF report can retain wrong client name after contact correction

Generated PDF reports can continue showing the wrong person’s name even after the order or contact information has been corrected. Transcript Context On the May 27, 2026 SE Attik call, the team discussed a case where the PDF output was still displaying the wrong name even though the record had been corrected. Ryan noted the bad value only appeared on the downloaded PDF output and said the immediate workaround would be to manually edit the PDF for the client. The group believes the issue may stem from an earlier client-name change flowing into the PDF output and not updating everywhere it should. This creates downstream confusion because the visible PDF can remain wrong even after the source order has been fixed.

Linear 3 days ago

🏠

Main App

Planned

Unified workspace: one tools shell across selected instances

Objective Let office staff (and similar internal roles) with memberships in multiple Attik instances work in one tools shell instead of switching company and reloading for every task. Support a user-defined workspace: a home instance for branding/navigation plus a selected set of included instances where the user’s permissioned reads (and eventually writes) are available in one place. Reduce coordinator friction for multi-brand operations (e.g. Florida / Monument-style groups) while preserving per-instance data isolation and permission boundaries. Background Today every tools session has a single active company (selectedCompany on the auth session, updated via POST /auth/web/switch-company in webAuthEndpoints.ts). Switching company in the UI calls switchCompanyAction and triggers a full page reload (NavUser.tsx). Nearly all tools APIs scope data with res.locals.company._id (calendar, schedule, quotes, inspections, events). Cross-company behavior exists only in narrow admin paths (e.g. cc-dashboard with getAllowedCompanyIds, GET /company/my-companies, Client Care Dashboard). The same user can hold multiple memberships (membershipSchema.ts) with different roles and permissions per _companyId; copyMembershipToCompanies.ts provisions separate employee records per company but does not unify the tools experience. This epic is separate from ATT-1714 (cross-instance inspector calendar blocking) and ATT-1763 (cross-instance quote handoff from workorder). Those solve scheduling availability and job→quote transfer; this epic solves coordinator UX and federated access across instances. Child issues Implement in dependency order via blockedBy: (created below) — Workspace model + session extension (created below) — Workspace configuration UI (Admin and/or account settings) (created below) — Cross-company authorization helpers (created below) — Federated calendar view (created below) — Federated worklist / tasks / inspections lists (created below) — Navigation shell without full reload on context change (created below) — Write paths and explicit create targeting Product decisions (proposed — confirm during implementation) Audience (v1): Office staff / coordinators with multiple active memberships; decision needed whether inspector tools roles are in scope. Workspace: homeCompanyId + includedCompanyIds[] ⊆ user’s active memberships. Permissions: Check against the resource’s _companyId membership (not a naive union of all permissions). Creates (v1): Always target one _companyId (explicit picker or row context); no ambiguous cross-company writes. Phasing: Prefer read federated first (calendar + high-traffic lists), then expand writes. Security: Optional client companyIds must always be intersected with allowed memberships (pattern from ccDashboard.ts). Scope (epic-level) Coordinates session, authorization, federated APIs, and tools UI. Detail lives on child issues. Key touchpoints: Session / switch: attik-backend/src/util/functions/betterAuth/web/webAuthEndpoints.ts, attik-frontend/src/actions/switchCompany.ts Memberships: attik-backend/src/models/membershipSchema.ts, attik-frontend/src/components/navbar/NavUser.tsx Multi-company precedent: attik-backend/src/routes/ccDashboard.ts, attik-backend/src/routes/company.ts (/my-companies), attik-frontend/src/app/admin/client-care-dashboard/ Calendar: attik-frontend/src/app/tools/calendar/MasterCalendar.tsx, attik-backend/src/routes/calendar.ts, attik-backend/src/routes/schedule.ts Out of scope: Merging tenant data; replacing instance-management; implementing ATT-1714 or ATT-1763 (integrate when those ship). References Related: ATT-1714 — Cross-instance shared calendars Related: ATT-1763 — Create quote in another instance from existing job Diagram flowchart TB subgraph today [Today] S[Session selectedCompany] T[Tools UI single company] API[APIs filter res.locals.company] S --> T --> API end subgraph target [Unified workspace] W[Workspace home + included companies] F[Federated read APIs] UI[One shell with company chips on rows] W --> F --> UI end today -.-> target

Linear 4 days ago

🏠

Main App

Planned

Link report and test-result context on cross-instance quotes

Objective Carry report or test-result context from the source job into the cross-instance handoff so destination CSRs can prepare proposals (e.g. radon mitigation) without hunting the source inspection. Background May 26 call called out supporting report/test-result context when helpful for the proposal. Source reports are tied to inspections via workorder reports UI (attik-frontend/src/app/tools/inspections/[id]/ reports section, client report URLs). Cross-instance report access may require links rather than file copy — decision needed. Scope Backend Decision needed: link-only (URL + metadata on quote or note) vs copy report artifacts into destination company storage. If link-only: persist source inspection slug/id, company id, and report identifiers readable by users with access to source instance. If copy: define allowed report types and permissions; likely larger scope. Optional note on destination quote (schedulerNotes / message on quoteSchema) with structured source reference. Frontend In workorder handoff flow: optional step or checkbox to include report context (list available published reports on source inspection). On destination pending quote: display link(s) back to source report(s) and/or attached summary for CSR. Out of scope Creating unconfirmed inspections in destination. Full report hub cross-instance permissions redesign. References attik-frontend/src/app/tools/inspections/[id]/components/reports/ (report actions on workorder) attik-backend/src/models/quoteSchema.ts (message, schedulerNotes)

Linear 4 days ago

🏠

Main App

Planned

Cross-instance service and charge mapping for destination quotes

Objective When handing off a job across instances, help CSRs map source inspection charges to destination company services so the new pending quote includes meaningful line items instead of an empty or manual-only quote. Background Parent epic: pending quote in destination from source inspection. Service catalogs differ by company/brand; source charge _serviceId values are not valid on destination quotes. Source charges live on inspection population and chargeSchema; destination quote charges are created via createOrUpdateCharges inside createQuoteWithCharges. Workorder UI child provides the handoff entry point; API child creates the quote shell. Scope Backend Preview or mapping endpoint: given source inspection + targetCompanyId, return source charges (name, category, amount) and suggested destination services (by name/category match — decision needed on matching rules). Accept user-selected service mapping (source charge or service id → destination service id) on create or follow-up PATCH to quote. Create destination charges with correct destination _serviceId and pricing rules for target company (decision needed: copy amounts vs recalculate via destination price engine). Do not copy source Mongo charge ids onto destination quote. Frontend In workorder handoff flow (after destination selected): show source line items and destination service picker or suggested matches. Allow proceed with partial mapping; clearly label unmapped items that CSR must add in destination scheduler. Out of scope Unconfirmed inspection creation. Spectora service id sync across companies. References attik-backend/src/util/functions/quote/createQuoteWithCharges.ts attik-backend/src/models/chargeSchema.ts attik-backend/src/models/serviceSchema.ts (services catalog)

Linear 4 days ago

🏠

Main App

Planned

Cross-instance contact match and role mapping

Objective Ensure contacts on cross-instance quotes are matched or created in the destination company with correct contact roles, including user-visible mapping when roles do not align across brands. Background Parent epic: pending quote in destination from source inspection. API child creates people[] on the quote; this child deepens role mapping and UX for mismatches. Action flow migration already solves cross-company contact role and required info mapping in ActionFlowMigrationClient.tsx with backend support in contactRoleFlowMigration.ts and copyActionFlowToCompany.ts — reuse patterns where possible, scoped to quote handoff. Scope Backend Extend cross-instance handoff to accept optional contactRoleMapping (source role id → target role id), similar to flow.ts copy body shape. Suggest target roles for unmapped source roles (endpoint or included in preview response — decision needed). Match-or-create contacts by email/phone; never persist source _contactId on destination quote. Handle primary contact flag and multiple people on source inspection. Frontend When source and target roles differ, present mapping UI before create (or on preview step) — patterns from attik-frontend/src/app/admin/settings/instance-management/action-flow-migration/ActionFlowMigrationClient.tsx (crSuggestions, crUserMapping). Surface unmapped roles clearly; block submit until required mappings resolved or decision needed for optional roles. Out of scope Service/charge mapping. Report attachments. References attik-backend/src/util/functions/actionFlows/contactRoleFlowMigration.ts attik-backend/src/util/functions/spectora/findSpectoraContactMatches.ts attik-frontend/src/app/admin/settings/instance-management/action-flow-migration/ActionFlowMigrationClient.tsx

Linear 4 days ago

🏠

Main App

Planned

Workorder UI: create quote in another instance

Objective Add a workorder action so CSRs can pick a destination instance and create a pending quote from the current inspection with clear confirmation of what will be copied. Navigate the user to the new quote (or quote workflow) in the destination instance after success. Background Preferred UX is inspection workorder only (not admin instance management). Depends on cross-instance create API (sibling child: pending quote from source inspection). Workorder shell and actions live under attik-frontend/src/app/tools/inspections/[id]/ (InspectionWorkorderShell.tsx, WorkorderActionsDropdown.tsx). Users switch companies today via membership / switchCompany in attik-frontend/src/actions/switchCompany.ts; target picker should list only companies the user can access. Scope Frontend New action on workorder (e.g. in WorkorderActionsDropdown.tsx or equivalent): Create quote in another instance… Modal or drawer flow: Destination company selector (memberships user shares with source). Summary of what will copy (property, contacts) and what will not (services until mapping child, integrations, source job link TBD). Call cross-instance API; handle errors (no permission, no membership). Success: navigate to destination pending quote (quote detail, task list, or scheduler entry point — decision needed). Show source context on success or on destination quote when provenance fields exist (source company name, inspection id/slug). Respect permission gate aligned with API child (dual membership + dedicated permission). Backend None beyond API contract from sibling child; optional small GET for eligible target companies if not inferred from session memberships. Out of scope Admin instance-management entry point. Contact role mapping UI (separate child). Service mapping and report attachment (separate children). References attik-frontend/src/app/tools/inspections/[id]/components/WorkorderActionsDropdown.tsx attik-frontend/src/app/tools/inspections/[id]/components/InspectionWorkorderShell.tsx attik-frontend/src/actions/switchCompany.ts attik-frontend/src/components/task-check/SingleQuoteBar.tsx (quote navigation patterns)

Linear 4 days ago

🏠

Main App

Planned

Cross-instance API: create pending quote from source inspection

Objective Provide a backend API that creates a pending quote in a target company from a source inspection, carrying property and contact data without cross-company ID leakage. Enforce dual-membership and a dedicated permission so only authorized users can hand off jobs across instances. Background Parent epic: cross-instance quote from existing job (workorder entry point). Quotes are created per company via createQuoteWithCharges in attik-backend/src/util/functions/quote/createQuoteWithCharges.ts with _companyId on quoteSchema. No existing route creates a quote in company B from an inspection in company A. Cross-instance action flow copy (copyActionFlowToCompany in attik-backend/src/util/functions/actionFlows/copyActionFlowToCompany.ts, endpoints in attik-backend/src/routes/flow.ts) is the closest pattern for targetCompanyId + allowed-company checks. Scope Backend New endpoint (e.g. under instance-management or inspection/:id/...) accepting source inspection id + targetCompanyId; validate user membership in both companies and new permission. Load populated source inspection (property, people, charges as reference); build quote payload for target _companyId. Create quote with status: 'pending' via createQuoteWithCharges (or equivalent transaction used by attik-backend/src/routes/quote.ts). Contacts: match-or-create in target company (patterns similar to findSpectoraContactMatches in attik-backend/src/util/functions/spectora/findSpectoraContactMatches.ts); map people[] with destination _contactId / _roleId only. Property: copy address/property fields onto destination quote property. Provenance: decision needed — optional fields on quote for _sourceInspectionId, _sourceCompanyId, created-by metadata for audit. Exclude from copy: wcLeadId, _hubspotDealId, _finalInspectionId, source charge/service Mongo IDs, source inspector IDs. Services/charges: initial create may omit charges or include empty charge list until service-mapping child lands; decision needed on minimum viable payload. Return created quote id/slug and target company id for frontend navigation. Frontend None in this child (workorder UI is a separate child); expose types/server action contract as needed for the UI child. Out of scope Workorder modal UX. Contact role mapping UI (separate child). Service mapping, report attachments. Creating unconfirmed inspections in destination. References attik-backend/src/util/functions/quote/createQuoteWithCharges.ts attik-backend/src/routes/quote.ts attik-backend/src/models/quoteSchema.ts attik-backend/src/routes/protectedRouteBarrel.ts (/instance-management) attik-backend/src/routes/flow.ts (cross-company copy guards)

Linear 4 days ago

🏠

Main App