order view calls view pages done

This commit is contained in:
suryac 2026-07-14 10:53:42 +05:30
parent 48c6a1cab9
commit b5e84a34f7
7 changed files with 121 additions and 37 deletions

View File

@ -54,8 +54,8 @@ export const ORDER_BOOKING = {
},
},
recordViews: {
ORDERS: '1e424561-9a27-44f5-9b68-64d63296d837',
CALLS: 'b4d7a710-24e7-416d-885a-31fd1e727aab',
ORDERS: '63714ac2-c9fb-40e2-abba-d4081a70b768',
CALLS: '56b21b46-6d2c-4e10-b5a3-c63d058d0afa',
},
detailViews: {
ORDERS: '0804c6c3-6cf9-4050-94bb-fa48dd5de87d',

View File

@ -146,7 +146,7 @@ export function DetailView({ client, instanceId, title, columns = 2 }: DetailVie
<tr key={idx} className="hover:bg-slate-50/50">
{headers.map(h => (
<td key={h} className="px-4 py-2 whitespace-nowrap text-sm text-strong">
{formatValue(row[h])}
{formatValue(row[h], h)}
</td>
))}
</tr>
@ -164,7 +164,7 @@ export function DetailView({ client, instanceId, title, columns = 2 }: DetailVie
{key.replace(/_/g, ' ')}
</dt>
<dd className="m-0 text-sm text-strong font-medium break-words">
{formatValue(value)}
{formatValue(value, key)}
</dd>
</div>
);

View File

@ -15,6 +15,9 @@ export function CallsView({ onRowClick, pageSize, headerActions, rowActions, ref
headerActions={headerActions}
rowActions={rowActions}
refreshKey={refreshKey}
sortBy="instance_id"
sortDir="desc"
omitColumns={['instance_id']}
/>
);
}

View File

@ -22,6 +22,9 @@ export function OrdersView({ onRowClick, pageSize, headerActions, rowActions, re
headerActions={headerActions}
rowActions={rowActions}
refreshKey={refreshKey}
sortBy="created_at"
sortDir="desc"
omitColumns={['created_at', 'instance_id']}
/>
);
}

View File

@ -21,6 +21,8 @@ export interface RecordViewProps {
title?: string;
/** Restrict/order visible columns by field_key. Defaults to all fields. */
columns?: string[];
/** Columns to hide. */
omitColumns?: string[];
/** Rows per page. @default 25 */
pageSize?: number;
/** Click handler — receives the raw row + index. */
@ -33,6 +35,10 @@ export interface RecordViewProps {
rowActions?: (row: Record<string, unknown>) => React.ReactNode;
/** Pass a new value to trigger a refresh. */
refreshKey?: number;
/** Field to sort by */
sortBy?: string;
/** Sort direction */
sortDir?: 'asc' | 'desc';
}
/**
@ -51,6 +57,9 @@ export function RecordView({
headerActions,
rowActions,
refreshKey,
sortBy,
sortDir,
omitColumns,
}: RecordViewProps) {
const [page, setPage] = useState(1);
const [search, setSearch] = useState('');
@ -89,7 +98,14 @@ export function RecordView({
setLoading(true);
setError(null);
try {
const r = await client.recordView(rvUid, { page, limit: pageSize, search: debounced, filters: filtersParam });
const r = await client.recordView(rvUid, {
page,
limit: pageSize,
search: debounced,
filters: filtersParam,
sortBy,
sortDir
});
console.log("RECORD VIEW RESP:", r);
if (live) setResp(r);
} catch (e) {
@ -102,16 +118,23 @@ export function RecordView({
return () => {
live = false;
};
}, [client, rvUid, page, pageSize, debounced, filtersParam, refreshKey]);
}, [client, rvUid, page, pageSize, debounced, filtersParam, refreshKey, sortBy, sortDir]);
const fields: RecordViewField[] = useMemo(() => {
const all = resp?.config.fields ?? [];
if (!columns) return all;
const byKey = new Map(all.map((f) => [f.field_key, f]));
return columns
.map((k) => byKey.get(k) ?? ({ field_key: k, output_label: k, data_type: 'string', is_filter: false, is_search: false } as RecordViewField))
.filter(Boolean);
}, [resp, columns]);
let result = all;
if (columns) {
const byKey = new Map(all.map((f) => [f.field_key, f]));
result = columns
.map((k) => byKey.get(k) ?? ({ field_key: k, output_label: k, data_type: 'string', is_filter: false, is_search: false } as RecordViewField))
.filter(Boolean);
}
if (omitColumns) {
const omitSet = new Set(omitColumns);
result = result.filter((f) => !omitSet.has(f.field_key));
}
return result;
}, [resp, columns, omitColumns]);
const rows = resp?.data ?? [];
const total = resp?.pagination?.total_count ?? rows.length;
@ -133,27 +156,55 @@ export function RecordView({
>
<div className="flex flex-wrap items-center justify-between gap-4 p-4 border-b border-border-subtle bg-slate-50/50">
<div className="flex flex-wrap items-center gap-3">
{resp?.config?.filter_options && Object.entries(resp.config.filter_options).map(([key, options]) => {
if (!options || options.length === 0) return null;
{(() => {
if (!resp?.config) return null;
const filterKeys = new Set<string>();
resp.config.fields.forEach(f => {
if (f.is_filter) filterKeys.add(f.field_key);
});
if (resp.config.filter_options) {
Object.keys(resp.config.filter_options).forEach(k => filterKeys.add(k));
}
// Find the field in config to get its proper label
const fieldDef = resp.config.fields.find(f => f.field_key === key);
const label = fieldDef?.output_label || key;
return Array.from(filterKeys).map(key => {
const fieldDef = resp.config.fields.find(f => f.field_key === key);
const label = fieldDef?.output_label || key;
const isDate = fieldDef?.data_type === 'date' || fieldDef?.data_type === 'datetime' || key.toLowerCase().includes('date') || key.toLowerCase() === 'created_at' || key.toLowerCase() === 'updated_at';
const opts = options.map(o => ({ value: o, label: o }));
return (
<Select
key={key}
value={activeFilters[key] || ''}
onChange={(e) => {
setActiveFilters(prev => ({ ...prev, [key]: e.target.value }));
setPage(1);
}}
options={[{ value: '', label: `All ${label}` }, ...opts]}
className="w-40 shrink-0"
/>
);
})}
if (isDate) {
return (
<input
key={key}
type="date"
title={`Filter by ${label}`}
value={activeFilters[key] || ''}
onChange={(e) => {
setActiveFilters(prev => ({ ...prev, [key]: e.target.value }));
setPage(1);
}}
className="w-36 sm:w-40 h-9 rounded-md border border-border-default bg-card px-3 font-sans text-sm text-strong outline-none focus-ring shrink-0"
/>
);
}
const options = resp.config.filter_options?.[key];
if (!options || options.length === 0) return null;
const opts = options.map(o => ({ value: o, label: o }));
return (
<Select
key={key}
value={activeFilters[key] || ''}
onChange={(e) => {
setActiveFilters(prev => ({ ...prev, [key]: e.target.value }));
setPage(1);
}}
options={[{ value: '', label: `All ${label}` }, ...opts]}
className="w-36 sm:w-40 shrink-0"
/>
);
});
})()}
</div>
<div className="relative w-full sm:w-[240px] shrink-0">
<Search size={15} className="absolute left-3 top-1/2 -translate-y-1/2 text-faint pointer-events-none" />
@ -206,7 +257,7 @@ export function RecordView({
>
{fields.map((f) => (
<td key={f.field_key} className="px-[18px] py-3 text-sm text-body whitespace-nowrap">
{formatValue(row[f.field_key])}
{formatValue(row[f.field_key], f.field_key)}
</td>
))}
{rowActions && (

View File

@ -13,10 +13,37 @@ function isObject(v: unknown): v is Record<string, unknown> {
}
/** Best-effort single-line display string for a field value. */
export function formatValue(value: unknown): string {
export function formatValue(value: unknown, fieldKey?: string): string {
if (value == null || value === '') return '—';
if (typeof value === 'boolean') return value ? 'Yes' : 'No';
if (typeof value === 'number' || typeof value === 'string') return String(value);
if (typeof value === 'number') return String(value);
if (typeof value === 'string') {
const isoDateRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/;
if (isoDateRegex.test(value)) {
const date = new Date(value);
if (!isNaN(date.getTime())) {
const lowerKey = fieldKey?.toLowerCase() || '';
const isTimeOnly = lowerKey.includes('time');
const isDateOnly = lowerKey.includes('date');
if (isTimeOnly && !isDateOnly) {
return date.toLocaleTimeString('en-IN', { hour: 'numeric', minute: '2-digit', hour12: true });
} else if (isDateOnly && !isTimeOnly) {
return date.toLocaleDateString('en-IN', { day: 'numeric', month: 'short', year: 'numeric' });
} else {
return date.toLocaleString('en-IN', {
day: 'numeric',
month: 'short',
year: 'numeric',
hour: 'numeric',
minute: '2-digit',
hour12: true
});
}
}
}
return value;
}
if (Array.isArray(value)) {
if (value.length === 0) return '—';

View File

@ -2,7 +2,7 @@ import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
const base = process.env.VITE_BASE_URL ?? '/'
const base = process.env.VITE_BASE_URL ?? '/krishna_sales'
export default defineConfig(() => ({
base,