# ROForm Service Catalog Search

## Overview

`src/components/ROForm.tsx` — the shared create/edit form used by both `ROModal` and `RODetailsPanel` — includes a service catalog search box above the manual service rows. This allows advisors to search the PocketBase `services` collection and add catalog items to the RO with one click, instead of manually typing name/labor/rate/parts for every service.

**Implemented:** 2026-07-08. See execution memo in `roadmap_5.2.md`.

## Files

- `src/components/ROForm.tsx` — main form component (shared between create modal and inline edit)
- `src/components/repairOrders/ROModal.tsx` — create/edit modal wrapper (inherits form change automatically)
- `src/pages/RepairOrders.tsx` — inline edit panel via `RODetailsPanel` (also inherits)

## State

```tsx
const [catalogServices, setCatalogServices] = useState<ServiceItem[]>([]);
const [catalogSearch, setCatalogSearch] = useState('');
const [catalogLoaded, setCatalogLoaded] = useState(false);
```

## Lazy Load

Catalog services are fetched **only on first search box focus** (`onFocus={handleCatalogFocus}`), not on component mount. This avoids an unnecessary PocketBase query every time the modal opens.

```tsx
const handleCatalogFocus = () => {
  if (!catalogLoaded) fetchCatalogServices();
};
```

## Fetch

Same pattern as `RODetailModal.tsx`:

```tsx
const fetchCatalogServices = useCallback(async () => {
  if (!userId || catalogLoaded) return;
  const records = await pb.collection('services').getFullList({
    filter: `userId="${userId}"`,
    sort: 'name',
  });
  setCatalogServices(records.map(...));
  setCatalogLoaded(true);
}, [userId, catalogLoaded]);
```

## Add from Catalog

Mirrors `RODetailModal.tsx:799` — detects split vs full pricing:

```tsx
const handleAddCatalogService = (svc: ServiceItem) => {
  const laborPrice = (svc.laborHours || 0) * (svc.laborRate || 0);
  const isSplit = laborPrice > 0 || (svc.partsCost || 0) > 0;
  setServices((prev) => [...prev, {
    id: `catalog-${svc.id}-${Date.now()}`,
    name: svc.name,
    description: svc.description,
    laborHours: isSplit ? svc.laborHours || 1 : 0,
    laborRate: isSplit ? svc.laborRate || DEFAULT_SHOP_RATE : 0,
    partsCost: isSplit ? svc.partsCost : 0,
    total: svc.price,
    status: 'pending' as const,
    technician: '',
    pricingMode: isSplit ? 'split' : 'full',
    applyShopCharge: true,
  }]);
};
```

## Filtered Results

Same search logic as RODetailModal — filters on name, category, and description:

```tsx
const filteredCatalog = catalogServices.filter((s) => {
  if (!catalogSearch.trim()) return true;
  const q = catalogSearch.toLowerCase();
  return (
    s.name.toLowerCase().includes(q) ||
    s.category.toLowerCase().includes(q) ||
    s.description.toLowerCase().includes(q)
  );
});
```

## UI Layout

Inserted between the "Services" heading and the `ServiceRow` list:

1. **Search input** with Search icon, lazy-loads on focus
2. **Scrollable results** (`max-h-48`) showing name + category + price + green "Add" button
3. **Empty state hint** when search yields no results: "Use Add Manual Service above"
4. **Manual button** renamed from "Add Service" to "Add Manual Service" to clarify the two paths

## Pitfalls

- The catalog search uses the same `pb.collection('services')` query as `RODetailModal.tsx` and `ServiceCatalog.tsx` — if you add new fields to the `services` collection, update the mapping in all three places.
- `handleAddCatalogService` pushes to the same `services` state array that `ServiceRow` renders — no additional plumbing needed.
- The existing `handleSubmit` serializer already filters services with `s.name?.trim()` and maps them to `ROService` — catalog-added services have names, so they flow through correctly.
