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:
@@ -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; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 & 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 & 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 & 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 & Create SO</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user