Compare commits

..

2 Commits

Author SHA1 Message Date
819b43bd77 fix: limit for select and made it as constant 2026-08-18 11:46:13 +05:30
a600121e0d fix: limit for select option and pdf limit 2026-08-18 11:08:40 +05:30
8 changed files with 81 additions and 47 deletions

View File

@ -260,7 +260,7 @@ export const SALES_REPORT_SO_NAMES = [
// --- Pipeline Endpoints --- // --- Pipeline Endpoints ---
export const PIPELINE = { export const PIPELINE = {
endpoints: { endpoints: {
nearestStores: '/api/papi2/nearest-stores', nearestStores: '/api/p/krishna-group/v1/nearest-stores',
dailySalesReport: '/api/papi2/daily-sales-report', dailySalesReport: '/api/papi2/daily-sales-report',
productiveCallSummary: '/api/papi2/productive-call-summary', productiveCallSummary: '/api/papi2/productive-call-summary',
salesOfficers: '/api/papi2/sales-officers' salesOfficers: '/api/papi2/sales-officers'
@ -270,3 +270,6 @@ export const PIPELINE = {
export const HIDDEN_FORM_FIELDS = [ export const HIDDEN_FORM_FIELDS = [
'store_code_3' 'store_code_3'
]; ];
export const LOOKUP_RECORD_LIMIT = 500;
export const MAX_SKUS_PER_CHUNK = 20;

View File

@ -1,7 +1,7 @@
import React, { useState, useEffect, useCallback, useRef } from 'react'; import React, { useState, useEffect, useCallback, useRef } from 'react';
import type { ZinoClient } from '../../api/client'; import type { ZinoClient } from '../../api/client';
import type { FormScreenResponse } from '../../api/types'; import type { FormScreenResponse } from '../../api/types';
import { ORDER_BOOKING, PIPELINE } from '../../api/config'; import { ORDER_BOOKING, PIPELINE, LOOKUP_RECORD_LIMIT } from '../../api/config';
import { Button } from '../buttons/Button'; import { Button } from '../buttons/Button';
import { Select } from '../reusable/Select'; import { Select } from '../reusable/Select';
import { DateField, TimeField, FileInput, TextField } from './fields'; import { DateField, TimeField, FileInput, TextField } from './fields';
@ -184,7 +184,7 @@ export function LogVisitForm({ client, onSuccess, onCancel, onActivityChange }:
activityId, activityId,
fieldId: 'select_store', fieldId: 'select_store',
formData: formDataToSend, formData: formDataToSend,
limit: 200, limit: LOOKUP_RECORD_LIMIT,
}); });
const arr = Array.isArray(lookupRes) const arr = Array.isArray(lookupRes)
@ -253,7 +253,7 @@ export function LogVisitForm({ client, onSuccess, onCancel, onActivityChange }:
activityId, activityId,
fieldId: 'daily_log', fieldId: 'daily_log',
formData: payload, formData: payload,
limit: 200, limit: LOOKUP_RECORD_LIMIT,
}); });
const arr = Array.isArray(dailyLogLookupRes) const arr = Array.isArray(dailyLogLookupRes)

View File

@ -1,5 +1,6 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import type { ZinoClient } from '../../../api/client'; import type { ZinoClient } from '../../../api/client';
import { LOOKUP_RECORD_LIMIT } from '../../../api/config';
import { SelectField } from './SelectField'; import { SelectField } from './SelectField';
export interface WfLookupFieldProps { export interface WfLookupFieldProps {
@ -31,7 +32,7 @@ export function WfLookupField({ label, required, value, onChange, client, config
activityId, activityId,
fieldId, fieldId,
formData: JSON.parse(formDataStr), formData: JSON.parse(formDataStr),
limit: 200 limit: LOOKUP_RECORD_LIMIT
}) })
.then(res => { .then(res => {
if (!mounted) return; if (!mounted) return;

View File

@ -71,13 +71,6 @@ export function DailyLogMap({
{ {
latitude: lat, latitude: lat,
longitude: lng longitude: lng
},
{
'accept': 'application/json, text/plain, */*',
'groupid': '25',
'orgid': '57',
'templateid': '189',
'x-pipeline-version': 'latest'
} }
); );

View File

@ -102,10 +102,7 @@ export function AnalyticsChart({ data, gridCols }: AnalyticsChartProps) {
const fullSpanClass = gridCols === 2 ? 'lg:col-span-2' : 'lg:col-span-3'; const fullSpanClass = gridCols === 2 ? 'lg:col-span-2' : 'lg:col-span-3';
// Calculate a dynamic width if there's a lot of data to allow horizontal scrolling const minChartWidth = 100;
const minChartWidth = chartType === 0 || chartType === 1 || chartType === 2
? Math.max(100, finalData.length * (hasSeries ? seriesKeys.length * 15 + 40 : 60))
: 100;
const renderChartContent = () => { const renderChartContent = () => {
return ( return (
@ -125,8 +122,11 @@ export function AnalyticsChart({ data, gridCols }: AnalyticsChartProps) {
dataKey="dimension" dataKey="dimension"
axisLine={false} axisLine={false}
tickLine={false} tickLine={false}
tick={{ fontSize: 12, fill: '#64748B' }} tick={{ fontSize: 11, fill: '#64748B', angle: -45, textAnchor: 'end' }}
interval={0}
dy={10} dy={10}
dx={-5}
height={70}
/> />
<YAxis <YAxis
axisLine={false} axisLine={false}
@ -190,7 +190,7 @@ export function AnalyticsChart({ data, gridCols }: AnalyticsChartProps) {
return ( return (
<Card key={chart.chart_uid || idx} title={title} className={`shadow-sm border border-gray-100 !bg-[var(--tiles-card-bg)] ${isFullWidth ? fullSpanClass : ''}`}> <Card key={chart.chart_uid || idx} title={title} className={`shadow-sm border border-gray-100 !bg-[var(--tiles-card-bg)] ${isFullWidth ? fullSpanClass : ''}`}>
<div className={`h-[320px] w-full mt-4 overflow-y-hidden ${minChartWidth > 100 ? 'overflow-x-auto scrollbar-slim' : ''}`}> <div className={`h-[320px] w-full mt-4 ${minChartWidth > 100 ? 'overflow-x-auto scrollbar-slim' : ''}`}>
{chartType === 5 ? ( {chartType === 5 ? (
<div className="w-full h-full overflow-y-auto pr-2 scrollbar-slim"> <div className="w-full h-full overflow-y-auto pr-2 scrollbar-slim">
<table className="w-full text-left text-sm text-gray-600 border-collapse"> <table className="w-full text-left text-sm text-gray-600 border-collapse">

View File

@ -77,13 +77,16 @@ export function RecordView({
const [internalSortBy, setInternalSortBy] = useState<string | undefined>(sortBy); const [internalSortBy, setInternalSortBy] = useState<string | undefined>(sortBy);
const [internalSortDir, setInternalSortDir] = useState<'asc' | 'desc' | undefined>(sortDir); const [internalSortDir, setInternalSortDir] = useState<'asc' | 'desc' | undefined>(sortDir);
const [isUserSorted, setIsUserSorted] = useState(false);
useEffect(() => { useEffect(() => {
setInternalSortBy(sortBy); setInternalSortBy(sortBy);
setInternalSortDir(sortDir); setInternalSortDir(sortDir);
setIsUserSorted(false);
}, [sortBy, sortDir]); }, [sortBy, sortDir]);
const handleSort = (fieldKey: string) => { const handleSort = (fieldKey: string) => {
setIsUserSorted(true);
if (internalSortBy === fieldKey) { if (internalSortBy === fieldKey) {
if (internalSortDir === 'asc') { if (internalSortDir === 'asc') {
setInternalSortDir('desc'); setInternalSortDir('desc');
@ -190,10 +193,8 @@ export function RecordView({
.map((k) => byKey.get(k) ?? ({ field_key: k, output_label: k, data_type: 'string', is_filter: false, is_search: false } as RecordViewField)) .map((k) => byKey.get(k) ?? ({ field_key: k, output_label: k, data_type: 'string', is_filter: false, is_search: false } as RecordViewField))
.filter(Boolean); .filter(Boolean);
} }
if (omitColumns) { const omitSet = new Set([...(omitColumns || []), 'instance_id']);
const omitSet = new Set(omitColumns);
result = result.filter((f) => !omitSet.has(f.field_key)); result = result.filter((f) => !omitSet.has(f.field_key));
}
return result; return result;
}, [resp, columns, omitColumns]); }, [resp, columns, omitColumns]);
@ -368,7 +369,7 @@ export function RecordView({
> >
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
{f.output_label} {f.output_label}
{internalSortBy === f.field_key && ( {internalSortBy === f.field_key && isUserSorted && (
internalSortDir === 'asc' ? <ChevronUp size={14} className="text-ruby-600" /> : <ChevronDown size={14} className="text-ruby-600" /> internalSortDir === 'asc' ? <ChevronUp size={14} className="text-ruby-600" /> : <ChevronDown size={14} className="text-ruby-600" />
)} )}
</div> </div>

View File

@ -79,6 +79,10 @@ export function formatValue(value: unknown, fieldKey?: string): string {
if ('business_name_2' in o && o.business_name_2 != null) return String(o.business_name_2); if ('business_name_2' in o && o.business_name_2 != null) return String(o.business_name_2);
if ('business_name_3' in o && o.business_name_3 != null) return String(o.business_name_3); if ('business_name_3' in o && o.business_name_3 != null) return String(o.business_name_3);
if ('business_name' in o && o.business_name != null) return String(o.business_name); if ('business_name' in o && o.business_name != null) return String(o.business_name);
// Location JSON
if ('latitude' in o && 'longitude' in o) return `latitude:${o.latitude},longitude:${o.longitude}`;
if ('lat' in o && 'lng' in o) return `latitude:${o.lat},longitude:${o.lng}`;
} }
try { try {

View File

@ -5,7 +5,8 @@ import {
SALES_REPORT_ROUTES, SALES_REPORT_ROUTES,
ROUTE_WISE_DISTRIBUTORS, ROUTE_WISE_DISTRIBUTORS,
BASE_URL, BASE_URL,
PIPELINE PIPELINE,
MAX_SKUS_PER_CHUNK
} from '../api/config'; } from '../api/config';
import { Download, Printer } from 'lucide-react'; import { Download, Printer } from 'lucide-react';
import jsPDF from 'jspdf'; import jsPDF from 'jspdf';
@ -72,29 +73,58 @@ export function DailySalesReportPage() {
// Meta Info // Meta Info
doc.setFontSize(10); doc.setFontSize(10);
doc.text(`Distributor Name : ${distributor}`, 14, 40);
doc.text(`Date : ${date}`, rightMargin, 40, { align: 'right' }); let leftY = 40;
if (distributor) {
doc.text(`Distributor Name : ${distributor}`, 14, leftY);
leftY += 5;
}
let rightY = 40;
if (date) {
doc.text(`Date : ${date}`, rightMargin, rightY, { align: 'right' });
rightY += 5;
}
if (soEmail) {
const selectedSo = soOptions.find(o => o.value === soEmail); const selectedSo = soOptions.find(o => o.value === soEmail);
doc.text(`SO Name : ${selectedSo ? selectedSo.label : soEmail}`, rightMargin, 45, { align: 'right' }); doc.text(`SO Name : ${selectedSo ? selectedSo.label : soEmail}`, rightMargin, rightY, { align: 'right' });
rightY += 5;
}
}; };
// Split headers into chunks to avoid jspdf-autotable's horizontal page break bugs // Split headers into chunks of exactly MAX_SKUS_PER_CHUNK, even if it means splitting a brand.
// This allows us to keep the 2-tier header perfectly intact and break at heading boundaries.
const MAX_SKUS_PER_CHUNK = 14;
const headerChunks: any[][] = []; const headerChunks: any[][] = [];
let currentChunk: any[] = []; let currentChunk: any[] = [];
let currentSkuCount = 0; let currentSkuCount = 0;
reportData?.response?.headers?.forEach((h: any) => { reportData?.response?.headers?.forEach((h: any) => {
const skus = h.skus || []; const allSkus = h.skus || [];
if (currentSkuCount + skus.length > MAX_SKUS_PER_CHUNK && currentSkuCount > 0) { if (allSkus.length === 0) return;
let remainingSkus = [...allSkus];
while (remainingSkus.length > 0) {
const availableSpace = MAX_SKUS_PER_CHUNK - currentSkuCount;
// Take as many SKUs as we can fit in the current chunk
const skusToTake = remainingSkus.splice(0, availableSpace);
currentChunk.push({
...h,
skus: skusToTake
});
currentSkuCount += skusToTake.length;
if (currentSkuCount >= MAX_SKUS_PER_CHUNK) {
headerChunks.push(currentChunk); headerChunks.push(currentChunk);
currentChunk = []; currentChunk = [];
currentSkuCount = 0; currentSkuCount = 0;
} }
currentChunk.push(h); }
currentSkuCount += skus.length;
}); });
if (currentChunk.length > 0) { if (currentChunk.length > 0) {
headerChunks.push(currentChunk); headerChunks.push(currentChunk);
} }
@ -161,7 +191,7 @@ export function DailySalesReportPage() {
startY: 50, startY: 50,
theme: 'grid', theme: 'grid',
horizontalPageBreak: false, horizontalPageBreak: false,
styles: { fontSize: 6, textColor: [0, 0, 0], lineColor: [0, 0, 0], lineWidth: 0.2, cellPadding: 1, minCellWidth: 10 }, styles: { fontSize: 6, textColor: [0, 0, 0], lineColor: [0, 0, 0], lineWidth: 0.2, cellPadding: 1, minCellWidth: 10, halign: 'center', valign: 'middle' },
headStyles: { fillColor: [243, 244, 246], fontStyle: 'bold', halign: 'center' }, headStyles: { fillColor: [243, 244, 246], fontStyle: 'bold', halign: 'center' },
didParseCell: (data) => { didParseCell: (data) => {
if (data.section === 'body' && reportData?.response?.total_row && data.row.index === body.length - 1) { if (data.section === 'body' && reportData?.response?.total_row && data.row.index === body.length - 1) {
@ -289,10 +319,12 @@ export function DailySalesReportPage() {
</div> </div>
<div className="flex justify-between items-start text-sm font-bold text-strong mb-2"> <div className="flex justify-between items-start text-sm font-bold text-strong mb-2">
<div>Distributor Name : {distributor}</div> <div className="flex flex-col gap-1.5">
{distributor && <div>Distributor Name : {distributor}</div>}
</div>
<div className="text-right flex flex-col gap-1.5"> <div className="text-right flex flex-col gap-1.5">
<div>Date : {date}</div> {date && <div>Date : {date}</div>}
<div>SO Name : {soOptions.find(o => o.value === soEmail)?.label || soEmail}</div> {soEmail && <div>SO Name : {soOptions.find(o => o.value === soEmail)?.label || soEmail}</div>}
</div> </div>
</div> </div>