Complete flow for change order creation, review, and history plus dispute filing, response, resolution, and escalation.
ContractorChangeOrderScreen — create mode
useMutation(api.jobs.changeOrders.create), MediaUpload component
PosterChangeOrderScreen — review mode
useMutation(api.jobs.changeOrders.approve / reject), ConfirmationDialog
ChangeOrderHistoryScreen
useQuery(api.jobs.changeOrders.listByJob), audit trail pattern
CreateDisputeScreen — Typeform wizard
useMutation(api.features.disputes.createDispute), MediaUpload, 5-step flow
SuccessScreen (inline component)
Navigation: router.replace to dispute detail
DisputeDetail (app/disputes/[id].tsx)
useQuery(api.features.disputes.getDisputeDetail), real-time messages
DisputeResponseScreen (contractor-side)
useMutation(api.features.disputes.addResponse), counter-evidence upload
DisputeDetail — resolution_proposed state
useMutation(api.features.disputes.acceptResolution), 48h deadline timer
DisputeDetail — resolved state
Escrow adjustment via api.admin.escrow, satisfaction survey mutation
DisputeDetail — escalated state
Admin escalation via api.admin.disputes.escalate, Slack: dispute_notice channel
escrowId: Id<"escrow"> jobId: Id<"jobs"> posterId: Id<"posterUsers"> contractorId: Id<"contractorUsers"> disputeNumber: string // "DIS-000001" initiatedBy: string // poster | contractor reason: string description: string status: string // open → under_review → awaiting_response // → resolution_proposed → resolved | escalated amountInDispute: number amountToPoster: number? amountToContractor: number? resolutionNotes: string? media: Array<{storageId, type, mimeType}> resolvedByAdminId: Id<"adminUsers">?
jobId: Id<"jobs"> escrowId: Id<"escrow"> changeOrderNumber: string? // "CO-01" requestedById: string requestedByType: string // poster | contractor description: string reason: string? additionalCost: number // + or - additionalDays: number? status: string // pending_signatures → pending_funding // → approved | rejected | paid media: Array<{storageId, type, mimeType}> paymentCompleted: boolean signedByContractorAt: number? signedByPosterAt: number?
// Disputes useQuery(api.features.disputes.getMyDisputes) useQuery(api.features.disputes.getDisputeDetail) useMutation(api.features.disputes.createDispute) useMutation(api.features.disputes.addResponse) useMutation(api.features.disputes.addEvidence) useMutation(api.features.disputes.acceptResolution) useMutation(api.features.disputes.withdrawDispute) // Change Orders useQuery(api.jobs.changeOrders.get) useQuery(api.jobs.changeOrders.listByJob) useMutation(api.jobs.changeOrders.create) useMutation(api.jobs.changeOrders.approve) useMutation(api.jobs.changeOrders.reject) useMutation(api.jobs.changeOrders.cancel) useMutation(api.jobs.changeOrders.markPaid)
// Slack Alerts Channel: dispute_notice → New dispute filed → Resolution accepted/appealed → Escalation triggered Channel: ops_alerts → Change order created → Change order funded // Escrow (refund processing) api.admin.escrow.processRefund api.payments.escrow.adjustForDispute // Stripe api.stripe.stripe.createChangeOrderPaymentIntent api.stripe.stripe.createRefund // Transactional Email (SendGrid) dispute_filed → poster + contractor dispute_resolved → both parties change_order_submitted → poster