diff --git a/src/components/JobCard.tsx b/src/components/JobCard.tsx index 6e6b80c..3a45dcd 100644 --- a/src/components/JobCard.tsx +++ b/src/components/JobCard.tsx @@ -157,13 +157,13 @@ const JobCard: React.FC = ({ job, onEdit, onDelete, onUpdateProduc }; return ( - - + +
-
+
- {job.outputItem} - + {job.outputItem} + {job.status} {job.billOfMaterials && job.billOfMaterials.length > 0 && ( @@ -172,7 +172,7 @@ const JobCard: React.FC = ({ job, onEdit, onDelete, onUpdateProduc - - -
-

Consumed Materials (click to copy)

-
- {job.consumedMaterials.map((item, index) => ( -
- {item.name} - {item.quantity.toLocaleString()} -
- ))} -
-
-
- - )}
-

+

Quantity: {job.outputQuantity.toLocaleString()} Produced: { @@ -269,7 +234,7 @@ const JobCard: React.FC = ({ job, onEdit, onDelete, onUpdateProduc

-
+
- -
-
- - Created: {formatDateTime(job.created)} -
-
- - Start: {formatDateTime(job.jobStart)} -
-
- - Job ID: {job.id} + +
+
+
+ + Created: {formatDateTime(job.created)} +
+
+ + Start: {formatDateTime(job.jobStart)} +
+
+ + Job ID: {job.id} +
+ + {job.saleStart && ( +
+
+ Sale Period: {formatDateTime(job.saleStart)} - {formatDateTime(job.saleEnd)} +
+ {itemsPerDay > 0 && ( +
+ Items/Day: {itemsPerDay.toFixed(2)} +
+ )} +
+ )}
- {job.saleStart && ( -
-
- Sale Period: {formatDateTime(job.saleStart)} - {formatDateTime(job.saleEnd)} -
- {itemsPerDay > 0 && ( -
- Items/Day: {itemsPerDay.toFixed(2)} -
- )} -
- )} +
-
+
Expenditure
-
{formatISK(totalExpenditure)}
+
{formatISK(totalExpenditure)}
{job.projectedCost > 0 && (
vs Projected: {formatISK(job.projectedCost)} @@ -341,7 +310,7 @@ const JobCard: React.FC = ({ job, onEdit, onDelete, onUpdateProduc Income
-
{formatISK(totalIncome)}
+
{formatISK(totalIncome)}
{job.projectedRevenue > 0 && (
vs Projected: {formatISK(job.projectedRevenue)} @@ -358,7 +327,7 @@ const JobCard: React.FC = ({ job, onEdit, onDelete, onUpdateProduc
Profit
-
= 0 ? 'text-green-400' : 'text-red-400'}`}> +
= 0 ? 'text-green-400' : 'text-red-400'}`}> {formatISK(profit)} = 0 ? 'default' : 'destructive'} className="ml-1 text-xs"> {margin.toFixed(1)}% diff --git a/src/components/JobForm.tsx b/src/components/JobForm.tsx index 6c0ecfd..2982f7c 100644 --- a/src/components/JobForm.tsx +++ b/src/components/JobForm.tsx @@ -1,15 +1,15 @@ + import React, { useState } from 'react'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; -import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; -import { IndJobStatusOptions, IndJobRecordNoId, IndBillitemRecord } from '@/lib/pbtypes'; -import MaterialsImportExport from './MaterialsImportExport'; +import { IndJobStatusOptions, IndJobRecordNoId } from '@/lib/pbtypes'; import { IndJob } from '@/lib/types'; import { parseISKAmount } from '@/utils/priceUtils'; -// import { getFacilities } from '@/services/facilityService'; +import { Import, Export } from 'lucide-react'; +import { useToast } from '@/components/ui/use-toast'; interface JobFormProps { job?: IndJob; @@ -18,38 +18,19 @@ interface JobFormProps { } const JobForm: React.FC = ({ job, onSubmit, onCancel }) => { - // const [facilities, setFacilities] = useState([]); - const [billOfMaterials, setBillOfMaterials] = useState(job?.billOfMaterials || []); - const [consumedMaterials, setConsumedMaterials] = useState(job?.consumedMaterials || []); + const { toast } = useToast(); const [formData, setFormData] = useState({ outputItem: job?.outputItem || '', outputQuantity: job?.outputQuantity || 0, - jobStart: job?.jobStart ? job.jobStart.slice(0, 16) : '', - jobEnd: job?.jobEnd ? job.jobEnd.slice(0, 16) : '', - saleStart: job?.saleStart ? job.saleStart.slice(0, 16) : '', - saleEnd: job?.saleEnd ? job.saleEnd.slice(0, 16) : '', + jobStart: job?.jobStart ? new Date(job.jobStart).toISOString().slice(0, 16) : '', + jobEnd: job?.jobEnd ? new Date(job.jobEnd).toISOString().slice(0, 16) : '', + saleStart: job?.saleStart ? new Date(job.saleStart).toISOString().slice(0, 16) : '', + saleEnd: job?.saleEnd ? new Date(job.saleEnd).toISOString().slice(0, 16) : '', status: job?.status || IndJobStatusOptions.Planned, - billOfMaterials: job?.billOfMaterials || [], - consumedMaterials: job?.consumedMaterials || [], - expenditures: job?.expenditures || [], - income: job?.income || [], projectedCost: job?.projectedCost || 0, projectedRevenue: job?.projectedRevenue || 0 }); - // useEffect(() => { - // const loadFacilities = async () => { - // try { - // const fetchedFacilities = await getFacilities(); - // setFacilities(fetchedFacilities); - // } catch (error) { - // console.error('Error loading facilities:', error); - // } - // }; - - // loadFacilities(); - // }, []); - const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); @@ -61,15 +42,79 @@ const JobForm: React.FC = ({ job, onSubmit, onCancel }) => { saleStart: formData.saleStart ? formData.saleStart : undefined, saleEnd: formData.saleEnd ? formData.saleEnd : undefined, status: formData.status, - billOfMaterials: formData.billOfMaterials.map(item => item.id), - consumedMaterials: formData.consumedMaterials.map(item => item.id), - expenditures: formData.expenditures.map(item => item.id), - income: formData.income.map(item => item.id), + billOfMaterials: job?.billOfMaterials?.map(item => item.id) || [], + consumedMaterials: job?.consumedMaterials?.map(item => item.id) || [], + expenditures: job?.expenditures?.map(item => item.id) || [], + income: job?.income?.map(item => item.id) || [], projectedCost: formData.projectedCost, projectedRevenue: formData.projectedRevenue }); }; + const handleImportBOM = async () => { + try { + const clipboardText = await navigator.clipboard.readText(); + const lines = clipboardText.split('\n').filter(line => line.trim()); + let importedCount = 0; + + for (const line of lines) { + const parts = line.trim().split(/\s+/); + if (parts.length >= 2) { + const name = parts.slice(0, -1).join(' '); + const quantity = parseInt(parts[parts.length - 1]); + if (name && !isNaN(quantity)) { + importedCount++; + } + } + } + + toast({ + title: "Import Preview", + description: `Found ${importedCount} items to import. This is just a preview - actual import functionality needs to be connected.`, + duration: 3000, + }); + } catch (err) { + toast({ + title: "Error", + description: "Failed to read from clipboard", + variant: "destructive", + duration: 2000, + }); + } + }; + + const handleExportBOM = async () => { + if (!job?.billOfMaterials?.length) { + toast({ + title: "Nothing to Export", + description: "No bill of materials found for this job", + variant: "destructive", + duration: 2000, + }); + return; + } + + const text = job.billOfMaterials + .map(item => `${item.name} ${item.quantity}`) + .join('\n'); + + try { + await navigator.clipboard.writeText(text); + toast({ + title: "Exported!", + description: "Bill of materials copied to clipboard", + duration: 2000, + }); + } catch (err) { + toast({ + title: "Error", + description: "Failed to copy to clipboard", + variant: "destructive", + duration: 2000, + }); + } + }; + const statusOptions = Object.values(IndJobStatusOptions); return ( @@ -80,193 +125,195 @@ const JobForm: React.FC = ({ job, onSubmit, onCancel }) => { - - - Basic Info - Materials - +
+
+
+ + setFormData({ + ...formData, + outputItem: e.target.value + })} + className="bg-gray-800 border-gray-600 text-white" + required + /> +
+
+ + setFormData({ + ...formData, + outputQuantity: parseInt(e.target.value) || 0 + })} + className="bg-gray-800 border-gray-600 text-white" + required + /> +
+
- - -
-
- - setFormData({ - ...formData, - outputItem: e.target.value - })} - className="bg-gray-800 border-gray-600 text-white" - required - /> -
-
- - setFormData({ - ...formData, - outputQuantity: parseInt(e.target.value) || 0 - })} - className="bg-gray-800 border-gray-600 text-white" - required - /> -
-
+
+ + +
-
- - setFormData({ + ...formData, + jobStart: e.target.value + })} + className="bg-gray-800 border-gray-600 text-white" + /> +
+
+ + setFormData({ + ...formData, + jobEnd: e.target.value + })} + className="bg-gray-800 border-gray-600 text-white" + /> +
+
+ +
+
+ + setFormData({ + ...formData, + saleStart: e.target.value + })} + className="bg-gray-800 border-gray-600 text-white" + /> +
+
+ + setFormData({ + ...formData, + saleEnd: e.target.value + })} + className="bg-gray-800 border-gray-600 text-white" + /> +
+
+ +
+
+ + setFormData({ + ...formData, + projectedCost: parseISKAmount(e.target.value) + })} + className="bg-gray-800 border-gray-600 text-white" + /> +
+
+ + setFormData({ + ...formData, + projectedRevenue: parseISKAmount(e.target.value) + })} + className="bg-gray-800 border-gray-600 text-white" + /> +
+
+ + {job && ( +
+ +
+
- -
-
- - setFormData({ - ...formData, - jobStart: e.target.value - })} - className="bg-gray-800 border-gray-600 text-white" - /> -
-
- - setFormData({ - ...formData, - jobEnd: e.target.value - })} - className="bg-gray-800 border-gray-600 text-white" - /> -
-
- -
-
- - setFormData({ - ...formData, - saleStart: e.target.value - })} - className="bg-gray-800 border-gray-600 text-white" - /> -
-
- - setFormData({ - ...formData, - saleEnd: e.target.value - })} - className="bg-gray-800 border-gray-600 text-white" - /> -
-
- -
-
- - setFormData({ - ...formData, - projectedCost: parseISKAmount(e.target.value) - })} - className="bg-gray-800 border-gray-600 text-white" - /> -
-
- - setFormData({ - ...formData, - projectedRevenue: parseISKAmount(e.target.value) - })} - className="bg-gray-800 border-gray-600 text-white" - /> -
-
- -
-
- - - - - - -
- - + {job.billOfMaterials && job.billOfMaterials.length > 0 && ( +
+ {job.billOfMaterials.map((item, index) => ( +
{item.name}: {item.quantity.toLocaleString()}
+ ))} +
+ )}
-
- + )} + +
+ + +
+ ); }; export default JobForm; + diff --git a/src/services/dataService.ts b/src/services/dataService.ts index 4662391..38557f0 100644 --- a/src/services/dataService.ts +++ b/src/services/dataService.ts @@ -1,4 +1,5 @@ + import { IndJob } from '@/lib/types'; import { IndJobRecord, IndJobRecordNoId, IndTransactionRecord, IndTransactionRecordNoId, IndBillitemRecord, IndBillitemRecordNoId } from '@/lib/pbtypes'; import * as jobService from './jobService'; @@ -77,10 +78,26 @@ export class DataService { const jobIndex = this.jobs.findIndex(job => job.id === id); if (jobIndex !== -1) { - // Preserve existing relations while updating the record fields + // Only update the scalar fields, preserve the relation arrays + const existingJob = this.jobs[jobIndex]; this.jobs[jobIndex] = { - ...this.jobs[jobIndex], - ...updatedRecord + id: updatedRecord.id, + outputItem: updatedRecord.outputItem, + outputQuantity: updatedRecord.outputQuantity, + status: updatedRecord.status, + created: updatedRecord.created, + updated: updatedRecord.updated, + jobStart: updatedRecord.jobStart, + jobEnd: updatedRecord.jobEnd, + saleStart: updatedRecord.saleStart, + saleEnd: updatedRecord.saleEnd, + produced: updatedRecord.produced, + projectedCost: updatedRecord.projectedCost, + projectedRevenue: updatedRecord.projectedRevenue, + expenditures: existingJob.expenditures, + income: existingJob.income, + billOfMaterials: existingJob.billOfMaterials, + consumedMaterials: existingJob.consumedMaterials }; this.notifyListeners(); return this.jobs[jobIndex]; @@ -271,3 +288,4 @@ export class DataService { // Export singleton instance export const dataService = DataService.getInstance(); +