diff --git a/copy.sh b/copy.sh new file mode 100644 index 0000000..5884f08 --- /dev/null +++ b/copy.sh @@ -0,0 +1,3 @@ +cd .. +cp -a industrializer/src/* industrializer-wails/frontend/src/ +cd industrializer-wails diff --git a/frontend/src/App.css b/frontend/src/App.css index b9d355d..9c7629a 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -11,9 +11,11 @@ will-change: filter; transition: filter 300ms; } + .logo:hover { filter: drop-shadow(0 0 2em #646cffaa); } + .logo.react:hover { filter: drop-shadow(0 0 2em #61dafbaa); } @@ -22,6 +24,7 @@ from { transform: rotate(0deg); } + to { transform: rotate(360deg); } @@ -39,4 +42,4 @@ .read-the-docs { color: #888; -} +} \ No newline at end of file diff --git a/frontend/src/components/BatchTransactionForm.tsx b/frontend/src/components/BatchTransactionForm.tsx index 70cef6a..d331da8 100644 --- a/frontend/src/components/BatchTransactionForm.tsx +++ b/frontend/src/components/BatchTransactionForm.tsx @@ -32,11 +32,11 @@ const BatchTransactionForm: React.FC = ({ onClose, on const [pastedData, setPastedData] = useState(''); const [transactionGroups, setTransactionGroups] = useState([]); const [duplicatesFound, setDuplicatesFound] = useState(0); - + // Filter jobs that are either running, selling, or tracked - const eligibleJobs = jobs.filter(job => - job.status === IndJobStatusOptions.Running || - job.status === IndJobStatusOptions.Selling || + const eligibleJobs = jobs.filter(job => + job.status === IndJobStatusOptions.Running || + job.status === IndJobStatusOptions.Selling || job.status === IndJobStatusOptions.Tracked ); @@ -46,7 +46,7 @@ const BatchTransactionForm: React.FC = ({ onClose, on if (exactMatch) return exactMatch.id; // Then try case-insensitive match - const caseInsensitiveMatch = eligibleJobs.find(job => + const caseInsensitiveMatch = eligibleJobs.find(job => job.outputItem.toLowerCase() === itemName.toLowerCase() ); if (caseInsensitiveMatch) return caseInsensitiveMatch.id; @@ -69,7 +69,7 @@ const BatchTransactionForm: React.FC = ({ onClose, on parsed.buyer, parsed.location ].join('|'); - console.log('Created key from parsed transaction:', { + console.log('Created key from parsed transaction:', { key, date: normalizeDate(parsed.date.toISOString()), itemName: parsed.itemName, @@ -128,13 +128,13 @@ const BatchTransactionForm: React.FC = ({ onClose, on if (parsed) { const transactionKey = createTransactionKey(parsed); const isDuplicate = seenTransactions.has(transactionKey); - console.log('Transaction check:', { - key: transactionKey, + console.log('Transaction check:', { + key: transactionKey, isDuplicate, setSize: seenTransactions.size, setContains: Array.from(seenTransactions).includes(transactionKey) }); - + if (isDuplicate) { console.log('DUPLICATE FOUND:', transactionKey); duplicates++; @@ -225,7 +225,7 @@ const BatchTransactionForm: React.FC = ({ onClose, on onClose(); }; - const allAssigned = transactionGroups.every(group => + const allAssigned = transactionGroups.every(group => group.transactions.every(tx => tx.assignedJobId) ); @@ -287,8 +287,8 @@ const BatchTransactionForm: React.FC = ({ onClose, on const matchingJob = autoAssigned ? jobs.find(j => j.id === autoAssigned) : undefined; return ( - @@ -315,7 +315,7 @@ const BatchTransactionForm: React.FC = ({ onClose, on value={group.transactions[0]?.assignedJobId || ''} onValueChange={(value) => handleAssignJob(index, value)} > - @@ -324,8 +324,8 @@ const BatchTransactionForm: React.FC = ({ onClose, on {eligibleJobs .filter(job => job.outputItem.includes(group.itemName) || job.status === 'Tracked') .map(job => ( - diff --git a/frontend/src/components/JobCard.tsx b/frontend/src/components/JobCard.tsx index 0b865af..c0225de 100644 --- a/frontend/src/components/JobCard.tsx +++ b/frontend/src/components/JobCard.tsx @@ -19,13 +19,13 @@ interface JobCardProps { isTracked?: boolean; } -const JobCard: React.FC = ({ - job, - onEdit, - onDelete, - onUpdateProduced, +const JobCard: React.FC = ({ + job, + onEdit, + onDelete, + onUpdateProduced, onImportBOM, - isTracked = false + isTracked = false }) => { const navigate = useNavigate(); const [isEditingProduced, setIsEditingProduced] = useState(false); @@ -33,10 +33,10 @@ const JobCard: React.FC = ({ const [copyingBom, setCopyingBom] = useState(false); 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() ); - const sortedIncome = [...job.income].sort((a, b) => + const sortedIncome = [...job.income].sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime() ); @@ -95,23 +95,34 @@ const JobCard: React.FC = ({ }; const importBillOfMaterials = async () => { + if (!onImportBOM) { + toast({ + title: "Error", + description: "Import functionality is not available", + variant: "destructive", + duration: 2000, + }); + return; + } + try { const clipboardText = await navigator.clipboard.readText(); const lines = clipboardText.split('\n').filter(line => line.trim()); const items: { name: string; quantity: number }[] = []; - + for (const line of lines) { - const parts = line.trim().split(/\s+/); + const parts = line.trim().split(/[\s\t]+/); if (parts.length >= 2) { 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)) { items.push({ name, quantity }); } } } - - if (items.length > 0 && onImportBOM) { + + if (items.length > 0) { onImportBOM(job.id, items); toast({ title: "BOM Imported", @@ -150,7 +161,7 @@ const JobCard: React.FC = ({ const text = job.billOfMaterials .map(item => `${item.name}\t${item.quantity.toLocaleString()}`) .join('\n'); - + try { await navigator.clipboard.writeText(text); setCopyingBom(true); @@ -202,7 +213,7 @@ const JobCard: React.FC = ({ }; return ( - @@ -214,31 +225,6 @@ const JobCard: React.FC = ({ {job.status} -
- - -

Quantity: {job.outputQuantity.toLocaleString()} @@ -257,7 +243,7 @@ const JobCard: React.FC = ({ autoFocus /> ) : ( - = ({

-
- - +
+
+ + +
+
+ + +
@@ -357,8 +370,8 @@ const JobCard: React.FC = ({ {job.projectedCost > 0 && (
vs Projected: {formatISK(job.projectedCost)} - {((totalExpenditure / job.projectedCost) * 100).toFixed(1)}% @@ -375,8 +388,8 @@ const JobCard: React.FC = ({ {job.projectedRevenue > 0 && (
vs Projected: {formatISK(job.projectedRevenue)} - = job.projectedRevenue ? 'default' : 'destructive'} + = job.projectedRevenue ? 'default' : 'destructive'} className="ml-1 text-xs" > {((totalIncome / job.projectedRevenue) * 100).toFixed(1)}% @@ -397,8 +410,8 @@ const JobCard: React.FC = ({ {job.projectedRevenue > 0 && job.projectedCost > 0 && (
vs Projected: {formatISK(job.projectedRevenue - job.projectedCost)} - = (job.projectedRevenue - job.projectedCost) ? 'default' : 'destructive'} + = (job.projectedRevenue - job.projectedCost) ? 'default' : 'destructive'} className="ml-1 text-xs" > {((profit / (job.projectedRevenue - job.projectedCost)) * 100).toFixed(1)}% diff --git a/frontend/src/components/JobForm.tsx b/frontend/src/components/JobForm.tsx index 633345c..60a95f5 100644 --- a/frontend/src/components/JobForm.tsx +++ b/frontend/src/components/JobForm.tsx @@ -1,4 +1,3 @@ - import React, { useState } from 'react'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; @@ -17,8 +16,21 @@ interface JobFormProps { const formatDateForInput = (dateString: string | undefined | null): string => { 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 = ({ job, onSubmit, onCancel }) => { diff --git a/frontend/src/components/MaterialsImportExport.tsx b/frontend/src/components/MaterialsImportExport.tsx index d0cc244..985652c 100644 --- a/frontend/src/components/MaterialsImportExport.tsx +++ b/frontend/src/components/MaterialsImportExport.tsx @@ -72,7 +72,7 @@ const MaterialsImportExport: React.FC = ({ const handleImportBom = async () => { if (!job) return; - + const materials = parseBillOfMaterials(bomInput); if (materials.length > 0) { try { @@ -87,7 +87,7 @@ const MaterialsImportExport: React.FC = ({ const handleImportConsumed = async () => { if (!job) return; - + const materials = parseConsumedMaterials(consumedInput); if (materials.length > 0) { try { diff --git a/frontend/src/components/TransactionForm.tsx b/frontend/src/components/TransactionForm.tsx index 2a52290..8087be3 100644 --- a/frontend/src/components/TransactionForm.tsx +++ b/frontend/src/components/TransactionForm.tsx @@ -7,7 +7,7 @@ import { Badge } from '@/components/ui/badge'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'; import { parseTransactionLine, formatISK } from '@/utils/priceUtils'; -import { IndTransactionRecord, IndTransactionRecordNoId } from '@/lib/pbtypes'; +import { IndTransactionRecordNoId } from '@/lib/pbtypes'; import { Check, X } from 'lucide-react'; interface TransactionFormProps { diff --git a/frontend/src/components/TransactionTable.tsx b/frontend/src/components/TransactionTable.tsx index 0278a27..5345dca 100644 --- a/frontend/src/components/TransactionTable.tsx +++ b/frontend/src/components/TransactionTable.tsx @@ -27,7 +27,7 @@ const TransactionTable: React.FC = ({ const [editingTransaction, setEditingTransaction] = useState(null); // 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() ); diff --git a/frontend/src/components/ui/badge.tsx b/frontend/src/components/ui/badge.tsx index f000e3e..b47f184 100644 --- a/frontend/src/components/ui/badge.tsx +++ b/frontend/src/components/ui/badge.tsx @@ -25,7 +25,7 @@ const badgeVariants = cva( export interface BadgeProps extends React.HTMLAttributes, - VariantProps {} + VariantProps { } function Badge({ className, variant, ...props }: BadgeProps) { return ( diff --git a/frontend/src/components/ui/button.tsx b/frontend/src/components/ui/button.tsx index 65cf69f..a238d80 100644 --- a/frontend/src/components/ui/button.tsx +++ b/frontend/src/components/ui/button.tsx @@ -35,7 +35,7 @@ const buttonVariants = cva( export interface ButtonProps extends React.ButtonHTMLAttributes, - VariantProps { + VariantProps { asChild?: boolean } diff --git a/frontend/src/components/ui/chart.tsx b/frontend/src/components/ui/chart.tsx index a21d77e..044ea3e 100644 --- a/frontend/src/components/ui/chart.tsx +++ b/frontend/src/components/ui/chart.tsx @@ -82,13 +82,13 @@ const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => { ([theme, prefix]) => ` ${prefix} [data-chart=${id}] { ${colorConfig - .map(([key, itemConfig]) => { - const color = - itemConfig.theme?.[theme as keyof typeof itemConfig.theme] || - itemConfig.color - return color ? ` --color-${key}: ${color};` : null - }) - .join("\n")} + .map(([key, itemConfig]) => { + const color = + itemConfig.theme?.[theme as keyof typeof itemConfig.theme] || + itemConfig.color + return color ? ` --color-${key}: ${color};` : null + }) + .join("\n")} } ` ) @@ -103,13 +103,13 @@ const ChartTooltip = RechartsPrimitive.Tooltip const ChartTooltipContent = React.forwardRef< HTMLDivElement, React.ComponentProps & - React.ComponentProps<"div"> & { - hideLabel?: boolean - hideIndicator?: boolean - indicator?: "line" | "dot" | "dashed" - nameKey?: string - labelKey?: string - } + React.ComponentProps<"div"> & { + hideLabel?: boolean + hideIndicator?: boolean + indicator?: "line" | "dot" | "dashed" + nameKey?: string + labelKey?: string + } >( ( { @@ -259,10 +259,10 @@ const ChartLegend = RechartsPrimitive.Legend const ChartLegendContent = React.forwardRef< HTMLDivElement, React.ComponentProps<"div"> & - Pick & { - hideIcon?: boolean - nameKey?: string - } + Pick & { + hideIcon?: boolean + nameKey?: string + } >( ( { className, hideIcon = false, payload, verticalAlign = "bottom", nameKey }, @@ -326,8 +326,8 @@ function getPayloadConfigFromPayload( const payloadPayload = "payload" in payload && - typeof payload.payload === "object" && - payload.payload !== null + typeof payload.payload === "object" && + payload.payload !== null ? payload.payload : undefined diff --git a/frontend/src/components/ui/command.tsx b/frontend/src/components/ui/command.tsx index 56a0979..7f13c87 100644 --- a/frontend/src/components/ui/command.tsx +++ b/frontend/src/components/ui/command.tsx @@ -21,7 +21,7 @@ const Command = React.forwardRef< )) Command.displayName = CommandPrimitive.displayName -interface CommandDialogProps extends DialogProps {} +interface CommandDialogProps extends DialogProps { } const CommandDialog = ({ children, ...props }: CommandDialogProps) => { return ( diff --git a/frontend/src/components/ui/label.tsx b/frontend/src/components/ui/label.tsx index 683faa7..bd76bd9 100644 --- a/frontend/src/components/ui/label.tsx +++ b/frontend/src/components/ui/label.tsx @@ -11,7 +11,7 @@ const labelVariants = cva( const Label = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef & - VariantProps + VariantProps >(({ className, ...props }, ref) => ( {children} diff --git a/frontend/src/components/ui/sheet.tsx b/frontend/src/components/ui/sheet.tsx index 7b1dbe4..fe07ff0 100644 --- a/frontend/src/components/ui/sheet.tsx +++ b/frontend/src/components/ui/sheet.tsx @@ -128,4 +128,3 @@ export { Sheet, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetOverlay, SheetPortal, SheetTitle, SheetTrigger } - diff --git a/frontend/src/components/ui/sidebar.tsx b/frontend/src/components/ui/sidebar.tsx index 1a566bf..38f1531 100644 --- a/frontend/src/components/ui/sidebar.tsx +++ b/frontend/src/components/ui/sidebar.tsx @@ -612,7 +612,7 @@ const SidebarMenuAction = React.forwardRef< "peer-data-[size=lg]/menu-button:top-2.5", "group-data-[collapsible=icon]:hidden", 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 )} {...props} diff --git a/frontend/src/components/ui/textarea.tsx b/frontend/src/components/ui/textarea.tsx index 9f9a6dc..7d450c9 100644 --- a/frontend/src/components/ui/textarea.tsx +++ b/frontend/src/components/ui/textarea.tsx @@ -3,7 +3,7 @@ import * as React from "react" import { cn } from "@/lib/utils" export interface TextareaProps - extends React.TextareaHTMLAttributes {} + extends React.TextareaHTMLAttributes { } const Textarea = React.forwardRef( ({ className, ...props }, ref) => { diff --git a/frontend/src/components/ui/toast.tsx b/frontend/src/components/ui/toast.tsx index a822477..4af2ace 100644 --- a/frontend/src/components/ui/toast.tsx +++ b/frontend/src/components/ui/toast.tsx @@ -41,7 +41,7 @@ const toastVariants = cva( const Toast = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef & - VariantProps + VariantProps >(({ className, variant, ...props }, ref) => { return ( , React.ComponentPropsWithoutRef & - VariantProps + VariantProps >(({ className, variant, size, children, ...props }, ref) => ( , React.ComponentPropsWithoutRef & - VariantProps + VariantProps >(({ className, children, variant, size, ...props }, ref) => { const context = React.useContext(ToggleGroupContext) diff --git a/frontend/src/components/ui/toggle.tsx b/frontend/src/components/ui/toggle.tsx index 9ecac28..3565e72 100644 --- a/frontend/src/components/ui/toggle.tsx +++ b/frontend/src/components/ui/toggle.tsx @@ -29,7 +29,7 @@ const toggleVariants = cva( const Toggle = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef & - VariantProps + VariantProps >(({ className, variant, size, ...props }, ref) => ( - } + type: ActionType["UPDATE_TOAST"] + toast: Partial + } | { - type: ActionType["DISMISS_TOAST"] - toastId?: ToasterToast["id"] - } + type: ActionType["DISMISS_TOAST"] + toastId?: ToasterToast["id"] + } | { - type: ActionType["REMOVE_TOAST"] - toastId?: ToasterToast["id"] - } + type: ActionType["REMOVE_TOAST"] + toastId?: ToasterToast["id"] + } interface State { toasts: ToasterToast[] @@ -105,9 +105,9 @@ export const reducer = (state: State, action: Action): State => { toasts: state.toasts.map((t) => t.id === toastId || toastId === undefined ? { - ...t, - open: false, - } + ...t, + open: false, + } : t ), } diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index 719464e..9361073 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -1,5 +1,5 @@ -import { createRoot } from 'react-dom/client' -import App from './App.tsx' -import './index.css' +import { createRoot } from 'react-dom/client'; +import App from './App.tsx'; +import './index.css'; createRoot(document.getElementById("root")!).render(); diff --git a/frontend/src/pages/Index.tsx b/frontend/src/pages/Index.tsx index f69e5ff..795a118 100644 --- a/frontend/src/pages/Index.tsx +++ b/frontend/src/pages/Index.tsx @@ -92,7 +92,7 @@ const Index = () => { return sum + (income - expenditure); }, 0); - const totalRevenue = regularJobs.reduce((sum, job) => + const totalRevenue = regularJobs.reduce((sum, job) => sum + job.income.reduce((sum, tx) => sum + tx.totalPrice, 0), 0 ); @@ -263,7 +263,7 @@ const Index = () => {
{Object.entries(jobGroups).map(([status, statusJobs]) => (
-
toggleGroup(status)} > @@ -277,7 +277,7 @@ const Index = () => { ({statusJobs.length} jobs)
- + {!collapsedGroups[status] && (
{statusJobs.map(job => ( @@ -299,7 +299,7 @@ const Index = () => { {trackedJobs.length > 0 && (
-
toggleGroup('Tracked')} > @@ -312,7 +312,7 @@ const Index = () => { ({trackedJobs.length} jobs)
- + {!collapsedGroups['Tracked'] && (
{trackedJobs.map(job => ( diff --git a/frontend/src/pages/JobDetails.tsx b/frontend/src/pages/JobDetails.tsx index 1abca2a..e6810b2 100644 --- a/frontend/src/pages/JobDetails.tsx +++ b/frontend/src/pages/JobDetails.tsx @@ -1,4 +1,3 @@ - import { useParams, useNavigate } from 'react-router-dom'; import { Button } from '@/components/ui/button'; import JobCard from '@/components/JobCard'; @@ -21,7 +20,8 @@ const JobDetails = () => { updateTransaction, deleteTransaction, updateJob, - deleteJob + deleteJob, + createMultipleBillItems } = useJobs(); 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) { return (
@@ -139,6 +152,7 @@ const JobDetails = () => { onEdit={handleEditJob} onDelete={handleDeleteJob} onUpdateProduced={handleUpdateProduced} + onImportBOM={handleImportBOM} /> { console.log('Adding bill item:', billItem); - return await pb.collection('ind_billItem').create(billItem); + // Set the job ID in the bill item record + const billItemWithJob = { + ...billItem, + job: jobId + }; + return await pb.collection('ind_billItem').create(billItemWithJob); +} + +export async function deleteBillItem(id: string): Promise { + console.log('Deleting bill item:', id); + await pb.collection('ind_billItem').delete(id); +} + +export async function deleteBillItems(ids: string[]): Promise { + console.log('Deleting bill items:', ids); + // Delete items in parallel for better performance + await Promise.all(ids.map(id => deleteBillItem(id))); } diff --git a/frontend/src/services/dataService.ts b/frontend/src/services/dataService.ts index 778c23b..dd0e3fe 100644 --- a/frontend/src/services/dataService.ts +++ b/frontend/src/services/dataService.ts @@ -1,5 +1,3 @@ - - import { IndJob } from '@/lib/types'; import { IndJobRecord, IndJobRecordNoId, IndTransactionRecord, IndTransactionRecordNoId, IndBillitemRecord, IndBillitemRecordNoId } from '@/lib/pbtypes'; import * as jobService from './jobService'; @@ -219,6 +217,12 @@ export class DataService { const job = this.getJob(jobId); 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[] = []; // Create all bill items @@ -227,17 +231,16 @@ export class DataService { createdBillItems.push(createdBillItem); } - // Update the job's bill item references in one database call - const currentIds = (job[type] || []).map(item => item.id); + // Update the job's bill item references with ONLY the new IDs const newIds = createdBillItems.map(item => item.id); await jobService.updateJob(jobId, { - [type]: [...currentIds, ...newIds] + [type]: newIds // Replace instead of append }); // Update local state const jobIndex = this.jobs.findIndex(j => j.id === jobId); if (jobIndex !== -1) { - this.jobs[jobIndex][type].push(...createdBillItems); + this.jobs[jobIndex][type] = createdBillItems; // Replace instead of append this.notifyListeners(); return this.jobs[jobIndex]; }