diff --git a/src/components/BOMActions.tsx b/src/components/BOMActions.tsx new file mode 100644 index 0000000..ee73fae --- /dev/null +++ b/src/components/BOMActions.tsx @@ -0,0 +1,119 @@ + +import { Button } from '@/components/ui/button'; +import { Import, Upload, Check } from 'lucide-react'; +import { IndJob } from '@/lib/types'; +import { useToast } from '@/hooks/use-toast'; +import { useClipboard } from '@/hooks/useClipboard'; + +interface BOMActionsProps { + job: IndJob; + onImportBOM?: (jobId: string, items: { name: string; quantity: number }[]) => void; +} + +const BOMActions: React.FC = ({ job, onImportBOM }) => { + const { toast } = useToast(); + const { copying, copyToClipboard } = useClipboard(); + + 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\t]+/); + if (parts.length >= 2) { + const name = parts.slice(0, -1).join(' '); + 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(job.id, items); + toast({ + title: "BOM Imported", + description: `Successfully imported ${items.length} items`, + duration: 3000, + }); + } else { + toast({ + title: "No Valid Items", + description: "No valid items found in clipboard. Format: 'Item Name Quantity' per line", + variant: "destructive", + 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'); + + await copyToClipboard(text, 'bom', 'Bill of materials copied to clipboard'); + }; + + return ( +
+ + +
+ ); +}; + +export default BOMActions; diff --git a/src/components/EditableProduced.tsx b/src/components/EditableProduced.tsx new file mode 100644 index 0000000..34350dc --- /dev/null +++ b/src/components/EditableProduced.tsx @@ -0,0 +1,69 @@ + +import { useState } from 'react'; +import { Input } from '@/components/ui/input'; +import { IndJob } from '@/lib/types'; + +interface EditableProducedProps { + job: IndJob; + onUpdateProduced?: (jobId: string, produced: number) => void; +} + +const EditableProduced: React.FC = ({ job, onUpdateProduced }) => { + const [isEditing, setIsEditing] = useState(false); + const [value, setValue] = useState(job.produced?.toString() || '0'); + + const handleUpdate = () => { + const newValue = parseInt(value); + if (!isNaN(newValue) && onUpdateProduced) { + onUpdateProduced(job.id, newValue); + setIsEditing(false); + } else { + setValue(job.produced?.toString() || '0'); + setIsEditing(false); + } + }; + + const handleKeyPress = (e: React.KeyboardEvent) => { + if (e.key === 'Enter') { + handleUpdate(); + } else if (e.key === 'Escape') { + setIsEditing(false); + setValue(job.produced?.toString() || '0'); + } + }; + + const handleClick = () => { + if (job.status !== 'Closed') { + setIsEditing(true); + } + }; + + if (isEditing && job.status !== 'Closed') { + return ( + setValue(e.target.value)} + onBlur={handleUpdate} + onKeyDown={handleKeyPress} + className="w-24 h-5 px-2 py-0 inline-block bg-gray-800 border-gray-600 text-white text-xs leading-5" + min="0" + autoFocus + data-no-navigate + /> + ); + } + + return ( + + {(job.produced || 0).toLocaleString()} + + ); +}; + +export default EditableProduced; diff --git a/src/components/JobCard.tsx b/src/components/JobCard.tsx index 37fe019..6d209df 100644 --- a/src/components/JobCard.tsx +++ b/src/components/JobCard.tsx @@ -1,6 +1,8 @@ + import { useNavigate } from 'react-router-dom'; import { Card, CardContent, CardHeader } from '@/components/ui/card'; import { IndJob } from '@/lib/types'; +import { getStatusBackgroundColor } from '@/utils/jobStatusUtils'; import JobCardHeader from './JobCardHeader'; import JobCardDetails from './JobCardDetails'; import JobCardMetrics from './JobCardMetrics'; @@ -24,30 +26,14 @@ const JobCard: React.FC = ({ }) => { const navigate = useNavigate(); - const getStatusBackgroundColor = (status: string) => { - switch (status) { - case 'Planned': return 'bg-gray-600/20'; - case 'Acquisition': return 'bg-yellow-600/20'; - case 'Running': return 'bg-blue-600/20'; - case 'Done': return 'bg-purple-600/20'; - case 'Selling': return 'bg-orange-600/20'; - case 'Closed': return 'bg-green-600/20'; - case 'Tracked': return 'bg-cyan-600/20'; - default: return 'bg-gray-600/20'; - } - }; - const handleCardClick = (e: React.MouseEvent) => { - // Check if the click target or any of its parents has the data-no-navigate attribute const target = e.target as HTMLElement; const hasNoNavigate = target.closest('[data-no-navigate]'); if (hasNoNavigate) { - // Don't navigate if clicking on elements marked as non-navigating return; } - // Only navigate if clicking on areas that aren't marked as non-navigating navigate(`/${job.id}`); }; diff --git a/src/components/JobCardHeader.tsx b/src/components/JobCardHeader.tsx index 3b7748c..ff52c00 100644 --- a/src/components/JobCardHeader.tsx +++ b/src/components/JobCardHeader.tsx @@ -1,18 +1,12 @@ -import { useState } from 'react'; import { CardTitle } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; -import { Input } from '@/components/ui/input'; -import { Import, Upload, Check, Copy } from 'lucide-react'; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from '@/components/ui/dropdown-menu'; +import { Copy } from 'lucide-react'; import { IndJob } from '@/lib/types'; -import { useToast } from '@/hooks/use-toast'; -import { useJobs } from '@/hooks/useDataService'; +import { useClipboard } from '@/hooks/useClipboard'; +import JobStatusDropdown from './JobStatusDropdown'; +import BOMActions from './BOMActions'; +import EditableProduced from './EditableProduced'; interface JobCardHeaderProps { job: IndJob; @@ -29,223 +23,22 @@ const JobCardHeader: React.FC = ({ onUpdateProduced, onImportBOM }) => { - const [isEditingProduced, setIsEditingProduced] = useState(false); - const [producedValue, setProducedValue] = useState(job.produced?.toString() || '0'); - const [copyingBom, setCopyingBom] = useState(false); - const [copyingName, setCopyingName] = useState(false); - const [copyingId, setCopyingId] = useState(false); - const { toast } = useToast(); - const { updateJob } = useJobs(); - - const statuses = ['Planned', 'Acquisition', 'Running', 'Done', 'Selling', 'Closed', 'Tracked']; - - const getStatusColor = (status: string) => { - switch (status) { - case 'Planned': return 'bg-gray-600'; - case 'Acquisition': return 'bg-yellow-600'; - case 'Running': return 'bg-blue-600'; - case 'Done': return 'bg-purple-600'; - case 'Selling': return 'bg-orange-600'; - case 'Closed': return 'bg-green-600'; - case 'Tracked': return 'bg-cyan-600'; - default: return 'bg-gray-600'; - } - }; - - const handleStatusChange = async (newStatus: string, e: React.MouseEvent) => { - try { - await updateJob(job.id, { status: newStatus }); - toast({ - title: "Status Updated", - description: `Job status changed to ${newStatus}`, - duration: 2000, - }); - } catch (error) { - console.error('Error updating status:', error); - toast({ - title: "Error", - description: "Failed to update status", - variant: "destructive", - duration: 2000, - }); - } - }; - - const handleProducedUpdate = () => { - const newValue = parseInt(producedValue); - if (!isNaN(newValue) && onUpdateProduced) { - onUpdateProduced(job.id, newValue); - setIsEditingProduced(false); - } else { - setProducedValue(job.produced?.toString() || '0'); - setIsEditingProduced(false); - } - }; - - const handleProducedKeyPress = (e: React.KeyboardEvent) => { - if (e.key === 'Enter') { - handleProducedUpdate(); - } else if (e.key === 'Escape') { - setIsEditingProduced(false); - setProducedValue(job.produced?.toString() || '0'); - } - }; - - 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\t]+/); - if (parts.length >= 2) { - const name = parts.slice(0, -1).join(' '); - 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(job.id, items); - toast({ - title: "BOM Imported", - description: `Successfully imported ${items.length} items`, - duration: 3000, - }); - } else { - toast({ - title: "No Valid Items", - description: "No valid items found in clipboard. Format: 'Item Name Quantity' per line", - variant: "destructive", - 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'); - - try { - await navigator.clipboard.writeText(text); - setCopyingBom(true); - toast({ - title: "Exported!", - description: "Bill of materials copied to clipboard", - duration: 2000, - }); - setTimeout(() => setCopyingBom(false), 1000); - } catch (err) { - toast({ - title: "Error", - description: "Failed to copy to clipboard", - variant: "destructive", - duration: 2000, - }); - } - }; - - const handleJobNameClick = async (e: React.MouseEvent) => { - try { - await navigator.clipboard.writeText(job.outputItem); - setCopyingName(true); - toast({ - title: "Copied!", - description: "Job name copied to clipboard", - duration: 2000, - }); - setTimeout(() => setCopyingName(false), 1000); - } catch (err) { - toast({ - title: "Error", - description: "Failed to copy to clipboard", - variant: "destructive", - duration: 2000, - }); - } - }; - - const handleJobIdClick = async (e: React.MouseEvent) => { - e.stopPropagation(); - try { - await navigator.clipboard.writeText(job.id); - setCopyingId(true); - toast({ - title: "Copied!", - description: "Job ID copied to clipboard", - duration: 2000, - }); - setTimeout(() => setCopyingId(false), 1000); - } catch (err) { - toast({ - title: "Error", - description: "Failed to copy to clipboard", - variant: "destructive", - duration: 2000, - }); - } - }; - - const handleProducedClick = (e: React.MouseEvent) => { - if (job.status !== 'Closed') { - setIsEditingProduced(true); - } - }; - - const handleEditClick = (e: React.MouseEvent) => { - onEdit(job); - }; - - const handleDeleteClick = (e: React.MouseEvent) => { - onDelete(job.id); - }; - - const handleImportClick = (e: React.MouseEvent) => { - importBillOfMaterials(); - }; - - const handleExportClick = (e: React.MouseEvent) => { - exportBillOfMaterials(); - }; + const { copying, copyToClipboard } = useClipboard(); const sortedIncome = [...job.income].sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime() ); const itemsSold = sortedIncome.reduce((sum, tx) => sum + tx.quantity, 0); + const handleJobNameClick = async (e: React.MouseEvent) => { + await copyToClipboard(job.outputItem, 'name', 'Job name copied to clipboard'); + }; + + const handleJobIdClick = async (e: React.MouseEvent) => { + e.stopPropagation(); + await copyToClipboard(job.id, 'id', 'Job ID copied to clipboard'); + }; + return (
@@ -258,37 +51,14 @@ const JobCardHeader: React.FC = ({ style={{ lineHeight: '1.4' }} > {job.outputItem} - {copyingName && } + {copying === 'name' && }
Runs: {job.outputQuantity.toLocaleString()} - Produced: { - isEditingProduced && job.status !== 'Closed' ? ( - setProducedValue(e.target.value)} - onBlur={handleProducedUpdate} - onKeyDown={handleProducedKeyPress} - className="w-24 h-5 px-2 py-0 inline-block bg-gray-800 border-gray-600 text-white text-xs leading-5" - min="0" - autoFocus - data-no-navigate - /> - ) : ( - - {(job.produced || 0).toLocaleString()} - - ) - } + Produced: Sold: {itemsSold.toLocaleString()} @@ -302,40 +72,18 @@ const JobCardHeader: React.FC = ({ data-no-navigate > {job.id} - {copyingId && } + {copying === 'id' && }
- - -
- {job.status} -
-
- - {statuses.map((status) => ( - handleStatusChange(status, e)} - className="hover:bg-gray-700 cursor-pointer" - data-no-navigate - > -
- {status} - - ))} - - +
-
- - -
+
); diff --git a/src/components/JobCategory.tsx b/src/components/JobCategory.tsx deleted file mode 100644 index 2646a1d..0000000 --- a/src/components/JobCategory.tsx +++ /dev/null @@ -1,107 +0,0 @@ -import { useState, useEffect } from 'react'; -import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; -import { Button } from '@/components/ui/button'; -import { IndJob, IndJobStatusOptions } from '@/types/industry'; -import JobCard from './JobCard'; -import { formatISK } from '@/utils/currency'; -import { ChevronDown, ChevronRight } from 'lucide-react'; - -interface JobCategoryProps { - status: IndJobStatusOptions; - jobs: IndJob[]; - onEdit: (job: any) => void; - onDelete: (jobId: string) => void; - onUpdateProduced?: (jobId: string, produced: number) => void; - onImportBOM?: (jobId: string, items: { name: string; quantity: number }[]) => void; -} - -export function JobCategory({ status, jobs, onEdit, onDelete, onUpdateProduced, onImportBOM }: JobCategoryProps) { - const [isCollapsed, setIsCollapsed] = useState(false); - - // Load collapsed state from localStorage - useEffect(() => { - const stored = localStorage.getItem(`job-category-collapsed-${status}`); - if (stored) { - setIsCollapsed(JSON.parse(stored)); - } - }, [status]); - - // Save collapsed state to localStorage - const toggleCollapsed = () => { - const newCollapsed = !isCollapsed; - setIsCollapsed(newCollapsed); - localStorage.setItem(`job-category-collapsed-${status}`, JSON.stringify(newCollapsed)); - }; - - // Calculate totals (excluding Tracked status) - const shouldIncludeInTotals = status !== IndJobStatusOptions.Tracked; - const totalExpenditure = shouldIncludeInTotals - ? jobs.reduce((sum, job) => sum + (job.expenditures?.reduce((s, t) => s + t.totalPrice, 0) || 0), 0) - : 0; - const totalIncome = shouldIncludeInTotals - ? jobs.reduce((sum, job) => sum + (job.income?.reduce((s, t) => s + t.totalPrice, 0) || 0), 0) - : 0; - const totalProfit = totalIncome - totalExpenditure; - - const getCategoryColor = (status: IndJobStatusOptions) => { - switch (status) { - case IndJobStatusOptions.Planned: return 'border-l-muted'; - case IndJobStatusOptions.Acquisition: return 'border-l-warning'; - case IndJobStatusOptions.Running: return 'border-l-primary'; - case IndJobStatusOptions.Done: return 'border-l-success'; - case IndJobStatusOptions.Selling: return 'border-l-accent'; - case IndJobStatusOptions.Closed: return 'border-l-muted'; - case IndJobStatusOptions.Tracked: return 'border-l-secondary'; - default: return 'border-l-muted'; - } - }; - - if (jobs.length === 0) return null; - - return ( - - - -
- {isCollapsed ? : } - {status} ({jobs.length}) -
- - {shouldIncludeInTotals && ( -
- Cost: {formatISK(totalExpenditure)} - Income: {formatISK(totalIncome)} - = 0 ? 'text-success' : 'text-destructive'}> - Profit: {formatISK(totalProfit)} - -
- )} - - {status === IndJobStatusOptions.Tracked && ( - - (Not included in totals) - - )} -
-
- - {!isCollapsed && ( - - {jobs.map(job => ( - - ))} - - )} -
- ); -} \ No newline at end of file diff --git a/src/components/JobGroup.tsx b/src/components/JobGroup.tsx new file mode 100644 index 0000000..1bc9de1 --- /dev/null +++ b/src/components/JobGroup.tsx @@ -0,0 +1,65 @@ + +import { IndJob } from '@/lib/types'; +import { getStatusColor } from '@/utils/jobStatusUtils'; +import JobCard from './JobCard'; + +interface JobGroupProps { + status: string; + jobs: IndJob[]; + isCollapsed: boolean; + onToggle: (status: string) => void; + onEdit: (job: IndJob) => void; + onDelete: (jobId: string) => void; + onUpdateProduced?: (jobId: string, produced: number) => void; + onImportBOM?: (jobId: string, items: { name: string; quantity: number }[]) => void; + isTracked?: boolean; +} + +const JobGroup: React.FC = ({ + status, + jobs, + isCollapsed, + onToggle, + onEdit, + onDelete, + onUpdateProduced, + onImportBOM, + isTracked = false +}) => { + return ( +
+
onToggle(status)} + > +
+

+ {status} + ({jobs.length} jobs) +

+
+ ⌄ +
+
+
+ + {!isCollapsed && ( +
+ {jobs.map(job => ( + + ))} +
+ )} +
+ ); +}; + +export default JobGroup; diff --git a/src/components/JobStatusDropdown.tsx b/src/components/JobStatusDropdown.tsx new file mode 100644 index 0000000..c745b5e --- /dev/null +++ b/src/components/JobStatusDropdown.tsx @@ -0,0 +1,67 @@ + +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu'; +import { IndJob } from '@/lib/types'; +import { getStatusColor, JOB_STATUSES } from '@/utils/jobStatusUtils'; +import { useJobs } from '@/hooks/useDataService'; +import { useToast } from '@/hooks/use-toast'; + +interface JobStatusDropdownProps { + job: IndJob; +} + +const JobStatusDropdown: React.FC = ({ job }) => { + const { updateJob } = useJobs(); + const { toast } = useToast(); + + const handleStatusChange = async (newStatus: string, e: React.MouseEvent) => { + try { + await updateJob(job.id, { status: newStatus }); + toast({ + title: "Status Updated", + description: `Job status changed to ${newStatus}`, + duration: 2000, + }); + } catch (error) { + console.error('Error updating status:', error); + toast({ + title: "Error", + description: "Failed to update status", + variant: "destructive", + duration: 2000, + }); + } + }; + + return ( + + +
+ {job.status} +
+
+ + {JOB_STATUSES.map((status) => ( + handleStatusChange(status, e)} + className="hover:bg-gray-700 cursor-pointer" + data-no-navigate + > +
+ {status} + + ))} + + + ); +}; + +export default JobStatusDropdown; diff --git a/src/components/RecapPopover.tsx b/src/components/RecapPopover.tsx index 428814b..ea0691d 100644 --- a/src/components/RecapPopover.tsx +++ b/src/components/RecapPopover.tsx @@ -4,6 +4,8 @@ import { formatISK } from '@/utils/priceUtils'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; import { useNavigate } from 'react-router-dom'; +import { ChevronDown, ChevronUp } from 'lucide-react'; +import { getStatusBackgroundColor } from '@/utils/jobStatusUtils'; interface RecapPopoverProps { title: string; @@ -19,6 +21,7 @@ const RecapPopover: React.FC = ({ calculateJobValue }) => { const navigate = useNavigate(); + const [sortDescending, setSortDescending] = useState(true); const jobContributions = jobs .map(job => ({ diff --git a/src/hooks/useClipboard.ts b/src/hooks/useClipboard.ts new file mode 100644 index 0000000..c7fb465 --- /dev/null +++ b/src/hooks/useClipboard.ts @@ -0,0 +1,30 @@ + +import { useState } from 'react'; +import { useToast } from '@/hooks/use-toast'; + +export const useClipboard = () => { + const [copying, setCopying] = useState(null); + const { toast } = useToast(); + + const copyToClipboard = async (text: string, key: string, successMessage: string) => { + try { + await navigator.clipboard.writeText(text); + setCopying(key); + toast({ + title: "Copied!", + description: successMessage, + duration: 2000, + }); + setTimeout(() => setCopying(null), 1000); + } catch (err) { + toast({ + title: "Error", + description: "Failed to copy to clipboard", + variant: "destructive", + duration: 2000, + }); + } + }; + + return { copying, copyToClipboard }; +}; diff --git a/src/hooks/useJobMetrics.ts b/src/hooks/useJobMetrics.ts new file mode 100644 index 0000000..636cc43 --- /dev/null +++ b/src/hooks/useJobMetrics.ts @@ -0,0 +1,33 @@ + +import { IndJob } from '@/lib/types'; + +export const useJobMetrics = (jobs: IndJob[]) => { + const calculateJobRevenue = (job: IndJob) => + job.income.reduce((sum, tx) => sum + tx.totalPrice, 0); + + const calculateJobProfit = (job: IndJob) => { + const expenditure = job.expenditures.reduce((sum, tx) => sum + tx.totalPrice, 0); + const income = job.income.reduce((sum, tx) => sum + tx.totalPrice, 0); + return income - expenditure; + }; + + const totalJobs = jobs.length; + + const totalProfit = jobs.reduce((sum, job) => { + const expenditure = job.expenditures.reduce((sum, tx) => sum + tx.totalPrice, 0); + const income = job.income.reduce((sum, tx) => sum + tx.totalPrice, 0); + return sum + (income - expenditure); + }, 0); + + const totalRevenue = jobs.reduce((sum, job) => + sum + job.income.reduce((sum, tx) => sum + tx.totalPrice, 0), 0 + ); + + return { + totalJobs, + totalProfit, + totalRevenue, + calculateJobRevenue, + calculateJobProfit + }; +}; diff --git a/src/pages/Index.tsx b/src/pages/Index.tsx index edc9408..5c40d2c 100644 --- a/src/pages/Index.tsx +++ b/src/pages/Index.tsx @@ -2,13 +2,16 @@ import { useState, useEffect } from 'react'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Plus, Factory, TrendingUp, Briefcase, FileText } from 'lucide-react'; -import { IndTransactionRecordNoId, IndJobRecordNoId, IndJobStatusOptions } from '@/lib/pbtypes'; +import { IndTransactionRecordNoId, IndJobRecordNoId } from '@/lib/pbtypes'; import { formatISK } from '@/utils/priceUtils'; +import { getStatusPriority } from '@/utils/jobStatusUtils'; import JobCard from '@/components/JobCard'; import JobForm from '@/components/JobForm'; +import JobGroup from '@/components/JobGroup'; import { IndJob } from '@/lib/types'; import BatchTransactionForm from '@/components/BatchTransactionForm'; import { useJobs } from '@/hooks/useDataService'; +import { useJobMetrics } from '@/hooks/useJobMetrics'; import SearchOverlay from '@/components/SearchOverlay'; import RecapPopover from '@/components/RecapPopover'; @@ -63,32 +66,6 @@ const Index = () => { ); } - const getStatusPriority = (status: IndJobStatusOptions): number => { - switch (status) { - case 'Planned': return 6; - case 'Acquisition': return 1; - case 'Running': return 2; - case 'Done': return 3; - case 'Selling': return 4; - case 'Closed': return 5; - case 'Tracked': return 7; - default: return 0; - } - }; - - const getStatusColor = (status: string) => { - switch (status) { - case 'Planned': return 'bg-gray-600'; - case 'Acquisition': return 'bg-yellow-600'; - case 'Running': return 'bg-blue-600'; - case 'Done': return 'bg-purple-600'; - case 'Selling': return 'bg-orange-600'; - case 'Closed': return 'bg-green-600'; - case 'Tracked': return 'bg-cyan-600'; - default: return 'bg-gray-600'; - } - }; - const filterJobs = (jobs: IndJob[]) => { if (!searchQuery) return jobs; const query = searchQuery.toLowerCase(); @@ -109,25 +86,7 @@ const Index = () => { const regularJobs = filterJobs(sortedJobs.filter(job => job.status !== 'Tracked')); const trackedJobs = filterJobs(sortedJobs.filter(job => job.status === 'Tracked')); - const totalJobs = regularJobs.length; - const totalProfit = regularJobs.reduce((sum, job) => { - const expenditure = job.expenditures.reduce((sum, tx) => sum + tx.totalPrice, 0); - const income = job.income.reduce((sum, tx) => sum + tx.totalPrice, 0); - return sum + (income - expenditure); - }, 0); - - const totalRevenue = regularJobs.reduce((sum, job) => - sum + job.income.reduce((sum, tx) => sum + tx.totalPrice, 0), 0 - ); - - const calculateJobRevenue = (job: IndJob) => - job.income.reduce((sum, tx) => sum + tx.totalPrice, 0); - - const calculateJobProfit = (job: IndJob) => { - const expenditure = job.expenditures.reduce((sum, tx) => sum + tx.totalPrice, 0); - const income = job.income.reduce((sum, tx) => sum + tx.totalPrice, 0); - return income - expenditure; - }; + const { totalJobs, totalProfit, totalRevenue, calculateJobRevenue, calculateJobProfit } = useJobMetrics(regularJobs); const handleCreateJob = async (jobData: IndJobRecordNoId) => { try { @@ -200,9 +159,7 @@ const Index = () => { setCollapsedGroups(newState); localStorage.setItem('jobGroupsCollapsed', JSON.stringify(newState)); - // Load jobs for newly opened groups if (collapsedGroups[status]) { - // Group is becoming visible, load jobs for this status loadJobsForStatuses([status]); } }; @@ -323,73 +280,34 @@ const Index = () => {
{Object.entries(jobGroups).map(([status, statusJobs]) => ( -
-
toggleGroup(status)} - > -
-

- {status} - ({statusJobs.length} jobs) -

-
- ⌄ -
-
-
- - {!collapsedGroups[status] && ( -
- {statusJobs.map(job => ( - - ))} -
- )} -
+ ))}
{trackedJobs.length > 0 && (
-
toggleGroup('Tracked')} - > -
-

- Tracked Transactions - ({trackedJobs.length} jobs) -

-
- ⌄ -
-
-
- - {!collapsedGroups['Tracked'] && ( -
- {trackedJobs.map(job => ( - - ))} -
- )} +
)} diff --git a/src/utils/jobStatusUtils.ts b/src/utils/jobStatusUtils.ts new file mode 100644 index 0000000..50a8d51 --- /dev/null +++ b/src/utils/jobStatusUtils.ts @@ -0,0 +1,43 @@ + +import { IndJobStatusOptions } from '@/lib/pbtypes'; + +export const getStatusColor = (status: string) => { + switch (status) { + case 'Planned': return 'bg-gray-600'; + case 'Acquisition': return 'bg-yellow-600'; + case 'Running': return 'bg-blue-600'; + case 'Done': return 'bg-purple-600'; + case 'Selling': return 'bg-orange-600'; + case 'Closed': return 'bg-green-600'; + case 'Tracked': return 'bg-cyan-600'; + default: return 'bg-gray-600'; + } +}; + +export const getStatusBackgroundColor = (status: string) => { + switch (status) { + case 'Planned': return 'bg-gray-600/20'; + case 'Acquisition': return 'bg-yellow-600/20'; + case 'Running': return 'bg-blue-600/20'; + case 'Done': return 'bg-purple-600/20'; + case 'Selling': return 'bg-orange-600/20'; + case 'Closed': return 'bg-green-600/20'; + case 'Tracked': return 'bg-cyan-600/20'; + default: return 'bg-gray-600/20'; + } +}; + +export const getStatusPriority = (status: IndJobStatusOptions): number => { + switch (status) { + case 'Planned': return 6; + case 'Acquisition': return 1; + case 'Running': return 2; + case 'Done': return 3; + case 'Selling': return 4; + case 'Closed': return 5; + case 'Tracked': return 7; + default: return 0; + } +}; + +export const JOB_STATUSES = ['Planned', 'Acquisition', 'Running', 'Done', 'Selling', 'Closed', 'Tracked'] as const;