Fix importing the bill of materials

This commit is contained in:
2025-07-06 22:24:01 +02:00
parent a598d6c15f
commit dc1c8f4136
5 changed files with 96 additions and 40 deletions

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}
> >
@@ -232,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}
@@ -266,18 +277,18 @@ const JobCard: React.FC<JobCardProps> = ({
</Button> </Button>
</div> </div>
<div className="flex gap-1 justify-end"> <div className="flex gap-1 justify-end">
<Button <Button
variant="ghost" variant="ghost"
size="sm" size="sm"
className="p-1 h-6 w-6" className="p-1 h-6 w-6"
onClick={handleImportClick} onClick={handleImportClick}
title="Import BOM from clipboard" title="Import BOM from clipboard"
> >
<Import className="w-4 h-4 text-blue-400" /> <Import className="w-4 h-4 text-blue-400" />
</Button> </Button>
<Button <Button
variant="ghost" variant="ghost"
size="sm" size="sm"
className="p-1 h-6 w-6" className="p-1 h-6 w-6"
onClick={handleExportClick} onClick={handleExportClick}
disabled={!job.billOfMaterials?.length} disabled={!job.billOfMaterials?.length}
@@ -359,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)}%
@@ -377,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)}%
@@ -399,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

@@ -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];
} }