Refactor: Use new services and types

Update frontend components to use the new services and types for job management, including facilities, jobs, and transactions. Remove old type definitions and integrate the new ones.
This commit is contained in:
gpt-engineer-app[bot]
2025-07-04 13:28:35 +00:00
parent 7808907861
commit fb16824137
8 changed files with 272 additions and 173 deletions

View File

@@ -1,33 +1,39 @@
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 { HoverCard, HoverCardContent, HoverCardTrigger } from '@/components/ui/hover-card';
import { Calendar, Factory, TrendingUp, TrendingDown, Clock, Package, Wrench } from 'lucide-react';
import { Job } from '@/services/jobService';
import { IndJobResponse, IndTransactionResponse, IndBillitemResponse } from '@/lib/pbtypes';
import { formatISK } from '@/utils/priceUtils';
interface JobCardProps {
job: Job;
onEdit: (job: Job) => void;
job: IndJobResponse & {
expenditures: IndTransactionResponse[];
income: IndTransactionResponse[];
billOfMaterials: IndBillitemResponse[];
consumedMaterials: { name: string; required: number }[];
};
onEdit: (job: any) => void;
onDelete: (jobId: string) => void;
}
const JobCard: React.FC<JobCardProps> = ({ 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 totalExpenditure = job.expenditures.reduce((sum, tx) => sum + tx.totalPrice, 0);
const totalIncome = job.income.reduce((sum, tx) => sum + tx.totalPrice, 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 saleStartTime = job.dates.saleStart?.getTime();
const saleStartTime = job.saleStart ? new Date(job.saleStart).getTime() : null;
const daysSinceStart = saleStartTime ? Math.max(1, Math.ceil((Date.now() - saleStartTime) / (1000 * 60 * 60 * 24))) : 0;
const itemsPerDay = daysSinceStart > 0 ? itemsSold / daysSinceStart : 0;
const getStatusColor = (status: string) => {
switch (status) {
case 'Planned': return 'bg-gray-600';
case 'Transporting Materials': return 'bg-yellow-600';
case 'Acquisition': return 'bg-yellow-600';
case 'Running': return 'bg-blue-600';
case 'Done': return 'bg-green-600';
case 'Selling': return 'bg-purple-600';
@@ -36,9 +42,9 @@ const JobCard: React.FC<JobCardProps> = ({ job, onEdit, onDelete }) => {
}
};
const formatDateTime = (date: Date | null) => {
if (!date) return 'Not set';
return date.toLocaleString('en-CA', {
const formatDateTime = (dateString: string | null | undefined) => {
if (!dateString) return 'Not set';
return new Date(dateString).toLocaleString('en-CA', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
@@ -53,7 +59,7 @@ const JobCard: React.FC<JobCardProps> = ({ job, onEdit, onDelete }) => {
<div className="flex justify-between items-start">
<div>
<div className="flex items-center gap-2 mb-2">
<CardTitle className="text-blue-400">{job.outputItem.name}</CardTitle>
<CardTitle className="text-blue-400">{job.outputItem}</CardTitle>
<Badge className={`${getStatusColor(job.status)} text-white`}>
{job.status}
</Badge>
@@ -102,7 +108,7 @@ const JobCard: React.FC<JobCardProps> = ({ job, onEdit, onDelete }) => {
</HoverCard>
)}
</div>
<p className="text-gray-400">Quantity: {job.outputItem.quantity.toLocaleString()}</p>
<p className="text-gray-400">Quantity: {job.outputQuantity.toLocaleString()}</p>
</div>
<div className="flex gap-2">
<Button
@@ -127,22 +133,22 @@ const JobCard: React.FC<JobCardProps> = ({ job, onEdit, onDelete }) => {
<div className="grid grid-cols-1 gap-2">
<div className="flex items-center gap-2 text-sm text-gray-400">
<Calendar className="w-4 h-4" />
Created: {formatDateTime(job.dates.creation)}
Created: {formatDateTime(job.created)}
</div>
<div className="flex items-center gap-2 text-sm text-gray-400">
<Clock className="w-4 h-4" />
Start: {formatDateTime(job.dates.start)}
Start: {formatDateTime(job.jobStart)}
</div>
<div className="flex items-center gap-2 text-sm text-gray-400">
<Factory className="w-4 h-4" />
Facility: {job.facilityId}
Job ID: {job.id}
</div>
</div>
{job.dates.saleStart && (
{job.saleStart && (
<div className="space-y-2">
<div className="text-sm text-gray-400">
Sale Period: {formatDateTime(job.dates.saleStart)} - {formatDateTime(job.dates.saleEnd)}
Sale Period: {formatDateTime(job.saleStart)} - {formatDateTime(job.saleEnd)}
</div>
{itemsPerDay > 0 && (
<div className="text-sm text-gray-400">

View File

@@ -1,3 +1,4 @@
import React, { useState, useEffect } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
@@ -5,35 +6,35 @@ 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 { Job, Facility, BillOfMaterialsItem, ConsumedMaterialsItem } from '@/services/jobService';
import { IndJobRecord, IndFacilityResponse, IndBillitemRecord, IndJobStatusOptions } from '@/lib/pbtypes';
import { facilityService } from '@/services/facilityService';
import MaterialsImportExport from './MaterialsImportExport';
interface JobFormProps {
job?: Job;
onSubmit: (job: Omit<Job, 'id' | 'expenditures' | 'income'>) => void;
job?: IndJobRecord & {
billOfMaterials?: IndBillitemRecord[];
consumedMaterials?: { name: string; required: number }[];
};
onSubmit: (job: Omit<IndJobRecord, 'id' | 'created' | 'updated'>) => void;
onCancel: () => void;
}
const JobForm: React.FC<JobFormProps> = ({ job, onSubmit, onCancel }) => {
const [facilities, setFacilities] = useState<Facility[]>([]);
const [billOfMaterials, setBillOfMaterials] = useState<BillOfMaterialsItem[]>(job?.billOfMaterials || []);
const [consumedMaterials, setConsumedMaterials] = useState<ConsumedMaterialsItem[]>(job?.consumedMaterials || []);
const [facilities, setFacilities] = useState<IndFacilityResponse[]>([]);
const [billOfMaterials, setBillOfMaterials] = useState<IndBillitemRecord[]>(job?.billOfMaterials || []);
const [consumedMaterials, setConsumedMaterials] = useState<{ name: string; required: number }[]>(job?.consumedMaterials || []);
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().slice(0, 16) : new Date().toISOString().slice(0, 16),
start: job?.dates.start ? job.dates.start.toISOString().slice(0, 16) : '',
end: job?.dates.end ? job.dates.end.toISOString().slice(0, 16) : '',
saleStart: job?.dates.saleStart ? job.dates.saleStart.toISOString().slice(0, 16) : '',
saleEnd: job?.dates.saleEnd ? job.dates.saleEnd.toISOString().slice(0, 16) : ''
},
status: job?.status || 'Planned' as const,
facilityId: job?.facilityId || ''
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) : '',
status: job?.status || IndJobStatusOptions.Planned,
billOfMaterials: job?.billOfMaterials || [],
consumedMaterials: job?.consumedMaterials || [],
expenditures: job?.expenditures || [],
income: job?.income || []
});
useEffect(() => {
@@ -54,28 +55,20 @@ const JobForm: React.FC<JobFormProps> = ({ job, onSubmit, onCancel }) => {
onSubmit({
outputItem: formData.outputItem,
dates: {
creation: new Date(formData.dates.creation),
start: formData.dates.start ? new Date(formData.dates.start) : null,
end: formData.dates.end ? new Date(formData.dates.end) : null,
saleStart: formData.dates.saleStart ? new Date(formData.dates.saleStart) : null,
saleEnd: formData.dates.saleEnd ? new Date(formData.dates.saleEnd) : null
},
outputQuantity: formData.outputQuantity,
jobStart: formData.jobStart ? formData.jobStart : undefined,
jobEnd: formData.jobEnd ? formData.jobEnd : undefined,
saleStart: formData.saleStart ? formData.saleStart : undefined,
saleEnd: formData.saleEnd ? formData.saleEnd : undefined,
status: formData.status,
facilityId: formData.facilityId,
billOfMaterials,
consumedMaterials
billOfMaterials: formData.billOfMaterials,
consumedMaterials: formData.consumedMaterials,
expenditures: formData.expenditures,
income: formData.income
});
};
const statusOptions = [
'Planned',
'Transporting Materials',
'Running',
'Done',
'Selling',
'Closed'
] as const;
const statusOptions = Object.values(IndJobStatusOptions);
return (
<Card className="bg-gray-900 border-gray-700 text-white">
@@ -98,10 +91,10 @@ const JobForm: React.FC<JobFormProps> = ({ job, onSubmit, onCancel }) => {
<Label htmlFor="itemName" className="text-gray-300">Output Item Name</Label>
<Input
id="itemName"
value={formData.outputItem.name}
value={formData.outputItem}
onChange={(e) => setFormData({
...formData,
outputItem: { ...formData.outputItem, name: e.target.value }
outputItem: e.target.value
})}
className="bg-gray-800 border-gray-600 text-white"
required
@@ -112,10 +105,10 @@ const JobForm: React.FC<JobFormProps> = ({ job, onSubmit, onCancel }) => {
<Input
id="itemQuantity"
type="number"
value={formData.outputItem.quantity}
value={formData.outputQuantity}
onChange={(e) => setFormData({
...formData,
outputItem: { ...formData.outputItem, quantity: parseInt(e.target.value) || 0 }
outputQuantity: parseInt(e.target.value) || 0
})}
className="bg-gray-800 border-gray-600 text-white"
required
@@ -123,58 +116,23 @@ const JobForm: React.FC<JobFormProps> = ({ job, onSubmit, onCancel }) => {
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="facility" className="text-gray-300">Facility</Label>
<Select
value={formData.facilityId}
onValueChange={(value) => setFormData({ ...formData, facilityId: value })}
>
<SelectTrigger className="bg-gray-800 border-gray-600 text-white">
<SelectValue placeholder="Select a facility" />
</SelectTrigger>
<SelectContent className="bg-gray-800 border-gray-600">
{facilities.map((facility) => (
<SelectItem key={facility.id} value={facility.id} className="text-white">
{facility.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="status" className="text-gray-300">Status</Label>
<Select
value={formData.status}
onValueChange={(value) => setFormData({ ...formData, status: value as any })}
>
<SelectTrigger className="bg-gray-800 border-gray-600 text-white">
<SelectValue placeholder="Select status" />
</SelectTrigger>
<SelectContent className="bg-gray-800 border-gray-600">
{statusOptions.map((status) => (
<SelectItem key={status} value={status} className="text-white">
{status}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="creationDate" className="text-gray-300">Creation Date & Time</Label>
<Input
id="creationDate"
type="datetime-local"
value={formData.dates.creation}
onChange={(e) => setFormData({
...formData,
dates: { ...formData.dates, creation: e.target.value }
})}
className="bg-gray-800 border-gray-600 text-white"
required
/>
<Label htmlFor="status" className="text-gray-300">Status</Label>
<Select
value={formData.status}
onValueChange={(value) => setFormData({ ...formData, status: value as IndJobStatusOptions })}
>
<SelectTrigger className="bg-gray-800 border-gray-600 text-white">
<SelectValue placeholder="Select status" />
</SelectTrigger>
<SelectContent className="bg-gray-800 border-gray-600">
{statusOptions.map((status) => (
<SelectItem key={status} value={status} className="text-white">
{status}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="grid grid-cols-2 gap-4">
@@ -183,10 +141,10 @@ const JobForm: React.FC<JobFormProps> = ({ job, onSubmit, onCancel }) => {
<Input
id="startDate"
type="datetime-local"
value={formData.dates.start}
value={formData.jobStart}
onChange={(e) => setFormData({
...formData,
dates: { ...formData.dates, start: e.target.value }
jobStart: e.target.value
})}
className="bg-gray-800 border-gray-600 text-white"
/>
@@ -196,10 +154,10 @@ const JobForm: React.FC<JobFormProps> = ({ job, onSubmit, onCancel }) => {
<Input
id="endDate"
type="datetime-local"
value={formData.dates.end}
value={formData.jobEnd}
onChange={(e) => setFormData({
...formData,
dates: { ...formData.dates, end: e.target.value }
jobEnd: e.target.value
})}
className="bg-gray-800 border-gray-600 text-white"
/>
@@ -212,10 +170,10 @@ const JobForm: React.FC<JobFormProps> = ({ job, onSubmit, onCancel }) => {
<Input
id="saleStartDate"
type="datetime-local"
value={formData.dates.saleStart}
value={formData.saleStart}
onChange={(e) => setFormData({
...formData,
dates: { ...formData.dates, saleStart: e.target.value }
saleStart: e.target.value
})}
className="bg-gray-800 border-gray-600 text-white"
/>
@@ -225,10 +183,10 @@ const JobForm: React.FC<JobFormProps> = ({ job, onSubmit, onCancel }) => {
<Input
id="saleEndDate"
type="datetime-local"
value={formData.dates.saleEnd}
value={formData.saleEnd}
onChange={(e) => setFormData({
...formData,
dates: { ...formData.dates, saleEnd: e.target.value }
saleEnd: e.target.value
})}
className="bg-gray-800 border-gray-600 text-white"
/>

View File

@@ -1,17 +1,16 @@
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 { Label } from '@/components/ui/label';
import { Import, Download, FileText } from 'lucide-react';
import { BillOfMaterialsItem, ConsumedMaterialsItem } from '@/services/jobService';
import { IndBillitemRecord } from '@/lib/pbtypes';
interface MaterialsImportExportProps {
billOfMaterials: BillOfMaterialsItem[];
consumedMaterials: ConsumedMaterialsItem[];
onBillOfMaterialsUpdate: (materials: BillOfMaterialsItem[]) => void;
onConsumedMaterialsUpdate: (materials: ConsumedMaterialsItem[]) => void;
billOfMaterials: IndBillitemRecord[];
consumedMaterials: { name: string; required: number }[];
onBillOfMaterialsUpdate: (materials: IndBillitemRecord[]) => void;
onConsumedMaterialsUpdate: (materials: { name: string; required: number }[]) => void;
}
const MaterialsImportExport: React.FC<MaterialsImportExportProps> = ({
@@ -23,21 +22,27 @@ const MaterialsImportExport: React.FC<MaterialsImportExportProps> = ({
const [bomInput, setBomInput] = useState('');
const [consumedInput, setConsumedInput] = useState('');
const parseBillOfMaterials = (text: string): BillOfMaterialsItem[] => {
const parseBillOfMaterials = (text: string): IndBillitemRecord[] => {
return text.split('\n')
.filter(line => line.trim())
.map(line => {
const parts = line.trim().split(/\s+/);
const quantity = parseInt(parts[parts.length - 1]);
const name = parts.slice(0, -1).join(' ');
return { name, quantity };
return {
id: '',
name,
quantity,
created: undefined,
updated: undefined
};
})
.filter(item => item.name && !isNaN(item.quantity));
};
const parseConsumedMaterials = (text: string): ConsumedMaterialsItem[] => {
const parseConsumedMaterials = (text: string): { name: string; required: number }[] => {
const lines = text.split('\n').filter(line => line.trim());
const materials: ConsumedMaterialsItem[] = [];
const materials: { name: string; required: number }[] = [];
for (const line of lines) {
const parts = line.trim().split('\t');

View File

@@ -7,31 +7,42 @@ 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';
import { IndTransactionRecord } from '@/lib/pbtypes';
import { Check, X } from 'lucide-react';
interface TransactionFormProps {
jobId: string;
onTransactionsAdded: (transactions: Transaction[], type: 'expenditure' | 'income') => void;
onTransactionsAdded: (transactions: IndTransactionRecord[], type: 'expenditure' | 'income') => void;
}
const TransactionForm: React.FC<TransactionFormProps> = ({ jobId, onTransactionsAdded }) => {
const [pastedData, setPastedData] = useState('');
const [parsedTransactions, setParsedTransactions] = useState<Transaction[]>([]);
const [parsedTransactions, setParsedTransactions] = useState<IndTransactionRecord[]>([]);
const [transactionType, setTransactionType] = useState<'expenditure' | 'income'>('expenditure');
const handlePaste = (value: string) => {
setPastedData(value);
const lines = value.trim().split('\n');
const transactions: Transaction[] = [];
const transactions: IndTransactionRecord[] = [];
lines.forEach((line, index) => {
const parsed = parseTransactionLine(line);
if (parsed) {
transactions.push({
id: `temp-${index}`,
...parsed
date: parsed.date.toISOString(),
quantity: parsed.quantity,
itemName: parsed.itemName,
unitPrice: parsed.unitPrice,
totalPrice: Math.abs(parsed.totalAmount),
buyer: parsed.buyer,
location: parsed.location,
corporation: parsed.corporation,
wallet: parsed.wallet,
job: jobId,
created: undefined,
updated: undefined
});
}
});
@@ -47,7 +58,7 @@ const TransactionForm: React.FC<TransactionFormProps> = ({ jobId, onTransactions
}
};
const totalAmount = parsedTransactions.reduce((sum, tx) => sum + Math.abs(tx.totalAmount), 0);
const totalAmount = parsedTransactions.reduce((sum, tx) => sum + tx.totalPrice, 0);
return (
<Card className="bg-gray-900 border-gray-700 text-white">
@@ -104,7 +115,7 @@ const TransactionForm: React.FC<TransactionFormProps> = ({ jobId, onTransactions
{parsedTransactions.map((tx, index) => (
<TableRow key={index} className="border-gray-700">
<TableCell className="text-gray-300">
{tx.date.toLocaleDateString()}
{new Date(tx.date).toLocaleDateString()}
</TableCell>
<TableCell className="text-white">{tx.itemName}</TableCell>
<TableCell className="text-gray-300">
@@ -114,7 +125,7 @@ const TransactionForm: React.FC<TransactionFormProps> = ({ jobId, onTransactions
{formatISK(tx.unitPrice)}
</TableCell>
<TableCell className={`font-semibold ${transactionType === 'expenditure' ? 'text-red-400' : 'text-green-400'}`}>
{formatISK(Math.abs(tx.totalAmount))}
{formatISK(tx.totalPrice)}
</TableCell>
</TableRow>
))}

View File

@@ -5,15 +5,15 @@ import { Button } from '@/components/ui/button';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import { Badge } from '@/components/ui/badge';
import { Input } from '@/components/ui/input';
import { Transaction } from '@/services/jobService';
import { IndTransactionResponse } from '@/lib/pbtypes';
import { formatISK } from '@/utils/priceUtils';
import { Edit, Save, X, Trash2 } from 'lucide-react';
interface TransactionTableProps {
title: string;
transactions: Transaction[];
transactions: IndTransactionResponse[];
type: 'expenditure' | 'income';
onUpdateTransaction: (transactionId: string, updates: Partial<Transaction>) => void;
onUpdateTransaction: (transactionId: string, updates: Partial<IndTransactionResponse>) => void;
onDeleteTransaction: (transactionId: string) => void;
}
@@ -25,11 +25,11 @@ const TransactionTable: React.FC<TransactionTableProps> = ({
onDeleteTransaction
}) => {
const [editingId, setEditingId] = useState<string | null>(null);
const [editingTransaction, setEditingTransaction] = useState<Transaction | null>(null);
const [editingTransaction, setEditingTransaction] = useState<IndTransactionResponse | null>(null);
const totalAmount = transactions.reduce((sum, tx) => sum + Math.abs(tx.totalAmount), 0);
const totalAmount = transactions.reduce((sum, tx) => sum + tx.totalPrice, 0);
const handleEdit = (transaction: Transaction) => {
const handleEdit = (transaction: IndTransactionResponse) => {
setEditingId(transaction.id);
setEditingTransaction({ ...transaction });
};
@@ -53,7 +53,7 @@ const TransactionTable: React.FC<TransactionTableProps> = ({
}
};
const updateEditingField = (field: keyof Transaction, value: any) => {
const updateEditingField = (field: keyof IndTransactionResponse, value: any) => {
if (editingTransaction) {
setEditingTransaction({
...editingTransaction,
@@ -97,12 +97,12 @@ const TransactionTable: React.FC<TransactionTableProps> = ({
{editingId === transaction.id ? (
<Input
type="datetime-local"
value={editingTransaction?.date.toISOString().slice(0, 16)}
onChange={(e) => updateEditingField('date', new Date(e.target.value))}
value={editingTransaction?.date ? new Date(editingTransaction.date).toISOString().slice(0, 16) : ''}
onChange={(e) => updateEditingField('date', e.target.value)}
className="bg-gray-800 border-gray-600 text-white text-xs"
/>
) : (
transaction.date.toLocaleString('sv-SE', {
new Date(transaction.date).toLocaleString('sv-SE', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
@@ -152,15 +152,12 @@ const TransactionTable: React.FC<TransactionTableProps> = ({
<Input
type="number"
step="0.01"
value={Math.abs(editingTransaction?.totalAmount || 0)}
onChange={(e) => {
const value = parseFloat(e.target.value) || 0;
updateEditingField('totalAmount', type === 'expenditure' ? -Math.abs(value) : Math.abs(value));
}}
value={editingTransaction?.totalPrice || 0}
onChange={(e) => updateEditingField('totalPrice', parseFloat(e.target.value) || 0)}
className="bg-gray-800 border-gray-600 text-white"
/>
) : (
formatISK(Math.abs(transaction.totalAmount))
formatISK(transaction.totalPrice)
)}
</TableCell>
<TableCell className="text-gray-300">

View File

@@ -2,18 +2,27 @@ import React, { useState, useEffect } from 'react';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Plus, Factory, TrendingUp, Briefcase } from 'lucide-react';
import { Job, Transaction, jobService } from '@/services/jobService';
import { IndJobResponse, IndTransactionResponse, IndJobRecord, IndTransactionRecord } from '@/lib/pbtypes';
import * as jobService from '@/services/jobService';
import { formatISK } from '@/utils/priceUtils';
import JobCard from '@/components/JobCard';
import JobForm from '@/components/JobForm';
import TransactionForm from '@/components/TransactionForm';
import TransactionTable from '@/components/TransactionTable';
// Extended job type for UI components
interface JobWithRelations extends IndJobResponse {
expenditures: IndTransactionResponse[];
income: IndTransactionResponse[];
billOfMaterials: any[];
consumedMaterials: { name: string; required: number }[];
}
const Index = () => {
const [jobs, setJobs] = useState<Job[]>([]);
const [jobs, setJobs] = useState<JobWithRelations[]>([]);
const [showJobForm, setShowJobForm] = useState(false);
const [editingJob, setEditingJob] = useState<Job | null>(null);
const [selectedJob, setSelectedJob] = useState<Job | null>(null);
const [editingJob, setEditingJob] = useState<JobWithRelations | null>(null);
const [selectedJob, setSelectedJob] = useState<JobWithRelations | null>(null);
useEffect(() => {
loadJobs();
@@ -22,38 +31,61 @@ const Index = () => {
const loadJobs = async () => {
try {
const fetchedJobs = await jobService.getJobs();
setJobs(fetchedJobs);
// Convert to JobWithRelations format
const jobsWithRelations: JobWithRelations[] = fetchedJobs.map(job => ({
...job,
expenditures: [],
income: [],
billOfMaterials: [],
consumedMaterials: []
}));
setJobs(jobsWithRelations);
} catch (error) {
console.error('Error loading jobs:', error);
}
};
const handleCreateJob = async (jobData: Omit<Job, 'id' | 'expenditures' | 'income'>) => {
const handleCreateJob = async (jobData: Omit<IndJobRecord, 'id' | 'created' | 'updated'>) => {
try {
const newJob = await jobService.createJob(jobData);
setJobs([...jobs, newJob]);
const jobWithRelations: JobWithRelations = {
...newJob,
expenditures: [],
income: [],
billOfMaterials: [],
consumedMaterials: []
};
setJobs([...jobs, jobWithRelations]);
setShowJobForm(false);
} catch (error) {
console.error('Error creating job:', error);
}
};
const handleEditJob = (job: Job) => {
const handleEditJob = (job: JobWithRelations) => {
setEditingJob(job);
setShowJobForm(true);
};
const handleUpdateJob = async (jobData: Omit<Job, 'id' | 'expenditures' | 'income'>) => {
const handleUpdateJob = async (jobData: Omit<IndJobRecord, 'id' | 'created' | 'updated'>) => {
if (!editingJob) return;
try {
const updatedJob = await jobService.updateJob(editingJob.id, jobData);
setJobs(jobs.map(job => job.id === editingJob.id ? updatedJob : job));
const updatedJobWithRelations: JobWithRelations = {
...updatedJob,
expenditures: editingJob.expenditures,
income: editingJob.income,
billOfMaterials: editingJob.billOfMaterials,
consumedMaterials: editingJob.consumedMaterials
};
setJobs(jobs.map(job => job.id === editingJob.id ? updatedJobWithRelations : job));
setShowJobForm(false);
setEditingJob(null);
// Update selectedJob if it's the same job being edited
if (selectedJob?.id === editingJob.id) {
setSelectedJob(updatedJob);
setSelectedJob(updatedJobWithRelations);
}
} catch (error) {
console.error('Error updating job:', error);
@@ -74,7 +106,7 @@ const Index = () => {
}
};
const handleTransactionsAdded = async (transactions: Transaction[], type: 'expenditure' | 'income') => {
const handleTransactionsAdded = async (transactions: IndTransactionRecord[], type: 'expenditure' | 'income') => {
if (!selectedJob) return;
try {
@@ -84,10 +116,12 @@ const Index = () => {
// Update local state
const updatedJob = { ...selectedJob };
const newTransactions = transactions as unknown as IndTransactionResponse[];
if (type === 'expenditure') {
updatedJob.expenditures = [...updatedJob.expenditures, ...transactions];
updatedJob.expenditures = [...updatedJob.expenditures, ...newTransactions];
} else {
updatedJob.income = [...updatedJob.income, ...transactions];
updatedJob.income = [...updatedJob.income, ...newTransactions];
}
setSelectedJob(updatedJob);
@@ -97,7 +131,7 @@ const Index = () => {
}
};
const handleUpdateTransaction = async (transactionId: string, updates: Partial<Transaction>) => {
const handleUpdateTransaction = async (transactionId: string, updates: Partial<IndTransactionResponse>) => {
if (!selectedJob) return;
try {
@@ -139,13 +173,13 @@ const Index = () => {
const totalJobs = jobs.length;
const totalProfit = jobs.reduce((sum, job) => {
const expenditure = job.expenditures.reduce((sum, tx) => sum + Math.abs(tx.totalAmount), 0);
const income = job.income.reduce((sum, tx) => sum + tx.totalAmount, 0);
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.totalAmount, 0), 0
sum + job.income.reduce((sum, tx) => sum + tx.totalPrice, 0), 0
);
if (showJobForm) {
@@ -172,7 +206,7 @@ const Index = () => {
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold text-white">Job Details</h1>
<p className="text-gray-400">{selectedJob.outputItem.name}</p>
<p className="text-gray-400">{selectedJob.outputItem}</p>
</div>
<Button
variant="outline"

View File

@@ -0,0 +1,87 @@
import * as jobService from './jobService';
import * as facilityService from './facilityService';
import { IndJobResponse, IndTransactionResponse, IndBillitemResponse } from '@/lib/pbtypes';
export interface JobWithRelations extends IndJobResponse {
expenditures: IndTransactionResponse[];
income: IndTransactionResponse[];
billOfMaterials: IndBillitemResponse[];
consumedMaterials: { name: string; required: number }[];
}
export async function getJobsWithRelations(): Promise<JobWithRelations[]> {
const jobs = await jobService.getJobs();
const jobsWithRelations: JobWithRelations[] = [];
for (const job of jobs) {
const expenditures: IndTransactionResponse[] = [];
const income: IndTransactionResponse[] = [];
const billOfMaterials: IndBillitemResponse[] = [];
// Fetch related transactions
if (job.expenditures) {
for (const txId of job.expenditures) {
try {
const tx = await pb.collection('ind_transaction').getOne(txId);
expenditures.push(tx as IndTransactionResponse);
} catch (e) {
console.warn('Failed to fetch expenditure transaction:', txId);
}
}
}
if (job.income) {
for (const txId of job.income) {
try {
const tx = await pb.collection('ind_transaction').getOne(txId);
income.push(tx as IndTransactionResponse);
} catch (e) {
console.warn('Failed to fetch income transaction:', txId);
}
}
}
// Fetch bill of materials
if (job.billOfMaterials) {
for (const itemId of job.billOfMaterials) {
try {
const item = await pb.collection('ind_billItem').getOne(itemId);
billOfMaterials.push(item as IndBillitemResponse);
} catch (e) {
console.warn('Failed to fetch bill item:', itemId);
}
}
}
jobsWithRelations.push({
...job,
expenditures,
income,
billOfMaterials,
consumedMaterials: [] // This would need to be stored/retrieved differently
});
}
return jobsWithRelations;
}
export async function getJobWithRelations(id: string): Promise<JobWithRelations | null> {
const job = await jobService.getJob(id);
if (!job) return null;
const expenditures: IndTransactionResponse[] = [];
const income: IndTransactionResponse[] = [];
const billOfMaterials: IndBillitemResponse[] = [];
// Fetch related data similar to above
// ... (similar logic as in getJobsWithRelations)
return {
...job,
expenditures,
income,
billOfMaterials,
consumedMaterials: []
};
}

View File

@@ -1,3 +1,4 @@
import type { IndJobRecord, IndJobResponse, IndTransactionRecord, IndTransactionResponse, IndBillitemRecord, IndBillitemResponse } from '../lib/pbtypes';
import pb from '../lib/pocketbase';