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.
This commit is contained in:
gpt-engineer-app[bot]
2025-07-07 17:40:12 +00:00
committed by PhatPhuckDave
parent 2e576b6d28
commit cb32ccaba9
3 changed files with 172 additions and 37 deletions

View File

@@ -1,3 +1,4 @@
import { useState } from 'react'; import { useState } from 'react';
import { CardTitle } from '@/components/ui/card'; import { CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
@@ -32,6 +33,7 @@ const JobCardHeader: React.FC<JobCardHeaderProps> = ({
const [producedValue, setProducedValue] = useState(job.produced?.toString() || '0'); const [producedValue, setProducedValue] = useState(job.produced?.toString() || '0');
const [copyingBom, setCopyingBom] = useState(false); const [copyingBom, setCopyingBom] = useState(false);
const [copyingName, setCopyingName] = useState(false); const [copyingName, setCopyingName] = useState(false);
const [copyingId, setCopyingId] = useState(false);
const { toast } = useToast(); const { toast } = useToast();
const { updateJob } = useJobs(); const { updateJob } = useJobs();
@@ -196,6 +198,27 @@ const JobCardHeader: React.FC<JobCardHeaderProps> = ({
} }
}; };
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) => { const handleProducedClick = (e: React.MouseEvent) => {
if (job.status !== 'Closed') { if (job.status !== 'Closed') {
setIsEditingProduced(true); setIsEditingProduced(true);
@@ -228,47 +251,61 @@ const JobCardHeader: React.FC<JobCardHeaderProps> = ({
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-2"> <div className="flex items-center gap-2 mb-2">
<CardTitle <CardTitle
className="text-blue-400 truncate cursor-pointer hover:text-blue-300 transition-colors flex items-center gap-1" className="text-blue-400 truncate cursor-pointer hover:text-blue-300 transition-colors flex items-center gap-1 leading-normal"
onClick={handleJobNameClick} onClick={handleJobNameClick}
title="Click to copy job name" title="Click to copy job name"
data-no-navigate data-no-navigate
style={{ lineHeight: '1.4' }}
> >
{job.outputItem} {job.outputItem}
{copyingName && <Copy className="w-4 h-4 text-green-400" />} {copyingName && <Copy className="w-4 h-4 text-green-400" />}
</CardTitle> </CardTitle>
</div> </div>
<p className="text-gray-400 text-sm"> <div className="text-gray-400 text-sm leading-relaxed" style={{ lineHeight: '1.4' }}>
Runs: {job.outputQuantity.toLocaleString()} <div className="mb-1">
<span className="ml-4"> Runs: {job.outputQuantity.toLocaleString()}
Produced: { <span className="ml-4">
isEditingProduced && job.status !== 'Closed' ? ( Produced: {
<Input isEditingProduced && job.status !== 'Closed' ? (
type="number" <Input
value={producedValue} type="number"
onChange={(e) => setProducedValue(e.target.value)} value={producedValue}
onBlur={handleProducedUpdate} onChange={(e) => setProducedValue(e.target.value)}
onKeyDown={handleProducedKeyPress} onBlur={handleProducedUpdate}
className="w-24 h-5 px-2 py-0 inline-block bg-gray-800 border-gray-600 text-white text-xs leading-5" onKeyDown={handleProducedKeyPress}
min="0" className="w-24 h-5 px-2 py-0 inline-block bg-gray-800 border-gray-600 text-white text-xs leading-5"
autoFocus min="0"
data-no-navigate autoFocus
/> data-no-navigate
) : ( />
<span ) : (
onClick={handleProducedClick} <span
className={`inline-block w-20 h-5 leading-5 text-left ${job.status !== 'Closed' ? "cursor-pointer hover:text-blue-400" : ""}`} onClick={handleProducedClick}
title={job.status !== 'Closed' ? "Click to edit" : undefined} className={`inline-block w-20 h-5 leading-5 text-left ${job.status !== 'Closed' ? "cursor-pointer hover:text-blue-400" : ""}`}
data-no-navigate title={job.status !== 'Closed' ? "Click to edit" : undefined}
> data-no-navigate
{(job.produced || 0).toLocaleString()} >
</span> {(job.produced || 0).toLocaleString()}
) </span>
} )
</span> }
<span className="ml-4"> </span>
Sold: <span className="text-green-400">{itemsSold.toLocaleString()}</span> <span className="ml-4">
</span> Sold: <span className="text-green-400">{itemsSold.toLocaleString()}</span>
</p> </span>
</div>
<div>
ID: <span
className="cursor-pointer hover:text-blue-400 transition-colors inline-flex items-center gap-1"
onClick={handleJobIdClick}
title="Click to copy job ID"
data-no-navigate
>
{job.id}
{copyingId && <Copy className="w-3 h-3 text-green-400" />}
</span>
</div>
</div>
</div> </div>
<div className="flex flex-col gap-2 flex-shrink-0 items-end"> <div className="flex flex-col gap-2 flex-shrink-0 items-end">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">

View File

@@ -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<RecapPopoverProps> = ({
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 (
<Popover>
<PopoverTrigger asChild>
{children}
</PopoverTrigger>
<PopoverContent className="w-80 bg-gray-800/95 border-gray-600 text-white max-h-96 overflow-y-auto">
<CardHeader className="pb-3">
<CardTitle className="text-lg text-white">{title}</CardTitle>
</CardHeader>
<CardContent className="space-y-2">
{jobContributions.length === 0 ? (
<p className="text-gray-400 text-sm">No contributions to display</p>
) : (
jobContributions.map(({ job, value }) => (
<div
key={job.id}
onClick={() => 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"
>
<div className="flex-1 min-w-0">
<div className="text-sm font-medium text-blue-400 truncate" title={job.outputItem}>
{job.outputItem}
</div>
<div className="text-xs text-gray-400">
ID: {job.id}
</div>
</div>
<div className={`text-sm font-medium ml-2 ${value >= 0 ? 'text-green-400' : 'text-red-400'}`}>
{formatISK(value)}
</div>
</div>
))
)}
</CardContent>
</PopoverContent>
</Popover>
);
};
export default RecapPopover;

View File

@@ -10,6 +10,7 @@ import { IndJob } from '@/lib/types';
import BatchTransactionForm from '@/components/BatchTransactionForm'; import BatchTransactionForm from '@/components/BatchTransactionForm';
import { useJobs } from '@/hooks/useDataService'; import { useJobs } from '@/hooks/useDataService';
import SearchOverlay from '@/components/SearchOverlay'; import SearchOverlay from '@/components/SearchOverlay';
import RecapPopover from '@/components/RecapPopover';
const Index = () => { const Index = () => {
const { const {
@@ -119,6 +120,15 @@ const Index = () => {
sum + job.income.reduce((sum, tx) => sum + tx.totalPrice, 0), 0 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) => { const handleCreateJob = async (jobData: IndJobRecordNoId) => {
try { try {
await createJob(jobData); await createJob(jobData);
@@ -254,7 +264,15 @@ const Index = () => {
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="text-2xl font-bold text-green-400">{formatISK(totalRevenue)}</div> <RecapPopover
title="Revenue Breakdown"
jobs={regularJobs}
calculateJobValue={calculateJobRevenue}
>
<div className="text-2xl font-bold text-green-400 cursor-pointer hover:text-green-300 transition-colors">
{formatISK(totalRevenue)}
</div>
</RecapPopover>
</CardContent> </CardContent>
</Card> </Card>
<Card className="bg-gray-900 border-gray-700 text-white"> <Card className="bg-gray-900 border-gray-700 text-white">
@@ -265,9 +283,15 @@ const Index = () => {
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className={`text-2xl font-bold ${totalProfit >= 0 ? 'text-green-400' : 'text-red-400'}`}> <RecapPopover
{formatISK(totalProfit)} title="Profit Breakdown"
</div> jobs={regularJobs}
calculateJobValue={calculateJobProfit}
>
<div className={`text-2xl font-bold cursor-pointer transition-colors ${totalProfit >= 0 ? 'text-green-400 hover:text-green-300' : 'text-red-400 hover:text-red-300'}`}>
{formatISK(totalProfit)}
</div>
</RecapPopover>
</CardContent> </CardContent>
</Card> </Card>
</div> </div>