From cb32ccaba9970b4c11b9f7bb3997adb9e340757c Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Mon, 7 Jul 2025 17:40:12 +0000 Subject: [PATCH] feat: Implement revenue/profit recap and fixes - Added a hover-over recap for total revenue and profit, displaying contributing jobs in a popup. - Fixed the issue where the lower parts of letters in job names were cut off. - Implemented job ID copy-to-clipboard functionality on click. --- src/components/JobCardHeader.tsx | 103 +++++++++++++++++++++---------- src/components/RecapPopover.tsx | 74 ++++++++++++++++++++++ src/pages/Index.tsx | 32 ++++++++-- 3 files changed, 172 insertions(+), 37 deletions(-) create mode 100644 src/components/RecapPopover.tsx diff --git a/src/components/JobCardHeader.tsx b/src/components/JobCardHeader.tsx index 5b34336..3b7748c 100644 --- a/src/components/JobCardHeader.tsx +++ b/src/components/JobCardHeader.tsx @@ -1,3 +1,4 @@ + import { useState } from 'react'; import { CardTitle } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; @@ -32,6 +33,7 @@ const JobCardHeader: React.FC = ({ const [producedValue, setProducedValue] = useState(job.produced?.toString() || '0'); const [copyingBom, setCopyingBom] = useState(false); const [copyingName, setCopyingName] = useState(false); + const [copyingId, setCopyingId] = useState(false); const { toast } = useToast(); const { updateJob } = useJobs(); @@ -196,6 +198,27 @@ const JobCardHeader: React.FC = ({ } }; + const handleJobIdClick = async (e: React.MouseEvent) => { + e.stopPropagation(); + try { + await navigator.clipboard.writeText(job.id); + setCopyingId(true); + toast({ + title: "Copied!", + description: "Job ID copied to clipboard", + duration: 2000, + }); + setTimeout(() => setCopyingId(false), 1000); + } catch (err) { + toast({ + title: "Error", + description: "Failed to copy to clipboard", + variant: "destructive", + duration: 2000, + }); + } + }; + const handleProducedClick = (e: React.MouseEvent) => { if (job.status !== 'Closed') { setIsEditingProduced(true); @@ -228,47 +251,61 @@ const JobCardHeader: React.FC = ({
{job.outputItem} {copyingName && }
-

- Runs: {job.outputQuantity.toLocaleString()} - - Produced: { - isEditingProduced && job.status !== 'Closed' ? ( - setProducedValue(e.target.value)} - onBlur={handleProducedUpdate} - onKeyDown={handleProducedKeyPress} - className="w-24 h-5 px-2 py-0 inline-block bg-gray-800 border-gray-600 text-white text-xs leading-5" - min="0" - autoFocus - data-no-navigate - /> - ) : ( - - {(job.produced || 0).toLocaleString()} - - ) - } - - - Sold: {itemsSold.toLocaleString()} - -

+
+
+ Runs: {job.outputQuantity.toLocaleString()} + + Produced: { + isEditingProduced && job.status !== 'Closed' ? ( + setProducedValue(e.target.value)} + onBlur={handleProducedUpdate} + onKeyDown={handleProducedKeyPress} + className="w-24 h-5 px-2 py-0 inline-block bg-gray-800 border-gray-600 text-white text-xs leading-5" + min="0" + autoFocus + data-no-navigate + /> + ) : ( + + {(job.produced || 0).toLocaleString()} + + ) + } + + + Sold: {itemsSold.toLocaleString()} + +
+
+ ID: + {job.id} + {copyingId && } + +
+
diff --git a/src/components/RecapPopover.tsx b/src/components/RecapPopover.tsx new file mode 100644 index 0000000..428814b --- /dev/null +++ b/src/components/RecapPopover.tsx @@ -0,0 +1,74 @@ + +import { IndJob } from '@/lib/types'; +import { formatISK } from '@/utils/priceUtils'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; +import { useNavigate } from 'react-router-dom'; + +interface RecapPopoverProps { + title: string; + jobs: IndJob[]; + children: React.ReactNode; + calculateJobValue: (job: IndJob) => number; +} + +const RecapPopover: React.FC = ({ + title, + jobs, + children, + calculateJobValue +}) => { + const navigate = useNavigate(); + + const jobContributions = jobs + .map(job => ({ + job, + value: calculateJobValue(job) + })) + .filter(({ value }) => value !== 0) + .sort((a, b) => Math.abs(b.value) - Math.abs(a.value)); + + const handleJobClick = (jobId: string) => { + navigate(`/${jobId}`); + }; + + return ( + + + {children} + + + + {title} + + + {jobContributions.length === 0 ? ( +

No contributions to display

+ ) : ( + jobContributions.map(({ job, value }) => ( +
handleJobClick(job.id)} + className="flex justify-between items-center p-2 rounded hover:bg-gray-700/50 cursor-pointer transition-colors border-l-2 border-l-gray-600" + > +
+
+ {job.outputItem} +
+
+ ID: {job.id} +
+
+
= 0 ? 'text-green-400' : 'text-red-400'}`}> + {formatISK(value)} +
+
+ )) + )} +
+
+
+ ); +}; + +export default RecapPopover; diff --git a/src/pages/Index.tsx b/src/pages/Index.tsx index ef1f25f..edc9408 100644 --- a/src/pages/Index.tsx +++ b/src/pages/Index.tsx @@ -10,6 +10,7 @@ import { IndJob } from '@/lib/types'; import BatchTransactionForm from '@/components/BatchTransactionForm'; import { useJobs } from '@/hooks/useDataService'; import SearchOverlay from '@/components/SearchOverlay'; +import RecapPopover from '@/components/RecapPopover'; const Index = () => { const { @@ -119,6 +120,15 @@ const Index = () => { sum + job.income.reduce((sum, tx) => sum + tx.totalPrice, 0), 0 ); + const calculateJobRevenue = (job: IndJob) => + job.income.reduce((sum, tx) => sum + tx.totalPrice, 0); + + const calculateJobProfit = (job: IndJob) => { + const expenditure = job.expenditures.reduce((sum, tx) => sum + tx.totalPrice, 0); + const income = job.income.reduce((sum, tx) => sum + tx.totalPrice, 0); + return income - expenditure; + }; + const handleCreateJob = async (jobData: IndJobRecordNoId) => { try { await createJob(jobData); @@ -254,7 +264,15 @@ const Index = () => { -
{formatISK(totalRevenue)}
+ +
+ {formatISK(totalRevenue)} +
+
@@ -265,9 +283,15 @@ const Index = () => { -
= 0 ? 'text-green-400' : 'text-red-400'}`}> - {formatISK(totalProfit)} -
+ +
= 0 ? 'text-green-400 hover:text-green-300' : 'text-red-400 hover:text-red-300'}`}> + {formatISK(totalProfit)} +
+