Appearance
API Integration
Hermina 360 uses two data sources:
- Hermina 360 REST API — application backend (auth, user management, future CRUD)
- 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:
| Instance | Purpose | Base URL | Auth |
|---|---|---|---|
restApi | App backend (auth, CRUD) | VITE_API_URL | Bearer token from useAuthStore (auto-injected) |
hfApi | Honeyframe BI data | VITE_HF_PROXY_BASE or /hf-api | Bearer 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-api→hospital.hubstudio.id(bypasses CORS) - Prod: set
VITE_HF_PROXY_BASEto 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
Fileobjects → switches tomultipart/form-data) - Query param cleaning (strips
null,undefined, empty strings) - Normalized error handling → always rejects with
FetchError - Success/error callbacks via
onSuccess/onErroroptions - 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 emptyerrorMessage)string→ use as custom error messagevoid→ 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 ofApiResponse.dataTVariables— shape of the mutation payload (must extendRecord<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:
| Function | Endpoint | Description |
|---|---|---|
fetchWebappConfig(appKey) | GET /api/webapps/{appKey} | Fetch app config (pages, nav, filters, cards) |
executeBatch(dashId, cardIds, params, assetId) | POST /api/dashboards/{dashId}/cards/execute-batch | Execute cards in a single dashboard |
executeBlock(appKey, pageKey, blockIdx, filters) | POST /api/webapps/{appKey}/pages/{pageKey}/blocks/{blockIdx}/execute | Execute a code/HTML block |
fetchFilterOptions(optionsSql) | POST /api/webapps/preview-filter-options | Fetch 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 datauseFilterOptions
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
| Type | Description |
|---|---|
HFWebappResponse | Top-level response (asset_id, config with pages/nav/filters/parameters) |
HFWebappConfig | Config object (pages, nav, filters, parameters) |
HFPage | Single page with card_refs and filters |
HFCardRef | Card reference ({ card_id, dashboard_id, kind? }) |
HFCodeBlock | SQL code block |
HFHtmlBlock | HTML block with optional data_sql |
HFBatchResult | Record<string, HFCardResult> (keyed by card_id) |
HFCardResult | { columns, rows, row_count, error, ... } |
HFBlockResult | Result for a single block execute |
HFFilterOptionsResult | { options, error } |
Apps & Webapps
| App Key | Asset ID | Description |
|---|---|---|
hermina360 | 1200 | Main executive + operational dashboard (277 cards, 34 pages) |
hermina360_financial | 1207 | CFO / 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 tokenVite 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/, ''),
},
}