diff --git a/src/components/cards/CallCard.tsx b/src/components/cards/CallCard.tsx index 625554c..a19224d 100644 --- a/src/components/cards/CallCard.tsx +++ b/src/components/cards/CallCard.tsx @@ -1,18 +1,70 @@ +import { Store, MapPin, TrendingUp, Calendar, Clock, Info } from 'lucide-react'; import { formatValue } from '../../lib/format'; import { BASE_URL, APP_ID } from '../../api/config'; +import './card.css'; + +// { +// "42b7f47d-96f0-4289-a6d9-38f455ad57dd__user_id": { +// "email": "surya.c@getzino.com", +// "job_title": "", +// "name": "Surya C", +// "user_id": 29113 +// }, +// "current_state_id": "33d7ab1f-ddf4-4549-ac99-7d23172c4ca5", +// "current_state_name": "Productive Call", +// "date_of_visit": "2026-07-15", +// "instance_id": 6592, +// "is_productive_call": "yes", +// "select_store": { +// "area": "Dummy Area", +// "business_name": "Business Name 777", +// "complete_address": "Dummy notes for Complete Address", +// "distributor_email": "test@example.com", +// "distributor_name": "Banashree Multi Millet Flour", +// "distributor_owner_name": "Dummy Distributor Owner Name", +// "distributor_phone_number": { +// "dial_code": "+91", +// "phone": "9876543210", +// "phone_with_dial_code": "+919876543210" +// }, +// "email": "test@example.com", +// "instance_id": 6118, +// "notes": "Dummy notes for Notes", +// "owner_name": "Dummy Owner Name", +// "phone_number": { +// "dial_code": "+91", +// "phone": "9876543210", +// "phone_with_dial_code": "+919876543210" +// }, +// "pin_code": "100000", +// "potential": "[{\"row_kgs\": 0, \"quantity\": 20, \"product_category\": \"MAIDA SUPER VALUE\"}, {\"row_kgs\": 0, \"quantity\": 30, \"product_category\": \"SUJI\"}, {\"row_kgs\": 0, \"quantity\": 10, \"product_category\": \"FORTIFIED MAIDA\"}, {\"row_kgs\": 0, \"quantity\": 300, \"product_category\": \"ATTA REGULAR\"}]", +// "route_code": "k", +// "route_name": "Krishna", +// "store_code": "STR-00024", +// "store_image": "[{\"uuid\": \"a449f6cb-0dce-4e21-9599-ab4d3e47042b\", \"blob_path\": \"434/a449f6cb-0dce-4e21-9599-ab4d3e47042b.png\", \"mime_type\": \"image/png\", \"size_bytes\": 32174, \"original_name\": \"profile.png\"}]", +// "store_location": "{\"latitude\":12.9716,\"longitude\":77.5946}", +// "sub_route": "A" +// }, +// "time_of_visit": "13:02:00", +// "upload_image": null +// } export function CallCard({ row, isDetailView = false }: { row: Record; isDetailView?: boolean }) { - // Status - const stateName = String(row.status || row.current_state_name || 'Pending'); - const isProductive = stateName.toLowerCase().includes('productive') || stateName.toLowerCase().includes('completed') || stateName.toLowerCase().includes('success'); + // Status Mapping + const stateName = row.current_state_name; + const lowerState = stateName.toLowerCase(); - // Image - let imageObj = null; - const imgData = row.upload_image || row.store_image || row.image; - if (Array.isArray(imgData) && imgData.length > 0) { - imageObj = imgData[0]; - } else if (imgData && typeof imgData === 'object' && imgData.blob_path) { - imageObj = imgData; + let statusClass = 'z-card-status--neutral'; + if (lowerState.includes('productive call')) { + statusClass = 'z-card-status--success'; + } else if (lowerState.includes('close order') || lowerState.includes('completed') || lowerState.includes('success')) { + statusClass = 'z-card-status--success'; + } else if (lowerState.includes('no order')) { + statusClass = 'z-card-status--danger'; + } else if (lowerState.includes('visited')) { + statusClass = 'z-card-status--primary'; + } else { + statusClass = 'z-card-status--primary'; // default fallback for other active states } // Extract nested objects if they exist @@ -20,14 +72,43 @@ export function CallCard({ row, isDetailView = false }: { row: Record k.includes('user_id')); const userObj = userKey ? row[userKey] : null; + // Image + let imageObj = null; + let imgData = row.upload_image || storeObj?.store_image || row.store_image || row.image; + if (typeof imgData === 'string' && imgData.startsWith('[')) { + try { imgData = JSON.parse(imgData); } catch (e) { } + } + if (Array.isArray(imgData) && imgData.length > 0) { + imageObj = imgData[0]; + } else if (imgData && typeof imgData === 'object' && imgData.blob_path) { + imageObj = imgData; + } + // Store Details const storeName = formatValue(storeObj?.business_name || storeObj?.distributor_name || row.store_name || row.customer_name || (typeof row.store === 'string' ? row.store : 'Unknown Store')); const storeCode = formatValue(storeObj?.store_code || row.store_code || '—'); - // Grid Details - const totalWeight = formatValue(row.total_kgs || row.total_weight || row.weight || 0); - const totalQuantity = formatValue(row.total_bags || row.total_quantity || row.quantity || 0); - const totalOrders = formatValue(row.order_count || row.total_orders || 0); + // Grid Details (Calculate from potential if missing) + let totalWeightVal = Number(row.total_kgs || row.total_weight || row.weight || 0); + let totalQuantityVal = Number(row.total_bags || row.total_quantity || row.quantity || 0); + let totalOrdersVal = Number(row.order_count || row.total_orders || 0); + + if (totalWeightVal === 0 && totalQuantityVal === 0 && storeObj?.potential) { + try { + const pot = typeof storeObj.potential === 'string' ? JSON.parse(storeObj.potential) : storeObj.potential; + if (Array.isArray(pot)) { + totalOrdersVal = pot.length; + pot.forEach((p: any) => { + totalWeightVal += Number(p.row_kgs || 0); + totalQuantityVal += Number(p.quantity || 0); + }); + } + } catch (e) { } + } + + const totalWeight = formatValue(totalWeightVal); + const totalQuantity = formatValue(totalQuantityVal); + const totalOrders = formatValue(totalOrdersVal); // Route & Assignment const routeName = formatValue(storeObj?.route_name || storeObj?.route || row.route_name || row.route || '—'); @@ -46,8 +127,9 @@ export function CallCard({ row, isDetailView = false }: { row: Record= 2) { + let h = parseInt(timeParts[0]); + const m = timeParts[1]; + const ampm = h >= 12 ? 'PM' : 'AM'; + h = h % 12; + h = h ? h : 12; + const hStr = h < 10 ? '0' + h : h; + timeStr = `${hStr}:${m} ${ampm}`; } else { timeStr = t; } @@ -66,114 +158,127 @@ export function CallCard({ row, isDetailView = false }: { row: Record - {/* Header Status Bar */} -
-
- - {stateName} +
+ + {/* Top Row */} +
+
+ + {storeCode} +
+ + {stateName} + +
+ + {/* Store Name & Location */} +
+ +

{storeName}

+
+ +
+ +

{areaName}

+
+ + {/* Divider */} +
+ + {/* Date & Time */} +
+
+
DATE
+ {dateStr} +
+
+
TIME
+ {timeStr}
- {/* Main Content */} -
- {/* Store Details */} -
-
- Store Details -

{storeName}

- {storeCode !== '—' && ( -

- Code: {storeCode} -

- )} -
+ {/* Button (List View Only) */} + {!isDetailView && ( +
+ + See More Details +
+ )} - {isDetailView && imageObj && imageObj.uuid && ( -
- Store + + {storeCode !== '—' && ( +

+ Code: {storeCode} +

+ )} + + {imageObj && imageObj.uuid && ( +
+ Store { (e.target as HTMLImageElement).style.display = 'none'; }} />
)} -
- {/* Logistics Grid */} - {isDetailView && ( -
-
-

Total Weight

-

- {totalWeight} Kgs -

+ {/* Logistics Grid */} +
+
+

Total Weight

+

+ {totalWeight} Kgs +

+
+
+

Total Quantity

+

+ {totalQuantity} Bags +

+
+
+ Total Orders + + {totalOrders} {Number(totalOrders) === 1 ? 'Order' : 'Orders'} + +
-
-

Total Quantity

-

- {totalQuantity} Bags -

-
-
- Total Orders - - {totalOrders} {Number(totalOrders) === 1 ? 'Order' : 'Orders'} - -
-
- )} - {/* Route & Assignment */} - {isDetailView && ( -
-
-

Route

-

{routeName}

-
-
-

Area

-

{areaName}

-
-
-

Sales Officer

-
-
- {initials} -
-
-

{userName}

- {userEmail !== '—' && ( -

{userEmail}

- )} + {/* Route & Assignment */} +
+
+

Route

+

{routeName}

+
+
+

Area

+

{areaName}

+
+
+

Sales Officer

+
+
+ {initials} +
+
+

{userName}

+ {userEmail !== '—' && ( +

{userEmail}

+ )} +
-
- )} -
- {/* Footer Timeline */} -
-
- - {dateStr}
-
- - {timeStr} -
-
+ )}
); } diff --git a/src/components/cards/card.css b/src/components/cards/card.css new file mode 100644 index 0000000..48d2a46 --- /dev/null +++ b/src/components/cards/card.css @@ -0,0 +1,318 @@ +.z-card { + position: relative; + width: 100%; + background-color: var(--z-bg-neutral-100); + border-radius: var(--block-radius); + padding: var(--block-padding); + box-shadow: var(--block-shadow); + transition: all 0.2s ease-in-out; + border: 1px solid var(--z-border-neutral-300); + font-family: var(--font-sans); +} + +.z-card:hover { + box-shadow: var(--z-shadow-md); +} + +.z-card-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-bottom: 12px; +} + +.z-card-header-icon { + display: flex; + align-items: center; + gap: 6px; + color: var(--z-text-primary-300); + font-weight: 700; + font-size: var(--z-font-sm); +} + +.z-card-status { + padding: 4px 12px; + border-radius: var(--z-border-radius-pill); + font-size: var(--z-font-size-xs); + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.z-card-status--success { + background-color: var(--z-bg-success-100); + color: var(--z-text-success-400); +} + +.z-card-status--warning { + background-color: var(--z-bg-warning-100); + color: var(--z-text-warning-400); +} + +.z-card-status--danger { + background-color: var(--z-bg-danger-100); + color: var(--z-text-danger-400); +} + +.z-card-status--primary { + background-color: var(--z-bg-primary-100); + color: var(--z-text-primary-400); +} + +.z-card-status--neutral { + background-color: var(--z-bg-neutral-200); + color: var(--z-text-neutral-600); +} + +.z-card-store-row { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 8px; +} + +.z-card-store-icon { + color: var(--z-text-neutral-500); +} + +.z-card-store-name { + font-size: 17px; + font-weight: 700; + color: var(--z-text-neutral-900); + line-height: 1.2; +} + +.z-card-area-row { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 16px; +} + +.z-card-area-icon { + color: var(--z-text-neutral-700); +} + +.z-card-area-name { + font-size: var(--z-font-sm); + color: var(--z-text-neutral-700); +} + +.z-card-divider { + width: 100%; + height: 1px; + background-color: var(--z-border-neutral-300); + margin-bottom: 16px; +} + +.z-card-footer { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 20px; +} + +.z-card-footer-item { + display: flex; + flex-direction: column; + gap: 4px; +} + +.z-card-footer-label { + display: flex; + align-items: center; + gap: 6px; + color: var(--z-text-neutral-600); + font-size: var(--z-font-size-sm); + font-weight: 600; + text-transform: uppercase; +} + +.z-card-footer-value { + color: var(--z-text-neutral-900); + font-size: 15px; + font-weight: 700; + letter-spacing: -0.02em; +} + +.z-card-action-btn { + width: 100%; + background-color: var(--z-bg-neutral-200); + border-radius: var(--z-border-radius-lg); + padding: 10px 0; + display: flex; + align-items: center; + justify-content: center; + gap: 6px; + color: var(--z-text-primary-400); + font-weight: 600; + font-size: var(--z-font-sm); + transition: background-color 0.2s ease; + cursor: pointer; +} + +.z-card-action-btn:hover { + background-color: var(--z-bg-neutral-300); +} + +.z-card-details { + display: flex; + flex-direction: column; + gap: 20px; + padding-top: 12px; +} + +.z-card-meta { + font-size: 13px; + color: var(--z-text-neutral-500); + margin-top: 2px; +} + +.z-card-meta-code { + font-family: var(--font-mono); + font-weight: 500; + color: var(--z-text-neutral-700); +} + +.z-card-image-box { + width: 100%; + height: 192px; + border-radius: var(--z-border-radius-md); + overflow: hidden; + border: 1px solid var(--z-border-neutral-300); + box-shadow: var(--z-shadow-sm); + background-color: var(--z-bg-neutral-200); + display: flex; + align-items: center; + justify-content: center; +} + +.z-card-image { + width: 100%; + height: 100%; + object-fit: cover; +} + +.z-card-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 16px; + background-color: var(--z-bg-neutral-200); + padding: 16px; + border-radius: var(--z-border-radius-lg); + border: 1px solid var(--z-border-neutral-300); +} + +.z-card-grid-item-label { + font-size: 10px; + font-weight: 700; + color: var(--z-text-neutral-400); + text-transform: uppercase; + letter-spacing: 0.1em; +} + +.z-card-grid-item-val { + font-size: 17px; + font-weight: 700; + color: var(--z-text-neutral-900); + margin-top: 2px; + line-height: 1; +} + +.z-card-grid-item-unit { + font-size: 12px; + font-weight: 500; + color: var(--z-text-neutral-500); +} + +.z-card-grid-footer { + grid-column: span 2; + padding-top: 12px; + border-top: 1px solid var(--z-border-neutral-300); + display: flex; + justify-content: space-between; + align-items: center; +} + +.z-card-grid-footer-label { + font-size: 12px; + font-weight: 500; + color: var(--z-text-neutral-500); +} + +.z-card-grid-footer-badge { + background-color: var(--z-bg-primary-100); + color: var(--z-text-primary-400); + border: 1px solid var(--z-border-primary-100); + font-size: 11px; + font-weight: 700; + padding: 2px 10px; + border-radius: var(--z-border-radius-full); +} + +.z-card-route { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 16px; + font-size: var(--z-font-sm); + border-top: 1px solid var(--z-border-neutral-300); + padding-top: 16px; +} + +.z-card-route-val { + font-weight: 600; + color: var(--z-text-neutral-700); + margin-top: 2px; + font-size: 13px; +} + +.z-card-so-box { + grid-column: span 2; +} + +.z-card-so-info { + display: flex; + align-items: center; + gap: 8px; + margin-top: 6px; +} + +.z-card-so-avatar { + width: 32px; + height: 32px; + background-color: var(--z-bg-neutral-300); + border-radius: var(--z-border-radius-full); + display: flex; + align-items: center; + justify-content: center; + font-weight: 700; + font-size: 11px; + color: var(--z-text-neutral-600); + text-transform: uppercase; + box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.06); +} + +.z-card-so-details { + display: flex; + flex-direction: column; + min-width: 0; +} + +.z-card-so-name { + font-weight: 600; + font-size: 13px; + color: var(--z-text-neutral-900); + line-height: 1.2; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.z-card-so-email { + font-size: 11px; + color: var(--z-text-neutral-500); + font-family: var(--font-mono); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} \ No newline at end of file diff --git a/src/components/forms/DynamicForm.tsx b/src/components/forms/DynamicForm.tsx index 16f85c8..808edaf 100644 --- a/src/components/forms/DynamicForm.tsx +++ b/src/components/forms/DynamicForm.tsx @@ -27,9 +27,10 @@ export interface DynamicFormProps { onCancel?: () => void; ignorePrefill?: boolean; customPrefillData?: Record; + onActivityChange?: (activityName: string) => void; } -export function DynamicForm({ client, activityId: initialActivityId, instanceId: initialInstanceId, onSuccess, onCancel, ignorePrefill, customPrefillData }: DynamicFormProps) { +export function DynamicForm({ client, activityId: initialActivityId, instanceId: initialInstanceId, onSuccess, onCancel, ignorePrefill, customPrefillData, onActivityChange }: DynamicFormProps) { const [currentActivityId, setCurrentActivityId] = useState(initialActivityId); const [currentInstanceId, setCurrentInstanceId] = useState(initialInstanceId); const [chainQueue, setChainQueue] = useState>([]); @@ -209,6 +210,7 @@ export function DynamicForm({ client, activityId: initialActivityId, instanceId: setChainQueue(pending); setCurrentActivityId(nextActivity.activity_uid); setCurrentInstanceId(res.instance_id ?? currentInstanceId); + onActivityChange?.(nextActivity.activity_name); } else { onSuccess?.(); } diff --git a/src/components/forms/fields/SmartGridField.tsx b/src/components/forms/fields/SmartGridField.tsx index dd9b71b..4ae8f94 100644 --- a/src/components/forms/fields/SmartGridField.tsx +++ b/src/components/forms/fields/SmartGridField.tsx @@ -24,7 +24,7 @@ export function SmartGridField({ const visibleColumns = columns.filter(c => { const normalized = c.id.toLowerCase().replace(/[^a-z]/g, ''); // Use a blacklist so we don't accidentally hide important columns from other grids - return !['sku', 'rowkgs', 'brcode'].includes(normalized); + return !['sku', 'skucode', 'rowkgs', 'brcode'].includes(normalized); }); const removeRow = (idx: number) => { diff --git a/src/components/reusable/StatsTile.tsx b/src/components/reusable/StatsTile.tsx new file mode 100644 index 0000000..9e09a28 --- /dev/null +++ b/src/components/reusable/StatsTile.tsx @@ -0,0 +1,41 @@ +import type { TileItem } from "../../api/types"; +import { Phone, TrendingUp, Activity, BarChart2, ShoppingCart, ShoppingBag, Scale } from 'lucide-react'; +import './reusable.css'; +import '../../styles/styles.css'; + +export interface StatsTileProps { + tile: TileItem; + idx?: number; +} + +export function StatsTile({ tile, idx = 0 }: StatsTileProps) { + const displayLabel = (tile.key || `tile_${idx}`) + .replace(/_/g, " ") + .replace(/\b\w/g, (c) => c.toUpperCase()); + + const lowerKey = String(tile.key).toLowerCase(); + + // Try to pick a relevant icon + 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 ( +
+
+ {displayLabel} + +
+ +

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

+
+ ); +} diff --git a/src/components/reusable/StatsTiles.tsx b/src/components/reusable/StatsTiles.tsx index 82e446d..8d59dcc 100644 --- a/src/components/reusable/StatsTiles.tsx +++ b/src/components/reusable/StatsTiles.tsx @@ -1,45 +1,20 @@ import type { TileItem } from "../../api/types"; +import { StatsTile } from "./StatsTile"; +import './reusable.css'; +import '../../styles/styles.css'; export interface StatsTilesProps { tiles?: TileItem[]; } -const TONES = [ - "bg-sunrise-500", - "bg-emerald-500", - "bg-sky-500", - "bg-amber-500", - "bg-navy-600", - "bg-ruby-500", -]; - export function StatsTiles({ tiles }: StatsTilesProps) { if (!tiles?.length) return null; return ( -
- {tiles.map((tile, idx) => { - const displayLabel = (tile.key || `tile_${idx}`) - .replace(/_/g, " ") - .replace(/\b\w/g, (c) => c.toUpperCase()); - - return ( -
- -

- {displayLabel} -

-

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

-
- ); - })} +
+ {tiles.map((tile, idx) => ( + + ))}
); } diff --git a/src/components/reusable/reusable.css b/src/components/reusable/reusable.css new file mode 100644 index 0000000..97184a0 --- /dev/null +++ b/src/components/reusable/reusable.css @@ -0,0 +1,113 @@ +.tile { + box-shadow: var(--block-shadow); +} + +/* Hide scrollbar for horizontal scroll containers */ +.hide-scrollbar { + -ms-overflow-style: none; + scrollbar-width: none; +} + +.hide-scrollbar::-webkit-scrollbar { + display: none; +} + +/* Horizontal Scroll Container */ +.z-stats-container { + display: flex; + overflow-x: auto; + gap: 12px; + padding-bottom: 8px; + /* For box-shadow clipping */ + scroll-snap-type: x mandatory; +} + +/* Base Tile */ +.z-stats-tile { + flex: 0 0 auto; + width: 160px; + height: 100px; + border-radius: var(--z-border-radius-lg); + padding: 16px; + display: flex; + flex-direction: column; + justify-content: space-between; + scroll-snap-align: start; + transition: transform 0.2s ease; + box-shadow: var(--block-shadow); + font-family: var(--font-sans); +} + +/* Secondary Tile (White) */ +.z-stats-tile--secondary { + background-color: var(--z-bg-neutral-100); + border: 1px solid var(--z-border-neutral-300); + color: var(--z-text-neutral-900); +} + +.z-stats-header { + display: flex; + justify-content: space-between; + align-items: center; +} + +.z-stats-label { + font-size: var(--z-font-size-xs); + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.05em; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 80px; +} + +.z-stats-tile--secondary .z-stats-label { + color: var(--z-text-neutral-500); +} + +.z-stats-tile--secondary .z-stats-label.z-stats-label--danger { + color: var(--z-text-danger-400); +} + +.z-stats-tile--secondary .z-stats-icon { + color: var(--z-text-success-400); +} + +.z-stats-tile--secondary .z-stats-icon.z-stats-icon--danger { + color: var(--z-text-danger-400); +} + +.z-stats-value { + font-size: 30px; + font-weight: 800; + line-height: 1.1; + font-family: var(--font-numeric); +} + +.z-stats-tile--secondary .z-stats-value { + color: var(--z-text-primary-400); +} + +.z-stats-tile--secondary .z-stats-value.z-stats-value--danger { + color: var(--z-text-danger-400); +} + +.z-stats-footer { + margin-top: auto; +} + +.z-stats-badge { + display: inline-flex; + align-items: center; + gap: 4px; + font-size: 11px; + font-weight: 700; + border-radius: var(--z-border-radius-pill); +} + +.z-stats-tile--secondary .z-stats-badge { + padding: 0; + color: var(--z-text-success-400); + text-transform: uppercase; +} \ No newline at end of file diff --git a/src/components/rv/CallsView.tsx b/src/components/rv/CallsView.tsx index b87b704..ed75986 100644 --- a/src/components/rv/CallsView.tsx +++ b/src/components/rv/CallsView.tsx @@ -18,6 +18,7 @@ export function CallsView({ onRowClick, pageSize, headerActions, rowActions, ref refreshKey={refreshKey} sortBy="instance_id" sortDir="desc" + hideChart={true} renderItem={(row) => } /> ); diff --git a/src/components/rv/RecordView.tsx b/src/components/rv/RecordView.tsx index 64b3c37..d4d7f50 100644 --- a/src/components/rv/RecordView.tsx +++ b/src/components/rv/RecordView.tsx @@ -1,5 +1,5 @@ import { useEffect, useMemo, useRef, useState } from 'react'; -import { Search, Filter } from 'lucide-react'; +import { Search, SlidersHorizontal } from 'lucide-react'; import { cn } from '../../lib/cn'; import { formatValue } from '../../lib/format'; import type { ZinoClient } from '../../api/client'; @@ -40,6 +40,8 @@ export interface RecordViewProps { sortBy?: string; /** Sort direction */ sortDir?: 'asc' | 'desc'; + /** If true, the analytics chart will not be rendered even if data is returned */ + hideChart?: boolean; } /** @@ -61,6 +63,7 @@ export function RecordView({ renderItem, sortBy, sortDir, + hideChart = false, }: RecordViewProps) { const [page, setPage] = useState(1); const [search, setSearch] = useState(''); @@ -139,7 +142,7 @@ export function RecordView({
- + {!hideChart && }

{title}

@@ -164,16 +167,13 @@ export function RecordView({ setShowFilters(true); }} className={cn( - "flex items-center justify-center h-11 px-4 rounded-pill border transition-colors shadow-sm focus:outline-none focus:ring-2 focus:ring-offset-1 focus:ring-slate-300", - Object.values(activeFilters).some(Boolean) - ? "bg-slate-800 text-white border-slate-800" - : "bg-white text-slate-700 border-border-subtle hover:bg-slate-50" + "relative flex items-center justify-center w-11 h-11 rounded-lg transition-colors shadow-sm focus:outline-none focus:ring-2 focus:ring-offset-1 focus:ring-[var(--z-bg-primary-400)]", + "bg-[var(--z-bg-primary-400)] text-white border border-[var(--z-bg-primary-400)] hover:opacity-90" )} > - - Filter + {Object.values(activeFilters).filter(Boolean).length > 0 && ( - + {Object.values(activeFilters).filter(Boolean).length} )} @@ -181,25 +181,25 @@ export function RecordView({ )}
{filterEntries.length > 0 && ( - setShowFilters(false)} + setShowFilters(false)} title="Filters" width="sm" actions={ - Object.values(pendingFilters).some(Boolean) && ( - - ) + Object.values(pendingFilters).some(Boolean) && ( + + ) } >
@@ -207,7 +207,7 @@ export function RecordView({ const fieldDef = resp?.config.fields.find(f => f.field_key === key); const label = fieldDef?.output_label || key; const isDateField = fieldDef?.data_type === 'date' || key.toLowerCase().includes('date'); - + if (isDateField) { return ( ); })} - - - } /> + {/* Global Floating Action Button for Add Call */} + + setIsCreating(false)} - title="Log Visit" + title={createTitle} width="md" > k + 1); }} onCancel={() => setIsCreating(false)} + onActivityChange={(name) => setCreateTitle(name)} /> @@ -136,7 +141,7 @@ export function CallsPage() { title={activeActivity?.name} width="md" > - {activeActivity && instanceId != null && ( + {activeActivity && ( k + 1); }} onCancel={() => setActiveActivity(null)} + onActivityChange={(name) => setActiveActivity(prev => prev ? { ...prev, name } : null)} /> )} diff --git a/src/screens/DailyLogsPage.tsx b/src/screens/DailyLogsPage.tsx index 18fcc5b..2de0cd2 100644 --- a/src/screens/DailyLogsPage.tsx +++ b/src/screens/DailyLogsPage.tsx @@ -14,7 +14,9 @@ export function DailyLogsPage() { const instanceId = params.instanceId ? Number(params.instanceId) : undefined; const navigate = useNavigate(); const [isCreating, setIsCreating] = useState(false); + const [createTitle, setCreateTitle] = useState("Punch In"); const [punchOutInstanceId, setPunchOutInstanceId] = useState(null); + const [punchOutTitle, setPunchOutTitle] = useState("Punch Out"); const [refreshKey, setRefreshKey] = useState(0); return ( @@ -26,17 +28,20 @@ export function DailyLogsPage() { if (id != null) navigate(`/daily/${id}`); }} headerActions={ - } - onPunchOutRow={(row) => setPunchOutInstanceId(row.instance_id as string | number)} + onPunchOutRow={(row) => { + setPunchOutInstanceId(row.instance_id as string | number); + setPunchOutTitle("Punch Out"); + }} /> setIsCreating(false)} - title="Punch In" + title={createTitle} width="md" > k + 1); }} onCancel={() => setIsCreating(false)} + onActivityChange={(name) => setCreateTitle(name)} /> setPunchOutInstanceId(null)} - title="Punch Out" + title={punchOutTitle} width="md" > {punchOutInstanceId != null && ( @@ -66,6 +72,7 @@ export function DailyLogsPage() { setRefreshKey(k => k + 1); }} onCancel={() => setPunchOutInstanceId(null)} + onActivityChange={(name) => setPunchOutTitle(name)} /> )} diff --git a/src/screens/OrdersPage.tsx b/src/screens/OrdersPage.tsx index d527b0e..26e3044 100644 --- a/src/screens/OrdersPage.tsx +++ b/src/screens/OrdersPage.tsx @@ -14,6 +14,7 @@ export function OrdersPage() { const instanceId = params.instanceId ? Number(params.instanceId) : undefined; const navigate = useNavigate(); const [isCreating, setIsCreating] = useState(false); + const [createTitle, setCreateTitle] = useState("Place Order"); const [refreshKey, setRefreshKey] = useState(0); const [activeActivity, setActiveActivity] = useState<{ id: string; name: string } | null>(null); @@ -26,7 +27,7 @@ export function OrdersPage() { if (id != null) navigate(`/orders/${id}`); }} headerActions={ - } @@ -35,7 +36,7 @@ export function OrdersPage() { setIsCreating(false)} - title="Place Order" + title={createTitle} width="md" > k + 1); }} onCancel={() => setIsCreating(false)} + onActivityChange={(name) => setCreateTitle(name)} /> @@ -75,6 +77,7 @@ export function OrdersPage() { setRefreshKey(k => k + 1); }} onCancel={() => setActiveActivity(null)} + onActivityChange={(name) => setActiveActivity(prev => prev ? { ...prev, name } : null)} /> )} diff --git a/src/screens/StoresPage.tsx b/src/screens/StoresPage.tsx index d276991..b68cec6 100644 --- a/src/screens/StoresPage.tsx +++ b/src/screens/StoresPage.tsx @@ -14,7 +14,9 @@ export function StoresPage() { const instanceId = params.instanceId ? Number(params.instanceId) : undefined; const navigate = useNavigate(); const [isCreating, setIsCreating] = useState(false); + const [createTitle, setCreateTitle] = useState("Create Store"); const [editingInstanceId, setEditingInstanceId] = useState(null); + const [editTitle, setEditTitle] = useState("Edit Store"); const [refreshKey, setRefreshKey] = useState(0); return ( @@ -26,17 +28,20 @@ export function StoresPage() { if (id != null) navigate(`/stores/${id}`); }} headerActions={ - } - onEditRow={(row) => setEditingInstanceId(row.instance_id as string | number)} + onEditRow={(row) => { + setEditingInstanceId(row.instance_id as string | number); + setEditTitle("Edit Store"); + }} /> setIsCreating(false)} - title="Create Store" + title={createTitle} width="md" > k + 1); }} onCancel={() => setIsCreating(false)} + onActivityChange={(name) => setCreateTitle(name)} /> setEditingInstanceId(null)} - title="Edit Store" + title={editTitle} width="md" > {editingInstanceId != null && ( @@ -66,6 +72,7 @@ export function StoresPage() { setRefreshKey(k => k + 1); }} onCancel={() => setEditingInstanceId(null)} + onActivityChange={(name) => setEditTitle(name)} /> )} diff --git a/src/styles/styles.css b/src/styles/styles.css index 32a9668..f98d676 100644 --- a/src/styles/styles.css +++ b/src/styles/styles.css @@ -192,7 +192,7 @@ /* root variable */ :root { - --primary-color: #0058be; + --primary-color: #1d4ed8; --secondary-color: #f9fafb; --font-color: #10182b; --font-family: "IBM Plex Sans", serif; @@ -257,7 +257,8 @@ --block-padding: 20px; --block-radius: 20px; --block-border: #e5e7eb; - --block-shadow: 0px 4px 8px 0px rgba(228, 231, 236, 0.3); + /* --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; @@ -327,7 +328,7 @@ --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: 8px; + --z-font-size-xs: 10px; --z-font-size-sm: 12px; --z-font-size-md: 14px; --z-font-size-lg: 16px; @@ -412,7 +413,7 @@ --z-bg-warning-400: #dfb400; /* Success Colors */ - --z-bg-success-100: #a6f4c5; + --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;