From 4fd55ffb3e11e3399ec04e28db069ca0dc611720 Mon Sep 17 00:00:00 2001 From: PhatPhuckDave Date: Sun, 6 Jul 2025 02:52:24 +0200 Subject: [PATCH] Factor jobs into collapsible categories and implement tracked jobs --- frontend/src/components/JobCard.tsx | 7 +- frontend/src/lib/pbtypes.ts | 1 + frontend/src/pages/Index.tsx | 286 ++++++++++++++++++++-------- 3 files changed, 212 insertions(+), 82 deletions(-) diff --git a/frontend/src/components/JobCard.tsx b/frontend/src/components/JobCard.tsx index b167f10..87def9f 100644 --- a/frontend/src/components/JobCard.tsx +++ b/frontend/src/components/JobCard.tsx @@ -13,9 +13,10 @@ interface JobCardProps { onEdit: (job: any) => void; onDelete: (jobId: string) => void; onUpdateProduced?: (jobId: string, produced: number) => void; + isTracked?: boolean; } -const JobCard: React.FC = ({ job, onEdit, onDelete, onUpdateProduced }) => { +const JobCard: React.FC = ({ job, onEdit, onDelete, onUpdateProduced, isTracked = false }) => { const [isEditingProduced, setIsEditingProduced] = useState(false); const [producedValue, setProducedValue] = useState(job.produced?.toString() || '0'); @@ -27,6 +28,7 @@ const JobCard: React.FC = ({ job, onEdit, onDelete, onUpdateProduc new Date(b.date).getTime() - new Date(a.date).getTime() ); + // Calculate totals for this job (including tracked jobs) const totalExpenditure = sortedExpenditures.reduce((sum, tx) => sum + tx.totalPrice, 0); const totalIncome = sortedIncome.reduce((sum, tx) => sum + tx.totalPrice, 0); const profit = totalIncome - totalExpenditure; @@ -45,6 +47,7 @@ const JobCard: React.FC = ({ job, onEdit, onDelete, onUpdateProduc 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'; } }; @@ -81,7 +84,7 @@ const JobCard: React.FC = ({ job, onEdit, onDelete, onUpdateProduc }; return ( - +
diff --git a/frontend/src/lib/pbtypes.ts b/frontend/src/lib/pbtypes.ts index 6f6bb38..5351954 100644 --- a/frontend/src/lib/pbtypes.ts +++ b/frontend/src/lib/pbtypes.ts @@ -122,6 +122,7 @@ export enum IndJobStatusOptions { "Done" = "Done", "Selling" = "Selling", "Closed" = "Closed", + "Tracked" = "Tracked", } export type IndJobRecord = { billOfMaterials?: RecordIdString[] diff --git a/frontend/src/pages/Index.tsx b/frontend/src/pages/Index.tsx index ff757f9..815cb2a 100644 --- a/frontend/src/pages/Index.tsx +++ b/frontend/src/pages/Index.tsx @@ -12,6 +12,7 @@ import TransactionForm from '@/components/TransactionForm'; import TransactionTable from '@/components/TransactionTable'; import { IndJob } from '@/lib/types'; import { createJob } from '@/services/jobService'; +import { Badge } from '@/components/ui/badge'; // TODO: Bill of materials just does not work currently Fix this shit // Extended job type for UI components @@ -45,10 +46,24 @@ const Index = () => { case 'Done': return 3; case 'Selling': return 4; case 'Closed': return 5; + case 'Tracked': return 7; // Put tracked jobs at the end 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 sortedJobs = [...jobs].sort((a, b) => { const priorityA = getStatusPriority(a.status); const priorityB = getStatusPriority(b.status); @@ -59,6 +74,22 @@ const Index = () => { return priorityA - priorityB; }); + // Separate regular and tracked jobs + const regularJobs = sortedJobs.filter(job => job.status !== 'Tracked'); + const trackedJobs = sortedJobs.filter(job => job.status === 'Tracked'); + + // Calculate totals excluding tracked jobs + 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 handleCreateJob = async (jobData: IndJobRecordNoId) => { try { const newJob = await createJob(jobData); @@ -190,16 +221,28 @@ const Index = () => { } }; - 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); + // Group jobs by status + const jobGroups = regularJobs.reduce((groups, job) => { + const status = job.status; + if (!groups[status]) { + groups[status] = []; + } + groups[status].push(job); + return groups; + }, {} as Record); - const totalRevenue = jobs.reduce((sum, job) => - sum + job.income.reduce((sum, tx) => sum + tx.totalPrice, 0), 0 - ); + // Load collapsed state from localStorage + const [collapsedGroups, setCollapsedGroups] = useState>(() => { + const saved = localStorage.getItem('jobGroupsCollapsed'); + return saved ? JSON.parse(saved) : {}; + }); + + // Toggle group collapse + const toggleGroup = (status: string) => { + const newState = { ...collapsedGroups, [status]: !collapsedGroups[status] }; + setCollapsedGroups(newState); + localStorage.setItem('jobGroupsCollapsed', JSON.stringify(newState)); + }; if (showJobForm) { return ( @@ -271,15 +314,55 @@ const Index = () => { } return ( -
-
-
-
-

EVE Industry Manager

-

Manage your industrial jobs and track profitability

-
+
+ {/* Stats Cards */} +
+ + + + + Active Jobs + + + +
{totalJobs}
+
+
+ + + + + Total Revenue + + + +
{formatISK(totalRevenue)}
+
+
+ + + + + Total Profit + + + +
= 0 ? 'text-green-400' : 'text-red-400'}`}> + {formatISK(totalProfit)} +
+
+
+
+ + {/* Regular Jobs */} +
+
+

Jobs

-
- - - Total Jobs - - - -
{totalJobs}
-

Active industrial operations

-
-
- - - - Total Revenue - - - -
{formatISK(totalRevenue)}
-

From all job sales

-
-
- - - - Total Profit - - - -
= 0 ? 'text-green-400' : 'text-red-400'}`}> - {formatISK(totalProfit)} +
+ {Object.entries(jobGroups).map(([status, statusJobs]) => ( +
+
toggleGroup(status)} + > +
+ ▶ +
+

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

-

Net profit across all jobs

+ + {!collapsedGroups[status] && ( +
+ {statusJobs.map(job => ( +
setSelectedJob(job)} className="cursor-pointer"> + +
+ ))} +
+ )} +
+ ))} +
+
+ + {/* Tracked Jobs */} + {trackedJobs.length > 0 && ( +
+
toggleGroup('Tracked')} + > +
+ ▶ +
+

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

+
+ + {!collapsedGroups['Tracked'] && ( +
+ {trackedJobs.map(job => ( +
setSelectedJob(job)} className="cursor-pointer"> + +
+ ))} +
+ )} +
+ )} + + {/* Job Form Modal */} + {showJobForm && ( + { + setShowJobForm(false); + setEditingJob(null); + }} + /> + )} + + {/* Transaction Details */} + {selectedJob && ( +
+ + + Transactions for {selectedJob.outputItem} + + + + ({ ...tx, type: 'expenditure' as const })), + ...selectedJob.income.map(tx => ({ ...tx, type: 'income' as const })) + ]} + type="expenditure" + onUpdateTransaction={handleUpdateTransaction} + onDeleteTransaction={handleDeleteTransaction} + />
- - {jobs.length === 0 ? ( - - - -

No jobs yet

-

Create your first industrial job to get started

- -
-
- ) : ( -
- {sortedJobs.map((job) => ( -
setSelectedJob(job)} className="cursor-pointer"> - -
- ))} -
- )} -
+ )}
); };