This commit is contained in:
2025-07-06 22:25:20 +02:00
parent eaf5295843
commit 01286cf9fd
27 changed files with 216 additions and 153 deletions

3
copy.sh Normal file
View File

@@ -0,0 +1,3 @@
cd ..
cp -a industrializer/src/* industrializer-wails/frontend/src/
cd industrializer-wails

View File

@@ -11,9 +11,11 @@
will-change: filter; will-change: filter;
transition: filter 300ms; transition: filter 300ms;
} }
.logo:hover { .logo:hover {
filter: drop-shadow(0 0 2em #646cffaa); filter: drop-shadow(0 0 2em #646cffaa);
} }
.logo.react:hover { .logo.react:hover {
filter: drop-shadow(0 0 2em #61dafbaa); filter: drop-shadow(0 0 2em #61dafbaa);
} }
@@ -22,6 +24,7 @@
from { from {
transform: rotate(0deg); transform: rotate(0deg);
} }
to { to {
transform: rotate(360deg); transform: rotate(360deg);
} }
@@ -39,4 +42,4 @@
.read-the-docs { .read-the-docs {
color: #888; color: #888;
} }

View File

@@ -32,11 +32,11 @@ const BatchTransactionForm: React.FC<BatchTransactionFormProps> = ({ onClose, on
const [pastedData, setPastedData] = useState(''); const [pastedData, setPastedData] = useState('');
const [transactionGroups, setTransactionGroups] = useState<TransactionGroup[]>([]); const [transactionGroups, setTransactionGroups] = useState<TransactionGroup[]>([]);
const [duplicatesFound, setDuplicatesFound] = useState(0); const [duplicatesFound, setDuplicatesFound] = useState(0);
// Filter jobs that are either running, selling, or tracked // Filter jobs that are either running, selling, or tracked
const eligibleJobs = jobs.filter(job => const eligibleJobs = jobs.filter(job =>
job.status === IndJobStatusOptions.Running || job.status === IndJobStatusOptions.Running ||
job.status === IndJobStatusOptions.Selling || job.status === IndJobStatusOptions.Selling ||
job.status === IndJobStatusOptions.Tracked job.status === IndJobStatusOptions.Tracked
); );
@@ -46,7 +46,7 @@ const BatchTransactionForm: React.FC<BatchTransactionFormProps> = ({ onClose, on
if (exactMatch) return exactMatch.id; if (exactMatch) return exactMatch.id;
// Then try case-insensitive match // Then try case-insensitive match
const caseInsensitiveMatch = eligibleJobs.find(job => const caseInsensitiveMatch = eligibleJobs.find(job =>
job.outputItem.toLowerCase() === itemName.toLowerCase() job.outputItem.toLowerCase() === itemName.toLowerCase()
); );
if (caseInsensitiveMatch) return caseInsensitiveMatch.id; if (caseInsensitiveMatch) return caseInsensitiveMatch.id;
@@ -69,7 +69,7 @@ const BatchTransactionForm: React.FC<BatchTransactionFormProps> = ({ onClose, on
parsed.buyer, parsed.buyer,
parsed.location parsed.location
].join('|'); ].join('|');
console.log('Created key from parsed transaction:', { console.log('Created key from parsed transaction:', {
key, key,
date: normalizeDate(parsed.date.toISOString()), date: normalizeDate(parsed.date.toISOString()),
itemName: parsed.itemName, itemName: parsed.itemName,
@@ -128,13 +128,13 @@ const BatchTransactionForm: React.FC<BatchTransactionFormProps> = ({ onClose, on
if (parsed) { if (parsed) {
const transactionKey = createTransactionKey(parsed); const transactionKey = createTransactionKey(parsed);
const isDuplicate = seenTransactions.has(transactionKey); const isDuplicate = seenTransactions.has(transactionKey);
console.log('Transaction check:', { console.log('Transaction check:', {
key: transactionKey, key: transactionKey,
isDuplicate, isDuplicate,
setSize: seenTransactions.size, setSize: seenTransactions.size,
setContains: Array.from(seenTransactions).includes(transactionKey) setContains: Array.from(seenTransactions).includes(transactionKey)
}); });
if (isDuplicate) { if (isDuplicate) {
console.log('DUPLICATE FOUND:', transactionKey); console.log('DUPLICATE FOUND:', transactionKey);
duplicates++; duplicates++;
@@ -225,7 +225,7 @@ const BatchTransactionForm: React.FC<BatchTransactionFormProps> = ({ onClose, on
onClose(); onClose();
}; };
const allAssigned = transactionGroups.every(group => const allAssigned = transactionGroups.every(group =>
group.transactions.every(tx => tx.assignedJobId) group.transactions.every(tx => tx.assignedJobId)
); );
@@ -287,8 +287,8 @@ const BatchTransactionForm: React.FC<BatchTransactionFormProps> = ({ onClose, on
const matchingJob = autoAssigned ? jobs.find(j => j.id === autoAssigned) : undefined; const matchingJob = autoAssigned ? jobs.find(j => j.id === autoAssigned) : undefined;
return ( return (
<TableRow <TableRow
key={group.itemName} key={group.itemName}
className={`border-gray-700 ${isDuplicate ? 'bg-red-900/30' : ''}`} className={`border-gray-700 ${isDuplicate ? 'bg-red-900/30' : ''}`}
> >
<TableCell className="text-white flex items-center gap-2"> <TableCell className="text-white flex items-center gap-2">
@@ -315,7 +315,7 @@ const BatchTransactionForm: React.FC<BatchTransactionFormProps> = ({ onClose, on
value={group.transactions[0]?.assignedJobId || ''} value={group.transactions[0]?.assignedJobId || ''}
onValueChange={(value) => handleAssignJob(index, value)} onValueChange={(value) => handleAssignJob(index, value)}
> >
<SelectTrigger <SelectTrigger
className={`bg-gray-800 border-gray-600 text-white ${autoAssigned ? 'border-green-600' : ''}`} className={`bg-gray-800 border-gray-600 text-white ${autoAssigned ? 'border-green-600' : ''}`}
> >
<SelectValue placeholder={autoAssigned ? `Auto-assigned to ${matchingJob?.outputItem}` : 'Select a job'} /> <SelectValue placeholder={autoAssigned ? `Auto-assigned to ${matchingJob?.outputItem}` : 'Select a job'} />
@@ -324,8 +324,8 @@ const BatchTransactionForm: React.FC<BatchTransactionFormProps> = ({ onClose, on
{eligibleJobs {eligibleJobs
.filter(job => job.outputItem.includes(group.itemName) || job.status === 'Tracked') .filter(job => job.outputItem.includes(group.itemName) || job.status === 'Tracked')
.map(job => ( .map(job => (
<SelectItem <SelectItem
key={job.id} key={job.id}
value={job.id} value={job.id}
className="text-white" className="text-white"
> >

View File

@@ -19,13 +19,13 @@ interface JobCardProps {
isTracked?: boolean; isTracked?: boolean;
} }
const JobCard: React.FC<JobCardProps> = ({ const JobCard: React.FC<JobCardProps> = ({
job, job,
onEdit, onEdit,
onDelete, onDelete,
onUpdateProduced, onUpdateProduced,
onImportBOM, onImportBOM,
isTracked = false isTracked = false
}) => { }) => {
const navigate = useNavigate(); const navigate = useNavigate();
const [isEditingProduced, setIsEditingProduced] = useState(false); const [isEditingProduced, setIsEditingProduced] = useState(false);
@@ -33,10 +33,10 @@ const JobCard: React.FC<JobCardProps> = ({
const [copyingBom, setCopyingBom] = useState(false); const [copyingBom, setCopyingBom] = useState(false);
const { toast } = useToast(); const { toast } = useToast();
const sortedExpenditures = [...job.expenditures].sort((a, b) => const sortedExpenditures = [...job.expenditures].sort((a, b) =>
new Date(b.date).getTime() - new Date(a.date).getTime() new Date(b.date).getTime() - new Date(a.date).getTime()
); );
const sortedIncome = [...job.income].sort((a, b) => const sortedIncome = [...job.income].sort((a, b) =>
new Date(b.date).getTime() - new Date(a.date).getTime() new Date(b.date).getTime() - new Date(a.date).getTime()
); );
@@ -95,23 +95,34 @@ const JobCard: React.FC<JobCardProps> = ({
}; };
const importBillOfMaterials = async () => { const importBillOfMaterials = async () => {
if (!onImportBOM) {
toast({
title: "Error",
description: "Import functionality is not available",
variant: "destructive",
duration: 2000,
});
return;
}
try { try {
const clipboardText = await navigator.clipboard.readText(); const clipboardText = await navigator.clipboard.readText();
const lines = clipboardText.split('\n').filter(line => line.trim()); const lines = clipboardText.split('\n').filter(line => line.trim());
const items: { name: string; quantity: number }[] = []; const items: { name: string; quantity: number }[] = [];
for (const line of lines) { for (const line of lines) {
const parts = line.trim().split(/\s+/); const parts = line.trim().split(/[\s\t]+/);
if (parts.length >= 2) { if (parts.length >= 2) {
const name = parts.slice(0, -1).join(' '); const name = parts.slice(0, -1).join(' ');
const quantity = parseInt(parts[parts.length - 1]); const quantityPart = parts[parts.length - 1].replace(/,/g, '');
const quantity = parseInt(quantityPart);
if (name && !isNaN(quantity)) { if (name && !isNaN(quantity)) {
items.push({ name, quantity }); items.push({ name, quantity });
} }
} }
} }
if (items.length > 0 && onImportBOM) { if (items.length > 0) {
onImportBOM(job.id, items); onImportBOM(job.id, items);
toast({ toast({
title: "BOM Imported", title: "BOM Imported",
@@ -150,7 +161,7 @@ const JobCard: React.FC<JobCardProps> = ({
const text = job.billOfMaterials const text = job.billOfMaterials
.map(item => `${item.name}\t${item.quantity.toLocaleString()}`) .map(item => `${item.name}\t${item.quantity.toLocaleString()}`)
.join('\n'); .join('\n');
try { try {
await navigator.clipboard.writeText(text); await navigator.clipboard.writeText(text);
setCopyingBom(true); setCopyingBom(true);
@@ -202,7 +213,7 @@ const JobCard: React.FC<JobCardProps> = ({
}; };
return ( return (
<Card <Card
className={`bg-gray-900 border-gray-700 text-white h-full flex flex-col cursor-pointer hover:bg-gray-800/50 transition-colors ${job.status === 'Tracked' ? 'border-l-4 border-l-cyan-600' : ''}`} className={`bg-gray-900 border-gray-700 text-white h-full flex flex-col cursor-pointer hover:bg-gray-800/50 transition-colors ${job.status === 'Tracked' ? 'border-l-4 border-l-cyan-600' : ''}`}
onClick={handleCardClick} onClick={handleCardClick}
> >
@@ -214,31 +225,6 @@ const JobCard: React.FC<JobCardProps> = ({
<Badge className={`${getStatusColor(job.status)} text-white flex-shrink-0`}> <Badge className={`${getStatusColor(job.status)} text-white flex-shrink-0`}>
{job.status} {job.status}
</Badge> </Badge>
<div className="flex gap-1 flex-shrink-0">
<Button
variant="ghost"
size="sm"
className="p-1 h-6 w-6"
onClick={handleImportClick}
title="Import BOM from clipboard"
>
<Import className="w-4 h-4 text-blue-400" />
</Button>
<Button
variant="ghost"
size="sm"
className="p-1 h-6 w-6"
onClick={handleExportClick}
disabled={!job.billOfMaterials?.length}
title="Export BOM to clipboard"
>
{copyingBom ? (
<Check className="w-4 h-4 text-green-400" />
) : (
<Upload className="w-4 h-4 text-blue-400" />
)}
</Button>
</div>
</div> </div>
<p className="text-gray-400 text-sm"> <p className="text-gray-400 text-sm">
Quantity: {job.outputQuantity.toLocaleString()} Quantity: {job.outputQuantity.toLocaleString()}
@@ -257,7 +243,7 @@ const JobCard: React.FC<JobCardProps> = ({
autoFocus autoFocus
/> />
) : ( ) : (
<span <span
onClick={handleProducedClick} onClick={handleProducedClick}
className={job.status !== 'Closed' ? "cursor-pointer hover:text-blue-400" : undefined} className={job.status !== 'Closed' ? "cursor-pointer hover:text-blue-400" : undefined}
title={job.status !== 'Closed' ? "Click to edit" : undefined} title={job.status !== 'Closed' ? "Click to edit" : undefined}
@@ -272,22 +258,49 @@ const JobCard: React.FC<JobCardProps> = ({
</span> </span>
</p> </p>
</div> </div>
<div className="flex gap-2 flex-shrink-0"> <div className="flex flex-col gap-2 flex-shrink-0">
<Button <div className="flex gap-2">
variant="outline" <Button
size="sm" variant="outline"
onClick={handleEditClick} size="sm"
className="border-gray-600 hover:bg-gray-800" onClick={handleEditClick}
> className="border-gray-600 hover:bg-gray-800"
Edit >
</Button> Edit
<Button </Button>
variant="destructive" <Button
size="sm" variant="destructive"
onClick={handleDeleteClick} size="sm"
> onClick={handleDeleteClick}
Delete >
</Button> Delete
</Button>
</div>
<div className="flex gap-1 justify-end">
<Button
variant="ghost"
size="sm"
className="p-1 h-6 w-6"
onClick={handleImportClick}
title="Import BOM from clipboard"
>
<Import className="w-4 h-4 text-blue-400" />
</Button>
<Button
variant="ghost"
size="sm"
className="p-1 h-6 w-6"
onClick={handleExportClick}
disabled={!job.billOfMaterials?.length}
title="Export BOM to clipboard"
>
{copyingBom ? (
<Check className="w-4 h-4 text-green-400" />
) : (
<Upload className="w-4 h-4 text-blue-400" />
)}
</Button>
</div>
</div> </div>
</div> </div>
</CardHeader> </CardHeader>
@@ -357,8 +370,8 @@ const JobCard: React.FC<JobCardProps> = ({
{job.projectedCost > 0 && ( {job.projectedCost > 0 && (
<div className="text-xs text-gray-400"> <div className="text-xs text-gray-400">
vs Projected: {formatISK(job.projectedCost)} vs Projected: {formatISK(job.projectedCost)}
<Badge <Badge
variant={totalExpenditure <= job.projectedCost ? 'default' : 'destructive'} variant={totalExpenditure <= job.projectedCost ? 'default' : 'destructive'}
className="ml-1 text-xs" className="ml-1 text-xs"
> >
{((totalExpenditure / job.projectedCost) * 100).toFixed(1)}% {((totalExpenditure / job.projectedCost) * 100).toFixed(1)}%
@@ -375,8 +388,8 @@ const JobCard: React.FC<JobCardProps> = ({
{job.projectedRevenue > 0 && ( {job.projectedRevenue > 0 && (
<div className="text-xs text-gray-400"> <div className="text-xs text-gray-400">
vs Projected: {formatISK(job.projectedRevenue)} vs Projected: {formatISK(job.projectedRevenue)}
<Badge <Badge
variant={totalIncome >= job.projectedRevenue ? 'default' : 'destructive'} variant={totalIncome >= job.projectedRevenue ? 'default' : 'destructive'}
className="ml-1 text-xs" className="ml-1 text-xs"
> >
{((totalIncome / job.projectedRevenue) * 100).toFixed(1)}% {((totalIncome / job.projectedRevenue) * 100).toFixed(1)}%
@@ -397,8 +410,8 @@ const JobCard: React.FC<JobCardProps> = ({
{job.projectedRevenue > 0 && job.projectedCost > 0 && ( {job.projectedRevenue > 0 && job.projectedCost > 0 && (
<div className="text-xs text-gray-400"> <div className="text-xs text-gray-400">
vs Projected: {formatISK(job.projectedRevenue - job.projectedCost)} vs Projected: {formatISK(job.projectedRevenue - job.projectedCost)}
<Badge <Badge
variant={profit >= (job.projectedRevenue - job.projectedCost) ? 'default' : 'destructive'} variant={profit >= (job.projectedRevenue - job.projectedCost) ? 'default' : 'destructive'}
className="ml-1 text-xs" className="ml-1 text-xs"
> >
{((profit / (job.projectedRevenue - job.projectedCost)) * 100).toFixed(1)}% {((profit / (job.projectedRevenue - job.projectedCost)) * 100).toFixed(1)}%

View File

@@ -1,4 +1,3 @@
import React, { useState } from 'react'; import React, { useState } 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';
@@ -17,8 +16,21 @@ interface JobFormProps {
const formatDateForInput = (dateString: string | undefined | null): string => { const formatDateForInput = (dateString: string | undefined | null): string => {
if (!dateString) return ''; if (!dateString) return '';
// Convert ISO string to datetime-local format (YYYY-MM-DDTHH:MM)
return new Date(dateString).toISOString().slice(0, 16); // Create a date object in local timezone
const date = new Date(dateString);
// Format to YYYY-MM-DD
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
// Format to HH:MM
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
// Combine into format required by datetime-local (YYYY-MM-DDTHH:MM)
return `${year}-${month}-${day}T${hours}:${minutes}`;
}; };
const JobForm: React.FC<JobFormProps> = ({ job, onSubmit, onCancel }) => { const JobForm: React.FC<JobFormProps> = ({ job, onSubmit, onCancel }) => {

View File

@@ -72,7 +72,7 @@ const MaterialsImportExport: React.FC<MaterialsImportExportProps> = ({
const handleImportBom = async () => { const handleImportBom = async () => {
if (!job) return; if (!job) return;
const materials = parseBillOfMaterials(bomInput); const materials = parseBillOfMaterials(bomInput);
if (materials.length > 0) { if (materials.length > 0) {
try { try {
@@ -87,7 +87,7 @@ const MaterialsImportExport: React.FC<MaterialsImportExportProps> = ({
const handleImportConsumed = async () => { const handleImportConsumed = async () => {
if (!job) return; if (!job) return;
const materials = parseConsumedMaterials(consumedInput); const materials = parseConsumedMaterials(consumedInput);
if (materials.length > 0) { if (materials.length > 0) {
try { try {

View File

@@ -7,7 +7,7 @@ import { Badge } from '@/components/ui/badge';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import { parseTransactionLine, formatISK } from '@/utils/priceUtils'; import { parseTransactionLine, formatISK } from '@/utils/priceUtils';
import { IndTransactionRecord, IndTransactionRecordNoId } from '@/lib/pbtypes'; import { IndTransactionRecordNoId } from '@/lib/pbtypes';
import { Check, X } from 'lucide-react'; import { Check, X } from 'lucide-react';
interface TransactionFormProps { interface TransactionFormProps {

View File

@@ -27,7 +27,7 @@ const TransactionTable: React.FC<TransactionTableProps> = ({
const [editingTransaction, setEditingTransaction] = useState<IndTransactionRecord | null>(null); const [editingTransaction, setEditingTransaction] = useState<IndTransactionRecord | null>(null);
// Sort transactions by date descending // Sort transactions by date descending
const sortedTransactions = [...transactions].sort((a, b) => const sortedTransactions = [...transactions].sort((a, b) =>
new Date(b.date).getTime() - new Date(a.date).getTime() new Date(b.date).getTime() - new Date(a.date).getTime()
); );

View File

@@ -25,7 +25,7 @@ const badgeVariants = cva(
export interface BadgeProps export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>, extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {} VariantProps<typeof badgeVariants> { }
function Badge({ className, variant, ...props }: BadgeProps) { function Badge({ className, variant, ...props }: BadgeProps) {
return ( return (

View File

@@ -35,7 +35,7 @@ const buttonVariants = cva(
export interface ButtonProps export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>, extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> { VariantProps<typeof buttonVariants> {
asChild?: boolean asChild?: boolean
} }

View File

@@ -82,13 +82,13 @@ const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
([theme, prefix]) => ` ([theme, prefix]) => `
${prefix} [data-chart=${id}] { ${prefix} [data-chart=${id}] {
${colorConfig ${colorConfig
.map(([key, itemConfig]) => { .map(([key, itemConfig]) => {
const color = const color =
itemConfig.theme?.[theme as keyof typeof itemConfig.theme] || itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
itemConfig.color itemConfig.color
return color ? ` --color-${key}: ${color};` : null return color ? ` --color-${key}: ${color};` : null
}) })
.join("\n")} .join("\n")}
} }
` `
) )
@@ -103,13 +103,13 @@ const ChartTooltip = RechartsPrimitive.Tooltip
const ChartTooltipContent = React.forwardRef< const ChartTooltipContent = React.forwardRef<
HTMLDivElement, HTMLDivElement,
React.ComponentProps<typeof RechartsPrimitive.Tooltip> & React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
React.ComponentProps<"div"> & { React.ComponentProps<"div"> & {
hideLabel?: boolean hideLabel?: boolean
hideIndicator?: boolean hideIndicator?: boolean
indicator?: "line" | "dot" | "dashed" indicator?: "line" | "dot" | "dashed"
nameKey?: string nameKey?: string
labelKey?: string labelKey?: string
} }
>( >(
( (
{ {
@@ -259,10 +259,10 @@ const ChartLegend = RechartsPrimitive.Legend
const ChartLegendContent = React.forwardRef< const ChartLegendContent = React.forwardRef<
HTMLDivElement, HTMLDivElement,
React.ComponentProps<"div"> & React.ComponentProps<"div"> &
Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & { Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
hideIcon?: boolean hideIcon?: boolean
nameKey?: string nameKey?: string
} }
>( >(
( (
{ className, hideIcon = false, payload, verticalAlign = "bottom", nameKey }, { className, hideIcon = false, payload, verticalAlign = "bottom", nameKey },
@@ -326,8 +326,8 @@ function getPayloadConfigFromPayload(
const payloadPayload = const payloadPayload =
"payload" in payload && "payload" in payload &&
typeof payload.payload === "object" && typeof payload.payload === "object" &&
payload.payload !== null payload.payload !== null
? payload.payload ? payload.payload
: undefined : undefined

View File

@@ -21,7 +21,7 @@ const Command = React.forwardRef<
)) ))
Command.displayName = CommandPrimitive.displayName Command.displayName = CommandPrimitive.displayName
interface CommandDialogProps extends DialogProps {} interface CommandDialogProps extends DialogProps { }
const CommandDialog = ({ children, ...props }: CommandDialogProps) => { const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
return ( return (

View File

@@ -11,7 +11,7 @@ const labelVariants = cva(
const Label = React.forwardRef< const Label = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>, React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> & React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
VariantProps<typeof labelVariants> VariantProps<typeof labelVariants>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<LabelPrimitive.Root <LabelPrimitive.Root
ref={ref} ref={ref}

View File

@@ -31,9 +31,9 @@ const ScrollBar = React.forwardRef<
className={cn( className={cn(
"flex touch-none select-none transition-colors", "flex touch-none select-none transition-colors",
orientation === "vertical" && orientation === "vertical" &&
"h-full w-2.5 border-l border-l-transparent p-[1px]", "h-full w-2.5 border-l border-l-transparent p-[1px]",
orientation === "horizontal" && orientation === "horizontal" &&
"h-2.5 flex-col border-t border-t-transparent p-[1px]", "h-2.5 flex-col border-t border-t-transparent p-[1px]",
className className
)} )}
{...props} {...props}

View File

@@ -75,7 +75,7 @@ const SelectContent = React.forwardRef<
className={cn( className={cn(
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", "relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
position === "popper" && position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1", "data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className className
)} )}
position={position} position={position}
@@ -86,7 +86,7 @@ const SelectContent = React.forwardRef<
className={cn( className={cn(
"p-1", "p-1",
position === "popper" && position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]" "h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
)} )}
> >
{children} {children}

View File

@@ -128,4 +128,3 @@ export {
Sheet, SheetClose, Sheet, SheetClose,
SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetOverlay, SheetPortal, SheetTitle, SheetTrigger SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetOverlay, SheetPortal, SheetTitle, SheetTrigger
} }

View File

@@ -612,7 +612,7 @@ const SidebarMenuAction = React.forwardRef<
"peer-data-[size=lg]/menu-button:top-2.5", "peer-data-[size=lg]/menu-button:top-2.5",
"group-data-[collapsible=icon]:hidden", "group-data-[collapsible=icon]:hidden",
showOnHover && showOnHover &&
"group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 peer-data-[active=true]/menu-button:text-sidebar-accent-foreground md:opacity-0", "group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 peer-data-[active=true]/menu-button:text-sidebar-accent-foreground md:opacity-0",
className className
)} )}
{...props} {...props}

View File

@@ -3,7 +3,7 @@ import * as React from "react"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
export interface TextareaProps export interface TextareaProps
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {} extends React.TextareaHTMLAttributes<HTMLTextAreaElement> { }
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>( const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
({ className, ...props }, ref) => { ({ className, ...props }, ref) => {

View File

@@ -41,7 +41,7 @@ const toastVariants = cva(
const Toast = React.forwardRef< const Toast = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Root>, React.ElementRef<typeof ToastPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> & React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
VariantProps<typeof toastVariants> VariantProps<typeof toastVariants>
>(({ className, variant, ...props }, ref) => { >(({ className, variant, ...props }, ref) => {
return ( return (
<ToastPrimitives.Root <ToastPrimitives.Root

View File

@@ -15,7 +15,7 @@ const ToggleGroupContext = React.createContext<
const ToggleGroup = React.forwardRef< const ToggleGroup = React.forwardRef<
React.ElementRef<typeof ToggleGroupPrimitive.Root>, React.ElementRef<typeof ToggleGroupPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Root> & React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Root> &
VariantProps<typeof toggleVariants> VariantProps<typeof toggleVariants>
>(({ className, variant, size, children, ...props }, ref) => ( >(({ className, variant, size, children, ...props }, ref) => (
<ToggleGroupPrimitive.Root <ToggleGroupPrimitive.Root
ref={ref} ref={ref}
@@ -33,7 +33,7 @@ ToggleGroup.displayName = ToggleGroupPrimitive.Root.displayName
const ToggleGroupItem = React.forwardRef< const ToggleGroupItem = React.forwardRef<
React.ElementRef<typeof ToggleGroupPrimitive.Item>, React.ElementRef<typeof ToggleGroupPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Item> & React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Item> &
VariantProps<typeof toggleVariants> VariantProps<typeof toggleVariants>
>(({ className, children, variant, size, ...props }, ref) => { >(({ className, children, variant, size, ...props }, ref) => {
const context = React.useContext(ToggleGroupContext) const context = React.useContext(ToggleGroupContext)

View File

@@ -29,7 +29,7 @@ const toggleVariants = cva(
const Toggle = React.forwardRef< const Toggle = React.forwardRef<
React.ElementRef<typeof TogglePrimitive.Root>, React.ElementRef<typeof TogglePrimitive.Root>,
React.ComponentPropsWithoutRef<typeof TogglePrimitive.Root> & React.ComponentPropsWithoutRef<typeof TogglePrimitive.Root> &
VariantProps<typeof toggleVariants> VariantProps<typeof toggleVariants>
>(({ className, variant, size, ...props }, ref) => ( >(({ className, variant, size, ...props }, ref) => (
<TogglePrimitive.Root <TogglePrimitive.Root
ref={ref} ref={ref}

View File

@@ -33,21 +33,21 @@ type ActionType = typeof actionTypes
type Action = type Action =
| { | {
type: ActionType["ADD_TOAST"] type: ActionType["ADD_TOAST"]
toast: ToasterToast toast: ToasterToast
} }
| { | {
type: ActionType["UPDATE_TOAST"] type: ActionType["UPDATE_TOAST"]
toast: Partial<ToasterToast> toast: Partial<ToasterToast>
} }
| { | {
type: ActionType["DISMISS_TOAST"] type: ActionType["DISMISS_TOAST"]
toastId?: ToasterToast["id"] toastId?: ToasterToast["id"]
} }
| { | {
type: ActionType["REMOVE_TOAST"] type: ActionType["REMOVE_TOAST"]
toastId?: ToasterToast["id"] toastId?: ToasterToast["id"]
} }
interface State { interface State {
toasts: ToasterToast[] toasts: ToasterToast[]
@@ -105,9 +105,9 @@ export const reducer = (state: State, action: Action): State => {
toasts: state.toasts.map((t) => toasts: state.toasts.map((t) =>
t.id === toastId || toastId === undefined t.id === toastId || toastId === undefined
? { ? {
...t, ...t,
open: false, open: false,
} }
: t : t
), ),
} }

View File

@@ -1,5 +1,5 @@
import { createRoot } from 'react-dom/client' import { createRoot } from 'react-dom/client';
import App from './App.tsx' import App from './App.tsx';
import './index.css' import './index.css';
createRoot(document.getElementById("root")!).render(<App />); createRoot(document.getElementById("root")!).render(<App />);

View File

@@ -92,7 +92,7 @@ const Index = () => {
return sum + (income - expenditure); return sum + (income - expenditure);
}, 0); }, 0);
const totalRevenue = regularJobs.reduce((sum, job) => const totalRevenue = regularJobs.reduce((sum, job) =>
sum + job.income.reduce((sum, tx) => sum + tx.totalPrice, 0), 0 sum + job.income.reduce((sum, tx) => sum + tx.totalPrice, 0), 0
); );
@@ -263,7 +263,7 @@ const Index = () => {
<div className="space-y-6"> <div className="space-y-6">
{Object.entries(jobGroups).map(([status, statusJobs]) => ( {Object.entries(jobGroups).map(([status, statusJobs]) => (
<div key={status} className="space-y-4"> <div key={status} className="space-y-4">
<div <div
className="flex items-center gap-3 cursor-pointer select-none p-3 rounded-lg hover:bg-gray-800/50 transition-colors" className="flex items-center gap-3 cursor-pointer select-none p-3 rounded-lg hover:bg-gray-800/50 transition-colors"
onClick={() => toggleGroup(status)} onClick={() => toggleGroup(status)}
> >
@@ -277,7 +277,7 @@ const Index = () => {
<span className="text-gray-400 text-lg">({statusJobs.length} jobs)</span> <span className="text-gray-400 text-lg">({statusJobs.length} jobs)</span>
</h3> </h3>
</div> </div>
{!collapsedGroups[status] && ( {!collapsedGroups[status] && (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{statusJobs.map(job => ( {statusJobs.map(job => (
@@ -299,7 +299,7 @@ const Index = () => {
{trackedJobs.length > 0 && ( {trackedJobs.length > 0 && (
<div className="space-y-4 mt-8 pt-8 border-t border-gray-700"> <div className="space-y-4 mt-8 pt-8 border-t border-gray-700">
<div <div
className="flex items-center gap-3 cursor-pointer select-none p-3 rounded-lg hover:bg-gray-800/50 transition-colors" className="flex items-center gap-3 cursor-pointer select-none p-3 rounded-lg hover:bg-gray-800/50 transition-colors"
onClick={() => toggleGroup('Tracked')} onClick={() => toggleGroup('Tracked')}
> >
@@ -312,7 +312,7 @@ const Index = () => {
<span className="text-gray-400 text-lg">({trackedJobs.length} jobs)</span> <span className="text-gray-400 text-lg">({trackedJobs.length} jobs)</span>
</h2> </h2>
</div> </div>
{!collapsedGroups['Tracked'] && ( {!collapsedGroups['Tracked'] && (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{trackedJobs.map(job => ( {trackedJobs.map(job => (

View File

@@ -1,4 +1,3 @@
import { useParams, useNavigate } from 'react-router-dom'; import { useParams, useNavigate } from 'react-router-dom';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import JobCard from '@/components/JobCard'; import JobCard from '@/components/JobCard';
@@ -21,7 +20,8 @@ const JobDetails = () => {
updateTransaction, updateTransaction,
deleteTransaction, deleteTransaction,
updateJob, updateJob,
deleteJob deleteJob,
createMultipleBillItems
} = useJobs(); } = useJobs();
const job = useJob(jobId || null); const job = useJob(jobId || null);
@@ -99,6 +99,19 @@ const JobDetails = () => {
} }
}; };
const handleImportBOM = async (jobId: string, items: { name: string; quantity: number }[]) => {
try {
const billItems = items.map(item => ({
name: item.name,
quantity: item.quantity,
unitPrice: 0
}));
await createMultipleBillItems(jobId, billItems, 'billOfMaterials');
} catch (error) {
console.error('Error importing BOM:', error);
}
};
if (showJobForm) { if (showJobForm) {
return ( return (
<div className="min-h-screen bg-gray-950 p-6"> <div className="min-h-screen bg-gray-950 p-6">
@@ -139,6 +152,7 @@ const JobDetails = () => {
onEdit={handleEditJob} onEdit={handleEditJob}
onDelete={handleDeleteJob} onDelete={handleDeleteJob}
onUpdateProduced={handleUpdateProduced} onUpdateProduced={handleUpdateProduced}
onImportBOM={handleImportBOM}
/> />
<TransactionForm <TransactionForm
jobId={job.id} jobId={job.id}

View File

@@ -6,5 +6,21 @@ export async function addBillItem(
billItem: IndBillitemRecordNoId billItem: IndBillitemRecordNoId
): Promise<IndBillitemRecord> { ): Promise<IndBillitemRecord> {
console.log('Adding bill item:', billItem); console.log('Adding bill item:', billItem);
return await pb.collection<IndBillitemRecord>('ind_billItem').create(billItem); // Set the job ID in the bill item record
const billItemWithJob = {
...billItem,
job: jobId
};
return await pb.collection<IndBillitemRecord>('ind_billItem').create(billItemWithJob);
}
export async function deleteBillItem(id: string): Promise<void> {
console.log('Deleting bill item:', id);
await pb.collection('ind_billItem').delete(id);
}
export async function deleteBillItems(ids: string[]): Promise<void> {
console.log('Deleting bill items:', ids);
// Delete items in parallel for better performance
await Promise.all(ids.map(id => deleteBillItem(id)));
} }

View File

@@ -1,5 +1,3 @@
import { IndJob } from '@/lib/types'; import { IndJob } from '@/lib/types';
import { IndJobRecord, IndJobRecordNoId, IndTransactionRecord, IndTransactionRecordNoId, IndBillitemRecord, IndBillitemRecordNoId } from '@/lib/pbtypes'; import { IndJobRecord, IndJobRecordNoId, IndTransactionRecord, IndTransactionRecordNoId, IndBillitemRecord, IndBillitemRecordNoId } from '@/lib/pbtypes';
import * as jobService from './jobService'; import * as jobService from './jobService';
@@ -219,6 +217,12 @@ export class DataService {
const job = this.getJob(jobId); const job = this.getJob(jobId);
if (!job) throw new Error(`Job with id ${jobId} not found`); if (!job) throw new Error(`Job with id ${jobId} not found`);
// Delete existing bill items
const existingItemIds = job[type].map(item => item.id);
if (existingItemIds.length > 0) {
await billItemService.deleteBillItems(existingItemIds);
}
const createdBillItems: IndBillitemRecord[] = []; const createdBillItems: IndBillitemRecord[] = [];
// Create all bill items // Create all bill items
@@ -227,17 +231,16 @@ export class DataService {
createdBillItems.push(createdBillItem); createdBillItems.push(createdBillItem);
} }
// Update the job's bill item references in one database call // Update the job's bill item references with ONLY the new IDs
const currentIds = (job[type] || []).map(item => item.id);
const newIds = createdBillItems.map(item => item.id); const newIds = createdBillItems.map(item => item.id);
await jobService.updateJob(jobId, { await jobService.updateJob(jobId, {
[type]: [...currentIds, ...newIds] [type]: newIds // Replace instead of append
}); });
// Update local state // Update local state
const jobIndex = this.jobs.findIndex(j => j.id === jobId); const jobIndex = this.jobs.findIndex(j => j.id === jobId);
if (jobIndex !== -1) { if (jobIndex !== -1) {
this.jobs[jobIndex][type].push(...createdBillItems); this.jobs[jobIndex][type] = createdBillItems; // Replace instead of append
this.notifyListeners(); this.notifyListeners();
return this.jobs[jobIndex]; return this.jobs[jobIndex];
} }