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 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">
|
||||||
|
@@ -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,58 +116,23 @@ 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">
|
|
||||||
<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">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="creationDate" className="text-gray-300">Creation Date & Time</Label>
|
<Label htmlFor="status" className="text-gray-300">Status</Label>
|
||||||
<Input
|
<Select
|
||||||
id="creationDate"
|
value={formData.status}
|
||||||
type="datetime-local"
|
onValueChange={(value) => setFormData({ ...formData, status: value as IndJobStatusOptions })}
|
||||||
value={formData.dates.creation}
|
>
|
||||||
onChange={(e) => setFormData({
|
<SelectTrigger className="bg-gray-800 border-gray-600 text-white">
|
||||||
...formData,
|
<SelectValue placeholder="Select status" />
|
||||||
dates: { ...formData.dates, creation: e.target.value }
|
</SelectTrigger>
|
||||||
})}
|
<SelectContent className="bg-gray-800 border-gray-600">
|
||||||
className="bg-gray-800 border-gray-600 text-white"
|
{statusOptions.map((status) => (
|
||||||
required
|
<SelectItem key={status} value={status} className="text-white">
|
||||||
/>
|
{status}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
@@ -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"
|
||||||
/>
|
/>
|
||||||
|
@@ -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');
|
||||||
|
@@ -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>
|
||||||
))}
|
))}
|
||||||
|
@@ -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">
|
||||||
|
@@ -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"
|
||||||
|
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 type { IndJobRecord, IndJobResponse, IndTransactionRecord, IndTransactionResponse, IndBillitemRecord, IndBillitemResponse } from '../lib/pbtypes';
|
||||||
import pb from '../lib/pocketbase';
|
import pb from '../lib/pocketbase';
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user