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

View File

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

View File

@@ -1,17 +1,16 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Textarea } from '@/components/ui/textarea'; import { Textarea } from '@/components/ui/textarea';
import { Label } from '@/components/ui/label'; import { Label } from '@/components/ui/label';
import { Import, Download, FileText } from 'lucide-react'; import { Import, Download, FileText } from 'lucide-react';
import { BillOfMaterialsItem, ConsumedMaterialsItem } from '@/services/jobService'; import { IndBillitemRecord } from '@/lib/pbtypes';
interface MaterialsImportExportProps { interface MaterialsImportExportProps {
billOfMaterials: BillOfMaterialsItem[]; billOfMaterials: IndBillitemRecord[];
consumedMaterials: ConsumedMaterialsItem[]; consumedMaterials: { name: string; required: number }[];
onBillOfMaterialsUpdate: (materials: BillOfMaterialsItem[]) => void; onBillOfMaterialsUpdate: (materials: IndBillitemRecord[]) => void;
onConsumedMaterialsUpdate: (materials: ConsumedMaterialsItem[]) => void; onConsumedMaterialsUpdate: (materials: { name: string; required: number }[]) => void;
} }
const MaterialsImportExport: React.FC<MaterialsImportExportProps> = ({ const MaterialsImportExport: React.FC<MaterialsImportExportProps> = ({
@@ -23,21 +22,27 @@ const MaterialsImportExport: React.FC<MaterialsImportExportProps> = ({
const [bomInput, setBomInput] = useState(''); const [bomInput, setBomInput] = useState('');
const [consumedInput, setConsumedInput] = useState(''); const [consumedInput, setConsumedInput] = useState('');
const parseBillOfMaterials = (text: string): BillOfMaterialsItem[] => { const parseBillOfMaterials = (text: string): IndBillitemRecord[] => {
return text.split('\n') return text.split('\n')
.filter(line => line.trim()) .filter(line => line.trim())
.map(line => { .map(line => {
const parts = line.trim().split(/\s+/); const parts = line.trim().split(/\s+/);
const quantity = parseInt(parts[parts.length - 1]); const quantity = parseInt(parts[parts.length - 1]);
const name = parts.slice(0, -1).join(' '); 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)); .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 lines = text.split('\n').filter(line => line.trim());
const materials: ConsumedMaterialsItem[] = []; const materials: { name: string; required: number }[] = [];
for (const line of lines) { for (const line of lines) {
const parts = line.trim().split('\t'); 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 { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import { parseTransactionLine, formatISK } from '@/utils/priceUtils'; import { parseTransactionLine, formatISK } from '@/utils/priceUtils';
import { Transaction } from '@/services/jobService'; import { IndTransactionRecord } from '@/lib/pbtypes';
import { Upload, Check, X } from 'lucide-react'; import { Check, X } from 'lucide-react';
interface TransactionFormProps { interface TransactionFormProps {
jobId: string; jobId: string;
onTransactionsAdded: (transactions: Transaction[], type: 'expenditure' | 'income') => void; onTransactionsAdded: (transactions: IndTransactionRecord[], type: 'expenditure' | 'income') => void;
} }
const TransactionForm: React.FC<TransactionFormProps> = ({ jobId, onTransactionsAdded }) => { const TransactionForm: React.FC<TransactionFormProps> = ({ jobId, onTransactionsAdded }) => {
const [pastedData, setPastedData] = useState(''); const [pastedData, setPastedData] = useState('');
const [parsedTransactions, setParsedTransactions] = useState<Transaction[]>([]); const [parsedTransactions, setParsedTransactions] = useState<IndTransactionRecord[]>([]);
const [transactionType, setTransactionType] = useState<'expenditure' | 'income'>('expenditure'); const [transactionType, setTransactionType] = useState<'expenditure' | 'income'>('expenditure');
const handlePaste = (value: string) => { const handlePaste = (value: string) => {
setPastedData(value); setPastedData(value);
const lines = value.trim().split('\n'); const lines = value.trim().split('\n');
const transactions: Transaction[] = []; const transactions: IndTransactionRecord[] = [];
lines.forEach((line, index) => { lines.forEach((line, index) => {
const parsed = parseTransactionLine(line); const parsed = parseTransactionLine(line);
if (parsed) { if (parsed) {
transactions.push({ transactions.push({
id: `temp-${index}`, 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 ( return (
<Card className="bg-gray-900 border-gray-700 text-white"> <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) => ( {parsedTransactions.map((tx, index) => (
<TableRow key={index} className="border-gray-700"> <TableRow key={index} className="border-gray-700">
<TableCell className="text-gray-300"> <TableCell className="text-gray-300">
{tx.date.toLocaleDateString()} {new Date(tx.date).toLocaleDateString()}
</TableCell> </TableCell>
<TableCell className="text-white">{tx.itemName}</TableCell> <TableCell className="text-white">{tx.itemName}</TableCell>
<TableCell className="text-gray-300"> <TableCell className="text-gray-300">
@@ -114,7 +125,7 @@ const TransactionForm: React.FC<TransactionFormProps> = ({ jobId, onTransactions
{formatISK(tx.unitPrice)} {formatISK(tx.unitPrice)}
</TableCell> </TableCell>
<TableCell className={`font-semibold ${transactionType === 'expenditure' ? 'text-red-400' : 'text-green-400'}`}> <TableCell className={`font-semibold ${transactionType === 'expenditure' ? 'text-red-400' : 'text-green-400'}`}>
{formatISK(Math.abs(tx.totalAmount))} {formatISK(tx.totalPrice)}
</TableCell> </TableCell>
</TableRow> </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 { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { Transaction } from '@/services/jobService'; import { IndTransactionResponse } from '@/lib/pbtypes';
import { formatISK } from '@/utils/priceUtils'; import { formatISK } from '@/utils/priceUtils';
import { Edit, Save, X, Trash2 } from 'lucide-react'; import { Edit, Save, X, Trash2 } from 'lucide-react';
interface TransactionTableProps { interface TransactionTableProps {
title: string; title: string;
transactions: Transaction[]; transactions: IndTransactionResponse[];
type: 'expenditure' | 'income'; type: 'expenditure' | 'income';
onUpdateTransaction: (transactionId: string, updates: Partial<Transaction>) => void; onUpdateTransaction: (transactionId: string, updates: Partial<IndTransactionResponse>) => void;
onDeleteTransaction: (transactionId: string) => void; onDeleteTransaction: (transactionId: string) => void;
} }
@@ -25,11 +25,11 @@ const TransactionTable: React.FC<TransactionTableProps> = ({
onDeleteTransaction onDeleteTransaction
}) => { }) => {
const [editingId, setEditingId] = useState<string | null>(null); 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); setEditingId(transaction.id);
setEditingTransaction({ ...transaction }); 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) { if (editingTransaction) {
setEditingTransaction({ setEditingTransaction({
...editingTransaction, ...editingTransaction,
@@ -97,12 +97,12 @@ const TransactionTable: React.FC<TransactionTableProps> = ({
{editingId === transaction.id ? ( {editingId === transaction.id ? (
<Input <Input
type="datetime-local" type="datetime-local"
value={editingTransaction?.date.toISOString().slice(0, 16)} value={editingTransaction?.date ? new Date(editingTransaction.date).toISOString().slice(0, 16) : ''}
onChange={(e) => updateEditingField('date', new Date(e.target.value))} onChange={(e) => updateEditingField('date', e.target.value)}
className="bg-gray-800 border-gray-600 text-white text-xs" 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', year: 'numeric',
month: '2-digit', month: '2-digit',
day: '2-digit', day: '2-digit',
@@ -152,15 +152,12 @@ const TransactionTable: React.FC<TransactionTableProps> = ({
<Input <Input
type="number" type="number"
step="0.01" step="0.01"
value={Math.abs(editingTransaction?.totalAmount || 0)} value={editingTransaction?.totalPrice || 0}
onChange={(e) => { onChange={(e) => updateEditingField('totalPrice', parseFloat(e.target.value) || 0)}
const value = parseFloat(e.target.value) || 0;
updateEditingField('totalAmount', type === 'expenditure' ? -Math.abs(value) : Math.abs(value));
}}
className="bg-gray-800 border-gray-600 text-white" className="bg-gray-800 border-gray-600 text-white"
/> />
) : ( ) : (
formatISK(Math.abs(transaction.totalAmount)) formatISK(transaction.totalPrice)
)} )}
</TableCell> </TableCell>
<TableCell className="text-gray-300"> <TableCell className="text-gray-300">

View File

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