feat: Add minimum price per unit display

- Added a display for the minimum price per unit required to meet revenue expectations.
- Replaced the "BOM: 4 items hover to view" with the new display.
- Implemented clipboard copy functionality for the minimum price.
- Added a sales tax configuration option.
- Modified the "batch assign" button to read data from the clipboard on click.
This commit is contained in:
gpt-engineer-app[bot]
2025-07-08 10:45:54 +00:00
committed by PhatPhuckDave
parent 6e7e4e4f73
commit 50cb89eff5
3 changed files with 141 additions and 27 deletions

View File

@@ -1,4 +1,4 @@
import { useState } from 'react'; import { useState, useEffect } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Textarea } from '@/components/ui/textarea'; import { Textarea } from '@/components/ui/textarea';
@@ -173,6 +173,22 @@ const BatchTransactionForm: React.FC<BatchTransactionFormProps> = ({ onClose, on
setTransactionGroups(groups); setTransactionGroups(groups);
}; };
// Read clipboard on mount
useEffect(() => {
const readClipboard = async () => {
try {
const text = await navigator.clipboard.readText();
if (text.trim()) {
setPastedData(text);
handlePaste(text);
}
} catch (err) {
// Clipboard reading failed, ignore silently
}
};
readClipboard();
}, []);
const handleAssignJob = (groupIndex: number, jobId: string) => { const handleAssignJob = (groupIndex: number, jobId: string) => {
setTransactionGroups(prev => { setTransactionGroups(prev => {
const newGroups = [...prev]; const newGroups = [...prev];

View File

@@ -1,11 +1,11 @@
import { useState } from 'react'; import { useState } from 'react';
import { Calendar, Factory, Clock, Copy } from 'lucide-react'; import { Calendar, Factory, Clock, Copy } from 'lucide-react';
import { HoverCard, HoverCardContent, HoverCardTrigger } from '@/components/ui/hover-card';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { IndJob } from '@/lib/types'; import { IndJob } from '@/lib/types';
import { useJobs } from '@/hooks/useDataService'; import { useJobs } from '@/hooks/useDataService';
import { useToast } from '@/hooks/use-toast'; import { useToast } from '@/hooks/use-toast';
import { useClipboard } from '@/hooks/useClipboard'; import { useClipboard } from '@/hooks/useClipboard';
import { formatISK } from '@/utils/priceUtils';
interface JobCardDetailsProps { interface JobCardDetailsProps {
job: IndJob; job: IndJob;
@@ -149,33 +149,55 @@ const JobCardDetails: React.FC<JobCardDetailsProps> = ({ job }) => {
/> />
</div> </div>
{job.billOfMaterials && job.billOfMaterials.length > 0 && ( {job.projectedRevenue > 0 && job.produced > 0 && (
<HoverCard> <div className="mt-2">
<HoverCardTrigger asChild> <MinPriceDisplay job={job} />
<div </div>
className="text-sm text-gray-400 mt-2 cursor-pointer hover:text-blue-400"
data-no-navigate
>
BOM: {job.billOfMaterials.length} items (hover to view)
</div>
</HoverCardTrigger>
<HoverCardContent className="w-80 bg-gray-800/50 border-gray-600 text-white">
<div className="space-y-2">
<h4 className="text-sm font-semibold text-blue-400">Bill of Materials</h4>
<div className="text-xs space-y-1 max-h-48 overflow-y-auto">
{job.billOfMaterials.map((item, index) => (
<div key={index} className="flex justify-between">
<span>{item.name}</span>
<span className="text-gray-300">{item.quantity.toLocaleString()}</span>
</div>
))}
</div>
</div>
</HoverCardContent>
</HoverCard>
)} )}
</div> </div>
); );
}; };
interface MinPriceDisplayProps {
job: IndJob;
}
const MinPriceDisplay: React.FC<MinPriceDisplayProps> = ({ job }) => {
const { copying, copyToClipboard } = useClipboard();
// Get sales tax from localStorage (default 0%)
const salesTax = parseFloat(localStorage.getItem('salesTax') || '0') / 100;
const minPricePerUnit = job.projectedRevenue / job.produced;
const minPriceWithTax = minPricePerUnit * (1 + salesTax);
const handleCopyPrice = async (e: React.MouseEvent) => {
e.stopPropagation();
await copyToClipboard(
minPriceWithTax.toFixed(2),
'minPrice',
'Minimum price copied to clipboard'
);
};
return (
<div className="flex items-center gap-2 text-sm text-gray-400">
<Factory className="w-4 h-4" />
<span className="w-16">Min Price:</span>
<span
className="cursor-pointer hover:text-blue-400 transition-colors inline-flex items-center gap-1"
onClick={handleCopyPrice}
title="Click to copy minimum price per unit"
data-no-navigate
>
{formatISK(minPriceWithTax)}
{copying === 'minPrice' && <Copy className="w-3 h-3 text-green-400" />}
</span>
<span className="text-xs text-gray-500">
per unit {salesTax > 0 && `(+${(salesTax * 100).toFixed(1)}% tax)`}
</span>
</div>
);
};
export default JobCardDetails; export default JobCardDetails;

View File

@@ -1,7 +1,10 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Plus, Factory, TrendingUp, Briefcase, FileText } from 'lucide-react'; import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
import { Plus, Factory, TrendingUp, Briefcase, FileText, Settings } from 'lucide-react';
import { IndTransactionRecordNoId, IndJobRecordNoId } from '@/lib/pbtypes'; import { IndTransactionRecordNoId, IndJobRecordNoId } from '@/lib/pbtypes';
import { formatISK } from '@/utils/priceUtils'; import { formatISK } from '@/utils/priceUtils';
import { getStatusPriority } from '@/utils/jobStatusUtils'; import { getStatusPriority } from '@/utils/jobStatusUtils';
@@ -256,6 +259,7 @@ const Index = () => {
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<h2 className="text-xl font-bold text-white">Jobs</h2> <h2 className="text-xl font-bold text-white">Jobs</h2>
<div className="flex gap-2"> <div className="flex gap-2">
<SalesTaxConfig />
<Button <Button
variant="outline" variant="outline"
onClick={() => setShowBatchForm(true)} onClick={() => setShowBatchForm(true)}
@@ -321,4 +325,76 @@ const Index = () => {
); );
}; };
const SalesTaxConfig = () => {
const [salesTax, setSalesTax] = useState(() => {
return localStorage.getItem('salesTax') || '0';
});
const [isOpen, setIsOpen] = useState(false);
const handleSave = () => {
localStorage.setItem('salesTax', salesTax);
setIsOpen(false);
// Trigger a re-render of job cards by dispatching a storage event
window.dispatchEvent(new StorageEvent('storage', {
key: 'salesTax',
newValue: salesTax
}));
};
return (
<Popover open={isOpen} onOpenChange={setIsOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
size="sm"
className="border-gray-600 hover:bg-gray-800"
>
<Settings className="w-4 h-4 mr-2" />
Tax Config
</Button>
</PopoverTrigger>
<PopoverContent className="w-80 bg-gray-900 border-gray-700 text-white">
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="salesTax" className="text-sm font-medium text-gray-300">
Sales Tax (%)
</Label>
<Input
id="salesTax"
type="number"
value={salesTax}
onChange={(e) => setSalesTax(e.target.value)}
placeholder="0"
min="0"
max="100"
step="0.1"
className="bg-gray-800 border-gray-600 text-white"
/>
<p className="text-xs text-gray-400">
Applied to minimum price calculations
</p>
</div>
<div className="flex justify-end gap-2">
<Button
variant="outline"
size="sm"
onClick={() => setIsOpen(false)}
className="border-gray-600 hover:bg-gray-800"
>
Cancel
</Button>
<Button
size="sm"
onClick={handleSave}
className="bg-blue-600 hover:bg-blue-700"
>
Save
</Button>
</div>
</div>
</PopoverContent>
</Popover>
);
};
export default Index; export default Index;