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 { CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
@@ -32,6 +33,7 @@ const JobCardHeader: React.FC<JobCardHeaderProps> = ({
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<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) => {
if (job.status !== 'Closed') {
setIsEditingProduced(true);
@@ -228,47 +251,61 @@ const JobCardHeader: React.FC<JobCardHeaderProps> = ({
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-2">
<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}
title="Click to copy job name"
data-no-navigate
style={{ lineHeight: '1.4' }}
>
{job.outputItem}
{copyingName && <Copy className="w-4 h-4 text-green-400" />}
</CardTitle>
</div>
<p className="text-gray-400 text-sm">
Runs: {job.outputQuantity.toLocaleString()}
<span className="ml-4">
Produced: {
isEditingProduced && job.status !== 'Closed' ? (
<Input
type="number"
value={producedValue}
onChange={(e) => 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
/>
) : (
<span
onClick={handleProducedClick}
className={`inline-block w-20 h-5 leading-5 text-left ${job.status !== 'Closed' ? "cursor-pointer hover:text-blue-400" : ""}`}
title={job.status !== 'Closed' ? "Click to edit" : undefined}
data-no-navigate
>
{(job.produced || 0).toLocaleString()}
</span>
)
}
</span>
<span className="ml-4">
Sold: <span className="text-green-400">{itemsSold.toLocaleString()}</span>
</span>
</p>
<div className="text-gray-400 text-sm leading-relaxed" style={{ lineHeight: '1.4' }}>
<div className="mb-1">
Runs: {job.outputQuantity.toLocaleString()}
<span className="ml-4">
Produced: {
isEditingProduced && job.status !== 'Closed' ? (
<Input
type="number"
value={producedValue}
onChange={(e) => 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
/>
) : (
<span
onClick={handleProducedClick}
className={`inline-block w-20 h-5 leading-5 text-left ${job.status !== 'Closed' ? "cursor-pointer hover:text-blue-400" : ""}`}
title={job.status !== 'Closed' ? "Click to edit" : undefined}
data-no-navigate
>
{(job.produced || 0).toLocaleString()}
</span>
)
}
</span>
<span className="ml-4">
Sold: <span className="text-green-400">{itemsSold.toLocaleString()}</span>
</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 className="flex flex-col gap-2 flex-shrink-0 items-end">
<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 { 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 = () => {
</CardTitle>
</CardHeader>
<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>
</Card>
<Card className="bg-gray-900 border-gray-700 text-white">
@@ -265,9 +283,15 @@ const Index = () => {
</CardTitle>
</CardHeader>
<CardContent>
<div className={`text-2xl font-bold ${totalProfit >= 0 ? 'text-green-400' : 'text-red-400'}`}>
{formatISK(totalProfit)}
</div>
<RecapPopover
title="Profit Breakdown"
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>
</Card>
</div>