Appearance
Focus Dashboard Module
Feature untuk manage akses Focus Dashboard per coupon code.
Source Code References
Untuk Claude: Gunakan path ini untuk akses langsung ke source code.
| Item | Path |
|---|---|
| Main Component | /Users/joko/Documents/projects/naluri-admin/src/modules/coupon/tabs/FocusDashboard.tsx |
| Grant Access Selector | /Users/joko/Documents/projects/naluri-admin/src/modules/coupon/components/GrantAccessSelector.tsx |
| API Queries | /Users/joko/Documents/projects/naluri-admin/src/api/queries/focusDashboard.ts |
| API Mutations | /Users/joko/Documents/projects/naluri-admin/src/api/mutations/focusDashboard.ts |
Backend files:
| Item | Path |
|---|---|
| admin-api Service | /Users/joko/Documents/projects/admin-api/src/focus-dashboard/focus-dashboard.service.ts |
| entitlement Controller | /Users/joko/Documents/projects/entitlement-service/src/analytics/analytics.controller.ts |
| entitlement Service | /Users/joko/Documents/projects/entitlement-service/src/analytics/analytics.service.ts |
Overview
Focus Dashboard memungkinkan admin untuk:
- Grant access ke specific users (members atau internal nalurians)
- Assign users ke specific coupon codes
Note:
clientName,textlineNumber,countryakan dipindahkan ke Coupon Detail tab. Lihat proposal untuk detail.
File Location
src/modules/coupon/tabs/FocusDashboard.tsx
src/modules/coupon/components/GrantAccessSelector.tsxUI Components (Simplified)
FocusDashboard.tsx (Main)
┌─────────────────────────────────────────────────────────────┐
│ Focus Dashboard │
│ Configure dashboard access for this sponsor code │
├─────────────────────────────────────────────────────────────┤
│ Grant Access │
│ Specify which users... ┌──────────────────┐ │
│ │ [Members][Nalurian]│ │
│ │ Search user... │ │
│ │ ┌──────────────┐ │ │
│ │ │ user@email │ │ │
│ │ │ [x] All codes│ │ │
│ │ └──────────────┘ │ │
│ └──────────────────┘ │
└─────────────────────────────────────────────────────────────┘
│ [Save Changes] │
└─────────────────────────────────────────────────────────────┘GrantAccessSelector.tsx
Component untuk search dan select users:
- Tab: Members / Nalurian
- Search input (min 3 chars, debounced 300ms)
- Dropdown untuk select user
- Coupon code assignment per user
Data Types
GrantedEmail
typescript
type GrantedEmail = {
id: string; // User ID from Naluri Core
email: string;
name?: string;
source: 'member' | 'internal';
couponCodeIds: string[]; // 'all' atau specific IDs
};ManageAccessPayload (UPDATED)
typescript
interface ManageAccessPayload {
sponsorCodeId: string; // coupons.id
couponCodeId: string; // coupon_codes.id
action: 'grant' | 'revoke'; // Changed from enable/disable
userIds: string[];
}FocusDashboardResponse (UPDATED)
typescript
interface FocusDashboardUser {
id: string;
email: string;
name?: string;
couponCodeIds: string[];
}
interface FocusDashboardResponse {
sponsorCodeId: string;
users: FocusDashboardUser[];
}API Flow
Load User Access
GET /focus-dashboard/details/{sponsorCodeId}
│
▼
admin-api (proxy)
│
▼
GET /analytics/focus-dashboard/{sponsorCodeId}
│
▼
entitlement-service
│
▼
Response: FocusDashboardResponseSearch Users
GET /focus-dashboard/search-users?email={email}&type={type}
│
▼
admin-api (proxy)
│
▼
GET /analytics/search-users?email={email}&type={type}
│
▼
entitlement-service (query Naluri Core users table)
│
▼
Response: { users: User[] }Save Changes (UPDATED)
POST /focus-dashboard/manage-access
{
sponsorCodeId: "uuid",
couponCodeId: "uuid",
action: "grant" | "revoke",
userIds: ["uuid1", "uuid2"]
}
│
▼
admin-api (proxy)
│
▼
POST /analytics/manage-access
│
▼
entitlement-service
│
▼
Response: { success: true }State Management
typescript
// Local state (simplified)
const [grantedEmails, setGrantedEmails] = useState<GrantedEmail[]>([]);
// Initial state (for dirty checking)
const [initialGrantedEmails, setInitialGrantedEmails] = useState<GrantedEmail[]>([]);
// Dirty check
const isDirty = useMemo(() => {
return JSON.stringify(grantedEmails) !== JSON.stringify(initialGrantedEmails);
}, [grantedEmails, initialGrantedEmails]);Save Logic
typescript
const handleSave = async () => {
// 1. Build mapping: couponCodeId -> userIds
const couponCodeToUsers: Record<string, string[]> = {};
for (const user of grantedEmails) {
const targetCouponCodeIds = user.couponCodeIds.includes('all')
? couponCodeIds // All coupon codes
: user.couponCodeIds;
for (const targetCouponCodeId of targetCouponCodeIds) {
if (!couponCodeToUsers[targetCouponCodeId]) {
couponCodeToUsers[targetCouponCodeId] = [];
}
couponCodeToUsers[targetCouponCodeId].push(user.id);
}
}
// 2. Ensure all coupon codes are included (empty array for unassigned)
for (const couponCodeId of couponCodeIds) {
if (!couponCodeToUsers[couponCodeId]) {
couponCodeToUsers[couponCodeId] = [];
}
}
// 3. Send parallel requests
const requests = Object.entries(couponCodeToUsers).map(
([targetCouponCodeId, userIds]) =>
manageFocusDashboardAccess.mutateAsync({
sponsorCodeId,
couponCodeId: targetCouponCodeId,
action: 'grant',
userIds,
})
);
await Promise.all(requests);
};Related Documentation
Last updated: March 2026