Compare commits
No commits in common. "eaf0ee3d6389483954440d04bb3b888a9c28b1ea" and "634e43a6c99f381ae6998941a44536b5595e791d" have entirely different histories.
eaf0ee3d63
...
634e43a6c9
71
src/App.tsx
71
src/App.tsx
@ -2,17 +2,17 @@ import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom'
|
|||||||
import { AuthProvider } from './auth/AuthProvider'
|
import { AuthProvider } from './auth/AuthProvider'
|
||||||
import { LoginPage } from './screens/LoginPage'
|
import { LoginPage } from './screens/LoginPage'
|
||||||
import { ConsoleLayout } from './screens/ConsoleLayout'
|
import { ConsoleLayout } from './screens/ConsoleLayout'
|
||||||
|
import { OrdersPage } from './screens/OrdersPage'
|
||||||
|
import { MyOrdersPage } from './screens/MyOrdersPage'
|
||||||
|
import { CallsPage } from './screens/CallsPage'
|
||||||
|
import { MyCallsPage } from './screens/MyCallsPage'
|
||||||
|
import { StoresPage } from './screens/StoresPage'
|
||||||
|
import { DailyLogsPage } from './screens/DailyLogsPage'
|
||||||
|
import { MyDailyLogsPage } from './screens/MyDailyLogsPage'
|
||||||
|
import { DailySalesReportPage } from './screens/DailySalesReportPage'
|
||||||
|
import { ReportPage } from './screens/ReportPage'
|
||||||
|
import { DatasetsPage } from './screens/admin/DatasetsPage'
|
||||||
import { DatasetItemsPage } from './screens/admin/DatasetItemsPage'
|
import { DatasetItemsPage } from './screens/admin/DatasetItemsPage'
|
||||||
import { ProtectedRoute, getDefaultRoute } from './auth/ProtectedRoute'
|
|
||||||
import { useAuth } from './auth/context'
|
|
||||||
|
|
||||||
function RootRedirect() {
|
|
||||||
const { roles, isAdmin } = useAuth();
|
|
||||||
return <Navigate to={getDefaultRoute(roles, isAdmin)} replace />;
|
|
||||||
}
|
|
||||||
|
|
||||||
import { routeConfig } from './routesConfig'
|
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
@ -21,32 +21,35 @@ function App() {
|
|||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/login" element={<LoginPage />} />
|
<Route path="/login" element={<LoginPage />} />
|
||||||
<Route element={<ConsoleLayout />}>
|
<Route element={<ConsoleLayout />}>
|
||||||
{routeConfig.map((route, i) => {
|
<Route path="orders" element={<OrdersPage />} />
|
||||||
const path = route.path.startsWith('/') ? route.path.substring(1) : route.path;
|
<Route path="orders/:instanceId" element={<OrdersPage />} />
|
||||||
|
|
||||||
// Maintain nested route for Dataset items specifically
|
|
||||||
if (path === 'admin/datasets') {
|
|
||||||
return (
|
|
||||||
<Route
|
|
||||||
key={i}
|
|
||||||
path={path}
|
|
||||||
element={<ProtectedRoute adminOnly={route.adminOnly} roles={route.roles}>{route.element}</ProtectedRoute>}
|
|
||||||
>
|
|
||||||
<Route path=":id" element={<DatasetItemsPage />} />
|
|
||||||
</Route>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
<Route path="my-orders" element={<MyOrdersPage />} />
|
||||||
<Route
|
<Route path="my-orders/:instanceId" element={<MyOrdersPage />} />
|
||||||
key={i}
|
|
||||||
path={path}
|
<Route path="calls" element={<CallsPage />} />
|
||||||
element={<ProtectedRoute roles={route.roles} adminOnly={route.adminOnly}>{route.element}</ProtectedRoute>}
|
<Route path="calls/:instanceId" element={<CallsPage />} />
|
||||||
/>
|
|
||||||
);
|
<Route path="my-calls" element={<MyCallsPage />} />
|
||||||
})}
|
<Route path="my-calls/:instanceId" element={<MyCallsPage />} />
|
||||||
|
|
||||||
|
<Route path="stores" element={<StoresPage />} />
|
||||||
|
<Route path="stores/:instanceId" element={<StoresPage />} />
|
||||||
|
|
||||||
|
<Route path="daily" element={<DailyLogsPage />} />
|
||||||
|
<Route path="daily/:instanceId" element={<DailyLogsPage />} />
|
||||||
|
|
||||||
|
<Route path="my-daily" element={<MyDailyLogsPage />} />
|
||||||
|
<Route path="my-daily/:instanceId" element={<MyDailyLogsPage />} />
|
||||||
|
|
||||||
|
<Route path="reports/:reportType" element={<ReportPage />} />
|
||||||
|
<Route path="sales-report" element={<DailySalesReportPage />} />
|
||||||
|
|
||||||
<Route path="*" element={<RootRedirect />} />
|
<Route path="admin/datasets" element={<DatasetsPage />}>
|
||||||
|
<Route path=":id" element={<DatasetItemsPage />} />
|
||||||
|
</Route>
|
||||||
|
|
||||||
|
<Route path="*" element={<Navigate to="/orders" replace />} />
|
||||||
</Route>
|
</Route>
|
||||||
</Routes>
|
</Routes>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
|
|||||||
@ -122,17 +122,6 @@ export class ZinoClient {
|
|||||||
this.setToken(null);
|
this.setToken(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getMe(): Promise<User> {
|
|
||||||
const res = await this.request<User>('GET', `/usr/app/${APP_ID}/me`);
|
|
||||||
if (this.user) {
|
|
||||||
this.user = { ...this.user, ...res };
|
|
||||||
if (typeof window !== 'undefined') {
|
|
||||||
localStorage.setItem(USER_KEY, JSON.stringify(this.user));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Decode the persisted JWT into a User (no network) or use the saved user. */
|
/** Decode the persisted JWT into a User (no network) or use the saved user. */
|
||||||
currentUser(): User | null {
|
currentUser(): User | null {
|
||||||
if (!this.token) return null;
|
if (!this.token) return null;
|
||||||
@ -144,7 +133,6 @@ export class ZinoClient {
|
|||||||
org_id: String(p.org_id ?? ''),
|
org_id: String(p.org_id ?? ''),
|
||||||
name: p.name ?? '',
|
name: p.name ?? '',
|
||||||
email: p.email ?? '',
|
email: p.email ?? '',
|
||||||
mobile: String(p.mobile ?? p.mobile_number ?? p.phone ?? p.phone_number ?? p.user_mobile ?? ''),
|
|
||||||
roles: p.roles ?? [],
|
roles: p.roles ?? [],
|
||||||
groups: p.groups ?? [],
|
groups: p.groups ?? [],
|
||||||
};
|
};
|
||||||
|
|||||||
@ -21,22 +21,15 @@ export function clientFor(slug: WorkflowSlug): ZinoClient {
|
|||||||
export const orderBookingClient = clientFor('orderBooking');
|
export const orderBookingClient = clientFor('orderBooking');
|
||||||
export const storeClient = clientFor('store');
|
export const storeClient = clientFor('store');
|
||||||
export const dailyReportsClient = clientFor('dailyReports');
|
export const dailyReportsClient = clientFor('dailyReports');
|
||||||
export const userClient = clientFor('user');
|
|
||||||
|
|
||||||
// The user JWT (from /usr/login) is valid across every workflow, but each
|
// The user JWT (from /usr/login) is valid across every workflow, but each
|
||||||
// client holds its own copy — so auth helpers fan out to all of them.
|
// client holds its own copy — so auth helpers fan out to all of them.
|
||||||
const ALL = [orderBookingClient, storeClient, dailyReportsClient, userClient];
|
const ALL = [orderBookingClient, storeClient, dailyReportsClient];
|
||||||
|
|
||||||
/** Log in once and share the JWT with every workflow client. */
|
/** Log in once and share the JWT with every workflow client. */
|
||||||
export async function loginAll(email: string, password: string, orgId?: string) {
|
export async function loginAll(email: string, password: string, orgId?: string) {
|
||||||
const res = await orderBookingClient.login(email, password, orgId);
|
const res = await orderBookingClient.login(email, password, orgId);
|
||||||
ALL.forEach((c) => c.setToken(res.token));
|
ALL.forEach((c) => c.setToken(res.token));
|
||||||
try {
|
|
||||||
const me = await orderBookingClient.getMe();
|
|
||||||
res.user = { ...res.user, ...me };
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Failed to fetch user profile:', e);
|
|
||||||
}
|
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -177,38 +177,11 @@ export const DAILY_REPORTS = {
|
|||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
// --- User Management (src/docs/api/user.md) ---
|
|
||||||
export const USER = {
|
|
||||||
workflowUuid: '9874af12-78b7-4730-834a-19a32221f3aa',
|
|
||||||
versionUuid: '', // No version UUID provided, server will use latest
|
|
||||||
activities: {
|
|
||||||
ADD_USER: {
|
|
||||||
uid: '3a8f132e-8c63-4d4d-b466-67595d345a99', // init
|
|
||||||
fields: {
|
|
||||||
name: 'name', // text
|
|
||||||
email: 'email', // email
|
|
||||||
password: 'password', // password
|
|
||||||
},
|
|
||||||
},
|
|
||||||
EDIT_USER: {
|
|
||||||
uid: '3091530c-4315-42b9-b53b-a148f2c54a81',
|
|
||||||
fields: {
|
|
||||||
name: 'name_3', // text
|
|
||||||
email: 'email_3', // email
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
recordViews: {
|
|
||||||
SALES_OFFICERS: '406f89d7-09f6-42ba-ba35-84334ccb6204',
|
|
||||||
},
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
// All workflows keyed by slug, for generic lookup.
|
// All workflows keyed by slug, for generic lookup.
|
||||||
export const WORKFLOWS = {
|
export const WORKFLOWS = {
|
||||||
orderBooking: ORDER_BOOKING,
|
orderBooking: ORDER_BOOKING,
|
||||||
store: STORE,
|
store: STORE,
|
||||||
dailyReports: DAILY_REPORTS,
|
dailyReports: DAILY_REPORTS,
|
||||||
user: USER,
|
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
// --- Daily Sales Report Constants ---
|
// --- Daily Sales Report Constants ---
|
||||||
@ -244,8 +217,6 @@ export const SALES_REPORT_SO_NAMES = [
|
|||||||
export const PIPELINE = {
|
export const PIPELINE = {
|
||||||
endpoints: {
|
endpoints: {
|
||||||
nearestStores: '/api/papi2/nearest-stores',
|
nearestStores: '/api/papi2/nearest-stores',
|
||||||
dailySalesReport: '/api/papi2/daily-sales-report',
|
dailySalesReport: '/api/papi2/daily-sales-report'
|
||||||
productiveCallSummary: '/api/papi2/productive-call-summary',
|
|
||||||
salesOfficers: '/api/papi2/sales-officers'
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@ -10,10 +10,8 @@ export interface User {
|
|||||||
org_id: string;
|
org_id: string;
|
||||||
name: string;
|
name: string;
|
||||||
email: string;
|
email: string;
|
||||||
mobile?: string;
|
|
||||||
roles: string[];
|
roles: string[];
|
||||||
groups: string[];
|
groups: string[];
|
||||||
is_admin?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LoginResponse {
|
export interface LoginResponse {
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { useState, type ReactNode, useEffect } from 'react';
|
import { useState, type ReactNode } from 'react';
|
||||||
import { loginAll, logoutAll, currentToken, orderBookingClient } from '../api/clients';
|
import { loginAll, logoutAll, currentToken } from '../api/clients';
|
||||||
import { AuthCtx, type AuthValue } from './context';
|
import { AuthCtx, type AuthValue } from './context';
|
||||||
|
|
||||||
export function AuthProvider({ children }: { children: ReactNode }) {
|
export function AuthProvider({ children }: { children: ReactNode }) {
|
||||||
@ -7,27 +7,9 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||||||
const [userEmail, setUserEmail] = useState<string | null>(() => {
|
const [userEmail, setUserEmail] = useState<string | null>(() => {
|
||||||
return typeof window !== 'undefined' ? localStorage.getItem('krishna_sales_user_email') : null;
|
return typeof window !== 'undefined' ? localStorage.getItem('krishna_sales_user_email') : null;
|
||||||
});
|
});
|
||||||
const [isAdmin, setIsAdmin] = useState<boolean>(() => {
|
|
||||||
return orderBookingClient.currentUser()?.is_admin ?? false;
|
|
||||||
});
|
|
||||||
const [roles, setRoles] = useState<string[]>(() => {
|
|
||||||
return orderBookingClient.currentUser()?.roles ?? [];
|
|
||||||
});
|
|
||||||
|
|
||||||
// If we are authenticated but don't have isAdmin from cache, we might want to fetch it
|
|
||||||
useEffect(() => {
|
|
||||||
if (authed && (!isAdmin || roles.length === 0)) {
|
|
||||||
orderBookingClient.getMe().then(me => {
|
|
||||||
if (me.is_admin) setIsAdmin(true);
|
|
||||||
if (me.roles) setRoles(me.roles);
|
|
||||||
}).catch(console.error);
|
|
||||||
}
|
|
||||||
}, [authed]);
|
|
||||||
|
|
||||||
const value: AuthValue = {
|
const value: AuthValue = {
|
||||||
authed,
|
authed,
|
||||||
isAdmin,
|
|
||||||
roles,
|
|
||||||
userEmail,
|
userEmail,
|
||||||
login: async (email, password, orgId) => {
|
login: async (email, password, orgId) => {
|
||||||
const res = await loginAll(email, password, orgId);
|
const res = await loginAll(email, password, orgId);
|
||||||
@ -36,15 +18,11 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||||||
setUserEmail(res.user.email);
|
setUserEmail(res.user.email);
|
||||||
localStorage.setItem('krishna_sales_user_email', res.user.email);
|
localStorage.setItem('krishna_sales_user_email', res.user.email);
|
||||||
}
|
}
|
||||||
setIsAdmin(!!res.user?.is_admin);
|
|
||||||
setRoles(res.user?.roles ?? []);
|
|
||||||
},
|
},
|
||||||
logout: () => {
|
logout: () => {
|
||||||
logoutAll();
|
logoutAll();
|
||||||
setAuthed(false);
|
setAuthed(false);
|
||||||
setUserEmail(null);
|
setUserEmail(null);
|
||||||
setIsAdmin(false);
|
|
||||||
setRoles([]);
|
|
||||||
localStorage.removeItem('krishna_sales_user_email');
|
localStorage.removeItem('krishna_sales_user_email');
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,63 +0,0 @@
|
|||||||
import { useNavigate } from 'react-router-dom';
|
|
||||||
import { ShieldAlert } from 'lucide-react';
|
|
||||||
import { useAuth } from './context';
|
|
||||||
import { Button } from '../components/buttons';
|
|
||||||
import { Card } from '../components/reusable';
|
|
||||||
|
|
||||||
export function getDefaultRoute(roles: string[], isAdmin: boolean) {
|
|
||||||
if (isAdmin) return '/orders';
|
|
||||||
if (roles.includes('Manager')) return '/orders';
|
|
||||||
if (roles.includes('Sales Officer')) return '/my-orders';
|
|
||||||
return '/stores';
|
|
||||||
}
|
|
||||||
|
|
||||||
function AccessDenied() {
|
|
||||||
const { roles, isAdmin } = useAuth();
|
|
||||||
const navigate = useNavigate();
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col items-center justify-center min-h-[500px] h-full p-4 m-4">
|
|
||||||
<Card
|
|
||||||
className="max-w-md w-full shadow-sm mx-auto"
|
|
||||||
bodyClassName="flex flex-col items-center justify-center text-center p-8"
|
|
||||||
pad={false}
|
|
||||||
>
|
|
||||||
<ShieldAlert className="w-16 h-16 text-red-500 mb-4 opacity-90 mx-auto" />
|
|
||||||
<h2 className="text-2xl font-bold text-gray-800 mb-2">Access Denied</h2>
|
|
||||||
<p className="text-gray-500 mb-8">
|
|
||||||
You don't have permission to view this page. If you believe this is an error, please contact your administrator.
|
|
||||||
</p>
|
|
||||||
<Button
|
|
||||||
variant="primary"
|
|
||||||
onClick={() => navigate(getDefaultRoute(roles, isAdmin))}
|
|
||||||
>
|
|
||||||
Return to Homepage
|
|
||||||
</Button>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ProtectedRoute({
|
|
||||||
children,
|
|
||||||
roles,
|
|
||||||
adminOnly
|
|
||||||
}: {
|
|
||||||
children: React.ReactNode,
|
|
||||||
roles?: string[],
|
|
||||||
adminOnly?: boolean
|
|
||||||
}) {
|
|
||||||
const { roles: userRoles, isAdmin } = useAuth();
|
|
||||||
|
|
||||||
if (adminOnly && !isAdmin) {
|
|
||||||
return <AccessDenied />;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (roles && roles.length > 0) {
|
|
||||||
const hasRole = roles.some(role => userRoles.includes(role));
|
|
||||||
if (!hasRole) {
|
|
||||||
return <AccessDenied />;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return <>{children}</>;
|
|
||||||
}
|
|
||||||
@ -2,8 +2,6 @@ import { createContext, useContext } from 'react';
|
|||||||
|
|
||||||
export interface AuthValue {
|
export interface AuthValue {
|
||||||
authed: boolean;
|
authed: boolean;
|
||||||
isAdmin: boolean;
|
|
||||||
roles: string[];
|
|
||||||
userEmail: string | null;
|
userEmail: string | null;
|
||||||
login: (email: string, password: string, orgId?: string) => Promise<void>;
|
login: (email: string, password: string, orgId?: string) => Promise<void>;
|
||||||
logout: () => void;
|
logout: () => void;
|
||||||
|
|||||||
@ -17,8 +17,7 @@ import {
|
|||||||
WfLookupField,
|
WfLookupField,
|
||||||
RadioField,
|
RadioField,
|
||||||
} from './fields';
|
} from './fields';
|
||||||
import { ORDER_BOOKING, STORE, DAILY_REPORTS, PIPELINE } from '../../api/config';
|
import { ORDER_BOOKING, STORE } from '../../api/config';
|
||||||
import { isCategoryColumn, isProductColumn } from './fields/gridUtils';
|
|
||||||
|
|
||||||
export interface DynamicFormProps {
|
export interface DynamicFormProps {
|
||||||
client: ZinoClient;
|
client: ZinoClient;
|
||||||
@ -53,7 +52,7 @@ export function DynamicForm({ client, activityId: initialActivityId, instanceId:
|
|||||||
let mounted = true;
|
let mounted = true;
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
client.formSchema(currentActivityId, currentInstanceId)
|
client.formSchema(currentActivityId, currentInstanceId)
|
||||||
.then(async (res) => {
|
.then(res => {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setSchema(res);
|
setSchema(res);
|
||||||
const defaultValues: Record<string, unknown> = {};
|
const defaultValues: Record<string, unknown> = {};
|
||||||
@ -79,54 +78,15 @@ export function DynamicForm({ client, activityId: initialActivityId, instanceId:
|
|||||||
res.fields.filter(f => f.data_type === 'image' || f.data_type === 'file').map(f => f.id)
|
res.fields.filter(f => f.data_type === 'image' || f.data_type === 'file').map(f => f.id)
|
||||||
);
|
);
|
||||||
|
|
||||||
const mapPrefillData = (sourceData: Record<string, unknown>) => {
|
|
||||||
res.fields.forEach(f => {
|
|
||||||
if (imageFieldIds.has(f.id)) return;
|
|
||||||
|
|
||||||
const getBaseId = (id: string) => id.replace(/_\d+$/, '');
|
|
||||||
|
|
||||||
if (sourceData[f.id] !== undefined) {
|
|
||||||
defaultValues[f.id] = sourceData[f.id];
|
|
||||||
} else if (f.mapped_workflow_field && sourceData[f.mapped_workflow_field] !== undefined) {
|
|
||||||
defaultValues[f.id] = sourceData[f.mapped_workflow_field];
|
|
||||||
} else if (sourceData[getBaseId(f.id)] !== undefined) {
|
|
||||||
defaultValues[f.id] = sourceData[getBaseId(f.id)];
|
|
||||||
} else {
|
|
||||||
const matchingDataKey = Object.keys(sourceData).find(k => getBaseId(k) === f.id || getBaseId(k) === getBaseId(f.id) || k === f.mapped_workflow_field);
|
|
||||||
if (matchingDataKey) {
|
|
||||||
defaultValues[f.id] = sourceData[matchingDataKey];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (f.data_type === 'grid' && Array.isArray(defaultValues[f.id])) {
|
|
||||||
defaultValues[f.id] = (defaultValues[f.id] as Record<string, unknown>[]).map(row => {
|
|
||||||
const newRow: Record<string, unknown> = { ...row };
|
|
||||||
f.columns?.forEach(col => {
|
|
||||||
const colBaseId = getBaseId(col.id);
|
|
||||||
if (row[col.id] !== undefined) {
|
|
||||||
newRow[col.id] = row[col.id];
|
|
||||||
} else if (col.mapped_workflow_field && row[col.mapped_workflow_field] !== undefined) {
|
|
||||||
newRow[col.id] = row[col.mapped_workflow_field];
|
|
||||||
} else if (row[colBaseId] !== undefined) {
|
|
||||||
newRow[col.id] = row[colBaseId];
|
|
||||||
} else {
|
|
||||||
const matchingRowKey = Object.keys(row).find(k => getBaseId(k) === col.id || getBaseId(k) === colBaseId || k === col.mapped_workflow_field);
|
|
||||||
if (matchingRowKey) {
|
|
||||||
newRow[col.id] = row[matchingRowKey];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return newRow;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!ignorePrefill) {
|
if (!ignorePrefill) {
|
||||||
if (res.prefill_data && Object.keys(res.prefill_data).length > 0) {
|
if (res.prefill_data) {
|
||||||
mapPrefillData(res.prefill_data);
|
Object.entries(res.prefill_data).forEach(([k, v]) => {
|
||||||
|
if (!imageFieldIds.has(k)) defaultValues[k] = v;
|
||||||
|
});
|
||||||
} else if (res.data) {
|
} else if (res.data) {
|
||||||
mapPrefillData(res.data);
|
Object.entries(res.data).forEach(([k, v]) => {
|
||||||
|
if (!imageFieldIds.has(k)) defaultValues[k] = v;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -141,81 +101,6 @@ export function DynamicForm({ client, activityId: initialActivityId, instanceId:
|
|||||||
if (!imageFieldIds.has(k)) defaultValues[k] = v;
|
if (!imageFieldIds.has(k)) defaultValues[k] = v;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentActivityId === DAILY_REPORTS.activities.PUNCH_OUT.uid) {
|
|
||||||
const user = client.currentUser();
|
|
||||||
const userEmail = user?.email;
|
|
||||||
if (userEmail) {
|
|
||||||
try {
|
|
||||||
const headers: Record<string, string> = {
|
|
||||||
'templateid': '192',
|
|
||||||
'x-pipeline-version': 'draft',
|
|
||||||
'orgid': '57',
|
|
||||||
'groupid': '25'
|
|
||||||
};
|
|
||||||
|
|
||||||
const summaryRes = await client.request<any>(
|
|
||||||
'POST',
|
|
||||||
PIPELINE.endpoints.productiveCallSummary,
|
|
||||||
{ email: userEmail },
|
|
||||||
headers
|
|
||||||
);
|
|
||||||
let summaryData = summaryRes.data || summaryRes;
|
|
||||||
if (Array.isArray(summaryData)) {
|
|
||||||
summaryData = summaryData[0] || {};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Map the API response keys to match the form field keys
|
|
||||||
if (summaryData.productive_call !== undefined) {
|
|
||||||
summaryData.total_productive_calls = summaryData.productive_call;
|
|
||||||
}
|
|
||||||
if (summaryData.non_productive_call !== undefined) {
|
|
||||||
summaryData.total_non_productive_calls = summaryData.non_productive_call;
|
|
||||||
}
|
|
||||||
|
|
||||||
mapPrefillData(summaryData);
|
|
||||||
} catch (e) {
|
|
||||||
console.warn('Failed to load productive call summary', e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentActivityId === ORDER_BOOKING.activities.LOG_VISIT.uid) {
|
|
||||||
const user = client.currentUser();
|
|
||||||
const userMobile = user?.mobile || localStorage.getItem('krishna_sales_user_mobile') || user?.email;
|
|
||||||
if (userMobile) {
|
|
||||||
try {
|
|
||||||
const headers: Record<string, string> = {
|
|
||||||
'templateid': '192',
|
|
||||||
'x-pipeline-version': 'draft',
|
|
||||||
'orgid': '57',
|
|
||||||
'groupid': '25'
|
|
||||||
};
|
|
||||||
|
|
||||||
const summaryRes = await client.request<any>(
|
|
||||||
'POST',
|
|
||||||
PIPELINE.endpoints.productiveCallSummary,
|
|
||||||
{ mobile: userMobile },
|
|
||||||
headers
|
|
||||||
);
|
|
||||||
let summaryData = summaryRes.data || summaryRes;
|
|
||||||
if (Array.isArray(summaryData)) {
|
|
||||||
summaryData = summaryData[0] || {};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (summaryData.productive_call !== undefined) {
|
|
||||||
summaryData.total_productive_calls = summaryData.productive_call;
|
|
||||||
}
|
|
||||||
if (summaryData.non_productive_call !== undefined) {
|
|
||||||
summaryData.total_non_productive_calls = summaryData.non_productive_call;
|
|
||||||
}
|
|
||||||
|
|
||||||
mapPrefillData(summaryData);
|
|
||||||
} catch (e) {
|
|
||||||
console.warn('Failed to load productive call summary for Log Visit', e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setValues(defaultValues);
|
setValues(defaultValues);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
@ -386,11 +271,11 @@ export function DynamicForm({ client, activityId: initialActivityId, instanceId:
|
|||||||
|
|
||||||
if (cat && prod) {
|
if (cat && prod) {
|
||||||
const key = `${cat}::${prod}`;
|
const key = `${cat}::${prod}`;
|
||||||
if (indexMap.has(key) && currentBags > 0) {
|
if (indexMap.has(key)) {
|
||||||
const targetIdx = indexMap.get(key)!;
|
const targetIdx = indexMap.get(key)!;
|
||||||
const existingRow = merged[targetIdx];
|
const existingRow = merged[targetIdx];
|
||||||
const existingBags = Number(getGridVal(existingRow, ['bags', 'quantity'])) || 0;
|
const existingBags = Number(getGridVal(existingRow, ['bags', 'quantity'])) || 0;
|
||||||
const addedBags = currentBags;
|
const addedBags = currentBags > 0 ? currentBags : 1;
|
||||||
const newBags = existingBags + addedBags;
|
const newBags = existingBags + addedBags;
|
||||||
const skuVal = Number(getGridVal(existingRow, ['sku'], ['code']) || currentSku || 0);
|
const skuVal = Number(getGridVal(existingRow, ['sku'], ['code']) || currentSku || 0);
|
||||||
|
|
||||||
@ -401,7 +286,7 @@ export function DynamicForm({ client, activityId: initialActivityId, instanceId:
|
|||||||
row_kgs: skuVal * newBags,
|
row_kgs: skuVal * newBags,
|
||||||
};
|
};
|
||||||
continue;
|
continue;
|
||||||
} else if (!indexMap.has(key)) {
|
} else {
|
||||||
indexMap.set(key, merged.length);
|
indexMap.set(key, merged.length);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -598,113 +483,13 @@ export function DynamicForm({ client, activityId: initialActivityId, instanceId:
|
|||||||
payload[f.id] = [fileMeta];
|
payload[f.id] = [fileMeta];
|
||||||
} else if (f.data_type.startsWith('grid') || f.data_type === 'smart_grid') {
|
} else if (f.data_type.startsWith('grid') || f.data_type === 'smart_grid') {
|
||||||
const gridRows = Array.isArray(val) ? val : [];
|
const gridRows = Array.isArray(val) ? val : [];
|
||||||
const getFormattedGridVal = (colKey: string, rawVal: unknown, colDef?: FormScreenField) => {
|
const cleanRows = gridRows.filter(r => {
|
||||||
if (rawVal === undefined || rawVal === null || String(rawVal).trim() === '') return undefined;
|
return Object.keys(r).some(k => {
|
||||||
|
const v = r[k];
|
||||||
const keyLower = colKey.toLowerCase();
|
return v !== undefined && v !== null && String(v).trim() !== '';
|
||||||
const colNameLower = (colDef?.name || '').toLowerCase();
|
|
||||||
const dataType = colDef?.data_type;
|
|
||||||
|
|
||||||
// 1. Explicit number data_type from form screen API
|
|
||||||
if (dataType === 'number') {
|
|
||||||
const num = Number(rawVal);
|
|
||||||
return !isNaN(num) ? num : rawVal;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Explicit string/select/id_gen data_type from API or SKU/code identifier columns
|
|
||||||
const isStringCol = dataType === 'select' ||
|
|
||||||
dataType === 'text' ||
|
|
||||||
dataType === 'string' ||
|
|
||||||
dataType === 'id_gen' ||
|
|
||||||
keyLower.includes('sku') ||
|
|
||||||
keyLower.includes('code') ||
|
|
||||||
keyLower.includes('br_code') ||
|
|
||||||
keyLower === 'br' ||
|
|
||||||
colNameLower.includes('sku') ||
|
|
||||||
colNameLower.includes('code');
|
|
||||||
|
|
||||||
if (isStringCol) {
|
|
||||||
return String(rawVal);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Bags / Quantity / Row Kgs quantity columns without explicit text colDef
|
|
||||||
const isNumericQuantity = keyLower === 'bags' ||
|
|
||||||
keyLower === 'quantity' ||
|
|
||||||
keyLower === 'row_kgs' ||
|
|
||||||
colNameLower === 'bags' ||
|
|
||||||
colNameLower === 'quantity';
|
|
||||||
|
|
||||||
if (isNumericQuantity) {
|
|
||||||
const num = Number(rawVal);
|
|
||||||
return !isNaN(num) ? num : rawVal;
|
|
||||||
}
|
|
||||||
|
|
||||||
return rawVal;
|
|
||||||
};
|
|
||||||
|
|
||||||
const cleanRows = gridRows
|
|
||||||
.filter(r => {
|
|
||||||
return Object.keys(r).some(k => {
|
|
||||||
const v = r[k];
|
|
||||||
return v !== undefined && v !== null && String(v).trim() !== '';
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.map(r => {
|
|
||||||
const formattedRow: Record<string, unknown> = {};
|
|
||||||
|
|
||||||
// 1. Process all existing keys in row r
|
|
||||||
for (const [k, v] of Object.entries(r)) {
|
|
||||||
if (v === undefined || v === null || String(v).trim() === '') continue;
|
|
||||||
|
|
||||||
const colDef = f.columns?.find(c =>
|
|
||||||
c.id === k ||
|
|
||||||
(c.name || '').toLowerCase() === k.toLowerCase() ||
|
|
||||||
c.mapped_workflow_field === k
|
|
||||||
);
|
|
||||||
|
|
||||||
const formattedVal = getFormattedGridVal(k, v, colDef);
|
|
||||||
if (formattedVal !== undefined) {
|
|
||||||
formattedRow[k] = formattedVal;
|
|
||||||
|
|
||||||
if (colDef && colDef.id && colDef.id !== k) {
|
|
||||||
formattedRow[colDef.id] = formattedVal;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Also map any schema defined columns from f.columns
|
|
||||||
if (f.columns && f.columns.length > 0) {
|
|
||||||
for (const col of f.columns) {
|
|
||||||
if (formattedRow[col.id] === undefined) {
|
|
||||||
const matchKey = Object.keys(r).find(rk =>
|
|
||||||
rk.toLowerCase() === col.id.toLowerCase() ||
|
|
||||||
rk.toLowerCase() === (col.name || '').toLowerCase() ||
|
|
||||||
(col.mapped_workflow_field && rk.toLowerCase() === col.mapped_workflow_field.toLowerCase()) ||
|
|
||||||
(isCategoryColumn(col.id, col.name) && (rk.toLowerCase().includes('category') || rk === 'cat')) ||
|
|
||||||
(isProductColumn(col.id, col.name) && (rk.toLowerCase().includes('product') || rk === 'productName')) ||
|
|
||||||
(col.name?.toLowerCase().includes('bags') && (rk.toLowerCase().includes('bags') || rk.toLowerCase().includes('quantity')))
|
|
||||||
);
|
|
||||||
|
|
||||||
if (matchKey && r[matchKey] !== undefined && r[matchKey] !== null && String(r[matchKey]).trim() !== '') {
|
|
||||||
const formattedVal = getFormattedGridVal(matchKey, r[matchKey], col);
|
|
||||||
if (formattedVal !== undefined) {
|
|
||||||
formattedRow[col.id] = formattedVal;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return formattedRow;
|
|
||||||
});
|
});
|
||||||
|
});
|
||||||
payload[f.id] = cleanRows;
|
payload[f.id] = cleanRows;
|
||||||
} else if (f.data_type === 'number') {
|
|
||||||
if (val !== undefined && val !== null && val !== '') {
|
|
||||||
const num = Number(val);
|
|
||||||
payload[f.id] = !isNaN(num) ? num : val;
|
|
||||||
} else {
|
|
||||||
payload[f.id] = val;
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
payload[f.id] = val;
|
payload[f.id] = val;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,555 +0,0 @@
|
|||||||
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
|
||||||
import type { ZinoClient } from '../../api/client';
|
|
||||||
import type { FormScreenResponse } from '../../api/types';
|
|
||||||
import { ORDER_BOOKING, PIPELINE } from '../../api/config';
|
|
||||||
import { Button } from '../buttons/Button';
|
|
||||||
import { Select } from '../reusable/Select';
|
|
||||||
import { DateField, TimeField, FileInput, TextField } from './fields';
|
|
||||||
import { Spinner } from '../reusable/Spinner';
|
|
||||||
import { DynamicForm } from './DynamicForm';
|
|
||||||
|
|
||||||
export interface LogVisitFormProps {
|
|
||||||
client: ZinoClient;
|
|
||||||
onSuccess?: () => void;
|
|
||||||
onCancel?: () => void;
|
|
||||||
onActivityChange?: (name: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Utility to clean empty values from form data before sending API requests
|
|
||||||
const cleanFormData = (data: Record<string, any>) => {
|
|
||||||
const result: Record<string, any> = {};
|
|
||||||
Object.entries(data).forEach(([k, v]) => {
|
|
||||||
if (v !== '' && v !== null && v !== undefined && !(Array.isArray(v) && v.length === 0)) {
|
|
||||||
result[k] = v;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Dedicated form component for the Log Visit activity.
|
|
||||||
* Calls client.formSchema() to retrieve form screen fields dynamically,
|
|
||||||
* pre-fills daily_log & route_code from initial daily log lookup,
|
|
||||||
* lazy-fetches select_store options after daily log completion,
|
|
||||||
* handles mobile pipeline prefill,
|
|
||||||
* AND wires activity chaining seamlessly into DynamicForm for subsequent activities.
|
|
||||||
*/
|
|
||||||
export function LogVisitForm({ client, onSuccess, onCancel, onActivityChange }: LogVisitFormProps) {
|
|
||||||
const activityId = ORDER_BOOKING.activities.LOG_VISIT.uid;
|
|
||||||
|
|
||||||
const todayStr = new Date().toISOString().split('T')[0];
|
|
||||||
const nowTimeStr = new Date().toTimeString().split(' ')[0].substring(0, 5);
|
|
||||||
|
|
||||||
const [schema, setSchema] = useState<FormScreenResponse | null>(null);
|
|
||||||
const schemaRef = useRef<FormScreenResponse | null>(null);
|
|
||||||
useEffect(() => { schemaRef.current = schema; }, [schema]);
|
|
||||||
|
|
||||||
const [loadingSchema, setLoadingSchema] = useState(true);
|
|
||||||
|
|
||||||
const [values, setValues] = useState<Record<string, any>>({
|
|
||||||
date_of_visit: todayStr,
|
|
||||||
time_of_visit: nowTimeStr,
|
|
||||||
select_store: '',
|
|
||||||
upload_image: [],
|
|
||||||
daily_log: '',
|
|
||||||
route_code: '',
|
|
||||||
});
|
|
||||||
|
|
||||||
const valuesRef = useRef(values);
|
|
||||||
useEffect(() => {
|
|
||||||
valuesRef.current = values;
|
|
||||||
}, [values]);
|
|
||||||
|
|
||||||
const [storeOptions, setStoreOptions] = useState<{ value: string; label: string; _raw?: any }[]>([]);
|
|
||||||
const [fetchingDailyLog, setFetchingDailyLog] = useState(false);
|
|
||||||
const [fetchingStores, setFetchingStores] = useState(false);
|
|
||||||
const fetchingStoresRef = useRef(false);
|
|
||||||
|
|
||||||
const [chainedActivity, setChainedActivity] = useState<{
|
|
||||||
activityId: string;
|
|
||||||
instanceId?: number | string;
|
|
||||||
prefillData?: Record<string, unknown>;
|
|
||||||
} | null>(null);
|
|
||||||
|
|
||||||
const [submitting, setSubmitting] = useState(false);
|
|
||||||
const [submitError, setSubmitError] = useState<string | null>(null);
|
|
||||||
|
|
||||||
// 1. Fetch form schema from API (/app/434/view/form-screens) & Mobile Pipeline prefill
|
|
||||||
useEffect(() => {
|
|
||||||
let mounted = true;
|
|
||||||
setLoadingSchema(true);
|
|
||||||
client.formSchema(activityId)
|
|
||||||
.then(async (res) => {
|
|
||||||
if (!mounted) return;
|
|
||||||
setSchema(res);
|
|
||||||
schemaRef.current = res;
|
|
||||||
|
|
||||||
const initialValues: Record<string, any> = {
|
|
||||||
date_of_visit: todayStr,
|
|
||||||
time_of_visit: nowTimeStr,
|
|
||||||
select_store: '',
|
|
||||||
upload_image: [],
|
|
||||||
daily_log: '',
|
|
||||||
route_code: '',
|
|
||||||
};
|
|
||||||
|
|
||||||
if (res.field_defaults) {
|
|
||||||
Object.entries(res.field_defaults).forEach(([fieldId, def]) => {
|
|
||||||
if (def.value != null) {
|
|
||||||
initialValues[fieldId] = def.value;
|
|
||||||
} else if (def.prefill) {
|
|
||||||
if (def.prefill.value === 'current_date') {
|
|
||||||
initialValues[fieldId] = new Date().toISOString().split('T')[0];
|
|
||||||
} else if (def.prefill.value === 'current_time') {
|
|
||||||
initialValues[fieldId] = new Date().toTimeString().split(' ')[0].substring(0, 5);
|
|
||||||
} else if (def.prefill.value === 'current_user_id') {
|
|
||||||
const user = client.currentUser();
|
|
||||||
initialValues[fieldId] = user ? Number(user.id) : '';
|
|
||||||
} else {
|
|
||||||
initialValues[fieldId] = def.prefill.value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (res.prefill_data && Object.keys(res.prefill_data).length > 0) {
|
|
||||||
Object.assign(initialValues, res.prefill_data);
|
|
||||||
} else if (res.data && Object.keys(res.data).length > 0) {
|
|
||||||
Object.assign(initialValues, res.data);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Execute mobile pipeline prefill
|
|
||||||
const user = client.currentUser();
|
|
||||||
const userMobile = user?.mobile || localStorage.getItem('krishna_sales_user_mobile') || user?.email;
|
|
||||||
if (userMobile) {
|
|
||||||
try {
|
|
||||||
const headers: Record<string, string> = {
|
|
||||||
'templateid': '192',
|
|
||||||
'x-pipeline-version': 'draft',
|
|
||||||
'orgid': '57',
|
|
||||||
'groupid': '25'
|
|
||||||
};
|
|
||||||
|
|
||||||
const summaryRes = await client.request<any>(
|
|
||||||
'POST',
|
|
||||||
PIPELINE.endpoints.productiveCallSummary,
|
|
||||||
{ mobile: userMobile },
|
|
||||||
headers
|
|
||||||
);
|
|
||||||
let summaryData = summaryRes.data || summaryRes;
|
|
||||||
if (Array.isArray(summaryData)) {
|
|
||||||
summaryData = summaryData[0] || {};
|
|
||||||
}
|
|
||||||
if (summaryData.productive_call !== undefined) {
|
|
||||||
summaryData.total_productive_calls = summaryData.productive_call;
|
|
||||||
}
|
|
||||||
if (summaryData.non_productive_call !== undefined) {
|
|
||||||
summaryData.total_non_productive_calls = summaryData.non_productive_call;
|
|
||||||
}
|
|
||||||
Object.assign(initialValues, summaryData);
|
|
||||||
} catch (e) {
|
|
||||||
console.warn('Failed to load productive call summary for Log Visit', e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setValues(prev => {
|
|
||||||
const next = { ...initialValues, ...prev };
|
|
||||||
valuesRef.current = next;
|
|
||||||
return next;
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
console.error('Failed to load Log Visit form schema:', err);
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
if (mounted) setLoadingSchema(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => { mounted = false; };
|
|
||||||
}, [client, activityId, todayStr, nowTimeStr]);
|
|
||||||
|
|
||||||
// 2. Helper to fetch select_store options ONLY using updated prefilled formData
|
|
||||||
const fetchStoreOptions = useCallback(async (formDataOverride?: Record<string, any>) => {
|
|
||||||
if (fetchingStoresRef.current) return;
|
|
||||||
fetchingStoresRef.current = true;
|
|
||||||
setFetchingStores(true);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const formDataToSend = cleanFormData(formDataOverride || valuesRef.current);
|
|
||||||
console.log('[LogVisitForm] Executing select_store lookup with prefilled formData:', formDataToSend);
|
|
||||||
|
|
||||||
const lookupRes = await client.wfLookupRecords({
|
|
||||||
activityId,
|
|
||||||
fieldId: 'select_store',
|
|
||||||
formData: formDataToSend,
|
|
||||||
limit: 200,
|
|
||||||
});
|
|
||||||
|
|
||||||
const arr = Array.isArray(lookupRes)
|
|
||||||
? lookupRes
|
|
||||||
: lookupRes?.data || lookupRes?.records || [];
|
|
||||||
|
|
||||||
// Find display_fields configured in schema for select_store
|
|
||||||
const selectStoreField = schemaRef.current?.fields.find(
|
|
||||||
f => f.id === 'select_store' || f.uid === 'field_1783057892381' || f.name.toLowerCase().includes('select store')
|
|
||||||
);
|
|
||||||
const displayFields = (selectStoreField?.properties?.wf_lookup_config as any)?.display_fields || [];
|
|
||||||
|
|
||||||
const opts = arr.map((row: any) => {
|
|
||||||
let labelText = '';
|
|
||||||
if (displayFields.length > 0) {
|
|
||||||
const labelParts = displayFields
|
|
||||||
.map((df: any) => row[df.field_id])
|
|
||||||
.filter((v: any) => v != null && v !== '');
|
|
||||||
if (labelParts.length > 0) {
|
|
||||||
labelText = labelParts.join(' - ');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!labelText) {
|
|
||||||
const storeName = row.business_name_2 || row.store_name || row.name || row.store;
|
|
||||||
const storeCode = row.store_code || row.code;
|
|
||||||
labelText = storeName
|
|
||||||
? `${storeCode ? `${storeCode} - ` : ''}${storeName}`
|
|
||||||
: `Store #${row.instance_id || row.id}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
value: String(row.instance_id || row.id),
|
|
||||||
label: labelText,
|
|
||||||
_raw: row,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
setStoreOptions(opts);
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Failed to load select_store options:', e);
|
|
||||||
} finally {
|
|
||||||
setFetchingStores(false);
|
|
||||||
fetchingStoresRef.current = false;
|
|
||||||
}
|
|
||||||
}, [client, activityId]);
|
|
||||||
|
|
||||||
// 3. Fetch Daily Log lookup data initially & prefill form state
|
|
||||||
const loadDailyLogData = useCallback(async (dateVal: string, timeVal: string) => {
|
|
||||||
setFetchingDailyLog(true);
|
|
||||||
try {
|
|
||||||
const payload = cleanFormData({
|
|
||||||
date_of_visit: dateVal,
|
|
||||||
time_of_visit: timeVal,
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('[LogVisitForm] Fetching initial daily_log lookup with payload:', payload);
|
|
||||||
|
|
||||||
const dailyLogLookupRes = await client.wfLookupRecords({
|
|
||||||
activityId,
|
|
||||||
fieldId: 'daily_log',
|
|
||||||
formData: payload,
|
|
||||||
limit: 200,
|
|
||||||
});
|
|
||||||
|
|
||||||
const arr = Array.isArray(dailyLogLookupRes)
|
|
||||||
? dailyLogLookupRes
|
|
||||||
: dailyLogLookupRes?.data || dailyLogLookupRes?.records || [];
|
|
||||||
|
|
||||||
if (arr.length > 0) {
|
|
||||||
const firstLog = arr[0];
|
|
||||||
console.log('[LogVisitForm] Successfully fetched daily log record:', firstLog);
|
|
||||||
|
|
||||||
const dailyLogInstanceId = String(firstLog.instance_id || firstLog.id || '');
|
|
||||||
const routeCode = String(
|
|
||||||
firstLog.route_code ||
|
|
||||||
firstLog.route_code_1 ||
|
|
||||||
firstLog.route_code_2 ||
|
|
||||||
firstLog.route_code_3 ||
|
|
||||||
firstLog.route ||
|
|
||||||
''
|
|
||||||
);
|
|
||||||
|
|
||||||
const updated = { ...valuesRef.current };
|
|
||||||
|
|
||||||
// Copy raw fields from daily log into state
|
|
||||||
Object.keys(firstLog).forEach(key => {
|
|
||||||
if (firstLog[key] != null && firstLog[key] !== '') {
|
|
||||||
updated[key] = firstLog[key];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (dailyLogInstanceId) {
|
|
||||||
updated['daily_log'] = dailyLogInstanceId;
|
|
||||||
updated['field_1785225403902'] = dailyLogInstanceId;
|
|
||||||
updated['instance_id'] = firstLog.instance_id || firstLog.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (routeCode) {
|
|
||||||
updated['route_code'] = routeCode;
|
|
||||||
updated['route_code_1'] = routeCode;
|
|
||||||
updated['route_code_2'] = routeCode;
|
|
||||||
updated['route_code_3'] = routeCode;
|
|
||||||
updated['field_1785311859486'] = routeCode;
|
|
||||||
}
|
|
||||||
|
|
||||||
valuesRef.current = updated;
|
|
||||||
setValues(updated);
|
|
||||||
console.log('[LogVisitForm] Daily log prefill complete:', updated);
|
|
||||||
} else {
|
|
||||||
console.warn('[LogVisitForm] No daily log records found for date/time:', dateVal, timeVal);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.warn('Failed to load daily_log lookup for Log Visit', e);
|
|
||||||
} finally {
|
|
||||||
setFetchingDailyLog(false);
|
|
||||||
}
|
|
||||||
}, [client, activityId]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
loadDailyLogData(values.date_of_visit, values.time_of_visit);
|
|
||||||
}, [loadDailyLogData, values.date_of_visit, values.time_of_visit]);
|
|
||||||
|
|
||||||
const handleStoreDropdownOpen = () => {
|
|
||||||
fetchStoreOptions();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleStoreSelect = (fieldId: string, val: string) => {
|
|
||||||
const selectedOpt = storeOptions.find(o => String(o.value) === String(val));
|
|
||||||
const rawRow = selectedOpt?._raw || {};
|
|
||||||
|
|
||||||
setValues(prev => {
|
|
||||||
const next: Record<string, any> = { ...prev, [fieldId]: val, select_store: val, field_1: val };
|
|
||||||
|
|
||||||
// Map raw row fields into values
|
|
||||||
Object.keys(rawRow).forEach(key => {
|
|
||||||
next[key] = rawRow[key];
|
|
||||||
});
|
|
||||||
|
|
||||||
valuesRef.current = next;
|
|
||||||
return next;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
setSubmitting(true);
|
|
||||||
setSubmitError(null);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const validFields = schema?.fields || [];
|
|
||||||
const validFieldIds = new Set(validFields.map(f => f.id));
|
|
||||||
|
|
||||||
// Always include standard Log Visit field IDs
|
|
||||||
['select_store', 'date_of_visit', 'time_of_visit', 'upload_image', 'daily_log', 'route_code'].forEach(id => validFieldIds.add(id));
|
|
||||||
|
|
||||||
const payload: Record<string, any> = {};
|
|
||||||
validFieldIds.forEach(fieldId => {
|
|
||||||
const val = valuesRef.current[fieldId];
|
|
||||||
if (val !== undefined && val !== null && val !== '') {
|
|
||||||
payload[fieldId] = val;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle image upload if present
|
|
||||||
let uploadedFiles: any[] = [];
|
|
||||||
const imgVal = valuesRef.current.upload_image;
|
|
||||||
const fileList = Array.isArray(imgVal) ? imgVal : (imgVal instanceof File ? [imgVal] : []);
|
|
||||||
|
|
||||||
for (const fileItem of fileList) {
|
|
||||||
if (fileItem instanceof File) {
|
|
||||||
const fileMeta = await client.uploadFile(fileItem, {
|
|
||||||
activityId,
|
|
||||||
fieldId: 'upload_image',
|
|
||||||
});
|
|
||||||
uploadedFiles.push(fileMeta);
|
|
||||||
} else {
|
|
||||||
uploadedFiles.push(fileItem);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
payload['upload_image'] = uploadedFiles;
|
|
||||||
|
|
||||||
console.log('[LogVisitForm] Submitting clean startInstance payload:', payload);
|
|
||||||
|
|
||||||
const res: any = await client.startInstance(activityId, payload);
|
|
||||||
|
|
||||||
const chainSource = res?.activity_chain || schema?.activity_chain || [];
|
|
||||||
if (chainSource && chainSource.length > 0) {
|
|
||||||
const nextAct = chainSource[0];
|
|
||||||
console.log('[LogVisitForm] Activity chain detected, transitioning to DynamicForm:', nextAct);
|
|
||||||
onActivityChange?.(nextAct.activity_name);
|
|
||||||
|
|
||||||
setChainedActivity({
|
|
||||||
activityId: nextAct.activity_uid,
|
|
||||||
instanceId: res?.instance_id,
|
|
||||||
prefillData: { ...valuesRef.current },
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
onSuccess?.();
|
|
||||||
}
|
|
||||||
} catch (err: any) {
|
|
||||||
setSubmitError(err?.message || 'Failed to log visit.');
|
|
||||||
} finally {
|
|
||||||
setSubmitting(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// If activity chain triggered (e.g. Productivity of Visit), render DynamicForm seamlessly
|
|
||||||
if (chainedActivity) {
|
|
||||||
return (
|
|
||||||
<DynamicForm
|
|
||||||
client={client}
|
|
||||||
activityId={chainedActivity.activityId}
|
|
||||||
instanceId={chainedActivity.instanceId}
|
|
||||||
customPrefillData={chainedActivity.prefillData}
|
|
||||||
onSuccess={onSuccess}
|
|
||||||
onCancel={onCancel}
|
|
||||||
onActivityChange={onActivityChange}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (loadingSchema) {
|
|
||||||
return (
|
|
||||||
<div className="flex justify-center items-center py-12">
|
|
||||||
<Spinner size={24} label="Loading Log Visit form..." />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const fields = schema?.fields || [];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<form onSubmit={handleSubmit} className="flex flex-col gap-4">
|
|
||||||
{fields.map(f => {
|
|
||||||
const fieldId = f.id;
|
|
||||||
const lowerId = fieldId.toLowerCase();
|
|
||||||
const lowerName = f.name.toLowerCase();
|
|
||||||
|
|
||||||
const isHidden = schema?.field_defaults?.[fieldId]?.hidden === true || (f.properties as any)?.hidden === true;
|
|
||||||
|
|
||||||
// Skip daily_log and route_code (or hidden fields) from visual rendering
|
|
||||||
if (
|
|
||||||
isHidden ||
|
|
||||||
lowerId === 'daily_log' ||
|
|
||||||
lowerId === 'field_1785225403902' ||
|
|
||||||
lowerId === 'route_code' ||
|
|
||||||
lowerId === 'field_1785311859486' ||
|
|
||||||
lowerName === 'daily log' ||
|
|
||||||
lowerName === 'route code'
|
|
||||||
) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const type = f.data_type;
|
|
||||||
const val = values[fieldId];
|
|
||||||
const isDisabled = schema?.field_defaults?.[fieldId]?.disabled === true || f.properties?.disabled === true;
|
|
||||||
|
|
||||||
if (type === 'wf_lookup' || lowerId === 'select_store' || lowerName.includes('select store')) {
|
|
||||||
return (
|
|
||||||
<div key={fieldId} className="relative">
|
|
||||||
<Select
|
|
||||||
label={f.name}
|
|
||||||
required={f.mandatory}
|
|
||||||
value={(val as string) || ''}
|
|
||||||
options={[{ value: '', label: `Select ${f.name}...` }, ...storeOptions]}
|
|
||||||
onDropdownOpen={handleStoreDropdownOpen}
|
|
||||||
onChange={e => handleStoreSelect(fieldId, e.target.value)}
|
|
||||||
disabled={isDisabled}
|
|
||||||
/>
|
|
||||||
{fetchingStores && (
|
|
||||||
<div className="absolute right-3 top-9">
|
|
||||||
<Spinner size={16} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (type.startsWith('date')) {
|
|
||||||
return (
|
|
||||||
<DateField
|
|
||||||
key={fieldId}
|
|
||||||
label={f.name}
|
|
||||||
required={f.mandatory}
|
|
||||||
disabled={isDisabled}
|
|
||||||
value={(val as string) || ''}
|
|
||||||
onChange={v => {
|
|
||||||
setValues(p => {
|
|
||||||
const next = { ...p, [fieldId]: v, date_of_visit: v };
|
|
||||||
valuesRef.current = next;
|
|
||||||
return next;
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (type.startsWith('time')) {
|
|
||||||
return (
|
|
||||||
<TimeField
|
|
||||||
key={fieldId}
|
|
||||||
label={f.name}
|
|
||||||
required={f.mandatory}
|
|
||||||
disabled={isDisabled}
|
|
||||||
value={(val as string) || ''}
|
|
||||||
onChange={v => {
|
|
||||||
setValues(p => {
|
|
||||||
const next = { ...p, [fieldId]: v, time_of_visit: v };
|
|
||||||
valuesRef.current = next;
|
|
||||||
return next;
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (type === 'image' || type === 'file') {
|
|
||||||
return (
|
|
||||||
<FileInput
|
|
||||||
key={fieldId}
|
|
||||||
label={f.name}
|
|
||||||
type={type}
|
|
||||||
required={f.mandatory}
|
|
||||||
value={val || []}
|
|
||||||
onChange={v => {
|
|
||||||
setValues(p => {
|
|
||||||
const next = { ...p, [fieldId]: v, upload_image: v };
|
|
||||||
valuesRef.current = next;
|
|
||||||
return next;
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TextField
|
|
||||||
key={fieldId}
|
|
||||||
label={f.name}
|
|
||||||
required={f.mandatory}
|
|
||||||
type={type}
|
|
||||||
value={(val as string) || ''}
|
|
||||||
onChange={v => {
|
|
||||||
setValues(p => {
|
|
||||||
const next = { ...p, [fieldId]: v };
|
|
||||||
valuesRef.current = next;
|
|
||||||
return next;
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
|
|
||||||
{submitError && <div className="text-sm text-ruby-600 mt-2">{submitError}</div>}
|
|
||||||
|
|
||||||
<div className="flex items-center justify-end gap-3 mt-4 pt-4 border-t border-border-subtle">
|
|
||||||
{onCancel && (
|
|
||||||
<Button type="button" variant="secondary" onClick={onCancel} disabled={submitting}>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
<Button type="submit" disabled={Boolean(submitting || fetchingDailyLog)}>
|
|
||||||
{submitting ? 'Submitting...' : 'Log Visit'}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -3,13 +3,11 @@ import { Input } from '../../reusable/Input';
|
|||||||
export function DateField({
|
export function DateField({
|
||||||
label,
|
label,
|
||||||
required,
|
required,
|
||||||
disabled,
|
|
||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
}: {
|
}: {
|
||||||
label: string;
|
label: string;
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
disabled?: boolean;
|
|
||||||
value: string;
|
value: string;
|
||||||
onChange: (val: string) => void;
|
onChange: (val: string) => void;
|
||||||
}) {
|
}) {
|
||||||
@ -17,7 +15,6 @@ export function DateField({
|
|||||||
<Input
|
<Input
|
||||||
label={label}
|
label={label}
|
||||||
required={required}
|
required={required}
|
||||||
disabled={disabled}
|
|
||||||
type="date"
|
type="date"
|
||||||
value={value ?? ''}
|
value={value ?? ''}
|
||||||
onChange={(e) => onChange(e.target.value)}
|
onChange={(e) => onChange(e.target.value)}
|
||||||
|
|||||||
@ -78,12 +78,12 @@ export function OrderGrid({
|
|||||||
|
|
||||||
if (cat && prod) {
|
if (cat && prod) {
|
||||||
const key = `${cat}::${prod}`;
|
const key = `${cat}::${prod}`;
|
||||||
if (indexMap.has(key) && currentBags > 0) {
|
if (indexMap.has(key)) {
|
||||||
const targetIdx = indexMap.get(key)!;
|
const targetIdx = indexMap.get(key)!;
|
||||||
const existingRow = merged[targetIdx];
|
const existingRow = merged[targetIdx];
|
||||||
|
|
||||||
const existingBags = Number(existingRow[bagsColId] ?? existingRow.bags ?? existingRow.quantity ?? 0);
|
const existingBags = Number(existingRow[bagsColId] ?? existingRow.bags ?? existingRow.quantity ?? 0);
|
||||||
const addedBags = currentBags;
|
const addedBags = currentBags > 0 ? currentBags : 1;
|
||||||
const newBags = existingBags + addedBags;
|
const newBags = existingBags + addedBags;
|
||||||
const skuVal = Number(existingRow.sku || r.sku || 0);
|
const skuVal = Number(existingRow.sku || r.sku || 0);
|
||||||
|
|
||||||
@ -97,7 +97,7 @@ export function OrderGrid({
|
|||||||
|
|
||||||
merged[targetIdx] = updatedRow;
|
merged[targetIdx] = updatedRow;
|
||||||
continue;
|
continue;
|
||||||
} else if (!indexMap.has(key)) {
|
} else {
|
||||||
indexMap.set(key, merged.length);
|
indexMap.set(key, merged.length);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -206,7 +206,6 @@ export function OrderGrid({
|
|||||||
|
|
||||||
const isCatField = isCategoryColumn(fieldId, colName);
|
const isCatField = isCategoryColumn(fieldId, colName);
|
||||||
const isProdField = isProductColumn(fieldId, colName);
|
const isProdField = isProductColumn(fieldId, colName);
|
||||||
const isBagsField = colDef?.data_type === 'number' || fieldId.includes('bags') || fieldId.includes('quantity') || colName.toLowerCase().includes('bags');
|
|
||||||
|
|
||||||
if (isCatField) {
|
if (isCatField) {
|
||||||
const catVal = String(val ?? '');
|
const catVal = String(val ?? '');
|
||||||
@ -223,10 +222,6 @@ export function OrderGrid({
|
|||||||
row['br'] = '';
|
row['br'] = '';
|
||||||
row['sku_code'] = '';
|
row['sku_code'] = '';
|
||||||
row['skucode'] = '';
|
row['skucode'] = '';
|
||||||
row['bags'] = '';
|
|
||||||
row['quantity'] = '';
|
|
||||||
if (bagsCol) row[bagsCol.id] = '';
|
|
||||||
row['row_kgs'] = 0;
|
|
||||||
} else if (isProdField) {
|
} else if (isProdField) {
|
||||||
const currentCat = String(row['category'] || row['product_category'] || (catCol ? row[catCol.id] : '') || '');
|
const currentCat = String(row['category'] || row['product_category'] || (catCol ? row[catCol.id] : '') || '');
|
||||||
const prodOptions = getProductsForCategory(currentCat);
|
const prodOptions = getProductsForCategory(currentCat);
|
||||||
@ -247,40 +242,25 @@ export function OrderGrid({
|
|||||||
row['productName'] = selectedName;
|
row['productName'] = selectedName;
|
||||||
if (prodNameCol) row[prodNameCol.id] = setVal;
|
if (prodNameCol) row[prodNameCol.id] = setVal;
|
||||||
|
|
||||||
row['sku'] = String(sku ?? '');
|
row['sku'] = sku;
|
||||||
const skuCol = findColumn(columns, 'sku');
|
const skuCol = findColumn(columns, 'sku');
|
||||||
if (skuCol) row[skuCol.id] = String(sku ?? '');
|
if (skuCol) row[skuCol.id] = sku;
|
||||||
|
|
||||||
row['br'] = String(br ?? '');
|
row['br'] = br;
|
||||||
row['br_code'] = String(br ?? '');
|
row['br_code'] = br;
|
||||||
const brCol = findColumn(columns, 'br_code', 'br');
|
const brCol = findColumn(columns, 'br_code', 'br');
|
||||||
if (brCol) row[brCol.id] = String(br ?? '');
|
if (brCol) row[brCol.id] = br;
|
||||||
|
|
||||||
row['skucode'] = String(skucode ?? '');
|
row['skucode'] = skucode;
|
||||||
row['sku_code'] = String(skucode ?? '');
|
row['sku_code'] = skucode;
|
||||||
const skuCodeCol = findColumn(columns, 'sku_code', 'skucode');
|
const skuCodeCol = findColumn(columns, 'sku_code', 'skucode');
|
||||||
if (skuCodeCol) row[skuCodeCol.id] = String(skucode ?? '');
|
if (skuCodeCol) row[skuCodeCol.id] = skucode;
|
||||||
|
|
||||||
if (cat) {
|
if (cat) {
|
||||||
row['category'] = cat;
|
row['category'] = cat;
|
||||||
row['product_category'] = cat;
|
row['product_category'] = cat;
|
||||||
if (catCol) row[catCol.id] = cat;
|
if (catCol) row[catCol.id] = cat;
|
||||||
}
|
}
|
||||||
|
|
||||||
const bagsVal = Number(row.bags ?? row.quantity ?? (bagsCol ? row[bagsCol.id] : 0)) || 0;
|
|
||||||
row['row_kgs'] = Number(sku || 0) * bagsVal;
|
|
||||||
} else if (isBagsField) {
|
|
||||||
const numericVal = val !== '' && val !== null && val !== undefined ? Number(val) : 0;
|
|
||||||
const isNumValid = !isNaN(numericVal);
|
|
||||||
const valToSave = isNumValid ? numericVal : 0;
|
|
||||||
|
|
||||||
row[fieldId] = valToSave;
|
|
||||||
row['bags'] = valToSave;
|
|
||||||
row['quantity'] = valToSave;
|
|
||||||
if (bagsCol) row[bagsCol.id] = valToSave;
|
|
||||||
|
|
||||||
const skuVal = Number(row.sku || 0);
|
|
||||||
row['row_kgs'] = skuVal * valToSave;
|
|
||||||
} else {
|
} else {
|
||||||
row[fieldId] = val;
|
row[fieldId] = val;
|
||||||
}
|
}
|
||||||
@ -298,7 +278,7 @@ export function OrderGrid({
|
|||||||
});
|
});
|
||||||
|
|
||||||
onChange(cleanRows);
|
onChange(cleanRows);
|
||||||
}, [rows, columns, visibleColumns, prodNameCol, catCol, bagsCol, getProductsForCategory, onChange]);
|
}, [rows, columns, visibleColumns, prodNameCol, catCol, getProductsForCategory, onChange]);
|
||||||
|
|
||||||
const { calculatedBags, calculatedKgs } = useMemo(() => {
|
const { calculatedBags, calculatedKgs } = useMemo(() => {
|
||||||
let bagsAcc = 0;
|
let bagsAcc = 0;
|
||||||
|
|||||||
@ -66,12 +66,12 @@ export function StorePotentialGrid({
|
|||||||
const currentBags = Number(r[bagsColId] ?? r.bags ?? r.quantity ?? 0);
|
const currentBags = Number(r[bagsColId] ?? r.bags ?? r.quantity ?? 0);
|
||||||
|
|
||||||
if (cat) {
|
if (cat) {
|
||||||
if (indexMap.has(cat) && currentBags > 0) {
|
if (indexMap.has(cat)) {
|
||||||
const targetIdx = indexMap.get(cat)!;
|
const targetIdx = indexMap.get(cat)!;
|
||||||
const existingRow = merged[targetIdx];
|
const existingRow = merged[targetIdx];
|
||||||
|
|
||||||
const existingBags = Number(existingRow[bagsColId] ?? existingRow.bags ?? existingRow.quantity ?? 0);
|
const existingBags = Number(existingRow[bagsColId] ?? existingRow.bags ?? existingRow.quantity ?? 0);
|
||||||
const addedBags = currentBags;
|
const addedBags = currentBags > 0 ? currentBags : 1;
|
||||||
const newBags = existingBags + addedBags;
|
const newBags = existingBags + addedBags;
|
||||||
|
|
||||||
const updatedRow: Record<string, unknown> = {
|
const updatedRow: Record<string, unknown> = {
|
||||||
@ -83,7 +83,7 @@ export function StorePotentialGrid({
|
|||||||
|
|
||||||
merged[targetIdx] = updatedRow;
|
merged[targetIdx] = updatedRow;
|
||||||
continue;
|
continue;
|
||||||
} else if (!indexMap.has(cat)) {
|
} else {
|
||||||
indexMap.set(cat, merged.length);
|
indexMap.set(cat, merged.length);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -124,27 +124,19 @@ export function StorePotentialGrid({
|
|||||||
const colDef = columns.find((c) => c.id === fieldId);
|
const colDef = columns.find((c) => c.id === fieldId);
|
||||||
const colName = colDef?.name || '';
|
const colName = colDef?.name || '';
|
||||||
const isCatField = isCategoryColumn(fieldId, colName);
|
const isCatField = isCategoryColumn(fieldId, colName);
|
||||||
const isBagsField = colDef?.data_type === 'number' || fieldId.includes('bags') || fieldId.includes('quantity') || colName.toLowerCase().includes('bags');
|
|
||||||
|
|
||||||
if (isCatField) {
|
if (isCatField) {
|
||||||
const catVal = String(val ?? '');
|
const catVal = String(val ?? '');
|
||||||
row[fieldId] = catVal;
|
row[fieldId] = catVal;
|
||||||
row['category'] = catVal;
|
row['category'] = catVal;
|
||||||
row['product_category'] = catVal;
|
row['product_category'] = catVal;
|
||||||
} else if (isBagsField) {
|
|
||||||
const numericVal = val !== '' && val !== null && val !== undefined ? Number(val) : 0;
|
|
||||||
const valToSave = !isNaN(numericVal) ? numericVal : 0;
|
|
||||||
row[fieldId] = valToSave;
|
|
||||||
row['bags'] = valToSave;
|
|
||||||
row['quantity'] = valToSave;
|
|
||||||
if (bagsCol) row[bagsCol.id] = valToSave;
|
|
||||||
} else {
|
} else {
|
||||||
row[fieldId] = val;
|
row[fieldId] = val;
|
||||||
}
|
}
|
||||||
|
|
||||||
next[rowIdx] = row;
|
next[rowIdx] = row;
|
||||||
onChange(next);
|
onChange(next);
|
||||||
}, [rows, columns, bagsCol, onChange]);
|
}, [rows, columns, onChange]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-w-full bg-[var(--tiles-card-bg)] rounded-xl border border-border-default p-6 space-y-6 shadow-sm font-sans">
|
<div className="max-w-full bg-[var(--tiles-card-bg)] rounded-xl border border-border-default p-6 space-y-6 shadow-sm font-sans">
|
||||||
|
|||||||
@ -3,13 +3,11 @@ import { Input } from '../../reusable/Input';
|
|||||||
export function TimeField({
|
export function TimeField({
|
||||||
label,
|
label,
|
||||||
required,
|
required,
|
||||||
disabled,
|
|
||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
}: {
|
}: {
|
||||||
label: string;
|
label: string;
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
disabled?: boolean;
|
|
||||||
value: string;
|
value: string;
|
||||||
onChange: (val: string) => void;
|
onChange: (val: string) => void;
|
||||||
}) {
|
}) {
|
||||||
@ -17,7 +15,6 @@ export function TimeField({
|
|||||||
<Input
|
<Input
|
||||||
label={label}
|
label={label}
|
||||||
required={required}
|
required={required}
|
||||||
disabled={disabled}
|
|
||||||
type="time"
|
type="time"
|
||||||
value={value ?? ''}
|
value={value ?? ''}
|
||||||
onChange={(e) => onChange(e.target.value)}
|
onChange={(e) => onChange(e.target.value)}
|
||||||
|
|||||||
@ -1,2 +0,0 @@
|
|||||||
export * from './DynamicForm';
|
|
||||||
export * from './LogVisitForm';
|
|
||||||
@ -1,4 +1,4 @@
|
|||||||
import { useState, useRef, useEffect, useLayoutEffect, useCallback, type SelectHTMLAttributes } from 'react';
|
import { useState, useRef, useEffect, useCallback, type SelectHTMLAttributes } from 'react';
|
||||||
import { createPortal } from 'react-dom';
|
import { createPortal } from 'react-dom';
|
||||||
import { ChevronDown, Search, X, Check } from 'lucide-react';
|
import { ChevronDown, Search, X, Check } from 'lucide-react';
|
||||||
import { cn } from '../../lib/cn';
|
import { cn } from '../../lib/cn';
|
||||||
@ -20,7 +20,6 @@ export interface SelectProps extends SelectHTMLAttributes<HTMLSelectElement> {
|
|||||||
className?: string;
|
className?: string;
|
||||||
/** Disable search header filter if set to false */
|
/** Disable search header filter if set to false */
|
||||||
searchable?: boolean;
|
searchable?: boolean;
|
||||||
onDropdownOpen?: () => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Custom searchable select component using React Portal to prevent container clipping. */
|
/** Custom searchable select component using React Portal to prevent container clipping. */
|
||||||
@ -34,7 +33,6 @@ export function Select({
|
|||||||
disabled,
|
disabled,
|
||||||
placeholder,
|
placeholder,
|
||||||
searchable = true,
|
searchable = true,
|
||||||
onDropdownOpen,
|
|
||||||
...rest
|
...rest
|
||||||
}: SelectProps) {
|
}: SelectProps) {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
@ -63,17 +61,10 @@ export function Select({
|
|||||||
const spaceBelow = window.innerHeight - rect.bottom;
|
const spaceBelow = window.innerHeight - rect.bottom;
|
||||||
const openUpwards = spaceBelow < dropdownHeight && rect.top > dropdownHeight;
|
const openUpwards = spaceBelow < dropdownHeight && rect.top > dropdownHeight;
|
||||||
|
|
||||||
const calculatedWidth = Math.max(rect.width, 280);
|
|
||||||
let leftPos = rect.left;
|
|
||||||
if (leftPos + calculatedWidth > window.innerWidth - 12) {
|
|
||||||
leftPos = Math.max(12, window.innerWidth - calculatedWidth - 12);
|
|
||||||
}
|
|
||||||
|
|
||||||
setDropdownStyle({
|
setDropdownStyle({
|
||||||
position: 'fixed',
|
position: 'fixed',
|
||||||
left: `${leftPos}px`,
|
left: `${rect.left}px`,
|
||||||
width: `${calculatedWidth}px`,
|
width: `${rect.width}px`,
|
||||||
maxWidth: 'calc(100vw - 24px)',
|
|
||||||
zIndex: 999999,
|
zIndex: 999999,
|
||||||
...(openUpwards
|
...(openUpwards
|
||||||
? { bottom: `${window.innerHeight - rect.top + 4}px` }
|
? { bottom: `${window.innerHeight - rect.top + 4}px` }
|
||||||
@ -82,7 +73,7 @@ export function Select({
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useEffect(() => {
|
||||||
if (isOpen) {
|
if (isOpen) {
|
||||||
updatePosition();
|
updatePosition();
|
||||||
const handleScrollOrResize = () => updatePosition();
|
const handleScrollOrResize = () => updatePosition();
|
||||||
@ -92,8 +83,6 @@ export function Select({
|
|||||||
window.removeEventListener('resize', handleScrollOrResize);
|
window.removeEventListener('resize', handleScrollOrResize);
|
||||||
window.removeEventListener('scroll', handleScrollOrResize, true);
|
window.removeEventListener('scroll', handleScrollOrResize, true);
|
||||||
};
|
};
|
||||||
} else {
|
|
||||||
setDropdownStyle({});
|
|
||||||
}
|
}
|
||||||
}, [isOpen, updatePosition]);
|
}, [isOpen, updatePosition]);
|
||||||
|
|
||||||
@ -111,26 +100,7 @@ export function Select({
|
|||||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleOpenToggle = (e: React.MouseEvent) => {
|
const handleSelect = (val: string) => {
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
if (!disabled) {
|
|
||||||
const next = !isOpen;
|
|
||||||
if (next) {
|
|
||||||
updatePosition();
|
|
||||||
onDropdownOpen?.();
|
|
||||||
} else {
|
|
||||||
setDropdownStyle({});
|
|
||||||
}
|
|
||||||
setIsOpen(next);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSelect = (val: string, e?: React.MouseEvent) => {
|
|
||||||
if (e) {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
}
|
|
||||||
if (rest.onChange) {
|
if (rest.onChange) {
|
||||||
const syntheticEvent = {
|
const syntheticEvent = {
|
||||||
target: { value: val, name: rest.name, id: rest.id },
|
target: { value: val, name: rest.name, id: rest.id },
|
||||||
@ -153,7 +123,7 @@ export function Select({
|
|||||||
|
|
||||||
{/* Trigger Box */}
|
{/* Trigger Box */}
|
||||||
<div
|
<div
|
||||||
onClick={handleOpenToggle}
|
onClick={() => !disabled && setIsOpen(!isOpen)}
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative bg-card rounded-md h-[42px] border px-3 flex items-center justify-between cursor-pointer transition-all duration-150 select-none",
|
"relative bg-card rounded-md h-[42px] border px-3 flex items-center justify-between cursor-pointer transition-all duration-150 select-none",
|
||||||
disabled && "opacity-60 cursor-not-allowed bg-slate-50",
|
disabled && "opacity-60 cursor-not-allowed bg-slate-50",
|
||||||
@ -167,7 +137,7 @@ export function Select({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Portaled Dropdown Menu Overlay */}
|
{/* Portaled Dropdown Menu Overlay */}
|
||||||
{isOpen && !disabled && Boolean(dropdownStyle.position) && createPortal(
|
{isOpen && !disabled && createPortal(
|
||||||
<div
|
<div
|
||||||
ref={dropdownRef}
|
ref={dropdownRef}
|
||||||
style={dropdownStyle}
|
style={dropdownStyle}
|
||||||
@ -188,11 +158,7 @@ export function Select({
|
|||||||
{searchQuery && (
|
{searchQuery && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={(e) => {
|
onClick={() => setSearchQuery('')}
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
setSearchQuery('');
|
|
||||||
}}
|
|
||||||
className="text-muted hover:text-strong p-0.5 rounded cursor-pointer"
|
className="text-muted hover:text-strong p-0.5 rounded cursor-pointer"
|
||||||
>
|
>
|
||||||
<X size={12} />
|
<X size={12} />
|
||||||
@ -208,14 +174,13 @@ export function Select({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={opt.value}
|
key={opt.value}
|
||||||
onClick={(e) => handleSelect(opt.value, e)}
|
onClick={() => handleSelect(opt.value)}
|
||||||
title={opt.label}
|
|
||||||
className={cn(
|
className={cn(
|
||||||
"px-3 py-2 text-sm flex items-center justify-between cursor-pointer transition-colors",
|
"px-3 py-2 text-sm flex items-center justify-between cursor-pointer transition-colors",
|
||||||
isSelected ? "bg-blue-50/50 text-blue-700 font-semibold" : "hover:bg-black/5 text-strong"
|
isSelected ? "bg-blue-50/50 text-blue-700 font-semibold" : "hover:bg-black/5 text-strong"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<span className="flex-1 break-words text-xs sm:text-sm leading-normal mr-2">{opt.label}</span>
|
<span className="truncate">{opt.label}</span>
|
||||||
{isSelected && <Check size={14} className="text-blue-600 shrink-0 ml-2" />}
|
{isSelected && <Check size={14} className="text-blue-600 shrink-0 ml-2" />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -298,21 +263,13 @@ export function MultiSelect({
|
|||||||
|
|
||||||
const currentValues = Array.isArray(value) ? value : [];
|
const currentValues = Array.isArray(value) ? value : [];
|
||||||
|
|
||||||
const handleOpenToggle = (e?: React.MouseEvent) => {
|
const handleOpenToggle = () => {
|
||||||
if (e) {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
}
|
|
||||||
if (!disabled) {
|
if (!disabled) {
|
||||||
const next = !isOpen;
|
if (!isOpen) {
|
||||||
if (next) {
|
|
||||||
setDraftValues(currentValues);
|
setDraftValues(currentValues);
|
||||||
setSearchQuery('');
|
setSearchQuery('');
|
||||||
updatePosition();
|
|
||||||
} else {
|
|
||||||
setDropdownStyle({});
|
|
||||||
}
|
}
|
||||||
setIsOpen(next);
|
setIsOpen(!isOpen);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -327,17 +284,10 @@ export function MultiSelect({
|
|||||||
const spaceBelow = window.innerHeight - rect.bottom;
|
const spaceBelow = window.innerHeight - rect.bottom;
|
||||||
const openUpwards = spaceBelow < dropdownHeight && rect.top > dropdownHeight;
|
const openUpwards = spaceBelow < dropdownHeight && rect.top > dropdownHeight;
|
||||||
|
|
||||||
const calculatedWidth = Math.max(rect.width, 280);
|
|
||||||
let leftPos = rect.left;
|
|
||||||
if (leftPos + calculatedWidth > window.innerWidth - 12) {
|
|
||||||
leftPos = Math.max(12, window.innerWidth - calculatedWidth - 12);
|
|
||||||
}
|
|
||||||
|
|
||||||
setDropdownStyle({
|
setDropdownStyle({
|
||||||
position: 'fixed',
|
position: 'fixed',
|
||||||
left: `${leftPos}px`,
|
left: `${rect.left}px`,
|
||||||
width: `${calculatedWidth}px`,
|
width: `${rect.width}px`,
|
||||||
maxWidth: 'calc(100vw - 24px)',
|
|
||||||
zIndex: 999999,
|
zIndex: 999999,
|
||||||
...(openUpwards
|
...(openUpwards
|
||||||
? { bottom: `${window.innerHeight - rect.top + 4}px` }
|
? { bottom: `${window.innerHeight - rect.top + 4}px` }
|
||||||
@ -346,7 +296,7 @@ export function MultiSelect({
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useEffect(() => {
|
||||||
if (isOpen) {
|
if (isOpen) {
|
||||||
updatePosition();
|
updatePosition();
|
||||||
const handleScrollOrResize = () => updatePosition();
|
const handleScrollOrResize = () => updatePosition();
|
||||||
@ -356,8 +306,6 @@ export function MultiSelect({
|
|||||||
window.removeEventListener('resize', handleScrollOrResize);
|
window.removeEventListener('resize', handleScrollOrResize);
|
||||||
window.removeEventListener('scroll', handleScrollOrResize, true);
|
window.removeEventListener('scroll', handleScrollOrResize, true);
|
||||||
};
|
};
|
||||||
} else {
|
|
||||||
setDropdownStyle({});
|
|
||||||
}
|
}
|
||||||
}, [isOpen, updatePosition]);
|
}, [isOpen, updatePosition]);
|
||||||
|
|
||||||
@ -375,30 +323,18 @@ export function MultiSelect({
|
|||||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const toggleOption = (val: string, e?: React.MouseEvent) => {
|
const toggleOption = (val: string) => {
|
||||||
if (e) {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
}
|
|
||||||
setDraftValues((prev) =>
|
setDraftValues((prev) =>
|
||||||
prev.includes(val) ? prev.filter((v) => v !== val) : [...prev, val]
|
prev.includes(val) ? prev.filter((v) => v !== val) : [...prev, val]
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleApply = (e?: React.MouseEvent) => {
|
const handleApply = () => {
|
||||||
if (e) {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
}
|
|
||||||
onChange?.(draftValues);
|
onChange?.(draftValues);
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClear = (e?: React.MouseEvent) => {
|
const handleClear = () => {
|
||||||
if (e) {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
}
|
|
||||||
setDraftValues([]);
|
setDraftValues([]);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -438,7 +374,7 @@ export function MultiSelect({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Portaled Dropdown Menu Overlay */}
|
{/* Portaled Dropdown Menu Overlay */}
|
||||||
{isOpen && !disabled && Boolean(dropdownStyle.position) && createPortal(
|
{isOpen && !disabled && createPortal(
|
||||||
<div
|
<div
|
||||||
ref={dropdownRef}
|
ref={dropdownRef}
|
||||||
style={dropdownStyle}
|
style={dropdownStyle}
|
||||||
@ -459,11 +395,7 @@ export function MultiSelect({
|
|||||||
{searchQuery && (
|
{searchQuery && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={(e) => {
|
onClick={() => setSearchQuery('')}
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
setSearchQuery('');
|
|
||||||
}}
|
|
||||||
className="text-muted hover:text-strong p-0.5 rounded cursor-pointer"
|
className="text-muted hover:text-strong p-0.5 rounded cursor-pointer"
|
||||||
>
|
>
|
||||||
<X size={12} />
|
<X size={12} />
|
||||||
@ -479,10 +411,9 @@ export function MultiSelect({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={opt.value}
|
key={opt.value}
|
||||||
onClick={(e) => toggleOption(opt.value, e)}
|
onClick={() => toggleOption(opt.value)}
|
||||||
title={opt.label}
|
|
||||||
className={cn(
|
className={cn(
|
||||||
"px-3 py-2 text-xs flex items-start gap-2.5 cursor-pointer transition-colors select-none",
|
"px-3 py-2 text-xs flex items-center gap-2.5 cursor-pointer transition-colors select-none",
|
||||||
isSelected ? "bg-blue-50/70 text-blue-800 font-semibold" : "hover:bg-black/5 text-strong"
|
isSelected ? "bg-blue-50/70 text-blue-800 font-semibold" : "hover:bg-black/5 text-strong"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@ -490,9 +421,9 @@ export function MultiSelect({
|
|||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={isSelected}
|
checked={isSelected}
|
||||||
onChange={() => {}}
|
onChange={() => {}}
|
||||||
className="rounded border-slate-300 text-blue-600 focus:ring-blue-500 pointer-events-none h-3.5 w-3.5 mt-0.5 shrink-0"
|
className="rounded border-slate-300 text-blue-600 focus:ring-blue-500 pointer-events-none h-3.5 w-3.5"
|
||||||
/>
|
/>
|
||||||
<span className="flex-1 break-words text-xs leading-normal">{opt.label}</span>
|
<span className="truncate flex-1">{opt.label}</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
|
|||||||
@ -10,7 +10,7 @@ import { Spinner } from '../reusable/Spinner';
|
|||||||
import { EmptyState } from '../reusable/EmptyState';
|
import { EmptyState } from '../reusable/EmptyState';
|
||||||
import { StatsTiles } from '../reusable/StatsTiles';
|
import { StatsTiles } from '../reusable/StatsTiles';
|
||||||
import { AnalyticsChart } from '../reusable/AnalyticsChart';
|
import { AnalyticsChart } from '../reusable/AnalyticsChart';
|
||||||
import { MultiSelect } from '../reusable/Select';
|
import { Select, MultiSelect } from '../reusable/Select';
|
||||||
|
|
||||||
export interface RecordViewProps {
|
export interface RecordViewProps {
|
||||||
/** Workflow-bound client (see api/clients.ts). */
|
/** Workflow-bound client (see api/clients.ts). */
|
||||||
|
|||||||
@ -1,24 +0,0 @@
|
|||||||
import { userClient } from '../../api/clients';
|
|
||||||
import { USER } from '../../api/config';
|
|
||||||
import { RecordView } from './RecordView';
|
|
||||||
import type { WiredRecordViewProps } from './OrdersView';
|
|
||||||
|
|
||||||
/** Users / Sales Officers record view (User workflow). */
|
|
||||||
export function UsersView({ onRowClick, pageSize, headerActions, rowActions, refreshKey, initialFilters, presetAlias }: WiredRecordViewProps) {
|
|
||||||
return (
|
|
||||||
<RecordView
|
|
||||||
client={userClient}
|
|
||||||
rvUid={USER.recordViews.SALES_OFFICERS}
|
|
||||||
title="Sales Officers"
|
|
||||||
onRowClick={onRowClick}
|
|
||||||
pageSize={pageSize}
|
|
||||||
headerActions={headerActions}
|
|
||||||
rowActions={rowActions}
|
|
||||||
refreshKey={refreshKey}
|
|
||||||
sortBy="instance_id"
|
|
||||||
sortDir="desc"
|
|
||||||
initialFilters={initialFilters}
|
|
||||||
presetAlias={presetAlias}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -5,4 +5,3 @@ export type { WiredRecordViewProps } from './OrdersView';
|
|||||||
export { CallsView } from './CallsView';
|
export { CallsView } from './CallsView';
|
||||||
export { StoresView } from './StoresView';
|
export { StoresView } from './StoresView';
|
||||||
export { DailyLogsView } from './DailyLogsView';
|
export { DailyLogsView } from './DailyLogsView';
|
||||||
export { UsersView } from './UsersView';
|
|
||||||
|
|||||||
@ -1,113 +0,0 @@
|
|||||||
import { OrdersPage } from './screens/OrdersPage'
|
|
||||||
import { MyOrdersPage } from './screens/MyOrdersPage'
|
|
||||||
import { CallsPage } from './screens/CallsPage'
|
|
||||||
import { MyCallsPage } from './screens/MyCallsPage'
|
|
||||||
import { StoresPage } from './screens/StoresPage'
|
|
||||||
import { DailyLogsPage } from './screens/DailyLogsPage'
|
|
||||||
import { MyDailyLogsPage } from './screens/MyDailyLogsPage'
|
|
||||||
import { DailySalesReportPage } from './screens/DailySalesReportPage'
|
|
||||||
import { ReportPage } from './screens/ReportPage'
|
|
||||||
import { DatasetsPage } from './screens/admin/DatasetsPage'
|
|
||||||
import { UsersPage } from './screens/admin/UsersPage'
|
|
||||||
|
|
||||||
export const routeConfig = [
|
|
||||||
// Sales Officer
|
|
||||||
{
|
|
||||||
path: "/my-orders",
|
|
||||||
element: <MyOrdersPage />,
|
|
||||||
roles: ["Sales Officer"],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "/my-orders/:instanceId",
|
|
||||||
element: <MyOrdersPage />,
|
|
||||||
roles: ["Sales Officer"],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "/my-calls",
|
|
||||||
element: <MyCallsPage />,
|
|
||||||
roles: ["Sales Officer"],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "/my-calls/:instanceId",
|
|
||||||
element: <MyCallsPage />,
|
|
||||||
roles: ["Sales Officer"],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "/my-daily",
|
|
||||||
element: <MyDailyLogsPage />,
|
|
||||||
roles: ["Sales Officer"],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "/my-daily/:instanceId",
|
|
||||||
element: <MyDailyLogsPage />,
|
|
||||||
roles: ["Sales Officer"],
|
|
||||||
},
|
|
||||||
|
|
||||||
// Manager & Admin
|
|
||||||
{
|
|
||||||
path: "/orders",
|
|
||||||
element: <OrdersPage />,
|
|
||||||
roles: ["Manager", "Admin"],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "/orders/:instanceId",
|
|
||||||
element: <OrdersPage />,
|
|
||||||
roles: ["Manager", "Admin"],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "/calls",
|
|
||||||
element: <CallsPage />,
|
|
||||||
roles: ["Manager", "Admin"],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "/calls/:instanceId",
|
|
||||||
element: <CallsPage />,
|
|
||||||
roles: ["Manager", "Admin"],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "/daily",
|
|
||||||
element: <DailyLogsPage />,
|
|
||||||
roles: ["Manager", "Admin"],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "/daily/:instanceId",
|
|
||||||
element: <DailyLogsPage />,
|
|
||||||
roles: ["Manager", "Admin"],
|
|
||||||
},
|
|
||||||
|
|
||||||
// Everyone
|
|
||||||
{
|
|
||||||
path: "/stores",
|
|
||||||
element: <StoresPage />,
|
|
||||||
roles: ["Sales Officer", "Manager", "Admin"],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "/stores/:instanceId",
|
|
||||||
element: <StoresPage />,
|
|
||||||
roles: ["Sales Officer", "Manager", "Admin"],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "/reports/:reportType",
|
|
||||||
element: <ReportPage />,
|
|
||||||
roles: ["Manager", "Admin"],
|
|
||||||
},
|
|
||||||
|
|
||||||
// Manager + Admin
|
|
||||||
{
|
|
||||||
path: "/sales-report",
|
|
||||||
element: <DailySalesReportPage />,
|
|
||||||
roles: ["Manager", "Admin", "Sales Officer"],
|
|
||||||
},
|
|
||||||
|
|
||||||
// Admin Panel
|
|
||||||
{
|
|
||||||
path: "/admin/datasets",
|
|
||||||
element: <DatasetsPage />,
|
|
||||||
adminOnly: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "/admin/users",
|
|
||||||
element: <UsersPage />,
|
|
||||||
adminOnly: true,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
@ -5,7 +5,7 @@ import { CallDetail } from '../components/dv';
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { Button } from '../components/buttons/Button';
|
import { Button } from '../components/buttons/Button';
|
||||||
import { Plus } from 'lucide-react';
|
import { Plus } from 'lucide-react';
|
||||||
import { DynamicForm, LogVisitForm } from '../components/forms';
|
import { DynamicForm } from '../components/forms/DynamicForm';
|
||||||
import { ORDER_BOOKING } from '../api/config';
|
import { ORDER_BOOKING } from '../api/config';
|
||||||
import { orderBookingClient } from '../api/clients';
|
import { orderBookingClient } from '../api/clients';
|
||||||
|
|
||||||
@ -185,8 +185,10 @@ export function CallsPage() {
|
|||||||
title={createTitle}
|
title={createTitle}
|
||||||
width="md"
|
width="md"
|
||||||
>
|
>
|
||||||
<LogVisitForm
|
<DynamicForm
|
||||||
client={orderBookingClient}
|
client={orderBookingClient}
|
||||||
|
activityId={ORDER_BOOKING.activities.LOG_VISIT.uid}
|
||||||
|
initialActivityName="Log Visit"
|
||||||
onSuccess={() => {
|
onSuccess={() => {
|
||||||
setIsCreating(false);
|
setIsCreating(false);
|
||||||
setRefreshKey(k => k + 1);
|
setRefreshKey(k => k + 1);
|
||||||
|
|||||||
@ -1,30 +1,17 @@
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { NavLink, Navigate, Outlet, useNavigate, useLocation } from 'react-router-dom';
|
import { NavLink, Navigate, Outlet, useNavigate, useLocation } from 'react-router-dom';
|
||||||
import { LogOut, ChevronDown, Database, Users } from 'lucide-react';
|
import { LogOut, ChevronDown, Database } from 'lucide-react';
|
||||||
import { cn } from '../lib/cn';
|
import { cn } from '../lib/cn';
|
||||||
import { useAuth } from '../auth/context';
|
import { useAuth } from '../auth/context';
|
||||||
import { onAuthErrorAll, orderBookingClient } from '../api/clients';
|
import { onAuthErrorAll, orderBookingClient } from '../api/clients';
|
||||||
|
|
||||||
|
|
||||||
|
import { SCREENS } from './tabs';
|
||||||
import { REPORT_MAP } from './ReportPage';
|
import { REPORT_MAP } from './ReportPage';
|
||||||
|
|
||||||
import { routeConfig } from '../routesConfig';
|
|
||||||
|
|
||||||
const PATH_LABELS: Record<string, string> = {
|
|
||||||
"/my-orders": "My Orders",
|
|
||||||
"/my-calls": "My Calls",
|
|
||||||
"/my-daily": "My Daily Logs",
|
|
||||||
"/orders": "Orders",
|
|
||||||
"/calls": "Calls",
|
|
||||||
"/daily": "Daily Logs",
|
|
||||||
"/stores": "Stores",
|
|
||||||
"/sales-report": "DSR",
|
|
||||||
};
|
|
||||||
|
|
||||||
/** Auth-guarded shell: navy top bar + tab nav + routed <Outlet>. */
|
/** Auth-guarded shell: navy top bar + tab nav + routed <Outlet>. */
|
||||||
export function ConsoleLayout() {
|
export function ConsoleLayout() {
|
||||||
const { authed, logout, isAdmin, roles: userRoles } = useAuth();
|
const { authed, logout } = useAuth();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const user = orderBookingClient.currentUser();
|
const user = orderBookingClient.currentUser();
|
||||||
@ -41,35 +28,18 @@ export function ConsoleLayout() {
|
|||||||
|
|
||||||
if (!authed) return <Navigate to="/login" replace />;
|
if (!authed) return <Navigate to="/login" replace />;
|
||||||
|
|
||||||
const reportRoute = routeConfig.find(r => r.path === '/reports/:reportType');
|
|
||||||
const canSeeReports = reportRoute && (
|
|
||||||
(reportRoute.adminOnly && isAdmin) ||
|
|
||||||
(reportRoute.roles && reportRoute.roles.some(r => userRoles.includes(r))) ||
|
|
||||||
(!reportRoute.adminOnly && !reportRoute.roles) // open to all
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-screen bg-[var(--secondary-color)] flex flex-col">
|
<div className="h-screen bg-[var(--secondary-color)] flex flex-col">
|
||||||
<header className="sticky top-0 z-50 shrink-0 flex items-center justify-between gap-6 px-6 h-[56px] shadow-sm" style={{ background: 'var(--nav-bg-color)' }}>
|
<header className="sticky top-0 z-10 shrink-0 flex items-center justify-between gap-6 px-6 h-[56px] shadow-sm" style={{ background: 'var(--nav-bg-color)' }}>
|
||||||
<div className="flex items-center min-w-max w-48">
|
<div className="flex items-center min-w-max w-48">
|
||||||
<span className="text-lg font-bold text-white tracking-wide leading-none">Krishna Sales</span>
|
<span className="text-lg font-bold text-white tracking-wide leading-none">Krishna Sales</span>
|
||||||
</div>
|
</div>
|
||||||
<nav className="flex items-center justify-end gap-2 h-full flex-1 mr-4">
|
<nav className="flex items-center justify-end gap-2 h-full flex-1 mr-4">
|
||||||
{routeConfig.filter(route => {
|
{SCREENS.map((t) => {
|
||||||
if (route.path.includes('/:')) return false; // Skip detail pages
|
|
||||||
if (!PATH_LABELS[route.path]) return false; // Skip unmapped routes
|
|
||||||
|
|
||||||
if (route.adminOnly && !isAdmin) return false;
|
|
||||||
if (!route.adminOnly && route.roles) {
|
|
||||||
return route.roles.some(r => userRoles.includes(r));
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}).map((route) => {
|
|
||||||
const key = route.path.replace('/', '');
|
|
||||||
return (
|
return (
|
||||||
<NavLink
|
<NavLink
|
||||||
key={key}
|
key={t.key}
|
||||||
to={route.path}
|
to={`/${t.key}`}
|
||||||
className={({ isActive }) =>
|
className={({ isActive }) =>
|
||||||
cn(
|
cn(
|
||||||
'flex items-center gap-1.5 no-underline font-sans text-[13px] font-semibold px-3 py-1.5 rounded-md transition-all duration-150',
|
'flex items-center gap-1.5 no-underline font-sans text-[13px] font-semibold px-3 py-1.5 rounded-md transition-all duration-150',
|
||||||
@ -77,39 +47,38 @@ export function ConsoleLayout() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{PATH_LABELS[route.path]}
|
{t.label}
|
||||||
</NavLink>
|
</NavLink>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
||||||
{canSeeReports && (
|
<div className="relative group flex items-center h-full">
|
||||||
<div className="relative group flex items-center h-full">
|
<button className={cn(
|
||||||
<button className={cn(
|
"flex items-center gap-1.5 no-underline font-sans text-[13px] font-semibold px-3 py-1.5 rounded-md transition-all duration-150 cursor-pointer",
|
||||||
"flex items-center gap-1.5 no-underline font-sans text-[13px] font-semibold px-3 py-1.5 rounded-md transition-all duration-150 cursor-pointer",
|
isReportsActive ? "bg-white/20 text-white" : "text-white/90 hover:bg-white/10 hover:text-white"
|
||||||
isReportsActive ? "bg-white/20 text-white" : "text-white/90 hover:bg-white/10 hover:text-white"
|
)}>
|
||||||
)}>
|
Reports
|
||||||
Reports
|
<ChevronDown size={14} className="ml-0.5 opacity-70" />
|
||||||
<ChevronDown size={14} className="ml-0.5 opacity-70" />
|
</button>
|
||||||
</button>
|
|
||||||
|
|
||||||
<div className="absolute top-[80%] right-0 mt-1 w-56 bg-white rounded-md shadow-lg py-1 border border-gray-200 hidden group-hover:block z-50">
|
<div className="absolute top-[80%] right-0 mt-1 w-56 bg-white rounded-md shadow-lg py-1 border border-gray-200 hidden group-hover:block z-50">
|
||||||
{Object.entries(REPORT_MAP).map(([key, report]) => (
|
{Object.entries(REPORT_MAP).map(([key, report]) => (
|
||||||
<NavLink
|
<NavLink
|
||||||
key={key}
|
key={key}
|
||||||
to={`/reports/${key}`}
|
to={`/reports/${key}`}
|
||||||
className={({ isActive }) =>
|
className={({ isActive }) =>
|
||||||
cn(
|
cn(
|
||||||
"block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100",
|
"block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100",
|
||||||
isActive && "bg-gray-100 font-semibold"
|
isActive && "bg-gray-100 font-semibold"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{report.title}
|
{report.title}
|
||||||
</NavLink>
|
</NavLink>
|
||||||
))}
|
))}
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
|
|
||||||
</nav>
|
</nav>
|
||||||
<div className="relative group flex items-center justify-end shrink-0 w-48 h-full py-2">
|
<div className="relative group flex items-center justify-end shrink-0 w-48 h-full py-2">
|
||||||
<button
|
<button
|
||||||
@ -123,24 +92,15 @@ export function ConsoleLayout() {
|
|||||||
<p className="text-sm font-bold text-gray-900 truncate">{user?.name || 'User'}</p>
|
<p className="text-sm font-bold text-gray-900 truncate">{user?.name || 'User'}</p>
|
||||||
<p className="text-xs text-gray-500 truncate mt-0.5">{user?.email || 'user@example.com'}</p>
|
<p className="text-xs text-gray-500 truncate mt-0.5">{user?.email || 'user@example.com'}</p>
|
||||||
</div>
|
</div>
|
||||||
{isAdmin && (
|
<div className="px-2 pb-1 border-b border-gray-100 mb-1">
|
||||||
<div className="px-2 pb-1 border-b border-gray-100 mb-1">
|
<NavLink
|
||||||
<NavLink
|
to="/admin/datasets"
|
||||||
to="/admin/datasets"
|
className={({ isActive }) => cn("w-full text-left px-3 py-2 text-sm font-medium hover:bg-indigo-50 hover:text-indigo-700 rounded-md transition-colors flex items-center gap-2 cursor-pointer", isActive ? "text-indigo-700 bg-indigo-50" : "text-gray-700")}
|
||||||
className={({ isActive }) => cn("w-full text-left px-3 py-2 text-sm font-medium hover:bg-indigo-50 hover:text-indigo-700 rounded-md transition-colors flex items-center gap-2 cursor-pointer", isActive ? "text-indigo-700 bg-indigo-50" : "text-gray-700")}
|
>
|
||||||
>
|
<Database size={16} />
|
||||||
<Database size={16} />
|
Manage Datasets
|
||||||
Manage Datasets
|
</NavLink>
|
||||||
</NavLink>
|
</div>
|
||||||
<NavLink
|
|
||||||
to="/admin/users"
|
|
||||||
className={({ isActive }) => cn("w-full text-left px-3 py-2 text-sm font-medium hover:bg-indigo-50 hover:text-indigo-700 rounded-md transition-colors flex items-center gap-2 cursor-pointer mt-1", isActive ? "text-indigo-700 bg-indigo-50" : "text-gray-700")}
|
|
||||||
>
|
|
||||||
<Users size={16} />
|
|
||||||
Manage Users
|
|
||||||
</NavLink>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="px-2">
|
<div className="px-2">
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
|||||||
@ -1,11 +1,10 @@
|
|||||||
import { useState, useEffect, type FormEvent } from 'react';
|
import { useState, type FormEvent } from 'react';
|
||||||
import { Card, Input, Select } from '../components/reusable';
|
import { Card, Input, Select } from '../components/reusable';
|
||||||
import { Button } from '../components/buttons/Button';
|
import { Button } from '../components/buttons/Button';
|
||||||
import {
|
import {
|
||||||
SALES_REPORT_ROUTES,
|
SALES_REPORT_ROUTES,
|
||||||
ROUTE_WISE_DISTRIBUTORS,
|
ROUTE_WISE_DISTRIBUTORS,
|
||||||
BASE_URL,
|
SALES_REPORT_SO_NAMES
|
||||||
PIPELINE
|
|
||||||
} from '../api/config';
|
} from '../api/config';
|
||||||
import { Download, Printer } from 'lucide-react';
|
import { Download, Printer } from 'lucide-react';
|
||||||
import jsPDF from 'jspdf';
|
import jsPDF from 'jspdf';
|
||||||
@ -15,36 +14,12 @@ export function DailySalesReportPage() {
|
|||||||
const [date, setDate] = useState('');
|
const [date, setDate] = useState('');
|
||||||
const [route, setRoute] = useState('');
|
const [route, setRoute] = useState('');
|
||||||
const [distributor, setDistributor] = useState('');
|
const [distributor, setDistributor] = useState('');
|
||||||
const [soEmail, setSoEmail] = useState('');
|
const [soName, setSoName] = useState('');
|
||||||
const [soOptions, setSoOptions] = useState<{value: string, label: string}[]>([]);
|
|
||||||
|
|
||||||
const [busy, setBusy] = useState(false);
|
const [busy, setBusy] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [reportData, setReportData] = useState<any>(null);
|
const [reportData, setReportData] = useState<any>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
async function fetchSOs() {
|
|
||||||
try {
|
|
||||||
const res = await fetch(`${BASE_URL}${PIPELINE.endpoints.salesOfficers}`, {
|
|
||||||
headers: {
|
|
||||||
'TemplateID': '194',
|
|
||||||
'X-Pipeline-Version': 'latest',
|
|
||||||
'OrgID': '57',
|
|
||||||
'GroupID': '25',
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const data = await res.json();
|
|
||||||
if (data?.response?.options) {
|
|
||||||
setSoOptions(data.response.options);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Failed to fetch SOs', err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fetchSOs();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const downloadPDF = () => {
|
const downloadPDF = () => {
|
||||||
const doc = new jsPDF({ orientation: 'landscape' });
|
const doc = new jsPDF({ orientation: 'landscape' });
|
||||||
const pageWidth = doc.internal.pageSize.getWidth();
|
const pageWidth = doc.internal.pageSize.getWidth();
|
||||||
@ -73,8 +48,7 @@ export function DailySalesReportPage() {
|
|||||||
doc.setFontSize(10);
|
doc.setFontSize(10);
|
||||||
doc.text(`Distributor Name : ${distributor}`, 14, 40);
|
doc.text(`Distributor Name : ${distributor}`, 14, 40);
|
||||||
doc.text(`Date : ${date}`, rightX, 40, { align: 'right' });
|
doc.text(`Date : ${date}`, rightX, 40, { align: 'right' });
|
||||||
const selectedSo = soOptions.find(o => o.value === soEmail);
|
doc.text(`SO Name : ${soName}`, rightX, 45, { align: 'right' });
|
||||||
doc.text(`SO Name : ${selectedSo ? selectedSo.label : soEmail}`, rightX, 45, { align: 'right' });
|
|
||||||
|
|
||||||
// Table
|
// Table
|
||||||
autoTable(doc, {
|
autoTable(doc, {
|
||||||
@ -116,8 +90,7 @@ export function DailySalesReportPage() {
|
|||||||
doc.setFontSize(10);
|
doc.setFontSize(10);
|
||||||
doc.text(`Distributor Name : ${distributor}`, 14, 40);
|
doc.text(`Distributor Name : ${distributor}`, 14, 40);
|
||||||
doc.text(`Date : ${date}`, rightX, 40, { align: 'right' });
|
doc.text(`Date : ${date}`, rightX, 40, { align: 'right' });
|
||||||
const selectedSo = soOptions.find(o => o.value === soEmail);
|
doc.text(`SO Name : ${soName}`, rightX, 45, { align: 'right' });
|
||||||
doc.text(`SO Name : ${selectedSo ? selectedSo.label : soEmail}`, rightX, 45, { align: 'right' });
|
|
||||||
|
|
||||||
// Table
|
// Table
|
||||||
autoTable(doc, {
|
autoTable(doc, {
|
||||||
@ -151,7 +124,7 @@ export function DailySalesReportPage() {
|
|||||||
setError(null);
|
setError(null);
|
||||||
setReportData(null);
|
setReportData(null);
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${BASE_URL}${PIPELINE.endpoints.dailySalesReport}`, {
|
const res = await fetch('https://sandbox.getzino.in/api/papi2/daily-sales-report', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'TemplateID': '157',
|
'TemplateID': '157',
|
||||||
@ -162,7 +135,7 @@ export function DailySalesReportPage() {
|
|||||||
date,
|
date,
|
||||||
route,
|
route,
|
||||||
distributor,
|
distributor,
|
||||||
so_name: soEmail,
|
so_name: soName,
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -208,9 +181,9 @@ export function DailySalesReportPage() {
|
|||||||
/>
|
/>
|
||||||
<Select
|
<Select
|
||||||
label="SO Name"
|
label="SO Name"
|
||||||
value={soEmail}
|
value={soName}
|
||||||
onChange={(e) => setSoEmail(e.target.value)}
|
onChange={(e) => setSoName(e.target.value)}
|
||||||
options={[{ value: '', label: 'Select SO Name' }, ...soOptions]}
|
options={[{ value: '', label: 'Select SO Name' }, ...SALES_REPORT_SO_NAMES.map(s => ({ value: s, label: s }))]}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{error && <div className="text-xs text-ruby-600 font-medium">{error}</div>}
|
{error && <div className="text-xs text-ruby-600 font-medium">{error}</div>}
|
||||||
@ -238,7 +211,7 @@ export function DailySalesReportPage() {
|
|||||||
<div>Distributor Name : {distributor}</div>
|
<div>Distributor Name : {distributor}</div>
|
||||||
<div className="text-right flex flex-col gap-1.5">
|
<div className="text-right flex flex-col gap-1.5">
|
||||||
<div>Date : {date}</div>
|
<div>Date : {date}</div>
|
||||||
<div>SO Name : {soOptions.find(o => o.value === soEmail)?.label || soEmail}</div>
|
<div>SO Name : {soName}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import { CallDetail } from '../components/dv';
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { Button } from '../components/buttons/Button';
|
import { Button } from '../components/buttons/Button';
|
||||||
import { Plus } from 'lucide-react';
|
import { Plus } from 'lucide-react';
|
||||||
import { DynamicForm, LogVisitForm } from '../components/forms';
|
import { DynamicForm } from '../components/forms/DynamicForm';
|
||||||
import { ORDER_BOOKING } from '../api/config';
|
import { ORDER_BOOKING } from '../api/config';
|
||||||
import { orderBookingClient } from '../api/clients';
|
import { orderBookingClient } from '../api/clients';
|
||||||
import { useAuth } from '../auth/context';
|
import { useAuth } from '../auth/context';
|
||||||
@ -190,8 +190,10 @@ export function MyCallsPage() {
|
|||||||
title={createTitle}
|
title={createTitle}
|
||||||
width="md"
|
width="md"
|
||||||
>
|
>
|
||||||
<LogVisitForm
|
<DynamicForm
|
||||||
client={orderBookingClient}
|
client={orderBookingClient}
|
||||||
|
activityId={ORDER_BOOKING.activities.LOG_VISIT.uid}
|
||||||
|
initialActivityName="Log Visit"
|
||||||
onSuccess={() => {
|
onSuccess={() => {
|
||||||
setIsCreating(false);
|
setIsCreating(false);
|
||||||
setRefreshKey(k => k + 1);
|
setRefreshKey(k => k + 1);
|
||||||
|
|||||||
@ -5,7 +5,8 @@ import { OrderDetail } from '../components/dv';
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Button } from '../components/buttons/Button';
|
import { Button } from '../components/buttons/Button';
|
||||||
import { Plus } from 'lucide-react';
|
import { Plus } from 'lucide-react';
|
||||||
import { LogVisitForm } from '../components/forms';
|
import { DynamicForm } from '../components/forms/DynamicForm';
|
||||||
|
import { ORDER_BOOKING } from '../api/config';
|
||||||
import { orderBookingClient } from '../api/clients';
|
import { orderBookingClient } from '../api/clients';
|
||||||
import { useAuth } from '../auth/context';
|
import { useAuth } from '../auth/context';
|
||||||
|
|
||||||
@ -45,8 +46,10 @@ export function MyOrdersPage() {
|
|||||||
title={createTitle}
|
title={createTitle}
|
||||||
width="md"
|
width="md"
|
||||||
>
|
>
|
||||||
<LogVisitForm
|
<DynamicForm
|
||||||
client={orderBookingClient}
|
client={orderBookingClient}
|
||||||
|
activityId={ORDER_BOOKING.activities.LOG_VISIT.uid}
|
||||||
|
initialActivityName="Place Order"
|
||||||
onSuccess={() => {
|
onSuccess={() => {
|
||||||
setIsCreating(false);
|
setIsCreating(false);
|
||||||
setRefreshKey(k => k + 1);
|
setRefreshKey(k => k + 1);
|
||||||
|
|||||||
@ -5,7 +5,8 @@ import { OrderDetail } from '../components/dv';
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Button } from '../components/buttons/Button';
|
import { Button } from '../components/buttons/Button';
|
||||||
import { Plus } from 'lucide-react';
|
import { Plus } from 'lucide-react';
|
||||||
import { LogVisitForm } from '../components/forms';
|
import { DynamicForm } from '../components/forms/DynamicForm';
|
||||||
|
import { ORDER_BOOKING } from '../api/config';
|
||||||
import { orderBookingClient } from '../api/clients';
|
import { orderBookingClient } from '../api/clients';
|
||||||
|
|
||||||
export function OrdersPage() {
|
export function OrdersPage() {
|
||||||
@ -39,8 +40,10 @@ export function OrdersPage() {
|
|||||||
title={createTitle}
|
title={createTitle}
|
||||||
width="md"
|
width="md"
|
||||||
>
|
>
|
||||||
<LogVisitForm
|
<DynamicForm
|
||||||
client={orderBookingClient}
|
client={orderBookingClient}
|
||||||
|
activityId={ORDER_BOOKING.activities.LOG_VISIT.uid}
|
||||||
|
initialActivityName="Place Order"
|
||||||
onSuccess={() => {
|
onSuccess={() => {
|
||||||
setIsCreating(false);
|
setIsCreating(false);
|
||||||
setRefreshKey(k => k + 1);
|
setRefreshKey(k => k + 1);
|
||||||
|
|||||||
@ -1,13 +1,14 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams, useNavigate } from 'react-router-dom';
|
||||||
import { orderBookingClient } from '../../api/clients';
|
import { orderBookingClient } from '../../api/clients';
|
||||||
import type { Dataset } from '../../api/types';
|
import type { Dataset } from '../../api/types';
|
||||||
import { Button } from '../../components/buttons/Button';
|
import { Button } from '../../components/buttons/Button';
|
||||||
import { Spinner } from '../../components/reusable/Spinner';
|
import { Spinner } from '../../components/reusable/Spinner';
|
||||||
import { Save, Trash2, Plus, Database, Upload } from 'lucide-react';
|
import { ArrowLeft, Save, Trash2, Plus, Database, Upload } from 'lucide-react';
|
||||||
|
|
||||||
export function DatasetItemsPage() {
|
export function DatasetItemsPage() {
|
||||||
const { id } = useParams<{ id: string }>();
|
const { id } = useParams<{ id: string }>();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const [dataset, setDataset] = useState<Dataset | null>(null);
|
const [dataset, setDataset] = useState<Dataset | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|||||||
@ -2,29 +2,21 @@ import { useEffect, useState } from 'react';
|
|||||||
import { orderBookingClient } from '../../api/clients';
|
import { orderBookingClient } from '../../api/clients';
|
||||||
import type { Dataset } from '../../api/types';
|
import type { Dataset } from '../../api/types';
|
||||||
import { Spinner } from '../../components/reusable/Spinner';
|
import { Spinner } from '../../components/reusable/Spinner';
|
||||||
import { Database } from 'lucide-react';
|
import { Database, LayoutList, Table2 } from 'lucide-react';
|
||||||
import { useNavigate, useLocation, Outlet, useParams, Navigate } from 'react-router-dom';
|
import { useNavigate, useLocation, Outlet, useParams } from 'react-router-dom';
|
||||||
import { useAuth } from '../../auth/context';
|
|
||||||
|
|
||||||
export function DatasetsPage() {
|
export function DatasetsPage() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const { id } = useParams<{ id: string }>();
|
const { id } = useParams<{ id: string }>();
|
||||||
const { isAdmin } = useAuth();
|
|
||||||
|
|
||||||
const [datasets, setDatasets] = useState<Dataset[]>([]);
|
const [datasets, setDatasets] = useState<Dataset[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isAdmin) {
|
fetchDatasets();
|
||||||
fetchDatasets();
|
}, []);
|
||||||
}
|
|
||||||
}, [isAdmin]);
|
|
||||||
|
|
||||||
if (!isAdmin) {
|
|
||||||
return <Navigate to="/orders" replace />;
|
|
||||||
}
|
|
||||||
|
|
||||||
const fetchDatasets = async () => {
|
const fetchDatasets = async () => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@ -1,64 +0,0 @@
|
|||||||
import { useState } from 'react';
|
|
||||||
import { Modal } from '../../components/reusable';
|
|
||||||
import { UsersView } from '../../components/rv';
|
|
||||||
import { DynamicForm } from '../../components/forms/DynamicForm';
|
|
||||||
import { USER } from '../../api/config';
|
|
||||||
import { userClient } from '../../api/clients';
|
|
||||||
import { Button } from '../../components/buttons/Button';
|
|
||||||
import { Plus, Edit } from 'lucide-react';
|
|
||||||
|
|
||||||
export function UsersPage() {
|
|
||||||
const [isCreating, setIsCreating] = useState(false);
|
|
||||||
const [refreshKey, setRefreshKey] = useState(0);
|
|
||||||
const [activeActivity, setActiveActivity] = useState<{ id: string; name: string, instanceId?: number } | null>(null);
|
|
||||||
|
|
||||||
const handleEditRow = (row: any) => {
|
|
||||||
setActiveActivity({
|
|
||||||
id: USER.activities.EDIT_USER.uid,
|
|
||||||
name: "Edit User",
|
|
||||||
instanceId: row.instance_id
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<UsersView
|
|
||||||
refreshKey={refreshKey}
|
|
||||||
headerActions={
|
|
||||||
<Button size="sm" iconLeft={<Plus size={14} />} onClick={() => setIsCreating(true)}>
|
|
||||||
Add Sales Officer
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
rowActions={(row) => (
|
|
||||||
<Button size="sm" variant="outline" iconLeft={<Edit size={14} />} onClick={() => handleEditRow(row)}>
|
|
||||||
Edit
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Modal
|
|
||||||
open={isCreating || activeActivity !== null}
|
|
||||||
onClose={() => {
|
|
||||||
setIsCreating(false);
|
|
||||||
setActiveActivity(null);
|
|
||||||
}}
|
|
||||||
title={activeActivity?.name || "Add User"}
|
|
||||||
>
|
|
||||||
<DynamicForm
|
|
||||||
client={userClient}
|
|
||||||
activityId={activeActivity?.id || USER.activities.ADD_USER.uid}
|
|
||||||
instanceId={activeActivity?.instanceId}
|
|
||||||
onSuccess={() => {
|
|
||||||
setIsCreating(false);
|
|
||||||
setActiveActivity(null);
|
|
||||||
setRefreshKey(k => k + 1);
|
|
||||||
}}
|
|
||||||
onCancel={() => {
|
|
||||||
setIsCreating(false);
|
|
||||||
setActiveActivity(null);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Modal>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -13,7 +13,6 @@ import {
|
|||||||
DailyLogDetail,
|
DailyLogDetail,
|
||||||
type WiredDetailViewProps,
|
type WiredDetailViewProps,
|
||||||
} from '../components/dv';
|
} from '../components/dv';
|
||||||
import { routeConfig } from '../routesConfig';
|
|
||||||
|
|
||||||
export type ScreenKey = 'orders' | 'my-orders' | 'calls' | 'my-calls' | 'stores' | 'daily' | 'my-daily' | 'sales-report';
|
export type ScreenKey = 'orders' | 'my-orders' | 'calls' | 'my-calls' | 'stores' | 'daily' | 'my-daily' | 'sales-report';
|
||||||
|
|
||||||
@ -25,22 +24,17 @@ export interface ScreenDef {
|
|||||||
Detail: (p: WiredDetailViewProps) => React.JSX.Element;
|
Detail: (p: WiredDetailViewProps) => React.JSX.Element;
|
||||||
/** Singular noun for the detail title. */
|
/** Singular noun for the detail title. */
|
||||||
noun: string;
|
noun: string;
|
||||||
roles?: string[];
|
|
||||||
adminOnly?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const getRoles = (path: string) => routeConfig.find(r => r.path === `/${path}`)?.roles;
|
|
||||||
const getAdminOnly = (path: string) => routeConfig.find(r => r.path === `/${path}`)?.adminOnly;
|
|
||||||
|
|
||||||
export const SCREENS: ScreenDef[] = [
|
export const SCREENS: ScreenDef[] = [
|
||||||
{ key: 'orders', label: 'Orders', icon: ShoppingCart, View: OrdersView, Detail: OrderDetail, noun: 'Order', roles: getRoles('orders'), adminOnly: getAdminOnly('orders') },
|
{ key: 'orders', label: 'Orders', icon: ShoppingCart, View: OrdersView, Detail: OrderDetail, noun: 'Order' },
|
||||||
{ key: 'my-orders', label: 'My Orders', icon: ShoppingCart, View: OrdersView, Detail: OrderDetail, noun: 'My Order', roles: getRoles('my-orders'), adminOnly: getAdminOnly('my-orders') },
|
{ key: 'my-orders', label: 'My Orders', icon: ShoppingCart, View: OrdersView, Detail: OrderDetail, noun: 'My Order' },
|
||||||
{ key: 'calls', label: 'Calls', icon: Phone, View: CallsView, Detail: CallDetail, noun: 'Call', roles: getRoles('calls'), adminOnly: getAdminOnly('calls') },
|
{ key: 'calls', label: 'Calls', icon: Phone, View: CallsView, Detail: CallDetail, noun: 'Call' },
|
||||||
{ key: 'my-calls', label: 'My Calls', icon: Phone, View: CallsView, Detail: CallDetail, noun: 'My Call', roles: getRoles('my-calls'), adminOnly: getAdminOnly('my-calls') },
|
{ key: 'my-calls', label: 'My Calls', icon: Phone, View: CallsView, Detail: CallDetail, noun: 'My Call' },
|
||||||
{ key: 'stores', label: 'Stores', icon: Store, View: StoresView, Detail: StoreDetail, noun: 'Store', roles: getRoles('stores'), adminOnly: getAdminOnly('stores') },
|
{ 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', roles: getRoles('daily'), adminOnly: getAdminOnly('daily') },
|
{ key: 'daily', label: 'Daily Logs', icon: ClipboardList, View: DailyLogsView, Detail: DailyLogDetail, noun: 'Daily Log' },
|
||||||
{ key: 'my-daily', label: 'My Daily Logs', icon: ClipboardList, View: DailyLogsView, Detail: DailyLogDetail, noun: 'My Daily Log', roles: getRoles('my-daily'), adminOnly: getAdminOnly('my-daily') },
|
{ key: 'my-daily', label: 'My Daily Logs', icon: ClipboardList, View: DailyLogsView, Detail: DailyLogDetail, noun: 'My Daily Log' },
|
||||||
{ key: 'sales-report', label: 'DSR', icon: FileText, View: null as any, Detail: null as any, noun: 'Sales Report', roles: getRoles('sales-report'), adminOnly: getAdminOnly('sales-report') },
|
{ key: 'sales-report', label: 'DSR', icon: FileText, View: null as any, Detail: null as any, noun: 'Sales Report' },
|
||||||
];
|
];
|
||||||
|
|
||||||
export function screenByKey(key: string | undefined): ScreenDef | undefined {
|
export function screenByKey(key: string | undefined): ScreenDef | undefined {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user