C8 — Search & Filter Patterns
Scope: Standard search and filter UX patterns used across all Ghasi Melmastoon surfaces. Covers the consumer meta-search, in-app list filtering (arrivals board, housekeeping board, folio list, report filter), and mobile search affordances.
State management: All filter state lives in URL (via
nuqson web / React Navigation params on mobile). Search results are server state managed by TanStack Query.
1. Consumer meta-search
1.1 Search bar anatomy
The primary search input on the consumer meta layer (web + mobile):
[Destination] [Check-in] [Check-out] [Guests] [Search]
- Destination field: Free-text with autocomplete. Suggestions from
search-aggregation-service(city, district, property name). Debounce 300 ms. - Date range picker:
<DateRangePicker>from@ghasi/ui-melmastoon. Shows Gregorian + Hijri (configurable by locale). Block past dates. - Guests picker: Compact popover — adult/child count stepper. Min 1 adult. Max per room type (from property config), surfaced as a soft limit.
- Search button: Submits; disables during fetch; shows L-02 inline spinner.
URL state (nuqs):
?dest=Kabul&from=2026-05-01&to=2026-05-03&adults=2&children=0&rooms=1
1.2 Filter panel
Appears below the search bar on results view (collapsed by default on mobile).
| Filter | Type | Values | URL param |
|---|---|---|---|
| Price range | Range slider (dual thumb) | 0 – 99,999 AFN per night | price_min, price_max |
| Star rating | Multi-select chips | 1★ to 5★ | stars (comma-separated) |
| Amenities | Multi-select checkboxes | Halal kitchen, Prayer room, Parking, Pool, Gym, Wi-Fi, AC, Elevator, Family rooms, Pet-friendly | amenities (comma-separated) |
| Property type | Multi-select chips | Guesthouse, Hotel, Serviced apartment, Hostel | prop_type |
| Cancellation | Single select | Free cancellation, Non-refundable, Both | cancel |
| Payment method | Multi-select chips | Cash on arrival, Card, Mobile money | payment |
| Language spoken | Multi-select checkboxes | Pashto, Dari, English, Uzbek, Arabic, Tajik | lang |
Filter behavior:
- Each filter change triggers an immediate re-search (debounced 150 ms for sliders).
- Active filters shown as dismissible chips above the results list.
- "Clear all filters" button appears when any filter is active.
- Filter count badge on mobile filter toggle button.
1.3 Sort order
| Sort | URL value | Default |
|---|---|---|
| Price (low to high) | price_asc | — |
| Price (high to low) | price_desc | — |
| Distance from center | distance | ✅ Default for city searches |
| Guest rating | rating_desc | — |
| Popularity | popularity | Default for "trending" |
Sort select exposed as a <Select> above the results list (desktop) or bottom sheet (mobile).
1.4 Map search
- Map view: Leaflet + OpenStreetMap tiles.
- "Search this area" button appears when user pans/zooms; triggers a bounding-box search (
bboxquery param). bboxformat:lng_min,lat_min,lng_max,lat_max.- Active bbox shown as a dotted rectangle overlay on the map.
- Pins cluster when > 20 in view (
leaflet.markercluster). - Pin tooltip: property name + min rate.
2. Backoffice list search (operator desktop)
2.1 Arrivals board search / filter
| Filter | Type | Behavior |
|---|---|---|
| Guest name | Text search | Client-side filter on loaded data, debounce 200 ms |
| Room number | Text / numeric | Exact match |
| Arrival date | Date picker | Filter by check-in date; default: today |
| Reservation status | Multi-select chips | Confirmed, Checked-in, No-show, Cancelled |
| Payment method | Multi-select | Cash, Card, Cash deposit |
Results update in real-time from the local SQLite query as the user types.
2.2 Housekeeping board search / filter
| Filter | Type |
|---|---|
| Room number | Text |
| Floor | Select |
| Status | Multi-select chips (Dirty / Cleaning / Clean / OOO / OOS) |
| Assigned to | Staff select |
| Priority | High / Normal / Low chips |
Housekeeping board is not paginated — all rooms for the property load in one SQLite query (< 500 rooms per property).
2.3 Folio / charge search
| Filter | Type |
|---|---|
| Date range | Date range picker |
| Charge category | Multi-select |
| Amount range | Range slider |
| Status | Multi-select (Posted / Voided / Settled) |
Paginated: 50 per page, cursor-based, client-side pagination on local SQLite.
3. Mobile search patterns
3.1 Full-screen search
On mobile (consumer and staff), search is a full-screen bottom sheet:
- Search bar at top.
- Below: recent searches (last 5), then suggestions.
- On input: autocomplete results replace suggestions.
- Submit via keyboard "Go" / "Search" action.
3.2 Filter bottom sheet
- Triggered by "Filter" chip at top of results.
- Full-height bottom sheet with filter groups.
- "Apply" button at bottom applies all filters atomically.
- "Reset" button clears all filters.
4. Search result rendering
4.1 Property card (consumer)
[Photo]
[Property name]
[Rating • Star count]
[Amenity icons row]
[From AFN X,XXX / night]
[Availability summary]
[Book / View →]
- Photos: lazy-loaded with LQIP placeholder.
- Availability summary: "X rooms left" or "Available" (not exact inventory — privacy).
- Rate: lowest available rate for the search period; "From" prefix.
- RTL: card layout flips (photo on right, text on left in RTL).
4.2 Map pin
[AFN X,XXX] ← clickable
- Selected pin expands to a popover with mini card.
- Popover contains: name, rating, min rate, "View" link.
4.3 Pagination
- Web: infinite scroll with IntersectionObserver + React Query
fetchNextPage. - Mobile: FlatList with
onEndReached. - Max 200 results per query (enforced by
search-aggregation-service). - "Load more" button as fallback for users who prefer it.
5. Telemetry
| Event | When | Key properties |
|---|---|---|
mel.consumer.search.executed | Search submitted | destination, dates, result_count, latency_ms |
mel.consumer.search.filter_applied | Filter added/removed | filter_key, filter_value, result_count_after |
mel.consumer.search.sort_changed | Sort changed | sort_key, result_count |
mel.consumer.search.no_results | 0 results | destination, filter_count |
Full schema in C1 — Telemetry Event Dictionary §2.1.
6. Accessibility
- All filter controls are keyboard-navigable.
- Active filter chips have a visible ✕ button with
aria-label="Remove {filter_name} filter". - "Clear all filters" is a visible button, not hidden behind a dropdown.
- Range sliders: two-thumb with ARIA
role="slider",aria-valuemin,aria-valuemax,aria-valuenow. - Map view: keyboard-navigable pins (
tabindex=0,role="button"on each marker),aria-label="<property name>, AFN X,XXX per night". - Map is not the only way to discover properties (list view always available).
7. Open Questions
- Price range filter: should the slider use AFN (local currency) or the user's preferred display currency? Currently AFN only.
- Guest-language filter: should it filter on
property.staffLanguages(languages the staff speak) orproperty.amenities.languages(languages the property supports overall)? - Arrivals board: should name search be fuzzy (trigram similarity) or exact prefix? Fuzzy adds SQLite FTS5 requirement.