Add transaction tables to job details

Implemented transaction tables within the JobCard component to display and allow editing of existing expenditures and income.
This commit is contained in:
gpt-engineer-app[bot]
2025-07-04 12:42:25 +00:00
parent 0a9ce41a74
commit ee4f00355f
3 changed files with 311 additions and 2 deletions

View File

@@ -0,0 +1,242 @@
import React, { useState } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
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 { formatISK } from '@/utils/priceUtils';
import { Edit, Save, X, Trash2 } from 'lucide-react';
interface TransactionTableProps {
title: string;
transactions: Transaction[];
type: 'expenditure' | 'income';
onUpdateTransaction: (transactionId: string, updates: Partial<Transaction>) => void;
onDeleteTransaction: (transactionId: string) => void;
}
const TransactionTable: React.FC<TransactionTableProps> = ({
title,
transactions,
type,
onUpdateTransaction,
onDeleteTransaction
}) => {
const [editingId, setEditingId] = useState<string | null>(null);
const [editingTransaction, setEditingTransaction] = useState<Transaction | null>(null);
const totalAmount = transactions.reduce((sum, tx) => sum + Math.abs(tx.totalAmount), 0);
const handleEdit = (transaction: Transaction) => {
setEditingId(transaction.id);
setEditingTransaction({ ...transaction });
};
const handleSave = () => {
if (editingTransaction && editingId) {
onUpdateTransaction(editingId, editingTransaction);
setEditingId(null);
setEditingTransaction(null);
}
};
const handleCancel = () => {
setEditingId(null);
setEditingTransaction(null);
};
const handleDelete = (transactionId: string) => {
if (confirm('Are you sure you want to delete this transaction?')) {
onDeleteTransaction(transactionId);
}
};
const updateEditingField = (field: keyof Transaction, value: any) => {
if (editingTransaction) {
setEditingTransaction({
...editingTransaction,
[field]: value
});
}
};
return (
<Card className="bg-gray-900 border-gray-700 text-white">
<CardHeader>
<div className="flex justify-between items-center">
<CardTitle className="text-blue-400">{title}</CardTitle>
<Badge variant={type === 'expenditure' ? 'destructive' : 'default'}>
Total: {formatISK(totalAmount)}
</Badge>
</div>
</CardHeader>
<CardContent>
{transactions.length === 0 ? (
<p className="text-gray-400 text-center py-4">No transactions yet</p>
) : (
<div className="overflow-x-auto">
<Table>
<TableHeader>
<TableRow className="border-gray-700">
<TableHead className="text-gray-300">Date</TableHead>
<TableHead className="text-gray-300">Item</TableHead>
<TableHead className="text-gray-300">Qty</TableHead>
<TableHead className="text-gray-300">Unit Price</TableHead>
<TableHead className="text-gray-300">Total</TableHead>
<TableHead className="text-gray-300">Buyer/Seller</TableHead>
<TableHead className="text-gray-300">Location</TableHead>
<TableHead className="text-gray-300">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{transactions.map((transaction) => (
<TableRow key={transaction.id} className="border-gray-700">
<TableCell className="text-gray-300">
{editingId === transaction.id ? (
<Input
type="datetime-local"
value={editingTransaction?.date.toISOString().slice(0, 16)}
onChange={(e) => updateEditingField('date', new Date(e.target.value))}
className="bg-gray-800 border-gray-600 text-white text-xs"
/>
) : (
transaction.date.toLocaleString('sv-SE', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
}).replace(' ', ' ')
)}
</TableCell>
<TableCell className="text-white">
{editingId === transaction.id ? (
<Input
value={editingTransaction?.itemName || ''}
onChange={(e) => updateEditingField('itemName', e.target.value)}
className="bg-gray-800 border-gray-600 text-white"
/>
) : (
transaction.itemName
)}
</TableCell>
<TableCell className="text-gray-300">
{editingId === transaction.id ? (
<Input
type="number"
value={editingTransaction?.quantity || 0}
onChange={(e) => updateEditingField('quantity', parseInt(e.target.value) || 0)}
className="bg-gray-800 border-gray-600 text-white"
/>
) : (
transaction.quantity.toLocaleString()
)}
</TableCell>
<TableCell className="text-gray-300">
{editingId === transaction.id ? (
<Input
type="number"
step="0.01"
value={editingTransaction?.unitPrice || 0}
onChange={(e) => updateEditingField('unitPrice', parseFloat(e.target.value) || 0)}
className="bg-gray-800 border-gray-600 text-white"
/>
) : (
formatISK(transaction.unitPrice)
)}
</TableCell>
<TableCell className={`font-semibold ${type === 'expenditure' ? 'text-red-400' : 'text-green-400'}`}>
{editingId === transaction.id ? (
<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));
}}
className="bg-gray-800 border-gray-600 text-white"
/>
) : (
formatISK(Math.abs(transaction.totalAmount))
)}
</TableCell>
<TableCell className="text-gray-300">
{editingId === transaction.id ? (
<Input
value={editingTransaction?.buyer || ''}
onChange={(e) => updateEditingField('buyer', e.target.value)}
className="bg-gray-800 border-gray-600 text-white"
/>
) : (
transaction.buyer || '-'
)}
</TableCell>
<TableCell className="text-gray-300 max-w-32 truncate">
{editingId === transaction.id ? (
<Input
value={editingTransaction?.location || ''}
onChange={(e) => updateEditingField('location', e.target.value)}
className="bg-gray-800 border-gray-600 text-white"
/>
) : (
<span title={transaction.location}>{transaction.location || '-'}</span>
)}
</TableCell>
<TableCell>
<div className="flex gap-1">
{editingId === transaction.id ? (
<>
<Button
size="sm"
variant="outline"
onClick={handleSave}
className="border-green-600 hover:bg-green-700 p-1"
>
<Save className="w-3 h-3" />
</Button>
<Button
size="sm"
variant="outline"
onClick={handleCancel}
className="border-gray-600 hover:bg-gray-700 p-1"
>
<X className="w-3 h-3" />
</Button>
</>
) : (
<>
<Button
size="sm"
variant="outline"
onClick={() => handleEdit(transaction)}
className="border-gray-600 hover:bg-gray-700 p-1"
>
<Edit className="w-3 h-3" />
</Button>
<Button
size="sm"
variant="destructive"
onClick={() => handleDelete(transaction.id)}
className="p-1"
>
<Trash2 className="w-3 h-3" />
</Button>
</>
)}
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
)}
</CardContent>
</Card>
);
};
export default TransactionTable;

View File

@@ -1,4 +1,3 @@
import React, { useState, useEffect } from 'react';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
@@ -8,6 +7,7 @@ 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';
const Index = () => {
const [jobs, setJobs] = useState<Job[]>([]);
@@ -93,6 +93,46 @@ const Index = () => {
}
};
const handleUpdateTransaction = async (transactionId: string, updates: Partial<Transaction>) => {
if (!selectedJob) return;
try {
await jobService.updateTransaction(selectedJob.id, transactionId, updates);
// Update local state
const updatedJob = { ...selectedJob };
updatedJob.expenditures = updatedJob.expenditures.map(tx =>
tx.id === transactionId ? { ...tx, ...updates } : tx
);
updatedJob.income = updatedJob.income.map(tx =>
tx.id === transactionId ? { ...tx, ...updates } : tx
);
setSelectedJob(updatedJob);
setJobs(jobs.map(job => job.id === selectedJob.id ? updatedJob : job));
} catch (error) {
console.error('Error updating transaction:', error);
}
};
const handleDeleteTransaction = async (transactionId: string) => {
if (!selectedJob) return;
try {
await jobService.deleteTransaction(selectedJob.id, transactionId);
// Update local state
const updatedJob = { ...selectedJob };
updatedJob.expenditures = updatedJob.expenditures.filter(tx => tx.id !== transactionId);
updatedJob.income = updatedJob.income.filter(tx => tx.id !== transactionId);
setSelectedJob(updatedJob);
setJobs(jobs.map(job => job.id === selectedJob.id ? updatedJob : job));
} catch (error) {
console.error('Error deleting transaction:', error);
}
};
const totalJobs = jobs.length;
const totalProfit = jobs.reduce((sum, job) => {
const expenditure = job.expenditures.reduce((sum, tx) => sum + Math.abs(tx.totalAmount), 0);
@@ -124,7 +164,7 @@ const Index = () => {
if (selectedJob) {
return (
<div className="min-h-screen bg-gray-950 p-6">
<div className="max-w-6xl mx-auto space-y-6">
<div className="max-w-7xl mx-auto space-y-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold text-white">Job Details</h1>
@@ -150,6 +190,23 @@ const Index = () => {
onTransactionsAdded={handleTransactionsAdded}
/>
</div>
<div className="space-y-6">
<TransactionTable
title="Expenditures"
transactions={selectedJob.expenditures}
type="expenditure"
onUpdateTransaction={handleUpdateTransaction}
onDeleteTransaction={handleDeleteTransaction}
/>
<TransactionTable
title="Income"
transactions={selectedJob.income}
type="income"
onUpdateTransaction={handleUpdateTransaction}
onDeleteTransaction={handleDeleteTransaction}
/>
</div>
</div>
</div>
);

View File

@@ -77,5 +77,15 @@ export const jobService = {
async addTransaction(jobId: string, transaction: Omit<Transaction, 'id'>, type: 'expenditure' | 'income'): Promise<void> {
// TODO: Implement with PocketBase
console.log('Adding transaction:', jobId, transaction, type);
},
async updateTransaction(jobId: string, transactionId: string, updates: Partial<Transaction>): Promise<void> {
// TODO: Implement with PocketBase
console.log('Updating transaction:', jobId, transactionId, updates);
},
async deleteTransaction(jobId: string, transactionId: string): Promise<void> {
// TODO: Implement with PocketBase
console.log('Deleting transaction:', jobId, transactionId);
}
};