From 930809a581d377018c3010f4ece235b0500d87ff Mon Sep 17 00:00:00 2001 From: suryacp23 Date: Tue, 21 Jul 2026 12:10:23 +0530 Subject: [PATCH] detailview ui improved --- src/App.tsx | 2 + src/api/client.ts | 1 + src/api/config.ts | 9 + src/api/types.ts | 1 + src/components/buttons/Button.tsx | 11 +- src/components/buttons/style.css | 45 +++ src/components/dv/CallDetail.tsx | 378 +++++++++++++++++- src/components/dv/DailyLogDetail.tsx | 249 +++++++++++- src/components/dv/DetailView.tsx | 121 +++--- src/components/dv/GridTable.tsx | 75 ++++ src/components/dv/OrderDetail.tsx | 247 +++++++++++- src/components/dv/PotentialMiningTable.tsx | 136 +++++++ src/components/dv/StoreDetail.tsx | 324 ++++++++++++++- src/components/dv/index.ts | 4 + src/components/forms/DynamicForm.tsx | 4 +- .../forms/fields/SmartGridField.tsx | 110 +++-- src/components/reusable/AnalyticsChart.tsx | 59 ++- src/components/reusable/Card.tsx | 6 +- src/components/reusable/Modal.tsx | 32 +- src/components/reusable/Pagination.tsx | 87 +++- src/components/reusable/StatsTiles.tsx | 63 ++- src/components/rv/DailyLogsView.tsx | 3 +- src/components/rv/OrdersView.tsx | 4 +- src/components/rv/RecordView.tsx | 96 ++++- src/screens/CallsPage.tsx | 68 ++-- src/screens/ConsoleLayout.tsx | 47 ++- src/screens/DailyLogsPage.tsx | 19 +- src/screens/DailySalesReportPage.tsx | 16 - src/screens/MyOrdersPage.tsx | 1 + src/screens/ReportPage.tsx | 33 ++ src/styles/styles.css | 348 +++++++++++++++- src/styles/tokens/colors.css | 2 +- 32 files changed, 2270 insertions(+), 331 deletions(-) create mode 100644 src/components/buttons/style.css create mode 100644 src/components/dv/GridTable.tsx create mode 100644 src/components/dv/PotentialMiningTable.tsx create mode 100644 src/screens/ReportPage.tsx diff --git a/src/App.tsx b/src/App.tsx index 990bb18..66b914c 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -8,6 +8,7 @@ import { CallsPage } from './screens/CallsPage' import { StoresPage } from './screens/StoresPage' import { DailyLogsPage } from './screens/DailyLogsPage' import { DailySalesReportPage } from './screens/DailySalesReportPage' +import { ReportPage } from './screens/ReportPage' function App() { return ( @@ -31,6 +32,7 @@ function App() { } /> } /> + } /> } /> } /> diff --git a/src/api/client.ts b/src/api/client.ts index c0b9f7f..27b7927 100644 --- a/src/api/client.ts +++ b/src/api/client.ts @@ -125,6 +125,7 @@ export class ZinoClient { recordView(rvUid: string, params: RecordViewParams = {}): Promise { return this.request('POST', `/app/${APP_ID}/view/recordview`, { rv_template_uid: rvUid, + ...(params.presetAlias ? { preset_alias: params.presetAlias } : {}), search_query: { page: params.page ?? 1, limit: params.limit ?? 50, diff --git a/src/api/config.ts b/src/api/config.ts index 799a3ea..037fa06 100644 --- a/src/api/config.ts +++ b/src/api/config.ts @@ -65,6 +65,15 @@ export const ORDER_BOOKING = { recordViews: { ORDERS: '63714ac2-c9fb-40e2-abba-d4081a70b768', CALLS: '56b21b46-6d2c-4e10-b5a3-c63d058d0afa', + PRODUCT_WISE_ORDERS:'f1b372de-9e00-4cc6-81b5-1e1a98e7daef', + ROUTE_WISE_ORDERS:'bfb346b6-b512-4870-946b-a6f2e621870e', + SO_WISE_ORDERS:'c155d266-4eef-420d-9fb8-cd6ace43c569', + SO_WISE_VISITS:'08767452-6f0d-46cc-ae7f-ddf2cb480159', + YEARLY_ORDERS:'b8fd04da-f076-4e8d-b711-cf9282bf450a', + MONTHLY_ORDERS:'6d4ce86a-7a05-446b-8c81-cd0b98b6b244', + WEEKLY_ORDERS:'c6e2c57f-ed17-4c92-a2de-189c61c89422', + STORE_WISE_PRODUCTS:'f8d71b1d-dffd-450a-b9c9-3a63db8992c5' + }, detailViews: { ORDERS: '0804c6c3-6cf9-4050-94bb-fa48dd5de87d', diff --git a/src/api/types.ts b/src/api/types.ts index 759b353..2a1efbe 100644 --- a/src/api/types.ts +++ b/src/api/types.ts @@ -53,6 +53,7 @@ export interface RecordViewParams { sortDir?: 'asc' | 'desc'; search?: string; filters?: Array<{ field_key: string; value: string; data_type?: string }>; + presetAlias?: string; } // --- Form schema (POST /app/{appId}/view/form-screens) --- diff --git a/src/components/buttons/Button.tsx b/src/components/buttons/Button.tsx index a7fda27..44b5df6 100644 --- a/src/components/buttons/Button.tsx +++ b/src/components/buttons/Button.tsx @@ -1,8 +1,9 @@ import type { ButtonHTMLAttributes, ReactNode } from 'react'; import { cn } from '../../lib/cn'; +import './style.css' -export type ButtonVariant = 'primary' | 'navy' | 'secondary' | 'ghost' | 'danger'; -export type ButtonSize = 'sm' | 'md' | 'lg'; +export type ButtonVariant = 'primary' | 'navy' | 'secondary' | 'ghost' | 'danger' | 'outline'; +export type ButtonSize = 'sm' | 'md' | 'lg' | 'fab'; export interface ButtonProps extends ButtonHTMLAttributes { /** Visual style. @default "primary" */ @@ -19,14 +20,16 @@ const SIZES: Record = { sm: 'h-[34px] px-3.5 text-[13px] rounded-sm', md: 'h-[42px] px-[18px] text-base rounded-md', lg: 'h-[50px] px-6 text-md rounded-md', + fab: 'h-[56px] w-[56px] text-sm rounded-full', }; const VARIANTS: Record = { - primary: 'bg-sunrise text-white border border-transparent shadow-sunrise', + primary: 'z-btn border border-transparent shadow-blue-600/30', navy: 'bg-navy-900 text-on-navy border border-transparent shadow-sm', secondary: 'bg-card text-strong border border-border-default shadow-xs', ghost: 'bg-transparent text-body border border-transparent', danger: 'bg-ruby-600 text-white border border-transparent shadow-sm', + outline: 'z-btn-outline' }; /** @@ -52,7 +55,7 @@ export function Button({ 'disabled:opacity-50 disabled:cursor-not-allowed disabled:active:scale-100', SIZES[size], VARIANTS[variant], - full ? 'w-full' : 'w-auto', + full ? 'w-full' : '', className, )} {...rest} diff --git a/src/components/buttons/style.css b/src/components/buttons/style.css new file mode 100644 index 0000000..9af2bfc --- /dev/null +++ b/src/components/buttons/style.css @@ -0,0 +1,45 @@ +.z-btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 6px; + padding: 8px 16px; + border-radius: var(--z-border-radius-md); + font-size: var(--z-font-sm); + font-weight: 500; + font-family: inherit; + cursor: pointer; + transition: all 0.2s ease; + text-decoration: none; + line-height: 1.4; + background: var(--z-btn-primary-bg); + color: #ffffff; +} + +.z-btn-primary { + border: none; +} + +.z-btn:hover { + background: var(--primary-btn-active); +} + +.z-btn-secondary { + background-color: var(--z-block-bg); + color: var(--z-text-primary); + border: 1px solid var(--z-block-border); +} + +.z-btn-secondary:hover { + background-color: var(--z-body-bg); +} + +.z-btn-outline { + background: transparent; + color: var(--z-btn-outline-color); + border: 1px solid var(--z-btn-outline-border); +} + +.z-btn-outline:hover { + background-color: var(--z-color-primary-light); +} \ No newline at end of file diff --git a/src/components/dv/CallDetail.tsx b/src/components/dv/CallDetail.tsx index 0fe0b75..3c3a2b2 100644 --- a/src/components/dv/CallDetail.tsx +++ b/src/components/dv/CallDetail.tsx @@ -1,17 +1,371 @@ +import { useEffect, useState, type ReactNode } from 'react'; import { orderBookingClient } from '../../api/clients'; -import { ORDER_BOOKING } from '../../api/config'; -import { DetailView } from './DetailView'; -import type { WiredDetailViewProps } from './OrderDetail'; +import { ORDER_BOOKING, APP_ID } from '../../api/config'; +import { Card } from '../reusable/Card'; +import { Spinner } from '../reusable/Spinner'; +import { EmptyState } from '../reusable/EmptyState'; +import { formatValue } from '../../lib/format'; +import { ShoppingCart, Store, User, ClipboardList, TrendingUp } from 'lucide-react'; +import { PotentialMiningTable, type MiningItem } from './PotentialMiningTable'; +export interface CallDetailProps { + instanceId: number | string; + selectedRow?: Record; + potentialMiningAction?: ReactNode; + placeOrderAction?: ReactNode; +} + +/** Custom Call detail view matching specific groupings. */ +export function CallDetail({ instanceId, selectedRow, potentialMiningAction, placeOrderAction }: CallDetailProps) { + const [data, setData] = useState | null>(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + let live = true; + async function run() { + setLoading(true); + setError(null); + try { + const r = await orderBookingClient.detailView(ORDER_BOOKING.detailViews.CALLS, instanceId); + if (live) { + setData(r.data || {}); + } + } catch (e) { + if (live) setError((e as { message?: string })?.message ?? 'Failed to load'); + } finally { + if (live) setLoading(false); + } + } + run(); + return () => { + live = false; + }; + }, [instanceId]); + + if (error) { + return ( + + + + ); + } + + if (loading) { + return ( + +
+ +
+
+ ); + } + + if (!data || Object.keys(data).length === 0) { + return ( + + + + ); + } + + // Identify state + const rowSrc = selectedRow || data; + const stateName = String(rowSrc.current_state_name || rowSrc.current_state_name_ || rowSrc.current_state || rowSrc.status || 'Unknown'); + const valLower = stateName.toLowerCase(); + const isSuccess = valLower.includes('approve') || valLower.includes('complete') || valLower.includes('success') || valLower.includes('active') || valLower.includes('productive') || valLower.includes('ordered'); + const isWarning = valLower.includes('pending') || valLower.includes('draft') || valLower.includes('hold'); + const isDanger = valLower.includes('reject') || valLower.includes('fail') || valLower.includes('cancel'); + let colorClass = "text-blue-500"; + if (isSuccess) colorClass = "text-emerald-500"; + else if (isWarning) colorClass = "text-amber-500"; + else if (isDanger) colorClass = "text-red-500"; + + // Data Extraction and Fallback + const remainingData = { ...data }; + + const extract = (key: string, obj: any = data) => { + if (obj && key in obj) { + const val = obj[key]; + if (obj === remainingData) { + delete remainingData[key]; + } + return val; + } + return '-'; + }; + + const selectStore = (data.select_store as any) || {}; + if ('select_store' in remainingData) { + delete remainingData.select_store; + } + + // Also remove current_state to not render them again + delete remainingData.current_state_id; + delete remainingData.current_state_name; + + let orderIdVal = extract('order_id', remainingData); + let instanceIdVal = extract('instance_id', remainingData); + const orderId = orderIdVal !== '-' ? orderIdVal : instanceIdVal; + const orderDate = extract('date_of_order_3', remainingData); + const totalBags = extract('total_bags_3', remainingData); + const totalKgs = extract('total_kgs_3', remainingData); + const orderDetailsGrid = extract('order_details_3', remainingData); + + let potentialGrid: any[] = []; + if ('potential' in remainingData) { + const pot = remainingData.potential as any; + if (Array.isArray(pot)) { + potentialGrid = pot; + } else if (pot && typeof pot === 'object' && Array.isArray(pot.potential)) { + potentialGrid = pot.potential; + } + delete remainingData.potential; + } + + const miningData: MiningItem[] = potentialGrid.map((item: any, index: number) => { + let badgeType: 'error' | 'success' | 'info' | 'warning' | 'default' = 'default'; + const reason = item.reason || ''; + if (reason.toLowerCase().includes('quality')) badgeType = 'error'; + else if (reason.toLowerCase().includes('stock')) badgeType = 'success'; + else if (reason.toLowerCase().includes('price')) badgeType = 'warning'; + else if (reason.toLowerCase().includes('competitive')) badgeType = 'info'; + + return { + id: String(item.id || index), + productName: item.product_category || item.product_category_ || '-', + badge: reason ? { label: reason, type: badgeType } : undefined, + potentialKgs: Number(item.actual_potential || item.store_potential || 0), + orderedKgs: Number(item.total_ordered || 0), + }; + }); + + const storeName = selectStore.business_name || selectStore.store_name || '-'; + const storeLocation = selectStore.area || selectStore.location || '-'; + const dateOfVisit = extract('date_of_visit', remainingData); + + const customerId = selectStore.store_code || selectStore.customer_id || '-'; + const customerName = selectStore.owner_name || selectStore.customer_name || '-'; + + let phoneNumber = '-'; + if (selectStore.phone_number) { + phoneNumber = typeof selectStore.phone_number === 'object' + ? selectStore.phone_number.phone_with_dial_code || selectStore.phone_number.phone + : selectStore.phone_number; + } + + const createdAtKey = Object.keys(remainingData).find(k => k.endsWith('__created_at')); + const createdAt = createdAtKey ? extract(createdAtKey, remainingData) : '-'; + + const userIdKey = Object.keys(remainingData).find(k => k.endsWith('__user_id')); + const userObj = userIdKey ? extract(userIdKey, remainingData) : null; + const userName = userObj && typeof userObj === 'object' ? userObj.name || userObj.email : '-'; + + const uploadImage = extract('upload_image', remainingData); + + // Helper for rendering a row + const Row = ({ label, value }: { label: string, value: any }) => ( +
+ {label}: + {formatValue(value)} +
+ ); -/** Call detail view (Order Booking workflow). */ -export function CallDetail({ instanceId, columns }: WiredDetailViewProps) { return ( - +
+ + {/* 1. Status Card */} + +
+
+
Status
+
+ {stateName} +
+
+ {placeOrderAction && ( +
{placeOrderAction}
+ )} +
+
+ + {/* 2. Order Details Card */} + +
+ +

Order Details

+
+
+
+ + + + +
+ + {Array.isArray(orderDetailsGrid) && orderDetailsGrid.length > 0 && ( +
+

Line Items

+
+ + + + + + + + + + + + {orderDetailsGrid.map((item: any, idx: number) => { + const skuCode = item.sku_code_3 || '-'; + let chipColor = 'bg-slate-50 text-slate-700 border-slate-200'; + if (skuCode !== '-') { + const hash = skuCode.split('').reduce((acc: number, char: string) => acc + char.charCodeAt(0), 0); + const colors = [ + 'bg-blue-50 text-blue-700 border-blue-200', + 'bg-emerald-50 text-emerald-700 border-emerald-200', + 'bg-amber-50 text-amber-700 border-amber-200', + 'bg-rose-50 text-rose-700 border-rose-200', + 'bg-purple-50 text-purple-700 border-purple-200', + 'bg-indigo-50 text-indigo-700 border-indigo-200', + 'bg-pink-50 text-pink-700 border-pink-200', + 'bg-cyan-50 text-cyan-700 border-cyan-200' + ]; + chipColor = colors[hash % colors.length]; + } + + return ( + + + + + + + + ); + })} + +
Product NameSKUSKU CodeBR CodeBags
{item.product_name_3 || item.product_category_3 || '-'}{item.sku_3 || '-'} + + {skuCode} + + {item.br_code_3 || '-'}{item.bags_3 || '-'}
+
+
+ )} + + {potentialMiningAction && ( +
+ {potentialMiningAction} +
+ )} + +
+ + +
+
+
+ + {/* 3. Store Details Card */} + +
+ +

Store Details

+
+
+ + + +
+
+ + {/* 4. Customer Details Card */} + +
+ +

Customer Details

+
+
+ + + +
+
+ + {/* 5. Potential Mining Card */} + {miningData.length > 0 && ( + +
+ +

Potential Mining

+
+
+ +
+
+ )} + + {/* 6. Log Visit Details Fallback */} + {Object.keys(remainingData).length > 0 && ( + +
+ +

Log Visit Details

+
+
+ {Object.entries(remainingData).map(([key, value]) => { + const lowerKey = key.toLowerCase(); + if (lowerKey.includes('uuid') || /^\d+$/.test(key)) return null; + // we just render them all as rows for simplicity in this custom view + let finalValue = value; + if (finalValue && typeof finalValue === 'object') { + try { + finalValue = JSON.stringify(finalValue); + } catch (e) { + finalValue = '-'; + } + } + // format label to be readable + const label = key.replace(/_/g, ' ') + .replace(/\b\w/g, l => l.toUpperCase()) + .replace(/[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}/i, '') + .replace(/^\s+|\s+$/g, ''); + + if (!label) return null; + + return ; + })} + + {Array.isArray(uploadImage) && uploadImage.length > 0 && ( +
+ Uploaded Image: +
+ {uploadImage.map((file: any, idx: number) => { + const previewUrl = `${orderBookingClient.baseUrl}/app/${APP_ID}/view/files/${file.uuid}/preview`; + return ( +
+ {file.original_name { + (e.target as HTMLImageElement).style.display = 'none'; + (e.target as HTMLImageElement).parentElement!.innerHTML = `${file.original_name || 'File'}`; + }} + /> + +
+ ); + })} +
+
+ )} +
+
+ )} + +
); } diff --git a/src/components/dv/DailyLogDetail.tsx b/src/components/dv/DailyLogDetail.tsx index 2de54bd..0702d0b 100644 --- a/src/components/dv/DailyLogDetail.tsx +++ b/src/components/dv/DailyLogDetail.tsx @@ -1,17 +1,242 @@ +import { useEffect, useState } from 'react'; import { dailyReportsClient } from '../../api/clients'; -import { DAILY_REPORTS } from '../../api/config'; -import { DetailView } from './DetailView'; -import type { WiredDetailViewProps } from './OrderDetail'; +import { DAILY_REPORTS, APP_ID } from '../../api/config'; +import { Card } from '../reusable/Card'; +import { Spinner } from '../reusable/Spinner'; +import { EmptyState } from '../reusable/EmptyState'; +import { formatValue } from '../../lib/format'; +import { Button } from '../buttons/Button'; +import { Map, User, ClipboardList, Camera, Clock } from 'lucide-react'; + +export interface DailyLogDetailProps { + instanceId: number | string; + onPunchOut?: () => void; +} + +export function DailyLogDetail({ instanceId, onPunchOut }: DailyLogDetailProps) { + const [data, setData] = useState | null>(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + let live = true; + async function run() { + setLoading(true); + setError(null); + try { + const r = await dailyReportsClient.detailView(DAILY_REPORTS.detailViews.DAILY_LOGS, instanceId); + if (live) setData(r.data || {}); + } catch (e) { + if (live) setError((e as { message?: string })?.message ?? 'Failed to load'); + } finally { + if (live) setLoading(false); + } + } + run(); + return () => { live = false; }; + }, [instanceId]); + + if (error) { + return ( + + + + ); + } + + if (loading) { + return ( + +
+
+ ); + } + + if (!data || Object.keys(data).length === 0) { + return ( + + + + ); + } + + const remainingData = { ...data }; + const extract = (key: string, obj: any = data) => { + if (obj && key in obj) { + const val = obj[key]; + delete obj[key]; + return val; + } + return '-'; + }; + + // State + const stateName = extract('current_state_name', remainingData) as string; + delete remainingData.current_state_id; + + const isPunchedIn = stateName.toLowerCase().includes('punched in'); + const isPunchedOut = stateName.toLowerCase().includes('punched out'); + const colorClass = isPunchedOut ? 'text-emerald-500' : (isPunchedIn ? 'text-blue-500' : 'text-slate-500'); + + // General Info + const date = extract('date', remainingData); + const time = extract('time', remainingData); + const routeCode = extract('route_code', remainingData); + const subRoute = extract('sub_route', remainingData); + const dayPlanNotes = extract('day_plan_notes', remainingData); + + // Officer Info + const officerNameRaw = extract('sales_officer_name', remainingData); + let officerName = '-'; + let officerEmail = '-'; + if (officerNameRaw && typeof officerNameRaw === 'object') { + officerName = (officerNameRaw as any).name || '-'; + officerEmail = (officerNameRaw as any).email || '-'; + } + + // Image + const storeImage = extract('store_image', remainingData); + + // EOD Info + const totalProductive = extract('total_productive_calls', remainingData); + const totalNonProductive = extract('total_non_productive_calls', remainingData); + const eodNotes = extract('eod_notes_remarks', remainingData); + + // Meta + const createdAtKey = Object.keys(remainingData).find(k => k.endsWith('__created_at')); + const createdAt = createdAtKey ? extract(createdAtKey, remainingData) : '-'; + + // Helper Row + const Row = ({ label, value }: { label: string, value: any }) => { + let finalValue = value; + if (finalValue && typeof finalValue === 'object') { + try { finalValue = JSON.stringify(finalValue); } catch (e) { finalValue = '-'; } + } + return ( +
+ {label}: + {formatValue(finalValue)} +
+ ); + }; -/** Daily Log detail view (Daily Reports workflow). */ -export function DailyLogDetail({ instanceId, columns }: WiredDetailViewProps) { return ( - +
+ + {/* 1. Status Card */} + +
+
+
Current State
+
+ {stateName} +
+
+ {isPunchedIn && onPunchOut && ( + + )} +
+
+ + {/* 2. Route & Plan Card */} + +
+ +

Day Plan & Route

+
+
+ + + + + +
+
+ + {/* 3. Officer Info */} + +
+ +

Sales Officer

+
+
+ + +
+
+ + {/* 4. EOD Details (Only if not punched in) */} + {!isPunchedIn && ( + +
+ +

End of Day Details

+
+
+ + + +
+
+ )} + + {/* 5. Image Card */} + {Array.isArray(storeImage) && storeImage.length > 0 && ( + +
+ +

Store Image

+
+
+
+ {storeImage.map((file: any, idx: number) => { + const previewUrl = `${dailyReportsClient.baseUrl}/app/${APP_ID}/view/files/${file.uuid}/preview`; + return ( +
+ {file.original_name { + (e.target as HTMLImageElement).style.display = 'none'; + (e.target as HTMLImageElement).parentElement!.innerHTML = `${file.original_name || 'File'}`; + }} + /> + +
+ ); + })} +
+
+
+ )} + + {/* 6. Other Details */} + {(Object.keys(remainingData).filter(k => !k.includes('uuid') && !/^\d+$/.test(k) && k !== 'instance_id').length > 0) && ( + +
+ +

Other Details

+
+
+ + {Object.entries(remainingData).map(([key, value]) => { + const lowerKey = key.toLowerCase(); + if (lowerKey.includes('uuid') || /^\d+$/.test(key) || lowerKey === 'instance_id') return null; + + const label = key.replace(/_/g, ' ') + .replace(/\b\w/g, l => l.toUpperCase()) + .replace(/[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}/i, '') + .replace(/^\s+|\s+$/g, ''); + + if (!label) return null; + + return ; + })} +
+
+ )} + +
); } diff --git a/src/components/dv/DetailView.tsx b/src/components/dv/DetailView.tsx index 69a0563..e503912 100644 --- a/src/components/dv/DetailView.tsx +++ b/src/components/dv/DetailView.tsx @@ -2,8 +2,7 @@ import { useEffect, useState } from 'react'; import { cn } from '../../lib/cn'; import { formatValue } from '../../lib/format'; import type { ZinoClient } from '../../api/client'; -import type { AuditEntry } from '../../api/types'; -import { WORKFLOWS } from '../../api/config'; +import { APP_ID } from '../../api/config'; import { Card } from '../reusable/Card'; import { Spinner } from '../reusable/Spinner'; import { EmptyState } from '../reusable/EmptyState'; @@ -11,7 +10,7 @@ import { EmptyState } from '../reusable/EmptyState'; export interface DetailViewProps { /** Workflow-bound client (see api/clients.ts). */ client: ZinoClient; - /** detailview template uid (deprecated, using audit endpoint now). */ + /** detailview template uid. */ dvUid?: string; /** Instance to render. */ instanceId: number | string; @@ -23,29 +22,13 @@ export interface DetailViewProps { columns?: 1 | 2 | 3; } -/** - * Helper to resolve a human-readable activity name from its UID. - */ -function getActivityName(uid: string): string | undefined { - for (const wf of Object.values(WORKFLOWS)) { - for (const [actName, actDef] of Object.entries(wf.activities)) { - if (actDef.uid === uid) { - return actName - .split('_') - .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()) - .join(' '); - } - } - } - return undefined; -} /** - * Generic Zino detail view. Fetches `GET /app/{id}/view/audit` and - * renders the audit trail as a labeled definition grid. + * Generic Zino detail view. Fetches `GET /app/{id}/view/detailview/{dvUid}` and + * renders the data as a labeled definition grid. */ -export function DetailView({ client, instanceId, title, columns = 2 }: DetailViewProps) { - const [entries, setEntries] = useState([]); +export function DetailView({ client, dvUid, instanceId, title, fields, columns = 2 }: DetailViewProps) { + const [data, setData] = useState | null>(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); @@ -55,8 +38,11 @@ export function DetailView({ client, instanceId, title, columns = 2 }: DetailVie setLoading(true); setError(null); try { - const r = await client.audit(instanceId); - if (live) setEntries(r); + if (!dvUid) throw new Error("dvUid is required to fetch detail view data."); + const r = await client.detailView(dvUid, instanceId); + if (live) { + setData(r.data || {}); + } } catch (e) { if (live) setError((e as { message?: string })?.message ?? 'Failed to load'); } finally { @@ -67,7 +53,7 @@ export function DetailView({ client, instanceId, title, columns = 2 }: DetailVie return () => { live = false; }; - }, [client, instanceId]); + }, [client, dvUid, instanceId]); const gridCols = { 1: 'grid-cols-1', 2: 'grid-cols-1 sm:grid-cols-2', 3: 'grid-cols-1 sm:grid-cols-3' }[columns]; @@ -89,29 +75,27 @@ export function DetailView({ client, instanceId, title, columns = 2 }: DetailVie ); } - if (entries.length === 0) { + if (!data || Object.keys(data).length === 0) { return ( - + ); } return (
- {entries.map((entry, i) => { - const actName = getActivityName(entry.activity_id); - const headerTitle = actName || (i === 0 && title ? title : 'Update'); - const updatedAt = new Date(entry.created_at).toLocaleString(); - - return ( - {updatedAt}} - > -
- {Object.entries(entry.data).map(([key, value]) => { + +
+ {Object.entries(data).map(([key, value]) => { + if (fields && !fields.includes(key)) return null; + const lowerKey = key.toLowerCase(); + if (lowerKey.includes('uuid') || /^\d+$/.test(key)) return null; + const isGridArray = Array.isArray(value) && value.length > 0 && @@ -127,7 +111,7 @@ export function DetailView({ client, instanceId, title, columns = 2 }: DetailVie return (
-
+
{key.replace(/_/g, ' ')}
@@ -146,7 +130,7 @@ export function DetailView({ client, instanceId, title, columns = 2 }: DetailVie {headers.map(h => ( - {formatValue(row[h], h)} + {formatValue(row[h])} ))} @@ -158,21 +142,60 @@ export function DetailView({ client, instanceId, title, columns = 2 }: DetailVie ); } + const isFileArray = + Array.isArray(value) && + value.length > 0 && + typeof value[0] === 'object' && + value[0] !== null && + ('original_name' in value[0] || 'uuid' in value[0]); + + if (isFileArray) { + return ( +
+
+ {key.replace(/_/g, ' ')} +
+
+ {(value as any[]).map((file, idx) => { + const previewUrl = `${client.baseUrl}/app/${APP_ID}/view/files/${file.uuid}/preview`; + return ( +
+ {file.original_name { + (e.target as HTMLImageElement).style.display = 'none'; + (e.target as HTMLImageElement).parentElement!.innerHTML = `${file.original_name || 'File'}`; + }} + /> + +
+ ); + })} +
+
+ ); + } + return ( -
-
+
+
{key.replace(/_/g, ' ')}
- {formatValue(value, key)} + {formatValue(value)}
); })}
-
- ); - })} +
); } diff --git a/src/components/dv/GridTable.tsx b/src/components/dv/GridTable.tsx new file mode 100644 index 0000000..049cfee --- /dev/null +++ b/src/components/dv/GridTable.tsx @@ -0,0 +1,75 @@ +import { Pencil, Trash2 } from 'lucide-react'; +import type { FormScreenField } from '../../api/types'; + +export interface GridTableProps { + data: Record[]; + columns: FormScreenField[]; + onEdit?: (idx: number) => void; + onDelete?: (idx: number) => void; +} + +export function GridTable({ data, columns, onEdit, onDelete }: GridTableProps) { + return ( +
+ + + + {columns.map((col, idx) => ( + + ))} + {(onEdit || onDelete) && ( + + )} + + + + {data.map((row, rowIdx) => ( + + {columns.map((col, colIdx) => { + const val = row[col.id]; + let displayVal = String(val ?? '-'); + if (col.data_type === 'select' || col.data_type === 'multiselect') { + const opt = col.properties?.options?.find((o: any) => String(o.value) === String(val)); + if (opt) displayVal = opt.label; + } + + const isNumeric = col.data_type === 'number'; + return ( + + ); + })} + {(onEdit || onDelete) && ( + + )} + + ))} + +
{col.name}Actions
+ {displayVal} + +
+ {onEdit && ( + + )} + {onDelete && ( + + )} +
+
+
+ ); +} diff --git a/src/components/dv/OrderDetail.tsx b/src/components/dv/OrderDetail.tsx index 9d8b1ce..f44510e 100644 --- a/src/components/dv/OrderDetail.tsx +++ b/src/components/dv/OrderDetail.tsx @@ -1,21 +1,248 @@ +import { useEffect, useState } from 'react'; import { orderBookingClient } from '../../api/clients'; import { ORDER_BOOKING } from '../../api/config'; -import { DetailView } from './DetailView'; +import { Card } from '../reusable/Card'; +import { Spinner } from '../reusable/Spinner'; +import { EmptyState } from '../reusable/EmptyState'; +import { formatValue } from '../../lib/format'; +import { ShoppingCart, Store, User, Package, Clock, ClipboardList } from 'lucide-react'; +import { GridTable } from './GridTable'; export interface WiredDetailViewProps { instanceId: number | string; columns?: 1 | 2 | 3; } -/** Order detail view (Order Booking workflow). */ -export function OrderDetail({ instanceId, columns }: WiredDetailViewProps) { +export function OrderDetail({ instanceId }: WiredDetailViewProps) { + const [data, setData] = useState | null>(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + let live = true; + async function run() { + setLoading(true); + setError(null); + try { + const r = await orderBookingClient.detailView(ORDER_BOOKING.detailViews.ORDERS, instanceId); + if (live) setData(r.data || {}); + } catch (e) { + if (live) setError((e as { message?: string })?.message ?? 'Failed to load'); + } finally { + if (live) setLoading(false); + } + } + run(); + return () => { live = false; }; + }, [instanceId]); + + if (error) { + return ( + + + + ); + } + + if (loading) { + return ( + +
+
+ ); + } + + if (!data || Object.keys(data).length === 0) { + return ( + + + + ); + } + + const remainingData = { ...data }; + const extract = (key: string, obj: any = data) => { + if (obj && key in obj) { + const val = obj[key]; + delete obj[key]; + return val; + } + return '-'; + }; + + // State + const stateName = extract('current_state_name', remainingData); + delete remainingData.current_state_id; + const isSuccess = ['ordered', 'active', 'approve'].some(s => stateName.toString().toLowerCase().includes(s)); + const colorClass = isSuccess ? 'text-emerald-500' : 'text-blue-500'; + + // Order Details + const orderId = extract('order_id', remainingData); + const dateOfOrder = extract('date_of_order_3', remainingData); + + // SO Info + const soKey = Object.keys(remainingData).find(k => k.endsWith('__user_id')) || 'so_name'; + const soRaw = extract(soKey, remainingData); + let soName = '-'; + let soEmail = '-'; + if (soRaw && typeof soRaw === 'object') { + soName = (soRaw as any).name || '-'; + soEmail = (soRaw as any).email || '-'; + } + + // Store Info + const storeRaw = extract('select_store', remainingData); + let storeName = '-'; + let distributorName = '-'; + let routeName = '-'; + let routeCode = '-'; + if (storeRaw && typeof storeRaw === 'object') { + storeName = (storeRaw as any).business_name || '-'; + distributorName = (storeRaw as any).distributor_name || '-'; + routeName = (storeRaw as any).route_name || '-'; + routeCode = (storeRaw as any).route_code || '-'; + } + + // Line Items + const orderDetailsGrid = extract('order_details_3', remainingData); + + // Totals + const totalBags = extract('total_bags_3', remainingData); + const totalKgs = extract('total_kgs_3', remainingData); + + // Meta + const createdAtKey = Object.keys(remainingData).find(k => k.endsWith('__created_at')); + const createdAt = createdAtKey ? extract(createdAtKey, remainingData) : '-'; + + // Helper Row + const Row = ({ label, value, highlight = false }: { label: string, value: any, highlight?: boolean }) => { + let finalValue = value; + if (finalValue && typeof finalValue === 'object') { + try { finalValue = JSON.stringify(finalValue); } catch (e) { finalValue = '-'; } + } + return ( +
+ {label}: + + {formatValue(finalValue)} + +
+ ); + }; + return ( - +
+ + {/* 1. Status Card */} + +
+
+
Status
+
+ {stateName} +
+
+
+
+ + {/* 2. Order Summary Card */} + +
+ +

Order Summary

+
+
+ + +
+
+ + {/* 3. Store details Card */} + +
+ +

Store Details

+
+
+ + + +
+
+ + {/* 4. Sales Officer Card */} + +
+ +

Sales Officer

+
+
+ + +
+
+ + {/* 5. Line Items Grid */} + {Array.isArray(orderDetailsGrid) && orderDetailsGrid.length > 0 && ( + +
+ +

Line Items

+
+
+ +
+
+ )} + + {/* 6. Order Totals */} + +
+ +

Order Totals

+
+
+ + +
+
+ + {/* 7. Log Visit Details Fallback & Meta */} + {(Object.keys(remainingData).filter(k => !k.includes('uuid') && !/^\d+$/.test(k) && k !== 'instance_id').length > 0 || createdAt !== '-') && ( + +
+ +

Other Details

+
+
+ + {Object.entries(remainingData).map(([key, value]) => { + const lowerKey = key.toLowerCase(); + if (lowerKey.includes('uuid') || /^\d+$/.test(key) || lowerKey === 'instance_id') return null; + + const label = key.replace(/_/g, ' ') + .replace(/\b\w/g, l => l.toUpperCase()) + .replace(/[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}/i, '') + .replace(/^\s+|\s+$/g, ''); + + if (!label) return null; + + return ; + })} +
+
+ )} + +
); } diff --git a/src/components/dv/PotentialMiningTable.tsx b/src/components/dv/PotentialMiningTable.tsx new file mode 100644 index 0000000..e85305a --- /dev/null +++ b/src/components/dv/PotentialMiningTable.tsx @@ -0,0 +1,136 @@ +import { ArrowUpRight, ArrowDownRight, Pencil, Trash2 } from 'lucide-react'; +import { cn } from '../../lib/cn'; + +export interface MiningItem { + id: string; + productName: string; + badge?: { + label: string; + type: 'error' | 'success' | 'info' | 'warning' | 'default'; + }; + status?: string; + potentialKgs: number; + orderedKgs: number; +} + +export interface PotentialMiningTableProps { + data: MiningItem[]; + onEdit?: (item: MiningItem) => void; + onDelete?: (item: MiningItem) => void; + hideOrderDetails?: boolean; +} + +export function PotentialMiningTable({ data, onEdit, onDelete, hideOrderDetails }: PotentialMiningTableProps) { + return ( +
+ + + + + + {!hideOrderDetails && ( + <> + + + + )} + {(onEdit || onDelete) && ( + + )} + + + + {data.map((item) => { + const difference = item.orderedKgs - item.potentialKgs; + const isSurplus = difference >= 0; + const absDiff = Math.abs(difference); + + return ( + + + + {!hideOrderDetails && ( + <> + + + + )} + {(onEdit || onDelete) && ( + + )} + + ); + })} + +
ProductPotential (Kgs)Ordered (Kgs)DifferenceActions
+
+ {item.productName} + {item.badge && ( + + {item.badge.label} + + )} +
+
+ {item.potentialKgs} + + {item.orderedKgs} + +
+
+
+ {isSurplus ? ( + + ) : ( + + )} + {absDiff} Kgs +
+ + {isSurplus ? 'Surplus' : 'Shortfall'} + +
+
+
+
+ {onEdit && ( + + )} + {onDelete && ( + + )} +
+
+
+ ); +} diff --git a/src/components/dv/StoreDetail.tsx b/src/components/dv/StoreDetail.tsx index 9d2b109..924c41c 100644 --- a/src/components/dv/StoreDetail.tsx +++ b/src/components/dv/StoreDetail.tsx @@ -1,17 +1,319 @@ +import { useEffect, useState } from 'react'; import { storeClient } from '../../api/clients'; -import { STORE } from '../../api/config'; -import { DetailView } from './DetailView'; +import { STORE, APP_ID } from '../../api/config'; +import { Card } from '../reusable/Card'; +import { Spinner } from '../reusable/Spinner'; +import { EmptyState } from '../reusable/EmptyState'; +import { formatValue } from '../../lib/format'; +import { Store, User, Truck, MapPin, TrendingUp, Camera, ClipboardList } from 'lucide-react'; +import { GridTable } from './GridTable'; import type { WiredDetailViewProps } from './OrderDetail'; -/** Store detail view (Store workflow). */ -export function StoreDetail({ instanceId, columns }: WiredDetailViewProps) { +export function StoreDetail({ instanceId }: WiredDetailViewProps) { + const [data, setData] = useState | null>(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + let live = true; + async function run() { + setLoading(true); + setError(null); + try { + const r = await storeClient.detailView(STORE.detailViews.STORE, instanceId); + if (live) setData(r.data || {}); + } catch (e) { + if (live) setError((e as { message?: string })?.message ?? 'Failed to load'); + } finally { + if (live) setLoading(false); + } + } + run(); + return () => { live = false; }; + }, [instanceId]); + + if (error) { + return ( + + + + ); + } + + if (loading) { + return ( + +
+
+ ); + } + + if (!data || Object.keys(data).length === 0) { + return ( + + + + ); + } + + const remainingData = { ...data }; + const extract = (key: string, obj: any = data) => { + if (obj && key in obj) { + const val = obj[key]; + delete obj[key]; + return val; + } + return '-'; + }; + + // State + const stateName = extract('current_state_name', remainingData); + delete remainingData.current_state_id; + const isSuccess = ['created', 'active', 'approve'].some(s => stateName.toString().toLowerCase().includes(s)); + const colorClass = isSuccess ? 'text-emerald-500' : 'text-blue-500'; + + // Store Overview + const storeCode = extract('store_code', remainingData); + const businessName = extract('business_name', remainingData); + const area = extract('area', remainingData); + const completeAddress = extract('complete_address', remainingData); + const pinCode = extract('pin_code', remainingData); + const notes = extract('notes', remainingData); + + // Owner Info + const ownerName = extract('owner_name', remainingData); + const email = extract('email', remainingData); + let phoneNumber = extract('phone_number', remainingData); + if (typeof phoneNumber === 'object' && phoneNumber !== null) { + phoneNumber = (phoneNumber as any).phone_with_dial_code || (phoneNumber as any).phone || '-'; + } + + // Distributor Info + const distributorName = extract('distributor_name', remainingData); + const distributorOwnerName = extract('distributor_owner_name', remainingData); + const distributorEmail = extract('distributor_email', remainingData); + let distributorPhone = extract('distributor_phone_number', remainingData); + if (typeof distributorPhone === 'object' && distributorPhone !== null) { + distributorPhone = (distributorPhone as any).phone_with_dial_code || (distributorPhone as any).phone || '-'; + } + + // Route Info + const routeCode = extract('route_code', remainingData); + const routeName = extract('route_name', remainingData); + const subRoute = extract('sub_route', remainingData); + + // Location + const storeLocation = extract('store_location', remainingData); + let lat = null; + let lng = null; + if (storeLocation) { + let locObj = storeLocation; + if (typeof locObj === 'string') { + try { locObj = JSON.parse(locObj); } catch (e) {} + } + if (locObj && typeof locObj === 'object') { + lat = (locObj as any).latitude; + lng = (locObj as any).longitude; + } + } + + // Potential + const potential = extract('potential', remainingData); + + // Image + const storeImage = extract('store_image', remainingData); + + // Meta + const createdAtKey = Object.keys(remainingData).find(k => k.endsWith('__created_at')); + const createdAt = createdAtKey ? extract(createdAtKey, remainingData) : '-'; + + const userIdKey = Object.keys(remainingData).find(k => k.endsWith('__user_id')); + const userObj = userIdKey ? extract(userIdKey, remainingData) : null; + const userName = userObj && typeof userObj === 'object' ? (userObj as any).name || (userObj as any).email : '-'; + + // Helper Row + const Row = ({ label, value }: { label: string, value: any }) => { + let finalValue = value; + if (finalValue && typeof finalValue === 'object') { + try { finalValue = JSON.stringify(finalValue); } catch (e) { finalValue = '-'; } + } + return ( +
+ {label}: + {formatValue(finalValue)} +
+ ); + }; + return ( - +
+ + {/* 1. Status Card */} + +
+
+
Status
+
+ {stateName} +
+
+
+
+ + {/* 2. Store Overview Card */} + +
+ +

Store Overview

+
+
+ + + + + + +
+
+ + {/* 3. Owner Information Card */} + +
+ +

Owner Information

+
+
+ + + +
+
+ + {/* 4. Distributor Details Card */} + +
+ +

Distributor Details

+
+
+ + + + +
+
+ + {/* 5. Route Details Card */} + +
+ +

Route Details

+
+
+ + + +
+
+ + {/* 6. Potential Card */} + {Array.isArray(potential) && potential.length > 0 && ( + +
+ +

Potential

+
+
+ +
+
+ )} + + {/* 7. Store Image Card */} + {Array.isArray(storeImage) && storeImage.length > 0 && ( + +
+ +

Store Image

+
+
+
+ {storeImage.map((file: any, idx: number) => { + const previewUrl = `${storeClient.baseUrl}/app/${APP_ID}/view/files/${file.uuid}/preview`; + return ( +
+ {file.original_name { + (e.target as HTMLImageElement).style.display = 'none'; + (e.target as HTMLImageElement).parentElement!.innerHTML = `${file.original_name || 'File'}`; + }} + /> + +
+ ); + })} +
+
+
+ )} + + {/* 7.5 Store Location Map */} + {lat && lng && ( + +
+ +

Store Location

+
+
+ +
+
+ )} + + {/* 8. Log Visit Details Fallback & Meta */} + {(Object.keys(remainingData).filter(k => !k.includes('uuid') && !/^\d+$/.test(k)).length > 0 || userName !== '-') && ( + +
+ +

Other Details

+
+
+ + + {Object.entries(remainingData).map(([key, value]) => { + const lowerKey = key.toLowerCase(); + if (lowerKey.includes('uuid') || /^\d+$/.test(key) || lowerKey === 'instance_id') return null; + + const label = key.replace(/_/g, ' ') + .replace(/\b\w/g, l => l.toUpperCase()) + .replace(/[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}/i, '') + .replace(/^\s+|\s+$/g, ''); + + if (!label) return null; + + return ; + })} +
+
+ )} + +
); } diff --git a/src/components/dv/index.ts b/src/components/dv/index.ts index f146b7d..519ce23 100644 --- a/src/components/dv/index.ts +++ b/src/components/dv/index.ts @@ -5,3 +5,7 @@ export type { WiredDetailViewProps } from './OrderDetail'; export { CallDetail } from './CallDetail'; export { StoreDetail } from './StoreDetail'; export { DailyLogDetail } from './DailyLogDetail'; +export { PotentialMiningTable } from './PotentialMiningTable'; +export { GridTable } from './GridTable'; +export type { GridTableProps } from './GridTable'; +export type { MiningItem, PotentialMiningTableProps } from './PotentialMiningTable'; diff --git a/src/components/forms/DynamicForm.tsx b/src/components/forms/DynamicForm.tsx index bca422c..eee7ac6 100644 --- a/src/components/forms/DynamicForm.tsx +++ b/src/components/forms/DynamicForm.tsx @@ -191,12 +191,12 @@ export function DynamicForm({ client, activityId: initialActivityId, instanceId: } else if ((f.data_type === 'image' || f.data_type === 'file') && Array.isArray(val) && val.length > 0 && val[0] instanceof File) { const uploadedFiles = []; for (const file of val) { - const fileMeta = await client.uploadFile(file, { activityId: currentActivityId, fieldId: f.id }); + const fileMeta = await client.uploadFile(file, { activityId: currentActivityId, fieldId: f.id, instanceId: currentInstanceId }); uploadedFiles.push(fileMeta); } payload[f.id] = uploadedFiles; } else if ((f.data_type === 'image' || f.data_type === 'file') && val instanceof File) { - const fileMeta = await client.uploadFile(val, { activityId: currentActivityId, fieldId: f.id }); + const fileMeta = await client.uploadFile(val, { activityId: currentActivityId, fieldId: f.id, instanceId: currentInstanceId }); payload[f.id] = [fileMeta]; } else { payload[f.id] = val; diff --git a/src/components/forms/fields/SmartGridField.tsx b/src/components/forms/fields/SmartGridField.tsx index d59749b..72e1311 100644 --- a/src/components/forms/fields/SmartGridField.tsx +++ b/src/components/forms/fields/SmartGridField.tsx @@ -2,8 +2,10 @@ import { useState, useRef } from 'react'; import { Button } from '../../buttons/Button'; import { Select } from '../../reusable/Select'; import { Input } from '../../reusable/Input'; -import { Trash2, Pencil } from 'lucide-react'; +import { Pencil } from 'lucide-react'; import type { FormScreenField } from '../../../api/types'; +import { PotentialMiningTable } from '../../dv/PotentialMiningTable'; +import { GridTable } from '../../dv/GridTable'; export function SmartGridField({ label, @@ -43,6 +45,9 @@ export function SmartGridField({ setEditingIdx(idx); setNewRow({ ...value[idx] }); setIsModalOpen(true); + setTimeout(() => { + formRef.current?.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); + }, 150); }; const startAdd = () => { @@ -101,6 +106,18 @@ export function SmartGridField({ row['row_kgs'] = skuVal * bagsVal; } + // Auto-calculate difference for potential mining + const potentialCol = columns.find(c => c.id.toLowerCase().includes('potential') || c.name.toLowerCase().includes('potential')); + const totalOrderedCol = columns.find(c => c.id.toLowerCase().includes('total_ordered') || c.name.toLowerCase().includes('total ordered')); + if (potentialCol && totalOrderedCol) { + const p = Number(row[potentialCol.id]) || 0; + const t = Number(row[totalOrderedCol.id]) || 0; + const diffCol = columns.find(c => c.id.toLowerCase().includes('difference') || c.name.toLowerCase().includes('difference')); + if (diffCol) { + row[diffCol.id] = t - p; + } + } + setNewRow(row); }; @@ -152,82 +169,59 @@ export function SmartGridField({ setEditingIdx(null); }; + const isPotentialGrid = label.toLowerCase().includes('potential'); + const hasOrderedColumn = columns.some(c => c.id.toLowerCase().includes('ordered') || (c.name || '').toLowerCase().includes('ordered')); + return (
{label} {value.length === 0 ? ( No rows added. - ) : ( -
- {value.map((row, i) => { - let productName = 'Unknown Product'; - let bags = '0'; + ) : isPotentialGrid ? ( + { + const reason = String(row.reason || row.Reason || row.potential_remarks || ''); + let badgeType: 'error' | 'success' | 'info' | 'warning' | 'default' = 'default'; + const lowerReason = reason.toLowerCase(); + if (lowerReason.includes('stock')) badgeType = 'success'; + else if (lowerReason.includes('quality')) badgeType = 'error'; + else if (lowerReason.includes('price')) badgeType = 'warning'; + else if (lowerReason.includes('competitive') || lowerReason.includes('loyalty')) badgeType = 'info'; + let productName = 'Unknown Product'; visibleColumns.forEach(col => { const isName = col.id.toLowerCase().includes('name') || col.name.toLowerCase().includes('name') || (col.id.toLowerCase().includes('category') && productName === 'Unknown Product'); - const isBags = col.id.toLowerCase().includes('bags') || col.name.toLowerCase().includes('bags') || col.id.toLowerCase().includes('quantity'); - - if (isName || isBags) { + if (isName) { const val = row[col.id]; let displayVal = String(val ?? '-'); - if (col.data_type === 'select' || col.data_type === 'multiselect') { const opt = col.properties?.options?.find(o => String(o.value) === String(val)); if (opt) displayVal = opt.label; } - - if (isName && (productName === 'Unknown Product' || col.id.toLowerCase().includes('name'))) { - productName = displayVal; - } - if (isBags) bags = displayVal; + productName = displayVal; } }); - return ( -
-
- {productName} - {label.toLowerCase().includes('potential') ? ( -
-
- Total Ordered: - {String(row.total_ordered ?? row.Total_Ordered ?? '-')} -
-
- Potential: - {String(row.actual_potential ?? row.Actual_Potential ?? row.store_potential ?? row.Store_Potential ?? '-')} -
-
- Difference: - {(() => { - const diff = Number(row.difference ?? row.Difference); - if (isNaN(diff)) return -; - return ( - - {diff < 0 ? `${Math.abs(diff)} Kgs Less Ordered` : `${diff} Kgs More Ordered`} - - ); - })()} -
-
- ) : ( - {bags} Bags - )} -
- -
- - -
-
- ); + return { + id: String(i), + productName, + badge: reason ? { label: reason, type: badgeType } : undefined, + potentialKgs: Number(row.actual_potential ?? row.Actual_Potential ?? row.store_potential ?? row.Store_Potential ?? row.quantity ?? row.Quantity ?? 0), + orderedKgs: Number(row.total_ordered ?? row.Total_Ordered ?? 0), + }; })} -
+ onEdit={(item) => startEdit(Number(item.id))} + onDelete={(item) => removeRow(Number(item.id))} + /> + ) : ( + )}
diff --git a/src/components/reusable/AnalyticsChart.tsx b/src/components/reusable/AnalyticsChart.tsx index aebc741..b9df0cb 100644 --- a/src/components/reusable/AnalyticsChart.tsx +++ b/src/components/reusable/AnalyticsChart.tsx @@ -6,12 +6,14 @@ import { YAxis, CartesianGrid, Tooltip, - ResponsiveContainer + ResponsiveContainer, + Legend } from 'recharts'; export interface ChartRow { dimension: string; value: number; + series?: string; } export interface ChartConfig { @@ -37,19 +39,42 @@ export function AnalyticsChart({ data }: AnalyticsChartProps) { .replace(/_/g, ' ') .replace(/\b\w/g, c => c.toUpperCase()); - // Safely format dimension string (fallbacks for empty strings) - const formattedRows = (chart.rows || []).map(r => ({ - ...r, - dimension: String(r.dimension || '').trim() || 'Unknown' - })); + const hasSeries = chart.rows?.some(r => r.series); + let finalData: any[] = []; + let seriesKeys: string[] = []; - if (formattedRows.length === 0) return null; + if (hasSeries) { + const grouped = new Map(); + const sKeys = new Set(); + chart.rows?.forEach(r => { + const dim = String(r.dimension || '').trim() || 'Unknown'; + const s = String(r.series || '').trim() || 'Unknown'; + sKeys.add(s); + + if (!grouped.has(dim)) { + grouped.set(dim, { dimension: dim }); + } + const entry = grouped.get(dim); + entry[s] = Number(r.value || 0); + }); + finalData = Array.from(grouped.values()); + seriesKeys = Array.from(sKeys).sort(); // Sort series keys for consistency + } else { + finalData = (chart.rows || []).map(r => ({ + ...r, + dimension: String(r.dimension || '').trim() || 'Unknown', + value: Number(r.value || 0) + })); + seriesKeys = ['value']; + } + + if (finalData.length === 0) return null; return (
- + - + {hasSeries && } + {seriesKeys.map((key, i) => ( + + ))}
diff --git a/src/components/reusable/Card.tsx b/src/components/reusable/Card.tsx index 6f29d67..3ccdcac 100644 --- a/src/components/reusable/Card.tsx +++ b/src/components/reusable/Card.tsx @@ -40,15 +40,15 @@ export function Card({ return (
{title && ( -
-

{title}

+
+

{title}

{action}
)} diff --git a/src/components/reusable/Modal.tsx b/src/components/reusable/Modal.tsx index abdda9e..1d719e7 100644 --- a/src/components/reusable/Modal.tsx +++ b/src/components/reusable/Modal.tsx @@ -5,12 +5,6 @@ import { cn } from '../../lib/cn'; export type ModalWidth = 'sm' | 'md' | 'lg' | 'xl'; -const WIDTH: Record = { - sm: 'max-w-[480px]', - md: 'max-w-[640px]', - lg: 'max-w-[820px]', - xl: 'max-w-[1000px]', -}; export interface ModalProps { open: boolean; @@ -23,6 +17,13 @@ export interface ModalProps { children?: ReactNode; } +const WIDTH_CLASSES: Record = { + sm: 'sm:w-[30vw]', + md: 'sm:w-[50vw]', + lg: 'sm:w-[70vw]', + xl: 'sm:w-[90vw]', +}; + /** Portal modal host — backdrop, Esc / click-out close, scroll-locked body. */ export function Modal({ open, onClose, title, subtitle, width = 'md', actions, children }: ModalProps) { useEffect(() => { @@ -43,21 +44,30 @@ export function Modal({ open, onClose, title, subtitle, width = 'md', actions, c return createPortal(
{ if (e.target === e.currentTarget) onClose(); }} > +
-
+
{title &&

{title}

} {subtitle &&
{subtitle}
} diff --git a/src/components/reusable/Pagination.tsx b/src/components/reusable/Pagination.tsx index c7be302..7891cd4 100644 --- a/src/components/reusable/Pagination.tsx +++ b/src/components/reusable/Pagination.tsx @@ -1,5 +1,6 @@ import { ChevronLeft, ChevronRight } from 'lucide-react'; import { Button } from '../buttons/Button'; +import { useState, useEffect } from 'react'; export interface PaginationProps { /** 1-based current page. */ @@ -8,33 +9,83 @@ export interface PaginationProps { /** Total rows across all pages. */ total: number; onPage: (page: number) => void; + onPageSizeChange?: (size: number) => void; } -/** Pager: "Showing a–b of N" + Prev/Next. Renders nothing when the set fits on - * one page. Shared by the record-view tables. */ -export function Pagination({ page, pageSize, total, onPage }: PaginationProps) { +/** Pager: "Showing a–b of N" + Rows per page + Jump to + Prev/Next. + * Shared by the record-view tables. */ +export function Pagination({ page, pageSize, total, onPage, onPageSizeChange }: PaginationProps) { + const [jumpPage, setJumpPage] = useState(String(page)); + + useEffect(() => { + setJumpPage(String(page)); + }, [page]); + if (total <= 0) return null; const totalPages = Math.max(1, Math.ceil(total / pageSize)); const safePage = Math.min(Math.max(page, 1), totalPages); const start = (safePage - 1) * pageSize; + const handleJump = () => { + const p = parseInt(jumpPage, 10); + if (!isNaN(p) && p >= 1 && p <= totalPages) { + onPage(p); + } else { + setJumpPage(String(safePage)); + } + }; + return ( -
- - Showing {start + 1}–{Math.min(start + pageSize, total)} of {total} - -
- - - {safePage} / {totalPages} +
+
+ + Showing {start + 1}–{Math.min(start + pageSize, total)} of {total} - + {onPageSizeChange && ( +
+ Rows per page: + +
+ )} +
+ +
+
+ Jump to: + setJumpPage(e.target.value)} + onKeyDown={e => e.key === 'Enter' && handleJump()} + onBlur={handleJump} + className="w-14 h-7 rounded border border-border-default bg-card px-2 py-1 text-xs text-strong text-center outline-none focus-ring" + style={{ MozAppearance: 'textfield' }} // best effort hide spinner on FF + /> +
+ +
+ + + {safePage} / {totalPages} + + +
); diff --git a/src/components/reusable/StatsTiles.tsx b/src/components/reusable/StatsTiles.tsx index ea6a3eb..94f8c3a 100644 --- a/src/components/reusable/StatsTiles.tsx +++ b/src/components/reusable/StatsTiles.tsx @@ -1,19 +1,10 @@ -import { Card } from "./Card"; +import { Phone, TrendingUp, Activity, BarChart2, ShoppingCart, ShoppingBag, Scale } from 'lucide-react'; import type { TileItem } from "../../api/types"; export interface StatsTilesProps { tiles?: TileItem[]; } -const colors = [ - "from-blue-500 to-cyan-500", - "from-emerald-500 to-green-500", - "from-orange-500 to-amber-500", - "from-violet-500 to-fuchsia-500", - "from-pink-500 to-rose-500", - "from-indigo-500 to-blue-500", -]; - export function StatsTiles({ tiles }: StatsTilesProps) { if (!tiles?.length) return null; @@ -24,35 +15,41 @@ export function StatsTiles({ tiles }: StatsTilesProps) { .replace(/_/g, " ") .replace(/\b\w/g, (c) => c.toUpperCase()); - const gradient = colors[idx % colors.length]; + const lowerKey = String(tile.key).toLowerCase(); + let Icon = BarChart2; + if (lowerKey.includes('call') || lowerKey.includes('total')) Icon = Phone; + if (lowerKey.includes('productive')) Icon = TrendingUp; + if (lowerKey.includes('order') || lowerKey.includes('cart')) Icon = ShoppingCart; + if (lowerKey.includes('bag')) Icon = ShoppingBag; + if (lowerKey.includes('kg') || lowerKey.includes('weight')) Icon = Scale; + if (lowerKey.includes('active')) Icon = Activity; + + const isDanger = lowerKey.includes('no_order'); return ( - - {/* Top Gradient */} -
- - {/* Background Decoration */} -
- -
-

+

+ {displayLabel} -

- -

- {(tile.value as React.ReactNode) ?? "-"} -

- -
+ +
- + +

+ {(tile.value as React.ReactNode) ?? "-"} +

+
); })}
diff --git a/src/components/rv/DailyLogsView.tsx b/src/components/rv/DailyLogsView.tsx index 780b5dd..c8aa516 100644 --- a/src/components/rv/DailyLogsView.tsx +++ b/src/components/rv/DailyLogsView.tsx @@ -4,7 +4,7 @@ import { RecordView } from './RecordView'; import type { WiredRecordViewProps } from './OrdersView'; /** Daily Logs record view (Daily Reports workflow). */ -export function DailyLogsView({ onRowClick, pageSize, headerActions, rowActions, refreshKey }: WiredRecordViewProps) { +export function DailyLogsView({ onRowClick, pageSize, headerActions, rowActions, refreshKey, presetAlias }: WiredRecordViewProps) { return ( ); } diff --git a/src/components/rv/OrdersView.tsx b/src/components/rv/OrdersView.tsx index 8fab1b2..a276d57 100644 --- a/src/components/rv/OrdersView.tsx +++ b/src/components/rv/OrdersView.tsx @@ -9,10 +9,11 @@ export interface WiredRecordViewProps { rowActions?: (row: Record) => React.ReactNode; refreshKey?: number; initialFilters?: Record; + presetAlias?: string; } /** Orders record view (Order Booking workflow). */ -export function OrdersView({ onRowClick, pageSize, headerActions, rowActions, refreshKey, initialFilters }: WiredRecordViewProps) { +export function OrdersView({ onRowClick, pageSize, headerActions, rowActions, refreshKey, initialFilters, presetAlias }: WiredRecordViewProps) { return ( ); } diff --git a/src/components/rv/RecordView.tsx b/src/components/rv/RecordView.tsx index 9c94469..e66d418 100644 --- a/src/components/rv/RecordView.tsx +++ b/src/components/rv/RecordView.tsx @@ -41,6 +41,8 @@ export interface RecordViewProps { sortDir?: 'asc' | 'desc'; /** Default filters to apply initially. */ initialFilters?: Record; + /** Custom preset alias to send with the request (e.g. preset_alias: my_orders) */ + presetAlias?: string; } /** @@ -53,7 +55,7 @@ export function RecordView({ rvUid, title = 'Records', columns, - pageSize = 50, + pageSize = 25, onRowClick, rowKey, headerActions, @@ -63,7 +65,9 @@ export function RecordView({ sortDir, omitColumns, initialFilters = {}, + presetAlias, }: RecordViewProps) { + const [internalPageSize, setInternalPageSize] = useState(pageSize); const [page, setPage] = useState(1); const [search, setSearch] = useState(''); const [debounced, setDebounced] = useState(''); @@ -103,11 +107,12 @@ export function RecordView({ try { const r = await client.recordView(rvUid, { page, - limit: pageSize, + limit: internalPageSize, search: debounced, filters: filtersParam, sortBy, - sortDir + sortDir, + presetAlias }); console.log("RECORD VIEW RESP:", r); if (live) setResp(r); @@ -121,7 +126,7 @@ export function RecordView({ return () => { live = false; }; - }, [client, rvUid, page, pageSize, debounced, filtersParam, refreshKey, sortBy, sortDir]); + }, [client, rvUid, page, internalPageSize, debounced, filtersParam, refreshKey, sortBy, sortDir]); const fields: RecordViewField[] = useMemo(() => { const all = resp?.config.fields ?? []; @@ -157,7 +162,7 @@ export function RecordView({ action={headerActions} pad={false} > -
+
{(() => { if (!resp?.config) return null; @@ -232,7 +237,7 @@ export function RecordView({
- + {fields.map((f) => ( - ))} + {fields.map((f) => { + const lowerKey = f.field_key.toLowerCase(); + const isSku = lowerKey === 'sku_code'; + const isStatus = lowerKey.includes('status') || lowerKey.includes('state'); + const valStr = formatValue(row[f.field_key], f.field_key); + + let content = <>{valStr}; + + if (valStr !== '—' && valStr !== '') { + if (isSku || isStatus) { + const palettes = [ + "bg-blue-50 text-blue-700 border-blue-200", + "bg-purple-50 text-purple-700 border-purple-200", + "bg-pink-50 text-pink-700 border-pink-200", + "bg-indigo-50 text-indigo-700 border-indigo-200", + "bg-cyan-50 text-cyan-700 border-cyan-200", + "bg-rose-50 text-rose-700 border-rose-200", + "bg-fuchsia-50 text-fuchsia-700 border-fuchsia-200", + "bg-teal-50 text-teal-700 border-teal-200", + ]; + + let hash = 0; + for (let i = 0; i < valStr.length; i++) { + hash = valStr.charCodeAt(i) + ((hash << 5) - hash); + } + const colorIndex = Math.abs(hash) % palettes.length; + let colorClass = palettes[colorIndex]; + + if (isSku) { + content = ( + + {valStr} + + ); + } else if (isStatus) { + const valLower = valStr.toLowerCase(); + const isSuccess = valLower.includes('approve') || valLower.includes('complete') || valLower.includes('success') || valLower.includes('active'); + const isWarning = valLower.includes('pending') || valLower.includes('draft') || valLower.includes('hold'); + const isDanger = valLower.includes('reject') || valLower.includes('fail') || valLower.includes('cancel'); + + if (isSuccess) colorClass = "bg-emerald-50 text-emerald-700 border-emerald-200"; + else if (isWarning) colorClass = "bg-amber-50 text-amber-700 border-amber-200"; + else if (isDanger) colorClass = "bg-red-50 text-red-700 border-red-200"; + + content = ( + + {valStr} + + ); + } + } + } + + return ( + + ); + })} {rowActions && (
onRowClick(row, i) : undefined} className={cn( - 'bg-surface border-b border-border-subtle transition-colors duration-150', - onRowClick && 'cursor-pointer hover:bg-slate-50', + 'bg-[var(--z-rv-table-bg)] border-b border-[var(--z-rv-table-border)] transition-colors duration-150', + onRowClick && 'cursor-pointer hover:bg-[var(--table-row-bg-select)]', )} > - {fields.map((f) => ( - - {formatValue(row[f.field_key], f.field_key)} - + {content} +
e.stopPropagation()}> @@ -276,7 +335,16 @@ export function RecordView({
)} - + { + setInternalPageSize(sz); + setPage(1); + }} + />
); diff --git a/src/screens/CallsPage.tsx b/src/screens/CallsPage.tsx index 5488583..94172a1 100644 --- a/src/screens/CallsPage.tsx +++ b/src/screens/CallsPage.tsx @@ -117,44 +117,50 @@ export function CallsPage() { onClose={() => navigate(`/calls`)} title={instanceId != null ? `Call #${instanceId}` : undefined} width="lg" - actions={ - <> - - {(() => { - const stateName = String(selectedRow?.current_state_name || selectedRow?.current_state_name_ || selectedRow?.current_state || selectedRow?.status || '').toLowerCase(); - if (stateName.includes('ordered') || stateName === 'ordered' || stateName.includes('order')) { - return ( - - ); - } else if (stateName.includes('productive')) { - return ( - - ); - } - return ( - - ); - })()} - - } - > - {instanceId != null && } + {instanceId != null && selectedRow && ( +
+ + {miningLoading ? 'Loading...' : 'Potential Mining'} + + } + placeOrderAction={ + (() => { + const stateName = String(selectedRow.current_state_name || selectedRow.current_state_name_ || selectedRow.current_state || selectedRow.status || '').toLowerCase(); + if (stateName.includes('ordered') || stateName === 'ordered' || stateName.includes('order')) { + return ( + + ); + } else if (stateName.includes('productive')) { + return ( + + ); + } + return ( + + ); + })() + } + /> +
+ )} setActiveActivity(null)} title={activeActivity?.name} - width="md" + width={activeActivity?.id === ORDER_BOOKING.activities.POTENTIAL_MINING.uid ? 'lg' : 'md'} > {activeActivity && instanceId != null && ( . */ export function ConsoleLayout() { const { authed, logout } = useAuth(); const navigate = useNavigate(); + const location = useLocation(); const user = orderBookingClient.currentUser(); + const isReportsActive = location.pathname.startsWith('/reports'); + // A 401 from any workflow client bounces back to login. useEffect(() => { onAuthErrorAll(() => { @@ -26,10 +30,9 @@ export function ConsoleLayout() { return (
-
+
- Krishna Sales - Field Sales · Sandbox {APP_ID} + Krishna Sales
- {user?.name && {user.name}} + {user?.name && {user.name}} } - rowActions={(row) => ( - - )} + rowActions={(row) => { + const state = String(row.current_state_name || row.current_state_name_ || row.current_state || row.status || '').toLowerCase(); + // Show only for Punched In, hide for Punched Out or others + if (state.includes('out') || !state.includes('in')) return null; + + return ( + + ); + }} /> - {instanceId != null && } + {instanceId != null && setPunchOutInstanceId(instanceId)} />} ); diff --git a/src/screens/DailySalesReportPage.tsx b/src/screens/DailySalesReportPage.tsx index f1107a5..5074685 100644 --- a/src/screens/DailySalesReportPage.tsx +++ b/src/screens/DailySalesReportPage.tsx @@ -97,22 +97,6 @@ export function DailySalesReportPage() { const blob = doc.output('blob'); - // Attempt mobile native share (which includes OS-level Print) - if (navigator.share && navigator.canShare) { - const file = new File([blob], `Daily_Sales_Report_${date || 'Draft'}.pdf`, { type: 'application/pdf' }); - try { - if (navigator.canShare({ files: [file] })) { - await navigator.share({ - files: [file], - title: 'Daily Sales Report', - }); - return; - } - } catch (err) { - console.warn('Share failed or was cancelled', err); - } - } - // Fallback for PC / unsupported browsers const blobUrl = URL.createObjectURL(blob); const iframe = document.createElement('iframe'); diff --git a/src/screens/MyOrdersPage.tsx b/src/screens/MyOrdersPage.tsx index 25df7fc..fe66ccb 100644 --- a/src/screens/MyOrdersPage.tsx +++ b/src/screens/MyOrdersPage.tsx @@ -23,6 +23,7 @@ export function MyOrdersPage() { <> { const id = row.instance_id as number | string | undefined; diff --git a/src/screens/ReportPage.tsx b/src/screens/ReportPage.tsx new file mode 100644 index 0000000..d4dc6e7 --- /dev/null +++ b/src/screens/ReportPage.tsx @@ -0,0 +1,33 @@ +import { useParams } from 'react-router-dom'; +import { RecordView } from '../components/rv/RecordView'; +import { ORDER_BOOKING } from '../api/config'; +import { orderBookingClient } from '../api/clients'; + +export const REPORT_MAP: Record = { + 'product-wise-orders': { uid: ORDER_BOOKING.recordViews.PRODUCT_WISE_ORDERS, title: 'Product Wise Orders' }, + 'route-wise-orders': { uid: ORDER_BOOKING.recordViews.ROUTE_WISE_ORDERS, title: 'Route Wise Orders' }, + 'so-wise-orders': { uid: ORDER_BOOKING.recordViews.SO_WISE_ORDERS, title: 'SO Wise Orders' }, + 'so-wise-visits': { uid: ORDER_BOOKING.recordViews.SO_WISE_VISITS, title: 'SO Wise Visits' }, + 'yearly-orders': { uid: ORDER_BOOKING.recordViews.YEARLY_ORDERS, title: 'Yearly Orders' }, + 'monthly-orders': { uid: ORDER_BOOKING.recordViews.MONTHLY_ORDERS, title: 'Monthly Orders' }, + 'weekly-orders': { uid: ORDER_BOOKING.recordViews.WEEKLY_ORDERS, title: 'Weekly Orders' }, + 'store-wise-products': { uid: ORDER_BOOKING.recordViews.STORE_WISE_PRODUCTS, title: 'Store Wise Products' }, +}; + +export function ReportPage() { + const { reportType } = useParams<{ reportType: string }>(); + const report = reportType ? REPORT_MAP[reportType] : null; + + if (!report) { + return
Report not found
; + } + + return ( + + ); +} diff --git a/src/styles/styles.css b/src/styles/styles.css index e3d6ef0..59a15b5 100644 --- a/src/styles/styles.css +++ b/src/styles/styles.css @@ -65,19 +65,19 @@ --color-slate-50: var(--slate-50); /* ---- Semantic aliases (short, readable utility names) ---- */ - --color-app: var(--surface-app); - --color-card: var(--surface-card); - --color-sunk: var(--surface-sunk); - --color-strong: var(--text-strong); - --color-body: var(--text-body); - --color-muted: var(--text-muted); - --color-faint: var(--text-faint); - --color-on-navy: var(--text-on-navy); - --color-on-navy-muted: var(--text-on-navy-muted); - --color-accent: var(--text-accent); - --color-link: var(--text-link); - --color-border-subtle: var(--border-subtle); - --color-border-default: var(--border-default); + --color-app: var(--z-body-bg); + --color-card: var(--z-block-bg); + --color-sunk: var(--z-bg-plain); + --color-strong: var(--z-text-default); + --color-body: var(--z-text-default); + --color-muted: var(--z-text-muted); + --color-faint: var(--z-text-secondary); + --color-on-navy: var(--z-text-inverse); + --color-on-navy-muted: var(--z-text-secondary); + --color-accent: var(--z-text-primary); + --color-link: var(--z-text-primary); + --color-border-subtle: var(--z-border-default); + --color-border-default: var(--z-border-secondary); /* ---- Radii ---- */ --radius-xs: var(--radius-xs); @@ -148,9 +148,9 @@ .nums { font-variant-numeric: tabular-nums; } - /* Sunrise focus ring on inputs/selects. */ + /* Primary focus ring on inputs/selects. */ .focus-ring:focus-within { - border-color: var(--sunrise-500); + border-color: var(--z-btn-primary-bg); box-shadow: var(--shadow-focus); } /* Slim, theme-tinted scrollbar for in-panel scroll areas. */ @@ -175,3 +175,321 @@ background-color: var(--text-faint); } } +/* root variable */ + +:root { + --primary-color: #1d4ed8; + --secondary-color: #f9fafb; + --font-color: #10182b; + --font-family: "IBM Plex Sans", serif; + --light-font-color: #667085; + --body-bg: #f0f4f8; + --bg-plain: #fff; + --bg-active: #00000014; + --border: #e5e7eb; + --shadow: 0px 4px 8px 0px rgba(228, 231, 236, 0.3); + --hover: #e4e4e4; + --hover-font: #fff; + --hover-bg: #f9fafb; + --hover-border: #667085; + --menu-item-bg: rgba(255, 255, 255, 0.1); + + + /* Navbar background color */ + --nav-bg-color: #fff; + --nav-item-color: #10182b; + --nav-item-active: #1b84ff; + --nav-item-active-bg: #c0d4ed45; + --nav-item-border-bottom: transparent; + --nav-dropdown-bg: #fff; + + /* Matching the navbar color */ + --nav-dropdown-color: #4B5675; + --nav-dropdown-active: #2e56e1; + --nav-dropdown-active-bg: #F9F9F9; + --nav-dropdown-border: none; + --nav-dropdown-radius: 4px; + --nav-dropdown-padding: 10px; + --menu-item-radius: 4px; + --p-8: 0px; + --nav-border: rgba(255, 255, 255, 0.1); + --nav-left-separator-line: #484848; + + /* tiles card */ + --tiles-card-bg: #fff; + --tiles-card-border: #e5e7eb; + --tiles-font-color: #10182b; + --tiles-font-size: 16px; + --tiles-count-size: 20px; + + /* button */ + --z-btn-primary-bg: #0058be; + --z-btn-primary-color: #fff; + --primary-btn-active: #196ed0; + --primary-btn-activeClr: #fff; + --z-btn-secondary-bg: #f4f4f4; + --z-btn-secondary-color: #10182b; + --secondary-btn-active-bg: #eaeaea; + --z-btn-outline-color: #0058be; + --z-btn-outline-border: #0058be; + + --button-height: 36px; + --z-btn-sm-height: 32px; + --z-btn-md-height: 40px; + --z-btn-lg-height: 48px; + + /* blocks */ + --block-bg: #fff; + --block-padding: 20px; + --block-radius: 20px; + --block-border: #e5e7eb; + /* --block-shadow: 0px 4px 8px 0px rgba(228, 231, 236, 0.3); */ + --block-shadow: 0 1px 3px 0 var(--tw-shadow-color, #0000001a), 0 1px 2px -1px var(--tw-shadow-color, #0000001a); + + /* button or input */ + --button-radius: 8px; + --input-shadow: 0px 4px 8px 0px rgba(228, 231, 236, 0.3); + --input-border: #e5e7eb; + + /* font-size */ + --small-font: 14px; + --medium-font: 16px; + --large-font: 20px; + --z-btn-font-sm: 12px; + --z-btn-font-md: 14px; + --z-btn-font-lg: 16px; + + --small-icon: 24px; + + /* record view table color */ + --rv-table-bg: #fff; + --rv-table-header: #f9fafb; + --table-row-bg-select: #fafafa; + --table-tr-active: #f9fafb; + --rv-table-height: 40px; + --rv-table-btn-height: 36px; + --table-td-padding: 8px 20px; + + /* form input tags */ + --form-input-height: 40px; + --form-bg: #fff; + --form-input-bg: #fff; + --inputBox-border: ##e5e7eb; + --inputBox-border-active: #a2acbf; + --disable-bg: #fafafa; + --disable-color: #98a2b3; + + --toolTip-color: #2185d0; + --toolTip-active-clr: #208fe3; + + /* new template style variables */ + --z-body-bg: #f0f4f8; + --z-bg-plain: #fff; + + --z-bg-primary: #0058be; + --z-bg-secondary: #f4f4f4; + --z-bg-success: #1fc16b; + --z-bg-error: #fa3748; + --z-bg-light: #ffffff; + --z-bg-dark: #1e2227; + + --z-text-default: #10182b; + --z-text-primary: #0058be; + --z-text-secondary: #6c757d; + --z-text-success: #fff; + --z-text-error: #fff; + --z-text-muted: #6c757d; + --z-text-inverse: #ffffff; + --z-text-plain: #fff; + + + --z-bg-active: #3d94fb; + + --z-border-primary: #0058be; + --z-border-secondary: #ced4da; + --z-border-success: #1fc16b; + --z-border-error: #fa3748; + + --z-shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05); + --z-shadow-md: 0 4px 6px rgba(0, 0, 0, 0.1); + --z-shadow-lg: 0 10px 20px rgba(0, 0, 0, 0.15); + + --z-font-size-xs: 10px; + --z-font-size-sm: 12px; + --z-font-size-md: 14px; + --z-font-size-lg: 16px; + --z-font-size-xl: 20px; + + --z-border-size-sm: 1px; + --z-border-size-md: 2px; + --z-border-size-lg: 4px; + + --z-border-radius-sm: 4px; + --z-border-radius-md: 8px; + --z-border-radius-lg: 16px; + --z-border-radius-full: 50%; + --z-border-radius-pill: 30px; + + /* app default text */ + --z-font-sm: 14px; + --z-font-md: 16px; + --z-font-lg: 20px; + + /* button style */ + --z-btn-radius: 8px; + --z-btn-height: 44px; + + /* block and section styles */ + --z-block-bg: #fff; + --z-block-radius: 8px; + --z-block-border: #e5e7eb; + --z-block-shadow: box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); + + /* record view table */ + --z-rv-table-header: #fff; + --z-rv-td-padding: 8px 20px; + --z-rv-table-border: #e5e7eb; + --z-rv-td-height: 40px; + + --z-tile-bg: #fff; + --z-tile-border: #e5e7eb; + + /*-----------------New BG Color Palatte----------------*/ + + /* Primary Colors */ + --z-bg-primary-100: #c0d4ed; + --z-bg-primary-200: #83b6f4; + --z-bg-primary-300: #5aa2f8; + --z-bg-primary-400: #0058be; + + /* Secondary Colors */ + --z-bg-secondary-100: #ffce8d; + --z-bg-secondary-200: #ffbb62; + --z-bg-secondary-300: #faa73b; + --z-bg-secondary-400: #f5f6f7; + + /* Gradient Colors */ + --z-bg-gradient-1: linear-gradient(90deg, #0058be 0%, #104F99 100%); + --z-bg-gradient-2: linear-gradient(90deg, #1C38A7 0%, #2E55DF 100%); + --z-bg-gradient-3: linear-gradient(94.13deg, #83B6F4 2.64%, #2B6BBA 97.36%); + --z-bg-gradient-4: linear-gradient(99.74deg, #A0C5F4 4.16%, #EFF3F8 97.03%); + + /* Neutral Colors */ + --z-bg-neutral-100: #ffffff; + --z-bg-neutral-200: #f9fafb; + --z-bg-neutral-300: #e5e7eb; + --z-bg-neutral-400: #d0d5dd; + --z-bg-neutral-500: #98a2b3; + --z-bg-neutral-600: #667085; + --z-bg-neutral-700: #475467; + --z-bg-neutral-800: #1d2939; + --z-bg-neutral-900: #10182b; + --z-bg-neutral-1000: #0c1323; + + /* Danger Colors */ + --z-bg-danger-100: #fecdca; + --z-bg-danger-200: #fda29b; + --z-bg-danger-300: #f97066; + --z-bg-danger-400: #f04438; + + /* Warning Colors */ + --z-bg-warning-100: #ffdf89; + --z-bg-warning-200: #fec84b; + --z-bg-warning-300: #ffdb43; + --z-bg-warning-400: #dfb400; + + /* Success Colors */ + --z-bg-success-100: oklch(97.9% .021 166.113); + --z-bg-success-200: #6ce9a6; + --z-bg-success-300: #32d584; + --z-bg-success-400: #12b76a; + + /*-----------------New Text Color Palatte----------------*/ + + /* Primary Colors */ + --z-text-primary-100: #c0d4ed; + --z-text-primary-200: #83b6f4; + --z-text-primary-300: #5aa2f8; + --z-text-primary-400: #0058be; + + /* Secondary Colors */ + --z-text-secondary-100: #ffce8d; + --z-text-secondary-200: #ffbb62; + --z-text-secondary-300: #faa73b; + --z-text-secondary-400: #f79009; + + /* Neutral Colors */ + --z-text-neutral-100: #ffffff; + --z-text-neutral-200: #f9fafb; + --z-text-neutral-300: #e5e7eb; + --z-text-neutral-400: #d0d5dd; + --z-text-neutral-500: #98a2b3; + --z-text-neutral-600: #667085; + --z-text-neutral-700: #475467; + --z-text-neutral-800: #1d2939; + --z-text-neutral-900: #10182b; + --z-text-neutral-1000: #0c1323; + + /* Danger Colors */ + --z-text-danger-100: #fecdca; + --z-text-danger-200: #fda29b; + --z-text-danger-300: #f97066; + --z-text-danger-400: #f04438; + + /* Warning Colors */ + --z-text-warning-100: #ffdf89; + --z-text-warning-200: #fec84b; + --z-text-warning-300: #ffdb43; + --z-text-warning-400: #dfb400; + + /* Success Colors */ + --z-text-success-100: #a6f4c5; + --z-text-success-200: #6ce9a6; + --z-text-success-300: #32d584; + --z-text-success-400: #12b76a; + + /*-----------------New Border Color Palatte----------------*/ + + /* Primary Colors */ + --z-border-default: #e5e7eb; + --z-border-primary-100: #c0d4ed; + --z-border-primary-200: #83b6f4; + --z-border-primary-300: #5aa2f8; + --z-border-primary-400: #0058be; + + /* Secondary Colors */ + --z-border-secondary-100: #ffce8d; + --z-border-secondary-200: #f3cfc4; + --z-border-secondary-300: #ebaf9d; + --z-border-secondary-400: #f79009; + + /* Neutral Colors */ + --z-border-neutral-100: #ffffff; + --z-border-neutral-200: #f9fafb; + --z-border-neutral-300: #e5e7eb; + --z-border-neutral-400: #d0d5dd; + --z-border-neutral-500: #98a2b3; + --z-border-neutral-600: #667085; + --z-border-neutral-700: #475467; + --z-border-neutral-800: #1d2939; + --z-border-neutral-900: #10182b; + --z-border-neutral-1000: #0c1323; + + /* Danger Colors */ + --z-border-danger-100: #fecdca; + --z-border-danger-200: #fda29b; + --z-border-danger-300: #f97066; + --z-border-danger-400: #f04438; + + /* Warning Colors */ + --z-border-warning-100: #ffdf89; + --z-border-warning-200: #fec84b; + --z-border-warning-300: #ffdb43; + --z-border-warning-400: #dfb400; + + /* Success Colors */ + --z-border-success-100: #a6f4c5; + --z-border-success-200: #6ce9a6; + --z-border-success-300: #32d584; + --z-border-success-400: #12b76a; +} diff --git a/src/styles/tokens/colors.css b/src/styles/tokens/colors.css index 8dc06b6..a05e6e4 100644 --- a/src/styles/tokens/colors.css +++ b/src/styles/tokens/colors.css @@ -97,7 +97,7 @@ --action-primary-hover: var(--sunrise-600); --action-navy: var(--navy-900); --action-navy-hover: var(--navy-800); - --focus-ring: rgba(242,107,58,0.45); + --focus-ring: rgba(0, 88, 190, 0.45); /* Status */ --status-success: var(--emerald-600);