diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000..46cf10e
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,3 @@
+# Local dev only — a deployed placement gets its API URL from the config.js
+# the frontgen server writes next to index.html (window.__RUNTIME_CONFIG__).
+VITE_ZINO_API_URL=https://dev.getzino.in
diff --git a/index.html b/index.html
index f1ad924..6bd3477 100644
--- a/index.html
+++ b/index.html
@@ -3,11 +3,13 @@
-
+
P2P
+
+
diff --git a/src/api/viewService.ts b/src/api/viewService.ts
index 098624c..91752be 100644
--- a/src/api/viewService.ts
+++ b/src/api/viewService.ts
@@ -1,9 +1,9 @@
-import { APP_ID } from "../config";
+import { APP_ID, BASE_URL } from "../config";
const TOKEN_KEY = "p2p_token";
function baseUrl(): string {
- return (import.meta.env.VITE_ZINO_API_URL || "https://studio.getzino.in").replace(/\/+$/, "");
+ return BASE_URL.replace(/\/+$/, "");
}
function headers(): Record {
@@ -62,6 +62,25 @@ async function postFile(path: string, file: File, ctx: FieldFileContext): Pro
return parseResponse(res);
}
+// ---------------------------------------------------------------------------
+// Current user (user-service)
+// ---------------------------------------------------------------------------
+
+export interface MeResponse {
+ id: string;
+ org_id: string;
+ name: string;
+ email: string;
+ mobile?: string;
+ roles?: string[];
+ groups?: string[];
+}
+
+/** Fresh profile for the logged-in user, app-scoped. */
+export function getMe(): Promise {
+ return request("GET", `/usr/app/${APP_ID}/me`);
+}
+
// ---------------------------------------------------------------------------
// Header
// ---------------------------------------------------------------------------
@@ -151,7 +170,11 @@ export interface SearchQuery {
sort_by?: string;
sort_dir?: "asc" | "desc";
search?: string;
- filters?: Array<{ field_key: string; value: string; data_type?: string }>;
+ // `values` (multi-value, ORed server-side) supersedes repeating the same
+ // field_key with single `value` entries.
+ filters?: Array<{ field_key: string; value?: string; values?: string[]; data_type?: string }>;
+ // Named filter preset configured on the RV template.
+ preset_alias?: string;
}
export interface RecordViewField {
@@ -203,13 +226,26 @@ export function getRecordView(
return request("POST", `/app/${APP_ID}/view/recordview`, {
rv_template_uid: rvTemplateUid,
rv_screen_id: String(rvScreenId),
+ ...(query.preset_alias ? { preset_alias: query.preset_alias } : {}),
search_query: {
page: query.page || 1,
limit: query.limit || 50,
sort_by: query.sort_by || "",
sort_dir: query.sort_dir || "desc",
search: query.search || "",
- filters: query.filters || [],
+ filters: (query.filters || []).map((f) => {
+ const item: Record = {
+ field_key: f.field_key,
+ data_type: f.data_type ?? "character varying",
+ };
+ if (f.values && f.values.length > 0) {
+ item.values = f.values;
+ } else {
+ item.value = f.value ?? "";
+ item.value2 = "";
+ }
+ return item;
+ }),
},
});
}
@@ -641,21 +677,83 @@ export interface RdbmsLookupResponse {
}
export interface RdbmsLookupQuery {
- workflowId: number;
+ workflowUuid: string;
activityId: string;
fieldId: string;
+ instanceId?: string;
limit?: number;
offset?: number;
search?: string;
+ // Current form values, so the server can apply the activity's field_rules
+ // filter_options (dependent/cascading lookups).
+ formData?: Record;
}
+// Core-service expects the workflow UUID as `workflow_uuid` — the numeric
+// `workflow_id` field was removed when the clone-portable identifier rolled out.
export function fetchRdbmsLookupRecords(templateUuid: string, q: RdbmsLookupQuery): Promise {
return request("POST", `/app/${APP_ID}/rdbms-templates/${encodeURIComponent(templateUuid)}/records`, {
- workflow_id: q.workflowId,
+ workflow_uuid: q.workflowUuid,
activity_id: q.activityId,
field_id: q.fieldId,
+ instance_id: q.instanceId || undefined,
limit: q.limit ?? 1000,
offset: q.offset ?? 0,
search: q.search ?? "",
+ form_data: q.formData ?? {},
+ });
+}
+
+// ---------------------------------------------------------------------------
+// WF Lookup records (cross-workflow references) & dataset-backed options
+// ---------------------------------------------------------------------------
+
+/** Fetch records for a wf_lookup field. The server resolves the field's lookup
+ * config + filter rules from activityId/fieldId against formData. */
+export function wfLookupRecords(
+ workflowUuid: string,
+ opts: {
+ activityId: string;
+ fieldId: string;
+ formData?: Record;
+ search?: string;
+ limit?: number;
+ offset?: number;
+ },
+): Promise<{ data?: Record[]; records?: Record[] } | Record[]> {
+ return request("POST", `/app/${APP_ID}/wf-lookup/records`, {
+ workflow_uuid: workflowUuid,
+ activity_id: opts.activityId,
+ field_id: opts.fieldId,
+ form_data: opts.formData ?? {},
+ search: opts.search ?? "",
+ limit: opts.limit ?? 100,
+ offset: opts.offset ?? 0,
+ });
+}
+
+/** Fetch options for a dataset-backed select. The server applies the activity's
+ * rules against formData, so e.g. City options come back scoped to the chosen
+ * State. Returns {label, value} rows (plus the raw dataset row). */
+export function datasetOptions(
+ workflowUuid: string,
+ opts: {
+ activityId: string;
+ fieldId: string;
+ search?: string;
+ instanceId?: string;
+ limit?: number;
+ formData?: Record;
+ },
+): Promise<{ options: Array<{ label: string; value: string; _raw?: Record }> }> {
+ return request("POST", `/app/${APP_ID}/dataset-options`, {
+ workflow_uuid: workflowUuid,
+ activity_id: opts.activityId,
+ field_id: opts.fieldId,
+ instance_id: opts.instanceId || undefined,
+ search: opts.search ?? "",
+ limit: opts.limit ?? 200,
+ offset: 0,
+ form_data: opts.formData ?? {},
});
}
diff --git a/src/components/RecordViewTable.tsx b/src/components/RecordViewTable.tsx
index e2093a7..adfe574 100644
--- a/src/components/RecordViewTable.tsx
+++ b/src/components/RecordViewTable.tsx
@@ -87,14 +87,12 @@ export default function RecordViewTable({ viewId, onRowClick, toolbarAction }: P
sort_by: params.sort || undefined,
sort_dir: params.dir,
search: params.q || undefined,
- // One entry per selected state. NOTE: this assumes the view-service ORs
- // repeated filters on the same field_key — verify against the live API,
- // since an AND would make any multi-select return zero rows.
- filters: params.status.map((value) => ({
- field_key: STATE_FIELD,
- value,
- data_type: "text",
- })),
+ // Multi-select ships as one filter with a `values` array, which the
+ // view-service ORs server-side.
+ filters:
+ params.status.length > 0
+ ? [{ field_key: STATE_FIELD, values: params.status, data_type: "text" }]
+ : [],
}),
[params],
);
diff --git a/src/config.ts b/src/config.ts
index 71e5e38..8550248 100644
--- a/src/config.ts
+++ b/src/config.ts
@@ -1,8 +1,29 @@
-// P2P Invoice System (dev) — App 169, Org 47
+// P2P Invoice System (dev) — App 519, Org 900000103
// Mirrors the studio app 20 build but with dev's UUID-based UIDs.
-export const ORG_ID = "47";
-export const APP_ID = "169";
+export const ORG_ID = "900000103";
+export const APP_ID = "519";
+
+// Backend URL comes from the runtime config.js the frontgen server writes at
+// placement time (window.__RUNTIME_CONFIG__.VITE_ZINO_API_URL) so one built
+// artifact can be promoted across environments. The env fallback is dev-gated:
+// a production build without a config.js must not silently serve the build
+// environment's API URL.
+declare global {
+ interface Window { __RUNTIME_CONFIG__?: Record }
+}
+const runtimeApiURL =
+ typeof window !== "undefined" ? window.__RUNTIME_CONFIG__?.VITE_ZINO_API_URL : undefined;
+
+// SAME_ORIGIN means "call the host that served this page": on a custom domain
+// the same Ingress serves /usr and /app/, so the API is this origin. It is a
+// sentinel rather than '' because the || below treats '' as absent.
+export const BASE_URL =
+ runtimeApiURL === "SAME_ORIGIN"
+ ? window.location.origin.replace(/\/$/, "")
+ : runtimeApiURL ||
+ (import.meta.env.DEV ? import.meta.env.VITE_ZINO_API_URL : undefined) ||
+ "https://dev.getzino.in";
// Core-service expects the workflow UUID under `workflow_uuid`.
export const WORKFLOW_ID = "0588b645-75f7-40e5-8251-e0761e4be632";
diff --git a/src/hooks/useAuth.ts b/src/hooks/useAuth.ts
index a2d8698..fcbffcd 100644
--- a/src/hooks/useAuth.ts
+++ b/src/hooks/useAuth.ts
@@ -1,5 +1,6 @@
import { useState, useCallback } from "react";
import type { User } from "../zino-sdk/types";
+import { BASE_URL } from "../config";
const TOKEN_KEY = "p2p_token";
const USER_KEY = "p2p_user";
@@ -30,9 +31,7 @@ export function useAuth() {
return { user, token, loading: false, error: null };
});
- const apiUrl = (
- import.meta.env.VITE_ZINO_API_URL || "https://studio.getzino.in"
- ).replace(/\/+$/, "");
+ const apiUrl = BASE_URL.replace(/\/+$/, "");
const login = useCallback(
async ({ email, password, orgId }: LoginParams) => {
@@ -44,7 +43,7 @@ export function useAuth() {
body: JSON.stringify({
email,
password,
- ...(orgId ? { org_id: orgId } : {}),
+ ...(orgId ? { org_id: Number(orgId) } : {}),
}),
});
diff --git a/src/main.tsx b/src/main.tsx
index 705969d..438f611 100644
--- a/src/main.tsx
+++ b/src/main.tsx
@@ -10,13 +10,19 @@ import { Toaster } from './components/ui/sonner'
import './index.css'
import App from './App'
-const apiUrl = import.meta.env.VITE_ZINO_API_URL || 'https://studio.getzino.in'
+import { BASE_URL } from './config'
+
+// Mount path comes from the document the frontgen server writes at
+// placement time, so one built artifact serves any mount without a hardcoded
+// slug. Falls back to "/" for local dev.
+const routerBasename =
+ (document.querySelector('base')?.getAttribute('href') || '/').replace(/\/$/, '') || '/'
createRoot(document.getElementById('root')!).render(
-
-
+
+
{/* NuqsAdapter must sit inside the router — it reads and writes the
router's search params, so table state can live in the URL. */}
diff --git a/vite.config.ts b/vite.config.ts
index ab7e91f..76e0187 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -3,7 +3,7 @@ import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
import { fileURLToPath, URL } from 'node:url'
-const devTarget = 'https://studio.getzino.in'
+const devTarget = 'https://dev.getzino.in'
const proxy = (target: string) => ({ target, changeOrigin: true, secure: false })
export default defineConfig({
@@ -11,7 +11,9 @@ export default defineConfig({
resolve: {
alias: { '@': fileURLToPath(new URL('./src', import.meta.url)) },
},
- base: process.env.VITE_BASE_URL ?? '/p2p/',
+ // Relative base so the built artifact is mount-path agnostic; the target
+ // frontgen writes the real into index.html at placement time.
+ base: './',
server: {
port: 3007,
proxy: {