TeeTime Benefits & Reciprocity Admin UI — Specification
Status: Implemented (v1.2) Owners: TeeTime Eng Last Updated: 2025-12-06
Goal
Provide an admin UI for managing reciprocity agreements, network memberships, home-club mappings, and eligibility previews so operators can configure and validate benefits without manual API calls.
Scope
In scope:
- TeeTime Admin app (web)
- CRUD for reciprocity agreements (Network + Bilateral) with rate configs
- Network membership management (add/remove clubs from networks)
- Home-club map editing/validation per provider
- Eligibility preview/testing surface
- Diagnostics combining home-club map warnings with preview results
Out of scope:
- Corporate benefit flows (Nedbank/Discovery) UI
- End-user/mobile benefit presentation
- Audit log backend (future enhancement)
- Bulk import/export of agreements
Endpoints
Available (backend implemented)
| Method | Path | Description |
|---|---|---|
| GET | /admin/reciprocity/agreements | List agreements with filters |
| GET | /admin/reciprocity/agreements/:id | Get single agreement |
| POST | /admin/reciprocity/agreements | Create agreement |
| PUT | /admin/reciprocity/agreements/:id | Update agreement |
| DELETE | /admin/reciprocity/agreements/:id | Delete agreement |
| GET | /admin/reciprocity/networks/memberships | List network memberships |
| PUT | /admin/reciprocity/networks/memberships | Upsert membership |
| DELETE | /admin/reciprocity/networks/:networkCode/clubs/:clubId | Remove membership |
| GET | /admin/reciprocity/home-clubs/:providerCode | Get home-club map diagnostics |
| PUT | /admin/reciprocity/home-clubs/:providerCode | Update home-club map |
| POST | /admin/reciprocity/preview | Preview eligibility |
Additional (implemented beyond original spec)
| Method | Path | Description |
|---|---|---|
| GET | /admin/benefits/providers | List provider codes for dropdowns |
| GET | /admin/reciprocity/diagnostics | Combined policy + provider + home-club diagnostics |
| GET | /admin/reciprocity/agreements/:id/audit | Get audit history for agreement (v1.2) |
UX Surfaces
1. Navigation
Benefits & Reciprocity entry in Admin sidebar under "Configuration" section.
Admin
├── Dashboard
├── Clubs
├── Configuration
│ ├── Benefits & Reciprocity ← NEW
│ ├── Rate Rules
│ └── Specials
└── ...
2. Agreements Tab (default)
Layout: Filter bar + data table + slide-out drawer for create/edit.
Filters:
| Filter | Type | Options |
|---|---|---|
| Type | Select | All / Network / Bilateral |
| Network | Autocomplete | Network codes from memberships |
| Club | Autocomplete | Club search by name/slug |
| Status | Toggle | Active / Inactive / All |
Table Columns:
| Column | Description | Sortable |
|---|---|---|
| Type | Badge: NETWORK or BILATERAL | Yes |
| Parties | Network code or "Club A ↔ Club B" | No |
| Direction | BOTH / A→B / B→A | No |
| Discount | Summary: "15%" or "R250 fixed" | No |
| Priority | Number (lower = higher priority) | Yes |
| Valid | Date range or "Ongoing" | Yes |
| Status | Active/Inactive toggle | No |
| Updated | Relative timestamp | Yes |
Row Actions:
- Edit → Opens drawer with form
- Clone → Opens drawer pre-filled (clears ID)
- Delete → Confirmation modal
Create/Edit Drawer Fields:
┌──────────────────────────────────────── ─────┐
│ [×] Create Agreement │
├─────────────────────────────────────────────┤
│ Type* [Network ▾] │
│ │
│ ┌─ Network Agreement ─────────────────────┐ │
│ │ Network Code* [SAGA_NETWORK ▾] │ │
│ └─────────────────────────────────────────┘ │
│ │
│ OR │
│ │
│ ┌─ Bilateral Agreement ───────────────────┐ │
│ │ Club A* [Randpark Golf Club ▾] │ │
│ │ Club B* [Glendower Golf Club ▾] │ │
│ │ Direction [Both ▾] │ │
│ └─────────────────────────────────────────┘ │
│ │
│ ─── Validity ───────────────────────────── │
│ Start Date* [2025-01-01] │
│ End Date [________] (optional) │
│ Active [✓] │
│ │
│ ─── Rate Configuration ─────────────────── │
│ Discount Type* [Percent ▾] │
│ Value* [15] % │
│ Priority [100] │
│ │
│ ─── Restrictions (optional) ────────────── │
│ Valid Days [☑Mon ☑Tue ☑Wed ☑Thu ☐Fri...│
│ Time Window [06:00] to [14:00] │
│ Blackout Dates [+ Add date] │
│ Handicap Range [0] to [24] │
│ │
│ ─── Preview ────────────────────────────── │
│ ┌──────────────────────────────────────┐ │
│ │ Members get 15% off visitor rates. │ │
│ │ Priority 100 (applies after p<100). │ │
│ └──────────────────────────────────────┘ │
│ │
│ [Cancel] [Save Agreement] │
└─────────────────────────────────────────────┘
3. Networks Tab
Layout: Accordion per network code with member list.
┌─────────────────────────────────────────────┐
│ Networks [+ Add Network] │
├─────────────────────────────────────────────┤
│ ▼ SAGA_NETWORK (12 clubs) │
│ ┌──────────────────────── ─────────────┐ │
│ │ Club │ Status │ Action │ │
│ ├───────────────────┼────────┼────────┤ │
│ │ Randpark Golf │ Active │ [🗑] │ │
│ │ Glendower Golf │ Active │ [🗑] │ │
│ │ Steenberg Golf │ Inactive│ [🗑] │ │
│ └─────────────────────────────────────┘ │
│ [+ Add Club to SAGA_NETWORK] │
│ │
│ ▶ OPEN_FAIRWAYS (8 clubs) │
│ ▶ ERNIE_ELS_TRAIL (5 clubs) │
└─────────────────────────────────────────────┘
Add Membership Modal:
- Club selector (autocomplete)
- Active toggle (default: true)
4. Home-Club Map Tab
Layout: Provider selector + key-value editor + diagnostics panel.
┌─────────────────────────────────────────────┐
│ Home-Club Mappings │
├─────────────────────────────────────────────┤
│ Provider: [SAGA_NETWORK ▾] [Refresh] │
│ │
│ ┌─ Mappings ────────────────────────────┐ │
│ │ Provider Code │ TeeTime Club │ │
│ ├──────────────────┼────────────────────┤ │
│ │ RANDPARK │ randpark-firethorn │ │
│ │ RPGC │ randpark-bushwillow│ │
│ │ GLENDOWER │ glendower │ │
│ │ [+ Add mapping] │ │
│ └───────────────────────────────────────┘ │
│ │
│ ┌─ Diagnostics ─────────────────────────┐ │
│ │ ⚠ 2 warnings │ │
│ │ • STEENBERG: No matching club found │ │
│ │ • RPGC: Mapped to inactive club │ │
│ │ │ │
│ │ ✓ 15 mappings resolved successfully │ │
│ └───────────────────────────────────────┘ │
│ │
│ [Discard Changes] [Save Map] │
└─────────────────────────────────────────────┘
5. Eligibility Preview Tab
Layout: Input form + results panel (side-by-side on desktop).
┌──────────────────────┬──────────────────────┐
│ Test Eligibility │ Results │
├──────────────────────┼─────── ───────────────┤
│ Tenant* [______] │ │
│ Club* [______] │ ┌─ Eligibility ────┐ │
│ Member #* [______] │ │ ✓ ELIGIBLE │ │
│ │ │ │ │
│ Provider [SAGA_..] │ │ Base: R500.00 │ │
│ Home Club [______] │ │ Disc: -R75.00 │ │
│ │ │ Final: R425.00 │ │
│ Tee Date [______] │ └──────────────────┘ │
│ Tee Time [__:__] │ │
│ Handicap [__] │ ┌─ Applied ────────┐ │
│ │ │ 1. SAGA_NETWORK │ │
│ Stacking: │ │ 15% off │ │
│ ○ Best Price │ │ -R75.00 │ │
│ ● Stack All │ └──────────────────┘ │
│ │ │
│ [Run Preview] │ [Copy as JSON] │
└──────────────────────┴──────────────────────┘
Result States:
- Eligible: Green checkmark, shows discount breakdown
- Not Eligible: Red X, shows reason (e.g., "No matching agreement", "Blackout date")
- Loading: Skeleton loader
- Error: Red alert with API error message
Functional Rules
Agreement Validation
| Type | Required Fields | Validation |
|---|---|---|
| BILATERAL | clubAId, clubBId, startDate | clubA ≠ clubB |
| NETWORK | networkCode, startDate | networkCode must exist |
Rate Configuration
| Discount Type | Required Field | Example |
|---|---|---|
| PERCENT | discountValue (0-100) | 15% off |
| FIXED_AMOUNT | discountValue (cents) | R50 off |
| FIXED_RATE | fixedRateCents | R250 flat rate |
| RATE_TIER | rateTierCode | Use "MEMBER" tier |
Priority & Stacking
- Priority: Lower number = higher priority (default: 100)
- BEST_PRICE mode: Applies single best discount
- STACK mode: Applies bilateral agreements in priority order, then network agreement
Data Formats
| Field | Format | Example |
|---|---|---|
| Times | HH:mm (24h) | 06:00, 14:30 |
| Dates | ISO 8601 | 2025-12-25 |
| Days of week | 3-letter codes | MON, TUE, WED |
Validation & Error UX
Client-side Validation
- Required field indicators (
*) - Inline validation messages on blur
- Disable submit until form valid
- Type-specific field visibility (show networkCode OR club selectors)
API Error Handling
- Preserve form state on failure
- Show error banner with message
- Highlight affected fields when possible
- Retry button for network errors
Destructive Actions
- Delete agreement: "Delete agreement between [parties]? This cannot be undone."
- Remove membership: "Remove [club] from [network]?"
- Clear home-club map: Confirmation required
Success Feedback
- Toast notification on save/delete
- Table row highlight animation on create
- Auto-close drawer on successful save
Accessibility
- All form inputs labeled with
aria-labelor visible labels - Focus management: trap focus in modals/drawers
- Keyboard navigation: Tab through form, Escape to close
- Screen reader announcements for state changes
- Color-blind safe status indicators (icons + color)
- Minimum touch target: 44×44px on mobile
Keyboard Shortcuts (v1.1)
| Shortcut | Action | Context |
|---|---|---|
/ | Focus search input | Agreements tab |
Ctrl/Cmd+N | Open create drawer | Agreements tab |
Escape | Close drawer/modal | Any |
Enhancements (v1.1)
Features implemented beyond original spec:
Optimistic UI
- Inline toggle: Status switches update immediately, rollback on failure
- Undo delete: Toast with "Undo" button for 5 seconds after deletion
- Row highlight: Newly created rows highlight blue and auto-scroll into view
Conflict Detection
- Overlap warnings: Form shows Alert when date range overlaps existing agreement for same network/club pair
- Duplicate club prevention: Bilateral agreements validate Club A ≠ Club B inline
Filter Enhancements
- Removable pills: Active filters shown as closable tags below filter bar
- Network/Club filters: Autocomplete dropdowns for precise filtering
- "Clear filters" button: One-click reset when filters active
Form Improvements
- Day presets: Quick buttons for "All days", "Weekdays", "Weekends"
- Time/blackout summaries: Inline text showing selected restrictions
- End-date validation: Prevents end date before start date
Home-Club Map
- Per-row diagnostics: Warning icons on individual mapping rows
- Issue counts: Badge showing total warnings per provider
Diagnostics Tab
- Dedicated tab for combined diagnostics view (bonus surface)
Bulk Operations (v1.1, enhanced v1.2)
- Multi-select: Checkbox column for row selection
- Bulk toggle: Enable/disable multiple agreements at once (with loading state)
- Bulk delete: Delete multiple agreements with confirmation
- Selection bar: Shows count with Activate/Deactivate/Delete/Clear buttons
- Optimistic updates: Bulk ops update UI immediately, rollback on failure
Import/Export (v1.2)
- Export CSV: Exports filtered agreements to CSV file (
benefits-agreements.csv) - Import CSV: Modal with text input, preview parsing, header mapping
- CSV format:
id,type,networkCode,clubAId,clubBId,discountType,discountValue,priority,
startDate,endDate,isActive,validDays,timeWindowStart,timeWindowEnd,blackoutDates - Multi-value fields:
validDaysandblackoutDatesuse;separator
Audit History (v1.2)
- History action: Per-row "History" menu item in actions dropdown
- History drawer: Shows timeline of changes (create, update, delete)
- Audit entry fields: action, actor, timestamp, details, changes (JSON diff)
- Endpoint:
GET /admin/reciprocity/agreements/:id/audit - Error states: Loading spinner, error alert if backend unavailable
Component Library
Use existing Ant Design components from teetime-admin:
| Component | Usage |
|---|---|
Table | Agreements list with sorting/filtering |
Drawer | Create/edit agreement form |
Form / Form.Item | All form fields with validation |
Select / AutoComplete | Club/network selectors |
DatePicker | Start/end dates, blackout dates |
TimePicker | Time window fields |
Switch | Active toggles |
Checkbox.Group | Days of week selector |
InputNumber | Priority, handicap, discount values |
Tag | Type badges, status indicators |
Collapse | Network accordions |
Alert | Diagnostics warnings |
Modal | Confirmation dialogs |
message | Success/error toasts |
Telemetry
Events to Track
| Event | Properties |
|---|---|
benefits_admin.agreement.create | type, hasRateConfig, success |
benefits_admin.agreement.update | agreementId, changedFields, success |
benefits_admin.agreement.delete | agreementId, type |
benefits_admin.membership.add | networkCode, clubId |
benefits_admin.membership.remove | networkCode, clubId |
benefits_admin.homeclubmap.update | providerCode, mappingCount, warningCount |
benefits_admin.preview.run | applicable, stackingMode, appliedCount, durationMs |
Metrics
- Time to complete agreement creation flow
- Preview API latency (p50, p95)
- Error rate by operation type
Testing
Unit Tests
- Form validation helper functions
- DTO transformation (API ↔ form values)
- Stacking priority calculation display
- Date/time formatting utilities
React Testing Library
describe('AgreementsTable', () => {
it('renders agreements with correct badges');
it('filters by type when selected');
it('calls delete API with confirmation');
});
describe('AgreementDrawer', () => {
it('shows network fields when type=NETWORK');
it('shows bilateral fields when type=BILATERAL');
it('validates required fields before submit');
it('displays rate config summary');
});
describe('EligibilityPreview', () => {
it('shows eligible state with discount breakdown');
it('shows ineligible state with reason');
it('displays applied agreements list');
});
describe('HomeClubMap', () => {
it('renders diagnostics warnings');
it('validates mappings before save');
});
E2E Tests (Playwright) — Implemented (v1.1)
Location: apps/teetime/teetime-admin-e2e/src/benefits-admin.e2e.ts
test('loads benefits admin and filters agreements', async ({ page }) => {
// Stubs reciprocity APIs with mock data
// Navigates to /benefits
// Verifies Agreements tab loads with SAGA_NETWORK agreement
// Tests network filter selection
// Opens and closes create drawer
});
Run: pnpm nx run teetime-admin-e2e:e2e -- --project=chromium
Planned additional E2E tests:
- Create network agreement → verify in table
- Add club to network → verify membership count
- Run eligibility preview → verify discount applied
- Update home-club map → verify diagnostics refresh
Rollout
Feature Flag
// Gate on nav entry and routes
if (featureFlags.ENABLE_BENEFITS_ADMIN) {
router.addRoute('/admin/benefits', BenefitsAdminPage);
sidebar.addItem({ label: 'Benefits & Reciprocity', path: '/admin/benefits' });
}
Phased Rollout
- Phase 1: Internal testing (DigiWedge staff only)
- Phase 2: Pilot clubs (SAGA network operators)
- Phase 3: General availability
Documentation Links
- Help icon in header →
/docs/benefits/admin - "Learn more" links in form sections →
/docs/benefits/reciprocity - Error states include link to troubleshooting guide
Open Questions
Bulk operations: Should we support bulk enable/disable of agreements?— Implemented v1.1History/audit: Show last-modified-by and change history?— Implemented v1.2Import/export: CSV import for home-club mappings?— Implemented v1.2 (agreements import/export)- Notifications: Alert when agreements expire soon?
- Home-club map import: CSV import for home-club mappings (agreements done, mappings pending)
Revision History
| Date | Author | Changes |
|---|---|---|
| 2025-12-05 | Rudi | Initial draft with wireframes |
| 2025-12-05 | Rudi | Added accessibility, component library, enhanced testing |
| 2025-12-06 | Rudi | v1.1: Keyboard shortcuts, optimistic UI, conflict detection, filter pills, day presets, bulk operations, CSV import, E2E tests |
| 2025-12-06 | Rudi | v1.2: CSV export, import modal with preview, audit history drawer, bulk action loading states, selection bar UX |