{plan.name}
{plan.description}
OPEN FIELDSERVICE
+Sign in to an organization workspace.
+ +{view.contract.identifier}
{view.contract.status} · Version {view.version.versionNumber} · {commercial.quoteIdentifier} / {commercial.optionName} · ${(commercial.totalCents / 100).toFixed(2)}
{token ? : null}{signer.name} · {signer.email} · {signer.role}
)}{view.contract.status === "draft" ? : null}Commercial agreements
{contact.email || contact.phone || "No contact details"}
No normalized contacts yet.
: null}CRM
| Name | Phone | |
|---|---|---|
| {customer.name} | {customer.email || "—"} | {customer.phone || "—"} |
No customers found.
: null}{file.contentType} · {file.sizeBytes} bytes
This Deal is closed. Reopening is intentionally unsupported.
; return ; } diff --git a/apps/web/src/app/(dashboard)/deals/[id]/interaction-actions.ts b/apps/web/src/app/(dashboard)/deals/[id]/interaction-actions.ts new file mode 100644 index 0000000..ea8cc2f --- /dev/null +++ b/apps/web/src/app/(dashboard)/deals/[id]/interaction-actions.ts @@ -0,0 +1,7 @@ +"use server"; +import { revalidatePath } from "next/cache"; +import { currentActor } from "@/auth/current-actor"; +import { ActivityService } from "@/modules/interactions/activity.service"; +import { NoteService } from "@/modules/interactions/note.service"; +export async function addDealNoteAction(dealId: string, formData: FormData): Promise{item.details}
:null}{item.kind}{result.deal.identifier}
CRM
| Deal | Value | Expected close |
|---|---|---|
| {deal.name} {deal.identifier} | {deal.amountCents} {deal.currency} | {deal.expectedCloseDate || "—"} |
No deals found.
: null}{view.quote.identifier}
{view.quote.status} · Version {view.version.versionNumber}
${(o.totalCents/100).toFixed(2)}
{view.quote.identifier} · {view.quote.status} · version {view.version.versionNumber}
{["sent","rejected","expired","cancelled"].includes(view.quote.status)?:null}Commercial proposals
{i.identifier}
{i.status} · {i.source} · {i.currency} {money(i.totalCents)} · balance {money(i.balanceCents)}{i.isOverdue?" · overdue":""}
+Subtotal {money(i.subtotalCents)}
Tax {money(i.taxCents)}
Total {money(i.totalCents)}
Effective paid {money(i.amountPaidCents)} · balance {money(i.balanceCents)}
{ledger.map(entry=>Commercial records
{view.job.title} · {view.job.status}
+ {actor.role === "member" ? Open offline field workspace : null} + +Completed {view.completion.completedAt.toISOString()} with immutable evidence record {view.completion.id}.
: null} +Status: {report?.status ?? "not started"}. Submitting freezes the reviewed content; editing it later returns it to draft.
+ {mutable && can(actor, "job.report.write") ? : null} + {mutable && report?.status === "draft" && can(actor, "job.report.submit") ? : null} +The signature image is private and is bound to the exact submitted report snapshot.
+ {view.signatures.length ?{view.allowed ? "All persisted completion requirements are satisfied." : "Complete the missing requirements above."}
+ +{initial.job.identifier}
{initial.job.title} · {initial.job.serviceAddress}
Work is stored only in this signed-in Technician workspace. Completion remains pending until every mutation is synchronized and confirmed by the server.
+Staging acknowledges the reviewed submitted report. A changed report is rejected during sync.
Synchronize and submit the report online before staging a signature.
}{job.identifier}
{job.serviceAddress} · {job.status}
{can(actor,"job.schedule")?:null}{can(actor,"job.status")?:null}Field service
| Job | Schedule | Status |
|---|---|---|
| {j.title} {j.identifier} | {j.scheduledDate?`${j.scheduledDate} ${j.scheduledTime??""}`:"Unscheduled"} | {j.status} |
{lead.identifier}
CRM
| Lead | Status | Source |
|---|---|---|
| {lead.name} {lead.identifier} | {lead.status} | {lead.source || "—"} |
No leads found.
: null}{detail.agreement.identifier}
{detail.agreement.status} · {plan.currency} {(plan.priceCents / 100).toFixed(2)} · {detail.version!.effectiveDate}–{detail.version!.expiresOn}
{location.address}
{detail.membership?.status ?? "Not active"} · entitlement {detail.entitlement === null ? "unlimited" : detail.entitlement}
{detail.schedule.status} · {detail.schedule.recurrence} · next due {detail.schedule.nextDueDate}
{detail.schedule.status !== "cancelled" ? : null}{item.dueDate} · {item.status}{item.jobId ? <> · Job> : null}
)}{item.milestoneDays}-day intent · {item.status}
) :No reminder intent is due.
}Maintenance configuration
{a.manufacturer} {a.model} · {a.serialNumber||"No serial"} · {a.status}
Recurring service
Plans, customer agreements, renewals and due work.
{customerName}
{agreement.status} · expires {expiresOn}
)}Maintenance report
Job {report.jobId}
{report.status==="draft"?:}Completion evidence
Job {r.jobId} · {r.submittedAt?.toISOString()??"not submitted"}
)}Immutable configuration
{x.title} · {x.currentPublishedVersionId?"published":"draft"}
)}{x.name} · {x.currentPublishedVersionId?"published":"draft"}
)}{view.call.direction} call
Private to authorized Phone Operations users.
{line.content}
CRM communications
| Direction | From / to | CRM match | Status | Started |
|---|---|---|---|---|
| {call.direction} | {call.normalizedFrom} to {call.normalizedTo} | {call.matchConfidence} | {call.status} | {call.createdAt.toISOString()} |
No calls match this view.
: null}Pipeline
{result.pipeline.description}
{stage.name}
{stage.kind} · {stage.probability}%
No stages configured.
: null}{error ?{error}
: null}CRM configuration
{pipeline.description || "No description"}
)}{!pipelines.length ?No pipelines found.
: null}Commercial
| Item | Type | Sell price | Status | |
|---|---|---|---|---|
| {i.name} {i.sku} | {i.type} | {(i.sellPriceCents/100).toFixed(2)} CAD | {i.status} | {can(actor,"pricebook.manage")&&i.status==="active"?:null} |
OPERATIONS REPORTING
++ Authoritative current-state metrics for in {report.timezone}. +
+Cash movement and current Invoice lifecycle state. Voided Invoices are excluded from outstanding counts.
+Up to 100 non-cancelled Jobs, ordered by scheduled time.
+| Time | Job | Customer | Technician | Status |
|---|---|---|---|---|
| {job.scheduledTime?.slice(0, 5) ?? "Unscheduled"} | +{job.identifier}{job.title} | +{job.customerName} | +{job.technicianName ?? "Unassigned"} | +{job.status.replaceAll("_", " ")} | +
No Jobs scheduled today
The schedule is clear for this business date.
Customer lifecycle
Server-authoritative consent, referral rewards, audience snapshots and pending delivery intents.
| Campaign | Status | Channel | Scheduled | Action |
|---|---|---|---|---|
| {c.name} | {c.status} | {c.channel} | {c.scheduledFor?.toLocaleString()??"—"} | {manage&&c.status==="draft"?:"—"} |
No campaigns yet.
:null}{referrals.total} tenant-local referral record(s). Rewards settle only from completed Jobs or paid Invoices.
America/Vancouver
{j.identifier} · {j.durationMinutes} minutes
)}Phone Operations
Twilio is the audited legacy provider. Secrets remain server-only and no call is placed while Phone Operations is disabled.
Organization settings
Controls are tenant-scoped and applied by the server when it evaluates completed Jobs.
Settings
{current.countryCode}-{current.regionCode} · {current.pricesIncludeTax?"Tax-inclusive":"Tax-exclusive"} · {current.components.map(x=>`${x.code} ${x.rateBasisPoints/100}%`).join(", ")}
:Tax is not configured.
}{task.targetType}
{task.description||"No description"}
CRM
| Task | Priority | Status | Due |
|---|---|---|---|
| {t.title} | {t.priority} | {t.status} | {t.dueAt?.toLocaleString()??"—"} |
Secure signing ceremony
{result.identifier} · {result.company.name} · {result.customer.name}
{commercial.optionName}
{commercial.currency ?? "CAD"} {(commercial.totalCents / 100).toFixed(2)}
Includes tax {(commercial.taxCents / 100).toFixed(2)}
We could not process that request. Please try again.
: null}{state.signed ?Signature recorded. You may close this page.
: state.declined ?Contract declined.
: }A member of our team will follow up with you shortly.
:null}{actionState.reviewUrl?:null}{actionState.planOffer?.length?{plan.name}
{plan.description}
Contact us to enroll. No Agreement or Membership is created from this page.
Service follow-up · {view.jobIdentifier}
Hi {view.customerName}, we’d appreciate feedback on your recent service.
{complete?There’s nothing else you need to submit.
Secure maintenance agreement
{view.identifier} · {view.customer.name} · {view.effectiveDate}–{view.expiresOn}
{view.currency} {(view.totalPriceCents / 100).toFixed(2)}
Service location: {view.serviceLocation.address}
We could not process that request. Please try again.
: null}{state.signed || view.signed ?Agreement signed. You may close this page.
: }OPEN FIELDSERVICE
++ The Next.js migration foundation is running side-by-side with the existing application. Features will move here only after their behavior and tenant isolation are verified. +
+{detail}
+Proposal
Valid until {view.expiresAt.toLocaleDateString()}
{q.accepted?Thank you. Your selection was recorded.
:null}{q.error?We could not process that request. Please try again.
:null}${(o.totalCents/100).toFixed(2)}
Selected
:null}Customer referral
This referral link is invalid, expired, or already used.
:null}{claimed?Thanks. The service team received your request.
:}Marketing emails and texts are disabled. Service messages such as appointments and receipts are unaffected.
You’ll stop receiving promotional email and text campaigns. Transactional service messages are unaffected.
{state.error?We could not process that request. Please try again.
:null}