Skip to content

API Integration

Hermina 360 uses two data sources:

  1. Hermina 360 REST API — application backend (auth, user management, future CRUD)
  2. Honeyframe Data API — read-only analytics/BI data source (dashboard metrics)

1. Hermina 360 REST API

Axios Instances

File: src/lib/api.ts

Two axios instances:

InstancePurposeBase URLAuth
restApiApp backend (auth, CRUD)VITE_API_URLBearer token from useAuthStore (auto-injected)
hfApiHoneyframe BI dataVITE_HF_PROXY_BASE or /hf-apiBearer token from VITE_HF_TOKEN (static)
ts
import { restApi, hfApi } from '@/lib/api';

restApi config:

  • Timeout: 30s
  • Request interceptor: injects Authorization: Bearer {accessToken} from Zustand auth store
  • Response interceptor: 401 → clears auth + redirects to /login

hfApi config:

  • No timeout (some HF queries are slow)
  • Static bearer token from env var
  • Dev: Vite proxy /hf-apihospital.hubstudio.id (bypasses CORS)
  • Prod: set VITE_HF_PROXY_BASE to your reverse-proxy path

Legacy alias: src/lib/axios.ts re-exports restApi as default — use api.ts for new code.

Response Types

File: src/types/api.ts

ts
interface ApiResponse<T> {
  success: boolean;
  code: number;
  status: string;
  message: string;
  alert: 'success' | 'error' | 'warning' | 'info';
  data: T;
  pagination?: ApiPagination;
  errors?: string[];
}

interface ApiResource<T> {
  id: string;
  type: string;
  attributes: T;
}

interface ApiPagination {
  count: number;
  page: number;
  limit: number;
  offset: number;
  last: number;
  from: number;
  to: number;
  prev: number | null;
  next: number | null;
}

interface FetchError {
  errorMessage: string;
  typeAlert: 'success' | 'error' | 'warning' | 'info';
  errors?: string[];
}

Auth Endpoints

POST /auth/login
Body: { email: string; password: string }
Response: { accessToken, user: { id, email, name, role, avatarUrl } }

2. fetchApi Utility

File: src/utils/fetchApi.ts

Low-level fetch wrapper around restApi. Handles:

  • JSON and FormData payloads (auto-detects File objects → switches to multipart/form-data)
  • Query param cleaning (strips null, undefined, empty strings)
  • Normalized error handling → always rejects with FetchError
  • Success/error callbacks via onSuccess / onError options
  • 401 handling: rejects with { errorMessage: 'Session expired' } for non-auth endpoints
ts
interface FetchOptions {
  url: string;
  method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
  headers?: Record<string, unknown>;
  payload?: Record<string, unknown>;
  params?: Record<string, unknown>;
  onSuccess?: (data: ApiResponse, request: FetchRequest) => unknown;
  onError?: (err: unknown, request: FetchRequest) => void | boolean | string;
  signal?: AbortSignal;
  responseType?: 'json' | 'blob' | 'arraybuffer';
}

Error callback return values:

  • false → suppress error message (reject with empty errorMessage)
  • string → use as custom error message
  • void → use server error message or fallback

3. React Hooks

All hooks exported from src/hooks/index.ts:

ts
export { default as useFetchApi } from './useFetchApi';
export { default as useQueryFetch } from './useQueryFetch';
export { default as useMutationSubmit } from './useMutationSubmit';
export { default as useDataPagination } from './useDataPagination';
export { useWebappConfig, usePageBatch, useBlockExecute, useFilterOptions } from './useHoneyframe';
export { default as useI18n } from './useI18n';
export { useToast, default as toast } from './useToast';

useFetchApi

File: src/hooks/useFetchApi.ts

Memoized wrapper around fetchApi with stable function reference. Used internally by useQueryFetch and useMutationSubmit.

ts
const fetch = useFetchApi({
  url: '/endpoint',
  method: 'GET',
  params: { page: 1 },
  onSuccess: (data) => { /* ... */ },
  onError: (err) => { /* ... */ },
});

// Call with runtime overrides
const result = await fetch(
  { name: 'payload' },      // runtime payload (merged with options.payload)
  { page: 2 },              // runtime params (merged with options.params)
  { signal: controller.signal },  // extra config overrides
);

useQueryFetch

File: src/hooks/useQueryFetch.ts

Combines React Query useQuery with fetchApi for GET requests.

ts
const { data, isLoading, error } = useQueryFetch<User[]>({
  queryKey: ['users'],
  url: '/users',
  params: { page: 1, limit: 10 },
  enabled: true,           // optional, default true
  staleTime: 5 * 60_000,   // optional
  queryOptions: { ... },   // pass-through to useQuery
});

// data is ApiResponse<User[]>
// Access: data?.data (the actual payload)

Query key: automatically appends params['users', { page: 1, limit: 10 }]

useMutationSubmit

File: src/hooks/useMutationSubmit.ts

Combines React Query useMutation with fetchApi for POST/PUT/PATCH/DELETE.

ts
const { mutate, isPending } = useMutationSubmit<ResponseType, VariablesType>({
  url: '/users',
  method: 'POST',              // default 'POST'
  onSuccess: (data) => toast.success('Created'),
  onError: (err) => toast.error(err.errorMessage),
  mutationOptions: { ... },    // pass-through to useMutation
});

// Call with variables (becomes the request payload)
mutate({ name: 'John', email: 'john@example.com' });

Type params:

  • TData — shape of ApiResponse.data
  • TVariables — shape of the mutation payload (must extend Record<string, unknown>)

useDataPagination

File: src/hooks/useDataPagination.ts

Higher-level hook for paginated list endpoints. Wraps useQueryFetch with search, filter, and pagination state management.

ts
const { data, pagination, isLoading, searchByQuery, goToPage } = useDataPagination({
  dataSourceUrl: '/branches',
  defaultParams: { page: 1, limit: 20 },
  generatedParamsKey: {
    search: ['search'],
    region: ['region'],
  },
});

4. Honeyframe Integration

Client Library

File: src/lib/honeyframe.ts

Uses hfApi axios instance. Four functions:

FunctionEndpointDescription
fetchWebappConfig(appKey)GET /api/webapps/{appKey}Fetch app config (pages, nav, filters, cards)
executeBatch(dashId, cardIds, params, assetId)POST /api/dashboards/{dashId}/cards/execute-batchExecute cards in a single dashboard
executeBlock(appKey, pageKey, blockIdx, filters)POST /api/webapps/{appKey}/pages/{pageKey}/blocks/{blockIdx}/executeExecute a code/HTML block
fetchFilterOptions(optionsSql)POST /api/webapps/preview-filter-optionsFetch filter dropdown options

Helper: executePageBatch(cardRefs, assetId, params) — groups card refs by dashboard_id, fires parallel executeBatch calls, merges results into one object.

React Query Hooks

File: src/hooks/useHoneyframe.ts

useWebappConfig

Fetches app config once, caches forever (staleTime: Infinity), stores in Zustand useHoneyframeStore.

ts
useWebappConfig('hermina360');

// Access config anywhere:
const app = useHoneyframeStore(s => s.apps['hermina360']);
const pages = app?.config.pages;
const assetId = app?.asset_id;

usePageBatch

Fetches all card data for a page. Groups card refs by dashboard_id internally.

ts
const page = app?.config.pages.find(p => p.key === 'executive');
const cardRefs = (page?.card_refs ?? []).filter(
  (b): b is HFCardRef => b.kind === undefined || b.kind === 'card',
);

const { data: batch, isLoading } = usePageBatch(
  'executive',       // page key (for query cache key)
  cardRefs,          // card refs from config
  app?.asset_id,     // required — from top-level config
  {},                // optional params (filters)
  true,              // enabled
);

// Read card data — ALWAYS check error first
const cardData = batch?.[String(cardId)];
if (cardData?.error) console.error(cardData.error);
const rows = cardData?.rows ?? [];

Stale time: 5 minutes. Auto-disabled when cardRefs is empty or assetId is null.

useBlockExecute

For code blocks or HTML blocks with data_sql (used by hermina360_financial webapp).

ts
const { data: blockResult } = useBlockExecute(
  'hermina360_financial',  // app key
  'ringkasan',             // page key
  2,                       // block index
  { period_end: '2026-07' },  // optional filters
);

// blockResult.rows contains the data

useFilterOptions

Fetches dropdown options for a filter. Pass options_sql from config verbatim — do NOT modify (403 if modified).

ts
const filter = page?.filters?.find(f => f.key === 'branch_id_codes');
const { data: options } = useFilterOptions(filter?.options_sql);

Standard Page Pattern

Every dashboard page follows this pattern:

ts
const MyPage = () => {
  useWebappConfig('hermina360');
  const app = useHoneyframeStore(s => s.apps['hermina360']);

  // 1. Find page by key
  const page = app?.config.pages.find(p => p.key === 'my-page-key');

  // 2. Filter card refs (exclude code/html blocks)
  const cardRefs = (page?.card_refs ?? []).filter(
    (b): b is HFCardRef => b.kind === undefined || b.kind === 'card',
  );

  // 3. Fetch batch
  const { data: batch, isLoading } = usePageBatch('my-page-key', cardRefs, app?.asset_id);

  // 4. Extract rows
  const rows = batch?.[String(CARD_ID)]?.rows ?? [];

  return <div>{/* render */}</div>;
};

TypeScript Types

File: src/types/honeyframe.ts

TypeDescription
HFWebappResponseTop-level response (asset_id, config with pages/nav/filters/parameters)
HFWebappConfigConfig object (pages, nav, filters, parameters)
HFPageSingle page with card_refs and filters
HFCardRefCard reference ({ card_id, dashboard_id, kind? })
HFCodeBlockSQL code block
HFHtmlBlockHTML block with optional data_sql
HFBatchResultRecord<string, HFCardResult> (keyed by card_id)
HFCardResult{ columns, rows, row_count, error, ... }
HFBlockResultResult for a single block execute
HFFilterOptionsResult{ options, error }

Apps & Webapps

App KeyAsset IDDescription
hermina3601200Main executive + operational dashboard (277 cards, 34 pages)
hermina360_financial1207CFO / financial dashboard (26 cards + 7 HTML blocks)

See docs/HONEYFRAME.md for full card inventory and data field documentation.

Environment Variables

bash
# .env.local (never commit)
VITE_API_URL=https://your-backend.com/api    # REST API base
VITE_HF_PROXY_BASE=/hf-api                  # Honeyframe proxy path (dev default)
VITE_HF_TOKEN=your-honeyframe-bearer-token   # Honeyframe API token

Vite proxy config (dev only) in vite.config.ts:

ts
proxy: {
  '/hf-api': {
    target: 'https://hospital.hubstudio.id',
    changeOrigin: true,
    rewrite: (path) => path.replace(/^\/hf-api/, ''),
  },
}