diff --git a/src/api/clients.ts b/src/api/clients.ts
index bbe0f12..b426984 100644
--- a/src/api/clients.ts
+++ b/src/api/clients.ts
@@ -21,10 +21,11 @@ export function clientFor(slug: WorkflowSlug): ZinoClient {
export const orderBookingClient = clientFor('orderBooking');
export const storeClient = clientFor('store');
export const dailyReportsClient = clientFor('dailyReports');
+export const userClient = clientFor('user');
// The user JWT (from /usr/login) is valid across every workflow, but each
// client holds its own copy — so auth helpers fan out to all of them.
-const ALL = [orderBookingClient, storeClient, dailyReportsClient];
+const ALL = [orderBookingClient, storeClient, dailyReportsClient, userClient];
/** Log in once and share the JWT with every workflow client. */
export async function loginAll(email: string, password: string, orgId?: string) {
diff --git a/src/api/config.ts b/src/api/config.ts
index 71ba692..181f093 100644
--- a/src/api/config.ts
+++ b/src/api/config.ts
@@ -170,11 +170,38 @@ export const DAILY_REPORTS = {
},
} as const;
+// --- User Management (src/docs/api/user.md) ---
+export const USER = {
+ workflowUuid: '9874af12-78b7-4730-834a-19a32221f3aa',
+ versionUuid: '', // No version UUID provided, server will use latest
+ activities: {
+ ADD_USER: {
+ uid: '3a8f132e-8c63-4d4d-b466-67595d345a99', // init
+ fields: {
+ name: 'name', // text
+ email: 'email', // email
+ password: 'password', // password
+ },
+ },
+ EDIT_USER: {
+ uid: '3091530c-4315-42b9-b53b-a148f2c54a81',
+ fields: {
+ name: 'name_3', // text
+ email: 'email_3', // email
+ },
+ },
+ },
+ recordViews: {
+ SALES_OFFICERS: '406f89d7-09f6-42ba-ba35-84334ccb6204',
+ },
+} as const;
+
// All workflows keyed by slug, for generic lookup.
export const WORKFLOWS = {
orderBooking: ORDER_BOOKING,
store: STORE,
dailyReports: DAILY_REPORTS,
+ user: USER,
} as const;
// --- Daily Sales Report Constants ---
diff --git a/src/components/cards/UserCard.tsx b/src/components/cards/UserCard.tsx
new file mode 100644
index 0000000..920ead8
--- /dev/null
+++ b/src/components/cards/UserCard.tsx
@@ -0,0 +1,48 @@
+import { User, Edit2 } from 'lucide-react';
+
+export function UserCard({ row, onEdit }: { row: any; onEdit?: (row: any) => void }) {
+ const name = String(row.name_2 || row.name || 'Unnamed User');
+ const email = row.email_2 || row.email || '';
+
+ // extract initials for a sleek avatar
+ const initials = name
+ .split(' ')
+ .filter(Boolean)
+ .map((n: string) => n[0])
+ .join('')
+ .substring(0, 2)
+ .toUpperCase();
+
+ return (
+
onEdit?.(row)}
+ >
+
+ {/* Avatar */}
+
+ {initials || }
+
+
+ {/* Info */}
+
+
{name}
+ {email && {email}}
+
+
+
+ {/* Edit Action */}
+ {onEdit && (
+
+ )}
+
+ );
+}
diff --git a/src/components/cards/index.ts b/src/components/cards/index.ts
new file mode 100644
index 0000000..166ae57
--- /dev/null
+++ b/src/components/cards/index.ts
@@ -0,0 +1,5 @@
+export * from './OrderCard';
+export * from './CallCard';
+export * from './StoreCard';
+export * from './DailyLogCard';
+export * from './UserCard';
diff --git a/src/components/rv/UsersView.tsx b/src/components/rv/UsersView.tsx
new file mode 100644
index 0000000..c830d8b
--- /dev/null
+++ b/src/components/rv/UsersView.tsx
@@ -0,0 +1,24 @@
+import { userClient } from '../../api/clients';
+import { USER } from '../../api/config';
+import { RecordView } from './RecordView';
+import type { WiredRecordViewProps } from './OrdersView';
+import { UserCard } from '../cards/UserCard';
+
+/** Users / Sales Officers record view (User workflow). */
+export function UsersView({ onRowClick, onEditRow, pageSize, headerActions, refreshKey, initialFilters }: WiredRecordViewProps & { headerActions?: React.ReactNode; onEditRow?: (row: any) => void }) {
+ return (
+ }
+ />
+ );
+}
diff --git a/src/components/rv/index.ts b/src/components/rv/index.ts
index 00fca48..72e5260 100644
--- a/src/components/rv/index.ts
+++ b/src/components/rv/index.ts
@@ -5,3 +5,4 @@ export type { WiredRecordViewProps } from './OrdersView';
export { CallsView } from './CallsView';
export { StoresView } from './StoresView';
export { DailyLogsView } from './DailyLogsView';
+export { UsersView } from './UsersView';
\ No newline at end of file
diff --git a/src/docs/api/user.md b/src/docs/api/user.md
new file mode 100644
index 0000000..68ce2fd
--- /dev/null
+++ b/src/docs/api/user.md
@@ -0,0 +1,247 @@
+# Data Collection API
+
+- core: `https://dev.getzino.in`
+- view: `https://dev.getzino.in`
+
+## Activity submission
+
+Calls that create an instance or submit an activity's form. Init = /start; subsequent = /activity. Upload/OCR fire during fill for file-bearing fields.
+
+#### Add User
+
+`POST /app/434/start`
+
+> Create a new workflow instance and run the init activity.
+
+**Headers**
+
+- `Authorization: Bearer ` (required) — User session JWT from login.
+- `Content-Type: application/json` (required)
+
+**Request body**
+
+```json
+{
+ "activity_id": "3a8f132e-8c63-4d4d-b466-67595d345a99",
+ "data": {
+ "email": "user@example.com",
+ "name": "string_value",
+ "password": "string_value"
+ },
+ "version": 1,
+ "workflow_uuid": "9874af12-78b7-4730-834a-19a32221f3aa"
+}
+```
+
+**Request fields**
+
+- `workflow_uuid` (string) *required* — Clone-portable workflow UID.
+- `activity_id` (string) *required* — The init activity UID.
+- `version` (int) — Deployed workflow version.
+- `data` (object) — Field values keyed by field id.
+- `name` (text) *required* — "Name" (text)
+- `email` (email) *required* — "Email" (email)
+- `password` (password) *required* — "Password" (password)
+
+**Response**
+
+```json
+{
+ "data": {},
+ "instance_id": 1024,
+ "message": "Activity completed successfully",
+ "status_code": 200,
+ "success": true
+}
+```
+
+**Response fields**
+
+- `success` (bool) — Whether the activity ran.
+- `status_code` (int) — HTTP status; mirrors a Response node if the trigger graph has one.
+- `message` (string) — Human-readable result; from a Response node when present.
+- `data` (object) — Response-node payload; empty object by default.
+- `instance_id` (int64) — The workflow instance affected/created.
+
+**curl**
+
+```bash
+curl -X POST "https://dev.getzino.in/app/434/start" \
+ -H "Authorization: Bearer " \
+ -H "Content-Type: application/json" \
+ -d '{
+ "activity_id": "3a8f132e-8c63-4d4d-b466-67595d345a99",
+ "data": {
+ "email": "user@example.com",
+ "name": "string_value",
+ "password": "string_value"
+ },
+ "version": 1,
+ "workflow_uuid": "9874af12-78b7-4730-834a-19a32221f3aa"
+}'
+```
+
+#### Edit User
+
+`POST /app/434/activity`
+
+> Advance an existing instance through this activity (normal submission).
+
+**Headers**
+
+- `Authorization: Bearer ` (required) — User session JWT from login.
+- `Content-Type: application/json` (required)
+
+**Request body**
+
+```json
+{
+ "activity_id": "3091530c-4315-42b9-b53b-a148f2c54a81",
+ "data": {
+ "email_3": "user@example.com",
+ "name_3": "string_value"
+ },
+ "instance_id": "",
+ "workflow_uuid": "9874af12-78b7-4730-834a-19a32221f3aa"
+}
+```
+
+**Request fields**
+
+- `workflow_uuid` (string) *required* — Clone-portable workflow UID.
+- `activity_id` (string) *required* — This activity's UID.
+- `instance_id` (int64) *required* — Target instance (runtime value).
+- `data` (object) — Field values keyed by field id.
+- `name_3` (text) — "Name" (text)
+- `email_3` (email) — "Email" (email)
+
+**Response**
+
+```json
+{
+ "data": {},
+ "instance_id": 1024,
+ "message": "Activity completed successfully",
+ "status_code": 200,
+ "success": true
+}
+```
+
+**Response fields**
+
+- `success` (bool) — Whether the activity ran.
+- `status_code` (int) — HTTP status; mirrors a Response node if the trigger graph has one.
+- `message` (string) — Human-readable result; from a Response node when present.
+- `data` (object) — Response-node payload; empty object by default.
+- `instance_id` (int64) — The workflow instance affected/created.
+
+**curl**
+
+```bash
+curl -X POST "https://dev.getzino.in/app/434/activity" \
+ -H "Authorization: Bearer " \
+ -H "Content-Type: application/json" \
+ -d '{
+ "activity_id": "3091530c-4315-42b9-b53b-a148f2c54a81",
+ "data": {
+ "email_3": "user@example.com",
+ "name_3": "string_value"
+ },
+ "instance_id": "",
+ "workflow_uuid": "9874af12-78b7-4730-834a-19a32221f3aa"
+}'
+```
+
+## View APIs
+
+Read endpoints that power record/detail/activity/chart views, segregated by source type with each view's UI name.
+
+### Record View
+
+#### Sales Officers
+
+`POST /app/434/view/recordview`
+
+> Paginated records + tiles + charts for this record view.
+
+**Headers**
+
+- `Authorization: Bearer ` (required) — User session JWT from login.
+- `Content-Type: application/json` (required)
+
+**Request body**
+
+```json
+{
+ "params": {},
+ "preset_alias": "",
+ "rv_template_uid": "406f89d7-09f6-42ba-ba35-84334ccb6204",
+ "search_query": {
+ "filters": [],
+ "limit": 25,
+ "page": 1,
+ "search": "",
+ "sort_by": "",
+ "sort_dir": "desc"
+ }
+}
+```
+
+**Request fields**
+
+- `rv_template_uid` (string) *required* — This view's template UID.
+- `preset_alias` (string) — Optional; selects a named prefilter preset declared on this view.
+- `params` (object) — Optional; values for the view's declared input params (key→value), consumed by ${input.} prefilter refs.
+- `search_query.page` (int) — 1-based page.
+- `search_query.limit` (int) — Default 25, max 200.
+- `search_query.sort_by` (string) — field_key | created_at | updated_at | instance_id.
+- `search_query.sort_dir` (string) — asc | desc.
+- `search_query.search` (string) — ILIKE term across searchable fields.
+- `search_query.filters` (array) — [{field_key, value, value2?, data_type}].
+
+**Response**
+
+```json
+{
+ "chart_data": [],
+ "data": [],
+ "pagination": {
+ "limit": 25,
+ "page": 1,
+ "total_records": 0
+ },
+ "tile_values": {}
+}
+```
+
+**Response fields**
+
+- `data` (array) — Companion-table rows.
+- `tile_values` (object)
+- `chart_data` (array)
+- `pagination` (object) — {page, limit, total_records}.
+
+**curl**
+
+```bash
+curl -X POST "https://dev.getzino.in/app/434/view/recordview" \
+ -H "Authorization: Bearer " \
+ -H "Content-Type: application/json" \
+ -d '{
+ "params": {},
+ "preset_alias": "",
+ "rv_template_uid": "406f89d7-09f6-42ba-ba35-84334ccb6204",
+ "search_query": {
+ "filters": [],
+ "limit": 25,
+ "page": 1,
+ "search": "",
+ "sort_by": "",
+ "sort_dir": "desc"
+ }
+}'
+```
+
+## In-form activity helper APIs
+
+Calls a form makes while being filled, segregated per activity: lookup, workflow-lookup, app-user, dataset options, field actions, and peer-instance prefetch.
diff --git a/src/routesConfig.tsx b/src/routesConfig.tsx
index 395163d..84ca1a4 100644
--- a/src/routesConfig.tsx
+++ b/src/routesConfig.tsx
@@ -6,6 +6,7 @@ import { AllDailyLogsPage } from './screens/AllDailyLogsPage'
import { AnalyticsPage } from './screens/AnalyticsPage'
import { DailySalesReportPage } from './screens/DailySalesReportPage'
import { DatasetsPage } from './screens/admin/DatasetsPage'
+import { UsersPage } from './screens/admin/UsersPage'
export const routeConfig = [
// Manager & Admin (and maybe Sales Officer for daily logs, adapting from krishna_sales where my-daily was Sales Officer and daily was Manager/Admin. In mobile we have daily and all-daily)
@@ -74,7 +75,12 @@ export const routeConfig = [
{
path: "/sales-report",
element: ,
- roles: ["Manager", "Admin", "Sales Officer"],
+ },
+ {
+ path: "/admin/users",
+ element: ,
+ roles: ["Manager", "Admin"],
+
},
// Admin Panel
@@ -83,4 +89,5 @@ export const routeConfig = [
element: ,
adminOnly: true,
},
+
];
diff --git a/src/screens/ConsoleLayout.tsx b/src/screens/ConsoleLayout.tsx
index 690f98c..a378d2e 100644
--- a/src/screens/ConsoleLayout.tsx
+++ b/src/screens/ConsoleLayout.tsx
@@ -1,6 +1,6 @@
import { useEffect, useState } from 'react';
import { NavLink, Navigate, Outlet, useNavigate } from 'react-router-dom';
-import { LogOut, Menu, X, Database } from 'lucide-react';
+import { LogOut, Menu, X, Database, Users } from 'lucide-react';
import { cn } from '../lib/cn';
import { useAuth } from '../auth/context';
import { onAuthErrorAll, orderBookingClient } from '../api/clients';
@@ -144,17 +144,31 @@ export function ConsoleLayout() {
{isAdmin && (
- setIsMenuOpen(false)}
- className={({ isActive }) => cn(
- "flex items-center gap-3 px-4 py-3 mx-2 rounded-lg no-underline transition-colors duration-150 sidebar-item",
- isActive ? "active" : ""
- )}
- >
-
- Manage Datasets
-
+ <>
+ setIsMenuOpen(false)}
+ className={({ isActive }) => cn(
+ "flex items-center gap-3 px-4 py-3 mx-2 rounded-lg no-underline transition-colors duration-150 sidebar-item",
+ isActive ? "active" : ""
+ )}
+ >
+
+ Sales Officers
+
+
+ setIsMenuOpen(false)}
+ className={({ isActive }) => cn(
+ "flex items-center gap-3 px-4 py-3 mx-2 rounded-lg no-underline transition-colors duration-150 sidebar-item",
+ isActive ? "active" : ""
+ )}
+ >
+
+ Manage Datasets
+
+ >
)}
diff --git a/src/screens/admin/UsersPage.tsx b/src/screens/admin/UsersPage.tsx
new file mode 100644
index 0000000..97b35be
--- /dev/null
+++ b/src/screens/admin/UsersPage.tsx
@@ -0,0 +1,62 @@
+import { useState } from 'react';
+import { UserPlus } from 'lucide-react';
+import { Modal } from '../../components/reusable';
+import { UsersView } from '../../components/rv';
+import { DynamicForm } from '../../components/forms/DynamicForm';
+import { USER } from '../../api/config';
+import { userClient } from '../../api/clients';
+
+export function UsersPage() {
+ const [isCreating, setIsCreating] = useState(false);
+ const [refreshKey, setRefreshKey] = useState(0);
+ const [activeActivity, setActiveActivity] = useState<{ id: string; name: string, instanceId?: number } | null>(null);
+
+ const handleEditRow = (row: any) => {
+ setActiveActivity({
+ id: USER.activities.EDIT_USER.uid,
+ name: "Edit User",
+ instanceId: row.instance_id
+ });
+ };
+
+ return (
+ <>
+
+
+
+
+ {
+ setIsCreating(false);
+ setActiveActivity(null);
+ }}
+ title={activeActivity?.name || "Add User"}
+ >
+ {
+ setIsCreating(false);
+ setActiveActivity(null);
+ setRefreshKey(k => k + 1);
+ }}
+ onCancel={() => {
+ setIsCreating(false);
+ setActiveActivity(null);
+ }}
+ />
+
+ >
+ );
+}