reports page added
This commit is contained in:
parent
9be169396e
commit
3861605df1
@ -8,6 +8,7 @@ import { StoresPage } from './screens/StoresPage'
|
|||||||
import { DailyLogsPage } from './screens/DailyLogsPage'
|
import { DailyLogsPage } from './screens/DailyLogsPage'
|
||||||
import { AllDailyLogsPage } from './screens/AllDailyLogsPage'
|
import { AllDailyLogsPage } from './screens/AllDailyLogsPage'
|
||||||
import { AnalyticsPage } from './screens/AnalyticsPage'
|
import { AnalyticsPage } from './screens/AnalyticsPage'
|
||||||
|
import { DailySalesReportPage } from './screens/DailySalesReportPage'
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
@ -33,6 +34,7 @@ function App() {
|
|||||||
|
|
||||||
|
|
||||||
<Route path="/analytics" element={<AnalyticsPage />} />
|
<Route path="/analytics" element={<AnalyticsPage />} />
|
||||||
|
<Route path="/sales-report" element={<DailySalesReportPage />} />
|
||||||
</Route>
|
</Route>
|
||||||
<Route path="*" element={<Navigate to="/daily" replace />} />
|
<Route path="*" element={<Navigate to="/daily" replace />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
|
|||||||
@ -175,3 +175,31 @@ export const WORKFLOWS = {
|
|||||||
store: STORE,
|
store: STORE,
|
||||||
dailyReports: DAILY_REPORTS,
|
dailyReports: DAILY_REPORTS,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
// --- Daily Sales Report Constants ---
|
||||||
|
export const SALES_REPORT_ROUTES = [
|
||||||
|
'Krishna',
|
||||||
|
'East-1',
|
||||||
|
'East-2',
|
||||||
|
'East-3',
|
||||||
|
'East-4',
|
||||||
|
'West-1',
|
||||||
|
'West-2',
|
||||||
|
'West-3',
|
||||||
|
'West-4'
|
||||||
|
];
|
||||||
|
|
||||||
|
export const SALES_REPORT_DISTRIBUTORS = [
|
||||||
|
'Banashree Multi Millet Flour',
|
||||||
|
'Shiva nandi Enterprises',
|
||||||
|
'BPK & Co',
|
||||||
|
'Gaviranga',
|
||||||
|
'Bhagwan Enterprises',
|
||||||
|
'Shreshtaa Trading Co',
|
||||||
|
'Rajarajeshwari Enterprises',
|
||||||
|
'Byraveshwara Trading Co'
|
||||||
|
];
|
||||||
|
|
||||||
|
export const SALES_REPORT_SO_NAMES = [
|
||||||
|
'Surya C'
|
||||||
|
];
|
||||||
|
|||||||
@ -27,7 +27,7 @@ export function ConsoleLayout() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-[100dvh] overflow-hidden bg-app flex flex-col">
|
<div className="h-[100dvh] overflow-hidden bg-app flex flex-col">
|
||||||
<header className="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">
|
||||||
<div className="flex items-center gap-3 flex-1">
|
<div className="flex items-center gap-3 flex-1">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@ -51,10 +51,10 @@ export function ConsoleLayout() {
|
|||||||
</main>
|
</main>
|
||||||
|
|
||||||
<nav
|
<nav
|
||||||
className="fixed bottom-0 inset-x-0 z-20 h-16 pb-[env(safe-area-inset-bottom)] bg-card border-t border-border-subtle grid shadow-[0_-2px_16px_rgba(11,27,59,0.06)]"
|
className="print:hidden fixed bottom-0 inset-x-0 z-20 h-16 pb-[env(safe-area-inset-bottom)] bg-card border-t border-border-subtle grid shadow-[0_-2px_16px_rgba(11,27,59,0.06)]"
|
||||||
style={{ gridTemplateColumns: `repeat(${SCREENS.filter(t => t.key !== 'analytics' && t.key !== 'all-daily').length}, minmax(0, 1fr))` }}
|
style={{ gridTemplateColumns: `repeat(${SCREENS.filter(t => t.key !== 'analytics' && t.key !== 'all-daily' && t.key !== 'sales-report').length}, minmax(0, 1fr))` }}
|
||||||
>
|
>
|
||||||
{SCREENS.filter(t => t.key !== 'analytics' && t.key !== 'all-daily').map((t) => {
|
{SCREENS.filter(t => t.key !== 'analytics' && t.key !== 'all-daily' && t.key !== 'sales-report').map((t) => {
|
||||||
const Icon = t.icon;
|
const Icon = t.icon;
|
||||||
return (
|
return (
|
||||||
<NavLink
|
<NavLink
|
||||||
|
|||||||
230
src/screens/DailySalesReportPage.tsx
Normal file
230
src/screens/DailySalesReportPage.tsx
Normal file
@ -0,0 +1,230 @@
|
|||||||
|
import { useState, type FormEvent } from 'react';
|
||||||
|
import { Card, Input, Select } from '../components/reusable';
|
||||||
|
import { Button } from '../components/buttons';
|
||||||
|
import {
|
||||||
|
SALES_REPORT_ROUTES,
|
||||||
|
SALES_REPORT_DISTRIBUTORS,
|
||||||
|
SALES_REPORT_SO_NAMES
|
||||||
|
} from '../api/config';
|
||||||
|
|
||||||
|
export function DailySalesReportPage() {
|
||||||
|
const [date, setDate] = useState('');
|
||||||
|
const [route, setRoute] = useState('');
|
||||||
|
const [distributor, setDistributor] = useState('');
|
||||||
|
const [soName, setSoName] = useState('');
|
||||||
|
|
||||||
|
const [busy, setBusy] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [reportData, setReportData] = useState<any>(null);
|
||||||
|
|
||||||
|
const printTable = () => {
|
||||||
|
const tableHtml = document.getElementById('report-table')?.outerHTML;
|
||||||
|
if (!tableHtml) return;
|
||||||
|
|
||||||
|
const printWindow = window.open('', '', 'height=600,width=800');
|
||||||
|
if (!printWindow) return;
|
||||||
|
|
||||||
|
printWindow.document.write(`
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Daily Sales Report</title>
|
||||||
|
<style>
|
||||||
|
body { font-family: sans-serif; padding: 20px; margin: 0; }
|
||||||
|
.header-block { text-align: center; margin-bottom: 20px; }
|
||||||
|
.header-block h1 { margin: 0; font-size: 18px; font-weight: bold; }
|
||||||
|
.header-block h2 { margin: 4px 0 12px; font-size: 14px; font-weight: normal; }
|
||||||
|
.header-block h3 { margin: 0 0 20px; font-size: 16px; font-weight: bold; text-decoration: underline; }
|
||||||
|
.meta-info { display: flex; justify-content: space-between; font-size: 14px; margin-bottom: 16px; font-weight: bold; }
|
||||||
|
.meta-right { text-align: right; line-height: 1.5; }
|
||||||
|
table { width: 100%; border-collapse: collapse; font-size: 12px; border: 1px solid black; }
|
||||||
|
th, td { border: 1px solid black; padding: 6px; text-align: left; }
|
||||||
|
th { font-weight: bold; background-color: #f3f4f6; text-align: center; }
|
||||||
|
.text-center { text-align: center; }
|
||||||
|
.align-bottom { vertical-align: bottom; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="header-block">
|
||||||
|
<h1>Krishna Flour Mills (Bangalore) Pvt. Limited</h1>
|
||||||
|
<h2>19, Platform Road, Bengaluru - 560 020</h2>
|
||||||
|
<h3>DAILY ORDER REPORT</h3>
|
||||||
|
</div>
|
||||||
|
<div class="meta-info">
|
||||||
|
<div>Distributor Name : ${distributor}</div>
|
||||||
|
<div class="meta-right">
|
||||||
|
<div>Date : ${date}</div>
|
||||||
|
<div>SO Name : ${soName}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
${tableHtml}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`);
|
||||||
|
|
||||||
|
printWindow.document.close();
|
||||||
|
printWindow.focus();
|
||||||
|
setTimeout(() => {
|
||||||
|
printWindow.print();
|
||||||
|
printWindow.close();
|
||||||
|
}, 250);
|
||||||
|
};
|
||||||
|
|
||||||
|
async function submit(e: FormEvent) {
|
||||||
|
e.preventDefault();
|
||||||
|
setBusy(true);
|
||||||
|
setError(null);
|
||||||
|
setReportData(null);
|
||||||
|
try {
|
||||||
|
const res = await fetch('https://sandbox.getzino.in/api/papi2/daily-sales-report', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'TemplateID': '157',
|
||||||
|
'Accept': 'application/json, text/plain, */*',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'sec-ch-ua-platform': '"Android"',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
date,
|
||||||
|
route,
|
||||||
|
distributor,
|
||||||
|
so_name: soName,
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error(`Error: ${res.status} ${res.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await res.json();
|
||||||
|
setReportData(data);
|
||||||
|
} catch (err) {
|
||||||
|
setError((err as Error).message ?? 'Failed to fetch report');
|
||||||
|
} finally {
|
||||||
|
setBusy(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-5">
|
||||||
|
<h1 className="m-0 text-xl font-extrabold text-strong tracking-[-0.01em] print:hidden">Daily Sales Report</h1>
|
||||||
|
|
||||||
|
<Card className="print:hidden">
|
||||||
|
<form onSubmit={submit} className="flex flex-col gap-4">
|
||||||
|
<Input
|
||||||
|
label="Date"
|
||||||
|
type="date"
|
||||||
|
value={date}
|
||||||
|
onChange={(e) => setDate(e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
label="Route"
|
||||||
|
value={route}
|
||||||
|
onChange={(e) => setRoute(e.target.value)}
|
||||||
|
options={[{ value: '', label: 'Select Route' }, ...SALES_REPORT_ROUTES]}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
label="Distributor"
|
||||||
|
value={distributor}
|
||||||
|
onChange={(e) => setDistributor(e.target.value)}
|
||||||
|
options={[{ value: '', label: 'Select Distributor' }, ...SALES_REPORT_DISTRIBUTORS]}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
label="SO Name"
|
||||||
|
value={soName}
|
||||||
|
onChange={(e) => setSoName(e.target.value)}
|
||||||
|
options={[{ value: '', label: 'Select SO Name' }, ...SALES_REPORT_SO_NAMES]}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
{error && <div className="text-xs text-ruby-600 font-medium">{error}</div>}
|
||||||
|
|
||||||
|
<Button type="submit" full disabled={busy}>
|
||||||
|
{busy ? 'Fetching...' : 'Get Report'}
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{reportData?.response && (
|
||||||
|
<Card className="overflow-x-auto p-0 pb-2 border-0 shadow-none print:m-0 print:p-0">
|
||||||
|
<div className="p-4 pb-2 print:hidden">
|
||||||
|
<h2 className="text-lg font-bold text-strong m-0">Report Details</h2>
|
||||||
|
</div>
|
||||||
|
<div className="w-full overflow-x-auto p-4 pt-2">
|
||||||
|
<div className="min-w-max">
|
||||||
|
<div className="flex flex-col items-center text-center gap-1">
|
||||||
|
<h1 className="m-0 text-lg font-bold text-strong">Krishna Flour Mills (Bangalore) Pvt. Limited</h1>
|
||||||
|
<h2 className="m-0 text-sm font-normal text-strong">19, Platform Road, Bengaluru - 560 020</h2>
|
||||||
|
<h3 className="m-0 text-base font-bold text-strong underline mt-1 mb-4">DAILY ORDER REPORT</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-between items-start text-sm font-bold text-strong mb-2">
|
||||||
|
<div>Distributor Name : {distributor}</div>
|
||||||
|
<div className="text-right flex flex-col gap-1.5">
|
||||||
|
<div>Date : {date}</div>
|
||||||
|
<div>SO Name : {soName}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table id="report-table" className="w-full text-sm text-left border border-black border-collapse mt-2">
|
||||||
|
<thead>
|
||||||
|
<tr className="border-b border-black bg-black/5">
|
||||||
|
<th className="py-2 px-3 font-bold text-black align-bottom border-r border-black" rowSpan={2}>Sl No</th>
|
||||||
|
<th className="py-2 px-3 font-bold text-black align-bottom" rowSpan={2}>Store</th>
|
||||||
|
{reportData.response.headers?.map((h: any) => (
|
||||||
|
<th key={h.br_code} colSpan={h.skus?.length || 1} className="py-1 px-3 font-bold text-black text-center border-l border-black">
|
||||||
|
{h.br_code}
|
||||||
|
</th>
|
||||||
|
))}
|
||||||
|
<th className="py-2 px-3 font-bold text-black align-bottom border-l border-black" rowSpan={2}>Total</th>
|
||||||
|
</tr>
|
||||||
|
<tr className="border-b border-black bg-black/5">
|
||||||
|
{reportData.response.headers?.flatMap((h: any) => h.skus || []).map((sku: string, idx: number) => (
|
||||||
|
<th key={`${sku}-${idx}`} className="py-1 px-3 font-bold text-black text-center border-l border-black whitespace-nowrap">
|
||||||
|
{sku}
|
||||||
|
</th>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{reportData.response.rows?.map((row: any) => (
|
||||||
|
<tr key={row.sl_no} className="border-b border-black hover:bg-black/5 transition-colors">
|
||||||
|
<td className="py-2 px-3 text-strong border-r border-black">{row.sl_no}</td>
|
||||||
|
<td className="py-2 px-3 text-strong whitespace-nowrap">{row.store}</td>
|
||||||
|
{reportData.response.headers?.flatMap((h: any) => h.skus || []).map((sku: string, idx: number) => (
|
||||||
|
<td key={`${sku}-${idx}`} className="py-2 px-3 text-strong text-center border-l border-black">
|
||||||
|
{row.products?.[sku] ?? 0}
|
||||||
|
</td>
|
||||||
|
))}
|
||||||
|
<td className="py-2 px-3 text-strong font-bold text-center border-l border-black bg-black/5">{row.total ?? 0}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
{reportData.response.total_row && (
|
||||||
|
<tr className="border-b border-black bg-black/10 font-bold">
|
||||||
|
<td className="py-2 px-3 text-strong border-r border-black">{reportData.response.total_row.sl_no}</td>
|
||||||
|
<td className="py-2 px-3 text-strong whitespace-nowrap text-right">{reportData.response.total_row.store}</td>
|
||||||
|
{reportData.response.headers?.flatMap((h: any) => h.skus || []).map((sku: string, idx: number) => (
|
||||||
|
<td key={`total-${sku}-${idx}`} className="py-2 px-3 text-strong text-center border-l border-black">
|
||||||
|
{reportData.response.total_row.products?.[sku] ?? 0}
|
||||||
|
</td>
|
||||||
|
))}
|
||||||
|
<td className="py-2 px-3 text-strong text-center border-l border-black">{reportData.response.total_row.total ?? 0}</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{(!reportData.response.rows || reportData.response.rows.length === 0) && (
|
||||||
|
<div className="py-8 text-center text-faint">No records found.</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="p-4 pt-2 flex justify-center print:hidden">
|
||||||
|
<Button type="button" onClick={printTable} full>Print / Save PDF</Button>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
import { Store, Phone, ShoppingCart, ClipboardList, Home, type LucideIcon } from 'lucide-react';
|
import { Store, Phone, ShoppingCart, ClipboardList, Home, FileText, type LucideIcon } from 'lucide-react';
|
||||||
import {
|
import {
|
||||||
OrdersView,
|
OrdersView,
|
||||||
CallsView,
|
CallsView,
|
||||||
@ -15,9 +15,10 @@ import {
|
|||||||
} from '../components/dv';
|
} from '../components/dv';
|
||||||
|
|
||||||
import { AnalyticsPage } from './AnalyticsPage';
|
import { AnalyticsPage } from './AnalyticsPage';
|
||||||
|
import { DailySalesReportPage } from './DailySalesReportPage';
|
||||||
import { BarChart2 } from 'lucide-react';
|
import { BarChart2 } from 'lucide-react';
|
||||||
|
|
||||||
export type ScreenKey = 'orders' | 'calls' | 'stores' | 'daily' | 'analytics' | 'all-daily';
|
export type ScreenKey = 'orders' | 'calls' | 'stores' | 'daily' | 'analytics' | 'all-daily' | 'sales-report';
|
||||||
|
|
||||||
export interface ScreenDef {
|
export interface ScreenDef {
|
||||||
key: ScreenKey;
|
key: ScreenKey;
|
||||||
@ -31,6 +32,7 @@ export interface ScreenDef {
|
|||||||
|
|
||||||
export const SCREENS: ScreenDef[] = [
|
export const SCREENS: ScreenDef[] = [
|
||||||
{ key: 'analytics', label: 'Analytics', icon: BarChart2, View: AnalyticsPage as any },
|
{ key: 'analytics', label: 'Analytics', icon: BarChart2, View: AnalyticsPage as any },
|
||||||
|
{ key: 'sales-report', label: 'Sales Report', icon: FileText, View: DailySalesReportPage as any },
|
||||||
{ key: 'daily', label: 'Home', icon: Home, View: DailyLogsView, Detail: DailyLogDetail, noun: 'Daily Log' },
|
{ key: 'daily', label: 'Home', icon: Home, View: DailyLogsView, Detail: DailyLogDetail, noun: 'Daily Log' },
|
||||||
{ key: 'orders', label: 'Orders', icon: ShoppingCart, View: OrdersView, Detail: OrderDetail, noun: 'Order' },
|
{ key: 'orders', label: 'Orders', icon: ShoppingCart, View: OrdersView, Detail: OrderDetail, noun: 'Order' },
|
||||||
{ key: 'calls', label: 'Calls', icon: Phone, View: CallsView, Detail: CallDetail, noun: 'Call' },
|
{ key: 'calls', label: 'Calls', icon: Phone, View: CallsView, Detail: CallDetail, noun: 'Call' },
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user