MiknasTrading/docs/superpowers/plans/2026-05-25-supplier-modal-wizard.md

18 KiB
Raw Blame History

Supplier Select Modal — Two-Step Wizard Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Replace the mutually-exclusive tab bar in the supplier select modal with a two-step wizard: Step 1 picks the supply method (Full Order / By Item), Step 2 shows the appropriate supplier selection UI with a "← Change method" back link that clears all selections.

Architecture: Single Blade component file change only — no controller, route, or migration work. The HTML is restructured into #sup-step1 (method cards) and #sup-step2 (existing form), both children of the modal shell. Two new JS functions (showStep / goBack) drive transitions; the existing form, panes, and submission logic are untouched.

Tech Stack: Laravel Blade, vanilla JS, inline CSS (Tailwind JIT not used — inline styles only per project convention)

Spec: docs/superpowers/specs/2026-05-25-supplier-modal-wizard-design.md


Task 1: Replace the header tab bar with the mode badge row

Files:

  • Modify: resources/views/components/purchase/supplier-select-modal.blade.php

The current header has a tab bar (stab-global, stab-item). Remove it. Replace with a hidden #sup-mode-badge-row div (shown only in Step 2). Also add id="sup-modal-subtitle" to the subtitle element so JS can update it dynamically. Change the initial title to "Request for Quotation" and subtitle to "How do you want to assign suppliers?". Change padding:20px 24px 0padding:20px 24px 16px (the 0 bottom padding was a tab-flush hack no longer needed).

  • Step 1: Replace the header block

In resources/views/components/purchase/supplier-select-modal.blade.php, find and replace this exact block:

    {{-- Header --}}
    <div style="padding:20px 24px 0;border-bottom:1px solid #f1f5f9;flex-shrink:0;">
      <div style="display:flex;align-items:flex-start;justify-content:space-between;gap:12px;margin-bottom:16px;">
        <div>
          <div id="sup-modal-title" style="font-size:17px;font-weight:700;color:#0f172a;">Select Suppliers</div>
          <div style="font-size:12px;color:#64748b;margin-top:3px;">Choose who receives the quote request</div>
        </div>
        <button onclick="closeSupplierModal()" aria-label="Close"
          style="width:32px;height:32px;border-radius:8px;border:none;background:#f1f5f9;cursor:pointer;font-size:18px;color:#64748b;display:flex;align-items:center;justify-content:center;flex-shrink:0;">×</button>
      </div>

      {{-- Tab bar --}}
      <div style="display:flex;gap:0;">
        <button id="stab-global" type="button" onclick="switchSupTab('global')"
          style="padding:10px 20px;font-size:13px;font-weight:700;border:none;background:none;cursor:pointer;color:#2563eb;border-bottom:2px solid #2563eb;margin-bottom:-1px;">
          Full Order
        </button>
        <button id="stab-item" type="button" onclick="switchSupTab('item')"
          style="padding:10px 20px;font-size:13px;font-weight:700;border:none;background:none;cursor:pointer;color:#94a3b8;border-bottom:2px solid transparent;margin-bottom:-1px;">
          By Item
        </button>
      </div>
    </div>

Replace with:

    {{-- Header --}}
    <div style="padding:20px 24px 16px;border-bottom:1px solid #f1f5f9;flex-shrink:0;">
      <div style="display:flex;align-items:flex-start;justify-content:space-between;gap:12px;margin-bottom:4px;">
        <div>
          <div id="sup-modal-title" style="font-size:17px;font-weight:700;color:#0f172a;">Request for Quotation</div>
          <div id="sup-modal-subtitle" style="font-size:12px;color:#64748b;margin-top:3px;">How do you want to assign suppliers?</div>
        </div>
        <button onclick="closeSupplierModal()" aria-label="Close"
          style="width:32px;height:32px;border-radius:8px;border:none;background:#f1f5f9;cursor:pointer;font-size:18px;color:#64748b;display:flex;align-items:center;justify-content:center;flex-shrink:0;">×</button>
      </div>
      {{-- Mode badge row: hidden in Step 1, shown in Step 2 --}}
      <div id="sup-mode-badge-row" style="display:none;align-items:center;gap:8px;margin-top:10px;">
        <button type="button" onclick="goBack()"
          style="display:flex;align-items:center;gap:4px;font-size:12px;color:#2563eb;background:none;border:none;cursor:pointer;padding:0;font-weight:600;">
          <svg width="13" height="13" fill="none" stroke="currentColor" stroke-width="2.5" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M15 18l-6-6 6-6"/></svg>
          Change method
        </button>
        <span style="color:#cbd5e1;">·</span>
        <span id="sup-mode-badge" style="font-size:11px;padding:2px 9px;border-radius:10px;font-weight:700;"></span>
      </div>
    </div>
  • Step 2: Verify the file saved correctly

Open resources/views/components/purchase/supplier-select-modal.blade.php and confirm:

  • No stab-global or stab-item buttons exist
  • sup-mode-badge-row div is present with display:none
  • Title reads "Request for Quotation"
  • Subtitle element has id="sup-modal-subtitle"

Task 2: Add Step 1 method-selection cards and wrap Step 2

Files:

  • Modify: resources/views/components/purchase/supplier-select-modal.blade.php

Insert #sup-step1 (method cards + Step 1 Cancel footer) immediately after the header closing </div>. Then wrap the existing <form> and the existing footer <div> inside a new #sup-step2 div. Step 1 is display:flex on load; Step 2 is display:none.

  • Step 1: Insert Step 1 cards immediately after the header

Find this exact line (the comment that opens the form section):

    {{-- Single form wrapping both panes --}}
    <form id="sup-form" action="{{ route('purchase.requests.rfq.select', $pr) }}" method="POST"
          style="flex:1;overflow:hidden;display:flex;flex-direction:column;min-height:0;">

Replace with:

    {{-- Step 1: Method selection --}}
    <div id="sup-step1" style="flex:1;display:flex;flex-direction:column;">
      <div style="padding:24px;display:flex;flex-direction:column;gap:12px;flex:1;">

        <button type="button" onclick="showStep('global')"
          style="width:100%;text-align:left;border:2px solid #e2e8f0;border-radius:12px;padding:18px 20px;cursor:pointer;display:flex;align-items:center;gap:16px;background:#fff;transition:border-color .15s,background .15s;"
          onmouseover="this.style.borderColor='#2563eb';this.style.background='#f8fbff'"
          onmouseout="this.style.borderColor='#e2e8f0';this.style.background='#fff'">
          <div style="width:44px;height:44px;background:#eff6ff;border-radius:10px;display:flex;align-items:center;justify-content:center;font-size:22px;flex-shrink:0;">📦</div>
          <div style="flex:1;">
            <div style="font-size:14px;font-weight:700;color:#0f172a;">Full Order</div>
            <div style="font-size:12px;color:#64748b;margin-top:3px;">One set of suppliers handles the entire purchase request</div>
          </div>
          <svg width="16" height="16" fill="none" stroke="#cbd5e1" stroke-width="2.5" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M9 18l6-6-6-6"/></svg>
        </button>

        <button type="button" onclick="showStep('item')"
          style="width:100%;text-align:left;border:2px solid #e2e8f0;border-radius:12px;padding:18px 20px;cursor:pointer;display:flex;align-items:center;gap:16px;background:#fff;transition:border-color .15s,background .15s;"
          onmouseover="this.style.borderColor='#2563eb';this.style.background='#f8fbff'"
          onmouseout="this.style.borderColor='#e2e8f0';this.style.background='#fff'">
          <div style="width:44px;height:44px;background:#f0fdf4;border-radius:10px;display:flex;align-items:center;justify-content:center;font-size:22px;flex-shrink:0;">🔀</div>
          <div style="flex:1;">
            <div style="font-size:14px;font-weight:700;color:#0f172a;">By Item</div>
            <div style="font-size:12px;color:#64748b;margin-top:3px;">Assign different suppliers to specific items in this request</div>
          </div>
          <svg width="16" height="16" fill="none" stroke="#cbd5e1" stroke-width="2.5" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M9 18l6-6-6-6"/></svg>
        </button>

      </div>
      <div style="padding:14px 24px;border-top:1px solid #f1f5f9;display:flex;justify-content:flex-end;flex-shrink:0;background:#fafafa;">
        <button type="button" onclick="closeSupplierModal()"
          style="padding:8px 18px;border:1px solid #e2e8f0;border-radius:8px;font-size:13px;font-weight:600;color:#64748b;background:#fff;cursor:pointer;">
          Cancel
        </button>
      </div>
    </div>

    {{-- Step 2: Supplier selection --}}
    <div id="sup-step2" style="flex:1;display:none;flex-direction:column;min-height:0;">

    {{-- Single form wrapping both panes --}}
    <form id="sup-form" action="{{ route('purchase.requests.rfq.select', $pr) }}" method="POST"
          style="flex:1;overflow:hidden;display:flex;flex-direction:column;min-height:0;">
  • Step 2: Change sup-mode initial value and close Step 2 wrapper

Find this exact line inside the form (just after @csrf):

      <input type="hidden" name="mode" id="sup-mode" value="global">

Replace with:

      <input type="hidden" name="mode" id="sup-mode" value="">
  • Step 3: Close the Step 2 wrapper after the existing footer

Find this exact block at the end of the modal shell (the original footer + closing divs):

    {{-- Footer --}}
    <div style="padding:14px 24px;border-top:1px solid #f1f5f9;display:flex;align-items:center;justify-content:space-between;flex-shrink:0;background:#fafafa;">
      <div style="font-size:12px;color:#64748b;" id="sup-footer-msg">0 selected</div>
      <div style="display:flex;gap:10px;">
        <button type="button" onclick="closeSupplierModal()"
          style="padding:8px 18px;border:1px solid #e2e8f0;border-radius:8px;font-size:13px;font-weight:600;color:#64748b;background:#fff;cursor:pointer;">
          Cancel
        </button>
        <button type="button" onclick="submitSuppliers()"
          style="padding:8px 22px;background:#2563eb;color:#fff;border:none;border-radius:8px;font-size:13px;font-weight:700;cursor:pointer;">
          Save &amp; Continue →
        </button>
      </div>
    </div>
  </div>
</div>

Replace with:

    {{-- Footer --}}
    <div style="padding:14px 24px;border-top:1px solid #f1f5f9;display:flex;align-items:center;justify-content:space-between;flex-shrink:0;background:#fafafa;">
      <div style="font-size:12px;color:#64748b;" id="sup-footer-msg">0 selected</div>
      <div style="display:flex;gap:10px;">
        <button type="button" onclick="closeSupplierModal()"
          style="padding:8px 18px;border:1px solid #e2e8f0;border-radius:8px;font-size:13px;font-weight:600;color:#64748b;background:#fff;cursor:pointer;">
          Cancel
        </button>
        <button type="button" onclick="submitSuppliers()"
          style="padding:8px 22px;background:#2563eb;color:#fff;border:none;border-radius:8px;font-size:13px;font-weight:700;cursor:pointer;">
          Save &amp; Continue →
        </button>
      </div>
    </div>

    </div>{{-- /sup-step2 --}}
  </div>
</div>
  • Step 4: Verify structure

Confirm the file now has this nesting order:

  1. #supplier-modal → modal shell
  2. Header (#sup-mode-badge-row inside)
  3. #sup-step1 with two card buttons + Cancel footer
  4. #sup-step2 containing #sup-form + the Step 2 footer
  5. #sup-step2 closing tag
  6. Modal shell closing tags

Task 3: Update JavaScript — add showStep/goBack, update openSupplierModal, remove switchSupTab

Files:

  • Modify: resources/views/components/purchase/supplier-select-modal.blade.php (script block)

  • Step 1: Replace openSupplierModal and remove switchSupTab

Find this exact block:

function openSupplierModal() {
  document.getElementById('supplier-modal').classList.add('open');
  switchSupTab('global');
  document.getElementById('sup-search').focus();
}
function closeSupplierModal() {
  closeAllItemDd();
  document.getElementById('supplier-modal').classList.remove('open');
}

Replace with:

function openSupplierModal() {
  document.getElementById('supplier-modal').classList.add('open');
  goBack();
}
function closeSupplierModal() {
  closeAllItemDd();
  document.getElementById('supplier-modal').classList.remove('open');
}
function showStep(method) {
  _supTab = method;
  document.getElementById('sup-mode').value = method === 'item' ? 'by_item' : 'global';

  var badge = document.getElementById('sup-mode-badge');
  if (method === 'global') {
    badge.textContent = '📦 Full Order';
    badge.style.background = '#eff6ff';
    badge.style.color = '#2563eb';
  } else {
    badge.textContent = '🔀 By Item';
    badge.style.background = '#f0fdf4';
    badge.style.color = '#15803d';
  }

  document.getElementById('sup-modal-title').textContent = 'Select Suppliers';
  document.getElementById('sup-modal-subtitle').textContent = 'Choose who receives the quote request';
  document.getElementById('sup-mode-badge-row').style.display = 'flex';
  document.getElementById('sup-step1').style.display = 'none';
  document.getElementById('sup-step2').style.display = 'flex';

  if (method === 'global') {
    setTimeout(function(){ document.getElementById('sup-search').focus(); }, 50);
  }
  updateFooter();
}
function goBack() {
  document.querySelectorAll('#sup-form input[type="checkbox"]:not([disabled])').forEach(function(cb) {
    cb.checked = false;
  });
  document.querySelectorAll('[id^="gchan-"]').forEach(function(el) { el.style.display = 'none'; });
  var searchEl = document.getElementById('sup-search');
  if (searchEl) { searchEl.value = ''; filterGlobalSups(''); }
  document.querySelectorAll('[id^="idd-label-"]').forEach(function(el) {
    el.textContent = 'Select suppliers…';
    el.style.color = '#94a3b8';
  });
  var ics = document.getElementById('item-chan-section');
  if (ics) ics.style.display = 'none';
  document.querySelectorAll('[id^="ic-"]').forEach(function(el) { el.style.display = 'none'; });

  document.getElementById('sup-mode').value = '';
  document.getElementById('sup-modal-title').textContent = 'Request for Quotation';
  document.getElementById('sup-modal-subtitle').textContent = 'How do you want to assign suppliers?';
  document.getElementById('sup-mode-badge-row').style.display = 'none';
  document.getElementById('sup-step1').style.display = 'flex';
  document.getElementById('sup-step2').style.display = 'none';
  closeAllItemDd();
}
  • Step 2: Delete the now-dead switchSupTab function

Find and delete this entire block:

function switchSupTab(tab) {
  _supTab = tab;
  document.getElementById('sup-mode').value = tab === 'item' ? 'by_item' : 'global';

  document.getElementById('sup-global-pane').style.display = tab === 'global' ? 'flex' : 'none';
  document.getElementById('sup-item-pane').style.display   = tab === 'item'   ? 'flex' : 'none';

  var gBtn = document.getElementById('stab-global');
  var iBtn = document.getElementById('stab-item');
  if (tab === 'global') {
    gBtn.style.color = '#2563eb'; gBtn.style.borderBottomColor = '#2563eb';
    iBtn.style.color = '#94a3b8'; iBtn.style.borderBottomColor = 'transparent';
  } else {
    iBtn.style.color = '#2563eb'; iBtn.style.borderBottomColor = '#2563eb';
    gBtn.style.color = '#94a3b8'; gBtn.style.borderBottomColor = 'transparent';
  }
  updateFooter();
}

(Replace with nothing — delete the block entirely.)

  • Step 3: Verify the script block

Confirm:

  • openSupplierModal calls goBack() only
  • showStep and goBack are both defined
  • switchSupTab does not appear anywhere in the file
  • stab-global and stab-item do not appear anywhere in the file

Task 4: Manual verification

Prerequisites: Laravel dev server running (php artisan serve) and Vite running (npm run dev).

  • Step 1: Open a purchase request that has the supplier modal

Navigate to http://localhost:8000/purchase/requests, open any request that has an "Assign Suppliers" or "Send RFQ" button, and click it.

Expected: Modal opens showing Step 1 — two cards ("Full Order", "By Item") and a Cancel button. No tabs visible.

  • Step 2: Test Full Order flow

Click the "Full Order" card.

Expected:

  • Modal transitions to Step 2
  • Header shows "← Change method · 📦 Full Order" badge row
  • Title changes to "Select Suppliers", subtitle to "Choose who receives the quote request"
  • Global supplier list is visible with search input
  • Search input is auto-focused

Select one or more suppliers via checkboxes. Confirm channel toggles appear per supplier selected.

  • Step 3: Test back navigation clears state

Click "← Change method".

Expected:

  • Returns to Step 1 (method cards)
  • Title resets to "Request for Quotation"
  • Mode badge row hidden

Click "Full Order" again.

Expected: All checkboxes unchecked, search cleared — fresh state.

  • Step 4: Test By Item flow

Click "By Item" card.

Expected:

  • Step 2 shows the per-item dropdown UI
  • Mode badge shows "🔀 By Item" in green

Assign a supplier to one item. Click "← Change method". Click "By Item" again.

Expected: Item dropdown labels reset to "Select suppliers…", channel section hidden.

  • Step 5: Test submission still works

Go to Full Order, select one supplier, click "Save & Continue →".

Expected: Form submits to purchase.requests.rfq.select with mode=global and the selected supplier_ids[]. No JS errors in the browser console.

  • Step 6: Test Escape key and backdrop click

Open modal, press Escape.

Expected: Modal closes regardless of which step is active.

Open modal, click outside the modal card.

Expected: Modal closes.


Task 5: Commit

  • Step 1: Stage the file
git add resources/views/components/purchase/supplier-select-modal.blade.php
  • Step 2: Commit
git commit -m "feat: replace supplier modal tabs with two-step wizard"