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 { 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];
|
||||||
|
@@ -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;
|
||||||
|
@@ -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;
|
||||||
|
Reference in New Issue
Block a user