From 7a535639fc71cc8a87591b2486fc5d0597c72a2d Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Fri, 4 Jul 2025 12:32:50 +0000 Subject: [PATCH] feat: Implement Eve Online job manager Create a basic application for managing Eve Online industry jobs, including job details, transaction history, and profit calculations. Implement data ingestion via a form with paste functionality for transaction data. --- index.html | 9 +- src/components/JobCard.tsx | 105 +++++++++++++ src/components/JobForm.tsx | 218 ++++++++++++++++++++++++++ src/components/TransactionForm.tsx | 154 ++++++++++++++++++ src/pages/Index.tsx | 241 ++++++++++++++++++++++++++++- src/services/facilityService.ts | 18 +++ src/services/jobService.ts | 80 ++++++++++ src/utils/priceUtils.ts | 74 +++++++++ 8 files changed, 890 insertions(+), 9 deletions(-) create mode 100644 src/components/JobCard.tsx create mode 100644 src/components/JobForm.tsx create mode 100644 src/components/TransactionForm.tsx create mode 100644 src/services/facilityService.ts create mode 100644 src/services/jobService.ts create mode 100644 src/utils/priceUtils.ts diff --git a/index.html b/index.html index dac3ced..6793598 100644 --- a/index.html +++ b/index.html @@ -1,14 +1,15 @@ + - eve-job-tracker-pro - + EVE Industry Manager + - - + + diff --git a/src/components/JobCard.tsx b/src/components/JobCard.tsx new file mode 100644 index 0000000..62e5a68 --- /dev/null +++ b/src/components/JobCard.tsx @@ -0,0 +1,105 @@ + +import React from 'react'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Badge } from '@/components/ui/badge'; +import { Button } from '@/components/ui/button'; +import { Calendar, Factory, TrendingUp, TrendingDown } from 'lucide-react'; +import { Job } from '@/services/jobService'; +import { formatISK } from '@/utils/priceUtils'; + +interface JobCardProps { + job: Job; + onEdit: (job: Job) => void; + onDelete: (jobId: string) => void; +} + +const JobCard: React.FC = ({ job, onEdit, onDelete }) => { + const totalExpenditure = job.expenditures.reduce((sum, tx) => sum + Math.abs(tx.totalAmount), 0); + const totalIncome = job.income.reduce((sum, tx) => sum + tx.totalAmount, 0); + const profit = totalIncome - totalExpenditure; + const margin = totalIncome > 0 ? ((profit / totalIncome) * 100) : 0; + + const itemsSold = job.income.reduce((sum, tx) => sum + tx.quantity, 0); + const daysSinceStart = Math.max(1, Math.ceil((Date.now() - job.dates.saleStart.getTime()) / (1000 * 60 * 60 * 24))); + const itemsPerDay = itemsSold / daysSinceStart; + + return ( + + +
+
+ {job.outputItem.name} +

Quantity: {job.outputItem.quantity.toLocaleString()}

+
+
+ + +
+
+
+ +
+
+
+ + Created: {job.dates.creation.toLocaleDateString()} +
+
+ + Facility: {job.facilityId} +
+
+
+
+ Sale Period: {job.dates.saleStart.toLocaleDateString()} - {job.dates.saleEnd.toLocaleDateString()} +
+
+ Items/Day: {itemsPerDay.toFixed(2)} +
+
+
+ +
+
+
+ + Expenditure +
+
{formatISK(totalExpenditure)}
+
+
+
+ + Income +
+
{formatISK(totalIncome)}
+
+
+
Profit
+
= 0 ? 'text-green-400' : 'text-red-400'}`}> + {formatISK(profit)} +
+ = 0 ? 'default' : 'destructive'} className="text-xs"> + {margin.toFixed(1)}% + +
+
+
+
+ ); +}; + +export default JobCard; diff --git a/src/components/JobForm.tsx b/src/components/JobForm.tsx new file mode 100644 index 0000000..5151e0c --- /dev/null +++ b/src/components/JobForm.tsx @@ -0,0 +1,218 @@ + +import React, { useState, useEffect } 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 { Job, Facility } from '@/services/jobService'; +import { facilityService } from '@/services/facilityService'; + +interface JobFormProps { + job?: Job; + onSubmit: (job: Omit) => void; + onCancel: () => void; +} + +const JobForm: React.FC = ({ job, onSubmit, onCancel }) => { + const [facilities, setFacilities] = useState([]); + const [formData, setFormData] = useState({ + outputItem: { + id: job?.outputItem.id || '', + name: job?.outputItem.name || '', + quantity: job?.outputItem.quantity || 0 + }, + dates: { + creation: job?.dates.creation ? job.dates.creation.toISOString().split('T')[0] : new Date().toISOString().split('T')[0], + start: job?.dates.start ? job.dates.start.toISOString().split('T')[0] : '', + end: job?.dates.end ? job.dates.end.toISOString().split('T')[0] : '', + saleStart: job?.dates.saleStart ? job.dates.saleStart.toISOString().split('T')[0] : '', + saleEnd: job?.dates.saleEnd ? job.dates.saleEnd.toISOString().split('T')[0] : '' + }, + facilityId: job?.facilityId || '' + }); + + useEffect(() => { + const loadFacilities = async () => { + try { + const fetchedFacilities = await facilityService.getFacilities(); + setFacilities(fetchedFacilities); + } catch (error) { + console.error('Error loading facilities:', error); + } + }; + + loadFacilities(); + }, []); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + + onSubmit({ + outputItem: formData.outputItem, + dates: { + creation: new Date(formData.dates.creation), + start: new Date(formData.dates.start), + end: new Date(formData.dates.end), + saleStart: new Date(formData.dates.saleStart), + saleEnd: new Date(formData.dates.saleEnd) + }, + facilityId: formData.facilityId + }); + }; + + return ( + + + + {job ? 'Edit Job' : 'Create New Job'} + + + +
+
+
+ + setFormData({ + ...formData, + outputItem: { ...formData.outputItem, name: e.target.value } + })} + className="bg-gray-800 border-gray-600 text-white" + required + /> +
+
+ + setFormData({ + ...formData, + outputItem: { ...formData.outputItem, quantity: parseInt(e.target.value) || 0 } + })} + className="bg-gray-800 border-gray-600 text-white" + required + /> +
+
+ +
+ + +
+ +
+
+ + setFormData({ + ...formData, + dates: { ...formData.dates, creation: e.target.value } + })} + className="bg-gray-800 border-gray-600 text-white" + required + /> +
+
+ + setFormData({ + ...formData, + dates: { ...formData.dates, start: e.target.value } + })} + className="bg-gray-800 border-gray-600 text-white" + required + /> +
+
+ +
+
+ + setFormData({ + ...formData, + dates: { ...formData.dates, end: e.target.value } + })} + className="bg-gray-800 border-gray-600 text-white" + required + /> +
+
+ + setFormData({ + ...formData, + dates: { ...formData.dates, saleStart: e.target.value } + })} + className="bg-gray-800 border-gray-600 text-white" + required + /> +
+
+ +
+ + setFormData({ + ...formData, + dates: { ...formData.dates, saleEnd: e.target.value } + })} + className="bg-gray-800 border-gray-600 text-white" + required + /> +
+ +
+ + +
+
+
+
+ ); +}; + +export default JobForm; diff --git a/src/components/TransactionForm.tsx b/src/components/TransactionForm.tsx new file mode 100644 index 0000000..f978747 --- /dev/null +++ b/src/components/TransactionForm.tsx @@ -0,0 +1,154 @@ + +import React, { useState } from 'react'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { Textarea } from '@/components/ui/textarea'; +import { Badge } from '@/components/ui/badge'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'; +import { parseTransactionLine, formatISK } from '@/utils/priceUtils'; +import { Transaction } from '@/services/jobService'; +import { Upload, Check, X } from 'lucide-react'; + +interface TransactionFormProps { + jobId: string; + onTransactionsAdded: (transactions: Transaction[], type: 'expenditure' | 'income') => void; +} + +const TransactionForm: React.FC = ({ jobId, onTransactionsAdded }) => { + const [pastedData, setPastedData] = useState(''); + const [parsedTransactions, setParsedTransactions] = useState([]); + const [transactionType, setTransactionType] = useState<'expenditure' | 'income'>('expenditure'); + + const handlePaste = (value: string) => { + setPastedData(value); + + const lines = value.trim().split('\n'); + const transactions: Transaction[] = []; + + lines.forEach((line, index) => { + const parsed = parseTransactionLine(line); + if (parsed) { + transactions.push({ + id: `temp-${index}`, + ...parsed + }); + } + }); + + setParsedTransactions(transactions); + }; + + const handleSubmit = () => { + if (parsedTransactions.length > 0) { + onTransactionsAdded(parsedTransactions, transactionType); + setPastedData(''); + setParsedTransactions([]); + } + }; + + const totalAmount = parsedTransactions.reduce((sum, tx) => sum + Math.abs(tx.totalAmount), 0); + + return ( + + + Add Transactions + + + setTransactionType(value as 'expenditure' | 'income')}> + + + Expenditures + + + Income + + + + +
+ +