Fix chart display and add global chart views
- Fixed chart legend cutoff issue. - Improved chart time-based granularity. - Prevented chart details from auto-opening on close. - Added chart icon next to "Sold" and to "Total Revenue" and "Total Profit" sections.
This commit is contained in:
@@ -61,8 +61,19 @@ const JobCardHeader: React.FC<JobCardHeaderProps> = ({
|
|||||||
<span className="ml-4">
|
<span className="ml-4">
|
||||||
Produced: <EditableProduced job={job} onUpdateProduced={onUpdateProduced} />
|
Produced: <EditableProduced job={job} onUpdateProduced={onUpdateProduced} />
|
||||||
</span>
|
</span>
|
||||||
<span className="ml-4">
|
<span className="ml-4 flex items-center gap-1">
|
||||||
Sold: <span className="text-green-400">{itemsSold.toLocaleString()}</span>
|
Sold: <span className="text-green-400">{itemsSold.toLocaleString()}</span>
|
||||||
|
<button
|
||||||
|
className="text-gray-400 hover:text-blue-300 transition-colors"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setOverviewChartOpen(true);
|
||||||
|
}}
|
||||||
|
data-no-navigate
|
||||||
|
title="View transaction charts"
|
||||||
|
>
|
||||||
|
<BarChart3 className="w-3 h-3" />
|
||||||
|
</button>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 text-xs">
|
<div className="flex items-center gap-2 text-xs">
|
||||||
@@ -116,8 +127,8 @@ const JobCardHeader: React.FC<JobCardHeaderProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<TransactionChart
|
<TransactionChart
|
||||||
jobs={jobs}
|
job={job}
|
||||||
type="overview"
|
type="profit"
|
||||||
isOpen={overviewChartOpen}
|
isOpen={overviewChartOpen}
|
||||||
onClose={() => setOverviewChartOpen(false)}
|
onClose={() => setOverviewChartOpen(false)}
|
||||||
/>
|
/>
|
||||||
|
@@ -33,6 +33,25 @@ const TransactionChart: React.FC<TransactionChartProps> = ({
|
|||||||
setHiddenLines(newHidden);
|
setHiddenLines(newHidden);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getSmartTimeFormat = (transactions: any[]) => {
|
||||||
|
if (transactions.length === 0) return 'day';
|
||||||
|
|
||||||
|
// Calculate time span
|
||||||
|
const dates = transactions.map(tx => new Date(tx.date));
|
||||||
|
const minDate = new Date(Math.min(...dates.map(d => d.getTime())));
|
||||||
|
const maxDate = new Date(Math.max(...dates.map(d => d.getTime())));
|
||||||
|
const timeSpanDays = (maxDate.getTime() - minDate.getTime()) / (1000 * 60 * 60 * 24);
|
||||||
|
|
||||||
|
// Calculate transaction density per day
|
||||||
|
const transactionsPerDay = transactions.length / Math.max(timeSpanDays, 1);
|
||||||
|
|
||||||
|
// Smart scoping: if many transactions per day, use hourly; otherwise daily
|
||||||
|
if (transactionsPerDay > 10 && timeSpanDays < 7) {
|
||||||
|
return 'hour';
|
||||||
|
}
|
||||||
|
return 'day';
|
||||||
|
};
|
||||||
|
|
||||||
const getJobChartData = (job: IndJob) => {
|
const getJobChartData = (job: IndJob) => {
|
||||||
// Combine all transactions and group by date
|
// Combine all transactions and group by date
|
||||||
const allTransactions = [
|
const allTransactions = [
|
||||||
@@ -40,11 +59,16 @@ const TransactionChart: React.FC<TransactionChartProps> = ({
|
|||||||
...job.income.map(tx => ({ ...tx, type: 'income' }))
|
...job.income.map(tx => ({ ...tx, type: 'income' }))
|
||||||
];
|
];
|
||||||
|
|
||||||
// Group by date
|
const timeFormat = getSmartTimeFormat(allTransactions);
|
||||||
|
|
||||||
|
// Group by appropriate time unit
|
||||||
const dateMap = new Map<string, { costs: number; revenue: number; date: string }>();
|
const dateMap = new Map<string, { costs: number; revenue: number; date: string }>();
|
||||||
|
|
||||||
allTransactions.forEach(tx => {
|
allTransactions.forEach(tx => {
|
||||||
const dateStr = format(parseISO(tx.date), 'yyyy-MM-dd');
|
const dateStr = timeFormat === 'hour'
|
||||||
|
? format(parseISO(tx.date), 'yyyy-MM-dd HH:00')
|
||||||
|
: format(parseISO(tx.date), 'yyyy-MM-dd');
|
||||||
|
|
||||||
if (!dateMap.has(dateStr)) {
|
if (!dateMap.has(dateStr)) {
|
||||||
dateMap.set(dateStr, { costs: 0, revenue: 0, date: dateStr });
|
dateMap.set(dateStr, { costs: 0, revenue: 0, date: dateStr });
|
||||||
}
|
}
|
||||||
@@ -61,30 +85,46 @@ const TransactionChart: React.FC<TransactionChartProps> = ({
|
|||||||
.map(entry => ({
|
.map(entry => ({
|
||||||
...entry,
|
...entry,
|
||||||
profit: entry.revenue - entry.costs,
|
profit: entry.revenue - entry.costs,
|
||||||
formattedDate: format(new Date(entry.date), 'MMM dd')
|
formattedDate: timeFormat === 'hour'
|
||||||
|
? format(new Date(entry.date), 'MMM dd HH:mm')
|
||||||
|
: format(new Date(entry.date), 'MMM dd')
|
||||||
}))
|
}))
|
||||||
.sort((a, b) => a.date.localeCompare(b.date));
|
.sort((a, b) => a.date.localeCompare(b.date));
|
||||||
};
|
};
|
||||||
|
|
||||||
const getOverviewChartData = (jobs: IndJob[]) => {
|
const getOverviewChartData = (jobs: IndJob[]) => {
|
||||||
|
// Combine all transactions from all jobs
|
||||||
|
const allTransactions = jobs.flatMap(job => [
|
||||||
|
...job.expenditures.map(tx => ({ ...tx, type: 'expenditure' })),
|
||||||
|
...job.income.map(tx => ({ ...tx, type: 'income' }))
|
||||||
|
]);
|
||||||
|
|
||||||
|
const timeFormat = getSmartTimeFormat(allTransactions);
|
||||||
const dateMap = new Map<string, { revenue: number; profit: number; date: string }>();
|
const dateMap = new Map<string, { revenue: number; profit: number; date: string }>();
|
||||||
|
|
||||||
jobs.forEach(job => {
|
allTransactions.forEach(tx => {
|
||||||
const jobData = getJobChartData(job);
|
const dateStr = timeFormat === 'hour'
|
||||||
jobData.forEach(entry => {
|
? format(parseISO(tx.date), 'yyyy-MM-dd HH:00')
|
||||||
if (!dateMap.has(entry.date)) {
|
: format(parseISO(tx.date), 'yyyy-MM-dd');
|
||||||
dateMap.set(entry.date, { revenue: 0, profit: 0, date: entry.date });
|
|
||||||
|
if (!dateMap.has(dateStr)) {
|
||||||
|
dateMap.set(dateStr, { revenue: 0, profit: 0, date: dateStr });
|
||||||
|
}
|
||||||
|
const entry = dateMap.get(dateStr)!;
|
||||||
|
if (tx.type === 'income') {
|
||||||
|
entry.revenue += tx.totalPrice;
|
||||||
|
entry.profit += tx.totalPrice;
|
||||||
|
} else {
|
||||||
|
entry.profit -= tx.totalPrice;
|
||||||
}
|
}
|
||||||
const overviewEntry = dateMap.get(entry.date)!;
|
|
||||||
overviewEntry.revenue += entry.revenue;
|
|
||||||
overviewEntry.profit += entry.profit;
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return Array.from(dateMap.values())
|
return Array.from(dateMap.values())
|
||||||
.map(entry => ({
|
.map(entry => ({
|
||||||
...entry,
|
...entry,
|
||||||
formattedDate: format(new Date(entry.date), 'MMM dd')
|
formattedDate: timeFormat === 'hour'
|
||||||
|
? format(new Date(entry.date), 'MMM dd HH:mm')
|
||||||
|
: format(new Date(entry.date), 'MMM dd')
|
||||||
}))
|
}))
|
||||||
.sort((a, b) => a.date.localeCompare(b.date));
|
.sort((a, b) => a.date.localeCompare(b.date));
|
||||||
};
|
};
|
||||||
@@ -111,10 +151,10 @@ const TransactionChart: React.FC<TransactionChartProps> = ({
|
|||||||
const renderChart = () => {
|
const renderChart = () => {
|
||||||
if (type === 'overview' || type === 'total-revenue' || type === 'total-profit') {
|
if (type === 'overview' || type === 'total-revenue' || type === 'total-profit') {
|
||||||
return (
|
return (
|
||||||
<AreaChart data={data} margin={{ top: 20, right: 30, left: 20, bottom: 5 }}>
|
<AreaChart data={data} margin={{ top: 20, right: 80, left: 80, bottom: 5 }}>
|
||||||
<CartesianGrid strokeDasharray="3 3" stroke="#374151" />
|
<CartesianGrid strokeDasharray="3 3" stroke="#374151" />
|
||||||
<XAxis dataKey="formattedDate" stroke="#9CA3AF" />
|
<XAxis dataKey="formattedDate" stroke="#9CA3AF" />
|
||||||
<YAxis stroke="#9CA3AF" tickFormatter={formatTooltipValue} />
|
<YAxis stroke="#9CA3AF" tickFormatter={formatTooltipValue} width={70} />
|
||||||
<Tooltip
|
<Tooltip
|
||||||
formatter={formatTooltipValue}
|
formatter={formatTooltipValue}
|
||||||
labelStyle={{ color: '#F3F4F6' }}
|
labelStyle={{ color: '#F3F4F6' }}
|
||||||
@@ -149,10 +189,10 @@ const TransactionChart: React.FC<TransactionChartProps> = ({
|
|||||||
|
|
||||||
if (type === 'costs') {
|
if (type === 'costs') {
|
||||||
return (
|
return (
|
||||||
<AreaChart data={data} margin={{ top: 20, right: 30, left: 20, bottom: 5 }}>
|
<AreaChart data={data} margin={{ top: 20, right: 80, left: 80, bottom: 5 }}>
|
||||||
<CartesianGrid strokeDasharray="3 3" stroke="#374151" />
|
<CartesianGrid strokeDasharray="3 3" stroke="#374151" />
|
||||||
<XAxis dataKey="formattedDate" stroke="#9CA3AF" />
|
<XAxis dataKey="formattedDate" stroke="#9CA3AF" />
|
||||||
<YAxis stroke="#9CA3AF" tickFormatter={formatTooltipValue} />
|
<YAxis stroke="#9CA3AF" tickFormatter={formatTooltipValue} width={70} />
|
||||||
<Tooltip
|
<Tooltip
|
||||||
formatter={formatTooltipValue}
|
formatter={formatTooltipValue}
|
||||||
labelStyle={{ color: '#F3F4F6' }}
|
labelStyle={{ color: '#F3F4F6' }}
|
||||||
@@ -172,10 +212,10 @@ const TransactionChart: React.FC<TransactionChartProps> = ({
|
|||||||
|
|
||||||
if (type === 'revenue') {
|
if (type === 'revenue') {
|
||||||
return (
|
return (
|
||||||
<AreaChart data={data} margin={{ top: 20, right: 30, left: 20, bottom: 5 }}>
|
<AreaChart data={data} margin={{ top: 20, right: 80, left: 80, bottom: 5 }}>
|
||||||
<CartesianGrid strokeDasharray="3 3" stroke="#374151" />
|
<CartesianGrid strokeDasharray="3 3" stroke="#374151" />
|
||||||
<XAxis dataKey="formattedDate" stroke="#9CA3AF" />
|
<XAxis dataKey="formattedDate" stroke="#9CA3AF" />
|
||||||
<YAxis stroke="#9CA3AF" tickFormatter={formatTooltipValue} />
|
<YAxis stroke="#9CA3AF" tickFormatter={formatTooltipValue} width={70} />
|
||||||
<Tooltip
|
<Tooltip
|
||||||
formatter={formatTooltipValue}
|
formatter={formatTooltipValue}
|
||||||
labelStyle={{ color: '#F3F4F6' }}
|
labelStyle={{ color: '#F3F4F6' }}
|
||||||
@@ -195,10 +235,10 @@ const TransactionChart: React.FC<TransactionChartProps> = ({
|
|||||||
|
|
||||||
// Combined profit chart (costs, revenue, profit)
|
// Combined profit chart (costs, revenue, profit)
|
||||||
return (
|
return (
|
||||||
<LineChart data={data} margin={{ top: 20, right: 30, left: 20, bottom: 5 }}>
|
<LineChart data={data} margin={{ top: 20, right: 80, left: 80, bottom: 5 }}>
|
||||||
<CartesianGrid strokeDasharray="3 3" stroke="#374151" />
|
<CartesianGrid strokeDasharray="3 3" stroke="#374151" />
|
||||||
<XAxis dataKey="formattedDate" stroke="#9CA3AF" />
|
<XAxis dataKey="formattedDate" stroke="#9CA3AF" />
|
||||||
<YAxis stroke="#9CA3AF" tickFormatter={formatTooltipValue} />
|
<YAxis stroke="#9CA3AF" tickFormatter={formatTooltipValue} width={70} />
|
||||||
<Tooltip
|
<Tooltip
|
||||||
formatter={formatTooltipValue}
|
formatter={formatTooltipValue}
|
||||||
labelStyle={{ color: '#F3F4F6' }}
|
labelStyle={{ color: '#F3F4F6' }}
|
||||||
@@ -243,7 +283,11 @@ const TransactionChart: React.FC<TransactionChartProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={isOpen} onOpenChange={onClose}>
|
<Dialog open={isOpen} onOpenChange={(open) => {
|
||||||
|
if (!open) {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
}}>
|
||||||
<DialogContent className="max-w-6xl w-[90vw] h-[80vh] bg-gray-900 border-gray-700">
|
<DialogContent className="max-w-6xl w-[90vw] h-[80vh] bg-gray-900 border-gray-700">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className="text-white">{getTitle()}</DialogTitle>
|
<DialogTitle className="text-white">{getTitle()}</DialogTitle>
|
||||||
@@ -254,7 +298,15 @@ const TransactionChart: React.FC<TransactionChartProps> = ({
|
|||||||
</ResponsiveContainer>
|
</ResponsiveContainer>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
<Button variant="outline" onClick={onClose} className="border-gray-600">
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onClose();
|
||||||
|
}}
|
||||||
|
className="border-gray-600"
|
||||||
|
data-no-navigate
|
||||||
|
>
|
||||||
Close
|
Close
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
Reference in New Issue
Block a user