users added

This commit is contained in:
suryacp23 2026-07-30 12:18:29 +05:30
parent 6c9a6d60a2
commit f8b6de86f2
10 changed files with 450 additions and 14 deletions

View File

@ -21,10 +21,11 @@ export function clientFor(slug: WorkflowSlug): ZinoClient {
export const orderBookingClient = clientFor('orderBooking'); export const orderBookingClient = clientFor('orderBooking');
export const storeClient = clientFor('store'); export const storeClient = clientFor('store');
export const dailyReportsClient = clientFor('dailyReports'); export const dailyReportsClient = clientFor('dailyReports');
export const userClient = clientFor('user');
// The user JWT (from /usr/login) is valid across every workflow, but each // 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. // 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. */ /** Log in once and share the JWT with every workflow client. */
export async function loginAll(email: string, password: string, orgId?: string) { export async function loginAll(email: string, password: string, orgId?: string) {

View File

@ -170,11 +170,38 @@ export const DAILY_REPORTS = {
}, },
} as const; } 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. // All workflows keyed by slug, for generic lookup.
export const WORKFLOWS = { export const WORKFLOWS = {
orderBooking: ORDER_BOOKING, orderBooking: ORDER_BOOKING,
store: STORE, store: STORE,
dailyReports: DAILY_REPORTS, dailyReports: DAILY_REPORTS,
user: USER,
} as const; } as const;
// --- Daily Sales Report Constants --- // --- Daily Sales Report Constants ---

View File

@ -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 (
<div
className="bg-app p-4 mb-3 flex items-center justify-between hover:bg-slate-50 transition-colors cursor-pointer border border-border-subtle rounded-xl shadow-[0_2px_8px_rgba(11,27,59,0.04)]"
onClick={() => onEdit?.(row)}
>
<div className="flex items-center gap-4 min-w-0">
{/* Avatar */}
<div className="w-11 h-11 rounded-full bg-primary/10 flex items-center justify-center text-primary shrink-0 font-bold tracking-tight">
{initials || <User size={20} />}
</div>
{/* Info */}
<div className="flex flex-col min-w-0 pr-4">
<h3 className="m-0 text-base font-semibold text-strong truncate">{name}</h3>
{email && <span className="text-sm text-faint truncate">{email}</span>}
</div>
</div>
{/* Edit Action */}
{onEdit && (
<button
onClick={(e) => {
e.stopPropagation();
onEdit(row);
}}
className="p-2 text-faint hover:text-primary hover:bg-primary/10 rounded-full transition-colors shrink-0"
>
<Edit2 size={18} />
</button>
)}
</div>
);
}

View File

@ -0,0 +1,5 @@
export * from './OrderCard';
export * from './CallCard';
export * from './StoreCard';
export * from './DailyLogCard';
export * from './UserCard';

View File

@ -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 (
<RecordView
client={userClient}
rvUid={USER.recordViews.SALES_OFFICERS}
title="Sales Officers"
onRowClick={onRowClick}
pageSize={pageSize}
headerActions={headerActions}
refreshKey={refreshKey}
initialFilters={initialFilters}
sortBy="instance_id"
sortDir="desc"
renderItem={(row) => <UserCard row={row} onEdit={onEditRow} />}
/>
);
}

View File

@ -5,3 +5,4 @@ export type { WiredRecordViewProps } from './OrdersView';
export { CallsView } from './CallsView'; export { CallsView } from './CallsView';
export { StoresView } from './StoresView'; export { StoresView } from './StoresView';
export { DailyLogsView } from './DailyLogsView'; export { DailyLogsView } from './DailyLogsView';
export { UsersView } from './UsersView';

247
src/docs/api/user.md Normal file
View File

@ -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 <JWT_TOKEN>` (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 <JWT_TOKEN>" \
-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 <JWT_TOKEN>` (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": "<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 <JWT_TOKEN>" \
-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": "<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 <JWT_TOKEN>` (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.<key>} 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 <JWT_TOKEN>" \
-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.

View File

@ -6,6 +6,7 @@ import { AllDailyLogsPage } from './screens/AllDailyLogsPage'
import { AnalyticsPage } from './screens/AnalyticsPage' import { AnalyticsPage } from './screens/AnalyticsPage'
import { DailySalesReportPage } from './screens/DailySalesReportPage' import { DailySalesReportPage } from './screens/DailySalesReportPage'
import { DatasetsPage } from './screens/admin/DatasetsPage' import { DatasetsPage } from './screens/admin/DatasetsPage'
import { UsersPage } from './screens/admin/UsersPage'
export const routeConfig = [ 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) // 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", path: "/sales-report",
element: <DailySalesReportPage />, element: <DailySalesReportPage />,
roles: ["Manager", "Admin", "Sales Officer"], },
{
path: "/admin/users",
element: <UsersPage />,
roles: ["Manager", "Admin"],
}, },
// Admin Panel // Admin Panel
@ -83,4 +89,5 @@ export const routeConfig = [
element: <DatasetsPage />, element: <DatasetsPage />,
adminOnly: true, adminOnly: true,
}, },
]; ];

View File

@ -1,6 +1,6 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { NavLink, Navigate, Outlet, useNavigate } from 'react-router-dom'; 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 { cn } from '../lib/cn';
import { useAuth } from '../auth/context'; import { useAuth } from '../auth/context';
import { onAuthErrorAll, orderBookingClient } from '../api/clients'; import { onAuthErrorAll, orderBookingClient } from '../api/clients';
@ -144,6 +144,19 @@ export function ConsoleLayout() {
<div className="my-2 border-t border-border-subtle" /> <div className="my-2 border-t border-border-subtle" />
{isAdmin && ( {isAdmin && (
<>
<NavLink
to="/admin/users"
onClick={() => 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" : ""
)}
>
<Users size={20} className="shrink-0" />
<span>Sales Officers</span>
</NavLink>
<NavLink <NavLink
to="/admin/datasets" to="/admin/datasets"
onClick={() => setIsMenuOpen(false)} onClick={() => setIsMenuOpen(false)}
@ -155,6 +168,7 @@ export function ConsoleLayout() {
<Database size={20} className="shrink-0" /> <Database size={20} className="shrink-0" />
<span>Manage Datasets</span> <span>Manage Datasets</span>
</NavLink> </NavLink>
</>
)} )}
</div> </div>

View File

@ -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 (
<>
<UsersView
refreshKey={refreshKey}
onEditRow={handleEditRow}
/>
<button
onClick={() => setIsCreating(true)}
className="fixed bottom-24 right-6 w-[60px] h-[60px] bg-primary rounded-3xl flex items-center justify-center shadow-lg text-white z-40 hover:opacity-90 transition-transform hover:scale-105 active:scale-95 cursor-pointer"
title="Add Sales Officer"
>
<UserPlus size={24} className="text-white" />
</button>
<Modal
open={isCreating || activeActivity !== null}
onClose={() => {
setIsCreating(false);
setActiveActivity(null);
}}
title={activeActivity?.name || "Add User"}
>
<DynamicForm
client={userClient}
activityId={activeActivity?.id || USER.activities.ADD_USER.uid}
instanceId={activeActivity?.instanceId}
onSuccess={() => {
setIsCreating(false);
setActiveActivity(null);
setRefreshKey(k => k + 1);
}}
onCancel={() => {
setIsCreating(false);
setActiveActivity(null);
}}
/>
</Modal>
</>
);
}