Compare commits
2 Commits
c24e13c29a
...
819b43bd77
| Author | SHA1 | Date | |
|---|---|---|---|
| 819b43bd77 | |||
| a600121e0d |
@ -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;
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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'
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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';
|
||||||
@ -16,7 +17,7 @@ export function DailySalesReportPage() {
|
|||||||
const [route, setRoute] = useState('');
|
const [route, setRoute] = useState('');
|
||||||
const [distributor, setDistributor] = useState('');
|
const [distributor, setDistributor] = useState('');
|
||||||
const [soEmail, setSoEmail] = useState('');
|
const [soEmail, setSoEmail] = useState('');
|
||||||
const [soOptions, setSoOptions] = useState<{value: string, label: string}[]>([]);
|
const [soOptions, setSoOptions] = useState<{ value: string, label: string }[]>([]);
|
||||||
|
|
||||||
const [busy, setBusy] = useState(false);
|
const [busy, setBusy] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
@ -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>
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user