Planned
Add Date Type filter to Reports Hub
Objective Add a Date Type option to Reports Hub so users can choose whether a report’s date range is based on Booked Date or Inspection Date. Make date-filtered reporting more trustworthy by letting users explicitly control which date basis is used instead of inferring it from the dataset or current implementation. Background Reports Hub currently supports date-range filtering, but there is no visible option to choose whether that range should apply to booked date or inspection date. The same distinction is already important elsewhere in the product, because booked-date reporting answers a different question than inspection-date reporting. Without an explicit date type selection, users can misread results when they are trying to understand what was booked during a period versus what was scheduled to occur during that period. Product Decisions Locked Add a user-facing Date Type control for Reports Hub date filtering. The available options should be Booked Date and Inspection Date. The selected date type should control how the report’s date range is interpreted. Open Decision needed: which Reports Hub datasets should support the date type option in the first release. Decision needed: whether one option should be the default for new reports and saved reports. Decision needed: whether saved reports should persist the selected date type. Decision needed: whether exports and preview results should both reflect the selected date type automatically. Scope Reports Hub Add a date type selector anywhere users currently set a date range for a report. Ensure the selected option changes the underlying date field used for filtering. Make the active date basis clear enough that users can understand whether results represent booked activity or scheduled inspection activity. Apply the same behavior consistently anywhere the report preview, saved report, or export depends on the selected date range. References Similar product pattern: ATT-1809 Related Reports Hub project: Reports Hub
Linear about 5 hours ago
Main App
Planned
Add Date Type filter to Reports Hub
Objective Add a Date Type option to Reports Hub so users can choose whether a report’s date range is based on Booked Date or Inspection Date. Make date-filtered reporting more trustworthy by letting users explicitly control which date basis is used instead of inferring it from the dataset or current implementation. Background Reports Hub currently supports date-range filtering, but there is no visible option to choose whether that range should apply to booked date or inspection date. The same distinction is already important elsewhere in the product, because booked-date reporting answers a different question than inspection-date reporting. Without an explicit date type selection, users can misread results when they are trying to understand what was booked during a period versus what was scheduled to occur during that period. Product Decisions Locked Add a user-facing Date Type control for Reports Hub date filtering. The available options should be Booked Date and Inspection Date. The selected date type should control how the report’s date range is interpreted. Open Decision needed: which Reports Hub datasets should support the date type option in the first release. Decision needed: whether one option should be the default for new reports and saved reports. Decision needed: whether saved reports should persist the selected date type. Decision needed: whether exports and preview results should both reflect the selected date type automatically. Scope Reports Hub Add a date type selector anywhere users currently set a date range for a report. Ensure the selected option changes the underlying date field used for filtering. Make the active date basis clear enough that users can understand whether results represent booked activity or scheduled inspection activity. Apply the same behavior consistently anywhere the report preview, saved report, or export depends on the selected date range. References Similar product pattern: ATT-1809 Related Reports Hub project: Reports Hub
Linear about 5 hours ago
Main App
Planned
Allow deleting quotes based on permissions
Make it possible to delete quotes based on permissions the same way orders can be deleted. The goal is to remove duplicate online orders instead of only archiving them, which does not clean up the search bar.
Linear about 8 hours ago
Main App
Planned
Allow deleting quotes based on permissions
Make it possible to delete quotes based on permissions the same way orders can be deleted. The goal is to remove duplicate online orders instead of only archiving them, which does not clean up the search bar.
Linear about 8 hours ago
Main App
Planned
BA contact missing from HubSpot payload
In some cases, the client is being listed as the client's agent and associated as the BA in HubSpot. When that happens, the actual BA is still listed on the Attik job but does not appear in the BA payload in HubSpot. There is also a question about supporting a primary role type when multiple people are listed for the same role. Relevant examples: inspection, HubSpot contact, HubSpot record.
Linear about 11 hours ago
Main App
Planned
BA contact missing from HubSpot payload
In some cases, the client is being listed as the client's agent and associated as the BA in HubSpot. When that happens, the actual BA is still listed on the Attik job but does not appear in the BA payload in HubSpot. There is also a question about supporting a primary role type when multiple people are listed for the same role. Relevant examples: inspection, HubSpot contact, HubSpot record.
Linear about 11 hours ago
Main App
Planned
Automate inspector travel fees with service area conditions in pricing rules
Objective Automate inspector-specific travel and trip fees so pricing reflects who is assigned and which service area the job falls in—not only the job’s map location. Expose service area as a reusable condition anywhere Attik already configures structured rules (starting with pricing modifiers), so teams can combine inspector + service area logic without one-off workarounds. Unblock online scheduling and reduce manual payroll adjustments by auto-calculating travel fees for most bookings, with manual edits reserved for exceptions. Background NE Region (2026-06-11): Operations and product discussed automating travel pricing enough that staff do not need to intervene on most orders. A practical path is conditional modifiers keyed on inspector plus service area, so specific inspectors can trigger different price and admin-fee changes in specific areas. The same discussion clarified that service area should not live only in service-area settings—it should appear in condition builders wherever rules are configured (e.g. “if service area is X” combined with inspector logic). Business pressure: Online scheduling is blocked until travel and trip-fee behavior is reliable enough to run without constant manual correction. The target discussed was ~90–95% of scenarios auto-calculated, with one-off exceptions handled manually. Current system behavior (not a bug): Expanded fees are one amount per service area polygon (expanded + expandedFee on the area). Pricing at schedule and edit time uses service-area/find-containing and applies the highest expandedFee among matching expanded areas via serviceAreaCost / expandedAreaPrice. Selected inspector does not change that fee today. Partial building blocks today: Conditional modifiers already support inspector-id in modifierPriceCalc.ts / modifierPriceAdjFn.ts, and scheduling passes inspectorIds when an inspector is assigned. Service area is not a condition attribute in templatingData.ts or modifier condition UIs yet. Related work (different axis): ATT-848 covers which services receive an expanded-area fee, not inspector-specific rules. ATT-1041 covers drive-time tiers and native inspector travel-fee configuration not addressed here. Product Decisions Locked Delivery path — Near-term delivery uses service area as a condition input plus inspector×area rules in pricing modifiers (and equivalent evaluation paths), not a new native travel-fees schema in this issue. Condition surfaces — Service area must be available anywhere structured conditions are configured that need it for pricing rules, not buried only under Settings → Service areas. Success target — Aim for ~90–95% of travel-fee scenarios to auto-calculate in scheduling and online booking; manual adjustment is acceptable for exceptions only. Online scheduler — Travel-fee automation must work in the online scheduler flow, not only internal scheduling tools. Open Expanded fee vs modifier travel — How today’s location-only expandedFee / serviceAreaCost path coexists with modifier-based travel rules (avoid double-charging; decide whether polygon expanded fees are superseded, zeroed, or kept for fallback). Pre-slot online pricing — Whether online scheduler shows an area-only estimate before slot selection, then refines when inspectorIds are known, or requires another UX pattern. Condition surface rollout — Whether v1 is modifiers only or also other condition builders (flows, quotes, etc.) in the same release. Admin fee / payroll — How modifier-driven travel admin fees align with payroll and commission splits so configured rules do not fight existing admin modifiers. Per-service expanded fees — Interaction with ATT-848 when multiple services on one job can carry area-related charges. Scope Backend Modifier evaluation: attik-backend/src/util/functions/schedule/modifierPriceCalc.ts — getAttributeValue supports inspector-id today; service area would need to be resolvable at quote/schedule time alongside existing ModifierContextData (inspectorIds, property, required info, equipment). Modifier model: attik-backend/src/models/modifierSchema.ts — conditional modifier shape already stores condition groups; no service-area attribute in evaluation yet. Service areas: attik-backend/src/models/serviceAreaSchema.ts, attik-backend/src/routes/serviceArea.ts — find-containing returns areas for a property; pricing flows must pass which containing area(s) apply into modifier context, not only a single expandedFee amount. Payroll context: attik-backend/src/util/functions/payroll/payrollContext.ts — applies modifiers with inspectorIds from inspections; travel rules must stay consistent here when admin fees change. Job payloads: attik-backend/src/util/functions/spectora/spectoraJobRequest.ts — serviceAreaCost on service matches; alignment needed if travel moves from polygon fee to modifier-driven amounts. Frontend Condition catalog: attik-frontend/src/components/conditions/templatingData.ts — add service-area conditional keys; today modifierSpecificConditionalKeys includes inspector-id under the inspector group but no service-area group. Condition UI: attik-frontend/src/components/conditions/ValueConditionInput.tsx, Conditions.tsx — value pickers for new service-area attribute(s). Modifier settings: attik-frontend/src/app/tools/settings/services/[id]/Modifier/ConditionalModifier.tsx, ModifierConditions.tsx — conditional modifier builder; extend available groups/keys for service area. Price calculation: attik-frontend/src/util/functions/schedulingHelpers/calculateServicePrices.ts, usePriceCalculation.ts — today adds serviceAreaCost to primary service only; must reconcile with modifier-based travel once rules exist. Internal scheduling: attik-frontend/src/components/scheduling/NewSchedulingForm.tsx — passes serviceAreaCost: finalServiceArea?.serviceArea?.expandedFee and inspectorIds from scheduledInspector; recalc when inspector or area context changes. Online scheduler: attik-frontend/src/app/scheduler/SchedulerContext.tsx — passes expandedFee and inspectorIds from selectedSlotInspectorInfo after slot pick; travel automation must behave correctly in this path. Edit charges: attik-frontend/src/app/tools/inspections/[id]/components/EditChargesModal.tsx — location-based expanded fee today; must stay consistent when inspector changes on an existing job. Out of scope (unless product expands) Drive-time tier fees (60–90 min, 90+ min) — tracked on ATT-1041. Native per-inspector fee overrides on inspector profile — tracked on ATT-1041; this issue may satisfy pricing behavior first via modifiers. Which services get an expanded-area fee — tracked on ATT-848. Mobile-specific UI unless mobile scheduling must surface the same calculated fees. References Featurebase: inspector-specific travel fees by service area Featurebase: service area assignment expanded fee per inspector ATT-1041 — drive-time and native travel-fee configuration ATT-848 — expanded fee on which services attik-backend/src/util/functions/schedule/modifierPriceCalc.ts attik-frontend/src/components/conditions/templatingData.ts attik-frontend/src/app/tools/settings/services/[id]/Modifier/ConditionalModifier.tsx attik-frontend/src/util/functions/schedulingHelpers/calculateServicePrices.ts attik-frontend/src/app/scheduler/SchedulerContext.tsx
Linear about 13 hours ago
Main App
Planned
Automate inspector travel fees with service area conditions in pricing rules
Objective Automate inspector-specific travel and trip fees so pricing reflects who is assigned and which service area the job falls in—not only the job’s map location. Expose service area as a reusable condition anywhere Attik already configures structured rules (starting with pricing modifiers), so teams can combine inspector + service area logic without one-off workarounds. Unblock online scheduling and reduce manual payroll adjustments by auto-calculating travel fees for most bookings, with manual edits reserved for exceptions. Background NE Region (2026-06-11): Operations and product discussed automating travel pricing enough that staff do not need to intervene on most orders. A practical path is conditional modifiers keyed on inspector plus service area, so specific inspectors can trigger different price and admin-fee changes in specific areas. The same discussion clarified that service area should not live only in service-area settings—it should appear in condition builders wherever rules are configured (e.g. “if service area is X” combined with inspector logic). Business pressure: Online scheduling is blocked until travel and trip-fee behavior is reliable enough to run without constant manual correction. The target discussed was ~90–95% of scenarios auto-calculated, with one-off exceptions handled manually. Current system behavior (not a bug): Expanded fees are one amount per service area polygon (expanded + expandedFee on the area). Pricing at schedule and edit time uses service-area/find-containing and applies the highest expandedFee among matching expanded areas via serviceAreaCost / expandedAreaPrice. Selected inspector does not change that fee today. Partial building blocks today: Conditional modifiers already support inspector-id in modifierPriceCalc.ts / modifierPriceAdjFn.ts, and scheduling passes inspectorIds when an inspector is assigned. Service area is not a condition attribute in templatingData.ts or modifier condition UIs yet. Related work (different axis): ATT-848 covers which services receive an expanded-area fee, not inspector-specific rules. ATT-1041 covers drive-time tiers and native inspector travel-fee configuration not addressed here. Product Decisions Locked Delivery path — Near-term delivery uses service area as a condition input plus inspector×area rules in pricing modifiers (and equivalent evaluation paths), not a new native travel-fees schema in this issue. Condition surfaces — Service area must be available anywhere structured conditions are configured that need it for pricing rules, not buried only under Settings → Service areas. Success target — Aim for ~90–95% of travel-fee scenarios to auto-calculate in scheduling and online booking; manual adjustment is acceptable for exceptions only. Online scheduler — Travel-fee automation must work in the online scheduler flow, not only internal scheduling tools. Open Expanded fee vs modifier travel — How today’s location-only expandedFee / serviceAreaCost path coexists with modifier-based travel rules (avoid double-charging; decide whether polygon expanded fees are superseded, zeroed, or kept for fallback). Pre-slot online pricing — Whether online scheduler shows an area-only estimate before slot selection, then refines when inspectorIds are known, or requires another UX pattern. Condition surface rollout — Whether v1 is modifiers only or also other condition builders (flows, quotes, etc.) in the same release. Admin fee / payroll — How modifier-driven travel admin fees align with payroll and commission splits so configured rules do not fight existing admin modifiers. Per-service expanded fees — Interaction with ATT-848 when multiple services on one job can carry area-related charges. Scope Backend Modifier evaluation: attik-backend/src/util/functions/schedule/modifierPriceCalc.ts — getAttributeValue supports inspector-id today; service area would need to be resolvable at quote/schedule time alongside existing ModifierContextData (inspectorIds, property, required info, equipment). Modifier model: attik-backend/src/models/modifierSchema.ts — conditional modifier shape already stores condition groups; no service-area attribute in evaluation yet. Service areas: attik-backend/src/models/serviceAreaSchema.ts, attik-backend/src/routes/serviceArea.ts — find-containing returns areas for a property; pricing flows must pass which containing area(s) apply into modifier context, not only a single expandedFee amount. Payroll context: attik-backend/src/util/functions/payroll/payrollContext.ts — applies modifiers with inspectorIds from inspections; travel rules must stay consistent here when admin fees change. Job payloads: attik-backend/src/util/functions/spectora/spectoraJobRequest.ts — serviceAreaCost on service matches; alignment needed if travel moves from polygon fee to modifier-driven amounts. Frontend Condition catalog: attik-frontend/src/components/conditions/templatingData.ts — add service-area conditional keys; today modifierSpecificConditionalKeys includes inspector-id under the inspector group but no service-area group. Condition UI: attik-frontend/src/components/conditions/ValueConditionInput.tsx, Conditions.tsx — value pickers for new service-area attribute(s). Modifier settings: attik-frontend/src/app/tools/settings/services/[id]/Modifier/ConditionalModifier.tsx, ModifierConditions.tsx — conditional modifier builder; extend available groups/keys for service area. Price calculation: attik-frontend/src/util/functions/schedulingHelpers/calculateServicePrices.ts, usePriceCalculation.ts — today adds serviceAreaCost to primary service only; must reconcile with modifier-based travel once rules exist. Internal scheduling: attik-frontend/src/components/scheduling/NewSchedulingForm.tsx — passes serviceAreaCost: finalServiceArea?.serviceArea?.expandedFee and inspectorIds from scheduledInspector; recalc when inspector or area context changes. Online scheduler: attik-frontend/src/app/scheduler/SchedulerContext.tsx — passes expandedFee and inspectorIds from selectedSlotInspectorInfo after slot pick; travel automation must behave correctly in this path. Edit charges: attik-frontend/src/app/tools/inspections/[id]/components/EditChargesModal.tsx — location-based expanded fee today; must stay consistent when inspector changes on an existing job. Out of scope (unless product expands) Drive-time tier fees (60–90 min, 90+ min) — tracked on ATT-1041. Native per-inspector fee overrides on inspector profile — tracked on ATT-1041; this issue may satisfy pricing behavior first via modifiers. Which services get an expanded-area fee — tracked on ATT-848. Mobile-specific UI unless mobile scheduling must surface the same calculated fees. References Featurebase: inspector-specific travel fees by service area Featurebase: service area assignment expanded fee per inspector ATT-1041 — drive-time and native travel-fee configuration ATT-848 — expanded fee on which services attik-backend/src/util/functions/schedule/modifierPriceCalc.ts attik-frontend/src/components/conditions/templatingData.ts attik-frontend/src/app/tools/settings/services/[id]/Modifier/ConditionalModifier.tsx attik-frontend/src/util/functions/schedulingHelpers/calculateServicePrices.ts attik-frontend/src/app/scheduler/SchedulerContext.tsx
Linear about 13 hours ago
Main App
Planned
Fix PDF invoice false discount styling on repriced charge lines
Objective Stop PDF invoices from showing crossed-out prices, “Discount applied” copy, and discount savings when the customer did not receive a real promotion—especially on orders where internal pricing adjustments (travel rebalance, modifiers, overrides) were applied. Keep legitimate bundle and discount-code savings visible on invoices where those discounts actually apply. Align client-facing invoice PDFs with the total amount due teams expect, without misleading discount styling on individual lines. Background Live example (RI5887): Travel-related admin-fee modifiers were used so total price would increase without affecting inspector commission. The total due looked correct on the client-facing side, but the PDF invoice showed the original amount crossed out with a lower amount beneath it (~$90), as if a customer discount had been applied. The team was not certain whether RI5887 was caused only by admin-fee modifier setup or by another pricing interaction on that order. Code review shows admin-only modifiers do not change customer amount—they adjust admin/commission math in modifierPriceCalc.ts. Misleading discount styling is driven by invoice display heuristics, not admin modifier evaluation itself. Current invoice behavior: In InvoicePDF.tsx and InvoiceAttachmentPDF.tsx, a charge line is treated as discounted when originalBasePrice amount, or when any discount metadata is present (discountAmount, bundle/code discount fields, _discountCodeId). Matching lines render strikethrough, a bold final amount, and generic “Discount applied” when no bundle/code name is available. The same originalBasePrice amount strikethrough appears on the workorder Charges & Payments table in ServicesPayments.tsx (comment in PDF code says to keep in sync). Surfaces: PDF download/preview via InvoiceDrawer.tsx (tools workorder and client portal); email attachments via backend buildTemplateEmailAttachments.ts using InvoiceAttachmentPDF.tsx. Product Decisions Locked Problem type — This is incorrect customer-facing presentation on PDF invoices, not a request to change how admin modifiers calculate payroll. Legitimate discounts — Bundle and discount-code savings that are real customer promotions should continue to show appropriate discount styling on invoices when product confirms the attribution fields are populated. Template parity — Frontend InvoicePDF.tsx and backend InvoiceAttachmentPDF.tsx must stay aligned; both are in scope. Open RI5887 trigger validation — Which charge fields on RI5887 caused the display (originalBasePrice, amount, preTaxAmount, discountAmount, bundle/code IDs, modifier types on the service)? Needed to confirm whether the fix is display-only or also charge persistence. Manual price overrides — When staff set a price override below list/modifier-calculated price, should the invoice show strikethrough/discount styling, a single flat amount, or something else? Charges & Payments table — Should the same discount-display fix apply to the workorder Charges & Payments strikethrough (ServicesPayments.tsx), or PDF/email only in v1? Generic “Discount applied” copy — When strikethrough is warranted, should generic fallback copy remain, or should strikethrough appear only when bundle name or discount code name is available? originalBasePrice vs amount comparison — originalBasePrice is pre-tax starting fee; amount includes tax. Should invoice discount detection require explicit discount fields (bundle/code/discountAmount) rather than any case where stored base exceeds final amount? Scope Frontend Invoice PDF (preview/download): attik-frontend/src/components/invoice/InvoicePDF.tsx — getBaseOriginalTotal, per-line hasDiscount, strikethrough rendering, subtotal/savings section (showDiscountUI, totalSavings). Invoice drawer entry points: attik-frontend/src/components/invoice/InvoiceDrawer.tsx — used from ServicesPayments.tsx (tools) and JobAccordion.tsx (client portal). Workorder charges table (likely same fix): attik-frontend/src/app/tools/inspections/[id]/components/ServicesPayments.tsx — originalBasePrice > charge.amount strikethrough; keep in sync with PDF per code comment. Backend Email/PDF attachments: attik-backend/src/util/functions/pdf/templates/InvoiceAttachmentPDF.tsx — duplicate discount logic; must match frontend fix. Attachment builder: attik-backend/src/util/functions/emailBuilder/buildTemplateEmailAttachments.ts — renders InvoiceAttachmentPDF for template emails. Context (not modifier logic changes unless validation requires) Charge shape: attik-backend/src/models/chargeSchema.ts — originalBasePrice, discountAmount, bundle/code discount fields, amount, preTaxAmount. Charge creation: attik-frontend/src/util/functions/quote/createChargeObjects.ts — sets originalBasePrice from startingFee, amount from totalPrice / priceOverride. Modifier evaluation: attik-backend/src/util/functions/schedule/modifierPriceCalc.ts — addType: 'admin' affects finalAdminAddition only; addType: 'cost' affects customer finalPrice. Display bug is separate from admin modifier math unless charge fields are written incorrectly on save. Out of scope (unless product expands) Changing how admin, cost, or travel modifiers calculate prices or payroll. Client portal non-PDF pricing displays (unless tied to the same shared discount helper after PDF fix). ATT-1892 service-area travel modifier work—related operationally but separate delivery. References Example order discussed: RI5887 attik-frontend/src/components/invoice/InvoicePDF.tsx attik-backend/src/util/functions/pdf/templates/InvoiceAttachmentPDF.tsx attik-frontend/src/app/tools/inspections/[id]/components/ServicesPayments.tsx attik-backend/src/util/functions/schedule/modifierPriceCalc.ts attik-frontend/src/util/functions/quote/createChargeObjects.ts
Linear about 13 hours ago
Main App
Planned
Fix PDF invoice false discount styling on repriced charge lines
Objective Stop PDF invoices from showing crossed-out prices, “Discount applied” copy, and discount savings when the customer did not receive a real promotion—especially on orders where internal pricing adjustments (travel rebalance, modifiers, overrides) were applied. Keep legitimate bundle and discount-code savings visible on invoices where those discounts actually apply. Align client-facing invoice PDFs with the total amount due teams expect, without misleading discount styling on individual lines. Background Live example (RI5887): Travel-related admin-fee modifiers were used so total price would increase without affecting inspector commission. The total due looked correct on the client-facing side, but the PDF invoice showed the original amount crossed out with a lower amount beneath it (~$90), as if a customer discount had been applied. The team was not certain whether RI5887 was caused only by admin-fee modifier setup or by another pricing interaction on that order. Code review shows admin-only modifiers do not change customer amount—they adjust admin/commission math in modifierPriceCalc.ts. Misleading discount styling is driven by invoice display heuristics, not admin modifier evaluation itself. Current invoice behavior: In InvoicePDF.tsx and InvoiceAttachmentPDF.tsx, a charge line is treated as discounted when originalBasePrice amount, or when any discount metadata is present (discountAmount, bundle/code discount fields, _discountCodeId). Matching lines render strikethrough, a bold final amount, and generic “Discount applied” when no bundle/code name is available. The same originalBasePrice amount strikethrough appears on the workorder Charges & Payments table in ServicesPayments.tsx (comment in PDF code says to keep in sync). Surfaces: PDF download/preview via InvoiceDrawer.tsx (tools workorder and client portal); email attachments via backend buildTemplateEmailAttachments.ts using InvoiceAttachmentPDF.tsx. Product Decisions Locked Problem type — This is incorrect customer-facing presentation on PDF invoices, not a request to change how admin modifiers calculate payroll. Legitimate discounts — Bundle and discount-code savings that are real customer promotions should continue to show appropriate discount styling on invoices when product confirms the attribution fields are populated. Template parity — Frontend InvoicePDF.tsx and backend InvoiceAttachmentPDF.tsx must stay aligned; both are in scope. Open RI5887 trigger validation — Which charge fields on RI5887 caused the display (originalBasePrice, amount, preTaxAmount, discountAmount, bundle/code IDs, modifier types on the service)? Needed to confirm whether the fix is display-only or also charge persistence. Manual price overrides — When staff set a price override below list/modifier-calculated price, should the invoice show strikethrough/discount styling, a single flat amount, or something else? Charges & Payments table — Should the same discount-display fix apply to the workorder Charges & Payments strikethrough (ServicesPayments.tsx), or PDF/email only in v1? Generic “Discount applied” copy — When strikethrough is warranted, should generic fallback copy remain, or should strikethrough appear only when bundle name or discount code name is available? originalBasePrice vs amount comparison — originalBasePrice is pre-tax starting fee; amount includes tax. Should invoice discount detection require explicit discount fields (bundle/code/discountAmount) rather than any case where stored base exceeds final amount? Scope Frontend Invoice PDF (preview/download): attik-frontend/src/components/invoice/InvoicePDF.tsx — getBaseOriginalTotal, per-line hasDiscount, strikethrough rendering, subtotal/savings section (showDiscountUI, totalSavings). Invoice drawer entry points: attik-frontend/src/components/invoice/InvoiceDrawer.tsx — used from ServicesPayments.tsx (tools) and JobAccordion.tsx (client portal). Workorder charges table (likely same fix): attik-frontend/src/app/tools/inspections/[id]/components/ServicesPayments.tsx — originalBasePrice > charge.amount strikethrough; keep in sync with PDF per code comment. Backend Email/PDF attachments: attik-backend/src/util/functions/pdf/templates/InvoiceAttachmentPDF.tsx — duplicate discount logic; must match frontend fix. Attachment builder: attik-backend/src/util/functions/emailBuilder/buildTemplateEmailAttachments.ts — renders InvoiceAttachmentPDF for template emails. Context (not modifier logic changes unless validation requires) Charge shape: attik-backend/src/models/chargeSchema.ts — originalBasePrice, discountAmount, bundle/code discount fields, amount, preTaxAmount. Charge creation: attik-frontend/src/util/functions/quote/createChargeObjects.ts — sets originalBasePrice from startingFee, amount from totalPrice / priceOverride. Modifier evaluation: attik-backend/src/util/functions/schedule/modifierPriceCalc.ts — addType: 'admin' affects finalAdminAddition only; addType: 'cost' affects customer finalPrice. Display bug is separate from admin modifier math unless charge fields are written incorrectly on save. Out of scope (unless product expands) Changing how admin, cost, or travel modifiers calculate prices or payroll. Client portal non-PDF pricing displays (unless tied to the same shared discount helper after PDF fix). ATT-1892 service-area travel modifier work—related operationally but separate delivery. References Example order discussed: RI5887 attik-frontend/src/components/invoice/InvoicePDF.tsx attik-backend/src/util/functions/pdf/templates/InvoiceAttachmentPDF.tsx attik-frontend/src/app/tools/inspections/[id]/components/ServicesPayments.tsx attik-backend/src/util/functions/schedule/modifierPriceCalc.ts attik-frontend/src/util/functions/quote/createChargeObjects.ts
Linear about 13 hours ago
Main App
Planned
Report status changes lack an audit trail
Catherine reported that Kris Smith said he did not publish reports, but Attik shows the Sewer Line Evaluation report completed on two orders. Affected work orders: 1008599255 1008599424 Relevant inspections: Inspection 1008599255 Inspection 1008599424 PostHog investigation found that both Sewer Line Evaluation reports moved from pending to completed, and the rendered activity feed attributes the change to Kris Smith. However, there is no instrumented event that captures the actual status change action, so it is not possible to determine with confidence who triggered it, from which surface, or at what exact time from PostHog data alone. What was confirmed: The status change did occur on inspection 1008599255; the activity feed shows Kris Smith's report status changed to completed. Kris Smith was active on both inspections via the iOS app around the relevant time window. Lou Qualtiere clicked the report-level Actions button on inspection 1008599424, but PostHog did not capture which dropdown option was selected. Other users interacted with the inspections on web, but no event definitively ties a user action to the status transition. Current gap: No PostHog event fires when a report status changes from pending to completed or on other report status transitions. No spectora_reports_synced event was found for either inspection. Autocapture records opening the Actions menu, but not all menu item selections. Likely paths where the status change originates without instrumentation: Mobile app flow used by Kris Smith, likely through the sync-spectora-reports or related reports pipeline. Web Actions dropdown flow, where the selected menu item is not always captured before navigation or API execution. Recommended fixes: Instrument a report_status_changed event whenever a report status transitions. Capture explicit menu item selections from the report Actions dropdown, especially Complete. Add a server-side PostHog event when Spectora sync successfully updates report status. Suggested event shape: posthog.capture('report_status_changed', { inspection_id: '1008599255', report_type: 'sewer_line_evaluation', from_status: 'pending', to_status: 'completed', changed_by_user_id: ' ', changed_by_email: 'kris@residentialinspector.com', surface: 'mobile' | 'web', trigger: 'actions_menu' | 'api_sync' | 'bulk_update', }) Notes from the investigation: Person-on-events mode is enabled in PostHog. Kris Smith has no display name set in PostHog; only kris@residentialinspector.com is populated. PostHog autocapture does not consistently record dropdown menu selections. Relevant PostHog insights referenced during investigation: sM5k v7DX 7V8T baiv QzLm o1I6
Linear 1 day ago
Main App
Planned
Report status changes lack an audit trail
Catherine reported that Kris Smith said he did not publish reports, but Attik shows the Sewer Line Evaluation report completed on two orders. Affected work orders: 1008599255 1008599424 Relevant inspections: Inspection 1008599255 Inspection 1008599424 PostHog investigation found that both Sewer Line Evaluation reports moved from pending to completed, and the rendered activity feed attributes the change to Kris Smith. However, there is no instrumented event that captures the actual status change action, so it is not possible to determine with confidence who triggered it, from which surface, or at what exact time from PostHog data alone. What was confirmed: The status change did occur on inspection 1008599255; the activity feed shows Kris Smith's report status changed to completed. Kris Smith was active on both inspections via the iOS app around the relevant time window. Lou Qualtiere clicked the report-level Actions button on inspection 1008599424, but PostHog did not capture which dropdown option was selected. Other users interacted with the inspections on web, but no event definitively ties a user action to the status transition. Current gap: No PostHog event fires when a report status changes from pending to completed or on other report status transitions. No spectora_reports_synced event was found for either inspection. Autocapture records opening the Actions menu, but not all menu item selections. Likely paths where the status change originates without instrumentation: Mobile app flow used by Kris Smith, likely through the sync-spectora-reports or related reports pipeline. Web Actions dropdown flow, where the selected menu item is not always captured before navigation or API execution. Recommended fixes: Instrument a report_status_changed event whenever a report status transitions. Capture explicit menu item selections from the report Actions dropdown, especially Complete. Add a server-side PostHog event when Spectora sync successfully updates report status. Suggested event shape: posthog.capture('report_status_changed', { inspection_id: '1008599255', report_type: 'sewer_line_evaluation', from_status: 'pending', to_status: 'completed', changed_by_user_id: ' ', changed_by_email: 'kris@residentialinspector.com', surface: 'mobile' | 'web', trigger: 'actions_menu' | 'api_sync' | 'bulk_update', }) Notes from the investigation: Person-on-events mode is enabled in PostHog. Kris Smith has no display name set in PostHog; only kris@residentialinspector.com is populated. PostHog autocapture does not consistently record dropdown menu selections. Relevant PostHog insights referenced during investigation: sM5k v7DX 7V8T baiv QzLm o1I6
Linear 1 day ago
Main App
Planned
Replacing inspector profile photos fails
Objective Staff with Settings → Inspectors access must be able to replace an inspector's profile photo and see the new image immediately on the inspector settings page. The updated photo must persist after refresh and appear anywhere the product reads the inspector's thumb / image fields (e.g. inspector list, scheduling, emails). Fix the reported failure for Residential Inspector (Catherine Lemoine) where replacing photos (e.g. Eli's) did not work across multiple inspectors and image files. Background Product feedback from Catherine Lemoine (catherine@residentialinspector.com): unable to replace Eli's profile picture; same behavior on multiple inspectors and photos, so this does not look limited to one profile or file. Repro attempt in progress internally using the same inspector image files. Short video shows the behavior. Code triage indicates the upload path exists but the UI/state flow is fragile: FilePond upload completes before the inspector PATCH finishes, local state only updates thumb (not image), and a useEffect can reset state from stale server data after cache revalidation. Triage verdict: Bug — intended behavior is that photo replacement works; current implementation can leave the old photo visible even when upload or save partially succeeds. Edge case to rule out during repro: Spectora bulk sync (POST /inspector/sync-inspectors) and manual Spectora re-sync (ResyncSpectoraModal) can overwrite image / thumb from Spectora, reverting manual uploads. Scope Frontend (attik-frontend) Inspector settings page: src/app/tools/settings/inspectors/[id]/InspectorForm.tsx — ProfilePicUploader with handleImageUpload PATCHes inspector/{id} with { image: file.url, thumb: file.preview }; on success shows toast and setData({ ...data, thumb: file.preview }) only (no router.refresh(), image not updated locally). Shared uploader: src/components/ui/ProfilePicUploader.tsx — FilePond uploads via callServerAction → POST files with path: profile-pic; calls handleUpload(resp) without awaiting before closing the editor. Server action / cache: src/util/functions/fetching/callServerAction.ts — revalidate uses path /settings/inspectors (other settings forms use /tools/settings/...). Same uploader pattern on office staff: src/app/tools/settings/office/[id]/OfficeForm.tsx — likely same class of bug if fix is shared. Spectora manual re-sync (separate from upload): src/app/tools/settings/ResyncSpectoraModal.tsx and DataForm.tsx handleResync can overwrite profile image from Spectora. Backend (attik-backend) File upload: src/routes/files.ts — POST /files with multipart, path: profile-pic → S3 upload + thumbnail preview URL in response. Inspector update: src/routes/inspector.ts — PATCH /:id saves any defined schema fields including image and thumb. Spectora bulk sync: POST /sync-inspectors in same file always sets image and thumb from Spectora on findOneAndUpdate. Inspector schema: src/models/inspectorSchema.ts — image and thumb are string fields. Out of scope (unless repro proves otherwise) Mobile app inspector profile editing (report is web Settings → Inspectors). Contact profile photos (different form path in ContactForm.tsx). Inspector photos in email delivery / CDN loading (see completed ATT-1780 — separate concern). References Video showing profile photo issue Slack thread — Catherine Lemoine report Frontend entry: attik-frontend/src/app/tools/settings/inspectors/[id]/InspectorForm.tsx Backend entry: attik-backend/src/routes/files.ts, attik-backend/src/routes/inspector.ts Decisions needed Source of truth for Spectora-linked inspectors — When an inspector has a _specId, should a manually uploaded Attik profile photo override Spectora's image / thumb until explicitly re-synced, or should Spectora remain authoritative and Attik uploads be blocked/warned for linked inspectors? Spectora re-sync behavior — Should manual Spectora re-sync (ResyncSpectoraModal) and bulk sync (POST /inspector/sync-inspectors) skip image / thumb when Attik has a custom upload, or continue overwriting (current behavior)? Scope of fix — Should the same fix apply to office staff profile photos (OfficeForm.tsx uses the same ProfilePicUploader pattern), or is this ticket inspectors-only? Done criteria / surfaces — Is success defined as the photo updating on the inspector settings page only, or must it also update immediately on the inspector list, scheduling UI, and email previews without a separate cache delay? Repro confirmation — After internal repro with Catherine's image files: confirm whether failure is upload error, PATCH error, UI revert with successful save, or save lost on refresh — this determines whether the fix is frontend-only or also backend/sync-related. User messaging on failure — If upload or save fails, should the product show a clear error (and keep the editor open) rather than closing FilePond and showing the old photo with no explanation?
Linear 2 days ago
Main App
Planned
Replacing inspector profile photos fails
Objective Staff with Settings → Inspectors access must be able to replace an inspector's profile photo and see the new image immediately on the inspector settings page. The updated photo must persist after refresh and appear anywhere the product reads the inspector's thumb / image fields (e.g. inspector list, scheduling, emails). Fix the reported failure for Residential Inspector (Catherine Lemoine) where replacing photos (e.g. Eli's) did not work across multiple inspectors and image files. Background Product feedback from Catherine Lemoine (catherine@residentialinspector.com): unable to replace Eli's profile picture; same behavior on multiple inspectors and photos, so this does not look limited to one profile or file. Repro attempt in progress internally using the same inspector image files. Short video shows the behavior. Code triage indicates the upload path exists but the UI/state flow is fragile: FilePond upload completes before the inspector PATCH finishes, local state only updates thumb (not image), and a useEffect can reset state from stale server data after cache revalidation. Triage verdict: Bug — intended behavior is that photo replacement works; current implementation can leave the old photo visible even when upload or save partially succeeds. Edge case to rule out during repro: Spectora bulk sync (POST /inspector/sync-inspectors) and manual Spectora re-sync (ResyncSpectoraModal) can overwrite image / thumb from Spectora, reverting manual uploads. Scope Frontend (attik-frontend) Inspector settings page: src/app/tools/settings/inspectors/[id]/InspectorForm.tsx — ProfilePicUploader with handleImageUpload PATCHes inspector/{id} with { image: file.url, thumb: file.preview }; on success shows toast and setData({ ...data, thumb: file.preview }) only (no router.refresh(), image not updated locally). Shared uploader: src/components/ui/ProfilePicUploader.tsx — FilePond uploads via callServerAction → POST files with path: profile-pic; calls handleUpload(resp) without awaiting before closing the editor. Server action / cache: src/util/functions/fetching/callServerAction.ts — revalidate uses path /settings/inspectors (other settings forms use /tools/settings/...). Same uploader pattern on office staff: src/app/tools/settings/office/[id]/OfficeForm.tsx — likely same class of bug if fix is shared. Spectora manual re-sync (separate from upload): src/app/tools/settings/ResyncSpectoraModal.tsx and DataForm.tsx handleResync can overwrite profile image from Spectora. Backend (attik-backend) File upload: src/routes/files.ts — POST /files with multipart, path: profile-pic → S3 upload + thumbnail preview URL in response. Inspector update: src/routes/inspector.ts — PATCH /:id saves any defined schema fields including image and thumb. Spectora bulk sync: POST /sync-inspectors in same file always sets image and thumb from Spectora on findOneAndUpdate. Inspector schema: src/models/inspectorSchema.ts — image and thumb are string fields. Out of scope (unless repro proves otherwise) Mobile app inspector profile editing (report is web Settings → Inspectors). Contact profile photos (different form path in ContactForm.tsx). Inspector photos in email delivery / CDN loading (see completed ATT-1780 — separate concern). References Video showing profile photo issue Slack thread — Catherine Lemoine report Frontend entry: attik-frontend/src/app/tools/settings/inspectors/[id]/InspectorForm.tsx Backend entry: attik-backend/src/routes/files.ts, attik-backend/src/routes/inspector.ts Decisions needed Source of truth for Spectora-linked inspectors — When an inspector has a _specId, should a manually uploaded Attik profile photo override Spectora's image / thumb until explicitly re-synced, or should Spectora remain authoritative and Attik uploads be blocked/warned for linked inspectors? Spectora re-sync behavior — Should manual Spectora re-sync (ResyncSpectoraModal) and bulk sync (POST /inspector/sync-inspectors) skip image / thumb when Attik has a custom upload, or continue overwriting (current behavior)? Scope of fix — Should the same fix apply to office staff profile photos (OfficeForm.tsx uses the same ProfilePicUploader pattern), or is this ticket inspectors-only? Done criteria / surfaces — Is success defined as the photo updating on the inspector settings page only, or must it also update immediately on the inspector list, scheduling UI, and email previews without a separate cache delay? Repro confirmation — After internal repro with Catherine's image files: confirm whether failure is upload error, PATCH error, UI revert with successful save, or save lost on refresh — this determines whether the fix is frontend-only or also backend/sync-related. User messaging on failure — If upload or save fails, should the product show a clear error (and keep the editor open) rather than closing FilePond and showing the old photo with no explanation?
Linear 2 days ago
Main App
Planned
Rebuild AI knowledge base bot in all instances
Objective Give internal staff in every Attik instance an in-platform AI knowledge base assistant that answers questions from company guidance instead of scattered Google Docs and external references. Replace the early Scott-only prototype with a maintainable, multi-tenant solution that Southwest, Southeast, and other regions can adopt. Keep guidance authoritative and instance-specific — each company's handbook/helpdocs and AI sources feed answers for that company only. Background Southwest and Southeast asked for the AI knowledge base bot that exists in Scott's instance, so operational information lives in one place inside Attik. The current bot was built early, has not been maintained, and likely needs a ground-up rebuild using newer platform tools (per engineering review). Priority: real request, but lower than dispatch — track separately and sequence accordingly. Triage verdict: Feature gap — the bot is not broken in other instances; it was never rolled out beyond Scott. Today, Scott staff see an AI Assistant star button in the global navbar; other instances do not. The chat proxies to an external Scott-specific agent service; backend AIChunk vector search and helpdoc sync are already company-scoped and are the likely foundation for a rebuilt bot. Scope Frontend (attik-frontend) Global navbar AI entry point: src/components/navbar/GlobalSearch.tsx — AI Assistant button and AISearchInput are gated to Scott's _companyId (624c9327e90fc4025d8f586b) only. Chat UI: src/components/navbar/AISearchInput.tsx, src/components/navbar/AIModal.tsx — streams answers via POST /api/ai-chat. API proxy: src/app/api/ai-chat/route.ts — forwards to scott-chatbot-production.up.railway.app/api/scott-agent (Scott-named external service). Knowledge source admin: src/app/tools/ai/sources/page.tsx and related components — browse/manage AI knowledge sources per company (not Scott-gated). Handbook / helpdocs authoring: src/app/tools/handbook/[[...id]]/ — company content that can feed the knowledge base. Backend (attik-backend) Vector search: src/routes/aiChunk.ts — $vectorSearch on AIChunk filtered by _companyId and access. Helpdoc → chunk sync: src/util/functions/ai/syncHelpdocsToAIChunks.ts and src/events/bullmq/helpdocsWorker.ts — embeds handbook/helpdoc content per company. Chunk model: src/models/AIChunkSchema.ts — stores embeddings, source, collection (helpdocs, etc.), company id. Separate AI assist paths (report skin / client portal) use similar chunk infrastructure in src/routes/reportChunk.ts — related but different user-facing surfaces. Out of scope (unless explicitly expanded) Client portal AI (recommendations, Q&A for homebuyers/agents) — see ATT-755, ATT-1183. Report-skin Agent Assist chatbot — separate surface; see ATT-1087 and related AI Features work. Migrating all legacy Google Docs content into Attik (content migration may be a prerequisite project, not part of the bot shell alone). References Overlapping backlog item: ATT-231 — Contextful AI Chat Built Into New Searchbar (same outcome: replicate Scott chatbot to all instances). AI Features project issues: ATT-1181 (usage analytics), ATT-1087 (KB grounding for agent assist). Frontend entry: attik-frontend/src/components/navbar/GlobalSearch.tsx, attik-frontend/src/app/api/ai-chat/route.ts Backend entry: attik-backend/src/routes/aiChunk.ts, attik-backend/src/util/functions/ai/syncHelpdocsToAIChunks.ts Decisions needed Relationship to ATT-231 — Should ATT-1875 supersede ATT-231, or should they be linked as duplicate tracking for the same outcome? Rollout scope — Enable for all instances at once, or pilot with Southwest + Southeast first? Agent architecture — Replace the Scott Railway scott-agent service with a company-aware Attik agent (using AIChunk + vector search), or extend/wrap the existing external service per instance? Content source of truth — Which sources must v1 answer from: Handbook / Helpdocs only, AI Sources uploads, both, and/or migrated Google Docs? Content readiness — Is each requesting instance expected to populate handbook/helpdocs first, or does Attik provide seed/migration support before the bot goes live? Access control — All logged-in tools users, or a specific role/permission (e.g. office staff only)? Who can manage knowledge sources vs consume the bot? UI placement — Keep the current global navbar star button pattern, or relocate (e.g. handbook sidebar, dedicated AI page)? Analytics in v1 — Include basic usage logging (questions asked, adoption) per ATT-1181, or defer to a follow-up? Priority / sequencing — Confirm this stays below dispatch and set a target milestone or "no date" until dispatch work clears.
Linear 2 days ago
Main App
Planned
Rebuild AI knowledge base bot in all instances
Objective Give internal staff in every Attik instance an in-platform AI knowledge base assistant that answers questions from company guidance instead of scattered Google Docs and external references. Replace the early Scott-only prototype with a maintainable, multi-tenant solution that Southwest, Southeast, and other regions can adopt. Keep guidance authoritative and instance-specific — each company's handbook/helpdocs and AI sources feed answers for that company only. Background Southwest and Southeast asked for the AI knowledge base bot that exists in Scott's instance, so operational information lives in one place inside Attik. The current bot was built early, has not been maintained, and likely needs a ground-up rebuild using newer platform tools (per engineering review). Priority: real request, but lower than dispatch — track separately and sequence accordingly. Triage verdict: Feature gap — the bot is not broken in other instances; it was never rolled out beyond Scott. Today, Scott staff see an AI Assistant star button in the global navbar; other instances do not. The chat proxies to an external Scott-specific agent service; backend AIChunk vector search and helpdoc sync are already company-scoped and are the likely foundation for a rebuilt bot. Scope Frontend (attik-frontend) Global navbar AI entry point: src/components/navbar/GlobalSearch.tsx — AI Assistant button and AISearchInput are gated to Scott's _companyId (624c9327e90fc4025d8f586b) only. Chat UI: src/components/navbar/AISearchInput.tsx, src/components/navbar/AIModal.tsx — streams answers via POST /api/ai-chat. API proxy: src/app/api/ai-chat/route.ts — forwards to scott-chatbot-production.up.railway.app/api/scott-agent (Scott-named external service). Knowledge source admin: src/app/tools/ai/sources/page.tsx and related components — browse/manage AI knowledge sources per company (not Scott-gated). Handbook / helpdocs authoring: src/app/tools/handbook/[[...id]]/ — company content that can feed the knowledge base. Backend (attik-backend) Vector search: src/routes/aiChunk.ts — $vectorSearch on AIChunk filtered by _companyId and access. Helpdoc → chunk sync: src/util/functions/ai/syncHelpdocsToAIChunks.ts and src/events/bullmq/helpdocsWorker.ts — embeds handbook/helpdoc content per company. Chunk model: src/models/AIChunkSchema.ts — stores embeddings, source, collection (helpdocs, etc.), company id. Separate AI assist paths (report skin / client portal) use similar chunk infrastructure in src/routes/reportChunk.ts — related but different user-facing surfaces. Out of scope (unless explicitly expanded) Client portal AI (recommendations, Q&A for homebuyers/agents) — see ATT-755, ATT-1183. Report-skin Agent Assist chatbot — separate surface; see ATT-1087 and related AI Features work. Migrating all legacy Google Docs content into Attik (content migration may be a prerequisite project, not part of the bot shell alone). References Overlapping backlog item: ATT-231 — Contextful AI Chat Built Into New Searchbar (same outcome: replicate Scott chatbot to all instances). AI Features project issues: ATT-1181 (usage analytics), ATT-1087 (KB grounding for agent assist). Frontend entry: attik-frontend/src/components/navbar/GlobalSearch.tsx, attik-frontend/src/app/api/ai-chat/route.ts Backend entry: attik-backend/src/routes/aiChunk.ts, attik-backend/src/util/functions/ai/syncHelpdocsToAIChunks.ts Decisions needed Relationship to ATT-231 — Should ATT-1875 supersede ATT-231, or should they be linked as duplicate tracking for the same outcome? Rollout scope — Enable for all instances at once, or pilot with Southwest + Southeast first? Agent architecture — Replace the Scott Railway scott-agent service with a company-aware Attik agent (using AIChunk + vector search), or extend/wrap the existing external service per instance? Content source of truth — Which sources must v1 answer from: Handbook / Helpdocs only, AI Sources uploads, both, and/or migrated Google Docs? Content readiness — Is each requesting instance expected to populate handbook/helpdocs first, or does Attik provide seed/migration support before the bot goes live? Access control — All logged-in tools users, or a specific role/permission (e.g. office staff only)? Who can manage knowledge sources vs consume the bot? UI placement — Keep the current global navbar star button pattern, or relocate (e.g. handbook sidebar, dedicated AI page)? Analytics in v1 — Include basic usage logging (questions asked, adoption) per ATT-1181, or defer to a follow-up? Priority / sequencing — Confirm this stays below dispatch and set a target milestone or "no date" until dispatch work clears.
Linear 2 days ago
Main App
Planned
Online scheduler allows skipping required info via navigation
In the online scheduler, clients can bypass the additional info step that contains required fields by using Next or the calendar icon at the top of the page. This appears to be a separate repro path from ATT-1508. That issue covers submission continuing with missing required property details; this one is specifically about navigation allowing the required-info page to be skipped. Video reference
Linear 3 days ago
Main App
Planned
Online scheduler allows skipping required info via navigation
In the online scheduler, clients can bypass the additional info step that contains required fields by using Next or the calendar icon at the top of the page. This appears to be a separate repro path from ATT-1508. That issue covers submission continuing with missing required property details; this one is specifically about navigation allowing the required-info page to be skipped. Video reference
Linear 3 days ago
Main App
Planned
Track repair list adoption by inspector
Objective Measure repair-list adoption by inspector so tablet/repair-list pilot rollouts can be evaluated per person, not only at company or market level. Near-term (v1): Add multi-select inspector filtering to the Post-Inspection Revenue admin dashboard so pilot cohorts can be reviewed together. Longer-term: Surface which inspectors are succeeding with repair-list usage and which may need coaching or process support. Background The team is piloting tablets and repair-list workflows with a subset of inspectors; the main blocker is knowing whether the pilot is working. They want to connect inspector → inspection → repair-list creation so adoption can be analyzed by person. A near-term path discussed was reporting first, before a full dedicated dashboard — specifically a multi-select inspector filter on Post-Inspection Revenue so several participating inspectors can be compared during the pilot. Longer term: broader visibility into inspector-level success and coaching needs. Triage verdict: Feature gap — company/date-level adoption reporting exists; inspector-level grouping and filtering do not. The Post-Inspection Revenue dashboard (shipped via ATT-1670) reports ProPair funnel metrics (eligible inspections → repair lists → quotes) filtered by company + date only. Scope Frontend (attik-frontend) — Admin Post-Inspection Revenue page: src/app/admin/post-inspection-revenue/PostInspectionRevenueClient.tsx — loads propair-adoption and concierge-adoption with companyIds, startDate, endDate only. Shared filters: src/app/admin/_components/DashboardHeader.tsx — company + timeframe; no inspector filter today. ProPair section: src/app/admin/post-inspection-revenue/_components/PropairAdoptionSection.tsx — displays aggregated funnel/chart data. Backend (attik-backend) Adoption API: src/routes/adminPostInspectionRevenue.ts — GET /post-inspection-revenue/propair-adoption aggregates by date period ($dateToString on inspection datetime); joins repairlists on _inspectionId; filters propairEligible inspections; accepts companyIds, startDate, endDate, groupBy — no inspectorIds. Dashboard view: scripts/createCcDashboardViews.ts / cc_inspection_base — ProPair eligibility flags on inspections; _inspectorId not projected into the view today. Data join path exists: repairListSchema _inspectionId → inspectionSchema _inspectorId (array of assigned inspectors); repair lists also have optional createdBy (user id, not inspector id). Repair list creation: src/routes/repairList.ts POST / — PostHog event repair list created logs inspection_id and company_id, not inspector. Out of scope (unless explicitly expanded) Client portal repair-list UX changes (see ATT-1847, ATT-1367). Tools-level inspector-facing repair-list UI (src/app/tools/inspections/repair-lists/) — separate from admin adoption reporting unless product asks for in-app inspector views in v1. Full coaching dashboard or automated outreach — longer-term follow-up beyond v1 filter/reporting. General repair-list admin lifecycle improvements (sample filtering, downstream actions) — see ATT-1384; link as related, not duplicate. References Parent dashboard (done): ATT-1670 — Build post-inspection revenue dashboard in Attik Admin Related: ATT-1384 — Improve repair list admin visibility and tracking Backend entry: attik-backend/src/routes/adminPostInspectionRevenue.ts Frontend entry: attik-frontend/src/app/admin/post-inspection-revenue/PostInspectionRevenueClient.tsx Decisions needed v1 deliverable — Is v1 inspector multi-select filter on existing ProPair charts sufficient, or must v1 also include a per-inspector breakdown table (counts/rates by inspector name)? Inspector attribution rule — When an inspection has multiple assigned inspectors (_inspectorId array), credit adoption to primary inspector only, all assigned inspectors, or inspector who created the repair list (createdBy user → employee mapping)? Adoption metric definition — What counts as “adopted” for the pilot: repair list created, ≥1 repair item added, quote requested/submitted, or a combination? Pilot cohort — Which brands/companies and inspector list define the initial pilot (Southwest/Southeast subset)? Should the filter default to that cohort or require manual selection each time? Dashboard scope — Admin Post-Inspection Revenue only, or also expose inspector-level adoption in tools (market/ops users without admin access)? Concierge section — Should inspector filtering apply to Concierge adoption on the same page, or ProPair/repair-list only in v1? Relationship to ATT-1384 — Should inspector adoption live under ATT-1869 while ATT-1384 covers broader admin repair-list visibility, or merge scope? Data model change — Add _inspectorId to cc_inspection_base view vs join at query time in the adoption aggregation?
Linear 3 days ago
Main App
Planned
Track repair list adoption by inspector
Objective Measure repair-list adoption by inspector so tablet/repair-list pilot rollouts can be evaluated per person, not only at company or market level. Near-term (v1): Add multi-select inspector filtering to the Post-Inspection Revenue admin dashboard so pilot cohorts can be reviewed together. Longer-term: Surface which inspectors are succeeding with repair-list usage and which may need coaching or process support. Background The team is piloting tablets and repair-list workflows with a subset of inspectors; the main blocker is knowing whether the pilot is working. They want to connect inspector → inspection → repair-list creation so adoption can be analyzed by person. A near-term path discussed was reporting first, before a full dedicated dashboard — specifically a multi-select inspector filter on Post-Inspection Revenue so several participating inspectors can be compared during the pilot. Longer term: broader visibility into inspector-level success and coaching needs. Triage verdict: Feature gap — company/date-level adoption reporting exists; inspector-level grouping and filtering do not. The Post-Inspection Revenue dashboard (shipped via ATT-1670) reports ProPair funnel metrics (eligible inspections → repair lists → quotes) filtered by company + date only. Scope Frontend (attik-frontend) — Admin Post-Inspection Revenue page: src/app/admin/post-inspection-revenue/PostInspectionRevenueClient.tsx — loads propair-adoption and concierge-adoption with companyIds, startDate, endDate only. Shared filters: src/app/admin/_components/DashboardHeader.tsx — company + timeframe; no inspector filter today. ProPair section: src/app/admin/post-inspection-revenue/_components/PropairAdoptionSection.tsx — displays aggregated funnel/chart data. Backend (attik-backend) Adoption API: src/routes/adminPostInspectionRevenue.ts — GET /post-inspection-revenue/propair-adoption aggregates by date period ($dateToString on inspection datetime); joins repairlists on _inspectionId; filters propairEligible inspections; accepts companyIds, startDate, endDate, groupBy — no inspectorIds. Dashboard view: scripts/createCcDashboardViews.ts / cc_inspection_base — ProPair eligibility flags on inspections; _inspectorId not projected into the view today. Data join path exists: repairListSchema _inspectionId → inspectionSchema _inspectorId (array of assigned inspectors); repair lists also have optional createdBy (user id, not inspector id). Repair list creation: src/routes/repairList.ts POST / — PostHog event repair list created logs inspection_id and company_id, not inspector. Out of scope (unless explicitly expanded) Client portal repair-list UX changes (see ATT-1847, ATT-1367). Tools-level inspector-facing repair-list UI (src/app/tools/inspections/repair-lists/) — separate from admin adoption reporting unless product asks for in-app inspector views in v1. Full coaching dashboard or automated outreach — longer-term follow-up beyond v1 filter/reporting. General repair-list admin lifecycle improvements (sample filtering, downstream actions) — see ATT-1384; link as related, not duplicate. References Parent dashboard (done): ATT-1670 — Build post-inspection revenue dashboard in Attik Admin Related: ATT-1384 — Improve repair list admin visibility and tracking Backend entry: attik-backend/src/routes/adminPostInspectionRevenue.ts Frontend entry: attik-frontend/src/app/admin/post-inspection-revenue/PostInspectionRevenueClient.tsx Decisions needed v1 deliverable — Is v1 inspector multi-select filter on existing ProPair charts sufficient, or must v1 also include a per-inspector breakdown table (counts/rates by inspector name)? Inspector attribution rule — When an inspection has multiple assigned inspectors (_inspectorId array), credit adoption to primary inspector only, all assigned inspectors, or inspector who created the repair list (createdBy user → employee mapping)? Adoption metric definition — What counts as “adopted” for the pilot: repair list created, ≥1 repair item added, quote requested/submitted, or a combination? Pilot cohort — Which brands/companies and inspector list define the initial pilot (Southwest/Southeast subset)? Should the filter default to that cohort or require manual selection each time? Dashboard scope — Admin Post-Inspection Revenue only, or also expose inspector-level adoption in tools (market/ops users without admin access)? Concierge section — Should inspector filtering apply to Concierge adoption on the same page, or ProPair/repair-list only in v1? Relationship to ATT-1384 — Should inspector adoption live under ATT-1869 while ATT-1384 covers broader admin repair-list visibility, or merge scope? Data model change — Add _inspectorId to cc_inspection_base view vs join at query time in the adoption aggregation?
Linear 3 days ago
Main App
Planned
Redirect scheduler failures to a branded outage page
Objective When the online scheduler cannot load or render, send customers to a branded recovery page instead of a generic technical error. Recovery page should explain the situation and give clear next steps: call the office, email, and optionally leave contact information for follow-up. Prefer redirect to a dedicated route over inline error copy on the main scheduler page. Background After a service interruption, customers can land on an unhelpful error state while internal teams already know the scheduler is down. Team discussion favored detect failure → redirect rather than only improving message text on the existing error boundary. Today (code audit): /scheduler/error.tsx — Next.js route error boundary: generic “We Had Trouble Loading Information” + Refresh only. No company logo, phone, email, or redirect. global-error.tsx — root fallback renders the default Next.js error page (PostHog capture only). page.tsx — missing/invalid token shows inline Invalid Access / Invalid Token (red text, not a recovery flow). EscapeHatchPage — branded fallback with company phone/email, but only for business rules (out-of-area, service-blocked, size-limit via checkEscapeHatch). Not used for infrastructure/render failures. schedulingLog.ts — captureSchedulerFailure logs failures but does not drive customer-facing recovery UI. No dedicated outage / unavailable route exists today. Scope Frontend (attik-frontend) — primary Add a branded recovery/outage page under the scheduler route (e.g. /scheduler/unavailable or equivalent) that can render with minimal company context when full scheduler bootstrap fails. Wire failure detection so customers are redirected (or server-rendered) to that page instead of generic errors: error.tsx — replace or supplement Refresh-only UX with redirect to recovery page (preserve error.digest for support if useful). page.tsx — handle bootstrap failures (e.g. company, service, or other required fetches in the initial Promise.all) with recovery redirect rather than uncaught throw → generic boundary. Consider whether global-error.tsx needs scheduler-specific handling when the failure escapes the route boundary. Reuse patterns from EscapeHatchPage (company name, phone, email, styled CTA buttons) where branding is available — do not conflate with business-rule escape hatches unless product confirms shared component. Optional v1: contact capture form on recovery page (name, phone, email, message) — only if product confirms (see Decisions needed). Out of scope (v1 unless product expands) Proactive maintenance mode toggle for ops (manual “scheduler off” banner) — separate from reactive failure redirect unless explicitly combined. BuildVersionGuard stale-build reload behavior — different failure class. Business-rule EscapeHatchPage flows — already shipped; do not regress. Mobile app scheduler (if any) — web /scheduler only unless stated otherwise. Done when Simulated scheduler bootstrap failure (e.g. company API unavailable) shows a branded recovery page with company contact options, not the generic Refresh-only screen. Client/render failure on the scheduler route redirects or renders recovery page with actionable next steps. Recovery page works when token is valid but downstream scheduler data cannot load. Failures are observable (existing logging / PostHog patterns preserved or extended). Decisions needed Failure classes for v1 — Which scenarios must redirect to recovery? SSR bootstrap failure (page.tsx data load)? Route error.tsx (client render / uncaught step error)? Transient step API errors mid-flow (e.g. slots fetch) — inline retry vs redirect? Recovery route — Dedicated path (e.g. /scheduler/unavailable?token=…) vs query param on existing /scheduler? Should invalid/missing token use the same page or stay separate? Branding when company/{id} fails — Show Attik-generic recovery, company name only from token metadata, or cached/minimal brand payload? What is acceptable if logo/phone cannot be fetched? Contact capture — v1 call/email CTAs only, or include a leave your info form that creates a lead/ticket? If form: where does submission go (email, CRM, internal queue)? Copy tone — Acknowledge outage explicitly (“temporarily unavailable”) vs softer “having trouble loading”? Any legal/compliance wording required? Global vs route error — Update global-error.tsx for scheduler host/path, or route-level recovery is sufficient? References Online scheduler entry: attik-frontend/src/app/scheduler/page.tsx Route error boundary: attik-frontend/src/app/scheduler/error.tsx Branded fallback reference (business rules): attik-frontend/src/app/scheduler/components/EscapeHatchPage.tsx, utils/checkEscapeHatch.ts Failure logging: attik-frontend/src/util/functions/observability/schedulingLog.ts (captureSchedulerFailure) Root error: attik-frontend/src/app/global-error.tsx Related pattern (stale client, different class): attik-frontend/src/components/BuildVersionGuard.tsx
Linear 3 days ago
Main App
Planned
Redirect scheduler failures to a branded outage page
Objective When the online scheduler cannot load or render, send customers to a branded recovery page instead of a generic technical error. Recovery page should explain the situation and give clear next steps: call the office, email, and optionally leave contact information for follow-up. Prefer redirect to a dedicated route over inline error copy on the main scheduler page. Background After a service interruption, customers can land on an unhelpful error state while internal teams already know the scheduler is down. Team discussion favored detect failure → redirect rather than only improving message text on the existing error boundary. Today (code audit): /scheduler/error.tsx — Next.js route error boundary: generic “We Had Trouble Loading Information” + Refresh only. No company logo, phone, email, or redirect. global-error.tsx — root fallback renders the default Next.js error page (PostHog capture only). page.tsx — missing/invalid token shows inline Invalid Access / Invalid Token (red text, not a recovery flow). EscapeHatchPage — branded fallback with company phone/email, but only for business rules (out-of-area, service-blocked, size-limit via checkEscapeHatch). Not used for infrastructure/render failures. schedulingLog.ts — captureSchedulerFailure logs failures but does not drive customer-facing recovery UI. No dedicated outage / unavailable route exists today. Scope Frontend (attik-frontend) — primary Add a branded recovery/outage page under the scheduler route (e.g. /scheduler/unavailable or equivalent) that can render with minimal company context when full scheduler bootstrap fails. Wire failure detection so customers are redirected (or server-rendered) to that page instead of generic errors: error.tsx — replace or supplement Refresh-only UX with redirect to recovery page (preserve error.digest for support if useful). page.tsx — handle bootstrap failures (e.g. company, service, or other required fetches in the initial Promise.all) with recovery redirect rather than uncaught throw → generic boundary. Consider whether global-error.tsx needs scheduler-specific handling when the failure escapes the route boundary. Reuse patterns from EscapeHatchPage (company name, phone, email, styled CTA buttons) where branding is available — do not conflate with business-rule escape hatches unless product confirms shared component. Optional v1: contact capture form on recovery page (name, phone, email, message) — only if product confirms (see Decisions needed). Out of scope (v1 unless product expands) Proactive maintenance mode toggle for ops (manual “scheduler off” banner) — separate from reactive failure redirect unless explicitly combined. BuildVersionGuard stale-build reload behavior — different failure class. Business-rule EscapeHatchPage flows — already shipped; do not regress. Mobile app scheduler (if any) — web /scheduler only unless stated otherwise. Done when Simulated scheduler bootstrap failure (e.g. company API unavailable) shows a branded recovery page with company contact options, not the generic Refresh-only screen. Client/render failure on the scheduler route redirects or renders recovery page with actionable next steps. Recovery page works when token is valid but downstream scheduler data cannot load. Failures are observable (existing logging / PostHog patterns preserved or extended). Decisions needed Failure classes for v1 — Which scenarios must redirect to recovery? SSR bootstrap failure (page.tsx data load)? Route error.tsx (client render / uncaught step error)? Transient step API errors mid-flow (e.g. slots fetch) — inline retry vs redirect? Recovery route — Dedicated path (e.g. /scheduler/unavailable?token=…) vs query param on existing /scheduler? Should invalid/missing token use the same page or stay separate? Branding when company/{id} fails — Show Attik-generic recovery, company name only from token metadata, or cached/minimal brand payload? What is acceptable if logo/phone cannot be fetched? Contact capture — v1 call/email CTAs only, or include a leave your info form that creates a lead/ticket? If form: where does submission go (email, CRM, internal queue)? Copy tone — Acknowledge outage explicitly (“temporarily unavailable”) vs softer “having trouble loading”? Any legal/compliance wording required? Global vs route error — Update global-error.tsx for scheduler host/path, or route-level recovery is sufficient? References Online scheduler entry: attik-frontend/src/app/scheduler/page.tsx Route error boundary: attik-frontend/src/app/scheduler/error.tsx Branded fallback reference (business rules): attik-frontend/src/app/scheduler/components/EscapeHatchPage.tsx, utils/checkEscapeHatch.ts Failure logging: attik-frontend/src/util/functions/observability/schedulingLog.ts (captureSchedulerFailure) Root error: attik-frontend/src/app/global-error.tsx Related pattern (stale client, different class): attik-frontend/src/components/BuildVersionGuard.tsx
Linear 3 days ago
Main App
Planned
Hide deprecated taxable toggle in payroll adjustments
Objective Remove or hide the Taxable toggle from payroll adjustment request/submit flows so inspectors and staff are not asked to set a value that does not control payroll export behavior. Reduce confusion during mobile and web adjustment entry: tax treatment is determined by the UKG pay code mapped to the adjustment category in payroll settings, not by the visible toggle. Keep category selection (and UKG mapping configuration) as the operational source of truth. Background During the mobile payroll-adjustment demo, the team discussed whether Taxable should be preset by category. Chris clarified the toggle is effectively deprecated — UKG pay code drives tax treatment, not the UI control. The current control is misleading for inspectors and payroll admins; the product direction is to hide/remove the toggle rather than ask users to manage a setting that no longer matters. Today (code audit / product triage): Mobile — payroll-adjustment.tsx shows a Taxable Switch (default true) and sends taxable on POST payroll/adjustment. Web — AddAdjustmentModal.tsx shows Payment Taxable for the Adjustment form; a useEffect auto-sets taxable from category name (e.g. category containing "reimbursement" → false, else true), but the switch remains visible and editable. UKG export — buildUKGEarnings.ts maps adjustment totals to UKG using settings.ukgPayrollMapping[category] only; the taxable field is not used in the export payload. Still uses taxable today (in-app only): stored on the adjustment document (payrollSchema.ts), Taxable Yes/No column in PayrollAdjustmentList.tsx, and taxableAdjustmentTotal in payroll overview metrics (overviewRowMetrics.ts). Related shipped work: adjustment categories → UKG pay-type mapping (ATT-1078); mobile job-linked adjustments (ATT-1124). Scope Mobile (attik-mobile) app/(app)/inspection/[id]/payroll-adjustment.tsx — remove/hide the Taxable switch from the submit UI. hooks/usePayrollAdjustmentSubmit.ts — continue sending taxable if the API requires it, using a server-side or derived default (see Decisions needed) rather than user input. If non-job-linked mobile adjustment entry exists or ships separately (ATT-1825), apply the same rule there. Frontend web (attik-frontend) AddAdjustmentModal.tsx — hide/remove the Payment Taxable switch from adjustment create/edit UI; preserve or replace category-based preset logic if taxable remains on the model. Confirm no other request/submit surfaces expose the toggle (adjustment-management admin views may still display historical values — product decision below). Backend (attik-backend) — likely minimal models/payrollSchema.ts — taxable remains on the Adjustment schema unless product retires the field entirely. routes/payrollData.ts — no UKG export change expected; export path already category-driven via frontend buildUKGEarnings.ts. Out of scope (v1 unless product expands) Changing UKG pay code mapping UI or category configuration. Removing taxable from the database schema / migrations (unless product locks retirement in Decisions needed). Miles / Hours / Sick forms (no Taxable toggle on those form types today). Done when Inspector mobile job-linked adjustment submit flow has no Taxable toggle. Web Add Adjustment flow has no Payment Taxable toggle. Submitted adjustments still save successfully; UKG export behavior unchanged (category → pay code). No user-facing copy implies the toggle controls UKG tax treatment. Decisions needed Surfaces — Hide toggle on request/submit flows only, or also remove Taxable Yes/No column / Non-taxable badges in payroll review and adjustment-management lists? Stored taxable field — Keep writing a derived value (e.g. category name contains "reimbursement" → false, else true, matching current web logic) for in-app totals, or stop using taxable entirely in v1? taxableAdjustmentTotal — Retain overview metric based on derived/stored taxable, or deprecate that total when the toggle is hidden? Admin edit flows — When payroll admins edit an existing adjustment, can they still change taxable, or is it hidden everywhere including edit? API default — If the client omits taxable, should backend default true, derive from category server-side, or require explicit value? Communication — Any in-app help text pointing admins to Payroll Settings → UKG mapping by category as the place tax treatment is configured? References Mobile submit UI: attik-mobile/app/(app)/inspection/[id]/payroll-adjustment.tsx Mobile API hook: attik-mobile/hooks/usePayrollAdjustmentSubmit.ts Web modal: attik-frontend/src/app/tools/hr/AddAdjustmentModal.tsx UKG earnings build: attik-frontend/src/app/tools/hr/payroll/utils/buildUKGEarnings.ts In-app taxable display: PayrollAdjustmentList.tsx, overviewRowMetrics.ts, adjustment-management/AdjustmentLineItem.tsx Backend model: attik-backend/src/models/payrollSchema.ts Related: ATT-1078, ATT-1124, ATT-1825
Linear 3 days ago
Main App
Planned
Hide deprecated taxable toggle in payroll adjustments
Objective Remove or hide the Taxable toggle from payroll adjustment request/submit flows so inspectors and staff are not asked to set a value that does not control payroll export behavior. Reduce confusion during mobile and web adjustment entry: tax treatment is determined by the UKG pay code mapped to the adjustment category in payroll settings, not by the visible toggle. Keep category selection (and UKG mapping configuration) as the operational source of truth. Background During the mobile payroll-adjustment demo, the team discussed whether Taxable should be preset by category. Chris clarified the toggle is effectively deprecated — UKG pay code drives tax treatment, not the UI control. The current control is misleading for inspectors and payroll admins; the product direction is to hide/remove the toggle rather than ask users to manage a setting that no longer matters. Today (code audit / product triage): Mobile — payroll-adjustment.tsx shows a Taxable Switch (default true) and sends taxable on POST payroll/adjustment. Web — AddAdjustmentModal.tsx shows Payment Taxable for the Adjustment form; a useEffect auto-sets taxable from category name (e.g. category containing "reimbursement" → false, else true), but the switch remains visible and editable. UKG export — buildUKGEarnings.ts maps adjustment totals to UKG using settings.ukgPayrollMapping[category] only; the taxable field is not used in the export payload. Still uses taxable today (in-app only): stored on the adjustment document (payrollSchema.ts), Taxable Yes/No column in PayrollAdjustmentList.tsx, and taxableAdjustmentTotal in payroll overview metrics (overviewRowMetrics.ts). Related shipped work: adjustment categories → UKG pay-type mapping (ATT-1078); mobile job-linked adjustments (ATT-1124). Scope Mobile (attik-mobile) app/(app)/inspection/[id]/payroll-adjustment.tsx — remove/hide the Taxable switch from the submit UI. hooks/usePayrollAdjustmentSubmit.ts — continue sending taxable if the API requires it, using a server-side or derived default (see Decisions needed) rather than user input. If non-job-linked mobile adjustment entry exists or ships separately (ATT-1825), apply the same rule there. Frontend web (attik-frontend) AddAdjustmentModal.tsx — hide/remove the Payment Taxable switch from adjustment create/edit UI; preserve or replace category-based preset logic if taxable remains on the model. Confirm no other request/submit surfaces expose the toggle (adjustment-management admin views may still display historical values — product decision below). Backend (attik-backend) — likely minimal models/payrollSchema.ts — taxable remains on the Adjustment schema unless product retires the field entirely. routes/payrollData.ts — no UKG export change expected; export path already category-driven via frontend buildUKGEarnings.ts. Out of scope (v1 unless product expands) Changing UKG pay code mapping UI or category configuration. Removing taxable from the database schema / migrations (unless product locks retirement in Decisions needed). Miles / Hours / Sick forms (no Taxable toggle on those form types today). Done when Inspector mobile job-linked adjustment submit flow has no Taxable toggle. Web Add Adjustment flow has no Payment Taxable toggle. Submitted adjustments still save successfully; UKG export behavior unchanged (category → pay code). No user-facing copy implies the toggle controls UKG tax treatment. Decisions needed Surfaces — Hide toggle on request/submit flows only, or also remove Taxable Yes/No column / Non-taxable badges in payroll review and adjustment-management lists? Stored taxable field — Keep writing a derived value (e.g. category name contains "reimbursement" → false, else true, matching current web logic) for in-app totals, or stop using taxable entirely in v1? taxableAdjustmentTotal — Retain overview metric based on derived/stored taxable, or deprecate that total when the toggle is hidden? Admin edit flows — When payroll admins edit an existing adjustment, can they still change taxable, or is it hidden everywhere including edit? API default — If the client omits taxable, should backend default true, derive from category server-side, or require explicit value? Communication — Any in-app help text pointing admins to Payroll Settings → UKG mapping by category as the place tax treatment is configured? References Mobile submit UI: attik-mobile/app/(app)/inspection/[id]/payroll-adjustment.tsx Mobile API hook: attik-mobile/hooks/usePayrollAdjustmentSubmit.ts Web modal: attik-frontend/src/app/tools/hr/AddAdjustmentModal.tsx UKG earnings build: attik-frontend/src/app/tools/hr/payroll/utils/buildUKGEarnings.ts In-app taxable display: PayrollAdjustmentList.tsx, overviewRowMetrics.ts, adjustment-management/AdjustmentLineItem.tsx Backend model: attik-backend/src/models/payrollSchema.ts Related: ATT-1078, ATT-1124, ATT-1825
Linear 3 days ago
Main App
Planned
Support inspector signatures on agreements
Objective Support inspector signatures on inspection agreements for markets (e.g. Washington, Oregon) where both the client and the inspector must sign before the inspection proceeds. Eliminate manual workarounds while preserving the correct inspector identity on the signed agreement record. Keep the existing client signing flow intact; add a complementary inspector signing capability. Background During an implementation call, the team discussed Washington law requiring both client and inspector signatures on the inspection agreement; Oregon was mentioned as a similar market. The open product question is how and when the inspector signature should be applied. Ideas discussed included: Storing an inspector signature on the inspector profile Stamping the agreement when the report is published or when the job starts Allowing the assigned inspector to sign directly on the job before the inspection begins Edge cases called out for clear handling: Inspector reassignment before the inspection Multiple inspectors on a job Someone other than the inspector publishing the report Signature timing must still satisfy pre-inspection compliance requirements Current behavior (code audit): Attik agreements today support client/contact signing only. The client portal flow in AgreementSigningSection.tsx / AgreementSigningForm.tsx collects one signature and submits via agreements.ts PATCH with status: 'signed'. Signing requires a portal-access token with contactId and a contact role granting agreement permission. The agreementSchema stores a single signer (signedBy, signedByEmail, signatureData, signedRenderedHtml, etc.) — no inspector signature fields. Agreement templates support text and initials blocks (AgreementBuilderBase.tsx, initialsBlock in merged content); there is no inspector signature block type. Report unlock treats agreements as signed when the client has signed all active non-stale agreements (computeAgreementSignedForReportLock.ts, agreementLockSync.ts, checkReportAccess.ts). Inspector signatures on Spectora reports (InspectorSignatureSection.tsx) are a separate path from report/Spectora data and do not satisfy agreement signing requirements. The Attik inspectorSchema has no stored signature field for agreements today. Decisions needed (product) Lock before or during implementation: When inspector signature is captured — profile stamp vs job-level sign before inspection vs tied to report publish/job start (only options that meet pre-inspection compliance are valid for WA/OR). Where the inspector signs — Tools inspection/workorder UI, mobile app, or automated stamp from profile. Which inspector when multiple are assigned — primary only, all assigned, or configurable per company. Reassignment behavior — invalidate/require re-sign, or keep original inspector signature. Report lock — whether inspector signature is required for agreement.signed / report access, or tracked separately from client sign. Company/market enablement — global feature vs per-company or per-state setting. Scope Backend Extend agreement signing data model beyond single client signature — e.g. inspector identity, timestamp, signature image/data, and rendered output — in or alongside agreementSchema.ts. New or extended routes in agreements.ts (or inspector-specific signing endpoint) with auth appropriate for inspector/staff (not client portal contactId flow). Agreement merge/render pipeline (mergeAgreementBlocksToContent.js, renderAgreementDocToHtml.ts, signedRenderedHtml capture) must include inspector signature in the immutable signed record when applicable. Decision needed: whether inspector signature invalidates or triggers stale agreement logic (agreementStaleWorker.ts, signedContentHash / computeSignedContentHash). Decision needed: updates to agreementLockSync.ts / checkReportAccess.ts if inspector sign is required for compliance before reports unlock. Inspector profile storage (if profile-based approach chosen) would touch inspectorSchema.ts and inspector settings routes — not present today. Frontend — Client portal Agreement view/sign pages under src/app/client/job/[slug]/agreement/[id]/ — today client-only. May need read-only display of inspector signature once applied, or dual-signer UX if inspector signs via portal (unlikely; decision needed). AgreementContent.tsx / template builder may need a new block type or merge-field for inspector signature placement in PDF/HTML output. Frontend — Tools Inspector signing entry point on inspection/workorder (src/app/tools/inspections/[id]/) or agreements UI — not built today. Agreement template builder (src/app/tools/agreements/templates/[id]/, AgreementGlobalBlockModal.tsx) — extend block types beyond text and initials if inspector signature appears in template body. Settings if profile-stored signature: inspector settings under src/app/tools/settings/inspectors/. Mobile Decision needed: whether inspectors must sign from attik-mobile in v1 or Tools web only. Explicitly out of scope unless product expands Using report publish inspector signatures (InspectorSignatureSection.tsx) as a substitute for agreement compliance without explicit product approval Replacing client signing flow or contact-role agreement permission model Acceptance themes (from edge cases) Correct inspector identity on the final agreement artifact after reassignment scenarios are resolved per product decision. Multi-inspector jobs handled consistently (not silently wrong signer). Pre-inspection timing: agreement with inspector signature satisfies compliance before inspection start, independent of who publishes the report. References Client agreement signing: attik-frontend/src/app/client/job/[slug]/agreement/[id]/components/AgreementSigningSection.tsx, AgreementSigningForm.tsx Agreement API & sign gate: attik-backend/src/routes/agreements.ts Agreement model: attik-backend/src/models/agreementSchema.ts Template blocks: attik-frontend/src/app/tools/agreements/templates/[id]/AgreementBuilderBase.tsx Report lock / agreement signed: attik-backend/src/util/functions/agreements/agreementLockSync.ts, attik-frontend/src/util/functions/report/computeAgreementSignedForReportLock.ts Report inspector signature (separate system): attik-frontend/src/app/client/reports/components/InspectorSignatureSection.tsx
Linear 3 days ago
Main App
Planned
Support inspector signatures on agreements
Objective Support inspector signatures on inspection agreements for markets (e.g. Washington, Oregon) where both the client and the inspector must sign before the inspection proceeds. Eliminate manual workarounds while preserving the correct inspector identity on the signed agreement record. Keep the existing client signing flow intact; add a complementary inspector signing capability. Background During an implementation call, the team discussed Washington law requiring both client and inspector signatures on the inspection agreement; Oregon was mentioned as a similar market. The open product question is how and when the inspector signature should be applied. Ideas discussed included: Storing an inspector signature on the inspector profile Stamping the agreement when the report is published or when the job starts Allowing the assigned inspector to sign directly on the job before the inspection begins Edge cases called out for clear handling: Inspector reassignment before the inspection Multiple inspectors on a job Someone other than the inspector publishing the report Signature timing must still satisfy pre-inspection compliance requirements Current behavior (code audit): Attik agreements today support client/contact signing only. The client portal flow in AgreementSigningSection.tsx / AgreementSigningForm.tsx collects one signature and submits via agreements.ts PATCH with status: 'signed'. Signing requires a portal-access token with contactId and a contact role granting agreement permission. The agreementSchema stores a single signer (signedBy, signedByEmail, signatureData, signedRenderedHtml, etc.) — no inspector signature fields. Agreement templates support text and initials blocks (AgreementBuilderBase.tsx, initialsBlock in merged content); there is no inspector signature block type. Report unlock treats agreements as signed when the client has signed all active non-stale agreements (computeAgreementSignedForReportLock.ts, agreementLockSync.ts, checkReportAccess.ts). Inspector signatures on Spectora reports (InspectorSignatureSection.tsx) are a separate path from report/Spectora data and do not satisfy agreement signing requirements. The Attik inspectorSchema has no stored signature field for agreements today. Decisions needed (product) Lock before or during implementation: When inspector signature is captured — profile stamp vs job-level sign before inspection vs tied to report publish/job start (only options that meet pre-inspection compliance are valid for WA/OR). Where the inspector signs — Tools inspection/workorder UI, mobile app, or automated stamp from profile. Which inspector when multiple are assigned — primary only, all assigned, or configurable per company. Reassignment behavior — invalidate/require re-sign, or keep original inspector signature. Report lock — whether inspector signature is required for agreement.signed / report access, or tracked separately from client sign. Company/market enablement — global feature vs per-company or per-state setting. Scope Backend Extend agreement signing data model beyond single client signature — e.g. inspector identity, timestamp, signature image/data, and rendered output — in or alongside agreementSchema.ts. New or extended routes in agreements.ts (or inspector-specific signing endpoint) with auth appropriate for inspector/staff (not client portal contactId flow). Agreement merge/render pipeline (mergeAgreementBlocksToContent.js, renderAgreementDocToHtml.ts, signedRenderedHtml capture) must include inspector signature in the immutable signed record when applicable. Decision needed: whether inspector signature invalidates or triggers stale agreement logic (agreementStaleWorker.ts, signedContentHash / computeSignedContentHash). Decision needed: updates to agreementLockSync.ts / checkReportAccess.ts if inspector sign is required for compliance before reports unlock. Inspector profile storage (if profile-based approach chosen) would touch inspectorSchema.ts and inspector settings routes — not present today. Frontend — Client portal Agreement view/sign pages under src/app/client/job/[slug]/agreement/[id]/ — today client-only. May need read-only display of inspector signature once applied, or dual-signer UX if inspector signs via portal (unlikely; decision needed). AgreementContent.tsx / template builder may need a new block type or merge-field for inspector signature placement in PDF/HTML output. Frontend — Tools Inspector signing entry point on inspection/workorder (src/app/tools/inspections/[id]/) or agreements UI — not built today. Agreement template builder (src/app/tools/agreements/templates/[id]/, AgreementGlobalBlockModal.tsx) — extend block types beyond text and initials if inspector signature appears in template body. Settings if profile-stored signature: inspector settings under src/app/tools/settings/inspectors/. Mobile Decision needed: whether inspectors must sign from attik-mobile in v1 or Tools web only. Explicitly out of scope unless product expands Using report publish inspector signatures (InspectorSignatureSection.tsx) as a substitute for agreement compliance without explicit product approval Replacing client signing flow or contact-role agreement permission model Acceptance themes (from edge cases) Correct inspector identity on the final agreement artifact after reassignment scenarios are resolved per product decision. Multi-inspector jobs handled consistently (not silently wrong signer). Pre-inspection timing: agreement with inspector signature satisfies compliance before inspection start, independent of who publishes the report. References Client agreement signing: attik-frontend/src/app/client/job/[slug]/agreement/[id]/components/AgreementSigningSection.tsx, AgreementSigningForm.tsx Agreement API & sign gate: attik-backend/src/routes/agreements.ts Agreement model: attik-backend/src/models/agreementSchema.ts Template blocks: attik-frontend/src/app/tools/agreements/templates/[id]/AgreementBuilderBase.tsx Report lock / agreement signed: attik-backend/src/util/functions/agreements/agreementLockSync.ts, attik-frontend/src/util/functions/report/computeAgreementSignedForReportLock.ts Report inspector signature (separate system): attik-frontend/src/app/client/reports/components/InspectorSignatureSection.tsx
Linear 3 days ago
Main App
Planned
Default CC Dash and Post inspection revenue dash filter
Default the CC Dash and Post inspection revenue dashboard to one company selected instead of all to help improve page load time.
Linear 3 days ago
Main App
Planned
Default CC Dash and Post inspection revenue dash filter
Default the CC Dash and Post inspection revenue dashboard to one company selected instead of all to help improve page load time.
Linear 3 days ago
Main App
Planned
Split inspection date and time into separate Reports Hub columns
Objective Export Inspection Date and Inspection Time as separate columns in Reports Hub on Inspections and Quotes, so finance can filter, sort, and pivot by date and time of day without parsing a combined datetime string in Excel. Provide date-only and time-only values formatted for spreadsheet normalization (consistent, predictable strings per row) — not the current combined YYYY-MM-DD HH:mm export. Keep the existing combined datetime column unchanged for backward compatibility; new columns are additive. Background Finance feedback (Andy/Erin session, parent epic Reports Hub update for Finance) calls for scheduling columns separate from revenue fields. Erin: when users pick date fields in exports, values download as a timestamp-style datetime (YYYY-MM-DD HH:mm) and must be cleaned up manually before sorting or manipulating in spreadsheets. Andy noted CSV as a workaround; the underlying need is a plain date field (and separate time). Today Inspections expose a single datetime field labeled Inspection Date in exportFieldDefinitions.ts; formatting.ts renders all date types as YYYY-MM-DD HH:mm when company timezone is set (runDataExportJob.ts → getExportTimeZone). There is no date-only or time-only export today. Quotes expose datetime as Requested Date (mapped to createdAt in exportFieldDefinitions.ts) — split date/time applies here too in v1. Template reference (not a separate deliverable): Email/SMS/agreement templates split scheduling via inspectionDate and inspectionTime in templatingData.ts (company timezone). Reports Hub should expose the same semantic split (date column + time column from one instant). Export formats are locked below and happen to match those template formats. Prebuilt Inspection Summary only includes datetime among scheduling fields (prebuiltReports.ts). Consolidated duplicate: ATT-1860 — same deliverable; tracked here. Out of scope: county, client name, address (ATT-1858); End Time split columns (no user ask). Locked product decisions (v1) Entities: Inspections and Quotes (quote split applies to the scheduling datetime field exposed today — Requested Date / createdAt mapping). Export formats: Date → M/D/YYYY; Time → h:mm A (12-hour with AM/PM), in company timezone. Primary goal: Normalization-friendly for spreadsheets — stable string output suitable for downstream imports and pivot workflows (not locale-pretty-only rendering that varies row to row). Backward compatibility: Existing combined datetime column stays as-is (YYYY-MM-DD HH:mm); new date-only and time-only columns are added alongside it. End Time: Out of scope — no split columns for endtime in v1. Template parity (clarified): Semantic — separate date and time from the same scheduling instant, like template variables. Format — locked to M/D/YYYY and h:mm A (same as inspectionDate / inspectionTime in templatingData.ts). Done when Inspection Date (date-only) and Inspection Time (time-only) appear in the Reports Hub column picker for Inspections and Quotes. Exported values use M/D/YYYY and h:mm A in company timezone; date column never includes time; time column never includes date. Combined datetime column behavior is unchanged on existing saved reports. Users can add the new columns to saved reports, preview them, and export CSV/XLSX. Scope Backend attik-backend/src/config/exportFieldDefinitions.ts — register date-only and time-only fields on inspections and quotes (computed from datetime; quotes continue to source from createdAt per existing datetime mapping unless dev chooses clearer field keys). attik-backend/src/util/functions/dataExports/formatting.ts — format new fields as M/D/YYYY / h:mm A via TZDayjs / company timezone; do not reuse formatDateForExport (YYYY-MM-DD HH:mm) for these columns. attik-backend/src/util/functions/dataExports/documentResolution.ts and/or runDataExportJob.ts — resolve split values when columns selected; timezone passthrough same as current date exports. Frontend Reports Hub column picker and preview (CreateReportForm.tsx, ReportPreviewSection.tsx) — surface new fields when backend exposes them via GET /data-exports/fields/inspections and.../quotes. Out of scope End Time (endtime) date/time split. Changing or removing the existing combined datetime column. County, client name, address (ATT-1858). Finance prebuilt template updates (optional follow-up after ATT-1856). References Parent epic: Reporting updates for Finance (ATT-1862) Duplicate consolidated: ATT-1860 — Add date-only field to inspections collection exports Field defs: attik-backend/src/config/exportFieldDefinitions.ts (datetime, endtime on inspections; quotes datetime → createdAt) Date formatting: attik-backend/src/util/functions/dataExports/formatting.ts Template variable reference: attik-frontend/src/components/conditions/templatingData.ts (inspectionDate, inspectionTime)
Linear 3 days ago
Main App
Planned
Split inspection date and time into separate Reports Hub columns
Objective Export Inspection Date and Inspection Time as separate columns in Reports Hub on Inspections and Quotes, so finance can filter, sort, and pivot by date and time of day without parsing a combined datetime string in Excel. Provide date-only and time-only values formatted for spreadsheet normalization (consistent, predictable strings per row) — not the current combined YYYY-MM-DD HH:mm export. Keep the existing combined datetime column unchanged for backward compatibility; new columns are additive. Background Finance feedback (Andy/Erin session, parent epic Reports Hub update for Finance) calls for scheduling columns separate from revenue fields. Erin: when users pick date fields in exports, values download as a timestamp-style datetime (YYYY-MM-DD HH:mm) and must be cleaned up manually before sorting or manipulating in spreadsheets. Andy noted CSV as a workaround; the underlying need is a plain date field (and separate time). Today Inspections expose a single datetime field labeled Inspection Date in exportFieldDefinitions.ts; formatting.ts renders all date types as YYYY-MM-DD HH:mm when company timezone is set (runDataExportJob.ts → getExportTimeZone). There is no date-only or time-only export today. Quotes expose datetime as Requested Date (mapped to createdAt in exportFieldDefinitions.ts) — split date/time applies here too in v1. Template reference (not a separate deliverable): Email/SMS/agreement templates split scheduling via inspectionDate and inspectionTime in templatingData.ts (company timezone). Reports Hub should expose the same semantic split (date column + time column from one instant). Export formats are locked below and happen to match those template formats. Prebuilt Inspection Summary only includes datetime among scheduling fields (prebuiltReports.ts). Consolidated duplicate: ATT-1860 — same deliverable; tracked here. Out of scope: county, client name, address (ATT-1858); End Time split columns (no user ask). Locked product decisions (v1) Entities: Inspections and Quotes (quote split applies to the scheduling datetime field exposed today — Requested Date / createdAt mapping). Export formats: Date → M/D/YYYY; Time → h:mm A (12-hour with AM/PM), in company timezone. Primary goal: Normalization-friendly for spreadsheets — stable string output suitable for downstream imports and pivot workflows (not locale-pretty-only rendering that varies row to row). Backward compatibility: Existing combined datetime column stays as-is (YYYY-MM-DD HH:mm); new date-only and time-only columns are added alongside it. End Time: Out of scope — no split columns for endtime in v1. Template parity (clarified): Semantic — separate date and time from the same scheduling instant, like template variables. Format — locked to M/D/YYYY and h:mm A (same as inspectionDate / inspectionTime in templatingData.ts). Done when Inspection Date (date-only) and Inspection Time (time-only) appear in the Reports Hub column picker for Inspections and Quotes. Exported values use M/D/YYYY and h:mm A in company timezone; date column never includes time; time column never includes date. Combined datetime column behavior is unchanged on existing saved reports. Users can add the new columns to saved reports, preview them, and export CSV/XLSX. Scope Backend attik-backend/src/config/exportFieldDefinitions.ts — register date-only and time-only fields on inspections and quotes (computed from datetime; quotes continue to source from createdAt per existing datetime mapping unless dev chooses clearer field keys). attik-backend/src/util/functions/dataExports/formatting.ts — format new fields as M/D/YYYY / h:mm A via TZDayjs / company timezone; do not reuse formatDateForExport (YYYY-MM-DD HH:mm) for these columns. attik-backend/src/util/functions/dataExports/documentResolution.ts and/or runDataExportJob.ts — resolve split values when columns selected; timezone passthrough same as current date exports. Frontend Reports Hub column picker and preview (CreateReportForm.tsx, ReportPreviewSection.tsx) — surface new fields when backend exposes them via GET /data-exports/fields/inspections and.../quotes. Out of scope End Time (endtime) date/time split. Changing or removing the existing combined datetime column. County, client name, address (ATT-1858). Finance prebuilt template updates (optional follow-up after ATT-1856). References Parent epic: Reporting updates for Finance (ATT-1862) Duplicate consolidated: ATT-1860 — Add date-only field to inspections collection exports Field defs: attik-backend/src/config/exportFieldDefinitions.ts (datetime, endtime on inspections; quotes datetime → createdAt) Date formatting: attik-backend/src/util/functions/dataExports/formatting.ts Template variable reference: attik-frontend/src/components/conditions/templatingData.ts (inspectionDate, inspectionTime)
Linear 3 days ago
Main App
Planned
Add property county to inspection exports (Reports Hub)
Objective Add property county as persisted data on inspections (inspection.property.county) so it is available for Reports Hub exports, report conditions/filters, and other product surfaces that use property fields. Enable users to include County in custom inspection reports (column picker, preview, and CSV/XLSX export) alongside existing address fields. Background Finance and ops need inspection rows identifiable by county alongside address and client context in flat exports. County is the net-new gap. Inspection property in attik-backend/src/models/propertySchema.ts stores street, unit, city, state, zip, and related fields but does not persist county. exportFieldDefinitions.ts has no county column on inspections. County already appears in transient sources but is not saved today: Scheduling: Zillow/Bridge parcel lookup returns county and displays it in ZillowPropertyDetails.tsx, but create payloads in NewSchedulingForm.tsx / createInspection.ts do not pass it through. Spectora: Webhook property types include county, but spectoraPropertyToServerProperty.ts does not map it — Spectora sync is out of scope for v1 write paths (see locked decisions). Already available today (not in scope unless product reopens): Full address: address on inspections (legacy field synced from property via generateAddress() in propertySchema.ts / inspectionSchema.ts pre-save). Address components: property.street, property.city, property.state, property.zip in the Reports Hub column picker. Client name: via linked People on an inspections report (firstName, lastName, roleName, filter people:roleName). No dedicated job-level Client Name column without linking People. Feature request: Featurebase — County Child of Reports Hub update for Finance (ATT-1862). Locked product decisions (v1) Persist county on inspection.property.county — not export-time-only resolution. Write paths in v1: persist county when available on: Scheduling (new jobs and scheduler-driven address updates) Manual property edits on the job Backfill for existing inspections missing county (see below) Not in v1 write paths: Spectora sync — do not map or persist county from Spectora webhooks/sync in this issue. Backfill: populate county on existing inspections where possible (e.g. Bridge/Zillow lookup via fetchBridgeParcel.ts using lat/lng or address). If the effort is large, assignee discusses scope with Ryan/Chris before expanding. Reports Hub: exportable County column on Inspections (picker, preview, CSV/XLSX). Out of scope for v1: job-level clientName computed column; changes to existing full-address / street–zip fields; Spectora county sync. Done when property.county exists on the schema and is saved on new/updated jobs via scheduling and manual edit paths when county data is present. Backfill executed or explicitly deferred with Ryan/Chris if not feasible in v1. County appears in the Reports Hub column picker for Inspections, works in preview and export, and persists on saved reports. County is available as persisted property data for conditions/filters following the same patterns as other property.* fields. Scope Backend attik-backend/src/models/propertySchema.ts — add county to PropertyType / schema. Scheduling create/update: attik-backend/src/util/functions/inspection/createInspection.ts — accept and persist county from address payload. Manual property edit: attik-backend/src/routes/inspection.ts — include property.county in PATCH handling alongside existing property fields. Backfill: batch or script approach using existing Bridge/Zillow utilities (fetchBridgeParcel.ts, frontend callZillowAndMatch.ts patterns) — scope TBD with Ryan/Chris if heavy. Explicitly out of scope: spectoraPropertyToServerProperty.ts and Spectora sync paths — no county mapping in v1. attik-backend/src/config/exportFieldDefinitions.ts — register property.county on inspectionFields with label County. attik-backend/src/util/functions/dataExports/documentResolution.ts — resolve county like other property.* fields. Frontend Scheduling: pass county from Zillow/Bridge into create/update payloads when present (PropertySection.tsx, NewSchedulingForm.tsx). Manual job edit: surface county on property save if in scope for edit UI (PropertySectionWrapper.tsx). Reports Hub: column picker and preview (CreateReportForm.tsx, ReportPreviewSection.tsx) — surface County when backend field defs expose it. Out of scope Spectora sync county persistence. Job-level client name computed field (People link covers today). Changes to Full Address / existing property.street–zip fields. Wide service columns (ATT-1856), split date/time (ATT-1863). References Featurebase — County Reports Hub update for Finance (ATT-1862) Property schema: attik-backend/src/models/propertySchema.ts Export field defs: attik-backend/src/config/exportFieldDefinitions.ts Scheduling create: attik-backend/src/util/functions/inspection/createInspection.ts Manual property PATCH: attik-backend/src/routes/inspection.ts Zillow/Bridge county source: attik-frontend/src/util/functions/fetching/callZillowAndMatch.ts, attik-backend/src/util/functions/zillow/fetchBridgeParcel.ts Spectora (out of scope v1): attik-backend/src/util/functions/spectora/spectoraPropertyToServerProperty.ts
Linear 3 days ago
Main App
Planned
Add property county to inspection exports (Reports Hub)
Objective Add property county as persisted data on inspections (inspection.property.county) so it is available for Reports Hub exports, report conditions/filters, and other product surfaces that use property fields. Enable users to include County in custom inspection reports (column picker, preview, and CSV/XLSX export) alongside existing address fields. Background Finance and ops need inspection rows identifiable by county alongside address and client context in flat exports. County is the net-new gap. Inspection property in attik-backend/src/models/propertySchema.ts stores street, unit, city, state, zip, and related fields but does not persist county. exportFieldDefinitions.ts has no county column on inspections. County already appears in transient sources but is not saved today: Scheduling: Zillow/Bridge parcel lookup returns county and displays it in ZillowPropertyDetails.tsx, but create payloads in NewSchedulingForm.tsx / createInspection.ts do not pass it through. Spectora: Webhook property types include county, but spectoraPropertyToServerProperty.ts does not map it — Spectora sync is out of scope for v1 write paths (see locked decisions). Already available today (not in scope unless product reopens): Full address: address on inspections (legacy field synced from property via generateAddress() in propertySchema.ts / inspectionSchema.ts pre-save). Address components: property.street, property.city, property.state, property.zip in the Reports Hub column picker. Client name: via linked People on an inspections report (firstName, lastName, roleName, filter people:roleName). No dedicated job-level Client Name column without linking People. Feature request: Featurebase — County Child of Reports Hub update for Finance (ATT-1862). Locked product decisions (v1) Persist county on inspection.property.county — not export-time-only resolution. Write paths in v1: persist county when available on: Scheduling (new jobs and scheduler-driven address updates) Manual property edits on the job Backfill for existing inspections missing county (see below) Not in v1 write paths: Spectora sync — do not map or persist county from Spectora webhooks/sync in this issue. Backfill: populate county on existing inspections where possible (e.g. Bridge/Zillow lookup via fetchBridgeParcel.ts using lat/lng or address). If the effort is large, assignee discusses scope with Ryan/Chris before expanding. Reports Hub: exportable County column on Inspections (picker, preview, CSV/XLSX). Out of scope for v1: job-level clientName computed column; changes to existing full-address / street–zip fields; Spectora county sync. Done when property.county exists on the schema and is saved on new/updated jobs via scheduling and manual edit paths when county data is present. Backfill executed or explicitly deferred with Ryan/Chris if not feasible in v1. County appears in the Reports Hub column picker for Inspections, works in preview and export, and persists on saved reports. County is available as persisted property data for conditions/filters following the same patterns as other property.* fields. Scope Backend attik-backend/src/models/propertySchema.ts — add county to PropertyType / schema. Scheduling create/update: attik-backend/src/util/functions/inspection/createInspection.ts — accept and persist county from address payload. Manual property edit: attik-backend/src/routes/inspection.ts — include property.county in PATCH handling alongside existing property fields. Backfill: batch or script approach using existing Bridge/Zillow utilities (fetchBridgeParcel.ts, frontend callZillowAndMatch.ts patterns) — scope TBD with Ryan/Chris if heavy. Explicitly out of scope: spectoraPropertyToServerProperty.ts and Spectora sync paths — no county mapping in v1. attik-backend/src/config/exportFieldDefinitions.ts — register property.county on inspectionFields with label County. attik-backend/src/util/functions/dataExports/documentResolution.ts — resolve county like other property.* fields. Frontend Scheduling: pass county from Zillow/Bridge into create/update payloads when present (PropertySection.tsx, NewSchedulingForm.tsx). Manual job edit: surface county on property save if in scope for edit UI (PropertySectionWrapper.tsx). Reports Hub: column picker and preview (CreateReportForm.tsx, ReportPreviewSection.tsx) — surface County when backend field defs expose it. Out of scope Spectora sync county persistence. Job-level client name computed field (People link covers today). Changes to Full Address / existing property.street–zip fields. Wide service columns (ATT-1856), split date/time (ATT-1863). References Featurebase — County Reports Hub update for Finance (ATT-1862) Property schema: attik-backend/src/models/propertySchema.ts Export field defs: attik-backend/src/config/exportFieldDefinitions.ts Scheduling create: attik-backend/src/util/functions/inspection/createInspection.ts Manual property PATCH: attik-backend/src/routes/inspection.ts Zillow/Bridge county source: attik-frontend/src/util/functions/fetching/callZillowAndMatch.ts, attik-backend/src/util/functions/zillow/fetchBridgeParcel.ts Spectora (out of scope v1): attik-backend/src/util/functions/spectora/spectoraPropertyToServerProperty.ts
Linear 3 days ago
Main App
Planned
Wide service fee columns in Reports Hub (All Service Fee Breakdown)
Objective Ship wide service fee columns in Reports Hub so finance can see one column per active catalog service on each inspection row — charge amount or $0 — without pivoting charges in Excel. Users opt in via a single “All Service Fee Breakdown” control when charges are linked on an Inspections-base report, then save and re-run that report like any other custom export. Background Andy and Erin need job-level revenue visibility: which services were charged on each job and for how much. Today, Line items (charges base + linked inspections) exports one row per charge — not one row per job with service-named columns. Wide service columns do not exist today. Reports Hub has aggregation pivot / addOnServiceName groupBy for upsell-by-ancillary reports, but not dynamic flat-export columns keyed by catalog service. Related fields already available (not part of this ticket): finance can add baseFee, addOnTotal, address, datetime, etc. from the existing inspections column picker (exportFieldDefinitions.ts, documentResolution.ts). Value aggregations on baseFee / addOnTotal already work via the standard aggregation UI when configured. Epic context: Child of Reports Hub update for Finance (ATT-1862). Identifying columns from siblings remain optional add-ons in the builder. No prebuilt report for v1. Users assemble the finance view manually and save it. Locked product decisions (v1) Wide service fee columns Column set: All active services in the company catalog, ordered like the Services List settings page — business segments sorted by order, then services within each segment sorted by order (ServiceSettingsList.tsx; order on serviceSchema.ts and business segments). Exclude inactive/deactivated services (active: false). Scope: Include all active catalog services in that order (primary and add-on). No separate add-on-only subset. Stability: Column headers do not change when filters or date range change. They only change when services are added, removed, reordered, renamed, or activated/deactivated on the Services List page. Multiple charges for same service on one job: Sum all charge amounts for that service into the single column for that service. Duplicate display names: Services with the same name remain distinct columns (keyed by service id internally). Column order follows Services List so category context disambiguates. Optional enhancement (not required for v1): append category abbreviation in brackets when names collide, e.g. Termite (CI). Max columns: No maximum in preview or export — full active-catalog width is acceptable. Charge amount source Use charge amount summed per _serviceId on the job. Wide columns reflect what was charged; they do not recalculate base vs add-on logic (originalBasePrice ?? amount on primary remains on existing baseFee field). Builder UX Opt-in via a single “All Service Fee Breakdown” option in the charges collection (linked from inspections). Selecting it adds the full set of wide service fee columns for the company’s active catalog — users do not pick individual service columns one by one. Done when On an Inspections-base report with charges linked, a user can enable All Service Fee Breakdown and: See one column per active catalog service on each job row, Services List order, with $0 where the job has no charge for that service. See wide columns in preview (horizontal scroll; no column cap). Export matches preview for wide columns. Save and re-run the report with wide columns persisted in the report config. Scope Backend Wide service fee columns (new behavior): extend flat export in joinedReport.ts and/or runDataExportJob.ts to emit dynamic columns keyed by catalog service id (header = service display name), ordered per company Services List. Load column set from active company services only — not from distinct service names in the filtered result set. Sum charge amount per _serviceId when multiple charges match the same service on one job; resolve display name via _serviceId / catalogName (chargeFields.catalogName in exportFieldDefinitions.ts). Expose All Service Fee Breakdown in export/report config so saved reports persist the opt-in (exact config shape: decision left to implementer). Frontend Report builder — All Service Fee Breakdown opt-in under linked charges on inspections-base reports (CreateReportForm.tsx, column-picker components). ReportPreviewSection.tsx — render dynamic wide columns; horizontal scroll; no column cap. Export run path — wide columns included when config has breakdown enabled. Services List order reference: attik-frontend/src/app/tools/settings/services/ServiceSettingsList.tsx. Out of scope Job-level add-on names computed field (delimited list) — wide columns supersede for finance; open a follow-up only if a compact text column is still wanted. baseFee / addOnTotal / addOnCount — already exportable; not modified by this ticket. Value aggregations on baseFee / addOnTotal or per-wide-column totals — existing aggregation UI; not part of this ticket. New prebuilt in prebuiltReports.ts Global category columns: ATT-1857 Convenience / PAC fees: ATT-1859 Client name, county, full address: ATT-1858 Business segments / linking: ATT-1861 Split date/time columns: ATT-1863 References Parent epic: Reports Hub update for Finance Charge rollup (existing base/add-on fields): attik-backend/src/util/functions/dataExports/documentResolution.ts Field defs: attik-backend/src/config/exportFieldDefinitions.ts Report builder: attik-frontend/src/app/tools/data-exports/CreateReportForm.tsx Services List ordering: attik-frontend/src/app/tools/settings/services/ServiceSettingsList.tsx Diagram flowchart LR subgraph catalog [Active company catalog] BL[Services List order\nsegment.order → service.order] S1[Service A] S2[Service B] S3[Service C] BL --> S1 BL --> S2 BL --> S3 end subgraph job [Inspection job charges] AC1[Charge Service A] AC2[Charge Service A duplicate] AC3[Charge Service B] end subgraph export [Wide export row same job] WA[Service A column\nsum amounts] WB[Service B column] WC[Service C column\n$0 if none] end AC1 --> WA AC2 --> WA AC3 --> WB S1 -. column header .-> WA S2 -. column header .-> WB S3 -. column header .-> WC
Linear 3 days ago
Main App
Planned
Wide service fee columns in Reports Hub (All Service Fee Breakdown)
Objective Ship wide service fee columns in Reports Hub so finance can see one column per active catalog service on each inspection row — charge amount or $0 — without pivoting charges in Excel. Users opt in via a single “All Service Fee Breakdown” control when charges are linked on an Inspections-base report, then save and re-run that report like any other custom export. Background Andy and Erin need job-level revenue visibility: which services were charged on each job and for how much. Today, Line items (charges base + linked inspections) exports one row per charge — not one row per job with service-named columns. Wide service columns do not exist today. Reports Hub has aggregation pivot / addOnServiceName groupBy for upsell-by-ancillary reports, but not dynamic flat-export columns keyed by catalog service. Related fields already available (not part of this ticket): finance can add baseFee, addOnTotal, address, datetime, etc. from the existing inspections column picker (exportFieldDefinitions.ts, documentResolution.ts). Value aggregations on baseFee / addOnTotal already work via the standard aggregation UI when configured. Epic context: Child of Reports Hub update for Finance (ATT-1862). Identifying columns from siblings remain optional add-ons in the builder. No prebuilt report for v1. Users assemble the finance view manually and save it. Locked product decisions (v1) Wide service fee columns Column set: All active services in the company catalog, ordered like the Services List settings page — business segments sorted by order, then services within each segment sorted by order (ServiceSettingsList.tsx; order on serviceSchema.ts and business segments). Exclude inactive/deactivated services (active: false). Scope: Include all active catalog services in that order (primary and add-on). No separate add-on-only subset. Stability: Column headers do not change when filters or date range change. They only change when services are added, removed, reordered, renamed, or activated/deactivated on the Services List page. Multiple charges for same service on one job: Sum all charge amounts for that service into the single column for that service. Duplicate display names: Services with the same name remain distinct columns (keyed by service id internally). Column order follows Services List so category context disambiguates. Optional enhancement (not required for v1): append category abbreviation in brackets when names collide, e.g. Termite (CI). Max columns: No maximum in preview or export — full active-catalog width is acceptable. Charge amount source Use charge amount summed per _serviceId on the job. Wide columns reflect what was charged; they do not recalculate base vs add-on logic (originalBasePrice ?? amount on primary remains on existing baseFee field). Builder UX Opt-in via a single “All Service Fee Breakdown” option in the charges collection (linked from inspections). Selecting it adds the full set of wide service fee columns for the company’s active catalog — users do not pick individual service columns one by one. Done when On an Inspections-base report with charges linked, a user can enable All Service Fee Breakdown and: See one column per active catalog service on each job row, Services List order, with $0 where the job has no charge for that service. See wide columns in preview (horizontal scroll; no column cap). Export matches preview for wide columns. Save and re-run the report with wide columns persisted in the report config. Scope Backend Wide service fee columns (new behavior): extend flat export in joinedReport.ts and/or runDataExportJob.ts to emit dynamic columns keyed by catalog service id (header = service display name), ordered per company Services List. Load column set from active company services only — not from distinct service names in the filtered result set. Sum charge amount per _serviceId when multiple charges match the same service on one job; resolve display name via _serviceId / catalogName (chargeFields.catalogName in exportFieldDefinitions.ts). Expose All Service Fee Breakdown in export/report config so saved reports persist the opt-in (exact config shape: decision left to implementer). Frontend Report builder — All Service Fee Breakdown opt-in under linked charges on inspections-base reports (CreateReportForm.tsx, column-picker components). ReportPreviewSection.tsx — render dynamic wide columns; horizontal scroll; no column cap. Export run path — wide columns included when config has breakdown enabled. Services List order reference: attik-frontend/src/app/tools/settings/services/ServiceSettingsList.tsx. Out of scope Job-level add-on names computed field (delimited list) — wide columns supersede for finance; open a follow-up only if a compact text column is still wanted. baseFee / addOnTotal / addOnCount — already exportable; not modified by this ticket. Value aggregations on baseFee / addOnTotal or per-wide-column totals — existing aggregation UI; not part of this ticket. New prebuilt in prebuiltReports.ts Global category columns: ATT-1857 Convenience / PAC fees: ATT-1859 Client name, county, full address: ATT-1858 Business segments / linking: ATT-1861 Split date/time columns: ATT-1863 References Parent epic: Reports Hub update for Finance Charge rollup (existing base/add-on fields): attik-backend/src/util/functions/dataExports/documentResolution.ts Field defs: attik-backend/src/config/exportFieldDefinitions.ts Report builder: attik-frontend/src/app/tools/data-exports/CreateReportForm.tsx Services List ordering: attik-frontend/src/app/tools/settings/services/ServiceSettingsList.tsx Diagram flowchart LR subgraph catalog [Active company catalog] BL[Services List order\nsegment.order → service.order] S1[Service A] S2[Service B] S3[Service C] BL --> S1 BL --> S2 BL --> S3 end subgraph job [Inspection job charges] AC1[Charge Service A] AC2[Charge Service A duplicate] AC3[Charge Service B] end subgraph export [Wide export row same job] WA[Service A column\nsum amounts] WB[Service B column] WC[Service C column\n$0 if none] end AC1 --> WA AC2 --> WA AC3 --> WB S1 -. column header .-> WA S2 -. column header .-> WB S3 -. column header .-> WC
Linear 3 days ago
Main App
Planned
Workorder job duration shows unbounded decimal places for partial-hour service combos
Objective Show scheduled job duration on the workorder in a consistent, human-readable form so teams are not confused by long decimal strings (for example 1.8333333333333333h) when services combine to partial hours. Align display across workorder surfaces that surface duration today, without changing scheduling behavior unless product decides stored values should also be normalized. Reduce support friction when modifier-driven time adjustments stack across multiple services on a single job. Background Product feedback and internal triage confirm the main workorder header can show more than one decimal place (and sometimes floating-point noise) for certain service combinations that yield partial hours. inspection.duration is an optional Number on the inspection document (attik-backend/src/models/inspectionSchema.ts); the backend PATCH path in attik-backend/src/routes/inspection.ts persists whatever number the client sends with no rounding on write. On the workorder shell, attik-frontend/src/app/tools/inspections/[id]/components/InspectionDateChip.tsx renders {inspection.duration}h directly in JSX (lines 30–34) with no display formatting — whatever is stored is stringified as-is. Partial-hour totals are expected when services stack: per-service duration in attik-frontend/src/util/functions/schedulingHelpers/calculateServicePrices.ts sums base hours (duration / durationAsPrimary) plus modifier time from attik-frontend/src/util/functions/data/modifierPriceAdjFn.ts, where time modifiers convert accumulated minutes to hours via / 60 without rounding the per-service result. Rounding is inconsistent across the codebase today: attik-frontend/src/util/functions/inspection/buildDurationEndtimePatch.ts rounds to two decimals when persisting duration + endtime from workorder/reschedule flows. computeTotalJobDurationHours in attik-frontend/src/util/functions/schedulingHelpers/recalculateChargesForRequiredInfo.ts uses round2 (two decimals) when recalculating from required-info changes. Initial job/quote creation paths (for example attik-frontend/src/components/scheduling/NewSchedulingForm.tsx saving priceData.totalDuration) and usePriceCalculation totals do not consistently round before save. Other workorder UI surfaces also differ: attik-frontend/src/app/tools/inspections/[id]/components/WorkorderPriceChangeConfirmModal.tsx shows duration with.toFixed(2), while attik-frontend/src/app/tools/inspections/[id]/components/EditChargesModal.tsx footer shows raw {totalDuration} Hour(s). Reschedule duration inputs use 0.5-hour steps (RescheduleJobModal.tsx, RescheduleManualPanel.tsx) but still display/store whatever number results from calculation. Related narrower issue: ATT-1654 (chip-only display, one-decimal target). This ticket captures the broader rounding/display inconsistency and partial-hour root cause from modifier stacking. Scope Frontend Primary display: attik-frontend/src/app/tools/inspections/[id]/components/InspectionDateChip.tsx — workorder header chip next to date/time; confirmed render path for user-reported behavior. Secondary display: attik-frontend/src/app/tools/inspections/[id]/components/EditChargesModal.tsx (footer total duration), WorkorderPriceChangeConfirmModal.tsx (already two-decimal preview — align or reuse formatter). Duration calculation / persistence touchpoints: calculateServicePrices.ts, modifierPriceAdjFn.ts, usePriceCalculation.ts, buildDurationEndtimePatch.ts, recalculateChargesForRequiredInfo.ts (computeTotalJobDurationHours), and scheduler save paths (NewSchedulingForm.tsx, attik-frontend/src/app/scheduler/hooks/useDraftSaveOrchestrator.ts, attik-frontend/src/app/scheduler/SchedulerContext.tsx). Reschedule flows: RescheduleJobModal.tsx derives totalDuration from charge service definitions and allows manual 0.5-hour edits — regression-check after any formatter or rounding change. Backend attik-backend/src/models/inspectionSchema.ts — duration field type only; no precision constraint. attik-backend/src/routes/inspection.ts — reschedule/PATCH assigns inspection.duration from request body without normalization. Decision needed: display-only rounding vs. canonical rounding on write (create + PATCH + Spectora sync paths) so stored values match what users see. Product / architecture decisions (for the implementer) Decision needed: Target precision — one decimal (e.g. 3.5h), two decimals (matches confirm modal / some save paths), or integers when whole (e.g. 3h vs 3.5h). Decision needed: Scope of fix — display formatter only vs. normalize at calculation and save so downstream scheduling, calendar blocks, and exports stay consistent. Decision needed: Whether to introduce a shared duration formatter under attik-frontend/src/util/... reused by chip, Edit Charges, and confirm modal. References Related: ATT-1654 — Duration shows too many decimal places Display site: attik-frontend/src/app/tools/inspections/[id]/components/InspectionDateChip.tsx Modifier time math: attik-frontend/src/util/functions/data/modifierPriceAdjFn.ts Stored field: attik-backend/src/models/inspectionSchema.ts (duration)
Linear 3 days ago
Main App
Planned
Workorder job duration shows unbounded decimal places for partial-hour service combos
Objective Show scheduled job duration on the workorder in a consistent, human-readable form so teams are not confused by long decimal strings (for example 1.8333333333333333h) when services combine to partial hours. Align display across workorder surfaces that surface duration today, without changing scheduling behavior unless product decides stored values should also be normalized. Reduce support friction when modifier-driven time adjustments stack across multiple services on a single job. Background Product feedback and internal triage confirm the main workorder header can show more than one decimal place (and sometimes floating-point noise) for certain service combinations that yield partial hours. inspection.duration is an optional Number on the inspection document (attik-backend/src/models/inspectionSchema.ts); the backend PATCH path in attik-backend/src/routes/inspection.ts persists whatever number the client sends with no rounding on write. On the workorder shell, attik-frontend/src/app/tools/inspections/[id]/components/InspectionDateChip.tsx renders {inspection.duration}h directly in JSX (lines 30–34) with no display formatting — whatever is stored is stringified as-is. Partial-hour totals are expected when services stack: per-service duration in attik-frontend/src/util/functions/schedulingHelpers/calculateServicePrices.ts sums base hours (duration / durationAsPrimary) plus modifier time from attik-frontend/src/util/functions/data/modifierPriceAdjFn.ts, where time modifiers convert accumulated minutes to hours via / 60 without rounding the per-service result. Rounding is inconsistent across the codebase today: attik-frontend/src/util/functions/inspection/buildDurationEndtimePatch.ts rounds to two decimals when persisting duration + endtime from workorder/reschedule flows. computeTotalJobDurationHours in attik-frontend/src/util/functions/schedulingHelpers/recalculateChargesForRequiredInfo.ts uses round2 (two decimals) when recalculating from required-info changes. Initial job/quote creation paths (for example attik-frontend/src/components/scheduling/NewSchedulingForm.tsx saving priceData.totalDuration) and usePriceCalculation totals do not consistently round before save. Other workorder UI surfaces also differ: attik-frontend/src/app/tools/inspections/[id]/components/WorkorderPriceChangeConfirmModal.tsx shows duration with.toFixed(2), while attik-frontend/src/app/tools/inspections/[id]/components/EditChargesModal.tsx footer shows raw {totalDuration} Hour(s). Reschedule duration inputs use 0.5-hour steps (RescheduleJobModal.tsx, RescheduleManualPanel.tsx) but still display/store whatever number results from calculation. Related narrower issue: ATT-1654 (chip-only display, one-decimal target). This ticket captures the broader rounding/display inconsistency and partial-hour root cause from modifier stacking. Scope Frontend Primary display: attik-frontend/src/app/tools/inspections/[id]/components/InspectionDateChip.tsx — workorder header chip next to date/time; confirmed render path for user-reported behavior. Secondary display: attik-frontend/src/app/tools/inspections/[id]/components/EditChargesModal.tsx (footer total duration), WorkorderPriceChangeConfirmModal.tsx (already two-decimal preview — align or reuse formatter). Duration calculation / persistence touchpoints: calculateServicePrices.ts, modifierPriceAdjFn.ts, usePriceCalculation.ts, buildDurationEndtimePatch.ts, recalculateChargesForRequiredInfo.ts (computeTotalJobDurationHours), and scheduler save paths (NewSchedulingForm.tsx, attik-frontend/src/app/scheduler/hooks/useDraftSaveOrchestrator.ts, attik-frontend/src/app/scheduler/SchedulerContext.tsx). Reschedule flows: RescheduleJobModal.tsx derives totalDuration from charge service definitions and allows manual 0.5-hour edits — regression-check after any formatter or rounding change. Backend attik-backend/src/models/inspectionSchema.ts — duration field type only; no precision constraint. attik-backend/src/routes/inspection.ts — reschedule/PATCH assigns inspection.duration from request body without normalization. Decision needed: display-only rounding vs. canonical rounding on write (create + PATCH + Spectora sync paths) so stored values match what users see. Product / architecture decisions (for the implementer) Decision needed: Target precision — one decimal (e.g. 3.5h), two decimals (matches confirm modal / some save paths), or integers when whole (e.g. 3h vs 3.5h). Decision needed: Scope of fix — display formatter only vs. normalize at calculation and save so downstream scheduling, calendar blocks, and exports stay consistent. Decision needed: Whether to introduce a shared duration formatter under attik-frontend/src/util/... reused by chip, Edit Charges, and confirm modal. References Related: ATT-1654 — Duration shows too many decimal places Display site: attik-frontend/src/app/tools/inspections/[id]/components/InspectionDateChip.tsx Modifier time math: attik-frontend/src/util/functions/data/modifierPriceAdjFn.ts Stored field: attik-backend/src/models/inspectionSchema.ts (duration)
Linear 3 days ago
Main App
Planned
Export fails from Post Inspection Revenue widget
Objective Fix HomeBinder CSV export from the Concierge Adoption section on Admin Post-Inspection Revenue so staff can download the same data they already see in the widget. Remove the generic “Export failed” dead-end when charts/KPIs load successfully for the same filters. Align export authentication with the rest of the Admin dashboard (session must work in production on the Admin host). Background Reproduced by Ryan on Admin Attik: open Post-Inspection Revenue → Concierge Adoption → Export (either option) → toast “Export failed” with no useful detail. Widget data for the same company/date filters loads normally. Screen: Admin → Post-Inspection Revenue (/admin/post-inspection-revenue). Export lives in ConciergeAdoptionSection only (ProPair section has no export). Two export modes (both fail today): By Inspection Date — Attik inspections in range with a HomeBinder integration record. Sent Within Date Range — HomeBinder records sent in range (spreadsheet column format). Root cause hypothesis (code trace): Dashboard fetches use callServerAction, which forwards the authjs.session_token cookie from the Next server to the API. Export uses clientApiFetch (browser fetch with credentials: 'include' directly to NEXT_PUBLIC_SERVER_URL). On Admin production (cross-origin to API), the session cookie often does not reach the API → 401 from auth middleware in login.ts. The export error handler only reads message / error from JSON bodies; auth failures return { result: 'failed', reason: '...' }, so the UI shows a generic “Export failed” title with no description. Backend export endpoints exist and are gated correctly (same permission as the widget): GET /homebinder/export/by-inspection-date and GET /homebinder/export/sent-spreadsheet, both behind requireAdminPerm('admin-post-inspection-revenue-view') in homebinder.ts. CSV builders in exportHomeBinderResults.ts and exportHomeBinderSentSpreadsheet.ts return validation errors as { message } or CSV on success (empty data → header-only CSV, not an error). Page access already requires admin-post-inspection-revenue-view via adminLoginCheck in the Post-Inspection Revenue layout. Product decisions (v1) Export must work for users who can view the Post-Inspection Revenue widget with the same filter params (companyIds, startDate, endDate). Both export menu items must succeed after fix. Failed exports should surface a actionable error message when the API rejects the request (not a silent/generic toast). Decision needed (dev): exact transport fix—likely match dashboard pattern (callServerAction or a server action returning CSV) rather than browser-direct clientApiFetch; dev owns approach. Scope Frontend (attik-frontend) src/app/admin/post-inspection-revenue/_components/ConciergeAdoptionSection.tsx — handleExport calls clientApiFetch to homebinder/export/...; error parsing in catch block (lines ~252–282). src/app/admin/post-inspection-revenue/PostInspectionRevenueClient.tsx — passes exportParams from applied filters; dashboard loads via callServerAction to post-inspection-revenue/concierge-adoption (working path to mirror). src/util/functions/fetching/clientApiFetch.ts vs src/util/functions/fetching/callServerAction.ts — auth/cookie forwarding difference is the likely break. src/app/admin/post-inspection-revenue/layout.tsx — page permission gate. Backend (attik-backend) src/routes/homebinder.ts — export route handlers + requireAdminPerm('admin-post-inspection-revenue-view'). src/util/functions/homebinder/exportHomeBinderResults.ts — by-inspection-date CSV aggregation. src/util/functions/homebinder/exportHomeBinderSentSpreadsheet.ts — sent-spreadsheet CSV. src/routes/login.ts — session auth middleware; 401 body shape { result, reason }. src/routes/adminPostInspectionRevenue.ts — concierge-adoption data endpoint (reference for working auth path). src/util/functions/admin/hasAdminPerm.ts — admin permission check. Out of scope Changing concierge-adoption chart/KPI aggregation logic. New export formats or ProPair export (not requested). Done when [ ] Ryan (or any user with admin-post-inspection-revenue-view) can export By Inspection Date CSV from Concierge Adoption on Admin production. [ ] Same user can export Sent Within Date Range CSV. [ ] Export uses the same applied filters as the visible widget (companyIds, startDate, endDate). [ ] Auth failures and API errors show a readable message in the toast (not generic “Export failed” only). [ ] Network trace on repro shows 200 + CSV body (not 401) after fix. References Admin page: Post-Inspection Revenue → Concierge Adoption (/admin/post-inspection-revenue) Slack report: attik-talk message attik-frontend/src/app/admin/post-inspection-revenue/_components/ConciergeAdoptionSection.tsx attik-backend/src/routes/homebinder.ts
Linear 3 days ago
Main App
Planned
Export fails from Post Inspection Revenue widget
Objective Fix HomeBinder CSV export from the Concierge Adoption section on Admin Post-Inspection Revenue so staff can download the same data they already see in the widget. Remove the generic “Export failed” dead-end when charts/KPIs load successfully for the same filters. Align export authentication with the rest of the Admin dashboard (session must work in production on the Admin host). Background Reproduced by Ryan on Admin Attik: open Post-Inspection Revenue → Concierge Adoption → Export (either option) → toast “Export failed” with no useful detail. Widget data for the same company/date filters loads normally. Screen: Admin → Post-Inspection Revenue (/admin/post-inspection-revenue). Export lives in ConciergeAdoptionSection only (ProPair section has no export). Two export modes (both fail today): By Inspection Date — Attik inspections in range with a HomeBinder integration record. Sent Within Date Range — HomeBinder records sent in range (spreadsheet column format). Root cause hypothesis (code trace): Dashboard fetches use callServerAction, which forwards the authjs.session_token cookie from the Next server to the API. Export uses clientApiFetch (browser fetch with credentials: 'include' directly to NEXT_PUBLIC_SERVER_URL). On Admin production (cross-origin to API), the session cookie often does not reach the API → 401 from auth middleware in login.ts. The export error handler only reads message / error from JSON bodies; auth failures return { result: 'failed', reason: '...' }, so the UI shows a generic “Export failed” title with no description. Backend export endpoints exist and are gated correctly (same permission as the widget): GET /homebinder/export/by-inspection-date and GET /homebinder/export/sent-spreadsheet, both behind requireAdminPerm('admin-post-inspection-revenue-view') in homebinder.ts. CSV builders in exportHomeBinderResults.ts and exportHomeBinderSentSpreadsheet.ts return validation errors as { message } or CSV on success (empty data → header-only CSV, not an error). Page access already requires admin-post-inspection-revenue-view via adminLoginCheck in the Post-Inspection Revenue layout. Product decisions (v1) Export must work for users who can view the Post-Inspection Revenue widget with the same filter params (companyIds, startDate, endDate). Both export menu items must succeed after fix. Failed exports should surface a actionable error message when the API rejects the request (not a silent/generic toast). Decision needed (dev): exact transport fix—likely match dashboard pattern (callServerAction or a server action returning CSV) rather than browser-direct clientApiFetch; dev owns approach. Scope Frontend (attik-frontend) src/app/admin/post-inspection-revenue/_components/ConciergeAdoptionSection.tsx — handleExport calls clientApiFetch to homebinder/export/...; error parsing in catch block (lines ~252–282). src/app/admin/post-inspection-revenue/PostInspectionRevenueClient.tsx — passes exportParams from applied filters; dashboard loads via callServerAction to post-inspection-revenue/concierge-adoption (working path to mirror). src/util/functions/fetching/clientApiFetch.ts vs src/util/functions/fetching/callServerAction.ts — auth/cookie forwarding difference is the likely break. src/app/admin/post-inspection-revenue/layout.tsx — page permission gate. Backend (attik-backend) src/routes/homebinder.ts — export route handlers + requireAdminPerm('admin-post-inspection-revenue-view'). src/util/functions/homebinder/exportHomeBinderResults.ts — by-inspection-date CSV aggregation. src/util/functions/homebinder/exportHomeBinderSentSpreadsheet.ts — sent-spreadsheet CSV. src/routes/login.ts — session auth middleware; 401 body shape { result, reason }. src/routes/adminPostInspectionRevenue.ts — concierge-adoption data endpoint (reference for working auth path). src/util/functions/admin/hasAdminPerm.ts — admin permission check. Out of scope Changing concierge-adoption chart/KPI aggregation logic. New export formats or ProPair export (not requested). Done when [ ] Ryan (or any user with admin-post-inspection-revenue-view) can export By Inspection Date CSV from Concierge Adoption on Admin production. [ ] Same user can export Sent Within Date Range CSV. [ ] Export uses the same applied filters as the visible widget (companyIds, startDate, endDate). [ ] Auth failures and API errors show a readable message in the toast (not generic “Export failed” only). [ ] Network trace on repro shows 200 + CSV body (not 401) after fix. References Admin page: Post-Inspection Revenue → Concierge Adoption (/admin/post-inspection-revenue) Slack report: attik-talk message attik-frontend/src/app/admin/post-inspection-revenue/_components/ConciergeAdoptionSection.tsx attik-backend/src/routes/homebinder.ts
Linear 3 days ago
Main App
Planned
Dispatch shows wrong map pin and drive time when inspection address text is correct
Objective Fix dispatch map pins and drive-time forecasts so they reflect the actual inspection location when the address text shown in dispatch is already correct. Give schedulers and dispatch staff trustworthy travel context between inspections—wrong GPS coordinates lead to incorrect Mapbox routes, inflated or deflated drive times, and poor scheduling decisions. Prevent split-brain location data on inspections where the work order property looks correct but calendar/dispatch routing uses stale coordinates. Background Reported example: Work order 1008598200 — the inspection address displays correctly in dispatch, but the map pin sits at incorrect GPS coordinates, producing a wrong drive-time forecast to/from neighboring jobs on that inspector's day. Dispatch loads calendar events and builds Mapbox driving routes from each stop's lat/lng (attik-frontend/src/app/tools/dispatch/dispatchContext.tsx → cleanSearchCoords → callDirectionsServer). Drive time is computed from coordinates, not from the address string. The calendar API mixes two fields on the same inspection document (attik-backend/src/routes/calendar.ts): Address label → inspection.property.address (fallback inspection.address) Map pin / routing coords → top-level inspection.lat / inspection.lng Inspections store coordinates in two places (attik-backend/src/models/inspectionSchema.ts): Top-level lat / lng — used by dispatch, calendar, reschedule preview Nested property.lat / property.lng — used by work order map, scheduler, pricing On create, both are set together (attik-backend/src/util/functions/inspection/createInspection.ts). On property edit from the work order, only property is PATCHed (attik-frontend/src/app/tools/inspections/[id]/components/PropertySectionWrapper.tsx sends { property: propertyData }). Backend updates property.lat / property.lng but does not mirror to top-level unless lat / lng are sent separately (attik-backend/src/routes/inspection.ts). Diagnostic split: If the work order static map (uses property.lat) looks correct but the dispatch pin is wrong → likely stale top-level coordinates after a property/address correction. If both are wrong → bad geocoding at creation or bad coords from import (ISN/Spectora). Root cause hypothesis (code trace) Primary: duplicate coordinate fields drift after property/address updates. User corrects address on work order → property.lat / property.lng update with new geocode. Top-level inspection.lat / inspection.lng remain at original scheduling/import values. Calendar aggregation projects $inspection.lat / $inspection.lng into dispatch events while address text comes from property.address. Mapbox route legs in cleanSearchCoords use the stale top-level coords → wrong pin + wrong drive time despite correct address tooltip. Secondary causes to rule out on example job: Bad geocoding at initial scheduler entry (Mapbox wrong match). ISN historical import using incorrect latitude / longitude from source order (attik-backend/src/util/functions/isn/importIsnHistoricalOrder.ts sets both levels the same at import—less likely unless address edited post-import). Missing/zero coords (lat/lng = 0) skipped in routing—usually omits pin rather than wrong location. Scope Backend (attik-backend) src/routes/calendar.ts — calendar event projection uses top-level lat/lng only; consider $ifNull fallback to property.lat / property.lng for legacy/drifted records. src/routes/inspection.ts — when property.lat / property.lng (or full property address) update, mirror coords (and optionally address) to top-level inspection.lat / inspection.lng / inspection.address. src/models/inspectionSchema.ts — document single source of truth or pre-save sync if appropriate. src/routes/schedule.ts — optimal-slots path also references $inspection.lat; verify parity after fix. Optional one-time backfill for inspections where property.lat/lng ≠ top-level lat/lng (or property has coords and top-level is 0). Frontend (attik-frontend) src/app/tools/inspections/[id]/components/PropertySectionWrapper.tsx — property save sends only property; may also send top-level lat/lng as belt-and-suspenders until backend sync exists. src/app/tools/dispatch/dispatchContext.tsx and src/components/scheduling/dispatchScheduling/cleanSearchCoords.ts — verify routing uses corrected calendar payload (no client-side workaround needed if backend is fixed). src/components/scheduling/DispatchSection.tsx — scheduler dispatch section uses same calendar + directions pattern; confirm fix applies there too. src/app/tools/inspections/[id]/components/RescheduleJobModal.tsx — uses top-level inspection.lat/lng; should benefit from backend sync. Verification on example job For inspection 1008598200, compare top-level vs property lat/lng in DB/API before and after fix. Confirm dispatch pin, Mapbox route, and drive-time blocks align with work order map for that job's scheduled day. Out of scope (v1) Persisting/caching Mapbox directions legs globally (see ATT-819). Dispatch optimization / Mapbox cost layer (ATT-1821). Re-geocoding every inspection in bulk without drift detection. Done when [ ] Dev confirms on 1008598200 (or repro case) whether top-level vs property coords diverge. [ ] Property/address updates keep top-level and property coordinates in sync going forward. [ ] Calendar/dispatch events use correct coords (fallback or synced source) so map pin matches address. [ ] Drive-time legs between inspections on the same day reflect actual travel distance for corrected jobs. [ ] Scheduler dispatch section and tools/dispatch page behave consistently. [ ] Optional backfill script or migration documented/run for existing drifted inspections. [ ] Test coverage for property PATCH updating both coordinate locations. References Example work order: https://www.attik.ai/inspections/1008598200 Related: ATT-819 (persist drive times), ATT-1048 (reschedule dispatch context) attik-backend/src/routes/calendar.ts attik-backend/src/routes/inspection.ts attik-frontend/src/app/tools/dispatch/dispatchContext.tsx attik-frontend/src/components/scheduling/dispatchScheduling/cleanSearchCoords.ts
Linear 4 days ago
Main App
Planned
Dispatch shows wrong map pin and drive time when inspection address text is correct
Objective Fix dispatch map pins and drive-time forecasts so they reflect the actual inspection location when the address text shown in dispatch is already correct. Give schedulers and dispatch staff trustworthy travel context between inspections—wrong GPS coordinates lead to incorrect Mapbox routes, inflated or deflated drive times, and poor scheduling decisions. Prevent split-brain location data on inspections where the work order property looks correct but calendar/dispatch routing uses stale coordinates. Background Reported example: Work order 1008598200 — the inspection address displays correctly in dispatch, but the map pin sits at incorrect GPS coordinates, producing a wrong drive-time forecast to/from neighboring jobs on that inspector's day. Dispatch loads calendar events and builds Mapbox driving routes from each stop's lat/lng (attik-frontend/src/app/tools/dispatch/dispatchContext.tsx → cleanSearchCoords → callDirectionsServer). Drive time is computed from coordinates, not from the address string. The calendar API mixes two fields on the same inspection document (attik-backend/src/routes/calendar.ts): Address label → inspection.property.address (fallback inspection.address) Map pin / routing coords → top-level inspection.lat / inspection.lng Inspections store coordinates in two places (attik-backend/src/models/inspectionSchema.ts): Top-level lat / lng — used by dispatch, calendar, reschedule preview Nested property.lat / property.lng — used by work order map, scheduler, pricing On create, both are set together (attik-backend/src/util/functions/inspection/createInspection.ts). On property edit from the work order, only property is PATCHed (attik-frontend/src/app/tools/inspections/[id]/components/PropertySectionWrapper.tsx sends { property: propertyData }). Backend updates property.lat / property.lng but does not mirror to top-level unless lat / lng are sent separately (attik-backend/src/routes/inspection.ts). Diagnostic split: If the work order static map (uses property.lat) looks correct but the dispatch pin is wrong → likely stale top-level coordinates after a property/address correction. If both are wrong → bad geocoding at creation or bad coords from import (ISN/Spectora). Root cause hypothesis (code trace) Primary: duplicate coordinate fields drift after property/address updates. User corrects address on work order → property.lat / property.lng update with new geocode. Top-level inspection.lat / inspection.lng remain at original scheduling/import values. Calendar aggregation projects $inspection.lat / $inspection.lng into dispatch events while address text comes from property.address. Mapbox route legs in cleanSearchCoords use the stale top-level coords → wrong pin + wrong drive time despite correct address tooltip. Secondary causes to rule out on example job: Bad geocoding at initial scheduler entry (Mapbox wrong match). ISN historical import using incorrect latitude / longitude from source order (attik-backend/src/util/functions/isn/importIsnHistoricalOrder.ts sets both levels the same at import—less likely unless address edited post-import). Missing/zero coords (lat/lng = 0) skipped in routing—usually omits pin rather than wrong location. Scope Backend (attik-backend) src/routes/calendar.ts — calendar event projection uses top-level lat/lng only; consider $ifNull fallback to property.lat / property.lng for legacy/drifted records. src/routes/inspection.ts — when property.lat / property.lng (or full property address) update, mirror coords (and optionally address) to top-level inspection.lat / inspection.lng / inspection.address. src/models/inspectionSchema.ts — document single source of truth or pre-save sync if appropriate. src/routes/schedule.ts — optimal-slots path also references $inspection.lat; verify parity after fix. Optional one-time backfill for inspections where property.lat/lng ≠ top-level lat/lng (or property has coords and top-level is 0). Frontend (attik-frontend) src/app/tools/inspections/[id]/components/PropertySectionWrapper.tsx — property save sends only property; may also send top-level lat/lng as belt-and-suspenders until backend sync exists. src/app/tools/dispatch/dispatchContext.tsx and src/components/scheduling/dispatchScheduling/cleanSearchCoords.ts — verify routing uses corrected calendar payload (no client-side workaround needed if backend is fixed). src/components/scheduling/DispatchSection.tsx — scheduler dispatch section uses same calendar + directions pattern; confirm fix applies there too. src/app/tools/inspections/[id]/components/RescheduleJobModal.tsx — uses top-level inspection.lat/lng; should benefit from backend sync. Verification on example job For inspection 1008598200, compare top-level vs property lat/lng in DB/API before and after fix. Confirm dispatch pin, Mapbox route, and drive-time blocks align with work order map for that job's scheduled day. Out of scope (v1) Persisting/caching Mapbox directions legs globally (see ATT-819). Dispatch optimization / Mapbox cost layer (ATT-1821). Re-geocoding every inspection in bulk without drift detection. Done when [ ] Dev confirms on 1008598200 (or repro case) whether top-level vs property coords diverge. [ ] Property/address updates keep top-level and property coordinates in sync going forward. [ ] Calendar/dispatch events use correct coords (fallback or synced source) so map pin matches address. [ ] Drive-time legs between inspections on the same day reflect actual travel distance for corrected jobs. [ ] Scheduler dispatch section and tools/dispatch page behave consistently. [ ] Optional backfill script or migration documented/run for existing drifted inspections. [ ] Test coverage for property PATCH updating both coordinate locations. References Example work order: https://www.attik.ai/inspections/1008598200 Related: ATT-819 (persist drive times), ATT-1048 (reschedule dispatch context) attik-backend/src/routes/calendar.ts attik-backend/src/routes/inspection.ts attik-frontend/src/app/tools/dispatch/dispatchContext.tsx attik-frontend/src/components/scheduling/dispatchScheduling/cleanSearchCoords.ts
Linear 4 days ago
Main App