Files
hermes-config/skills/self-hosting/shop-pro-quote/references/pricing-toggle.md
T
2026-07-12 10:17:17 -04:00

4.4 KiB

Split/Full Pricing Toggle

Where It Lives

Three separate components — each has its own copy:

Location Component File
Quote Generator ServiceRow (memo) src/components/quoteGenerator/ServiceRow.tsx
RO Create/Edit ServiceRow (inline) src/components/ROForm.tsx
RO Detail Modal EditableServiceRow (inline) src/pages/RODetailModal.tsx

All three share the same toggle pattern. When adding the toggle to a new service row, copy the pattern from any existing one.

Type Fields

Added to Service / QuoteService / ROService in src/types.ts:

pricingMode?: 'full' | 'split';
laborHours?: number;
laborRate?: number;
partsCost?: number;

ROService already had pricingMode and split fields. QuoteService extended Service to add them.

Toggle Pattern

A pill-button group rendered in the row header:

<div className="flex rounded-lg border border-gray-300 dark:border-gray-600 overflow-hidden text-xs">
  <button
    type="button"
    onClick={() => {
      if (isSplit) {
        onChange(index, 'pricingMode', 'full');
      }
    }}
    className={`px-2.5 py-1 font-medium transition-colors ${
      !isSplit
        ? 'bg-blue-600 text-white'
        : 'bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-600'
    }`}
  >
    Full
  </button>
  <button
    type="button"
    onClick={() => {
      if (!isSplit) {
        const flatTotal = service.total || total;
        const rate = defaultLaborRate;
        const hours = rate > 0 ? Math.round((flatTotal / rate) * 100) / 100 : 0;
        onChange(index, 'pricingMode', 'split');
        onChange(index, 'laborHours', hours);
        onChange(index, 'laborRate', rate);
        onChange(index, 'partsCost', 0);
      }
    }}
    className={`px-2.5 py-1 font-medium transition-colors ${
      isSplit
        ? 'bg-blue-600 text-white'
        : 'bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-600'
    }`}
  >
    Split
  </button>
</div>

State Derivation

const isSplit = service.pricingMode === 'split'; // default Full

When pricingMode is undefined (existing data without the field), it defaults to Full.

Mode Switch Behavior

Switch What Happens
Full → Split Derives laborHours = flatten Total / laborRate from the flat price. Sets laborRate = defaultLaborRate, partsCost = 0
Split → Full Just sets pricingMode = 'full'. Split field values persist in state but are hidden

Full Mode Display

  • Grid collapses to 2 columns: Name + Total Price (editable input for service.total)

Split Mode Display

  • Grid shows 5 columns: Name, Labor Hrs, Rate/hr, Parts $, Total (read-only, calculated)

Total Calculation

roServiceTotal() in src/lib/totals.ts:

export function roServiceTotal(s): number {
  const split = (s.laborHours || 0) * (s.laborRate || 0) + (s.partsCost || 0);
  return split || s.total || 0;
}

For Full mode: hours=0 → split=0 → falls back to s.total. Every call site must pass total: s.total || 0 to roServiceTotal — otherwise Full-mode services compute to $0.

Where Total Must Be Passed

Search the codebase for roServiceTotal call sites and ensure total is in the argument:

  • ROForm.tsx ServiceRow render — the total const used for display
  • ROForm.tsx computeROTotals mapper
  • ROForm.tsx handleSubmit cleanedServices mapper
  • RODetailModal.tsx EditableServiceRow render
  • RODetailModal.tsx service cleanup on save

Initial Defaults

Every place that creates a new service must set pricingMode: 'full':

Location File Line
ROForm edit path fallback ROForm.tsx ~385
ROForm create path ROForm.tsx ~413
ROForm handleAddService ROForm.tsx ~553
RODetailModal handleAddService RODetailModal.tsx ~822
RODetailModal createEmptyServiceForm RODetailModal.tsx 57-67 (already 'full')
QuoteGenerator ServiceSearch handleAdd ServiceSearch.tsx 56 (addService passes defaults)

Persistence

When saving services back to PocketBase, pricingMode must be included in the cleaned service object:

pricingMode: (s.pricingMode || 'full') as 'full' | 'split',

This is in:

  • ROForm.tsx handleSubmit cleanedServices
  • RODetailModal.tsx service save handler