users added
This commit is contained in:
parent
6c9a6d60a2
commit
f8b6de86f2
@ -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) {
|
||||
|
||||
@ -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 ---
|
||||
|
||||
48
src/components/cards/UserCard.tsx
Normal file
48
src/components/cards/UserCard.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
5
src/components/cards/index.ts
Normal file
5
src/components/cards/index.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export * from './OrderCard';
|
||||
export * from './CallCard';
|
||||
export * from './StoreCard';
|
||||
export * from './DailyLogCard';
|
||||
export * from './UserCard';
|
||||
24
src/components/rv/UsersView.tsx
Normal file
24
src/components/rv/UsersView.tsx
Normal 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} />}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -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';
|
||||
247
src/docs/api/user.md
Normal file
247
src/docs/api/user.md
Normal 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.
|
||||
@ -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: <DailySalesReportPage />,
|
||||
roles: ["Manager", "Admin", "Sales Officer"],
|
||||
},
|
||||
{
|
||||
path: "/admin/users",
|
||||
element: <UsersPage />,
|
||||
roles: ["Manager", "Admin"],
|
||||
|
||||
},
|
||||
|
||||
// Admin Panel
|
||||
@ -83,4 +89,5 @@ export const routeConfig = [
|
||||
element: <DatasetsPage />,
|
||||
adminOnly: true,
|
||||
},
|
||||
|
||||
];
|
||||
|
||||
@ -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,6 +144,19 @@ export function ConsoleLayout() {
|
||||
<div className="my-2 border-t border-border-subtle" />
|
||||
|
||||
{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
|
||||
to="/admin/datasets"
|
||||
onClick={() => setIsMenuOpen(false)}
|
||||
@ -155,6 +168,7 @@ export function ConsoleLayout() {
|
||||
<Database size={20} className="shrink-0" />
|
||||
<span>Manage Datasets</span>
|
||||
</NavLink>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
62
src/screens/admin/UsersPage.tsx
Normal file
62
src/screens/admin/UsersPage.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user