From f8b6de86f236ea035d3f4a497a65424fa90f5419 Mon Sep 17 00:00:00 2001 From: suryacp23 Date: Thu, 30 Jul 2026 12:18:29 +0530 Subject: [PATCH] users added --- src/api/clients.ts | 3 +- src/api/config.ts | 27 ++++ src/components/cards/UserCard.tsx | 48 ++++++ src/components/cards/index.ts | 5 + src/components/rv/UsersView.tsx | 24 +++ src/components/rv/index.ts | 1 + src/docs/api/user.md | 247 ++++++++++++++++++++++++++++++ src/routesConfig.tsx | 9 +- src/screens/ConsoleLayout.tsx | 38 +++-- src/screens/admin/UsersPage.tsx | 62 ++++++++ 10 files changed, 450 insertions(+), 14 deletions(-) create mode 100644 src/components/cards/UserCard.tsx create mode 100644 src/components/cards/index.ts create mode 100644 src/components/rv/UsersView.tsx create mode 100644 src/docs/api/user.md create mode 100644 src/screens/admin/UsersPage.tsx 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); + }} + /> + + + ); +}