Build app-434 field-sales console
API layer for sandbox app 434 (three workflows: Order Booking, Store, Daily Reports) — ZinoClient, per-workflow client singletons, config ids, types, and the source API docs. Component library (Tailwind v4 + design tokens): buttons, reusable (Card/Badge/Input/Select/Pagination/Modal/Spinner/EmptyState), generic + wired record views (rv) and detail views (dv). Screens + routing (react-router v7): login gate, auth-guarded console shell with tab nav, and deep-linkable record/detail routes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
3f36c92075
commit
d5e4ebee5d
32
CLAUDE.md
Normal file
32
CLAUDE.md
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
# Krishna Sales
|
||||||
|
|
||||||
|
Vite + React + TypeScript frontend for **sandbox app 434** on the Zino platform.
|
||||||
|
|
||||||
|
- Base URL: `https://sandbox.getzino.in`
|
||||||
|
- Dev: `npm run dev` · Build: `npm run build` (`tsc -b && vite build`)
|
||||||
|
|
||||||
|
## Layout
|
||||||
|
|
||||||
|
- `src/api/client.ts` — `ZinoClient`, the HTTP client for the Zino gateway. Constructor is `new ZinoClient(baseUrl, workflowUuid, onAuthError?)`. Auth (login/logout/currentUser) + view/activity/upload calls. JWT persists in localStorage.
|
||||||
|
- `src/api/config.ts` — all app-434 ids: `APP_ID`, `BASE_URL`, and per-workflow uuids/activities/fields/views.
|
||||||
|
- `src/api/types.ts` — shared API response types.
|
||||||
|
- `src/docs/api/*.md` — the source API docs (one per workflow) these ids come from.
|
||||||
|
|
||||||
|
## Workflows (app 434)
|
||||||
|
|
||||||
|
The app has **three workflows**, each with its own `workflow_uuid`. `APP_ID` (434) is shared; every route is app-scoped (`/app/434/...`) except `/usr/login`. Instantiate one client per workflow: `new ZinoClient(BASE_URL, WORKFLOWS.<slug>.workflowUuid)`.
|
||||||
|
|
||||||
|
| Slug (config) | Name | Init activity | Other activities |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `orderBooking` | Order Booking | Log Visit | Place Order, Productivity of Visit, Potential Mining, Close Order |
|
||||||
|
| `store` | Store | Init Activity | Edit Store |
|
||||||
|
| `dailyReports` | Daily Reports | Init Activity | Punch Out |
|
||||||
|
|
||||||
|
Field ids differ per activity even for the "same" field (e.g. Store's fields carry a `_2` suffix on init, `_3` on edit). Grid columns are noted in inline comments in `config.ts`. Always source ids from `config.ts`, never hardcode.
|
||||||
|
|
||||||
|
## Conventions
|
||||||
|
|
||||||
|
- Init activity → `POST /app/434/start`; subsequent activities → `POST /app/434/activity`. Both take `{workflow_uuid, activity_id, data, instance_id?}`.
|
||||||
|
- File/image fields: `POST /app/434/upload` (multipart) first, then send the returned `blob_path` as the field value.
|
||||||
|
- In-form option sources vary by field type: `wf-lookup/records`, `dataset-options`, `app-user/records` — see the per-workflow doc.
|
||||||
|
- When workflows/fields change, update `config.ts` from the docs in `src/docs/api/`.
|
||||||
125
README.md
125
README.md
@ -1,75 +1,70 @@
|
|||||||
# React + TypeScript + Vite
|
# Krishna Sales
|
||||||
|
|
||||||
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
Field-sales console for **Zino sandbox app 434**. A Vite + React + TypeScript SPA that browses the app's three workflows (Order Booking, Store, Daily Reports) over the Zino gateway.
|
||||||
|
|
||||||
Currently, two official plugins are available:
|
- **Base URL:** `https://sandbox.getzino.in`
|
||||||
|
- **Stack:** React 19 · TypeScript · Vite 8 · Tailwind v4 · react-router v7 · lucide-react
|
||||||
|
|
||||||
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs)
|
## Getting started
|
||||||
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/)
|
|
||||||
|
|
||||||
## React Compiler
|
|
||||||
|
|
||||||
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
|
|
||||||
|
|
||||||
## Expanding the ESLint configuration
|
|
||||||
|
|
||||||
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
|
|
||||||
|
|
||||||
```js
|
|
||||||
export default defineConfig([
|
|
||||||
globalIgnores(['dist']),
|
|
||||||
{
|
|
||||||
files: ['**/*.{ts,tsx}'],
|
|
||||||
extends: [
|
|
||||||
// Other configs...
|
|
||||||
|
|
||||||
// Remove tseslint.configs.recommended and replace with this
|
|
||||||
tseslint.configs.recommendedTypeChecked,
|
|
||||||
// Alternatively, use this for stricter rules
|
|
||||||
tseslint.configs.strictTypeChecked,
|
|
||||||
// Optionally, add this for stylistic rules
|
|
||||||
tseslint.configs.stylisticTypeChecked,
|
|
||||||
|
|
||||||
// Other configs...
|
|
||||||
],
|
|
||||||
languageOptions: {
|
|
||||||
parserOptions: {
|
|
||||||
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
|
||||||
tsconfigRootDir: import.meta.dirname,
|
|
||||||
},
|
|
||||||
// other options...
|
|
||||||
},
|
|
||||||
},
|
|
||||||
])
|
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
npm run dev # http://localhost:5173
|
||||||
```
|
```
|
||||||
|
|
||||||
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
|
Sign in with your sandbox credentials (email / password, optional org id). The JWT is stored in `localStorage` and shared across all three workflow clients.
|
||||||
|
|
||||||
```js
|
Other scripts:
|
||||||
// eslint.config.js
|
|
||||||
import reactX from 'eslint-plugin-react-x'
|
|
||||||
import reactDom from 'eslint-plugin-react-dom'
|
|
||||||
|
|
||||||
export default defineConfig([
|
```bash
|
||||||
globalIgnores(['dist']),
|
npm run build # tsc -b && vite build → dist/
|
||||||
{
|
npm run preview # serve the production build
|
||||||
files: ['**/*.{ts,tsx}'],
|
npm run lint # eslint
|
||||||
extends: [
|
```
|
||||||
// Other configs...
|
|
||||||
// Enable lint rules for React
|
## Workflows (app 434)
|
||||||
reactX.configs['recommended-typescript'],
|
|
||||||
// Enable lint rules for React DOM
|
The app has three workflows, each with its own `workflow_uuid` (shared `APP_ID` 434). One `ZinoClient` is bound per workflow.
|
||||||
reactDom.configs.recommended,
|
|
||||||
],
|
| Slug (`config.ts`) | UI | Record views | Detail views |
|
||||||
languageOptions: {
|
| --- | --- | --- | --- |
|
||||||
parserOptions: {
|
| `orderBooking` | Order Booking | Orders, Calls | Orders, Calls |
|
||||||
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
| `store` | Store | Store | Store |
|
||||||
tsconfigRootDir: import.meta.dirname,
|
| `dailyReports` | Daily Reports | Daily Logs | Daily Logs |
|
||||||
},
|
|
||||||
// other options...
|
All ids live in `src/api/config.ts`, sourced from the API docs in `src/docs/api/`.
|
||||||
},
|
|
||||||
},
|
## Structure
|
||||||
])
|
|
||||||
|
|
||||||
```
|
```
|
||||||
|
src/
|
||||||
|
api/
|
||||||
|
client.ts ZinoClient — HTTP client (login, views, activities, upload)
|
||||||
|
clients.ts per-workflow client singletons + shared auth (loginAll…)
|
||||||
|
config.ts app 434 ids: APP_ID, workflow uuids, activities, fields, views
|
||||||
|
types.ts API response types
|
||||||
|
auth/ AuthProvider + useAuth (JWT session)
|
||||||
|
components/
|
||||||
|
buttons/ Button, IconButton
|
||||||
|
reusable/ Card, Badge, Input, Select, Pagination, Modal, Spinner, EmptyState
|
||||||
|
rv/ generic RecordView + wired Orders/Calls/Stores/DailyLogs views
|
||||||
|
dv/ generic DetailView + wired Order/Call/Store/DailyLog details
|
||||||
|
screens/ LoginPage, ConsoleLayout, WorkflowPage, tab defs
|
||||||
|
lib/ cn (classnames), format (value formatting)
|
||||||
|
styles/ Tailwind v4 theme + design tokens
|
||||||
|
docs/api/ source API docs (one per workflow)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Routing
|
||||||
|
|
||||||
|
| Path | Screen |
|
||||||
|
| --- | --- |
|
||||||
|
| `/login` | Login gate |
|
||||||
|
| `/:tab` | Record view (`orders` · `calls` · `stores` · `daily`) |
|
||||||
|
| `/:tab/:instanceId` | Same list with the detail modal open (deep-linkable) |
|
||||||
|
|
||||||
|
Unknown paths redirect to `/orders`. Routes under the console require an authenticated session; a 401 bounces back to `/login`.
|
||||||
|
|
||||||
|
## Design system
|
||||||
|
|
||||||
|
Tailwind v4 with CSS-variable design tokens (`src/styles/`) — navy brand surface, sunrise accent gradient, slate neutrals. Utility names (`bg-navy-900`, `text-strong`, `shadow-md`, `rounded-lg`) resolve to the tokens via an `@theme` block. Referenced from the lead-to-policy console.
|
||||||
|
|||||||
406
package-lock.json
generated
406
package-lock.json
generated
@ -8,11 +8,14 @@
|
|||||||
"name": "frontend",
|
"name": "frontend",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"lucide-react": "^0.400.0",
|
||||||
"react": "^19.2.7",
|
"react": "^19.2.7",
|
||||||
"react-dom": "^19.2.7"
|
"react-dom": "^19.2.7",
|
||||||
|
"react-router-dom": "^7.18.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^10.0.1",
|
"@eslint/js": "^10.0.1",
|
||||||
|
"@tailwindcss/vite": "^4.0.0",
|
||||||
"@types/node": "^24.13.2",
|
"@types/node": "^24.13.2",
|
||||||
"@types/react": "^19.2.17",
|
"@types/react": "^19.2.17",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
@ -21,6 +24,7 @@
|
|||||||
"eslint-plugin-react-hooks": "^7.1.1",
|
"eslint-plugin-react-hooks": "^7.1.1",
|
||||||
"eslint-plugin-react-refresh": "^0.5.3",
|
"eslint-plugin-react-refresh": "^0.5.3",
|
||||||
"globals": "^17.7.0",
|
"globals": "^17.7.0",
|
||||||
|
"tailwindcss": "^4.0.0",
|
||||||
"typescript": "~6.0.2",
|
"typescript": "~6.0.2",
|
||||||
"typescript-eslint": "^8.62.0",
|
"typescript-eslint": "^8.62.0",
|
||||||
"vite": "^8.1.1"
|
"vite": "^8.1.1"
|
||||||
@ -837,6 +841,278 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@tailwindcss/node": {
|
||||||
|
"version": "4.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.2.tgz",
|
||||||
|
"integrity": "sha512-yWP/sqEcBLaD8JuA6zNwxoYKr75qxTioYwlRwekj5Jr/I5GXnoJfjetH/psLUIv74cYTH2lBUEzBkinthoYcBg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@jridgewell/remapping": "^2.3.5",
|
||||||
|
"enhanced-resolve": "5.21.6",
|
||||||
|
"jiti": "^2.7.0",
|
||||||
|
"lightningcss": "1.32.0",
|
||||||
|
"magic-string": "^0.30.21",
|
||||||
|
"source-map-js": "^1.2.1",
|
||||||
|
"tailwindcss": "4.3.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tailwindcss/oxide": {
|
||||||
|
"version": "4.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.2.tgz",
|
||||||
|
"integrity": "sha512-z8ZgnzX8gdNoWLBLqBPoh/sjnxkwvf9ZuWjnO0l0yIzbLa5/9S+eC5QxGZKRobVHIC3/1BoMWjHblqWjcgFgag==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 20"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@tailwindcss/oxide-android-arm64": "4.3.2",
|
||||||
|
"@tailwindcss/oxide-darwin-arm64": "4.3.2",
|
||||||
|
"@tailwindcss/oxide-darwin-x64": "4.3.2",
|
||||||
|
"@tailwindcss/oxide-freebsd-x64": "4.3.2",
|
||||||
|
"@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.2",
|
||||||
|
"@tailwindcss/oxide-linux-arm64-gnu": "4.3.2",
|
||||||
|
"@tailwindcss/oxide-linux-arm64-musl": "4.3.2",
|
||||||
|
"@tailwindcss/oxide-linux-x64-gnu": "4.3.2",
|
||||||
|
"@tailwindcss/oxide-linux-x64-musl": "4.3.2",
|
||||||
|
"@tailwindcss/oxide-wasm32-wasi": "4.3.2",
|
||||||
|
"@tailwindcss/oxide-win32-arm64-msvc": "4.3.2",
|
||||||
|
"@tailwindcss/oxide-win32-x64-msvc": "4.3.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tailwindcss/oxide-android-arm64": {
|
||||||
|
"version": "4.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.2.tgz",
|
||||||
|
"integrity": "sha512-WHxqIuHpvZ5VtdX6GTl1Ik/Vp2YuN42Et+0CdeaVd/frQ9jAvGmvR8vLT+jk3e8/Q3x8kECB9+R17pgpp2BulA==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 20"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tailwindcss/oxide-darwin-arm64": {
|
||||||
|
"version": "4.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.2.tgz",
|
||||||
|
"integrity": "sha512-GZypeUY/IDJW3877KeM+O67vbXr3MBnbtEL4aYhNErv/JWZhye2vGSWWG9tB6iiqR2MqRNkY8IOUy4NdSZV26w==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 20"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tailwindcss/oxide-darwin-x64": {
|
||||||
|
"version": "4.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.2.tgz",
|
||||||
|
"integrity": "sha512-UIIzmefR6KO1sDU7MzRqAxC8iBpft/VhkGjTjnhoS6k7Z3rQ9wEgA1ODSiyH/tcSYssulNm4Ci3hOeK1jH7ccQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 20"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tailwindcss/oxide-freebsd-x64": {
|
||||||
|
"version": "4.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.2.tgz",
|
||||||
|
"integrity": "sha512-GN+uAmcI6DNspnCDwtOAZrTz6oukJnp337qZvxqCGLd3BHBzJpO0ZbTLRvJNdztOeAmTzewewGIMPb0tk2R4WA==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"freebsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 20"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
|
||||||
|
"version": "4.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.2.tgz",
|
||||||
|
"integrity": "sha512-4ABn7qSbdHRwTiDiuWNegCyb5+2FJ4vKIKc3DmKrvAFw7MU1Lm11dIkTPwUaFdTzc7IsOpDbqBrlh0x6y36U/w==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 20"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
|
||||||
|
"version": "4.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.2.tgz",
|
||||||
|
"integrity": "sha512-wDgEIGwoM8w8pufh9LVt1PahDgNdKXrLC2qfAnV3vAmococ9RWbxeAw4pxPttd/TsJfwjyLf90Dg1y9y8I6Emw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 20"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tailwindcss/oxide-linux-arm64-musl": {
|
||||||
|
"version": "4.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.2.tgz",
|
||||||
|
"integrity": "sha512-J5Nuk0uZQIiMTJj3LEx4sAA9tMFUoXQZFv1J6An+QGYe53HKRJuFDi0rpq/tuouCZeAbOBY3kQ6g8qeD4TUjtA==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 20"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tailwindcss/oxide-linux-x64-gnu": {
|
||||||
|
"version": "4.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.2.tgz",
|
||||||
|
"integrity": "sha512-kqCZpSKOBEJO4mz7OqWoofBZeXTAwaVGPj0ErAj7CojmhKpWVWVOnrt9dE8odoIraZq4oj3ausM37kXi+Tow8w==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 20"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tailwindcss/oxide-linux-x64-musl": {
|
||||||
|
"version": "4.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.2.tgz",
|
||||||
|
"integrity": "sha512-cixpqbh2toJDmkuCRI68nXA8ZxNmdK9Y+9v5h3MC3ZQKy/0BO8AWzlkWyRM7JAFSGBlfig4YVTPsK6MVgqz1uw==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 20"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tailwindcss/oxide-wasm32-wasi": {
|
||||||
|
"version": "4.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.2.tgz",
|
||||||
|
"integrity": "sha512-4ec2Z/LOmRsAgU23CS4xeJfcJlmRg94A/XrbGRCF1gyU/zdDfRLYDVsS+ynSZCmGNxQ1jQriQOKMQeQxBA3Isw==",
|
||||||
|
"bundleDependencies": [
|
||||||
|
"@napi-rs/wasm-runtime",
|
||||||
|
"@emnapi/core",
|
||||||
|
"@emnapi/runtime",
|
||||||
|
"@tybys/wasm-util",
|
||||||
|
"@emnapi/wasi-threads",
|
||||||
|
"tslib"
|
||||||
|
],
|
||||||
|
"cpu": [
|
||||||
|
"wasm32"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@emnapi/core": "^1.11.1",
|
||||||
|
"@emnapi/runtime": "^1.11.1",
|
||||||
|
"@emnapi/wasi-threads": "^1.2.2",
|
||||||
|
"@napi-rs/wasm-runtime": "^1.1.4",
|
||||||
|
"@tybys/wasm-util": "^0.10.2",
|
||||||
|
"tslib": "^2.8.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
|
||||||
|
"version": "4.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.2.tgz",
|
||||||
|
"integrity": "sha512-Zyr/M0+XcYZu3bZrUytc7TXvrk0ftWfl8gN2MwekNDzhqhKRUucMPSeOzM0o0wH5AWOU49BsKRrfKxI2atCPMQ==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 20"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tailwindcss/oxide-win32-x64-msvc": {
|
||||||
|
"version": "4.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.2.tgz",
|
||||||
|
"integrity": "sha512-QI9BO7KlNZsp2GuO0jwAAj5jCDABOKXRkCk2XuKTSaNEFSdfzqswYVTtCHBNKHLsqyjFyFkqlDiwkNbTYSssMQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 20"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tailwindcss/vite": {
|
||||||
|
"version": "4.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.3.2.tgz",
|
||||||
|
"integrity": "sha512-eHpMeX4JXfVNJDEcsouTeCBubJBTcTLigeaw/NTUW6PB5ATKKXdyonnXgTBX2VuRbjz1hjfz6C5XAhr52ImQXA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@tailwindcss/node": "4.3.2",
|
||||||
|
"@tailwindcss/oxide": "4.3.2",
|
||||||
|
"tailwindcss": "4.3.2"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"vite": "^5.2.0 || ^6 || ^7 || ^8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@tybys/wasm-util": {
|
"node_modules/@tybys/wasm-util": {
|
||||||
"version": "0.10.3",
|
"version": "0.10.3",
|
||||||
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz",
|
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz",
|
||||||
@ -1306,6 +1582,19 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/cookie": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/cross-spawn": {
|
"node_modules/cross-spawn": {
|
||||||
"version": "7.0.6",
|
"version": "7.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||||
@ -1370,6 +1659,20 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/enhanced-resolve": {
|
||||||
|
"version": "5.21.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.6.tgz",
|
||||||
|
"integrity": "sha512-aNnGCvbJ/RIyWo1IuhNdVjnNF+EjH9wpzpNHt+ci/m9He9LJvUN8wrCcXjp9cWsGNAuvSpVFTx/vraAFQ8qGjQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"graceful-fs": "^4.2.4",
|
||||||
|
"tapable": "^2.3.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.13.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/escalade": {
|
"node_modules/escalade": {
|
||||||
"version": "3.2.0",
|
"version": "3.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
|
||||||
@ -1719,6 +2022,13 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/graceful-fs": {
|
||||||
|
"version": "4.2.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
||||||
|
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/hermes-estree": {
|
"node_modules/hermes-estree": {
|
||||||
"version": "0.25.1",
|
"version": "0.25.1",
|
||||||
"resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz",
|
"resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz",
|
||||||
@ -1786,6 +2096,16 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/jiti": {
|
||||||
|
"version": "2.7.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz",
|
||||||
|
"integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"jiti": "lib/jiti-cli.mjs"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/js-tokens": {
|
"node_modules/js-tokens": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||||
@ -2151,6 +2471,25 @@
|
|||||||
"yallist": "^3.0.2"
|
"yallist": "^3.0.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/lucide-react": {
|
||||||
|
"version": "0.400.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.400.0.tgz",
|
||||||
|
"integrity": "sha512-rpp7pFHh3Xd93KHixNgB0SqThMHpYNzsGUu69UaQbSZ75Q/J3m5t6EhKyMT3m4w2WOxmJ2mY0tD3vebnXqQryQ==",
|
||||||
|
"license": "ISC",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/magic-string": {
|
||||||
|
"version": "0.30.21",
|
||||||
|
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
|
||||||
|
"integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@jridgewell/sourcemap-codec": "^1.5.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/minimatch": {
|
"node_modules/minimatch": {
|
||||||
"version": "10.2.5",
|
"version": "10.2.5",
|
||||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
|
||||||
@ -2370,6 +2709,44 @@
|
|||||||
"react": "^19.2.7"
|
"react": "^19.2.7"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-router": {
|
||||||
|
"version": "7.18.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.1.tgz",
|
||||||
|
"integrity": "sha512-GDLgg3i3uM0aeJO3Fm+TCS+sDQ7gu12T6x0qdTEzcwqEfleci7JwugVNIF3U//0FWKnJT7ptG+20B2jfDqnZAg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"cookie": "^1.0.1",
|
||||||
|
"set-cookie-parser": "^2.6.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=18",
|
||||||
|
"react-dom": ">=18"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/react-router-dom": {
|
||||||
|
"version": "7.18.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.18.1.tgz",
|
||||||
|
"integrity": "sha512-KaZh+X/6UtEp28x51AUYZDMg9NGoz2ja3dNHa+ta/tk40vCzKhQ/RypCWBMLbmDr6//E24Vv5uPsrqXFozdkAg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"react-router": "7.18.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=18",
|
||||||
|
"react-dom": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/rolldown": {
|
"node_modules/rolldown": {
|
||||||
"version": "1.1.4",
|
"version": "1.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.4.tgz",
|
||||||
@ -2420,6 +2797,12 @@
|
|||||||
"semver": "bin/semver.js"
|
"semver": "bin/semver.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/set-cookie-parser": {
|
||||||
|
"version": "2.7.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz",
|
||||||
|
"integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/shebang-command": {
|
"node_modules/shebang-command": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||||
@ -2453,6 +2836,27 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/tailwindcss": {
|
||||||
|
"version": "4.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.2.tgz",
|
||||||
|
"integrity": "sha512-WtctNNSH8A9jlMIqxzuYumOHU5uGZyRv0Q5svQl+oEPy5w84YpBxdb7MdqyiSPQge5jTJ6zFQLq0PFygdccSBA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/tapable": {
|
||||||
|
"version": "2.3.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz",
|
||||||
|
"integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/webpack"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/tinyglobby": {
|
"node_modules/tinyglobby": {
|
||||||
"version": "0.2.17",
|
"version": "0.2.17",
|
||||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
|
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
|
||||||
|
|||||||
@ -10,11 +10,14 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"lucide-react": "^0.400.0",
|
||||||
"react": "^19.2.7",
|
"react": "^19.2.7",
|
||||||
"react-dom": "^19.2.7"
|
"react-dom": "^19.2.7",
|
||||||
|
"react-router-dom": "^7.18.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^10.0.1",
|
"@eslint/js": "^10.0.1",
|
||||||
|
"@tailwindcss/vite": "^4.0.0",
|
||||||
"@types/node": "^24.13.2",
|
"@types/node": "^24.13.2",
|
||||||
"@types/react": "^19.2.17",
|
"@types/react": "^19.2.17",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
@ -23,6 +26,7 @@
|
|||||||
"eslint-plugin-react-hooks": "^7.1.1",
|
"eslint-plugin-react-hooks": "^7.1.1",
|
||||||
"eslint-plugin-react-refresh": "^0.5.3",
|
"eslint-plugin-react-refresh": "^0.5.3",
|
||||||
"globals": "^17.7.0",
|
"globals": "^17.7.0",
|
||||||
|
"tailwindcss": "^4.0.0",
|
||||||
"typescript": "~6.0.2",
|
"typescript": "~6.0.2",
|
||||||
"typescript-eslint": "^8.62.0",
|
"typescript-eslint": "^8.62.0",
|
||||||
"vite": "^8.1.1"
|
"vite": "^8.1.1"
|
||||||
|
|||||||
132
src/App.tsx
132
src/App.tsx
@ -1,121 +1,23 @@
|
|||||||
import { useState } from 'react'
|
import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom'
|
||||||
import reactLogo from './assets/react.svg'
|
import { AuthProvider } from './auth/AuthProvider'
|
||||||
import viteLogo from './assets/vite.svg'
|
import { LoginPage } from './screens/LoginPage'
|
||||||
import heroImg from './assets/hero.png'
|
import { ConsoleLayout } from './screens/ConsoleLayout'
|
||||||
import './App.css'
|
import { WorkflowPage } from './screens/WorkflowPage'
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [count, setCount] = useState(0)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<AuthProvider>
|
||||||
<section id="center">
|
<BrowserRouter>
|
||||||
<div className="hero">
|
<Routes>
|
||||||
<img src={heroImg} className="base" width="170" height="179" alt="" />
|
<Route path="/login" element={<LoginPage />} />
|
||||||
<img src={reactLogo} className="framework" alt="React logo" />
|
<Route element={<ConsoleLayout />}>
|
||||||
<img src={viteLogo} className="vite" alt="Vite logo" />
|
<Route path="/:tab" element={<WorkflowPage />} />
|
||||||
</div>
|
<Route path="/:tab/:instanceId" element={<WorkflowPage />} />
|
||||||
<div>
|
</Route>
|
||||||
<h1>Get started</h1>
|
<Route path="*" element={<Navigate to="/orders" replace />} />
|
||||||
<p>
|
</Routes>
|
||||||
Edit <code>src/App.tsx</code> and save to test <code>HMR</code>
|
</BrowserRouter>
|
||||||
</p>
|
</AuthProvider>
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="counter"
|
|
||||||
onClick={() => setCount((count) => count + 1)}
|
|
||||||
>
|
|
||||||
Count is {count}
|
|
||||||
</button>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<div className="ticks"></div>
|
|
||||||
|
|
||||||
<section id="next-steps">
|
|
||||||
<div id="docs">
|
|
||||||
<svg className="icon" role="presentation" aria-hidden="true">
|
|
||||||
<use href="/icons.svg#documentation-icon"></use>
|
|
||||||
</svg>
|
|
||||||
<h2>Documentation</h2>
|
|
||||||
<p>Your questions, answered</p>
|
|
||||||
<ul>
|
|
||||||
<li>
|
|
||||||
<a href="https://vite.dev/" target="_blank">
|
|
||||||
<img className="logo" src={viteLogo} alt="" />
|
|
||||||
Explore Vite
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="https://react.dev/" target="_blank">
|
|
||||||
<img className="button-icon" src={reactLogo} alt="" />
|
|
||||||
Learn more
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div id="social">
|
|
||||||
<svg className="icon" role="presentation" aria-hidden="true">
|
|
||||||
<use href="/icons.svg#social-icon"></use>
|
|
||||||
</svg>
|
|
||||||
<h2>Connect with us</h2>
|
|
||||||
<p>Join the Vite community</p>
|
|
||||||
<ul>
|
|
||||||
<li>
|
|
||||||
<a href="https://github.com/vitejs/vite" target="_blank">
|
|
||||||
<svg
|
|
||||||
className="button-icon"
|
|
||||||
role="presentation"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<use href="/icons.svg#github-icon"></use>
|
|
||||||
</svg>
|
|
||||||
GitHub
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="https://chat.vite.dev/" target="_blank">
|
|
||||||
<svg
|
|
||||||
className="button-icon"
|
|
||||||
role="presentation"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<use href="/icons.svg#discord-icon"></use>
|
|
||||||
</svg>
|
|
||||||
Discord
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="https://x.com/vite_js" target="_blank">
|
|
||||||
<svg
|
|
||||||
className="button-icon"
|
|
||||||
role="presentation"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<use href="/icons.svg#x-icon"></use>
|
|
||||||
</svg>
|
|
||||||
X.com
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="https://bsky.app/profile/vite.dev" target="_blank">
|
|
||||||
<svg
|
|
||||||
className="button-icon"
|
|
||||||
role="presentation"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<use href="/icons.svg#bluesky-icon"></use>
|
|
||||||
</svg>
|
|
||||||
Bluesky
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<div className="ticks"></div>
|
|
||||||
<section id="spacer"></section>
|
|
||||||
</>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
287
src/api/client.ts
Normal file
287
src/api/client.ts
Normal file
@ -0,0 +1,287 @@
|
|||||||
|
import type {
|
||||||
|
ActivityResult,
|
||||||
|
AiDecision,
|
||||||
|
ApiError,
|
||||||
|
AuditEntry,
|
||||||
|
FormScreenResponse,
|
||||||
|
LoginResponse,
|
||||||
|
RecordViewParams,
|
||||||
|
RecordViewResponse,
|
||||||
|
User,
|
||||||
|
} from './types';
|
||||||
|
import { APP_ID } from './config';
|
||||||
|
|
||||||
|
const TOKEN_KEY = 'krishna_sales_token';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP client for the Zino gateway (sandbox).
|
||||||
|
*
|
||||||
|
* Routes are app-scoped (`/app/385/...`); login + ai-employee monitor are not.
|
||||||
|
* JWT persists in localStorage so the session survives refresh.
|
||||||
|
*/
|
||||||
|
export class ZinoClient {
|
||||||
|
readonly baseUrl: string;
|
||||||
|
readonly workflowUuid: string;
|
||||||
|
private token: string | null = null;
|
||||||
|
private onAuthError?: () => void;
|
||||||
|
|
||||||
|
constructor(baseUrl: string, workflowUuid: string, onAuthError?: () => void) {
|
||||||
|
this.baseUrl = baseUrl.replace(/\/+$/, '');
|
||||||
|
this.workflowUuid = workflowUuid;
|
||||||
|
this.onAuthError = onAuthError;
|
||||||
|
if (typeof window !== 'undefined') this.token = localStorage.getItem(TOKEN_KEY);
|
||||||
|
}
|
||||||
|
|
||||||
|
setAuthErrorHandler(fn: () => void): void {
|
||||||
|
this.onAuthError = fn;
|
||||||
|
}
|
||||||
|
|
||||||
|
setToken(token: string | null): void {
|
||||||
|
this.token = token;
|
||||||
|
if (typeof window === 'undefined') return;
|
||||||
|
if (token) localStorage.setItem(TOKEN_KEY, token);
|
||||||
|
else localStorage.removeItem(TOKEN_KEY);
|
||||||
|
}
|
||||||
|
|
||||||
|
getToken(): string | null {
|
||||||
|
return this.token;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async request<T>(method: string, path: string, body?: unknown): Promise<T> {
|
||||||
|
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
|
||||||
|
if (this.token) headers['Authorization'] = `Bearer ${this.token}`;
|
||||||
|
|
||||||
|
return this.send<T>(method, path, {
|
||||||
|
headers,
|
||||||
|
body: body !== undefined ? JSON.stringify(body) : undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Multipart variant — no Content-Type header (the browser sets the boundary).
|
||||||
|
private async requestForm<T>(path: string, form: FormData): Promise<T> {
|
||||||
|
const headers: Record<string, string> = {};
|
||||||
|
if (this.token) headers['Authorization'] = `Bearer ${this.token}`;
|
||||||
|
return this.send<T>('POST', path, { headers, body: form });
|
||||||
|
}
|
||||||
|
|
||||||
|
private async send<T>(method: string, path: string, init: RequestInit): Promise<T> {
|
||||||
|
const res = await fetch(`${this.baseUrl}${path}`, { method, ...init });
|
||||||
|
|
||||||
|
if (res.status === 401) {
|
||||||
|
this.setToken(null);
|
||||||
|
this.onAuthError?.();
|
||||||
|
throw { status: 401, message: 'Unauthorized' } as ApiError;
|
||||||
|
}
|
||||||
|
if (!res.ok) {
|
||||||
|
let message = res.statusText;
|
||||||
|
try {
|
||||||
|
const j = (await res.json()) as { error?: string };
|
||||||
|
if (j.error) message = j.error;
|
||||||
|
} catch {
|
||||||
|
/* non-JSON body */
|
||||||
|
}
|
||||||
|
throw { status: res.status, message } as ApiError;
|
||||||
|
}
|
||||||
|
if (res.status === 204) return undefined as T;
|
||||||
|
return (await res.json()) as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Auth ---
|
||||||
|
|
||||||
|
async login(email: string, password: string, orgId?: string): Promise<LoginResponse> {
|
||||||
|
const res = await this.request<LoginResponse>('POST', '/usr/login', {
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
...(orgId ? { org_id: Number(orgId) } : {}),
|
||||||
|
});
|
||||||
|
this.setToken(res.token);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
logout(): void {
|
||||||
|
this.setToken(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Decode the persisted JWT into a User (no network). */
|
||||||
|
currentUser(): User | null {
|
||||||
|
if (!this.token) return null;
|
||||||
|
try {
|
||||||
|
const p = JSON.parse(atob(this.token.split('.')[1]));
|
||||||
|
return {
|
||||||
|
id: String(p.user_id ?? p.sub ?? ''),
|
||||||
|
org_id: String(p.org_id ?? ''),
|
||||||
|
name: p.name ?? '',
|
||||||
|
email: p.email ?? '',
|
||||||
|
roles: p.roles ?? [],
|
||||||
|
groups: p.groups ?? [],
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Views ---
|
||||||
|
|
||||||
|
recordView(rvUid: string, params: RecordViewParams = {}): Promise<RecordViewResponse> {
|
||||||
|
return this.request<RecordViewResponse>('POST', `/app/${APP_ID}/view/recordview`, {
|
||||||
|
rv_template_uid: rvUid,
|
||||||
|
search_query: {
|
||||||
|
page: params.page ?? 1,
|
||||||
|
limit: params.limit ?? 50,
|
||||||
|
sort_by: params.sortBy ?? '',
|
||||||
|
sort_dir: params.sortDir ?? 'desc',
|
||||||
|
search: params.search ?? '',
|
||||||
|
filters: (params.filters ?? []).map((f) => ({
|
||||||
|
field_key: f.field_key,
|
||||||
|
value: f.value,
|
||||||
|
value2: '',
|
||||||
|
data_type: f.data_type ?? 'string',
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
detailView(dvUid: string, instanceId: number | string): Promise<{
|
||||||
|
config: { fields: Array<{ field_key: string; output_label: string; data_type: string }> };
|
||||||
|
data: Record<string, unknown>;
|
||||||
|
}> {
|
||||||
|
return this.request(
|
||||||
|
'GET',
|
||||||
|
`/app/${APP_ID}/view/detailview/${dvUid}?instance_id=${encodeURIComponent(String(instanceId))}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
audit(instanceId: number | string): Promise<AuditEntry[]> {
|
||||||
|
return this.request<AuditEntry[]>(
|
||||||
|
'GET',
|
||||||
|
`/app/${APP_ID}/view/audit?instance_id=${encodeURIComponent(String(instanceId))}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** AI-employee decision stream for an instance (public monitor endpoint). */
|
||||||
|
aiDecisions(instanceId: number | string): Promise<AiDecision[]> {
|
||||||
|
return this.request<{ decisions: AiDecision[] }>(
|
||||||
|
'GET',
|
||||||
|
`/monitor/decisions?instance_id=${encodeURIComponent(String(instanceId))}`,
|
||||||
|
).then((r) => r.decisions ?? []);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Form schema (view-service) ---
|
||||||
|
|
||||||
|
/** Live activity form config — fields, types, options, lookup/ocr config and
|
||||||
|
* the designer layout. The single source of truth for rendering a form. */
|
||||||
|
formSchema(activityId: string, instanceId?: number | string): Promise<FormScreenResponse> {
|
||||||
|
return this.request<FormScreenResponse>('POST', `/app/${APP_ID}/view/form-screens`, {
|
||||||
|
activity_id: activityId,
|
||||||
|
device_type: 'desktop',
|
||||||
|
...(instanceId != null ? { instance_id: instanceId } : {}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Workflow execution (core) ---
|
||||||
|
|
||||||
|
startInstance(activityUid: string, data: Record<string, unknown>): Promise<ActivityResult> {
|
||||||
|
return this.request<ActivityResult>('POST', `/app/${APP_ID}/start`, {
|
||||||
|
workflow_uuid: this.workflowUuid,
|
||||||
|
activity_id: activityUid,
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
performActivity(
|
||||||
|
instanceId: number | string,
|
||||||
|
activityUid: string,
|
||||||
|
data: Record<string, unknown>,
|
||||||
|
): Promise<ActivityResult> {
|
||||||
|
return this.request<ActivityResult>('POST', `/app/${APP_ID}/activity`, {
|
||||||
|
workflow_uuid: this.workflowUuid,
|
||||||
|
instance_id: typeof instanceId === 'string' ? Number(instanceId) || instanceId : instanceId,
|
||||||
|
activity_id: activityUid,
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- RDBMS lookup records (owner-agent picker etc.) ---
|
||||||
|
|
||||||
|
/** Search an RDBMS lookup template. Returns the matching rows. `formData`
|
||||||
|
* carries the current form values so the server can apply the activity's
|
||||||
|
* `field_rules` filter_options (dependent/cascading lookups, e.g. owner
|
||||||
|
* agents scoped to the chosen region). */
|
||||||
|
lookupRecords(
|
||||||
|
templateUid: string,
|
||||||
|
opts: {
|
||||||
|
activityId: string;
|
||||||
|
fieldId: string;
|
||||||
|
search?: string;
|
||||||
|
instanceId?: number | string;
|
||||||
|
limit?: number;
|
||||||
|
formData?: Record<string, unknown>;
|
||||||
|
},
|
||||||
|
): Promise<{ records: Array<Record<string, unknown>> }> {
|
||||||
|
return this.request('POST', `/app/${APP_ID}/rdbms-templates/${templateUid}/records`, {
|
||||||
|
workflow_uuid: this.workflowUuid,
|
||||||
|
activity_id: opts.activityId,
|
||||||
|
field_id: opts.fieldId,
|
||||||
|
instance_id: opts.instanceId || undefined,
|
||||||
|
search: opts.search ?? '',
|
||||||
|
limit: opts.limit ?? 50,
|
||||||
|
offset: 0,
|
||||||
|
form_data: opts.formData ?? {},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Dataset-backed select options (state/city cascade etc.) ---
|
||||||
|
|
||||||
|
/** Fetch options for a dataset-backed select. The server resolves the field's
|
||||||
|
* dataset + filter_options from `activityId`/`fieldId`, applying the activity's
|
||||||
|
* rules against `formData` — so e.g. City options come back already scoped to
|
||||||
|
* the chosen State. Returns `{label, value}` rows (plus the raw dataset row). */
|
||||||
|
datasetOptions(opts: {
|
||||||
|
activityId: string;
|
||||||
|
fieldId: string;
|
||||||
|
search?: string;
|
||||||
|
instanceId?: number | string;
|
||||||
|
limit?: number;
|
||||||
|
formData?: Record<string, unknown>;
|
||||||
|
}): Promise<{ options: Array<{ label: string; value: string; _raw?: Record<string, string> }> }> {
|
||||||
|
return this.request('POST', `/app/${APP_ID}/dataset-options`, {
|
||||||
|
workflow_uuid: this.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 ?? {},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- File upload + OCR (multipart, field-scoped) ---
|
||||||
|
|
||||||
|
private fieldForm(file: File, ctx: FieldContext): FormData {
|
||||||
|
const form = new FormData();
|
||||||
|
form.append('file', file);
|
||||||
|
form.append('workflow_uuid', this.workflowUuid);
|
||||||
|
form.append('activity_id', ctx.activityId);
|
||||||
|
form.append('field_id', ctx.fieldId);
|
||||||
|
if (ctx.instanceId != null) form.append('instance_id', String(ctx.instanceId));
|
||||||
|
return form;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Upload a form-field file; returns the FileMeta to store as the field value. */
|
||||||
|
uploadFile(file: File, ctx: FieldContext): Promise<Record<string, unknown>> {
|
||||||
|
return this.requestForm(`/app/${APP_ID}/upload`, this.fieldForm(file, ctx));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Run OCR on a document; `extracted` is keyed by the field's extraction keys. */
|
||||||
|
extractOcr(file: File, ctx: FieldContext): Promise<{ extracted: Record<string, unknown>; raw?: string; parse_error?: string }> {
|
||||||
|
return this.requestForm(`/app/${APP_ID}/ocr-extract`, this.fieldForm(file, ctx));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Identifiers the field-scoped CORE endpoints resolve config + RBAC from. */
|
||||||
|
export interface FieldContext {
|
||||||
|
activityId: string;
|
||||||
|
fieldId: string;
|
||||||
|
instanceId?: number | string;
|
||||||
|
}
|
||||||
49
src/api/clients.ts
Normal file
49
src/api/clients.ts
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
// Per-workflow ZinoClient singletons. app 434 has three workflows, each with
|
||||||
|
// its own workflow_uuid, so each needs its own client (ZinoClient binds one
|
||||||
|
// workflow uuid at construction). Import the one you need, or use clientFor().
|
||||||
|
import { ZinoClient } from './client';
|
||||||
|
import { BASE_URL, WORKFLOWS } from './config';
|
||||||
|
|
||||||
|
export type WorkflowSlug = keyof typeof WORKFLOWS;
|
||||||
|
|
||||||
|
const cache = new Map<WorkflowSlug, ZinoClient>();
|
||||||
|
|
||||||
|
/** Shared client for a workflow (memoized so the JWT/session is reused). */
|
||||||
|
export function clientFor(slug: WorkflowSlug): ZinoClient {
|
||||||
|
let c = cache.get(slug);
|
||||||
|
if (!c) {
|
||||||
|
c = new ZinoClient(BASE_URL, WORKFLOWS[slug].workflowUuid);
|
||||||
|
cache.set(slug, c);
|
||||||
|
}
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const orderBookingClient = clientFor('orderBooking');
|
||||||
|
export const storeClient = clientFor('store');
|
||||||
|
export const dailyReportsClient = clientFor('dailyReports');
|
||||||
|
|
||||||
|
// 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];
|
||||||
|
|
||||||
|
/** Log in once and share the JWT with every workflow client. */
|
||||||
|
export async function loginAll(email: string, password: string, orgId?: string) {
|
||||||
|
const res = await orderBookingClient.login(email, password, orgId);
|
||||||
|
ALL.forEach((c) => c.setToken(res.token));
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Clear the session on every client. */
|
||||||
|
export function logoutAll(): void {
|
||||||
|
ALL.forEach((c) => c.setToken(null));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Current shared token (null when logged out). */
|
||||||
|
export function currentToken(): string | null {
|
||||||
|
return orderBookingClient.getToken();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Register a 401 handler on every client (e.g. to bounce back to login). */
|
||||||
|
export function onAuthErrorAll(fn: () => void): void {
|
||||||
|
ALL.forEach((c) => c.setAuthErrorHandler(fn));
|
||||||
|
}
|
||||||
167
src/api/config.ts
Normal file
167
src/api/config.ts
Normal file
@ -0,0 +1,167 @@
|
|||||||
|
// Krishna Sales — sandbox app 434 (base https://sandbox.getzino.in).
|
||||||
|
// Three workflows share the app: Order Booking, Store, Daily Reports. Each has
|
||||||
|
// its own workflow_uuid + version_uuid; instantiate one ZinoClient per workflow
|
||||||
|
// (new ZinoClient(baseUrl, WORKFLOWS.<x>.workflowUuid)). All ids from the API
|
||||||
|
// docs under src/docs/api.
|
||||||
|
|
||||||
|
export const APP_ID = '434';
|
||||||
|
export const BASE_URL = 'https://sandbox.getzino.in';
|
||||||
|
|
||||||
|
// --- Order Booking (src/docs/api/order_booking.md) ---
|
||||||
|
export const ORDER_BOOKING = {
|
||||||
|
workflowUuid: 'b5a21627-9422-40cf-b7f5-31fa070bf42f',
|
||||||
|
versionUuid: 'f19b2146-88e5-448a-9a51-e517ebce680a',
|
||||||
|
activities: {
|
||||||
|
LOG_VISIT: {
|
||||||
|
uid: '42b7f47d-96f0-4289-a6d9-38f455ad57dd', // init
|
||||||
|
fields: {
|
||||||
|
selectStore: 'select_store', // wf_lookup
|
||||||
|
dateOfVisit: 'date_of_visit', // date
|
||||||
|
timeOfVisit: 'time_of_visit', // time
|
||||||
|
uploadImage: 'upload_image', // image
|
||||||
|
},
|
||||||
|
},
|
||||||
|
PLACE_ORDER: {
|
||||||
|
uid: 'f66403d7-e31f-4d64-bcfd-ff5485aec8b8',
|
||||||
|
fields: {
|
||||||
|
dateOfOrder: 'date_of_order', // date
|
||||||
|
orderDetails: 'order_details', // grid: br_code, sku_code, product_category, product_name, sku, bags
|
||||||
|
totalBags: 'total_bags', // number
|
||||||
|
totalKgs: 'total_kgs', // number
|
||||||
|
},
|
||||||
|
},
|
||||||
|
PRODUCTIVITY_OF_VISIT: {
|
||||||
|
uid: '11bd10f9-a001-470e-867f-33dee8eabe4b',
|
||||||
|
fields: {
|
||||||
|
isProductiveCall: 'is_productive_call', // radio
|
||||||
|
storeStatus: 'store_status', // select
|
||||||
|
orderReceivedChannel: 'order_received_channel', // select
|
||||||
|
remarks: 'remarks', // longtext
|
||||||
|
action: 'action', // radio
|
||||||
|
},
|
||||||
|
},
|
||||||
|
POTENTIAL_MINING: {
|
||||||
|
uid: 'af8ac8df-d868-4f7d-88b2-123955d69c56',
|
||||||
|
fields: {
|
||||||
|
potential: 'potential', // grid: product_category_, total_ordered, actual_potential, difference, reason, potential_remarks
|
||||||
|
},
|
||||||
|
},
|
||||||
|
CLOSE_ORDER: {
|
||||||
|
uid: '8580d7a2-5d31-40ab-b7ea-2a38d144e59a',
|
||||||
|
fields: {
|
||||||
|
remarks: 'remarks_2', // longtext
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
recordViews: {
|
||||||
|
ORDERS: '1e424561-9a27-44f5-9b68-64d63296d837',
|
||||||
|
CALLS: 'b4d7a710-24e7-416d-885a-31fd1e727aab',
|
||||||
|
},
|
||||||
|
detailViews: {
|
||||||
|
ORDERS: '0804c6c3-6cf9-4050-94bb-fa48dd5de87d',
|
||||||
|
CALLS: '09a32bfe-9e43-4e0f-bb7d-db89c8a2557c',
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// --- Store (src/docs/api/store.md) ---
|
||||||
|
export const STORE = {
|
||||||
|
workflowUuid: '2f827d27-b7a4-4ed9-9336-879fa46dbcba',
|
||||||
|
versionUuid: 'ced1fb5b-c6a4-4305-b85d-3c263d9d618e',
|
||||||
|
activities: {
|
||||||
|
// Init (fields carry the _2 suffix).
|
||||||
|
INIT: {
|
||||||
|
uid: '8626a77f-9e70-448e-8ac9-2c51eae5015b', // init
|
||||||
|
fields: {
|
||||||
|
businessName: 'business_name_2', // text
|
||||||
|
area: 'area_2', // text
|
||||||
|
completeAddress: 'complete_address_2', // longtext
|
||||||
|
pinCode: 'pin_code_2', // number
|
||||||
|
ownerName: 'owner_name_2', // text
|
||||||
|
phoneNumber: 'phone_number_2', // phone
|
||||||
|
email: 'email_2', // email
|
||||||
|
storeLocation: 'store_location_2', // geolocation
|
||||||
|
storeImage: 'store_image_2', // image
|
||||||
|
routeName: 'route_name_2', // select
|
||||||
|
routeCode: 'route_code_2', // select
|
||||||
|
subRoute: 'sub_route_2', // select
|
||||||
|
distributorName: 'distributor_name_2', // select
|
||||||
|
distributorOwnerName: 'distributor_owner_name_2', // text
|
||||||
|
distributorEmail: 'distributor_email_2', // email
|
||||||
|
distributorPhoneNumber: 'distributor_phone_number_2', // phone
|
||||||
|
notes: 'notes_2', // longtext
|
||||||
|
potential: 'potential_2', // grid: product_category_1, quantity_1
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Edit (same fields, _3 suffix).
|
||||||
|
EDIT_STORE: {
|
||||||
|
uid: '87647fae-3b07-445f-9615-21881b163276',
|
||||||
|
fields: {
|
||||||
|
businessName: 'business_name_3', // text
|
||||||
|
area: 'area_3', // text
|
||||||
|
completeAddress: 'complete_address_3', // longtext
|
||||||
|
pinCode: 'pin_code_3', // number
|
||||||
|
ownerName: 'owner_name_3', // text
|
||||||
|
phoneNumber: 'phone_number_3', // phone
|
||||||
|
email: 'email_3', // email
|
||||||
|
storeLocation: 'store_location_3', // geolocation
|
||||||
|
storeImage: 'store_image_3', // image
|
||||||
|
routeName: 'route_name_3', // select
|
||||||
|
routeCode: 'route_code_3', // select
|
||||||
|
subRoute: 'sub_route_3', // select
|
||||||
|
distributorName: 'distributor_name_3', // select
|
||||||
|
distributorOwnerName: 'distributor_owner_name_3', // text
|
||||||
|
distributorEmail: 'distributor_email_3', // email
|
||||||
|
distributorPhoneNumber: 'distributor_phone_number_3', // phone
|
||||||
|
notes: 'notes_3', // longtext
|
||||||
|
potential: 'potential_3', // grid: col_1, col_2
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
recordViews: {
|
||||||
|
STORE: 'c03b411a-8454-4d8b-8220-00095e92a5de',
|
||||||
|
},
|
||||||
|
detailViews: {
|
||||||
|
STORE: 'af868269-d340-4ea9-9793-5fabe4f6ae27',
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// --- Daily Reports (src/docs/api/daily_reports.md) ---
|
||||||
|
export const DAILY_REPORTS = {
|
||||||
|
workflowUuid: 'c420dc04-2ba3-488e-902d-66bc9a2032cd',
|
||||||
|
versionUuid: 'd4d4c738-bf74-4b44-a297-9a6653458f3a',
|
||||||
|
activities: {
|
||||||
|
INIT: {
|
||||||
|
uid: 'ac04a444-9b96-463d-bc91-447fa80b028f', // init
|
||||||
|
fields: {
|
||||||
|
salesOfficerName: 'sales_officer_name', // app_user
|
||||||
|
date: 'date', // date
|
||||||
|
time: 'field_1', // time
|
||||||
|
routeCode: 'route_code', // select
|
||||||
|
subRoute: 'sub_route', // select
|
||||||
|
storeImage: 'store_image', // image
|
||||||
|
dayPlanNotes: 'day_plan_notes', // longtext
|
||||||
|
},
|
||||||
|
},
|
||||||
|
PUNCH_OUT: {
|
||||||
|
uid: '37c218fc-fd91-41d1-9964-fcaeade54e74',
|
||||||
|
fields: {
|
||||||
|
totalProductiveCalls: 'total_productive_calls', // number
|
||||||
|
totalNonProductiveCalls: 'total_non_productive_calls', // number
|
||||||
|
eodNotesRemarks: 'eod_notes_remarks', // longtext
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
recordViews: {
|
||||||
|
DAILY_LOGS: '269da629-1701-477a-a37b-a78bb056d4b3',
|
||||||
|
},
|
||||||
|
detailViews: {
|
||||||
|
DAILY_LOGS: '26b3c6de-a1d7-4f71-be88-7e4929932b0b',
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// All workflows keyed by slug, for generic lookup.
|
||||||
|
export const WORKFLOWS = {
|
||||||
|
orderBooking: ORDER_BOOKING,
|
||||||
|
store: STORE,
|
||||||
|
dailyReports: DAILY_REPORTS,
|
||||||
|
} as const;
|
||||||
191
src/api/types.ts
Normal file
191
src/api/types.ts
Normal file
@ -0,0 +1,191 @@
|
|||||||
|
// Live API shapes + the SDK's domain types.
|
||||||
|
|
||||||
|
export interface ApiError {
|
||||||
|
status: number;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface User {
|
||||||
|
id: string;
|
||||||
|
org_id: string;
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
roles: string[];
|
||||||
|
groups: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LoginResponse {
|
||||||
|
token: string;
|
||||||
|
user: User;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Record view (POST /app/{appId}/view/recordview) ---
|
||||||
|
|
||||||
|
export interface RecordViewField {
|
||||||
|
field_key: string;
|
||||||
|
output_label: string;
|
||||||
|
data_type: string;
|
||||||
|
is_filter: boolean;
|
||||||
|
is_search: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RecordViewResponse {
|
||||||
|
config: { fields: RecordViewField[] };
|
||||||
|
data: Record<string, unknown>[];
|
||||||
|
pagination?: { page: number; limit: number; total_count: number; total_pages: number };
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RecordViewParams {
|
||||||
|
page?: number;
|
||||||
|
limit?: number;
|
||||||
|
sortBy?: string;
|
||||||
|
sortDir?: 'asc' | 'desc';
|
||||||
|
search?: string;
|
||||||
|
filters?: Array<{ field_key: string; value: string; data_type?: string }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Form schema (POST /app/{appId}/view/form-screens) ---
|
||||||
|
// The live activity form config. `fields` is the metadata pool; `grid_config`
|
||||||
|
// is the designer's layout and decides which fields are user-facing.
|
||||||
|
|
||||||
|
export interface FormScreenFieldProps {
|
||||||
|
options?: Array<{ label: string; value: string }>;
|
||||||
|
/** Dataset-backed select/multiselect: options come live from
|
||||||
|
* /dataset-options (filter_options-aware), not the static `options` above.
|
||||||
|
* `display_keys` are joined for the label; `value_key` is the stored value. */
|
||||||
|
option_source?: string; // 'dataset' | (else inline)
|
||||||
|
dataset_uuid?: string;
|
||||||
|
display_keys?: string[];
|
||||||
|
value_key?: string;
|
||||||
|
lookup_config?: {
|
||||||
|
rdbms_template_uuid: string;
|
||||||
|
display_columns?: Array<{ name: string }>;
|
||||||
|
storage_columns?: Array<{ name: string }>;
|
||||||
|
};
|
||||||
|
ocr_config?: {
|
||||||
|
template?: string;
|
||||||
|
field_mappings?: Array<{ target_field: string; extraction_key: string }>;
|
||||||
|
};
|
||||||
|
source?: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FormScreenField {
|
||||||
|
id: string;
|
||||||
|
uid: string;
|
||||||
|
name: string;
|
||||||
|
data_type: string;
|
||||||
|
mandatory?: boolean;
|
||||||
|
/** Companion/record column this field writes — the canonical seed key. */
|
||||||
|
mapped_workflow_field?: string;
|
||||||
|
type?: string; // 'mapped' | 'local'
|
||||||
|
properties?: FormScreenFieldProps | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FormScreenGridItem {
|
||||||
|
type: string; // 'field' | ...
|
||||||
|
fieldUid: string;
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** One condition of a field_rule filter_config. The lookup row's
|
||||||
|
* `target_column` is compared (currently only `equals`) against the current
|
||||||
|
* value of `form_field`. When the source field is itself a lookup/object,
|
||||||
|
* `source_value_key` names the sub-key to read off it (e.g. a region lookup's
|
||||||
|
* "region" column); empty means use the form value directly. */
|
||||||
|
export interface FieldRuleCondition {
|
||||||
|
operator: string;
|
||||||
|
form_field: string;
|
||||||
|
target_column: string;
|
||||||
|
source_value_key?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A designer field_rule. We honour `action: "filter_options"`, which scopes a
|
||||||
|
* target field's options/lookup rows by another field's current value. */
|
||||||
|
export interface FieldRule {
|
||||||
|
id: string;
|
||||||
|
action: string;
|
||||||
|
active?: boolean;
|
||||||
|
target_field: string;
|
||||||
|
filter_config?: {
|
||||||
|
logic?: string; // "AND" | "OR"
|
||||||
|
conditions?: FieldRuleCondition[];
|
||||||
|
} | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Per-field designer defaults, keyed by field id. `hidden` drops the field
|
||||||
|
* from the form UI while its mapped value is still seeded + submitted. */
|
||||||
|
export interface FieldDefault {
|
||||||
|
hidden?: boolean;
|
||||||
|
/** Read-only in the form — value is computed (e.g. premium_amount by custom_js). */
|
||||||
|
disabled?: boolean;
|
||||||
|
value?: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FormScreenResponse {
|
||||||
|
activity_uid: string;
|
||||||
|
activity_name: string;
|
||||||
|
fields: FormScreenField[];
|
||||||
|
grid_config?: FormScreenGridItem[];
|
||||||
|
/** Designer field_rules (cascading filter_options etc.). */
|
||||||
|
field_rules?: FieldRule[];
|
||||||
|
/** Per-field designer defaults (e.g. `{ assign_city: { hidden: true } }`). */
|
||||||
|
field_defaults?: Record<string, FieldDefault>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Audit (GET /app/{appId}/view/audit) ---
|
||||||
|
|
||||||
|
export interface AuditEntry {
|
||||||
|
id: number;
|
||||||
|
user_id: string;
|
||||||
|
user_roles: string[];
|
||||||
|
activity_id: string;
|
||||||
|
data: Record<string, unknown>;
|
||||||
|
execution_state?: string;
|
||||||
|
created_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- AI decisions (GET /monitor/decisions) ---
|
||||||
|
|
||||||
|
export interface AiDecision {
|
||||||
|
id: number;
|
||||||
|
ai_user_id: string;
|
||||||
|
instance_id: string;
|
||||||
|
activity_id: string;
|
||||||
|
activity_name?: string;
|
||||||
|
/** Server-resolved display name for the AI employee (preferred over the
|
||||||
|
* static AI_EMPLOYEES map). */
|
||||||
|
employee_name?: string;
|
||||||
|
reasoning: string;
|
||||||
|
confidence: number;
|
||||||
|
status: string;
|
||||||
|
instance_state?: string;
|
||||||
|
knowledge_sources?: unknown;
|
||||||
|
tool_calls?: unknown;
|
||||||
|
/** The activity payload the AI employee submitted — e.g. the Aria Advisor
|
||||||
|
* product recommendation carries ai_recommended_product_input /
|
||||||
|
* ai_suggested_sum_assured_input / ai_recommendation_confidence_input here. */
|
||||||
|
data?: Record<string, unknown>;
|
||||||
|
created_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Workflow execution (core) ---
|
||||||
|
|
||||||
|
export interface ActivityResult {
|
||||||
|
success?: boolean;
|
||||||
|
instance_id?: number | string;
|
||||||
|
data?: Record<string, unknown>;
|
||||||
|
message?: string;
|
||||||
|
status_code?: number;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Several recordview fields come back as structured objects, not scalars.
|
||||||
|
// Normalize via the helpers in adapters.ts before rendering/seeding.
|
||||||
|
export interface PhoneValue {
|
||||||
|
dial_code?: string;
|
||||||
|
phone?: string;
|
||||||
|
phone_with_dial_code?: string;
|
||||||
|
}
|
||||||
|
|
||||||
21
src/auth/AuthProvider.tsx
Normal file
21
src/auth/AuthProvider.tsx
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { useState, type ReactNode } from 'react';
|
||||||
|
import { loginAll, logoutAll, currentToken } from '../api/clients';
|
||||||
|
import { AuthCtx, type AuthValue } from './context';
|
||||||
|
|
||||||
|
export function AuthProvider({ children }: { children: ReactNode }) {
|
||||||
|
const [authed, setAuthed] = useState(() => !!currentToken());
|
||||||
|
|
||||||
|
const value: AuthValue = {
|
||||||
|
authed,
|
||||||
|
login: async (email, password, orgId) => {
|
||||||
|
await loginAll(email, password, orgId);
|
||||||
|
setAuthed(true);
|
||||||
|
},
|
||||||
|
logout: () => {
|
||||||
|
logoutAll();
|
||||||
|
setAuthed(false);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return <AuthCtx.Provider value={value}>{children}</AuthCtx.Provider>;
|
||||||
|
}
|
||||||
15
src/auth/context.ts
Normal file
15
src/auth/context.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { createContext, useContext } from 'react';
|
||||||
|
|
||||||
|
export interface AuthValue {
|
||||||
|
authed: boolean;
|
||||||
|
login: (email: string, password: string, orgId?: string) => Promise<void>;
|
||||||
|
logout: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AuthCtx = createContext<AuthValue | null>(null);
|
||||||
|
|
||||||
|
export function useAuth(): AuthValue {
|
||||||
|
const v = useContext(AuthCtx);
|
||||||
|
if (!v) throw new Error('useAuth must be used within <AuthProvider>');
|
||||||
|
return v;
|
||||||
|
}
|
||||||
65
src/components/buttons/Button.tsx
Normal file
65
src/components/buttons/Button.tsx
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import type { ButtonHTMLAttributes, ReactNode } from 'react';
|
||||||
|
import { cn } from '../../lib/cn';
|
||||||
|
|
||||||
|
export type ButtonVariant = 'primary' | 'navy' | 'secondary' | 'ghost' | 'danger';
|
||||||
|
export type ButtonSize = 'sm' | 'md' | 'lg';
|
||||||
|
|
||||||
|
export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||||
|
/** Visual style. @default "primary" */
|
||||||
|
variant?: ButtonVariant;
|
||||||
|
/** @default "md" */
|
||||||
|
size?: ButtonSize;
|
||||||
|
iconLeft?: ReactNode;
|
||||||
|
iconRight?: ReactNode;
|
||||||
|
/** Stretch to container width. @default false */
|
||||||
|
full?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SIZES: Record<ButtonSize, string> = {
|
||||||
|
sm: 'h-[34px] px-3.5 text-[13px] rounded-sm',
|
||||||
|
md: 'h-[42px] px-[18px] text-base rounded-md',
|
||||||
|
lg: 'h-[50px] px-6 text-md rounded-md',
|
||||||
|
};
|
||||||
|
|
||||||
|
const VARIANTS: Record<ButtonVariant, string> = {
|
||||||
|
primary: 'bg-sunrise text-white border border-transparent shadow-sunrise',
|
||||||
|
navy: 'bg-navy-900 text-on-navy border border-transparent shadow-sm',
|
||||||
|
secondary: 'bg-card text-strong border border-border-default shadow-xs',
|
||||||
|
ghost: 'bg-transparent text-body border border-transparent',
|
||||||
|
danger: 'bg-ruby-600 text-white border border-transparent shadow-sm',
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Primary action button. Sunrise gradient for primary, solid navy for
|
||||||
|
* secondary emphasis, quiet ghost for tertiary.
|
||||||
|
*/
|
||||||
|
export function Button({
|
||||||
|
children,
|
||||||
|
variant = 'primary',
|
||||||
|
size = 'md',
|
||||||
|
iconLeft,
|
||||||
|
iconRight,
|
||||||
|
full = false,
|
||||||
|
className,
|
||||||
|
...rest
|
||||||
|
}: ButtonProps) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
className={cn(
|
||||||
|
'inline-flex items-center justify-center gap-2 font-sans font-semibold leading-none',
|
||||||
|
'cursor-pointer transition-[transform,filter,box-shadow] duration-150',
|
||||||
|
'hover:brightness-[0.98] active:scale-[0.97]',
|
||||||
|
'disabled:opacity-50 disabled:cursor-not-allowed disabled:active:scale-100',
|
||||||
|
SIZES[size],
|
||||||
|
VARIANTS[variant],
|
||||||
|
full ? 'w-full' : 'w-auto',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
|
{iconLeft}
|
||||||
|
{children}
|
||||||
|
{iconRight}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
36
src/components/buttons/IconButton.tsx
Normal file
36
src/components/buttons/IconButton.tsx
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import type { ButtonHTMLAttributes, ReactNode } from 'react';
|
||||||
|
import { cn } from '../../lib/cn';
|
||||||
|
|
||||||
|
export interface IconButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||||
|
/** The icon (e.g. a lucide-react element). */
|
||||||
|
icon: ReactNode;
|
||||||
|
/** Accessible label — required since there's no visible text. */
|
||||||
|
label: string;
|
||||||
|
/** @default "md" */
|
||||||
|
size?: 'sm' | 'md';
|
||||||
|
}
|
||||||
|
|
||||||
|
const SIZES = {
|
||||||
|
sm: 'h-8 w-8',
|
||||||
|
md: 'h-[42px] w-[42px]',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
/** Square icon-only button. Quiet by default; use for row/toolbar actions. */
|
||||||
|
export function IconButton({ icon, label, size = 'md', className, ...rest }: IconButtonProps) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
aria-label={label}
|
||||||
|
title={label}
|
||||||
|
className={cn(
|
||||||
|
'inline-flex items-center justify-center rounded-md border border-border-default bg-card text-muted shadow-xs',
|
||||||
|
'cursor-pointer transition-colors duration-150 hover:text-strong hover:bg-slate-50',
|
||||||
|
'disabled:opacity-50 disabled:cursor-not-allowed',
|
||||||
|
SIZES[size],
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
|
{icon}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
4
src/components/buttons/index.ts
Normal file
4
src/components/buttons/index.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export { Button } from './Button';
|
||||||
|
export type { ButtonProps, ButtonVariant, ButtonSize } from './Button';
|
||||||
|
export { IconButton } from './IconButton';
|
||||||
|
export type { IconButtonProps } from './IconButton';
|
||||||
17
src/components/dv/CallDetail.tsx
Normal file
17
src/components/dv/CallDetail.tsx
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { orderBookingClient } from '../../api/clients';
|
||||||
|
import { ORDER_BOOKING } from '../../api/config';
|
||||||
|
import { DetailView } from './DetailView';
|
||||||
|
import type { WiredDetailViewProps } from './OrderDetail';
|
||||||
|
|
||||||
|
/** Call detail view (Order Booking workflow). */
|
||||||
|
export function CallDetail({ instanceId, columns }: WiredDetailViewProps) {
|
||||||
|
return (
|
||||||
|
<DetailView
|
||||||
|
client={orderBookingClient}
|
||||||
|
dvUid={ORDER_BOOKING.detailViews.CALLS}
|
||||||
|
instanceId={instanceId}
|
||||||
|
title="Call"
|
||||||
|
columns={columns}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
17
src/components/dv/DailyLogDetail.tsx
Normal file
17
src/components/dv/DailyLogDetail.tsx
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { dailyReportsClient } from '../../api/clients';
|
||||||
|
import { DAILY_REPORTS } from '../../api/config';
|
||||||
|
import { DetailView } from './DetailView';
|
||||||
|
import type { WiredDetailViewProps } from './OrderDetail';
|
||||||
|
|
||||||
|
/** Daily Log detail view (Daily Reports workflow). */
|
||||||
|
export function DailyLogDetail({ instanceId, columns }: WiredDetailViewProps) {
|
||||||
|
return (
|
||||||
|
<DetailView
|
||||||
|
client={dailyReportsClient}
|
||||||
|
dvUid={DAILY_REPORTS.detailViews.DAILY_LOGS}
|
||||||
|
instanceId={instanceId}
|
||||||
|
title="Daily Log"
|
||||||
|
columns={columns}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
97
src/components/dv/DetailView.tsx
Normal file
97
src/components/dv/DetailView.tsx
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { cn } from '../../lib/cn';
|
||||||
|
import { formatValue } from '../../lib/format';
|
||||||
|
import type { ZinoClient } from '../../api/client';
|
||||||
|
import { Card } from '../reusable/Card';
|
||||||
|
import { Spinner } from '../reusable/Spinner';
|
||||||
|
import { EmptyState } from '../reusable/EmptyState';
|
||||||
|
|
||||||
|
interface DetailField {
|
||||||
|
field_key: string;
|
||||||
|
output_label: string;
|
||||||
|
data_type: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DetailResponse {
|
||||||
|
config: { fields: DetailField[] };
|
||||||
|
data: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DetailViewProps {
|
||||||
|
/** Workflow-bound client (see api/clients.ts). */
|
||||||
|
client: ZinoClient;
|
||||||
|
/** detailview template uid. */
|
||||||
|
dvUid: string;
|
||||||
|
/** Instance to render. */
|
||||||
|
instanceId: number | string;
|
||||||
|
/** Card header title. */
|
||||||
|
title?: string;
|
||||||
|
/** Restrict/order visible fields by field_key. Defaults to all. */
|
||||||
|
fields?: string[];
|
||||||
|
/** Columns in the definition grid. @default 2 */
|
||||||
|
columns?: 1 | 2 | 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generic Zino detail view. Fetches `GET /app/{id}/view/detailview/{uid}` and
|
||||||
|
* renders config.fields as a labeled definition grid. Wire it to a specific
|
||||||
|
* view via the thin wrappers in this folder.
|
||||||
|
*/
|
||||||
|
export function DetailView({ client, dvUid, instanceId, title, fields, columns = 2 }: DetailViewProps) {
|
||||||
|
const [resp, setResp] = useState<DetailResponse | null>(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let live = true;
|
||||||
|
async function run() {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
const r = await client.detailView(dvUid, instanceId);
|
||||||
|
if (live) setResp(r as DetailResponse);
|
||||||
|
} catch (e) {
|
||||||
|
if (live) setError((e as { message?: string })?.message ?? 'Failed to load');
|
||||||
|
} finally {
|
||||||
|
if (live) setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
run();
|
||||||
|
return () => {
|
||||||
|
live = false;
|
||||||
|
};
|
||||||
|
}, [client, dvUid, instanceId]);
|
||||||
|
|
||||||
|
const all = resp?.config.fields ?? [];
|
||||||
|
const shown = fields
|
||||||
|
? fields
|
||||||
|
.map((k) => all.find((f) => f.field_key === k) ?? { field_key: k, output_label: k, data_type: 'string' })
|
||||||
|
: all;
|
||||||
|
|
||||||
|
const gridCols = { 1: 'grid-cols-1', 2: 'grid-cols-1 sm:grid-cols-2', 3: 'grid-cols-1 sm:grid-cols-3' }[columns];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card title={title}>
|
||||||
|
{error ? (
|
||||||
|
<EmptyState title="Couldn’t load record" hint={error} />
|
||||||
|
) : loading ? (
|
||||||
|
<div className="py-8 flex justify-center">
|
||||||
|
<Spinner label="Loading…" />
|
||||||
|
</div>
|
||||||
|
) : shown.length === 0 ? (
|
||||||
|
<EmptyState title="No fields to display" />
|
||||||
|
) : (
|
||||||
|
<dl className={cn('grid gap-x-6 gap-y-4', gridCols)}>
|
||||||
|
{shown.map((f) => (
|
||||||
|
<div key={f.field_key} className="flex flex-col gap-1 min-w-0">
|
||||||
|
<dt className="text-[10px] font-bold uppercase tracking-[0.06em] text-faint">{f.output_label}</dt>
|
||||||
|
<dd className="m-0 text-sm text-strong font-medium break-words">
|
||||||
|
{formatValue(resp?.data[f.field_key])}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</dl>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
21
src/components/dv/OrderDetail.tsx
Normal file
21
src/components/dv/OrderDetail.tsx
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { orderBookingClient } from '../../api/clients';
|
||||||
|
import { ORDER_BOOKING } from '../../api/config';
|
||||||
|
import { DetailView } from './DetailView';
|
||||||
|
|
||||||
|
export interface WiredDetailViewProps {
|
||||||
|
instanceId: number | string;
|
||||||
|
columns?: 1 | 2 | 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Order detail view (Order Booking workflow). */
|
||||||
|
export function OrderDetail({ instanceId, columns }: WiredDetailViewProps) {
|
||||||
|
return (
|
||||||
|
<DetailView
|
||||||
|
client={orderBookingClient}
|
||||||
|
dvUid={ORDER_BOOKING.detailViews.ORDERS}
|
||||||
|
instanceId={instanceId}
|
||||||
|
title="Order"
|
||||||
|
columns={columns}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
17
src/components/dv/StoreDetail.tsx
Normal file
17
src/components/dv/StoreDetail.tsx
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { storeClient } from '../../api/clients';
|
||||||
|
import { STORE } from '../../api/config';
|
||||||
|
import { DetailView } from './DetailView';
|
||||||
|
import type { WiredDetailViewProps } from './OrderDetail';
|
||||||
|
|
||||||
|
/** Store detail view (Store workflow). */
|
||||||
|
export function StoreDetail({ instanceId, columns }: WiredDetailViewProps) {
|
||||||
|
return (
|
||||||
|
<DetailView
|
||||||
|
client={storeClient}
|
||||||
|
dvUid={STORE.detailViews.STORE}
|
||||||
|
instanceId={instanceId}
|
||||||
|
title="Store"
|
||||||
|
columns={columns}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
7
src/components/dv/index.ts
Normal file
7
src/components/dv/index.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
export { DetailView } from './DetailView';
|
||||||
|
export type { DetailViewProps } from './DetailView';
|
||||||
|
export { OrderDetail } from './OrderDetail';
|
||||||
|
export type { WiredDetailViewProps } from './OrderDetail';
|
||||||
|
export { CallDetail } from './CallDetail';
|
||||||
|
export { StoreDetail } from './StoreDetail';
|
||||||
|
export { DailyLogDetail } from './DailyLogDetail';
|
||||||
6
src/components/index.ts
Normal file
6
src/components/index.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
// Krishna Sales component library. Single entry point:
|
||||||
|
// import { Button, Card, RecordView, OrdersView, StoreDetail } from './components';
|
||||||
|
export * from './buttons';
|
||||||
|
export * from './reusable';
|
||||||
|
export * from './rv';
|
||||||
|
export * from './dv';
|
||||||
51
src/components/reusable/Badge.tsx
Normal file
51
src/components/reusable/Badge.tsx
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import type { ReactNode } from 'react';
|
||||||
|
import { cn } from '../../lib/cn';
|
||||||
|
|
||||||
|
export type BadgeTone =
|
||||||
|
| 'neutral'
|
||||||
|
| 'navy'
|
||||||
|
| 'success'
|
||||||
|
| 'warning'
|
||||||
|
| 'danger'
|
||||||
|
| 'info'
|
||||||
|
| 'accent';
|
||||||
|
|
||||||
|
export interface BadgeProps {
|
||||||
|
children?: ReactNode;
|
||||||
|
/** @default "neutral" */
|
||||||
|
tone?: BadgeTone;
|
||||||
|
/** Show a leading status dot. @default false */
|
||||||
|
dot?: boolean;
|
||||||
|
/** @default "md" */
|
||||||
|
size?: 'sm' | 'md';
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TONES: Record<BadgeTone, { wrap: string; dot: string }> = {
|
||||||
|
neutral: { wrap: 'bg-slate-100 text-slate-700', dot: 'bg-slate-500' },
|
||||||
|
navy: { wrap: 'bg-navy-900 text-white', dot: 'bg-sunrise-400' },
|
||||||
|
success: { wrap: 'bg-emerald-100 text-emerald-700', dot: 'bg-emerald-600' },
|
||||||
|
warning: { wrap: 'bg-amber-100 text-amber-700', dot: 'bg-amber-600' },
|
||||||
|
danger: { wrap: 'bg-ruby-100 text-ruby-700', dot: 'bg-ruby-600' },
|
||||||
|
info: { wrap: 'bg-sky-100 text-sky-700', dot: 'bg-sky-600' },
|
||||||
|
accent: { wrap: 'bg-sunrise-100 text-sunrise-600', dot: 'bg-sunrise-500' },
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Compact status pill with optional leading dot. */
|
||||||
|
export function Badge({ children, tone = 'neutral', dot = false, size = 'md', className }: BadgeProps) {
|
||||||
|
const t = TONES[tone];
|
||||||
|
const sm = size === 'sm';
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
'inline-flex items-center gap-1.5 rounded-pill font-sans font-semibold leading-none whitespace-nowrap',
|
||||||
|
sm ? 'text-2xs px-2 py-1' : 'text-xs px-2.5 py-[5px]',
|
||||||
|
t.wrap,
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{dot && <span className={cn('w-1.5 h-1.5 rounded-full', t.dot)} />}
|
||||||
|
{children}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
58
src/components/reusable/Card.tsx
Normal file
58
src/components/reusable/Card.tsx
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import type { CSSProperties, ReactNode } from 'react';
|
||||||
|
import { cn } from '../../lib/cn';
|
||||||
|
|
||||||
|
export type Elevation = 'none' | 'xs' | 'sm' | 'md' | 'lg';
|
||||||
|
|
||||||
|
export interface CardProps {
|
||||||
|
children?: ReactNode;
|
||||||
|
/** Optional header title. */
|
||||||
|
title?: ReactNode;
|
||||||
|
/** Optional header trailing node (button, menu). */
|
||||||
|
action?: ReactNode;
|
||||||
|
/** Apply body padding. @default true */
|
||||||
|
pad?: boolean;
|
||||||
|
/** @default "sm" */
|
||||||
|
elevation?: Elevation;
|
||||||
|
className?: string;
|
||||||
|
bodyClassName?: string;
|
||||||
|
style?: CSSProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SHADOW: Record<Elevation, string> = {
|
||||||
|
none: 'shadow-none',
|
||||||
|
xs: 'shadow-xs',
|
||||||
|
sm: 'shadow-sm',
|
||||||
|
md: 'shadow-md',
|
||||||
|
lg: 'shadow-lg',
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Surface container with soft elevation. Optional header & padding control. */
|
||||||
|
export function Card({
|
||||||
|
children,
|
||||||
|
title,
|
||||||
|
action,
|
||||||
|
pad = true,
|
||||||
|
elevation = 'sm',
|
||||||
|
className,
|
||||||
|
bodyClassName,
|
||||||
|
style,
|
||||||
|
}: CardProps) {
|
||||||
|
return (
|
||||||
|
<section
|
||||||
|
className={cn(
|
||||||
|
'bg-card rounded-lg border border-border-subtle overflow-hidden font-sans',
|
||||||
|
SHADOW[elevation],
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
style={style}
|
||||||
|
>
|
||||||
|
{title && (
|
||||||
|
<header className="flex items-center justify-between px-5 py-4 border-b border-border-subtle">
|
||||||
|
<h3 className="m-0 text-md font-semibold text-strong">{title}</h3>
|
||||||
|
{action}
|
||||||
|
</header>
|
||||||
|
)}
|
||||||
|
<div className={cn(pad ? 'p-5' : 'p-0', bodyClassName)}>{children}</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
24
src/components/reusable/EmptyState.tsx
Normal file
24
src/components/reusable/EmptyState.tsx
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import type { ReactNode } from 'react';
|
||||||
|
import { cn } from '../../lib/cn';
|
||||||
|
|
||||||
|
export interface EmptyStateProps {
|
||||||
|
/** Optional leading icon. */
|
||||||
|
icon?: ReactNode;
|
||||||
|
title: string;
|
||||||
|
hint?: string;
|
||||||
|
/** Optional action (e.g. a Button). */
|
||||||
|
action?: ReactNode;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Centered placeholder for empty lists / error fallbacks. */
|
||||||
|
export function EmptyState({ icon, title, hint, action, className }: EmptyStateProps) {
|
||||||
|
return (
|
||||||
|
<div className={cn('flex flex-col items-center justify-center gap-2 px-6 py-12 text-center', className)}>
|
||||||
|
{icon && <div className="text-faint mb-1">{icon}</div>}
|
||||||
|
<div className="text-sm font-semibold text-strong">{title}</div>
|
||||||
|
{hint && <div className="text-xs text-faint max-w-[36ch]">{hint}</div>}
|
||||||
|
{action && <div className="mt-3">{action}</div>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
60
src/components/reusable/Input.tsx
Normal file
60
src/components/reusable/Input.tsx
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import type { InputHTMLAttributes, ReactNode } from 'react';
|
||||||
|
import { cn } from '../../lib/cn';
|
||||||
|
|
||||||
|
export interface InputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'prefix'> {
|
||||||
|
label?: string;
|
||||||
|
hint?: string;
|
||||||
|
error?: string;
|
||||||
|
/** Leading adornment, e.g. "₹". */
|
||||||
|
prefix?: ReactNode;
|
||||||
|
/** Trailing adornment, e.g. "/ year". */
|
||||||
|
suffix?: ReactNode;
|
||||||
|
/** Class for the outer label wrapper. */
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Labeled text/number input with optional prefix, suffix, hint and error. */
|
||||||
|
export function Input({
|
||||||
|
label,
|
||||||
|
hint,
|
||||||
|
error,
|
||||||
|
prefix,
|
||||||
|
suffix,
|
||||||
|
required,
|
||||||
|
className,
|
||||||
|
disabled,
|
||||||
|
...rest
|
||||||
|
}: InputProps) {
|
||||||
|
return (
|
||||||
|
<label className={cn('flex flex-col gap-1.5 font-sans', disabled && 'cursor-not-allowed', className)}>
|
||||||
|
{label && (
|
||||||
|
<span className="text-sm font-medium text-muted">
|
||||||
|
{label}
|
||||||
|
{required && <span className="text-ruby-600"> *</span>}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'flex items-center gap-2 rounded-md px-3 h-[42px] border transition-[border-color,box-shadow] duration-150',
|
||||||
|
disabled ? 'bg-sunk border-border-subtle' : 'bg-card',
|
||||||
|
error ? 'border-ruby-600' : !disabled && 'border-border-default focus-ring',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{prefix && <span className="text-base font-semibold text-faint">{prefix}</span>}
|
||||||
|
<input
|
||||||
|
required={required}
|
||||||
|
disabled={disabled}
|
||||||
|
className={cn(
|
||||||
|
'flex-1 w-full border-none outline-none bg-transparent font-sans text-base',
|
||||||
|
disabled ? 'text-muted cursor-not-allowed' : 'text-strong',
|
||||||
|
)}
|
||||||
|
{...rest}
|
||||||
|
/>
|
||||||
|
{suffix && <span className="text-faint text-sm">{suffix}</span>}
|
||||||
|
</div>
|
||||||
|
{(hint || error) && (
|
||||||
|
<span className={cn('text-xs', error ? 'text-ruby-600' : 'text-faint')}>{error || hint}</span>
|
||||||
|
)}
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
}
|
||||||
77
src/components/reusable/Modal.tsx
Normal file
77
src/components/reusable/Modal.tsx
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
import { useEffect, type ReactNode } from 'react';
|
||||||
|
import { createPortal } from 'react-dom';
|
||||||
|
import { X } from 'lucide-react';
|
||||||
|
import { cn } from '../../lib/cn';
|
||||||
|
|
||||||
|
export type ModalWidth = 'sm' | 'md' | 'lg' | 'xl';
|
||||||
|
|
||||||
|
const WIDTH: Record<ModalWidth, string> = {
|
||||||
|
sm: 'max-w-[480px]',
|
||||||
|
md: 'max-w-[640px]',
|
||||||
|
lg: 'max-w-[820px]',
|
||||||
|
xl: 'max-w-[1000px]',
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface ModalProps {
|
||||||
|
open: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
title?: ReactNode;
|
||||||
|
subtitle?: ReactNode;
|
||||||
|
/** @default "md" */
|
||||||
|
width?: ModalWidth;
|
||||||
|
children?: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Portal modal host — backdrop, Esc / click-out close, scroll-locked body. */
|
||||||
|
export function Modal({ open, onClose, title, subtitle, width = 'md', children }: ModalProps) {
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open) return;
|
||||||
|
const onKey = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === 'Escape') onClose();
|
||||||
|
};
|
||||||
|
document.addEventListener('keydown', onKey);
|
||||||
|
const prev = document.body.style.overflow;
|
||||||
|
document.body.style.overflow = 'hidden';
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('keydown', onKey);
|
||||||
|
document.body.style.overflow = prev;
|
||||||
|
};
|
||||||
|
}, [open, onClose]);
|
||||||
|
|
||||||
|
if (!open) return null;
|
||||||
|
|
||||||
|
return createPortal(
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 z-[10000] flex items-start justify-center overflow-y-auto bg-black/40 p-4 sm:p-8"
|
||||||
|
onMouseDown={(e) => {
|
||||||
|
if (e.target === e.currentTarget) onClose();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
className={cn(
|
||||||
|
'w-full bg-card rounded-lg border border-border-subtle shadow-lg my-auto',
|
||||||
|
'flex flex-col max-h-[85vh]',
|
||||||
|
WIDTH[width],
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<header className="shrink-0 flex items-start justify-between gap-3 px-5 py-4 border-b border-border-subtle">
|
||||||
|
<div className="min-w-0">
|
||||||
|
{title && <h3 className="m-0 text-md font-semibold text-strong truncate">{title}</h3>}
|
||||||
|
{subtitle && <div className="text-xs text-faint mt-0.5">{subtitle}</div>}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
aria-label="Close"
|
||||||
|
className="shrink-0 -mr-1 -mt-1 p-1.5 rounded-md text-faint hover:text-strong hover:bg-black/5"
|
||||||
|
>
|
||||||
|
<X size={18} />
|
||||||
|
</button>
|
||||||
|
</header>
|
||||||
|
<div className="flex-1 min-h-0 overflow-y-auto p-5 scrollbar-slim">{children}</div>
|
||||||
|
</div>
|
||||||
|
</div>,
|
||||||
|
document.body,
|
||||||
|
);
|
||||||
|
}
|
||||||
41
src/components/reusable/Pagination.tsx
Normal file
41
src/components/reusable/Pagination.tsx
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import { ChevronLeft, ChevronRight } from 'lucide-react';
|
||||||
|
import { Button } from '../buttons/Button';
|
||||||
|
|
||||||
|
export interface PaginationProps {
|
||||||
|
/** 1-based current page. */
|
||||||
|
page: number;
|
||||||
|
pageSize: number;
|
||||||
|
/** Total rows across all pages. */
|
||||||
|
total: number;
|
||||||
|
onPage: (page: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Pager: "Showing a–b of N" + Prev/Next. Renders nothing when the set fits on
|
||||||
|
* one page. Shared by the record-view tables. */
|
||||||
|
export function Pagination({ page, pageSize, total, onPage }: PaginationProps) {
|
||||||
|
if (total <= pageSize) return null;
|
||||||
|
const totalPages = Math.max(1, Math.ceil(total / pageSize));
|
||||||
|
const safePage = Math.min(Math.max(page, 1), totalPages);
|
||||||
|
const start = (safePage - 1) * pageSize;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-between gap-3 border-t border-border-subtle px-[18px] py-3">
|
||||||
|
<span className="text-xs text-faint">
|
||||||
|
Showing {start + 1}–{Math.min(start + pageSize, total)} of {total}
|
||||||
|
</span>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Button variant="secondary" size="sm" disabled={safePage <= 1} onClick={() => onPage(safePage - 1)}>
|
||||||
|
<ChevronLeft size={15} />
|
||||||
|
Prev
|
||||||
|
</Button>
|
||||||
|
<span className="px-1 text-xs font-medium text-muted nums">
|
||||||
|
{safePage} / {totalPages}
|
||||||
|
</span>
|
||||||
|
<Button variant="secondary" size="sm" disabled={safePage >= totalPages} onClick={() => onPage(safePage + 1)}>
|
||||||
|
Next
|
||||||
|
<ChevronRight size={15} />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
50
src/components/reusable/Select.tsx
Normal file
50
src/components/reusable/Select.tsx
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import type { SelectHTMLAttributes } from 'react';
|
||||||
|
import { ChevronDown } from 'lucide-react';
|
||||||
|
import { cn } from '../../lib/cn';
|
||||||
|
|
||||||
|
export interface SelectOption {
|
||||||
|
value: string;
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SelectProps extends SelectHTMLAttributes<HTMLSelectElement> {
|
||||||
|
label?: string;
|
||||||
|
hint?: string;
|
||||||
|
/** Either strings or {value,label} objects. */
|
||||||
|
options?: Array<string | SelectOption>;
|
||||||
|
/** Class for the outer label wrapper. */
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Labeled native select styled to match Input. */
|
||||||
|
export function Select({ label, hint, options = [], required, className, ...rest }: SelectProps) {
|
||||||
|
return (
|
||||||
|
<label className={cn('flex flex-col gap-1.5 font-sans', className)}>
|
||||||
|
{label && (
|
||||||
|
<span className="text-sm font-medium text-muted">
|
||||||
|
{label}
|
||||||
|
{required && <span className="text-ruby-600"> *</span>}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<div className="relative bg-card rounded-md h-[42px] border border-border-default transition-[border-color,box-shadow] duration-150 focus-ring">
|
||||||
|
<select
|
||||||
|
required={required}
|
||||||
|
className="w-full h-full border-none outline-none bg-transparent appearance-none pl-3 pr-9 font-sans text-base text-strong cursor-pointer"
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
|
{options.map((o) => {
|
||||||
|
const val = typeof o === 'string' ? o : o.value;
|
||||||
|
const lab = typeof o === 'string' ? o : o.label;
|
||||||
|
return (
|
||||||
|
<option key={val} value={val}>
|
||||||
|
{lab}
|
||||||
|
</option>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</select>
|
||||||
|
<ChevronDown size={15} className="absolute right-3 top-1/2 -translate-y-1/2 pointer-events-none text-faint" />
|
||||||
|
</div>
|
||||||
|
{hint && <span className="text-xs text-faint">{hint}</span>}
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
}
|
||||||
20
src/components/reusable/Spinner.tsx
Normal file
20
src/components/reusable/Spinner.tsx
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { Loader2 } from 'lucide-react';
|
||||||
|
import { cn } from '../../lib/cn';
|
||||||
|
|
||||||
|
export interface SpinnerProps {
|
||||||
|
/** Pixel size of the icon. @default 18 */
|
||||||
|
size?: number;
|
||||||
|
/** Optional caption shown beside the spinner. */
|
||||||
|
label?: string;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Inline loading indicator. */
|
||||||
|
export function Spinner({ size = 18, label, className }: SpinnerProps) {
|
||||||
|
return (
|
||||||
|
<span className={cn('inline-flex items-center gap-2 text-faint', className)}>
|
||||||
|
<Loader2 size={size} className="animate-spin" />
|
||||||
|
{label && <span className="text-sm">{label}</span>}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
16
src/components/reusable/index.ts
Normal file
16
src/components/reusable/index.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
export { Card } from './Card';
|
||||||
|
export type { CardProps, Elevation } from './Card';
|
||||||
|
export { Badge } from './Badge';
|
||||||
|
export type { BadgeProps, BadgeTone } from './Badge';
|
||||||
|
export { Input } from './Input';
|
||||||
|
export type { InputProps } from './Input';
|
||||||
|
export { Select } from './Select';
|
||||||
|
export type { SelectProps, SelectOption } from './Select';
|
||||||
|
export { Pagination } from './Pagination';
|
||||||
|
export type { PaginationProps } from './Pagination';
|
||||||
|
export { Modal } from './Modal';
|
||||||
|
export type { ModalProps, ModalWidth } from './Modal';
|
||||||
|
export { Spinner } from './Spinner';
|
||||||
|
export type { SpinnerProps } from './Spinner';
|
||||||
|
export { EmptyState } from './EmptyState';
|
||||||
|
export type { EmptyStateProps } from './EmptyState';
|
||||||
17
src/components/rv/CallsView.tsx
Normal file
17
src/components/rv/CallsView.tsx
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { orderBookingClient } from '../../api/clients';
|
||||||
|
import { ORDER_BOOKING } from '../../api/config';
|
||||||
|
import { RecordView } from './RecordView';
|
||||||
|
import type { WiredRecordViewProps } from './OrdersView';
|
||||||
|
|
||||||
|
/** Calls record view (Order Booking workflow). */
|
||||||
|
export function CallsView({ onRowClick, pageSize }: WiredRecordViewProps) {
|
||||||
|
return (
|
||||||
|
<RecordView
|
||||||
|
client={orderBookingClient}
|
||||||
|
rvUid={ORDER_BOOKING.recordViews.CALLS}
|
||||||
|
title="Calls"
|
||||||
|
onRowClick={onRowClick}
|
||||||
|
pageSize={pageSize}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
17
src/components/rv/DailyLogsView.tsx
Normal file
17
src/components/rv/DailyLogsView.tsx
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { dailyReportsClient } from '../../api/clients';
|
||||||
|
import { DAILY_REPORTS } from '../../api/config';
|
||||||
|
import { RecordView } from './RecordView';
|
||||||
|
import type { WiredRecordViewProps } from './OrdersView';
|
||||||
|
|
||||||
|
/** Daily Logs record view (Daily Reports workflow). */
|
||||||
|
export function DailyLogsView({ onRowClick, pageSize }: WiredRecordViewProps) {
|
||||||
|
return (
|
||||||
|
<RecordView
|
||||||
|
client={dailyReportsClient}
|
||||||
|
rvUid={DAILY_REPORTS.recordViews.DAILY_LOGS}
|
||||||
|
title="Daily Logs"
|
||||||
|
onRowClick={onRowClick}
|
||||||
|
pageSize={pageSize}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
21
src/components/rv/OrdersView.tsx
Normal file
21
src/components/rv/OrdersView.tsx
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { orderBookingClient } from '../../api/clients';
|
||||||
|
import { ORDER_BOOKING } from '../../api/config';
|
||||||
|
import { RecordView } from './RecordView';
|
||||||
|
|
||||||
|
export interface WiredRecordViewProps {
|
||||||
|
onRowClick?: (row: Record<string, unknown>, index: number) => void;
|
||||||
|
pageSize?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Orders record view (Order Booking workflow). */
|
||||||
|
export function OrdersView({ onRowClick, pageSize }: WiredRecordViewProps) {
|
||||||
|
return (
|
||||||
|
<RecordView
|
||||||
|
client={orderBookingClient}
|
||||||
|
rvUid={ORDER_BOOKING.recordViews.ORDERS}
|
||||||
|
title="Orders"
|
||||||
|
onRowClick={onRowClick}
|
||||||
|
pageSize={pageSize}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
158
src/components/rv/RecordView.tsx
Normal file
158
src/components/rv/RecordView.tsx
Normal file
@ -0,0 +1,158 @@
|
|||||||
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
import { Search } from 'lucide-react';
|
||||||
|
import { cn } from '../../lib/cn';
|
||||||
|
import { formatValue } from '../../lib/format';
|
||||||
|
import type { ZinoClient } from '../../api/client';
|
||||||
|
import type { RecordViewField, RecordViewResponse } from '../../api/types';
|
||||||
|
import { Card } from '../reusable/Card';
|
||||||
|
import { Pagination } from '../reusable/Pagination';
|
||||||
|
import { Spinner } from '../reusable/Spinner';
|
||||||
|
import { EmptyState } from '../reusable/EmptyState';
|
||||||
|
|
||||||
|
export interface RecordViewProps {
|
||||||
|
/** Workflow-bound client (see api/clients.ts). */
|
||||||
|
client: ZinoClient;
|
||||||
|
/** rv_template_uid for this view. */
|
||||||
|
rvUid: string;
|
||||||
|
/** Card header title. */
|
||||||
|
title?: string;
|
||||||
|
/** Restrict/order visible columns by field_key. Defaults to all fields. */
|
||||||
|
columns?: string[];
|
||||||
|
/** Rows per page. @default 25 */
|
||||||
|
pageSize?: number;
|
||||||
|
/** Click handler — receives the raw row + index. */
|
||||||
|
onRowClick?: (row: Record<string, unknown>, index: number) => void;
|
||||||
|
/** Extract a stable key for a row. @default row.instance_id ?? index */
|
||||||
|
rowKey?: (row: Record<string, unknown>, index: number) => string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generic Zino record-view table. Fetches `POST /app/{id}/view/recordview`
|
||||||
|
* (server-side paginated + searchable) and renders config.fields as columns.
|
||||||
|
* Wire it to a specific view via the thin wrappers in this folder.
|
||||||
|
*/
|
||||||
|
export function RecordView({
|
||||||
|
client,
|
||||||
|
rvUid,
|
||||||
|
title,
|
||||||
|
columns,
|
||||||
|
pageSize = 25,
|
||||||
|
onRowClick,
|
||||||
|
rowKey,
|
||||||
|
}: RecordViewProps) {
|
||||||
|
const [page, setPage] = useState(1);
|
||||||
|
const [search, setSearch] = useState('');
|
||||||
|
const [debounced, setDebounced] = useState('');
|
||||||
|
const [resp, setResp] = useState<RecordViewResponse | null>(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// Debounce the search box; reset to page 1 whenever the term changes.
|
||||||
|
const timer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
|
useEffect(() => {
|
||||||
|
if (timer.current) clearTimeout(timer.current);
|
||||||
|
timer.current = setTimeout(() => {
|
||||||
|
setDebounced(search);
|
||||||
|
setPage(1);
|
||||||
|
}, 300);
|
||||||
|
return () => {
|
||||||
|
if (timer.current) clearTimeout(timer.current);
|
||||||
|
};
|
||||||
|
}, [search]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let live = true;
|
||||||
|
async function run() {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
const r = await client.recordView(rvUid, { page, limit: pageSize, search: debounced });
|
||||||
|
if (live) setResp(r);
|
||||||
|
} catch (e) {
|
||||||
|
if (live) setError((e as { message?: string })?.message ?? 'Failed to load');
|
||||||
|
} finally {
|
||||||
|
if (live) setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
run();
|
||||||
|
return () => {
|
||||||
|
live = false;
|
||||||
|
};
|
||||||
|
}, [client, rvUid, page, pageSize, debounced]);
|
||||||
|
|
||||||
|
const fields: RecordViewField[] = useMemo(() => {
|
||||||
|
const all = resp?.config.fields ?? [];
|
||||||
|
if (!columns) return all;
|
||||||
|
const byKey = new Map(all.map((f) => [f.field_key, f]));
|
||||||
|
return columns
|
||||||
|
.map((k) => byKey.get(k) ?? ({ field_key: k, output_label: k, data_type: 'string', is_filter: false, is_search: false } as RecordViewField))
|
||||||
|
.filter(Boolean);
|
||||||
|
}, [resp, columns]);
|
||||||
|
|
||||||
|
const rows = resp?.data ?? [];
|
||||||
|
const total = resp?.pagination?.total_count ?? rows.length;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
title={title}
|
||||||
|
pad={false}
|
||||||
|
action={
|
||||||
|
<div className="relative w-[240px] max-w-full">
|
||||||
|
<Search size={15} className="absolute left-3 top-1/2 -translate-y-1/2 text-faint pointer-events-none" />
|
||||||
|
<input
|
||||||
|
value={search}
|
||||||
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
|
placeholder="Search…"
|
||||||
|
className="w-full h-9 rounded-md border border-border-default bg-card pl-9 pr-3 font-sans text-sm text-strong outline-none focus-ring"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{error ? (
|
||||||
|
<EmptyState title="Couldn’t load records" hint={error} />
|
||||||
|
) : loading && !resp ? (
|
||||||
|
<div className="p-10 flex justify-center">
|
||||||
|
<Spinner label="Loading records…" />
|
||||||
|
</div>
|
||||||
|
) : rows.length === 0 ? (
|
||||||
|
<EmptyState title="No records" hint={debounced ? 'Nothing matches your search.' : undefined} />
|
||||||
|
) : (
|
||||||
|
<div className="overflow-x-auto scrollbar-slim">
|
||||||
|
<table className="w-full border-collapse min-w-[640px]">
|
||||||
|
<thead>
|
||||||
|
<tr className="bg-slate-50 border-b border-border-subtle">
|
||||||
|
{fields.map((f) => (
|
||||||
|
<th
|
||||||
|
key={f.field_key}
|
||||||
|
className="text-left px-[18px] py-[11px] text-[10px] font-bold uppercase tracking-[0.06em] text-faint whitespace-nowrap"
|
||||||
|
>
|
||||||
|
{f.output_label}
|
||||||
|
</th>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{rows.map((row, i) => (
|
||||||
|
<tr
|
||||||
|
key={rowKey ? rowKey(row, i) : String(row.instance_id ?? i)}
|
||||||
|
onClick={onRowClick ? () => onRowClick(row, i) : undefined}
|
||||||
|
className={cn(
|
||||||
|
'border-b border-border-subtle transition-colors duration-150',
|
||||||
|
onRowClick && 'cursor-pointer hover:bg-slate-50',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{fields.map((f) => (
|
||||||
|
<td key={f.field_key} className="px-[18px] py-3 text-sm text-body whitespace-nowrap">
|
||||||
|
{formatValue(row[f.field_key])}
|
||||||
|
</td>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<Pagination page={page} pageSize={pageSize} total={total} onPage={setPage} />
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
17
src/components/rv/StoresView.tsx
Normal file
17
src/components/rv/StoresView.tsx
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { storeClient } from '../../api/clients';
|
||||||
|
import { STORE } from '../../api/config';
|
||||||
|
import { RecordView } from './RecordView';
|
||||||
|
import type { WiredRecordViewProps } from './OrdersView';
|
||||||
|
|
||||||
|
/** Store record view (Store workflow). */
|
||||||
|
export function StoresView({ onRowClick, pageSize }: WiredRecordViewProps) {
|
||||||
|
return (
|
||||||
|
<RecordView
|
||||||
|
client={storeClient}
|
||||||
|
rvUid={STORE.recordViews.STORE}
|
||||||
|
title="Stores"
|
||||||
|
onRowClick={onRowClick}
|
||||||
|
pageSize={pageSize}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
7
src/components/rv/index.ts
Normal file
7
src/components/rv/index.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
export { RecordView } from './RecordView';
|
||||||
|
export type { RecordViewProps } from './RecordView';
|
||||||
|
export { OrdersView } from './OrdersView';
|
||||||
|
export type { WiredRecordViewProps } from './OrdersView';
|
||||||
|
export { CallsView } from './CallsView';
|
||||||
|
export { StoresView } from './StoresView';
|
||||||
|
export { DailyLogsView } from './DailyLogsView';
|
||||||
363
src/docs/api/daily_reports.md
Normal file
363
src/docs/api/daily_reports.md
Normal file
@ -0,0 +1,363 @@
|
|||||||
|
# Daily Reports — Data API Docs
|
||||||
|
|
||||||
|
**App ID:** `434`
|
||||||
|
**Version UUID:** `d4d4c738-bf74-4b44-a297-9a6653458f3a`
|
||||||
|
**Workflow UUID:** `c420dc04-2ba3-488e-902d-66bc9a2032cd`
|
||||||
|
|
||||||
|
## Base URLs
|
||||||
|
|
||||||
|
| Key | URL |
|
||||||
|
| ---- | ---------------------------- |
|
||||||
|
| core | `https://sandbox.getzino.in` |
|
||||||
|
| view | `https://sandbox.getzino.in` |
|
||||||
|
| user | `https://sandbox.getzino.in` |
|
||||||
|
|
||||||
|
All requests require header `Authorization: Bearer <JWT_TOKEN>` (user session JWT from login).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 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.
|
||||||
|
|
||||||
|
### 1.1 Init Activity (init)
|
||||||
|
|
||||||
|
Create a new workflow instance and run the init activity.
|
||||||
|
|
||||||
|
- **Method:** `POST`
|
||||||
|
- **Path:** `/app/434/start` (base: core)
|
||||||
|
- **Content-Type:** `application/json`
|
||||||
|
|
||||||
|
**Request body**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"activity_id": "ac04a444-9b96-463d-bc91-447fa80b028f",
|
||||||
|
"data": {
|
||||||
|
"date": "2024-01-15",
|
||||||
|
"day_plan_notes": "string_value",
|
||||||
|
"field_1": "09:30",
|
||||||
|
"route_code": "option_value",
|
||||||
|
"sales_officer_name": "string_value",
|
||||||
|
"store_image": "<IMAGE_UPLOAD>",
|
||||||
|
"sub_route": "option_value"
|
||||||
|
},
|
||||||
|
"version": 1,
|
||||||
|
"workflow_uuid": "c420dc04-2ba3-488e-902d-66bc9a2032cd"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Request fields**
|
||||||
|
|
||||||
|
| Key | Type | Required | Note |
|
||||||
|
| ------------------ | -------- | -------- | ----------------------------- |
|
||||||
|
| workflow_uuid | string | ✅ | Clone-portable workflow UID. |
|
||||||
|
| activity_id | string | ✅ | The init activity UID. |
|
||||||
|
| version | int | ❌ | Deployed workflow version. |
|
||||||
|
| data | object | ❌ | Field values keyed by field id. |
|
||||||
|
| sales_officer_name | app_user | ❌ | "Sales Officer Name" (app_user) |
|
||||||
|
| date | date | ❌ | "Date" (date) |
|
||||||
|
| field_1 | time | ❌ | "Time" (time) |
|
||||||
|
| route_code | select | ❌ | "Route Code" (select) |
|
||||||
|
| sub_route | select | ❌ | "Sub Route" (select) |
|
||||||
|
| store_image | image | ❌ | "Upload Store Image" (image) |
|
||||||
|
| day_plan_notes | longtext | ❌ | "Day Plan (notes)" (longtext) |
|
||||||
|
|
||||||
|
### 1.2 Punch Out
|
||||||
|
|
||||||
|
Advance an existing instance through this activity (normal submission).
|
||||||
|
|
||||||
|
- **Method:** `POST`
|
||||||
|
- **Path:** `/app/434/activity` (base: core)
|
||||||
|
- **Content-Type:** `application/json`
|
||||||
|
|
||||||
|
**Request body**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"activity_id": "37c218fc-fd91-41d1-9964-fcaeade54e74",
|
||||||
|
"data": {
|
||||||
|
"eod_notes_remarks": "string_value",
|
||||||
|
"total_non_productive_calls": 123,
|
||||||
|
"total_productive_calls": 123
|
||||||
|
},
|
||||||
|
"instance_id": "<INSTANCE_ID>",
|
||||||
|
"workflow_uuid": "c420dc04-2ba3-488e-902d-66bc9a2032cd"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Request fields**
|
||||||
|
|
||||||
|
| Key | Type | Required | Note |
|
||||||
|
| -------------------------- | -------- | -------- | ------------------------------------ |
|
||||||
|
| workflow_uuid | string | ✅ | Clone-portable workflow UID. |
|
||||||
|
| activity_id | string | ✅ | This activity's UID. |
|
||||||
|
| instance_id | int64 | ✅ | Target instance (runtime value). |
|
||||||
|
| data | object | ❌ | Field values keyed by field id. |
|
||||||
|
| total_productive_calls | number | ❌ | "Total Productive Calls" (number) |
|
||||||
|
| total_non_productive_calls | number | ❌ | "Total Non Productive Calls" (number) |
|
||||||
|
| eod_notes_remarks | longtext | ❌ | "EOD Notes (Remarks)" (longtext) |
|
||||||
|
|
||||||
|
### 1.3 Upload file
|
||||||
|
|
||||||
|
Upload a file/image during form fill; returns `blob_path` that replaces the File in the submit payload.
|
||||||
|
|
||||||
|
- **Method:** `POST`
|
||||||
|
- **Path:** `/app/434/upload` (base: core)
|
||||||
|
- **Content-Type:** `multipart/form-data` (browser sets the boundary automatically)
|
||||||
|
|
||||||
|
**Form fields**
|
||||||
|
|
||||||
|
| Key | Type | Required | Note |
|
||||||
|
| ------------- | ------ | -------- | ------------------------------------------- |
|
||||||
|
| file | binary | ✅ | The file contents. |
|
||||||
|
| workflow_uuid | string | ✅ | Fixed: `c420dc04-2ba3-488e-902d-66bc9a2032cd`. |
|
||||||
|
| activity_id | string | ✅ | |
|
||||||
|
| field_id | string | ✅ | |
|
||||||
|
| column_id | string | ❌ | Only for grid-cell file columns. |
|
||||||
|
| version_uuid | string | ❌ | |
|
||||||
|
| instance_id | int64 | ❌ | Runtime value when editing an instance. |
|
||||||
|
|
||||||
|
**Response body**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"blob_path": "12/a1b2c3.png",
|
||||||
|
"mime_type": "image/png",
|
||||||
|
"original_name": "photo.png",
|
||||||
|
"size_bytes": 20480,
|
||||||
|
"url": "<STORAGE_URL>"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Key | Type | Note |
|
||||||
|
| ------------- | ------ | --------------------------------------------- |
|
||||||
|
| blob_path | string | Storage path that replaces the File in `data[field_id]`. |
|
||||||
|
| original_name | string | |
|
||||||
|
| mime_type | string | |
|
||||||
|
| size_bytes | int64 | |
|
||||||
|
| url | string | |
|
||||||
|
|
||||||
|
### Common submission response
|
||||||
|
|
||||||
|
Applies to Init Activity and Punch Out.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"data": {},
|
||||||
|
"instance_id": 1024,
|
||||||
|
"message": "Activity completed successfully",
|
||||||
|
"status_code": 200,
|
||||||
|
"success": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Key | Type | Note |
|
||||||
|
| ----------- | ------ | --------------------------------------------------------------- |
|
||||||
|
| 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. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. View APIs
|
||||||
|
|
||||||
|
Read endpoints that power record/detail/activity/chart views, segregated by source type with each view's UI name.
|
||||||
|
|
||||||
|
### 2.1 Record Views
|
||||||
|
|
||||||
|
Paginated records + tiles + charts.
|
||||||
|
|
||||||
|
- **Method:** `POST`
|
||||||
|
- **Path:** `/app/434/view/recordview` (base: view)
|
||||||
|
- **Content-Type:** `application/json`
|
||||||
|
|
||||||
|
| UI Name | rv_template_uid |
|
||||||
|
| ---------- | -------------------------------------- |
|
||||||
|
| Daily Logs | `269da629-1701-477a-a37b-a78bb056d4b3` |
|
||||||
|
|
||||||
|
**Request body**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"params": {},
|
||||||
|
"preset_alias": "",
|
||||||
|
"rv_template_uid": "269da629-1701-477a-a37b-a78bb056d4b3",
|
||||||
|
"search_query": {
|
||||||
|
"filters": [],
|
||||||
|
"limit": 25,
|
||||||
|
"page": 1,
|
||||||
|
"search": "",
|
||||||
|
"sort_by": "",
|
||||||
|
"sort_dir": "desc"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Request fields**
|
||||||
|
|
||||||
|
| Key | Type | Required | Note |
|
||||||
|
| --------------------- | ------ | -------- | ----------------------------------------------------------- |
|
||||||
|
| rv_template_uid | string | ✅ | This view's template UID. |
|
||||||
|
| preset_alias | string | ❌ | Optional; selects a named prefilter preset on this view. |
|
||||||
|
| params | object | ❌ | Values for declared input params (`${input.<key>}` 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 body**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"chart_data": [],
|
||||||
|
"data": [],
|
||||||
|
"pagination": { "limit": 25, "page": 1, "total_records": 0 },
|
||||||
|
"tile_values": {}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Key | Type | Note |
|
||||||
|
| ----------- | ------ | ------------------------------- |
|
||||||
|
| data | array | Companion-table rows. |
|
||||||
|
| tile_values | object | |
|
||||||
|
| chart_data | array | |
|
||||||
|
| pagination | object | `{page, limit, total_records}`. |
|
||||||
|
|
||||||
|
### 2.2 Detail Views
|
||||||
|
|
||||||
|
Single-instance detail record.
|
||||||
|
|
||||||
|
- **Method:** `GET`
|
||||||
|
- **Base:** view
|
||||||
|
|
||||||
|
| UI Name | Path |
|
||||||
|
| ---------- | ---------------------------------------------------------------------------- |
|
||||||
|
| Daily Logs | `/app/434/view/detailview/26b3c6de-a1d7-4f71-be88-7e4929932b0b?instance_id=<INSTANCE_ID>` |
|
||||||
|
|
||||||
|
**Request fields**
|
||||||
|
|
||||||
|
| Key | Type | Required | Note |
|
||||||
|
| ----------- | ----- | -------- | ----------------------------------------------------------------- |
|
||||||
|
| instance_id | int64 | ✅ | Query param (runtime value); some DVs use declared extra_params. |
|
||||||
|
|
||||||
|
**Response body**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "config": {}, "data": {} }
|
||||||
|
```
|
||||||
|
|
||||||
|
| Key | Type | Note |
|
||||||
|
| ------ | ------ | ----------------- |
|
||||||
|
| config | object | |
|
||||||
|
| data | object | `field_key→value`. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 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.
|
||||||
|
|
||||||
|
### Activity: Init Activity (`ac04a444-9b96-463d-bc91-447fa80b028f`)
|
||||||
|
|
||||||
|
#### App User — Sales Officer Name
|
||||||
|
|
||||||
|
App-user dropdown: open / search.
|
||||||
|
|
||||||
|
- **Method:** `POST`
|
||||||
|
- **Path:** `/app/434/app-user/records` (base: core)
|
||||||
|
- **Content-Type:** `application/json`
|
||||||
|
|
||||||
|
**Request body**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"activity_id": "ac04a444-9b96-463d-bc91-447fa80b028f",
|
||||||
|
"field_id": "sales_officer_name",
|
||||||
|
"form_data": {},
|
||||||
|
"limit": 50,
|
||||||
|
"offset": 0,
|
||||||
|
"search": "",
|
||||||
|
"workflow_uuid": "c420dc04-2ba3-488e-902d-66bc9a2032cd"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Request fields**
|
||||||
|
|
||||||
|
| Key | Type | Required | Note |
|
||||||
|
| ------------- | ------ | -------- | ----------------------------------------------- |
|
||||||
|
| workflow_uuid | string | ✅ | Fixed: `c420dc04-2ba3-488e-902d-66bc9a2032cd`. |
|
||||||
|
| activity_id | string | ✅ | |
|
||||||
|
| field_id | string | ✅ | |
|
||||||
|
| search | string | ❌ | Search term. |
|
||||||
|
| limit | int | ❌ | Default 50, max 200. |
|
||||||
|
| offset | int | ❌ | |
|
||||||
|
| filters | array | ❌ | `[{column, operator, value}]`. |
|
||||||
|
| form_data | object | ❌ | Current form values for filter_options. |
|
||||||
|
| version_uuid | string | ❌ | Pins filter_options to the deployed version. |
|
||||||
|
|
||||||
|
**Response body**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "limit": 50, "offset": 0, "records": [], "total": 0 }
|
||||||
|
```
|
||||||
|
|
||||||
|
| Key | Type | Note |
|
||||||
|
| ------- | ----- | ------------------------------------------------- |
|
||||||
|
| records | array | For dataset-options this is `options:[{value,label}]`. |
|
||||||
|
| total | int | |
|
||||||
|
| limit | int | |
|
||||||
|
| offset | int | |
|
||||||
|
|
||||||
|
#### Dataset options — Route Code
|
||||||
|
|
||||||
|
Dataset-backed options: mount + dependency change.
|
||||||
|
|
||||||
|
- **Method:** `POST`
|
||||||
|
- **Path:** `/app/434/dataset-options` (base: core)
|
||||||
|
- **Content-Type:** `application/json`
|
||||||
|
|
||||||
|
**Request body**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"activity_id": "ac04a444-9b96-463d-bc91-447fa80b028f",
|
||||||
|
"field_id": "route_code",
|
||||||
|
"form_data": {},
|
||||||
|
"limit": 50,
|
||||||
|
"offset": 0,
|
||||||
|
"search": "",
|
||||||
|
"workflow_uuid": "c420dc04-2ba3-488e-902d-66bc9a2032cd"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Request fields**
|
||||||
|
|
||||||
|
| Key | Type | Required | Note |
|
||||||
|
| ------------- | ------ | -------- | ----------------------------------------------- |
|
||||||
|
| workflow_uuid | string | ✅ | Fixed: `c420dc04-2ba3-488e-902d-66bc9a2032cd`. |
|
||||||
|
| activity_id | string | ✅ | |
|
||||||
|
| field_id | string | ✅ | |
|
||||||
|
| search | string | ❌ | Search term. |
|
||||||
|
| limit | int | ❌ | Default 50, max 200. |
|
||||||
|
| offset | int | ❌ | |
|
||||||
|
| filters | array | ❌ | `[{column, operator, value}]`. |
|
||||||
|
| form_data | object | ❌ | Current form values for filter_options. |
|
||||||
|
| version_uuid | string | ❌ | Pins filter_options to the deployed version. |
|
||||||
|
|
||||||
|
**Response body**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "limit": 50, "offset": 0, "records": [], "total": 0 }
|
||||||
|
```
|
||||||
|
|
||||||
|
| Key | Type | Note |
|
||||||
|
| ------- | ----- | ------------------------------------------------- |
|
||||||
|
| records | array | For dataset-options this is `options:[{value,label}]`. |
|
||||||
|
| total | int | |
|
||||||
|
| limit | int | |
|
||||||
|
| offset | int | |
|
||||||
456
src/docs/api/order_booking.md
Normal file
456
src/docs/api/order_booking.md
Normal file
@ -0,0 +1,456 @@
|
|||||||
|
# Order Booking — Data API Docs
|
||||||
|
|
||||||
|
**App ID:** `434`
|
||||||
|
**Version UUID:** `f19b2146-88e5-448a-9a51-e517ebce680a`
|
||||||
|
**Workflow UUID:** `b5a21627-9422-40cf-b7f5-31fa070bf42f`
|
||||||
|
|
||||||
|
## Base URLs
|
||||||
|
|
||||||
|
| Key | URL |
|
||||||
|
| ---- | ---------------------------- |
|
||||||
|
| core | `https://sandbox.getzino.in` |
|
||||||
|
| view | `https://sandbox.getzino.in` |
|
||||||
|
| user | `https://sandbox.getzino.in` |
|
||||||
|
|
||||||
|
All requests require header `Authorization: Bearer <JWT_TOKEN>` (user session JWT from login).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 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.
|
||||||
|
|
||||||
|
### 1.1 Log Visit (init)
|
||||||
|
|
||||||
|
Create a new workflow instance and run the init activity.
|
||||||
|
|
||||||
|
- **Method:** `POST`
|
||||||
|
- **Path:** `/app/434/start` (base: core)
|
||||||
|
- **Content-Type:** `application/json`
|
||||||
|
|
||||||
|
**Request body**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"activity_id": "42b7f47d-96f0-4289-a6d9-38f455ad57dd",
|
||||||
|
"data": {
|
||||||
|
"date_of_visit": "2024-01-15",
|
||||||
|
"select_store": "string_value",
|
||||||
|
"time_of_visit": "09:30",
|
||||||
|
"upload_image": "<IMAGE_UPLOAD>"
|
||||||
|
},
|
||||||
|
"version": 1,
|
||||||
|
"workflow_uuid": "b5a21627-9422-40cf-b7f5-31fa070bf42f"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Request fields**
|
||||||
|
|
||||||
|
| Key | Type | Required | Note |
|
||||||
|
| -------------- | --------- | -------- | ----------------------------- |
|
||||||
|
| workflow_uuid | string | ✅ | Clone-portable workflow UID. |
|
||||||
|
| activity_id | string | ✅ | The init activity UID. |
|
||||||
|
| version | int | ❌ | Deployed workflow version. |
|
||||||
|
| data | object | ❌ | Field values keyed by field id. |
|
||||||
|
| select_store | wf_lookup | ❌ | "Select Store" (wf_lookup) |
|
||||||
|
| date_of_visit | date | ❌ | "Date of Visit" (date) |
|
||||||
|
| time_of_visit | time | ❌ | "Time of Visit" (time) |
|
||||||
|
| upload_image | image | ❌ | "Upload Image" (image) |
|
||||||
|
|
||||||
|
### 1.2 Place Order
|
||||||
|
|
||||||
|
Advance an existing instance through this activity (normal submission).
|
||||||
|
|
||||||
|
- **Method:** `POST`
|
||||||
|
- **Path:** `/app/434/activity` (base: core)
|
||||||
|
- **Content-Type:** `application/json`
|
||||||
|
|
||||||
|
**Request body**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"activity_id": "f66403d7-e31f-4d64-bcfd-ff5485aec8b8",
|
||||||
|
"data": {
|
||||||
|
"date_of_order": "2024-01-15",
|
||||||
|
"order_details": [
|
||||||
|
{
|
||||||
|
"bags": 123,
|
||||||
|
"br_code": "string_value",
|
||||||
|
"product_category": "option_value",
|
||||||
|
"product_name": "option_value",
|
||||||
|
"sku": "string_value",
|
||||||
|
"sku_code": "string_value"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"total_bags": 123,
|
||||||
|
"total_kgs": 123
|
||||||
|
},
|
||||||
|
"instance_id": "<INSTANCE_ID>",
|
||||||
|
"workflow_uuid": "b5a21627-9422-40cf-b7f5-31fa070bf42f"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Request fields**
|
||||||
|
|
||||||
|
| Key | Type | Required | Note |
|
||||||
|
| ------------- | ------ | -------- | ----------------------------- |
|
||||||
|
| workflow_uuid | string | ✅ | Clone-portable workflow UID. |
|
||||||
|
| activity_id | string | ✅ | This activity's UID. |
|
||||||
|
| instance_id | int64 | ✅ | Target instance (runtime value). |
|
||||||
|
| data | object | ❌ | Field values keyed by field id. |
|
||||||
|
| date_of_order | date | ❌ | "Date of Order" (date) |
|
||||||
|
| order_details | grid | ❌ | "Order Details" (grid) |
|
||||||
|
| total_bags | number | ❌ | "Total bags" (number) |
|
||||||
|
| total_kgs | number | ❌ | "Total Kgs" (number) |
|
||||||
|
|
||||||
|
### 1.3 Productivity of Visit
|
||||||
|
|
||||||
|
Advance an existing instance through this activity (normal submission).
|
||||||
|
|
||||||
|
- **Method:** `POST`
|
||||||
|
- **Path:** `/app/434/activity` (base: core)
|
||||||
|
- **Content-Type:** `application/json`
|
||||||
|
|
||||||
|
**Request body**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"activity_id": "11bd10f9-a001-470e-867f-33dee8eabe4b",
|
||||||
|
"data": {
|
||||||
|
"action": "string_value",
|
||||||
|
"is_productive_call": "string_value",
|
||||||
|
"order_received_channel": "on_call",
|
||||||
|
"remarks": "string_value",
|
||||||
|
"store_status": "opened"
|
||||||
|
},
|
||||||
|
"instance_id": "<INSTANCE_ID>",
|
||||||
|
"workflow_uuid": "b5a21627-9422-40cf-b7f5-31fa070bf42f"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Request fields**
|
||||||
|
|
||||||
|
| Key | Type | Required | Note |
|
||||||
|
| ---------------------- | -------- | -------- | -------------------------------- |
|
||||||
|
| workflow_uuid | string | ✅ | Clone-portable workflow UID. |
|
||||||
|
| activity_id | string | ✅ | This activity's UID. |
|
||||||
|
| instance_id | int64 | ✅ | Target instance (runtime value). |
|
||||||
|
| data | object | ❌ | Field values keyed by field id. |
|
||||||
|
| is_productive_call | radio | ❌ | "Is Productive Call?" (radio) |
|
||||||
|
| store_status | select | ❌ | "Store Status" (select) |
|
||||||
|
| order_received_channel | select | ❌ | "Order Received Channel" (select) |
|
||||||
|
| remarks | longtext | ❌ | "Remarks" (longtext) |
|
||||||
|
| action | radio | ❌ | "Action" (radio) |
|
||||||
|
|
||||||
|
### 1.4 Potential Mining
|
||||||
|
|
||||||
|
Advance an existing instance through this activity (normal submission).
|
||||||
|
|
||||||
|
- **Method:** `POST`
|
||||||
|
- **Path:** `/app/434/activity` (base: core)
|
||||||
|
- **Content-Type:** `application/json`
|
||||||
|
|
||||||
|
**Request body**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"activity_id": "af8ac8df-d868-4f7d-88b2-123955d69c56",
|
||||||
|
"data": {
|
||||||
|
"potential": [
|
||||||
|
{
|
||||||
|
"actual_potential": 123,
|
||||||
|
"difference": 123,
|
||||||
|
"potential_remarks": "string_value",
|
||||||
|
"product_category_": "option_value",
|
||||||
|
"reason": "option_value",
|
||||||
|
"total_ordered": 123
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"instance_id": "<INSTANCE_ID>",
|
||||||
|
"workflow_uuid": "b5a21627-9422-40cf-b7f5-31fa070bf42f"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Request fields**
|
||||||
|
|
||||||
|
| Key | Type | Required | Note |
|
||||||
|
| ------------- | ------ | -------- | -------------------------------- |
|
||||||
|
| workflow_uuid | string | ✅ | Clone-portable workflow UID. |
|
||||||
|
| activity_id | string | ✅ | This activity's UID. |
|
||||||
|
| instance_id | int64 | ✅ | Target instance (runtime value). |
|
||||||
|
| data | object | ❌ | Field values keyed by field id. |
|
||||||
|
| potential | grid | ❌ | "Potential" (grid) |
|
||||||
|
|
||||||
|
### 1.5 Close Order
|
||||||
|
|
||||||
|
Advance an existing instance through this activity (normal submission).
|
||||||
|
|
||||||
|
- **Method:** `POST`
|
||||||
|
- **Path:** `/app/434/activity` (base: core)
|
||||||
|
- **Content-Type:** `application/json`
|
||||||
|
|
||||||
|
**Request body**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"activity_id": "8580d7a2-5d31-40ab-b7ea-2a38d144e59a",
|
||||||
|
"data": {
|
||||||
|
"remarks_2": "string_value"
|
||||||
|
},
|
||||||
|
"instance_id": "<INSTANCE_ID>",
|
||||||
|
"workflow_uuid": "b5a21627-9422-40cf-b7f5-31fa070bf42f"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Request fields**
|
||||||
|
|
||||||
|
| Key | Type | Required | Note |
|
||||||
|
| ------------- | -------- | -------- | -------------------------------- |
|
||||||
|
| workflow_uuid | string | ✅ | Clone-portable workflow UID. |
|
||||||
|
| activity_id | string | ✅ | This activity's UID. |
|
||||||
|
| instance_id | int64 | ✅ | Target instance (runtime value). |
|
||||||
|
| data | object | ❌ | Field values keyed by field id. |
|
||||||
|
| remarks_2 | longtext | ❌ | "Remarks" (longtext) |
|
||||||
|
|
||||||
|
### 1.6 Upload file
|
||||||
|
|
||||||
|
Upload a file/image during form fill; returns `blob_path` that replaces the File in the submit payload.
|
||||||
|
|
||||||
|
- **Method:** `POST`
|
||||||
|
- **Path:** `/app/434/upload` (base: core)
|
||||||
|
- **Content-Type:** `multipart/form-data` (browser sets the boundary automatically)
|
||||||
|
|
||||||
|
**Form fields**
|
||||||
|
|
||||||
|
| Key | Type | Required | Note |
|
||||||
|
| ------------- | ------ | -------- | ------------------------------------------- |
|
||||||
|
| file | binary | ✅ | The file contents. |
|
||||||
|
| workflow_uuid | string | ✅ | Fixed: `b5a21627-9422-40cf-b7f5-31fa070bf42f`. |
|
||||||
|
| activity_id | string | ✅ | |
|
||||||
|
| field_id | string | ✅ | |
|
||||||
|
| column_id | string | ❌ | Only for grid-cell file columns. |
|
||||||
|
| version_uuid | string | ❌ | |
|
||||||
|
| instance_id | int64 | ❌ | Runtime value when editing an instance. |
|
||||||
|
|
||||||
|
**Response body**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"blob_path": "12/a1b2c3.png",
|
||||||
|
"mime_type": "image/png",
|
||||||
|
"original_name": "photo.png",
|
||||||
|
"size_bytes": 20480,
|
||||||
|
"url": "<STORAGE_URL>"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Key | Type | Note |
|
||||||
|
| ------------- | ------ | --------------------------------------------- |
|
||||||
|
| blob_path | string | Storage path that replaces the File in `data[field_id]`. |
|
||||||
|
| original_name | string | |
|
||||||
|
| mime_type | string | |
|
||||||
|
| size_bytes | int64 | |
|
||||||
|
| url | string | |
|
||||||
|
|
||||||
|
### Common submission response
|
||||||
|
|
||||||
|
Applies to Log Visit, Place Order, Productivity of Visit, Potential Mining, Close Order.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"data": {},
|
||||||
|
"instance_id": 1024,
|
||||||
|
"message": "Activity completed successfully",
|
||||||
|
"status_code": 200,
|
||||||
|
"success": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Key | Type | Note |
|
||||||
|
| ----------- | ------ | --------------------------------------------------------------- |
|
||||||
|
| 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. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. View APIs
|
||||||
|
|
||||||
|
Read endpoints that power record/detail/activity/chart views, segregated by source type with each view's UI name.
|
||||||
|
|
||||||
|
### 2.1 Record Views
|
||||||
|
|
||||||
|
Paginated records + tiles + charts.
|
||||||
|
|
||||||
|
- **Method:** `POST`
|
||||||
|
- **Path:** `/app/434/view/recordview` (base: view)
|
||||||
|
- **Content-Type:** `application/json`
|
||||||
|
|
||||||
|
| UI Name | rv_template_uid |
|
||||||
|
| ------- | -------------------------------------- |
|
||||||
|
| Orders | `1e424561-9a27-44f5-9b68-64d63296d837` |
|
||||||
|
| Calls | `b4d7a710-24e7-416d-885a-31fd1e727aab` |
|
||||||
|
|
||||||
|
**Request body**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"params": {},
|
||||||
|
"preset_alias": "",
|
||||||
|
"rv_template_uid": "1e424561-9a27-44f5-9b68-64d63296d837",
|
||||||
|
"search_query": {
|
||||||
|
"filters": [],
|
||||||
|
"limit": 25,
|
||||||
|
"page": 1,
|
||||||
|
"search": "",
|
||||||
|
"sort_by": "",
|
||||||
|
"sort_dir": "desc"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Request fields**
|
||||||
|
|
||||||
|
| Key | Type | Required | Note |
|
||||||
|
| --------------------- | ------ | -------- | ----------------------------------------------------------- |
|
||||||
|
| rv_template_uid | string | ✅ | This view's template UID. |
|
||||||
|
| preset_alias | string | ❌ | Optional; selects a named prefilter preset on this view. |
|
||||||
|
| params | object | ❌ | Values for declared input params (`${input.<key>}` 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 body**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"chart_data": [],
|
||||||
|
"data": [],
|
||||||
|
"pagination": { "limit": 25, "page": 1, "total_records": 0 },
|
||||||
|
"tile_values": {}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Key | Type | Note |
|
||||||
|
| ----------- | ------ | ------------------------------- |
|
||||||
|
| data | array | Companion-table rows. |
|
||||||
|
| tile_values | object | |
|
||||||
|
| chart_data | array | |
|
||||||
|
| pagination | object | `{page, limit, total_records}`. |
|
||||||
|
|
||||||
|
### 2.2 Detail Views
|
||||||
|
|
||||||
|
Single-instance detail record.
|
||||||
|
|
||||||
|
- **Method:** `GET`
|
||||||
|
- **Base:** view
|
||||||
|
|
||||||
|
| UI Name | Path |
|
||||||
|
| ------- | ---------------------------------------------------------------------------- |
|
||||||
|
| Orders | `/app/434/view/detailview/0804c6c3-6cf9-4050-94bb-fa48dd5de87d?instance_id=<INSTANCE_ID>` |
|
||||||
|
| Calls | `/app/434/view/detailview/09a32bfe-9e43-4e0f-bb7d-db89c8a2557c?instance_id=<INSTANCE_ID>` |
|
||||||
|
|
||||||
|
**Request fields**
|
||||||
|
|
||||||
|
| Key | Type | Required | Note |
|
||||||
|
| ----------- | ----- | -------- | ----------------------------------------------------------------- |
|
||||||
|
| instance_id | int64 | ✅ | Query param (runtime value); some DVs use declared extra_params. |
|
||||||
|
|
||||||
|
**Response body**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "config": {}, "data": {} }
|
||||||
|
```
|
||||||
|
|
||||||
|
| Key | Type | Note |
|
||||||
|
| ------ | ------ | ----------------- |
|
||||||
|
| config | object | |
|
||||||
|
| data | object | `field_key→value`. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 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.
|
||||||
|
|
||||||
|
### Activity: Log Visit (`42b7f47d-96f0-4289-a6d9-38f455ad57dd`)
|
||||||
|
|
||||||
|
#### WF Lookup — Select Store
|
||||||
|
|
||||||
|
Workflow-lookup dropdown: open / search / dependency change.
|
||||||
|
|
||||||
|
- **Method:** `POST`
|
||||||
|
- **Path:** `/app/434/wf-lookup/records` (base: core)
|
||||||
|
- **Content-Type:** `application/json`
|
||||||
|
|
||||||
|
**Request body**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"activity_id": "42b7f47d-96f0-4289-a6d9-38f455ad57dd",
|
||||||
|
"field_id": "select_store",
|
||||||
|
"form_data": {},
|
||||||
|
"limit": 50,
|
||||||
|
"offset": 0,
|
||||||
|
"search": "",
|
||||||
|
"workflow_uuid": "b5a21627-9422-40cf-b7f5-31fa070bf42f"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Request fields**
|
||||||
|
|
||||||
|
| Key | Type | Required | Note |
|
||||||
|
| ------------- | ------ | -------- | ----------------------------------------------- |
|
||||||
|
| workflow_uuid | string | ✅ | Fixed: `b5a21627-9422-40cf-b7f5-31fa070bf42f`. |
|
||||||
|
| activity_id | string | ✅ | |
|
||||||
|
| field_id | string | ✅ | |
|
||||||
|
| search | string | ❌ | Search term. |
|
||||||
|
| limit | int | ❌ | Default 50, max 200. |
|
||||||
|
| offset | int | ❌ | |
|
||||||
|
| filters | array | ❌ | `[{column, operator, value}]`. |
|
||||||
|
| form_data | object | ❌ | Current form values for filter_options. |
|
||||||
|
| version_uuid | string | ❌ | Pins filter_options to the deployed version. |
|
||||||
|
|
||||||
|
**Response body**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "limit": 50, "offset": 0, "records": [], "total": 0 }
|
||||||
|
```
|
||||||
|
|
||||||
|
| Key | Type | Note |
|
||||||
|
| ------- | ----- | ------------------------------------------------- |
|
||||||
|
| records | array | For dataset-options this is `options:[{value,label}]`. |
|
||||||
|
| total | int | |
|
||||||
|
| limit | int | |
|
||||||
|
| offset | int | |
|
||||||
|
|
||||||
|
#### Peer instance data
|
||||||
|
|
||||||
|
Form-open prefetch of a peer instance referenced by field_rules / compute_value chains.
|
||||||
|
|
||||||
|
- **Method:** `GET`
|
||||||
|
- **Path:** `/app/434/view/instance-data?workflow_id=<PEER_WORKFLOW_ID>&instance_id=<INSTANCE_ID>` (base: view)
|
||||||
|
|
||||||
|
**Request fields**
|
||||||
|
|
||||||
|
| Key | Type | Required | Note |
|
||||||
|
| ----------- | ----- | -------- | ------------------------------------------------------------------- |
|
||||||
|
| workflow_id | int64 | ✅ | Query param; the peer/target workflow's runtime id (not this one). |
|
||||||
|
| instance_id | int64 | ✅ | Query param; peer instance id. |
|
||||||
|
|
||||||
|
**Response body**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "instance": {}, "workflow_fields": {} }
|
||||||
|
```
|
||||||
|
|
||||||
|
| Key | Type | Note |
|
||||||
|
| --------------- | ------ | ---- |
|
||||||
|
| instance | object | |
|
||||||
|
| workflow_fields | object | |
|
||||||
386
src/docs/api/store.md
Normal file
386
src/docs/api/store.md
Normal file
@ -0,0 +1,386 @@
|
|||||||
|
# Store — Data API Docs
|
||||||
|
|
||||||
|
**App ID:** `434`
|
||||||
|
**Version UUID:** `ced1fb5b-c6a4-4305-b85d-3c263d9d618e`
|
||||||
|
**Workflow UUID:** `2f827d27-b7a4-4ed9-9336-879fa46dbcba`
|
||||||
|
|
||||||
|
## Base URLs
|
||||||
|
|
||||||
|
| Key | URL |
|
||||||
|
| ---- | ---------------------------- |
|
||||||
|
| core | `https://sandbox.getzino.in` |
|
||||||
|
| view | `https://sandbox.getzino.in` |
|
||||||
|
| user | `https://sandbox.getzino.in` |
|
||||||
|
|
||||||
|
All requests require header `Authorization: Bearer <JWT_TOKEN>` (user session JWT from login).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 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.
|
||||||
|
|
||||||
|
### 1.1 Init Activity (init)
|
||||||
|
|
||||||
|
Create a new workflow instance and run the init activity.
|
||||||
|
|
||||||
|
- **Method:** `POST`
|
||||||
|
- **Path:** `/app/434/start` (base: core)
|
||||||
|
- **Content-Type:** `application/json`
|
||||||
|
|
||||||
|
**Request body**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"activity_id": "8626a77f-9e70-448e-8ac9-2c51eae5015b",
|
||||||
|
"data": {
|
||||||
|
"area_2": "string_value",
|
||||||
|
"business_name_2": "string_value",
|
||||||
|
"complete_address_2": "string_value",
|
||||||
|
"distributor_email_2": "user@example.com",
|
||||||
|
"distributor_name_2": "option_value",
|
||||||
|
"distributor_owner_name_2": "string_value",
|
||||||
|
"distributor_phone_number_2": {
|
||||||
|
"dial_code": "+1",
|
||||||
|
"phone": "2345678901",
|
||||||
|
"phone_with_dial_code": "+12345678901"
|
||||||
|
},
|
||||||
|
"email_2": "user@example.com",
|
||||||
|
"notes_2": "string_value",
|
||||||
|
"owner_name_2": "string_value",
|
||||||
|
"phone_number_2": {
|
||||||
|
"dial_code": "+1",
|
||||||
|
"phone": "2345678901",
|
||||||
|
"phone_with_dial_code": "+12345678901"
|
||||||
|
},
|
||||||
|
"pin_code_2": 123,
|
||||||
|
"potential_2": [
|
||||||
|
{ "product_category_1": "option_value", "quantity_1": 123 }
|
||||||
|
],
|
||||||
|
"route_code_2": "option_value",
|
||||||
|
"route_name_2": "option_value",
|
||||||
|
"store_image_2": "<IMAGE_UPLOAD>",
|
||||||
|
"store_location_2": "string_value",
|
||||||
|
"sub_route_2": "option_value"
|
||||||
|
},
|
||||||
|
"version": 1,
|
||||||
|
"workflow_uuid": "2f827d27-b7a4-4ed9-9336-879fa46dbcba"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Request fields**
|
||||||
|
|
||||||
|
| Key | Type | Required | Note |
|
||||||
|
| -------------------------- | ----------- | -------- | -------------------------------- |
|
||||||
|
| workflow_uuid | string | ✅ | Clone-portable workflow UID. |
|
||||||
|
| activity_id | string | ✅ | The init activity UID. |
|
||||||
|
| version | int | ❌ | Deployed workflow version. |
|
||||||
|
| data | object | ❌ | Field values keyed by field id. |
|
||||||
|
| business_name_2 | text | ❌ | "Business Name" (text) |
|
||||||
|
| area_2 | text | ❌ | "Area" (text) |
|
||||||
|
| complete_address_2 | longtext | ❌ | "Complete Address" (longtext) |
|
||||||
|
| pin_code_2 | number | ❌ | "PIN Code" (number) |
|
||||||
|
| owner_name_2 | text | ❌ | "Owner Name" (text) |
|
||||||
|
| phone_number_2 | phone | ❌ | "Phone Number" (phone) |
|
||||||
|
| email_2 | email | ❌ | "Email" (email) |
|
||||||
|
| store_location_2 | geolocation | ❌ | "Store Location" (geolocation) |
|
||||||
|
| store_image_2 | image | ❌ | "Store Image" (image) |
|
||||||
|
| route_name_2 | select | ❌ | "Route Name" (select) |
|
||||||
|
| route_code_2 | select | ❌ | "Route Code" (select) |
|
||||||
|
| sub_route_2 | select | ❌ | "Sub Route" (select) |
|
||||||
|
| distributor_name_2 | select | ❌ | "Distributor Name" (select) |
|
||||||
|
| distributor_owner_name_2 | text | ❌ | "Distributor Owner Name" (text) |
|
||||||
|
| distributor_email_2 | email | ❌ | "Distributor Email" (email) |
|
||||||
|
| distributor_phone_number_2 | phone | ❌ | "Distributor Phone Number" (phone) |
|
||||||
|
| notes_2 | longtext | ❌ | "Notes" (longtext) |
|
||||||
|
| potential_2 | grid | ❌ | "Potential" (grid) |
|
||||||
|
|
||||||
|
### 1.2 Edit Store
|
||||||
|
|
||||||
|
Advance an existing instance through this activity (normal submission).
|
||||||
|
|
||||||
|
- **Method:** `POST`
|
||||||
|
- **Path:** `/app/434/activity` (base: core)
|
||||||
|
- **Content-Type:** `application/json`
|
||||||
|
|
||||||
|
**Request body**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"activity_id": "87647fae-3b07-445f-9615-21881b163276",
|
||||||
|
"data": {
|
||||||
|
"area_3": "string_value",
|
||||||
|
"business_name_3": "string_value",
|
||||||
|
"complete_address_3": "string_value",
|
||||||
|
"distributor_email_3": "user@example.com",
|
||||||
|
"distributor_name_3": "option_value",
|
||||||
|
"distributor_owner_name_3": "string_value",
|
||||||
|
"distributor_phone_number_3": {
|
||||||
|
"dial_code": "+1",
|
||||||
|
"phone": "2345678901",
|
||||||
|
"phone_with_dial_code": "+12345678901"
|
||||||
|
},
|
||||||
|
"email_3": "user@example.com",
|
||||||
|
"notes_3": "string_value",
|
||||||
|
"owner_name_3": "string_value",
|
||||||
|
"phone_number_3": {
|
||||||
|
"dial_code": "+1",
|
||||||
|
"phone": "2345678901",
|
||||||
|
"phone_with_dial_code": "+12345678901"
|
||||||
|
},
|
||||||
|
"pin_code_3": 123,
|
||||||
|
"potential_3": [
|
||||||
|
{ "col_1": "option_value", "col_2": 123 }
|
||||||
|
],
|
||||||
|
"route_code_3": "option_value",
|
||||||
|
"route_name_3": "option_value",
|
||||||
|
"store_image_3": "<IMAGE_UPLOAD>",
|
||||||
|
"store_location_3": "string_value",
|
||||||
|
"sub_route_3": "option_value"
|
||||||
|
},
|
||||||
|
"instance_id": "<INSTANCE_ID>",
|
||||||
|
"workflow_uuid": "2f827d27-b7a4-4ed9-9336-879fa46dbcba"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Request fields**
|
||||||
|
|
||||||
|
| Key | Type | Required | Note |
|
||||||
|
| -------------------------- | ----------- | -------- | -------------------------------- |
|
||||||
|
| workflow_uuid | string | ✅ | Clone-portable workflow UID. |
|
||||||
|
| activity_id | string | ✅ | This activity's UID. |
|
||||||
|
| instance_id | int64 | ✅ | Target instance (runtime value). |
|
||||||
|
| data | object | ❌ | Field values keyed by field id. |
|
||||||
|
| business_name_3 | text | ❌ | "Business Name" (text) |
|
||||||
|
| area_3 | text | ❌ | "Area" (text) |
|
||||||
|
| complete_address_3 | longtext | ❌ | "Complete Address" (longtext) |
|
||||||
|
| pin_code_3 | number | ❌ | "PIN Code" (number) |
|
||||||
|
| owner_name_3 | text | ❌ | "Owner Name" (text) |
|
||||||
|
| phone_number_3 | phone | ❌ | "Phone Number" (phone) |
|
||||||
|
| email_3 | email | ❌ | "Email" (email) |
|
||||||
|
| store_location_3 | geolocation | ❌ | "Store Location" (geolocation) |
|
||||||
|
| store_image_3 | image | ❌ | "Store Image" (image) |
|
||||||
|
| route_name_3 | select | ❌ | "Route Name" (select) |
|
||||||
|
| route_code_3 | select | ❌ | "Route Code" (select) |
|
||||||
|
| sub_route_3 | select | ❌ | "Sub Route" (select) |
|
||||||
|
| distributor_name_3 | select | ❌ | "Distributor Name" (select) |
|
||||||
|
| distributor_owner_name_3 | text | ❌ | "Distributor Owner Name" (text) |
|
||||||
|
| distributor_email_3 | email | ❌ | "Distributor Email" (email) |
|
||||||
|
| distributor_phone_number_3 | phone | ❌ | "Distributor Phone Number" (phone) |
|
||||||
|
| notes_3 | longtext | ❌ | "Notes" (longtext) |
|
||||||
|
| potential_3 | grid | ❌ | "Potential" (grid) |
|
||||||
|
|
||||||
|
### 1.3 Upload file
|
||||||
|
|
||||||
|
Upload a file/image during form fill; returns `blob_path` that replaces the File in the submit payload.
|
||||||
|
|
||||||
|
- **Method:** `POST`
|
||||||
|
- **Path:** `/app/434/upload` (base: core)
|
||||||
|
- **Content-Type:** `multipart/form-data` (browser sets the boundary automatically)
|
||||||
|
|
||||||
|
**Form fields**
|
||||||
|
|
||||||
|
| Key | Type | Required | Note |
|
||||||
|
| ------------- | ------ | -------- | ------------------------------------------- |
|
||||||
|
| file | binary | ✅ | The file contents. |
|
||||||
|
| workflow_uuid | string | ✅ | Fixed: `2f827d27-b7a4-4ed9-9336-879fa46dbcba`. |
|
||||||
|
| activity_id | string | ✅ | |
|
||||||
|
| field_id | string | ✅ | |
|
||||||
|
| column_id | string | ❌ | Only for grid-cell file columns. |
|
||||||
|
| version_uuid | string | ❌ | |
|
||||||
|
| instance_id | int64 | ❌ | Runtime value when editing an instance. |
|
||||||
|
|
||||||
|
**Response body**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"blob_path": "12/a1b2c3.png",
|
||||||
|
"mime_type": "image/png",
|
||||||
|
"original_name": "photo.png",
|
||||||
|
"size_bytes": 20480,
|
||||||
|
"url": "<STORAGE_URL>"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Key | Type | Note |
|
||||||
|
| ------------- | ------ | --------------------------------------------- |
|
||||||
|
| blob_path | string | Storage path that replaces the File in `data[field_id]`. |
|
||||||
|
| original_name | string | |
|
||||||
|
| mime_type | string | |
|
||||||
|
| size_bytes | int64 | |
|
||||||
|
| url | string | |
|
||||||
|
|
||||||
|
### Common submission response
|
||||||
|
|
||||||
|
Applies to Init Activity and Edit Store.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"data": {},
|
||||||
|
"instance_id": 1024,
|
||||||
|
"message": "Activity completed successfully",
|
||||||
|
"status_code": 200,
|
||||||
|
"success": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Key | Type | Note |
|
||||||
|
| ----------- | ------ | --------------------------------------------------------------- |
|
||||||
|
| 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. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. View APIs
|
||||||
|
|
||||||
|
Read endpoints that power record/detail/activity/chart views, segregated by source type with each view's UI name.
|
||||||
|
|
||||||
|
### 2.1 Record Views
|
||||||
|
|
||||||
|
Paginated records + tiles + charts.
|
||||||
|
|
||||||
|
- **Method:** `POST`
|
||||||
|
- **Path:** `/app/434/view/recordview` (base: view)
|
||||||
|
- **Content-Type:** `application/json`
|
||||||
|
|
||||||
|
| UI Name | rv_template_uid |
|
||||||
|
| ------- | -------------------------------------- |
|
||||||
|
| Store | `c03b411a-8454-4d8b-8220-00095e92a5de` |
|
||||||
|
|
||||||
|
**Request body**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"params": {},
|
||||||
|
"preset_alias": "",
|
||||||
|
"rv_template_uid": "c03b411a-8454-4d8b-8220-00095e92a5de",
|
||||||
|
"search_query": {
|
||||||
|
"filters": [],
|
||||||
|
"limit": 25,
|
||||||
|
"page": 1,
|
||||||
|
"search": "",
|
||||||
|
"sort_by": "",
|
||||||
|
"sort_dir": "desc"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Request fields**
|
||||||
|
|
||||||
|
| Key | Type | Required | Note |
|
||||||
|
| --------------------- | ------ | -------- | ----------------------------------------------------------- |
|
||||||
|
| rv_template_uid | string | ✅ | This view's template UID. |
|
||||||
|
| preset_alias | string | ❌ | Optional; selects a named prefilter preset on this view. |
|
||||||
|
| params | object | ❌ | Values for declared input params (`${input.<key>}` 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 body**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"chart_data": [],
|
||||||
|
"data": [],
|
||||||
|
"pagination": { "limit": 25, "page": 1, "total_records": 0 },
|
||||||
|
"tile_values": {}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Key | Type | Note |
|
||||||
|
| ----------- | ------ | ------------------------------- |
|
||||||
|
| data | array | Companion-table rows. |
|
||||||
|
| tile_values | object | |
|
||||||
|
| chart_data | array | |
|
||||||
|
| pagination | object | `{page, limit, total_records}`. |
|
||||||
|
|
||||||
|
### 2.2 Detail Views
|
||||||
|
|
||||||
|
Single-instance detail record.
|
||||||
|
|
||||||
|
- **Method:** `GET`
|
||||||
|
- **Base:** view
|
||||||
|
|
||||||
|
| UI Name | Path |
|
||||||
|
| ------- | ---------------------------------------------------------------------------- |
|
||||||
|
| Store | `/app/434/view/detailview/af868269-d340-4ea9-9793-5fabe4f6ae27?instance_id=<INSTANCE_ID>` |
|
||||||
|
|
||||||
|
**Request fields**
|
||||||
|
|
||||||
|
| Key | Type | Required | Note |
|
||||||
|
| ----------- | ----- | -------- | ----------------------------------------------------------------- |
|
||||||
|
| instance_id | int64 | ✅ | Query param (runtime value); some DVs use declared extra_params. |
|
||||||
|
|
||||||
|
**Response body**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "config": {}, "data": {} }
|
||||||
|
```
|
||||||
|
|
||||||
|
| Key | Type | Note |
|
||||||
|
| ------ | ------ | ----------------- |
|
||||||
|
| config | object | |
|
||||||
|
| data | object | `field_key→value`. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 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.
|
||||||
|
|
||||||
|
### Activity: Init Activity (`8626a77f-9e70-448e-8ac9-2c51eae5015b`)
|
||||||
|
|
||||||
|
#### Dataset options — Route Name
|
||||||
|
|
||||||
|
Dataset-backed options: mount + dependency change.
|
||||||
|
|
||||||
|
- **Method:** `POST`
|
||||||
|
- **Path:** `/app/434/dataset-options` (base: core)
|
||||||
|
- **Content-Type:** `application/json`
|
||||||
|
|
||||||
|
**Request body**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"activity_id": "8626a77f-9e70-448e-8ac9-2c51eae5015b",
|
||||||
|
"field_id": "route_name_2",
|
||||||
|
"form_data": {},
|
||||||
|
"limit": 50,
|
||||||
|
"offset": 0,
|
||||||
|
"search": "",
|
||||||
|
"workflow_uuid": "2f827d27-b7a4-4ed9-9336-879fa46dbcba"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Request fields**
|
||||||
|
|
||||||
|
| Key | Type | Required | Note |
|
||||||
|
| ------------- | ------ | -------- | ----------------------------------------------- |
|
||||||
|
| workflow_uuid | string | ✅ | Fixed: `2f827d27-b7a4-4ed9-9336-879fa46dbcba`. |
|
||||||
|
| activity_id | string | ✅ | |
|
||||||
|
| field_id | string | ✅ | |
|
||||||
|
| search | string | ❌ | Search term. |
|
||||||
|
| limit | int | ❌ | Default 50, max 200. |
|
||||||
|
| offset | int | ❌ | |
|
||||||
|
| filters | array | ❌ | `[{column, operator, value}]`. |
|
||||||
|
| form_data | object | ❌ | Current form values for filter_options. |
|
||||||
|
| version_uuid | string | ❌ | Pins filter_options to the deployed version. |
|
||||||
|
|
||||||
|
**Response body**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "limit": 50, "offset": 0, "records": [], "total": 0 }
|
||||||
|
```
|
||||||
|
|
||||||
|
| Key | Type | Note |
|
||||||
|
| ------- | ----- | ------------------------------------------------- |
|
||||||
|
| records | array | For dataset-options this is `options:[{value,label}]`. |
|
||||||
|
| total | int | |
|
||||||
|
| limit | int | |
|
||||||
|
| offset | int | |
|
||||||
4
src/lib/cn.ts
Normal file
4
src/lib/cn.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
/** Join truthy class names. Tiny helper — no dependency needed. */
|
||||||
|
export function cn(...parts: Array<string | false | null | undefined>): string {
|
||||||
|
return parts.filter(Boolean).join(' ');
|
||||||
|
}
|
||||||
50
src/lib/format.ts
Normal file
50
src/lib/format.ts
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
// Turn a raw record/detail-view field value into a display string. The Zino
|
||||||
|
// view APIs return heterogeneous shapes — scalars, phone objects, file blobs,
|
||||||
|
// {label,value} option objects, and arrays (grids) — so normalize them here.
|
||||||
|
|
||||||
|
interface PhoneLike {
|
||||||
|
phone_with_dial_code?: string;
|
||||||
|
phone?: string;
|
||||||
|
dial_code?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isObject(v: unknown): v is Record<string, unknown> {
|
||||||
|
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Best-effort single-line display string for a field value. */
|
||||||
|
export function formatValue(value: unknown): string {
|
||||||
|
if (value == null || value === '') return '—';
|
||||||
|
if (typeof value === 'boolean') return value ? 'Yes' : 'No';
|
||||||
|
if (typeof value === 'number' || typeof value === 'string') return String(value);
|
||||||
|
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
if (value.length === 0) return '—';
|
||||||
|
return `${value.length} item${value.length === 1 ? '' : 's'}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isObject(value)) {
|
||||||
|
const o = value as PhoneLike & Record<string, unknown>;
|
||||||
|
// Phone
|
||||||
|
if (o.phone_with_dial_code) return String(o.phone_with_dial_code);
|
||||||
|
if (o.phone) return `${o.dial_code ?? ''}${o.phone}`.trim();
|
||||||
|
// Option / lookup {label,value}
|
||||||
|
if ('label' in o && o.label != null) return String(o.label);
|
||||||
|
// File / image blob
|
||||||
|
if ('original_name' in o && o.original_name != null) return String(o.original_name);
|
||||||
|
if ('url' in o && o.url != null) return String(o.url);
|
||||||
|
if ('value' in o && o.value != null) return String(o.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return JSON.stringify(value);
|
||||||
|
} catch {
|
||||||
|
return String(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Number with Indian-locale grouping; falls back to the raw string. */
|
||||||
|
export function formatNumber(value: unknown): string {
|
||||||
|
const n = typeof value === 'number' ? value : Number(value);
|
||||||
|
return Number.isFinite(n) ? n.toLocaleString('en-IN') : formatValue(value);
|
||||||
|
}
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import { StrictMode } from 'react'
|
import { StrictMode } from 'react'
|
||||||
import { createRoot } from 'react-dom/client'
|
import { createRoot } from 'react-dom/client'
|
||||||
import './index.css'
|
import './styles/styles.css'
|
||||||
import App from './App.tsx'
|
import App from './App.tsx'
|
||||||
|
|
||||||
createRoot(document.getElementById('root')!).render(
|
createRoot(document.getElementById('root')!).render(
|
||||||
|
|||||||
76
src/screens/ConsoleLayout.tsx
Normal file
76
src/screens/ConsoleLayout.tsx
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
|
import { NavLink, Navigate, Outlet, useNavigate } from 'react-router-dom';
|
||||||
|
import { LogOut } from 'lucide-react';
|
||||||
|
import { cn } from '../lib/cn';
|
||||||
|
import { useAuth } from '../auth/context';
|
||||||
|
import { onAuthErrorAll, orderBookingClient } from '../api/clients';
|
||||||
|
import { APP_ID } from '../api/config';
|
||||||
|
import { Button } from '../components/buttons';
|
||||||
|
import { TABS } from './tabs';
|
||||||
|
|
||||||
|
/** Auth-guarded shell: navy top bar + tab nav + routed <Outlet>. */
|
||||||
|
export function ConsoleLayout() {
|
||||||
|
const { authed, logout } = useAuth();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const user = orderBookingClient.currentUser();
|
||||||
|
|
||||||
|
// A 401 from any workflow client bounces back to login.
|
||||||
|
useEffect(() => {
|
||||||
|
onAuthErrorAll(() => {
|
||||||
|
logout();
|
||||||
|
navigate('/login', { replace: true });
|
||||||
|
});
|
||||||
|
}, [logout, navigate]);
|
||||||
|
|
||||||
|
if (!authed) return <Navigate to="/login" replace />;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-app">
|
||||||
|
<header className="sticky top-0 z-10 flex items-center justify-between gap-3 px-6 h-[60px] bg-navy-grad border-b border-border-navy">
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span className="text-md font-extrabold text-on-navy tracking-[-0.01em] leading-none">Krishna Sales</span>
|
||||||
|
<span className="text-2xs text-on-navy-muted">Field Sales · Sandbox {APP_ID}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
{user?.name && <span className="text-sm text-on-navy hidden sm:inline">{user.name}</span>}
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
size="sm"
|
||||||
|
iconLeft={<LogOut size={14} />}
|
||||||
|
onClick={() => {
|
||||||
|
logout();
|
||||||
|
navigate('/login', { replace: true });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Sign out
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main className="max-w-[1240px] mx-auto p-6 flex flex-col gap-5">
|
||||||
|
<nav className="flex gap-0.5 bg-slate-100 rounded-md p-[3px] w-fit">
|
||||||
|
{TABS.map((t) => {
|
||||||
|
const Icon = t.icon;
|
||||||
|
return (
|
||||||
|
<NavLink
|
||||||
|
key={t.key}
|
||||||
|
to={`/${t.key}`}
|
||||||
|
className={({ isActive }) =>
|
||||||
|
cn(
|
||||||
|
'inline-flex items-center gap-1.5 no-underline font-sans text-xs font-semibold px-3.5 py-2 rounded-sm transition-all duration-150',
|
||||||
|
isActive ? 'bg-card text-strong shadow-xs' : 'bg-transparent text-muted hover:text-strong',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Icon size={14} />
|
||||||
|
{t.label}
|
||||||
|
</NavLink>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<Outlet />
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
53
src/screens/LoginPage.tsx
Normal file
53
src/screens/LoginPage.tsx
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import { useState, type FormEvent } from 'react';
|
||||||
|
import { Navigate, useNavigate } from 'react-router-dom';
|
||||||
|
import { useAuth } from '../auth/context';
|
||||||
|
import { APP_ID } from '../api/config';
|
||||||
|
import { Button } from '../components/buttons';
|
||||||
|
import { Card, Input } from '../components/reusable';
|
||||||
|
|
||||||
|
/** Login gate. Redirects to /orders once authenticated. */
|
||||||
|
export function LoginPage() {
|
||||||
|
const { authed, login } = useAuth();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [email, setEmail] = useState('');
|
||||||
|
const [password, setPassword] = useState('');
|
||||||
|
const [orgId, setOrgId] = useState('');
|
||||||
|
const [busy, setBusy] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
if (authed) return <Navigate to="/orders" replace />;
|
||||||
|
|
||||||
|
async function submit(e: FormEvent) {
|
||||||
|
e.preventDefault();
|
||||||
|
setBusy(true);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
await login(email, password, orgId || undefined);
|
||||||
|
navigate('/orders', { replace: true });
|
||||||
|
} catch (err) {
|
||||||
|
setError((err as { message?: string })?.message ?? 'Login failed');
|
||||||
|
} finally {
|
||||||
|
setBusy(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen flex items-center justify-center p-4 bg-app">
|
||||||
|
<Card className="w-full max-w-[400px]">
|
||||||
|
<div className="flex flex-col gap-1 mb-5">
|
||||||
|
<h1 className="m-0 text-2xl font-extrabold text-strong tracking-[-0.02em]">Krishna Sales</h1>
|
||||||
|
<p className="m-0 text-sm text-faint">Field Sales console · Sandbox {APP_ID}</p>
|
||||||
|
</div>
|
||||||
|
<form onSubmit={submit} className="flex flex-col gap-4">
|
||||||
|
<Input label="Email" type="email" value={email} onChange={(e) => setEmail(e.target.value)} required autoFocus />
|
||||||
|
<Input label="Password" type="password" value={password} onChange={(e) => setPassword(e.target.value)} required />
|
||||||
|
<Input label="Org ID" hint="Optional" value={orgId} onChange={(e) => setOrgId(e.target.value)} />
|
||||||
|
{error && <div className="text-xs text-ruby-600 font-medium">{error}</div>}
|
||||||
|
<Button type="submit" full disabled={busy}>
|
||||||
|
{busy ? 'Signing in…' : 'Sign in'}
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
33
src/screens/WorkflowPage.tsx
Normal file
33
src/screens/WorkflowPage.tsx
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import { Navigate, useNavigate, useParams } from 'react-router-dom';
|
||||||
|
import { Modal } from '../components/reusable';
|
||||||
|
import { tabByKey } from './tabs';
|
||||||
|
|
||||||
|
/** Record view for /:tab, with a deep-linkable detail modal at /:tab/:instanceId. */
|
||||||
|
export function WorkflowPage() {
|
||||||
|
const { tab, instanceId } = useParams();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const def = tabByKey(tab);
|
||||||
|
|
||||||
|
// Unknown tab → default to orders.
|
||||||
|
if (!def) return <Navigate to="/orders" replace />;
|
||||||
|
|
||||||
|
const { View, Detail, noun, key } = def;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<View onRowClick={(row) => {
|
||||||
|
const id = row.instance_id as number | string | undefined;
|
||||||
|
if (id != null) navigate(`/${key}/${id}`);
|
||||||
|
}} />
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
open={instanceId != null}
|
||||||
|
onClose={() => navigate(`/${key}`)}
|
||||||
|
title={instanceId != null ? `${noun} #${instanceId}` : undefined}
|
||||||
|
width="lg"
|
||||||
|
>
|
||||||
|
{instanceId != null && <Detail instanceId={instanceId} />}
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
38
src/screens/tabs.ts
Normal file
38
src/screens/tabs.ts
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import { Store, Phone, ShoppingCart, ClipboardList, type LucideIcon } from 'lucide-react';
|
||||||
|
import {
|
||||||
|
OrdersView,
|
||||||
|
CallsView,
|
||||||
|
StoresView,
|
||||||
|
DailyLogsView,
|
||||||
|
type WiredRecordViewProps,
|
||||||
|
} from '../components/rv';
|
||||||
|
import {
|
||||||
|
OrderDetail,
|
||||||
|
CallDetail,
|
||||||
|
StoreDetail,
|
||||||
|
DailyLogDetail,
|
||||||
|
type WiredDetailViewProps,
|
||||||
|
} from '../components/dv';
|
||||||
|
|
||||||
|
export type TabKey = 'orders' | 'calls' | 'stores' | 'daily';
|
||||||
|
|
||||||
|
export interface TabDef {
|
||||||
|
key: TabKey;
|
||||||
|
label: string;
|
||||||
|
icon: LucideIcon;
|
||||||
|
View: (p: WiredRecordViewProps) => React.JSX.Element;
|
||||||
|
Detail: (p: WiredDetailViewProps) => React.JSX.Element;
|
||||||
|
/** Singular noun for the detail title. */
|
||||||
|
noun: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TABS: TabDef[] = [
|
||||||
|
{ key: 'orders', label: 'Orders', icon: ShoppingCart, View: OrdersView, Detail: OrderDetail, noun: 'Order' },
|
||||||
|
{ key: 'calls', label: 'Calls', icon: Phone, View: CallsView, Detail: CallDetail, noun: 'Call' },
|
||||||
|
{ key: 'stores', label: 'Stores', icon: Store, View: StoresView, Detail: StoreDetail, noun: 'Store' },
|
||||||
|
{ key: 'daily', label: 'Daily Logs', icon: ClipboardList, View: DailyLogsView, Detail: DailyLogDetail, noun: 'Daily Log' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export function tabByKey(key: string | undefined): TabDef | undefined {
|
||||||
|
return TABS.find((t) => t.key === key);
|
||||||
|
}
|
||||||
177
src/styles/styles.css
Normal file
177
src/styles/styles.css
Normal file
@ -0,0 +1,177 @@
|
|||||||
|
/* ============================================================
|
||||||
|
KRISHNA SALES — global stylesheet
|
||||||
|
Tailwind v4 + design tokens. The @theme inline block wires
|
||||||
|
Tailwind's utility namespaces to the CSS-variable tokens, so
|
||||||
|
`bg-navy-900`, `rounded-lg`, `shadow-md`, `font-numeric` etc.
|
||||||
|
resolve to the design system with zero duplication.
|
||||||
|
(Referenced from the lead-to-policy console.)
|
||||||
|
============================================================ */
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=Inter:opsz,wght@14..32,400;14..32,500;14..32,600;14..32,700;14..32,800&family=Inter+Tight:wght@500;600;700;800&display=swap');
|
||||||
|
@import 'tailwindcss';
|
||||||
|
@import './tokens/colors.css';
|
||||||
|
@import './tokens/typography.css';
|
||||||
|
@import './tokens/spacing.css';
|
||||||
|
|
||||||
|
@theme inline {
|
||||||
|
/* ---- Brand: Navy ---- */
|
||||||
|
--color-navy-950: var(--navy-950);
|
||||||
|
--color-navy-900: var(--navy-900);
|
||||||
|
--color-navy-800: var(--navy-800);
|
||||||
|
--color-navy-700: var(--navy-700);
|
||||||
|
--color-navy-600: var(--navy-600);
|
||||||
|
--color-navy-500: var(--navy-500);
|
||||||
|
--color-navy-400: var(--navy-400);
|
||||||
|
|
||||||
|
/* ---- Brand: Sunrise ---- */
|
||||||
|
--color-sunrise-600: var(--sunrise-600);
|
||||||
|
--color-sunrise-500: var(--sunrise-500);
|
||||||
|
--color-sunrise-400: var(--sunrise-400);
|
||||||
|
--color-sunrise-300: var(--sunrise-300);
|
||||||
|
--color-sunrise-200: var(--sunrise-200);
|
||||||
|
--color-sunrise-100: var(--sunrise-100);
|
||||||
|
|
||||||
|
/* ---- Semantic: Emerald / Amber / Ruby / Sky ---- */
|
||||||
|
--color-emerald-700: var(--emerald-700);
|
||||||
|
--color-emerald-600: var(--emerald-600);
|
||||||
|
--color-emerald-500: var(--emerald-500);
|
||||||
|
--color-emerald-300: var(--emerald-300);
|
||||||
|
--color-emerald-100: var(--emerald-100);
|
||||||
|
--color-amber-700: var(--amber-700);
|
||||||
|
--color-amber-600: var(--amber-600);
|
||||||
|
--color-amber-500: var(--amber-500);
|
||||||
|
--color-amber-300: var(--amber-300);
|
||||||
|
--color-amber-100: var(--amber-100);
|
||||||
|
--color-ruby-700: var(--ruby-700);
|
||||||
|
--color-ruby-600: var(--ruby-600);
|
||||||
|
--color-ruby-500: var(--ruby-500);
|
||||||
|
--color-ruby-100: var(--ruby-100);
|
||||||
|
--color-sky-700: var(--sky-700);
|
||||||
|
--color-sky-600: var(--sky-600);
|
||||||
|
--color-sky-500: var(--sky-500);
|
||||||
|
--color-sky-100: var(--sky-100);
|
||||||
|
|
||||||
|
/* ---- Neutral: Slate ---- */
|
||||||
|
--color-slate-950: var(--slate-950);
|
||||||
|
--color-slate-900: var(--slate-900);
|
||||||
|
--color-slate-800: var(--slate-800);
|
||||||
|
--color-slate-700: var(--slate-700);
|
||||||
|
--color-slate-600: var(--slate-600);
|
||||||
|
--color-slate-500: var(--slate-500);
|
||||||
|
--color-slate-400: var(--slate-400);
|
||||||
|
--color-slate-300: var(--slate-300);
|
||||||
|
--color-slate-200: var(--slate-200);
|
||||||
|
--color-slate-150: var(--slate-150);
|
||||||
|
--color-slate-100: var(--slate-100);
|
||||||
|
--color-slate-50: var(--slate-50);
|
||||||
|
|
||||||
|
/* ---- Semantic aliases (short, readable utility names) ---- */
|
||||||
|
--color-app: var(--surface-app);
|
||||||
|
--color-card: var(--surface-card);
|
||||||
|
--color-sunk: var(--surface-sunk);
|
||||||
|
--color-strong: var(--text-strong);
|
||||||
|
--color-body: var(--text-body);
|
||||||
|
--color-muted: var(--text-muted);
|
||||||
|
--color-faint: var(--text-faint);
|
||||||
|
--color-on-navy: var(--text-on-navy);
|
||||||
|
--color-on-navy-muted: var(--text-on-navy-muted);
|
||||||
|
--color-accent: var(--text-accent);
|
||||||
|
--color-link: var(--text-link);
|
||||||
|
--color-border-subtle: var(--border-subtle);
|
||||||
|
--color-border-default: var(--border-default);
|
||||||
|
|
||||||
|
/* ---- Radii ---- */
|
||||||
|
--radius-xs: var(--radius-xs);
|
||||||
|
--radius-sm: var(--radius-sm);
|
||||||
|
--radius-md: var(--radius-md);
|
||||||
|
--radius-lg: var(--radius-lg);
|
||||||
|
--radius-xl: var(--radius-xl);
|
||||||
|
--radius-2xl: var(--radius-2xl);
|
||||||
|
--radius-pill: var(--radius-pill);
|
||||||
|
|
||||||
|
/* ---- Elevation ---- */
|
||||||
|
--shadow-xs: var(--shadow-xs);
|
||||||
|
--shadow-sm: var(--shadow-sm);
|
||||||
|
--shadow-md: var(--shadow-md);
|
||||||
|
--shadow-lg: var(--shadow-lg);
|
||||||
|
--shadow-xl: var(--shadow-xl);
|
||||||
|
--shadow-sunrise: var(--shadow-sunrise);
|
||||||
|
|
||||||
|
/* ---- Fonts ---- */
|
||||||
|
--font-sans: var(--font-sans);
|
||||||
|
--font-numeric: var(--font-numeric);
|
||||||
|
--font-mono: var(--font-mono);
|
||||||
|
|
||||||
|
/* ---- Type scale ---- */
|
||||||
|
--text-2xs: var(--text-2xs);
|
||||||
|
--text-xs: var(--text-xs);
|
||||||
|
--text-sm: var(--text-sm);
|
||||||
|
--text-base: var(--text-base);
|
||||||
|
--text-md: var(--text-md);
|
||||||
|
--text-lg: var(--text-lg);
|
||||||
|
--text-xl: var(--text-xl);
|
||||||
|
--text-2xl: var(--text-2xl);
|
||||||
|
--text-3xl: var(--text-3xl);
|
||||||
|
--text-4xl: var(--text-4xl);
|
||||||
|
--text-5xl: var(--text-5xl);
|
||||||
|
--text-6xl: var(--text-6xl);
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
html {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
body,
|
||||||
|
#root {
|
||||||
|
min-height: 100%;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
background: var(--surface-app);
|
||||||
|
color: var(--text-body);
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer components {
|
||||||
|
/* Brand gradients — not expressible as plain utilities. */
|
||||||
|
.bg-sunrise {
|
||||||
|
background-image: var(--gradient-sunrise);
|
||||||
|
}
|
||||||
|
.bg-sunrise-soft {
|
||||||
|
background-image: var(--gradient-sunrise-soft);
|
||||||
|
}
|
||||||
|
.bg-navy-grad {
|
||||||
|
background-image: var(--gradient-navy);
|
||||||
|
}
|
||||||
|
/* Tabular figures for aligning currency & counts. */
|
||||||
|
.nums {
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
/* Sunrise focus ring on inputs/selects. */
|
||||||
|
.focus-ring:focus-within {
|
||||||
|
border-color: var(--sunrise-500);
|
||||||
|
box-shadow: var(--shadow-focus);
|
||||||
|
}
|
||||||
|
/* Slim, theme-tinted scrollbar for in-panel scroll areas. */
|
||||||
|
.scrollbar-slim {
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: var(--border-default) transparent;
|
||||||
|
}
|
||||||
|
.scrollbar-slim::-webkit-scrollbar {
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
}
|
||||||
|
.scrollbar-slim::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
.scrollbar-slim::-webkit-scrollbar-thumb {
|
||||||
|
background-color: var(--border-default);
|
||||||
|
background-clip: padding-box;
|
||||||
|
border: 3px solid transparent;
|
||||||
|
border-radius: 9999px;
|
||||||
|
}
|
||||||
|
.scrollbar-slim:hover::-webkit-scrollbar-thumb {
|
||||||
|
background-color: var(--text-faint);
|
||||||
|
}
|
||||||
|
}
|
||||||
111
src/styles/tokens/colors.css
Normal file
111
src/styles/tokens/colors.css
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
/* ============================================================
|
||||||
|
KRISHNA SALES — COLOR TOKENS
|
||||||
|
Field-sales console. Base deep navy, sunrise accent gradient,
|
||||||
|
emerald success, amber warning, slate neutrals.
|
||||||
|
(Design system referenced from the lead-to-policy console.)
|
||||||
|
============================================================ */
|
||||||
|
:root {
|
||||||
|
/* ---- Brand: Navy (primary surface + ink) ---- */
|
||||||
|
--navy-950: #060F24;
|
||||||
|
--navy-900: #0B1B3B; /* base deep navy */
|
||||||
|
--navy-800: #122549;
|
||||||
|
--navy-700: #1B3260;
|
||||||
|
--navy-600: #294780;
|
||||||
|
--navy-500: #3A5DA0;
|
||||||
|
--navy-400: #6987BF;
|
||||||
|
|
||||||
|
/* ---- Brand: Sunrise (accent gradient) ---- */
|
||||||
|
--sunrise-600: #E2552A;
|
||||||
|
--sunrise-500: #F26B3A; /* gradient start */
|
||||||
|
--sunrise-400: #FB8A4A;
|
||||||
|
--sunrise-300: #FBA94C; /* gradient end */
|
||||||
|
--sunrise-200: #FDD09A;
|
||||||
|
--sunrise-100: #FEF0E0;
|
||||||
|
--gradient-sunrise: linear-gradient(100deg, #F26B3A 0%, #FBA94C 100%);
|
||||||
|
--gradient-sunrise-soft: linear-gradient(135deg, #FEF0E0 0%, #FDE4D2 100%);
|
||||||
|
--gradient-navy: linear-gradient(160deg, #0B1B3B 0%, #122549 60%, #1B3260 100%);
|
||||||
|
|
||||||
|
/* ---- Semantic: Emerald (success) ---- */
|
||||||
|
--emerald-700: #0A7A52;
|
||||||
|
--emerald-600: #0E9466;
|
||||||
|
--emerald-500: #16B17C;
|
||||||
|
--emerald-300: #6FD7B0;
|
||||||
|
--emerald-100: #DBF5EB;
|
||||||
|
|
||||||
|
/* ---- Semantic: Amber (warning) ---- */
|
||||||
|
--amber-700: #B26B05;
|
||||||
|
--amber-600: #D98512;
|
||||||
|
--amber-500: #F0A529;
|
||||||
|
--amber-300: #FBCF7E;
|
||||||
|
--amber-100: #FCF1DB;
|
||||||
|
|
||||||
|
/* ---- Semantic: Ruby (error) ---- */
|
||||||
|
--ruby-700: #B4243B;
|
||||||
|
--ruby-600: #D63A52;
|
||||||
|
--ruby-500: #E85A70;
|
||||||
|
--ruby-100: #FBE2E6;
|
||||||
|
|
||||||
|
/* ---- Semantic: Sky (info) ---- */
|
||||||
|
--sky-700: #1565B8;
|
||||||
|
--sky-600: #2480DB;
|
||||||
|
--sky-500: #4AA0EF;
|
||||||
|
--sky-100: #DCEEFC;
|
||||||
|
|
||||||
|
/* ---- Neutral: Slate ---- */
|
||||||
|
--slate-950: #0E1726;
|
||||||
|
--slate-900: #1B2638;
|
||||||
|
--slate-800: #2E3B52;
|
||||||
|
--slate-700: #45536E;
|
||||||
|
--slate-600: #5E6E8C;
|
||||||
|
--slate-500: #7C8AA6;
|
||||||
|
--slate-400: #9DA9C2;
|
||||||
|
--slate-300: #C2CADB;
|
||||||
|
--slate-200: #DDE3EE;
|
||||||
|
--slate-150: #E8ECF4;
|
||||||
|
--slate-100: #F0F3F8;
|
||||||
|
--slate-50: #F7F9FC;
|
||||||
|
--white: #FFFFFF;
|
||||||
|
|
||||||
|
/* ---- Semantic aliases (light mode) ---- */
|
||||||
|
/* Surfaces */
|
||||||
|
--surface-app: var(--slate-50);
|
||||||
|
--surface-card: var(--white);
|
||||||
|
--surface-sunk: var(--slate-100);
|
||||||
|
--surface-raised: var(--white);
|
||||||
|
--surface-navy: var(--navy-900);
|
||||||
|
--surface-navy-soft: var(--navy-800);
|
||||||
|
|
||||||
|
/* Text */
|
||||||
|
--text-strong: var(--navy-900);
|
||||||
|
--text-body: var(--slate-800);
|
||||||
|
--text-muted: var(--slate-600);
|
||||||
|
--text-faint: var(--slate-500);
|
||||||
|
--text-on-navy: #EAF0FA;
|
||||||
|
--text-on-navy-muted: #9DB0D2;
|
||||||
|
--text-on-accent: #FFFFFF;
|
||||||
|
--text-accent: var(--sunrise-600);
|
||||||
|
--text-link: var(--sky-700);
|
||||||
|
|
||||||
|
/* Borders / hairlines */
|
||||||
|
--border-subtle: var(--slate-200);
|
||||||
|
--border-default: var(--slate-300);
|
||||||
|
--border-strong: var(--slate-400);
|
||||||
|
--border-navy: rgba(255,255,255,0.12);
|
||||||
|
|
||||||
|
/* Brand action */
|
||||||
|
--action-primary: var(--sunrise-500);
|
||||||
|
--action-primary-hover: var(--sunrise-600);
|
||||||
|
--action-navy: var(--navy-900);
|
||||||
|
--action-navy-hover: var(--navy-800);
|
||||||
|
--focus-ring: rgba(242,107,58,0.45);
|
||||||
|
|
||||||
|
/* Status */
|
||||||
|
--status-success: var(--emerald-600);
|
||||||
|
--status-success-soft: var(--emerald-100);
|
||||||
|
--status-warning: var(--amber-600);
|
||||||
|
--status-warning-soft: var(--amber-100);
|
||||||
|
--status-error: var(--ruby-600);
|
||||||
|
--status-error-soft: var(--ruby-100);
|
||||||
|
--status-info: var(--sky-600);
|
||||||
|
--status-info-soft: var(--sky-100);
|
||||||
|
}
|
||||||
25
src/styles/tokens/spacing.css
Normal file
25
src/styles/tokens/spacing.css
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
/* ============================================================
|
||||||
|
KRISHNA SALES — RADII & ELEVATION
|
||||||
|
============================================================ */
|
||||||
|
:root {
|
||||||
|
/* ---- Corner radii ---- */
|
||||||
|
--radius-xs: 6px;
|
||||||
|
--radius-sm: 8px;
|
||||||
|
--radius-md: 12px; /* default control radius */
|
||||||
|
--radius-lg: 16px; /* default card radius */
|
||||||
|
--radius-xl: 20px;
|
||||||
|
--radius-2xl: 28px;
|
||||||
|
--radius-pill: 999px;
|
||||||
|
|
||||||
|
/* ---- Soft elevation (navy-tinted, low spread) ---- */
|
||||||
|
--shadow-xs: 0 1px 2px rgba(11,27,59,0.06);
|
||||||
|
--shadow-sm: 0 1px 3px rgba(11,27,59,0.08), 0 1px 2px rgba(11,27,59,0.04);
|
||||||
|
--shadow-md: 0 4px 12px rgba(11,27,59,0.08), 0 2px 4px rgba(11,27,59,0.04);
|
||||||
|
--shadow-lg: 0 12px 28px rgba(11,27,59,0.12), 0 4px 8px rgba(11,27,59,0.06);
|
||||||
|
--shadow-xl: 0 24px 56px rgba(11,27,59,0.18), 0 8px 16px rgba(11,27,59,0.08);
|
||||||
|
--shadow-focus: 0 0 0 3px var(--focus-ring);
|
||||||
|
--shadow-sunrise: 0 8px 24px rgba(242,107,58,0.22);
|
||||||
|
|
||||||
|
/* ---- Layout ---- */
|
||||||
|
--container-max: 1280px;
|
||||||
|
}
|
||||||
30
src/styles/tokens/typography.css
Normal file
30
src/styles/tokens/typography.css
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
/* ============================================================
|
||||||
|
KRISHNA SALES — TYPOGRAPHY TOKENS
|
||||||
|
Inter for UI & body. Inter Tight for oversized numerals.
|
||||||
|
============================================================ */
|
||||||
|
:root {
|
||||||
|
--font-sans: 'Inter', system-ui, -apple-system, 'Segoe UI', sans-serif;
|
||||||
|
--font-numeric: 'Inter Tight', 'Inter', system-ui, sans-serif;
|
||||||
|
--font-mono: 'SF Mono', ui-monospace, 'Menlo', monospace;
|
||||||
|
|
||||||
|
/* ---- Type scale (px) ---- */
|
||||||
|
--text-2xs: 11px;
|
||||||
|
--text-xs: 12px;
|
||||||
|
--text-sm: 13px;
|
||||||
|
--text-base: 14px;
|
||||||
|
--text-md: 15px;
|
||||||
|
--text-lg: 17px;
|
||||||
|
--text-xl: 20px;
|
||||||
|
--text-2xl: 24px;
|
||||||
|
--text-3xl: 30px;
|
||||||
|
--text-4xl: 38px;
|
||||||
|
--text-5xl: 48px;
|
||||||
|
--text-6xl: 64px;
|
||||||
|
|
||||||
|
/* ---- Weights ---- */
|
||||||
|
--weight-regular: 400;
|
||||||
|
--weight-medium: 500;
|
||||||
|
--weight-semibold: 600;
|
||||||
|
--weight-bold: 700;
|
||||||
|
--weight-extrabold: 800;
|
||||||
|
}
|
||||||
@ -1,7 +1,8 @@
|
|||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from 'vite'
|
||||||
import react from '@vitejs/plugin-react'
|
import react from '@vitejs/plugin-react'
|
||||||
|
import tailwindcss from '@tailwindcss/vite'
|
||||||
|
|
||||||
// https://vite.dev/config/
|
// https://vite.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react()],
|
plugins: [react(), tailwindcss()],
|
||||||
})
|
})
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user