tile design done

This commit is contained in:
suryacp23 2026-07-21 17:54:49 +05:30
parent d30fd56d4f
commit 1894b1f9a5
14 changed files with 169 additions and 130 deletions

View File

@ -65,7 +65,8 @@ export const ORDER_BOOKING = {
recordViews: {
ORDERS: '1e424561-9a27-44f5-9b68-64d63296d837',
CALLS: 'b4d7a710-24e7-416d-885a-31fd1e727aab',
ANALYTICS: 'b4d7a710-24e7-416d-885a-31fd1e727aab'
ANALYTICS: 'b4d7a710-24e7-416d-885a-31fd1e727aab',
},
detailViews: {
ORDERS: '0804c6c3-6cf9-4050-94bb-fa48dd5de87d',

View File

@ -330,6 +330,13 @@ export function DynamicForm({ client, activityId: initialActivityId, instanceId:
const val = values[f.id];
const isDisabled = schema.field_defaults?.[f.id]?.disabled || f.properties?.disabled;
const isPlaceOrder = currentActivityId === ORDER_BOOKING.activities.PLACE_ORDER.uid;
const isOrderId = f.name.toLowerCase() === 'order id' || f.id.toLowerCase().includes('order_id') || f.mapped_workflow_field === 'order_id';
if (isPlaceOrder && isOrderId) {
return null;
}
const renderField = () => {
if (type === 'wf_lookup') {
return (

View File

@ -29,9 +29,10 @@ export function FileInput({
const files = Array.isArray(value) ? value : (value instanceof File ? [value] : []);
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files) {
if (e.target.files && e.target.files.length > 0) {
const newFiles = Array.from(e.target.files);
onChange([...files, ...newFiles]);
// Since we only want 1 file, replace the existing one instead of appending
onChange(newFiles);
}
// reset input so the same file can be selected again if needed
e.target.value = '';
@ -53,7 +54,6 @@ export function FileInput({
<div className="flex flex-col gap-3">
<input
type="file"
multiple
required={required && files.length === 0}
accept={type === 'image' ? 'image/*' : undefined}
onChange={handleFileChange}
@ -63,7 +63,7 @@ export function FileInput({
{files.length > 0 && (
<div className="flex flex-wrap gap-3">
{files.map((file, i) => (
<div key={i} className="relative w-24 h-24 rounded-md overflow-hidden border border-border-default shadow-sm bg-slate-50 flex-shrink-0 group">
<div key={i} className="relative w-24 h-24 rounded-md overflow-hidden border border-border-default shadow-sm bg-slate-50 flex-shrink-0">
{type === 'image' ? (
<ImagePreview file={file} />
) : (
@ -74,7 +74,7 @@ export function FileInput({
<button
type="button"
onClick={() => removeFile(i)}
className="absolute top-1 right-1 bg-ruby-600/90 hover:bg-ruby-700 text-white rounded-full w-6 h-6 flex items-center justify-center text-xs opacity-0 group-hover:opacity-100 transition-opacity"
className="absolute top-1 right-1 bg-ruby-600/90 hover:bg-ruby-700 text-white rounded-full w-6 h-6 flex items-center justify-center text-xs shadow-md"
title="Remove file"
>

View File

@ -152,6 +152,8 @@ export function SmartGridField({
setEditingIdx(null);
};
const isPotentialMining = label.toLowerCase().includes('potential');
return (
<div className="flex flex-col gap-2 font-sans border border-border-default rounded-md p-4 bg-slate-50">
<span className="text-sm font-semibold text-strong mb-2">{label}</span>
@ -163,12 +165,16 @@ export function SmartGridField({
{value.map((row, i) => {
let productName = 'Unknown Product';
let bags = '0';
let hasBagsCol = false;
let diffVal: number | null = null;
let reasonStr = '';
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');
const isDiff = col.id.toLowerCase().includes('diff') || col.name.toLowerCase().includes('diff');
const isReason = col.id.toLowerCase().includes('reason') || col.name.toLowerCase().includes('reason');
if (isName || isBags) {
const val = row[col.id];
let displayVal = String(val ?? '-');
@ -180,15 +186,41 @@ export function SmartGridField({
if (isName && (productName === 'Unknown Product' || col.id.toLowerCase().includes('name'))) {
productName = displayVal;
}
if (isBags) bags = displayVal;
if (isBags) {
bags = displayVal;
hasBagsCol = true;
}
if (isPotentialMining && isDiff && val != null && val !== '') {
diffVal = Number(val);
}
if (isPotentialMining && isReason && val && displayVal !== 'Select...' && displayVal !== '-') {
reasonStr = displayVal;
}
});
return (
<div key={i} className="bg-slate-50 border border-slate-200 rounded-xl p-3 shadow-sm flex items-center justify-between">
<div className="flex flex-col min-w-0 pr-4">
<div className="flex flex-col min-w-0 pr-4 gap-1.5">
<span className="font-bold text-slate-800 text-[14px] truncate">{productName}</span>
<span className="font-extrabold text-indigo-600 text-[13px] mt-0.5">{bags} Bags</span>
<div className="flex flex-wrap items-center gap-2">
{(!isPotentialMining || hasBagsCol) && (
<span className="font-extrabold text-indigo-600 text-[13px]">{bags} Bags</span>
)}
{isPotentialMining && diffVal !== null && !isNaN(diffVal) && diffVal !== 0 && (
<span className={`text-[11px] font-bold px-2 py-0.5 rounded-md ${diffVal > 0 ? 'bg-emerald-100 text-emerald-700' : 'bg-red-100 text-red-700'}`}>
{Math.abs(diffVal)} {diffVal > 0 ? 'Surplus' : 'Less'}
</span>
)}
{isPotentialMining && reasonStr && (
<span className="text-[11px] font-medium bg-slate-200 text-slate-700 px-2 py-0.5 rounded-md truncate max-w-[120px]" title={reasonStr}>
{reasonStr}
</span>
)}
</div>
</div>
<div className="flex items-center gap-1.5 shrink-0">

View File

@ -6,7 +6,8 @@ import {
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer
ResponsiveContainer,
LabelList
} from 'recharts';
export interface ChartRow {
@ -28,7 +29,6 @@ export function AnalyticsChart({ data }: AnalyticsChartProps) {
if (!data || !Array.isArray(data) || data.length === 0) return null;
const charts = data as ChartConfig[];
const colors = ['#3B82F6', '#10B981', '#F59E0B', '#8B5CF6', '#EC4899', '#14B8A6'];
return (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 w-full">
@ -46,10 +46,16 @@ export function AnalyticsChart({ data }: AnalyticsChartProps) {
if (formattedRows.length === 0) return null;
return (
<Card key={chart.chart_uid || idx} title={title} className="shadow-sm border-t-4 border-t-indigo-500">
<Card key={chart.chart_uid || idx} title={title} className="shadow-sm">
<div className="h-[320px] w-full mt-4">
<ResponsiveContainer width="100%" height="100%">
<BarChart data={formattedRows} margin={{ top: 10, right: 10, left: -20, bottom: 0 }}>
<BarChart data={formattedRows} margin={{ top: 25, right: 10, left: -20, bottom: 0 }}>
<defs>
<linearGradient id={`colorGradient-${idx}`} x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="var(--primary-color)" stopOpacity={1} />
<stop offset="100%" stopColor="var(--primary-color-300)" stopOpacity={1} />
</linearGradient>
</defs>
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#E2E8F0" />
<XAxis
dataKey="dimension"
@ -72,10 +78,12 @@ export function AnalyticsChart({ data }: AnalyticsChartProps) {
<Bar
dataKey="value"
name="Value"
fill={colors[idx % colors.length]}
radius={[4, 4, 0, 0]}
fill={`url(#colorGradient-${idx})`}
radius={[8, 8, 0, 0]}
maxBarSize={40}
/>
>
<LabelList dataKey="value" position="top" fill="#475569" fontSize={12} fontWeight="bold" />
</Bar>
</BarChart>
</ResponsiveContainer>
</div>

View File

@ -28,7 +28,7 @@ export function Input({
return (
<label className={cn('flex flex-col gap-1.5 font-sans', disabled && 'cursor-not-allowed', className)}>
{label && (
<span className="text-sm font-medium text-muted">
<span className="text-sm font-semibold text-slate-700">
{label}
{required && <span className="text-ruby-600"> *</span>}
</span>

View File

@ -53,7 +53,7 @@ export function Modal({ open, onClose, title, subtitle, width = 'md', actions, c
aria-modal="true"
className={cn(
'relative w-full bg-card rounded-t-2xl sm:rounded-lg border border-border-subtle shadow-lg sm:my-auto',
'flex flex-col max-h-[92vh] sm:max-h-[85vh]',
'flex flex-col max-h-[70vh]',
WIDTH[width],
)}
>

View File

@ -21,7 +21,7 @@ export function Select({ label, hint, options = [], required, className, ...rest
return (
<label className={cn('flex flex-col gap-1.5 font-sans', className)}>
{label && (
<span className="text-sm font-medium text-muted">
<span className="text-sm font-semibold text-slate-700">
{label}
{required && <span className="text-ruby-600"> *</span>}
</span>

View File

@ -1,5 +1,5 @@
import type { TileItem } from "../../api/types";
import { Phone, TrendingUp, Activity, BarChart2, ShoppingCart, ShoppingBag, Scale } from 'lucide-react';
import { Phone, TrendingUp, Activity, BarChart2, ShoppingCart, ShoppingBag, Scale, Store, Map } from 'lucide-react';
import './reusable.css';
import '../../styles/styles.css';
@ -15,27 +15,30 @@ export function StatsTile({ tile, idx = 0 }: StatsTileProps) {
const lowerKey = String(tile.key).toLowerCase();
// Try to pick a relevant icon
// Try to pick a relevant icon and color theme
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;
let colorTheme = 'blue';
const isDanger = lowerKey.includes('no_order');
if (lowerKey.includes('call') || lowerKey.includes('total')) { Icon = Phone; colorTheme = 'blue'; }
if (lowerKey.includes('productive')) { Icon = TrendingUp; colorTheme = 'green'; }
if (lowerKey.includes('order') || lowerKey.includes('cart')) { Icon = ShoppingCart; colorTheme = 'blue'; }
if (lowerKey.includes('bag')) { Icon = ShoppingBag; colorTheme = 'green'; }
if (lowerKey.includes('kg') || lowerKey.includes('weight')) { Icon = Scale; colorTheme = 'purple'; }
if (lowerKey.includes('active')) { Icon = Activity; colorTheme = 'blue'; }
if (lowerKey.includes('store')) { Icon = Store; colorTheme = 'blue'; }
if (lowerKey.includes('area') || lowerKey.includes('region')) { Icon = Map; colorTheme = 'purple'; }
return (
<div className={`z-stats-tile z-stats-tile--secondary ${isDanger ? 'z-stats-tile--danger' : ''}`}>
<div className="z-stats-header">
<span className={`z-stats-label`}>{displayLabel}</span>
<Icon size={16} className={`z-stats-icon ${isDanger ? 'z-stats-icon--danger' : ''}`} />
<div className={`z-stats-tile z-stats-tile--${colorTheme}`}>
<div className="z-stats-icon-wrapper">
<Icon size={26} className="z-stats-icon" strokeWidth={2.5} />
</div>
<p className={`z-stats-value nums ${isDanger ? 'z-stats-value--danger' : ''}`}>
<div className="z-stats-content">
<span className="z-stats-label">{displayLabel}</span>
<p className="z-stats-value">
{(tile.value as React.ReactNode) ?? "-"}
</p>
</div>
</div>
);
}

View File

@ -20,7 +20,7 @@ export function Textarea({
return (
<label className={cn('flex flex-col gap-1.5 font-sans', disabled && 'cursor-not-allowed', className)}>
{label && (
<span className="text-sm font-medium text-muted">
<span className="text-sm font-semibold text-slate-700">
{label}
{required && <span className="text-ruby-600"> *</span>}
</span>

View File

@ -17,99 +17,86 @@
display: flex;
flex-direction: column;
gap: 12px;
padding-bottom: 8px;
margin-bottom: 16px;
}
/* Base Tile */
.z-stats-tile {
flex: 0 0 auto;
width: 100%;
height: 100px;
border-radius: var(--z-border-radius-lg);
padding: 16px;
display: flex;
flex-direction: row;
align-items: center;
padding: 16px 20px;
background: var(--z-bg-neutral-200);
border-radius: var(--z-border-radius-lg, 12px);
box-shadow: var(--block-shadow, 0 7px 7px rgba(0, 0, 0, 0.1));
border: 1px solid var(--z-neutral-500);
font-family: var(--font-sans, system-ui, -apple-system, sans-serif);
}
.z-stats-tile:last-child {
margin-bottom: 0;
}
.z-stats-icon-wrapper {
width: 60px;
height: 60px;
border-radius: 14px;
display: flex;
align-items: center;
justify-content: center;
margin-right: 16px;
flex-shrink: 0;
}
/* Color Themes */
.z-stats-tile--blue .z-stats-icon-wrapper {
background-color: #e0ebff;
}
.z-stats-tile--blue .z-stats-icon {
color: var(--z-text-primary-400, #0055ff);
}
.z-stats-tile--green .z-stats-icon-wrapper {
background-color: #dcfce7;
}
.z-stats-tile--green .z-stats-icon {
color: #00b050;
/* adjusted for the green from the image */
}
.z-stats-tile--purple .z-stats-icon-wrapper {
background-color: #f3e8ff;
}
.z-stats-tile--purple .z-stats-icon {
color: #6a35ff;
/* adjusted for purple from image */
}
/* Content */
.z-stats-content {
display: flex;
flex-direction: column;
justify-content: space-between;
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);
border-left: 4px solid var(--z-text-primary-400);
color: var(--z-text-neutral-900);
}
.z-stats-header {
display: flex;
justify-content: space-between;
align-items: center;
justify-content: center;
}
.z-stats-label {
font-size: var(--z-font-size-xs);
font-size: 11px;
font-weight: 700;
color: #8892a0;
/* subtle gray */
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);
letter-spacing: 0.04em;
margin-bottom: 2px;
}
.z-stats-value {
font-size: 30px;
font-size: 32px;
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-tile--secondary.z-stats-tile--danger {
border-left-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;
color: var(--z-text-primary-400, #0055ff);
font-family: var(--font-numeric, inherit);
letter-spacing: -0.02em;
}

View File

@ -27,7 +27,7 @@ export function ConsoleLayout() {
return (
<div className="h-[100dvh] overflow-hidden bg-app flex flex-col">
<header className="print:hidden sticky top-0 z-20 shrink-0 flex items-center justify-between gap-3 px-4 h-14 pt-[env(safe-area-inset-top)] top-navbar">
<header className="print:hidden sticky top-0 z-20 shrink-0 flex items-center justify-between gap-3 px-4 h-14 pt-[env(safe-area-inset-top)] top-navbar shadow-md">
<div className="flex items-center gap-3 flex-1">
<button
type="button"

View File

@ -156,9 +156,9 @@
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(--primary-color);
box-shadow: var(--shadow-focus);
}
@ -193,6 +193,7 @@
:root {
--primary-color: #1d4ed8;
--primary-color-300: #93c5fd;
--secondary-color: #f9fafb;
--font-color: #10182b;
--font-family: "IBM Plex Sans", serif;

View File

@ -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(29,78,216,0.3); /* Matches primary-color #1d4ed8 with opacity */
/* Status */
--status-success: var(--emerald-600);