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:
@@ -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">
|
||||
|
@@ -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"
|
||||
/>
|
||||
|
@@ -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');
|
||||
|
@@ -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>
|
||||
))}
|
||||
|
@@ -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">
|
||||
|
@@ -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"
|
||||
|
87
src/services/jobDataService.ts
Normal file
87
src/services/jobDataService.ts
Normal 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: []
|
||||
};
|
||||
}
|
@@ -1,3 +1,4 @@
|
||||
|
||||
import type { IndJobRecord, IndJobResponse, IndTransactionRecord, IndTransactionResponse, IndBillitemRecord, IndBillitemResponse } from '../lib/pbtypes';
|
||||
import pb from '../lib/pocketbase';
|
||||
|
||||
|
Reference in New Issue
Block a user