Files
Odoo-Modules/fusion_repairs/views/portal_client_repair_templates.xml
gsinghpal ad553b1082 feat(fusion_repairs): Phase 1 sales rep + public client portals
Both portals share the existing fusion.repair.intake.service so behaviour
stays identical across all three intake surfaces (backend wizard,
sales rep portal, public client portal).

Sales rep portal
- Hard depends on fusion_authorizer_portal (reuses is_sales_rep_portal
  flag + group_sales_rep_portal scaffolding)
- /my/repair/new  - mobile-friendly intake form with phone-first
  partner search (jsonrpc lookup), category select, third-party flag,
  urgency, photo capture
- /my/repairs     - list of repairs the rep submitted (paginated)
- /my/repair/<id> - read-only detail with status, equipment, scheduled
  visit
- Interaction-class JS (Odoo 19 public.interactions), safe DOM construction
- Mobile SCSS with 44px tap targets, sticky CTA on small screens
- Record rule scopes portal users to repairs where
  x_fc_intake_user_id = user.id

Public client portal
- auth='public' - voicemail-ready /repair URL
- /repair         - landing page with 911 disclaimer and Start CTA
- /repair/new     - single-page form: contact, equipment, issue, urgency,
  optional photos. QR pre-fill via ?sn=<serial>
- /repair/submit  - CSRF + honeypot + per-IP rate limit (configurable);
  finds or creates partner; calls intake service with sudo
- /repair/thanks  - confirmation with reference number
- /repair/lookup_phone (jsonrpc) - safe partner match returning ONLY
  masked name (first + last initial) + city (no other PII leakage)

Security fix: technician record rule on repair.order now uses STORED
fields (technician_id + additional_technician_ids) instead of the
non-stored all_technician_ids compute, which was failing SQL generation.

Verified end-to-end on local westin-v19:
- Sales rep create via intake service with the rep user context creates
  the repair with x_fc_intake_source='sales_rep_portal' and proper
  activities
- /repair/submit posts urlencoded data -> creates partner + repair
  ('BR-WA/RO/00010', source='client_portal', urgency='urgent') ->
  redirects to /repair/thanks with the reference

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-20 21:52:12 -04:00

186 lines
11 KiB
XML

<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- ============================================================== -->
<!-- /repair landing -->
<!-- ============================================================== -->
<template id="portal_client_repair_landing" name="Repair - Landing">
<t t-call="website.layout">
<div id="wrap" class="o_fusion_repairs_client">
<section class="container py-5">
<div class="row justify-content-center text-center">
<div class="col-12 col-lg-8">
<h1 class="display-5 fw-bold">Need a repair?</h1>
<p class="lead text-muted mb-4">
Tell us about your equipment and what's going wrong.
We'll respond on the next business day - or sooner if it's urgent.
</p>
<a href="/repair/new" class="btn btn-primary btn-lg px-5 py-3">
Start a Service Request
</a>
<div class="text-muted mt-4 small">
<i class="fa fa-shield-alt me-1"/>
Your information is private and used only to schedule your repair.
</div>
<div class="alert alert-warning mt-4 text-start">
<strong>Is anyone hurt right now?</strong>
If you have a medical emergency, please hang up and dial <strong>9-1-1</strong>.
</div>
</div>
</div>
</section>
</div>
</t>
</template>
<!-- ============================================================== -->
<!-- /repair/new form -->
<!-- ============================================================== -->
<template id="portal_client_repair_form" name="Repair - Form">
<t t-call="website.layout">
<div id="wrap" class="o_fusion_repairs_client">
<section class="container py-5">
<div class="row justify-content-center">
<div class="col-12 col-lg-7">
<h2 class="mb-3">Service Request</h2>
<p class="text-muted">
Fill in the form below. A team member will follow up shortly.
</p>
<t t-if="error == 'missing'">
<div class="alert alert-danger">Please fill in all required fields.</div>
</t>
<t t-if="error == 'spam'">
<div class="alert alert-danger">Submission blocked. If this is a mistake, please call our office.</div>
</t>
<t t-if="error == 'rate_limited'">
<div class="alert alert-warning">Too many requests from your location. Please try again in an hour.</div>
</t>
<t t-if="error == 'server'">
<div class="alert alert-danger">Something went wrong. Please try again or call us directly.</div>
</t>
<form action="/repair/submit" method="POST"
enctype="multipart/form-data" class="card shadow-sm">
<input type="hidden" name="csrf_token"
t-att-value="request.csrf_token()"/>
<!-- Honeypot. Real users never see this. -->
<div style="position:absolute;left:-9999px;top:-9999px;" aria-hidden="true">
<label>Company name</label>
<input type="text" name="hp_company" tabindex="-1" autocomplete="off"/>
</div>
<div class="card-body p-4">
<h5>1. Your contact details</h5>
<div class="mb-3">
<label class="form-label">Your name <span class="text-danger">*</span></label>
<input type="text" name="client_name" class="form-control form-control-lg" required="required"/>
</div>
<div class="mb-3">
<label class="form-label">Phone number <span class="text-danger">*</span></label>
<input type="tel" name="client_phone" class="form-control form-control-lg" required="required" placeholder="(519) 555-1234"/>
</div>
<div class="mb-3">
<label class="form-label">Email (so we can send a confirmation)</label>
<input type="email" name="client_email" class="form-control"/>
</div>
<div class="mb-3">
<label class="form-label">Street address</label>
<input type="text" name="client_street" class="form-control"/>
</div>
<div class="mb-3">
<label class="form-label">City</label>
<input type="text" name="client_city" class="form-control"/>
</div>
<hr/>
<h5>2. What equipment needs service?</h5>
<div class="mb-3">
<label class="form-label">Equipment category <span class="text-danger">*</span></label>
<select name="category_id" class="form-select form-select-lg" required="required">
<option value="">Choose one...</option>
<t t-foreach="categories" t-as="cat">
<option t-att-value="cat.id">
<t t-out="cat.name"/>
</option>
</t>
</select>
</div>
<div class="form-check mb-3">
<input type="checkbox" class="form-check-input" id="third_party" name="third_party"/>
<label class="form-check-label" for="third_party">
I didn't buy this equipment from Fusion / Westin
</label>
</div>
<hr/>
<h5>3. What's wrong?</h5>
<div class="mb-3">
<label class="form-label">Short description <span class="text-danger">*</span></label>
<input type="text" name="issue_summary" class="form-control form-control-lg" required="required" placeholder="e.g. 'stairlift beeps and won't move'"/>
</div>
<div class="mb-3">
<label class="form-label">Anything else we should know?</label>
<textarea name="internal_notes" class="form-control" rows="3"></textarea>
</div>
<div class="mb-3">
<label class="form-label">Photos or short video (optional)</label>
<input type="file" name="photos" class="form-control" accept="image/*,video/*" multiple="multiple" capture="environment"/>
</div>
<hr/>
<h5>4. How urgent is it?</h5>
<div class="mb-3">
<select name="urgency" class="form-select form-select-lg" required="required">
<option value="normal">Normal - within a few days</option>
<option value="urgent">Urgent - within 24 hours</option>
<option value="safety">Safety issue - right now</option>
</select>
<small class="text-muted">
If anyone is hurt, hang up and call <strong>9-1-1</strong>.
</small>
</div>
</div>
<div class="card-footer text-end">
<button type="submit" class="btn btn-primary btn-lg">
Submit Request
</button>
</div>
</form>
</div>
</div>
</section>
</div>
</t>
</template>
<!-- ============================================================== -->
<!-- /repair/thanks -->
<!-- ============================================================== -->
<template id="portal_client_repair_thanks" name="Repair - Thanks">
<t t-call="website.layout">
<div id="wrap" class="o_fusion_repairs_client">
<section class="container py-5">
<div class="row justify-content-center text-center">
<div class="col-12 col-lg-7">
<i class="fa fa-check-circle fa-4x text-success mb-3"/>
<h1 class="mb-3">Got it!</h1>
<p class="lead text-muted">
Your service request <strong t-if="ref"><t t-out="ref"/></strong> was received.
We'll get back to you on the next business day or sooner if you marked it urgent.
</p>
<a href="/repair" class="btn btn-outline-secondary mt-3">Back to home</a>
</div>
</div>
</section>
</div>
</t>
</template>
</odoo>