feat(jobs): Record Inputs OWL Dialog (v4) — replaces list-as-cards hack

Scrapped the v2/v3 form-view + list-as-cards CSS approach after
extensive failure to make Odoo's editable list look like cards.
Built a proper OWL Dialog component instead, mirroring the pattern
used by fusion_plating_shopfloor's move_parts_dialog.js.

What changed
============
* New OWL Dialog: fp_record_inputs_dialog.js
  - Loads step + prompt definitions via /fp/record_inputs/load
  - Renders each prompt as a semantic <div class="o_fp_ri_card">
  - Per-row widget chosen by input_type:
      numeric/temperature/thickness/time_seconds/ph -> number input
      boolean/pass_fail   -> custom CSS toggle (clearer than Bootstrap)
      date                -> datetime-local input
      photo               -> file picker w/ preview + clear
      multi_point_thickness -> 5-cell grid + live average
      bath_chemistry_panel  -> pH/Conc/Temp/Bath grid
      selection           -> dropdown sourced from selection_options
      text/signature/...  -> text input
  - Live in-range hint for numeric prompts
      ("in range" / "below target" / "above target")
  - Save validates ad-hoc rows have a Prompt label
  - Save dispatches the next_action returned by the wizard model
    (e.g. action_finish_and_advance for the Finish & Next flow)

* New XML template: fp_record_inputs_dialog.xml
  Full DOM control. No fighting Odoo's list view, no class-stripping
  bugs from canUseFormatter, no read-mode-vs-edit-mode CSS dance.

* New SCSS: fp_record_inputs_dialog.scss
  - Dark mode aware (compile-time @if $o-webclient-color-scheme==dark)
  - Pure semantic selectors (.o_fp_ri_card, .o_fp_ri_input, etc.)
  - 14 surface tokens with light/dark hex pairs
  - Tablet polish via @media (max-width: 768px)
  - Custom toggle widget (no <input type="checkbox"> hidden trick)

* New controller: controllers/record_inputs.py
  - /fp/record_inputs/load: returns step + prompts payload
  - /fp/record_inputs/commit: creates a wizard, populates lines,
    calls action_commit (reuses existing audit-trail / synthetic
    move semantics — no commit logic duplicated)

* fp_job_step.py wired to dispatch the new action
  - _fp_open_input_wizard returns
    { type: 'ir.actions.client', tag: 'fp_record_inputs_dialog' }
  - action_open_input_wizard same
  - Contract-review redirect gate preserved (Sub 4 work intact)

* Manifest registers JS/XML/SCSS in BOTH backend + dark bundles
  per the dark-mode pattern in CLAUDE.md.

What was kept
=============
* fp.job.step.input.wizard TransientModel — UNCHANGED. The new
  controller's commit endpoint creates a wizard record and calls
  action_commit() on it, so all the audit-trail / synthetic-move
  / chatter logic stays in Python where it belongs.
* v2 + v3 form views still exist in the XML file. If the OWL
  dialog ever fails, switch action_open_input_wizard back to
  ir.actions.act_window with view_id=v2 or v3.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-05-03 22:17:30 -04:00
parent 328599d539
commit d53fd53b80
7 changed files with 1186 additions and 36 deletions

View File

@@ -0,0 +1,514 @@
// =============================================================================
// Record Inputs Dialog — Sub 12e v4 (proper OWL component)
// Copyright 2026 Nexa Systems Inc.
//
// Pure semantic HTML inside a Dialog. No list view to fight, no
// table-cell unwinding, no class-stripping bugs. Just cards.
//
// Dark mode: branched at compile time on $o-webclient-color-scheme,
// per fusion-plating/CLAUDE.md. Registered in BOTH backend + dark
// asset bundles.
// =============================================================================
$o-webclient-color-scheme: bright !default;
// ---------- Surface tokens ---------------------------------------------------
$_fp-rid-card-hex : #ffffff;
$_fp-rid-card-hover-hex: #f8f9fa;
$_fp-rid-page-hex : #f3f4f6;
$_fp-rid-input-hex : #ffffff;
$_fp-rid-pill-hex : #f1f3f5;
$_fp-rid-border-hex : #d8dadd;
$_fp-rid-border-strong-hex: #b6babf;
$_fp-rid-border-focus-hex : #714B67;
$_fp-rid-ink-hex : #1f2937;
$_fp-rid-ink-soft-hex : #4b5563;
$_fp-rid-ink-mute-hex : #6b7280;
$_fp-rid-ink-faint-hex : #9ca3af;
$_fp-rid-required-hex : #dc3545;
$_fp-rid-ok-hex : #198754;
$_fp-rid-warn-hex : #b18307;
@if $o-webclient-color-scheme == dark {
$_fp-rid-card-hex : #2a2f37 !global;
$_fp-rid-card-hover-hex: #323843 !global;
$_fp-rid-page-hex : #1a1d21 !global;
$_fp-rid-input-hex : #1a1d21 !global;
$_fp-rid-pill-hex : #353a44 !global;
$_fp-rid-border-hex : #3f4651 !global;
$_fp-rid-border-strong-hex: #5a606b !global;
$_fp-rid-border-focus-hex : #a78bca !global;
$_fp-rid-ink-hex : #e5e7eb !global;
$_fp-rid-ink-soft-hex : #c8ccd2 !global;
$_fp-rid-ink-mute-hex : #8a909a !global;
$_fp-rid-ink-faint-hex : #6a707b !global;
$_fp-rid-required-hex : #ea868f !global;
$_fp-rid-ok-hex : #75b798 !global;
$_fp-rid-warn-hex : #ffd866 !global;
}
$rid-card : var(--fp-rid-card-bg, #{$_fp-rid-card-hex});
$rid-card-hover : var(--fp-rid-card-hover-bg, #{$_fp-rid-card-hover-hex});
$rid-page : var(--fp-rid-page-bg, #{$_fp-rid-page-hex});
$rid-input : var(--fp-rid-input-bg, #{$_fp-rid-input-hex});
$rid-pill : var(--fp-rid-pill-bg, #{$_fp-rid-pill-hex});
$rid-border : var(--fp-rid-border, #{$_fp-rid-border-hex});
$rid-border-strong: var(--fp-rid-border-strong, #{$_fp-rid-border-strong-hex});
$rid-border-focus: var(--fp-rid-border-focus, #{$_fp-rid-border-focus-hex});
$rid-ink : var(--fp-rid-ink, #{$_fp-rid-ink-hex});
$rid-ink-soft : var(--fp-rid-ink-soft, #{$_fp-rid-ink-soft-hex});
$rid-ink-mute : var(--fp-rid-ink-mute, #{$_fp-rid-ink-mute-hex});
$rid-ink-faint : var(--fp-rid-ink-faint, #{$_fp-rid-ink-faint-hex});
$rid-required : var(--fp-rid-required, #{$_fp-rid-required-hex});
$rid-ok : var(--fp-rid-ok, #{$_fp-rid-ok-hex});
$rid-warn : var(--fp-rid-warn, #{$_fp-rid-warn-hex});
// =============================================================================
// Dialog frame — generous body, scrollable card stack
// =============================================================================
.o_fp_ri_dialog_content {
.modal-body {
padding: 16px 20px;
background-color: $rid-page;
}
}
.o_fp_ri_header {
display: flex;
align-items: baseline;
gap: 12px;
width: 100%;
}
.o_fp_ri_step_name {
margin: 0;
font-size: 1.25rem;
font-weight: 600;
color: $rid-ink;
}
.o_fp_ri_job_label {
font-size: 0.875rem;
color: $rid-ink-mute;
}
.o_fp_ri_loading,
.o_fp_ri_empty {
padding: 48px 24px;
text-align: center;
color: $rid-ink-mute;
background-color: $rid-card;
border: 1px dashed $rid-border;
border-radius: 12px;
p { margin-bottom: 12px; }
i.fa-spinner { color: $rid-border-focus; font-size: 1.5rem; }
}
// =============================================================================
// Card stack
// =============================================================================
.o_fp_ri_cards {
display: flex;
flex-direction: column;
gap: 12px;
}
.o_fp_ri_card {
padding: 16px 20px;
background-color: $rid-card;
border: 1px solid $rid-border;
border-radius: 12px;
transition: border-color 120ms ease, background-color 120ms ease;
&:hover {
border-color: $rid-border-strong;
}
&:focus-within {
border-color: $rid-border-focus;
box-shadow: 0 0 0 3px
color-mix(in srgb, #{$rid-border-focus} 18%, transparent);
}
&.o_fp_ri_card_required {
border-left: 3px solid $rid-border-focus;
}
}
// ---------- Card header — prompt + meta + remove ----------------------------
.o_fp_ri_card_head {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 8px;
}
.o_fp_ri_prompt {
flex: 1 1 auto;
min-width: 0;
}
.o_fp_ri_prompt_label {
display: inline-flex;
align-items: baseline;
gap: 6px;
font-size: 1rem;
font-weight: 600;
color: $rid-ink;
}
.o_fp_ri_required_mark {
color: $rid-required;
font-weight: 700;
}
.o_fp_ri_prompt_input {
width: 100%;
padding: 6px 10px;
background: $rid-input;
color: $rid-ink;
border: 1px solid $rid-border;
border-radius: 6px;
font-size: 1rem;
font-weight: 600;
&:focus {
border-color: $rid-border-focus;
outline: none;
}
&::placeholder { color: $rid-ink-faint; }
}
.o_fp_ri_meta {
display: inline-flex;
align-items: center;
gap: 6px;
flex-shrink: 0;
}
.o_fp_ri_pill {
display: inline-block;
padding: 3px 10px;
background-color: $rid-pill;
color: $rid-ink-soft;
border: 1px solid $rid-border;
border-radius: 999px;
font-size: 0.75rem;
line-height: 1.2;
text-transform: lowercase;
white-space: nowrap;
}
.o_fp_ri_pill_unit {
background-color: transparent;
border-color: transparent;
color: $rid-ink-mute;
font-weight: 600;
text-transform: none;
}
.o_fp_ri_remove {
flex-shrink: 0;
color: $rid-ink-faint;
padding: 4px 8px;
opacity: 0.5;
transition: opacity 120ms ease, color 120ms ease;
&:hover {
opacity: 1;
color: $rid-required;
}
}
// ---------- Target / hint helpers ------------------------------------------
.o_fp_ri_target {
margin: 0 0 8px 0;
font-size: 0.8125rem;
color: $rid-ink-mute;
}
.o_fp_ri_hint {
margin: 0 0 8px 0;
font-size: 0.8125rem;
color: $rid-ink-faint;
font-style: italic;
}
// =============================================================================
// Card body — inputs per type
// =============================================================================
.o_fp_ri_card_body {
display: flex;
align-items: center;
gap: 12px;
}
// ---------- Common input chrome --------------------------------------------
.o_fp_ri_input {
width: 100%;
max-width: 420px;
padding: 10px 14px;
min-height: 48px;
background-color: $rid-input;
color: $rid-ink;
border: 1px solid $rid-border-strong;
border-radius: 8px;
font-size: 1.125rem;
font-weight: 500;
line-height: 1.4;
transition: border-color 120ms ease, box-shadow 120ms ease;
&::placeholder { color: $rid-ink-faint; font-weight: 400; }
&:hover {
border-color: $rid-ink-mute;
}
&:focus {
border-color: $rid-border-focus;
box-shadow: 0 0 0 3px
color-mix(in srgb, #{$rid-border-focus} 25%, transparent);
outline: none;
}
}
.o_fp_ri_input_select {
appearance: auto;
cursor: pointer;
}
.o_fp_ri_input_numeric {
text-align: left;
font-variant-numeric: tabular-nums;
}
// ---------- Numeric — input + range hint -----------------------------------
.o_fp_ri_numeric {
display: flex;
align-items: center;
gap: 12px;
}
.o_fp_ri_range_hint {
font-size: 0.8125rem;
font-weight: 600;
padding: 4px 10px;
border-radius: 999px;
&.o_fp_ri_range_ok {
background-color: color-mix(in srgb, #{$rid-ok} 15%, transparent);
color: $rid-ok;
}
&.o_fp_ri_range_low,
&.o_fp_ri_range_high {
background-color: color-mix(in srgb, #{$rid-warn} 18%, transparent);
color: $rid-warn;
}
}
// ---------- Boolean toggle (custom — bigger + clearer than Bootstrap) ------
.o_fp_ri_toggle {
display: inline-flex;
align-items: center;
gap: 12px;
cursor: pointer;
user-select: none;
> input { position: absolute; opacity: 0; pointer-events: none; }
}
.o_fp_ri_toggle_track {
position: relative;
display: inline-block;
width: 56px;
height: 32px;
background-color: $rid-pill;
border: 1px solid $rid-border-strong;
border-radius: 999px;
transition: background-color 150ms ease, border-color 150ms ease;
}
.o_fp_ri_toggle_thumb {
position: absolute;
top: 3px;
left: 3px;
width: 24px;
height: 24px;
background-color: $rid-ink-mute;
border-radius: 50%;
transition: transform 150ms ease, background-color 150ms ease;
}
.o_fp_ri_toggle > input:checked ~ .o_fp_ri_toggle_track {
background-color: $rid-border-focus;
border-color: $rid-border-focus;
.o_fp_ri_toggle_thumb {
transform: translateX(24px);
background-color: #fff;
}
}
.o_fp_ri_toggle_label {
font-size: 1rem;
font-weight: 600;
color: $rid-ink-soft;
}
// ---------- Photo upload — modest size, semantic ---------------------------
.o_fp_ri_photo {
display: inline-block;
}
.o_fp_ri_photo_placeholder .btn {
background-color: $rid-pill;
color: $rid-ink-soft;
border: 1px dashed $rid-border-strong;
padding: 12px 18px;
&:hover {
border-color: $rid-border-focus;
color: $rid-border-focus;
}
}
.o_fp_ri_photo_preview {
display: flex;
align-items: center;
gap: 12px;
img {
max-width: 200px;
max-height: 150px;
border-radius: 8px;
border: 1px solid $rid-border;
}
}
// ---------- Multi-point thickness ------------------------------------------
.o_fp_ri_multi_grid {
display: grid;
grid-template-columns: repeat(6, minmax(70px, 1fr));
gap: 8px;
label {
display: flex;
flex-direction: column;
font-size: 0.75rem;
color: $rid-ink-mute;
font-weight: 600;
input {
margin-top: 4px;
padding: 8px 10px;
background: $rid-input;
color: $rid-ink;
border: 1px solid $rid-border-strong;
border-radius: 6px;
font-size: 1rem;
font-variant-numeric: tabular-nums;
&:focus {
border-color: $rid-border-focus;
outline: none;
}
}
}
}
.o_fp_ri_multi_avg {
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: flex-end;
padding-bottom: 4px;
strong {
font-size: 1.125rem;
color: $rid-ink;
font-variant-numeric: tabular-nums;
}
}
// ---------- Bath chemistry panel — same shape as multi ---------------------
.o_fp_ri_panel {
display: grid;
grid-template-columns: repeat(4, minmax(120px, 1fr));
gap: 12px;
width: 100%;
label {
display: flex;
flex-direction: column;
font-size: 0.75rem;
color: $rid-ink-mute;
font-weight: 600;
input {
margin-top: 4px;
padding: 8px 10px;
background: $rid-input;
color: $rid-ink;
border: 1px solid $rid-border-strong;
border-radius: 6px;
font-size: 1rem;
&:focus {
border-color: $rid-border-focus;
outline: none;
}
}
}
}
// ---------- Add-row CTA -----------------------------------------------------
.o_fp_ri_add_btn {
align-self: flex-start;
padding: 10px 18px;
color: $rid-ink-soft;
background-color: $rid-card;
border: 1px dashed $rid-border-strong;
border-radius: 10px;
font-weight: 600;
text-decoration: none;
&:hover {
color: $rid-border-focus;
border-color: $rid-border-focus;
background-color: $rid-card-hover;
text-decoration: none;
}
}
// =============================================================================
// Tablet polish — bigger inputs on narrow screens
// =============================================================================
@media (max-width: 768px) {
.o_fp_ri_card_head {
flex-wrap: wrap;
}
.o_fp_ri_meta {
order: 3;
width: 100%;
}
.o_fp_ri_input {
max-width: 100%;
min-height: 56px;
font-size: 1.25rem;
}
.o_fp_ri_multi_grid {
grid-template-columns: repeat(3, 1fr);
}
.o_fp_ri_panel {
grid-template-columns: repeat(2, 1fr);
}
}