fix(fusion_claims): service-booking wizard responsive — namespace Bootstrap classes + reorder media queries

The 19.0.9.5.0 CSS pass (padding/scroll) did not fix "fields sitting on each
other" on small screens. Deep dive against the live web.assets_backend bundle
found two compounding root causes (both measured, not guessed):

1. Dead media query. The @media(max-width:560px) collapse for the inner field
   grids (.two/.three) and .timepick was nested BEFORE the base .two/.three/
   .timepick rules. Equal specificity → the later base rule wins → the media
   query never applied. matchMedia matched at 320/390px yet grids stayed 2–3
   columns and crammed/truncated. Moved all @media overrides to the END of the
   .o_service_booking block so they win the cascade.

2. Bootstrap class collision. The wizard reused row/card/grid/btn; Odoo's
   backend Bootstrap applies .row{display:flex;margin:0 -16px}, .card{display:flex},
   etc. globally even under the scoped parent (they win for properties the scoped
   rule doesn't set). Measured: every .row computed display:flex + margin-left/
   right:-16px. Renamed all layout classes to sb-* (sb-row/sb-card/sb-grid/sb-btn)
   in service_booking.xml + service_booking.scss.

Verified at 320/390/768/1280 against the real prod bundle (computed
grid-template-columns now 1fr at <=560px; .sb-row margin 0 / display block;
2-col desktop intact). Documented both gotchas in fusion_claims/CLAUDE.md §47.

Bump fusion_claims 19.0.9.5.0 -> 19.0.9.6.0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-06-04 20:38:52 -04:00
parent 423f288507
commit 53fe13344d
4 changed files with 92 additions and 45 deletions

View File

@@ -66,26 +66,17 @@
.step.draft { margin-left: auto; color: var(--sb-money); background: var(--sb-money-soft); }
.body { padding: 20px 24px 6px; }
.grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
@media (max-width: 780px) { .grid { grid-template-columns: 1fr; } }
@media (max-width: 560px) {
.wrap { margin: 12px auto; padding: 0 10px; }
.body { padding: 14px 16px 4px; }
.topbar { padding: 14px 16px; }
.foot { padding: 14px 16px; flex-wrap: wrap; }
.two, .three { grid-template-columns: 1fr; }
.timepick { flex-wrap: wrap; }
}
.sb-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
.card {
.sb-card {
background: var(--sb-card);
border: 1px solid var(--sb-border);
border-radius: 13px;
padding: 16px 17px;
box-shadow: 0 1px 3px rgba(16, 24, 40, .08), 0 1px 2px rgba(16, 24, 40, .06);
}
.card.span2 { grid-column: 1 / -1; }
.card h3 {
.sb-card.span2 { grid-column: 1 / -1; }
.sb-card h3 {
margin: 0 0 13px;
font-size: 11.5px;
font-weight: 700;
@@ -96,8 +87,8 @@
align-items: center;
gap: 7px;
}
.card h3 .dot { width: 7px; height: 7px; border-radius: 50%; background: linear-gradient(135deg, #5ba848, #2e7aad); }
.card h3 .tag {
.sb-card h3 .dot { width: 7px; height: 7px; border-radius: 50%; background: linear-gradient(135deg, #5ba848, #2e7aad); }
.sb-card h3 .tag {
margin-left: auto;
font-size: 10px;
font-weight: 700;
@@ -109,8 +100,8 @@
}
label.fl { display: block; font-size: 12px; font-weight: 600; color: var(--sb-muted); margin: 0 0 5px; }
.row { margin-bottom: 12px; }
.row:last-child { margin-bottom: 0; }
.sb-row { margin-bottom: 12px; }
.sb-row:last-child { margin-bottom: 0; }
.two { display: grid; grid-template-columns: 1fr 1fr; gap: 11px; }
.three { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 9px; }
@@ -276,7 +267,7 @@
}
.foot .spacer { margin-right: auto; font-size: 12px; color: var(--sb-faint); }
.btn {
.sb-btn {
border: none;
border-radius: 10px;
padding: 11px 18px;
@@ -285,13 +276,32 @@
cursor: pointer;
font-family: inherit;
}
.btn.ghost { background: transparent; color: var(--sb-muted); border: 1px solid var(--sb-border); }
.btn.primary {
.sb-btn.ghost { background: transparent; color: var(--sb-muted); border: 1px solid var(--sb-border); }
.sb-btn.primary {
color: #fff;
background: linear-gradient(135deg, #5ba848, #2e7aad);
box-shadow: 0 3px 10px color-mix(in srgb, #2e7aad 40%, transparent);
}
.btn[disabled] { opacity: .6; cursor: not-allowed; }
.sb-btn[disabled] { opacity: .6; cursor: not-allowed; }
.hide { display: none !important; }
// Responsive overrides — MUST come AFTER the base layout rules above. These
// selectors (.two/.three/.timepick/.sb-grid/.foot/…) have the same specificity
// as their base rules, so the cascade only lets the media query win when it is
// emitted later in the source. Previously this block sat right after .sb-grid
// (BEFORE the base .two/.three/.timepick rules), so the later base rules
// overrode it and the inner field-grids never collapsed to one column on a
// phone — fields crammed side-by-side. Keep these last.
@media (max-width: 780px) {
.sb-grid { grid-template-columns: 1fr; }
}
@media (max-width: 560px) {
.wrap { margin: 12px auto; padding: 0 10px; }
.body { padding: 14px 16px 4px; }
.topbar { padding: 14px 16px; }
.foot { padding: 14px 16px; flex-wrap: wrap; }
.two, .three { grid-template-columns: 1fr; }
.timepick { flex-wrap: wrap; }
}
}

View File

@@ -20,11 +20,11 @@
</div>
<div class="body">
<div class="grid">
<div class="sb-grid">
<!-- CUSTOMER -->
<div class="card">
<div class="sb-card">
<h3><span class="dot"></span>Customer</h3>
<div class="row">
<div class="sb-row">
<div class="seg full">
<button t-att-class="{ on: state.custMode === 'existing' }"
t-on-click="() => this.setCust('existing')">Existing customer</button>
@@ -33,22 +33,22 @@
</div>
</div>
<div t-if="state.custMode === 'existing'">
<div class="row">
<div class="sb-row">
<label class="fl">Search by phone, name or SO</label>
<input class="f" t-model="state.soSearch" placeholder="e.g. (416) 555-0142 …"/>
<div class="hint">Inbound call? Type the phone number — we match the contact &amp; their history.</div>
</div>
</div>
<div t-if="state.custMode === 'new'">
<div class="row two">
<div class="sb-row two">
<div><label class="fl">Client name *</label><input class="f" t-model="state.customer.name" placeholder="Full name"/></div>
<div><label class="fl">Phone *</label><input class="f" t-model="state.customer.phone" placeholder="(416) 555-…"/></div>
</div>
<div class="row"><label class="fl">Email</label><input class="f" type="email" t-model="state.customer.email" placeholder="client@email.com"/></div>
<div class="row"><label class="fl">Address</label>
<div class="sb-row"><label class="fl">Email</label><input class="f" type="email" t-model="state.customer.email" placeholder="client@email.com"/></div>
<div class="sb-row"><label class="fl">Address</label>
<div class="with-icon"><input class="f" t-model="state.customer.street" placeholder="Start typing an address…"/><span class="pin">📍</span></div>
</div>
<div class="row three">
<div class="sb-row three">
<div><label class="fl">Unit</label><input class="f" t-model="state.customer.unit" placeholder="#"/></div>
<div><label class="fl">Buzz</label><input class="f" t-model="state.customer.buzz" placeholder="—"/></div>
<div><label class="fl">City</label><input class="f" t-model="state.customer.city" placeholder="City"/></div>
@@ -58,9 +58,9 @@
</div>
<!-- SERVICE & PRICING -->
<div class="card">
<div class="sb-card">
<h3><span class="dot"></span>Service &amp; Pricing<span class="tag">$ REVENUE</span></h3>
<div class="row two">
<div class="sb-row two">
<div>
<label class="fl">Device being serviced</label>
<select class="f" t-on-change="onDevice">
@@ -79,7 +79,7 @@
<input class="f" t-model="state.issue" placeholder="e.g. won't power on"/>
</div>
</div>
<div class="row" t-if="!state.inShop">
<div class="sb-row" t-if="!state.inShop">
<label class="fl">Service call type</label>
<select class="f"
t-on-change="onCallType">
@@ -100,9 +100,9 @@
</div>
<!-- SCHEDULE -->
<div class="card">
<div class="sb-card">
<h3><span class="dot"></span>Schedule</h3>
<div class="row two">
<div class="sb-row two">
<div><label class="fl">Date</label><input class="f" type="date" t-model="state.date"/></div>
<div><label class="fl">Duration</label>
<select class="f" t-model.number="state.durationHr">
@@ -114,7 +114,7 @@
</select>
</div>
</div>
<div class="row">
<div class="sb-row">
<label class="fl">Start time</label>
<div class="timepick">
<select class="f" t-model.number="state.hour">
@@ -140,7 +140,7 @@
</div>
<div class="endtime">Ends at <b><t t-esc="endLabel"/></b> · your local time</div>
</div>
<div class="row">
<div class="sb-row">
<label class="fl">Technician</label>
<select class="f" t-model.number="state.technicianId">
<option value="">— Choose —</option>
@@ -152,17 +152,17 @@
</div>
<!-- LOCATION -->
<div class="card">
<div class="sb-card">
<h3><span class="dot"></span>Location</h3>
<div class="opt" style="border:none; padding-top:0;">
<div class="lab">In-shop job<small>At the store — no call-out, labour @ $<t t-esc="fmt(state.labour.inshop)"/>/hr</small></div>
<div class="sw" t-att-class="{ on: state.inShop }" t-on-click="toggleInShop"></div>
</div>
<div t-if="!state.inShop">
<div class="row"><label class="fl">Job address</label>
<div class="sb-row"><label class="fl">Job address</label>
<div class="with-icon"><input class="f" t-model="state.customer.street" placeholder="Auto-fills from customer…"/><span class="pin">📍</span></div>
</div>
<div class="row two">
<div class="sb-row two">
<div><label class="fl">Unit / Suite</label><input class="f" t-model="state.customer.unit" placeholder="#"/></div>
<div><label class="fl">Buzz code</label><input class="f" t-model="state.customer.buzz" placeholder="—"/></div>
</div>
@@ -170,11 +170,11 @@
</div>
<!-- JOB DETAILS -->
<div class="card span2">
<div class="sb-card span2">
<h3><span class="dot"></span>Job details</h3>
<div class="two">
<div class="row"><label class="fl">Work description</label><textarea class="f" t-model="state.description" placeholder="Symptom, what to check, history…"></textarea></div>
<div class="row"><label class="fl">Parts / materials to bring</label><textarea class="f" t-model="state.materials" placeholder="Batteries, controller, casters…"></textarea></div>
<div class="sb-row"><label class="fl">Work description</label><textarea class="f" t-model="state.description" placeholder="Symptom, what to check, history…"></textarea></div>
<div class="sb-row"><label class="fl">Parts / materials to bring</label><textarea class="f" t-model="state.materials" placeholder="Batteries, controller, casters…"></textarea></div>
</div>
<div class="opt"><div class="lab">Under manufacturer warranty<small>Parts not billed when covered</small></div><div class="sw" t-att-class="{ on: state.warranty }" t-on-click="() => state.warranty = !state.warranty"></div></div>
<div class="opt"><div class="lab">POD required<small>Capture proof of delivery on completion</small></div><div class="sw" t-att-class="{ on: state.pod }" t-on-click="() => state.pod = !state.pod"></div></div>
@@ -197,8 +197,8 @@
<div class="foot">
<span class="spacer">Local time · America/Toronto · <t t-esc="state.distanceKm"/> km away</span>
<button class="btn ghost" t-on-click="() => this.action.doAction({ type: 'ir.actions.act_window_close' })">Cancel</button>
<button class="btn primary" t-on-click="submit" t-att-disabled="state.saving">Book &amp; Create SO</button>
<button class="sb-btn ghost" t-on-click="() => this.action.doAction({ type: 'ir.actions.act_window_close' })">Cancel</button>
<button class="sb-btn primary" t-on-click="submit" t-att-disabled="state.saving">Book &amp; Create SO</button>
</div>
</div>
</div>