diff --git a/src/App.tsx b/src/App.tsx index 18daf2e..e0b1353 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,9 +1,11 @@ + import { Toaster } from "@/components/ui/toaster"; import { Toaster as Sonner } from "@/components/ui/sonner"; import { TooltipProvider } from "@/components/ui/tooltip"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { BrowserRouter, Routes, Route } from "react-router-dom"; import Index from "./pages/Index"; +import JobDetails from "./pages/JobDetails"; import NotFound from "./pages/NotFound"; const queryClient = new QueryClient(); @@ -16,6 +18,7 @@ const App = () => ( } /> + } /> {/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */} } /> diff --git a/src/components/JobCard.tsx b/src/components/JobCard.tsx index 3a45dcd..d6bff32 100644 --- a/src/components/JobCard.tsx +++ b/src/components/JobCard.tsx @@ -1,9 +1,10 @@ + import { useState } from 'react'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { HoverCard, HoverCardContent, HoverCardTrigger } from '@/components/ui/hover-card'; -import { Calendar, Factory, TrendingUp, TrendingDown, Clock, Package, Wrench, Check } from 'lucide-react'; +import { Calendar, Factory, TrendingUp, TrendingDown, Clock, Import, Export, Wrench, Check } from 'lucide-react'; import { formatISK } from '@/utils/priceUtils'; import { IndJob } from '@/lib/types'; import { Input } from '@/components/ui/input'; @@ -21,7 +22,6 @@ const JobCard: React.FC = ({ job, onEdit, onDelete, onUpdateProduc const [isEditingProduced, setIsEditingProduced] = useState(false); const [producedValue, setProducedValue] = useState(job.produced?.toString() || '0'); const [copyingBom, setCopyingBom] = useState(false); - const [copyingConsumed, setCopyingConsumed] = useState(false); const { toast } = useToast(); // Sort transactions by date descending @@ -87,9 +87,49 @@ const JobCard: React.FC = ({ job, onEdit, onDelete, onUpdateProduc } }; - const copyBillOfMaterials = async () => { - if (!job.billOfMaterials?.length) return; - + const importBillOfMaterials = 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 exportBillOfMaterials = 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}\t${item.quantity.toLocaleString()}`) .join('\n'); @@ -98,7 +138,7 @@ const JobCard: React.FC = ({ job, onEdit, onDelete, onUpdateProduc await navigator.clipboard.writeText(text); setCopyingBom(true); toast({ - title: "Copied!", + title: "Exported!", description: "Bill of materials copied to clipboard", duration: 2000, }); @@ -113,32 +153,6 @@ const JobCard: React.FC = ({ job, onEdit, onDelete, onUpdateProduc } }; - const copyConsumedMaterials = async () => { - if (!job.consumedMaterials?.length) return; - - const text = job.consumedMaterials - .map(item => `${item.name}\t${item.quantity.toLocaleString()}`) - .join('\n'); - - try { - await navigator.clipboard.writeText(text); - setCopyingConsumed(true); - toast({ - title: "Copied!", - description: "Consumed materials copied to clipboard", - duration: 2000, - }); - setTimeout(() => setCopyingConsumed(false), 1000); - } catch (err) { - toast({ - title: "Error", - description: "Failed to copy to clipboard", - variant: "destructive", - duration: 2000, - }); - } - }; - const handleProducedClick = (e: React.MouseEvent) => { e.stopPropagation(); if (job.status !== 'Closed') { @@ -156,6 +170,16 @@ const JobCard: React.FC = ({ job, onEdit, onDelete, onUpdateProduc onDelete(job.id); }; + const handleImportClick = (e: React.MouseEvent) => { + e.stopPropagation(); + importBillOfMaterials(); + }; + + const handleExportClick = (e: React.MouseEvent) => { + e.stopPropagation(); + exportBillOfMaterials(); + }; + return ( @@ -166,41 +190,31 @@ const JobCard: React.FC = ({ job, onEdit, onDelete, onUpdateProduc {job.status} - {job.billOfMaterials && job.billOfMaterials.length > 0 && ( - - - - - -
-

Bill of Materials (click to copy)

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

Quantity: {job.outputQuantity.toLocaleString()} @@ -282,6 +296,29 @@ const JobCard: React.FC = ({ job, onEdit, onDelete, onUpdateProduc )} )} + + {job.billOfMaterials && job.billOfMaterials.length > 0 && ( + + +

+ BOM: {job.billOfMaterials.length} items (hover to view) +
+ + +
+

Bill of Materials

+
+ {job.billOfMaterials.map((item, index) => ( +
+ {item.name} + {item.quantity.toLocaleString()} +
+ ))} +
+
+
+ + )}
diff --git a/src/components/JobForm.tsx b/src/components/JobForm.tsx index 093cb8d..5de39df 100644 --- a/src/components/JobForm.tsx +++ b/src/components/JobForm.tsx @@ -8,8 +8,6 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@ import { IndJobStatusOptions, IndJobRecordNoId } from '@/lib/pbtypes'; import { IndJob } from '@/lib/types'; import { parseISKAmount } from '@/utils/priceUtils'; -import { Import, Upload } from 'lucide-react'; -import { useToast } from '@/components/ui/use-toast'; interface JobFormProps { job?: IndJob; @@ -18,7 +16,6 @@ interface JobFormProps { } const JobForm: React.FC = ({ job, onSubmit, onCancel }) => { - const { toast } = useToast(); const [formData, setFormData] = useState({ outputItem: job?.outputItem || '', outputQuantity: job?.outputQuantity || 0, @@ -51,70 +48,6 @@ const JobForm: React.FC = ({ job, onSubmit, onCancel }) => { }); }; - 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 ( @@ -262,40 +195,6 @@ const JobForm: React.FC = ({ job, onSubmit, onCancel }) => {
- {job && ( -
- -
- - -
- {job.billOfMaterials && job.billOfMaterials.length > 0 && ( -
- {job.billOfMaterials.map((item, index) => ( -
{item.name}: {item.quantity.toLocaleString()}
- ))} -
- )} -
- )} -
-
- -
- - -
- -
- - -
- - - ); - } - return (
@@ -359,7 +274,7 @@ const Index = () => { {!collapsedGroups[status] && (
{statusJobs.map(job => ( -
setSelectedJobId(job.id)} className="cursor-pointer"> +
handleJobCardClick(job.id)} className="cursor-pointer"> { {!collapsedGroups['Tracked'] && (
{trackedJobs.map(job => ( -
setSelectedJobId(job.id)} className="cursor-pointer"> +
handleJobCardClick(job.id)} className="cursor-pointer"> { + const { jobId } = useParams<{ jobId: string }>(); + const navigate = useNavigate(); + const [showJobForm, setShowJobForm] = useState(false); + const [editingJob, setEditingJob] = useState(null); + + const { + createMultipleTransactions, + updateTransaction, + deleteTransaction, + updateJob, + deleteJob + } = useJobs(); + + const job = useJob(jobId || null); + + if (!jobId) { + navigate('/'); + return null; + } + + if (!job) { + return ( +
+
Job not found
+
+ ); + } + + const handleEditJob = (job: IndJob) => { + setEditingJob(job); + setShowJobForm(true); + }; + + const handleUpdateJob = async (jobData: IndJobRecordNoId) => { + if (!editingJob) return; + + try { + await updateJob(editingJob.id, jobData); + setShowJobForm(false); + setEditingJob(null); + } catch (error) { + console.error('Error updating job:', error); + } + }; + + const handleDeleteJob = async (jobId: string) => { + if (confirm('Are you sure you want to delete this job?')) { + try { + await deleteJob(jobId); + navigate('/'); + } catch (error) { + console.error('Error deleting job:', error); + } + } + }; + + const handleTransactionsAdded = async (transactions: IndTransactionRecordNoId[], type: 'expenditure' | 'income') => { + try { + await createMultipleTransactions(jobId, transactions, type); + } catch (error) { + console.error('Error adding transactions:', error); + } + }; + + const handleUpdateTransaction = async (transactionId: string, updates: Partial) => { + try { + await updateTransaction(jobId, transactionId, updates); + } catch (error) { + console.error('Error updating transaction:', error); + } + }; + + const handleDeleteTransaction = async (transactionId: string) => { + try { + await deleteTransaction(jobId, transactionId); + } catch (error) { + console.error('Error deleting transaction:', error); + } + }; + + const handleUpdateProduced = async (jobId: string, produced: number) => { + try { + await updateJob(jobId, { produced }); + } catch (error) { + console.error('Error updating produced quantity:', error); + } + }; + + if (showJobForm) { + return ( +
+
+ { + setShowJobForm(false); + setEditingJob(null); + }} + /> +
+
+ ); + } + + return ( +
+
+
+
+

Job Details

+

{job.outputItem}

+
+ +
+ +
+ + +
+ +
+ + +
+
+
+ ); +}; + +export default JobDetails;