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:
@@ -1,4 +1,4 @@
|
||||
import { useState } from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
@@ -173,6 +173,22 @@ const BatchTransactionForm: React.FC<BatchTransactionFormProps> = ({ onClose, on
|
||||
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) => {
|
||||
setTransactionGroups(prev => {
|
||||
const newGroups = [...prev];
|
||||
|
@@ -1,11 +1,11 @@
|
||||
import { useState } from '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 { IndJob } from '@/lib/types';
|
||||
import { useJobs } from '@/hooks/useDataService';
|
||||
import { useToast } from '@/hooks/use-toast';
|
||||
import { useClipboard } from '@/hooks/useClipboard';
|
||||
import { formatISK } from '@/utils/priceUtils';
|
||||
|
||||
interface JobCardDetailsProps {
|
||||
job: IndJob;
|
||||
@@ -149,33 +149,55 @@ const JobCardDetails: React.FC<JobCardDetailsProps> = ({ job }) => {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{job.billOfMaterials && job.billOfMaterials.length > 0 && (
|
||||
<HoverCard>
|
||||
<HoverCardTrigger asChild>
|
||||
<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)
|
||||
{job.projectedRevenue > 0 && job.produced > 0 && (
|
||||
<div className="mt-2">
|
||||
<MinPriceDisplay job={job} />
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
||||
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;
|
||||
|
@@ -1,7 +1,10 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
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 { formatISK } from '@/utils/priceUtils';
|
||||
import { getStatusPriority } from '@/utils/jobStatusUtils';
|
||||
@@ -256,6 +259,7 @@ const Index = () => {
|
||||
<div className="flex justify-between items-center">
|
||||
<h2 className="text-xl font-bold text-white">Jobs</h2>
|
||||
<div className="flex gap-2">
|
||||
<SalesTaxConfig />
|
||||
<Button
|
||||
variant="outline"
|
||||
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;
|
||||
|
Reference in New Issue
Block a user