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:
242
src/components/TransactionTable.tsx
Normal file
242
src/components/TransactionTable.tsx
Normal 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;
|
@@ -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>
|
||||
);
|
||||
|
@@ -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);
|
||||
}
|
||||
};
|
||||
|
Reference in New Issue
Block a user