Skip to content

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.

ItemPath
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:

ItemPath
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, country akan dipindahkan ke Coupon Detail tab. Lihat proposal untuk detail.


File Location

src/modules/coupon/tabs/FocusDashboard.tsx
src/modules/coupon/components/GrantAccessSelector.tsx

UI 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: FocusDashboardResponse

Search 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);
};


Last updated: March 2026

Internal Documentation