Appearance
Coupon Module
Module untuk manage coupons/sponsor codes di Naluri.
Overview
Coupon module menampilkan dan mengelola:
- Coupon/Sponsor codes
- Trial periods
- Member assignments
- Blood test configurations
- Domain whitelists
- Focus Dashboard access
File Structure
src/modules/coupon/
├── Coupon.tsx # Parent layout dengan tab navigation
├── CouponList.tsx # Listing page dengan search/filter
├── tabs/
│ ├── Details.tsx # Coupon details & trial management
│ ├── AllMember.tsx # All members list
│ ├── Groups.tsx # Member groups
│ ├── Onboarding.tsx # Domain whitelist & blood test
│ └── FocusDashboard.tsx # Focus Dashboard access
├── components/
│ ├── CouponTable.tsx # Data table component
│ ├── EditDependentCouponCodeModal.tsx
│ ├── FocusDashboardToggle.tsx
│ └── GrantAccessSelector.tsx
└── store/
└── Coupon.store.ts # Type definitionsData Types
CouponCodeDetails
typescript
interface CouponCodeDetails {
id: string; // coupons.id (sponsorCodeId)
name: string; // coupon_codes.name
couponId: string; // Reference ke parent coupon
sponsor: string; // Sponsor name
planId: string;
active: boolean;
couponCodes?: Array<{
couponId: string;
id: string; // coupon_codes.id (couponCodeId)
name: string;
}>;
plan: {
id: string;
title: string;
description: string;
priceCents: number;
currency: string;
recurrence: number;
active: boolean;
trialDays: number;
private: boolean;
subscriptions: Subscription[];
};
}Tab Components
Details Tab
File: tabs/Details.tsx
| Feature | API Endpoint | Description |
|---|---|---|
| View coupon info | GET /coupon/show/{id} | Load coupon details |
| Update trial date | PUT /coupon/update-end-date | Change trial period |
| Dependent codes | GET/PUT /coupon/dependent-sponsor-code/{id} | Manage dependent relationships |
Onboarding Tab
File: tabs/Onboarding.tsx
| Feature | API Endpoint | Description |
|---|---|---|
| Domain whitelist | GET/PUT /coupon/{code}/domain-whitelists | Manage allowed domains |
| Blood test availability | GET/POST/PUT/DELETE /coupon/{id}/blood-test-availability | Configure periods |
| Blood test locations | GET/POST/PUT/DELETE /coupon/{id}/blood-test-locations | Manage locations |
Focus Dashboard Tab
File: tabs/FocusDashboard.tsx
| Feature | API Endpoint | Description |
|---|---|---|
| Get details | GET /focus-dashboard/details/{sponsorCodeId} | Load current config |
| Search users | GET /focus-dashboard/search-users | Find members/nalurians |
| Manage access | POST /focus-dashboard/manage-access | Enable/disable access |
API Hooks
Queries (GET)
typescript
// src/api/queries/coupon.ts
useCouponList() // List all coupons
useCouponDetail(couponId) // Single coupon details
useGetDependentCouponCode(id) // Dependent code config
// src/api/queries/focusDashboard.ts
useFocusDashboardDetails(sponsorCodeId)
useSearchFocusDashboardUsers(email, type)Mutations (POST/PUT/DELETE)
typescript
// src/api/mutations/coupon.ts
useUpdateCouponEndDate()
useUpdateDependentSponsorCode()
useUpdateCouponDomainWhitelists()
// src/api/mutations/focusDashboard.ts
useManageFocusDashboardAccess()Navigation Flow
CouponList
│
│ click row
▼
Coupon.tsx (parent layout)
│
├── useCouponDetail(couponId) ──▶ Fetch data
│
└── <Outlet context={couponData} />
│
├── Details tab
├── AllMember tab
├── Groups tab
├── Onboarding tab
└── FocusDashboard tabKey Patterns
Outlet Context
typescript
// Parent (Coupon.tsx)
<Outlet context={{ ...couponData, setCSVData }} />
// Child tab
const context = useOutletContext<CouponCodeDetails>();
const sponsorCodeId = context.id;Form State Management
typescript
// Track changes
const [enabled, setEnabled] = useState(false);
const [initialEnabled, setInitialEnabled] = useState(false);
const isDirty = useMemo(() => {
return enabled !== initialEnabled;
}, [enabled, initialEnabled]);
// Only save if dirty
<Button disabled={!isDirty} onClick={handleSave}>
Save Changes
</Button>Batch Operations
typescript
// Save multiple coupon codes at once
const requests = Object.entries(couponCodeToUsers).map(
([couponCodeId, userIds]) =>
manageFocusDashboardAccess.mutateAsync({
sponsorCodeId,
couponCodeId,
action: enabled ? 'enable' : 'disable',
userIds,
})
);
await Promise.all(requests);Last updated: March 2026