Spaces:
Running
Running
Update src/App.js
Browse files- src/App.js +97 -97
src/App.js
CHANGED
|
@@ -47,49 +47,49 @@ const useStore = () => {
|
|
| 47 |
|
| 48 |
const mockExtractTextFromImage = async (file) => {
|
| 49 |
await new Promise(resolve => setTimeout(resolve, 2000));
|
| 50 |
-
return `SUPERMARKET
|
| 51 |
-
Address:
|
| 52 |
Date: ${new Date().toLocaleDateString()}
|
| 53 |
----------------------------------------
|
| 54 |
-
Whole Milk 1L 2.50
|
| 55 |
-
Whole Wheat Bread 500g 1.80
|
| 56 |
-
Tomatoes 1kg 3.20
|
| 57 |
-
Chicken Fillets 500g 4.50
|
| 58 |
-
Olive Oil 500ml 5.80
|
| 59 |
-
Dozen Eggs 2.90
|
| 60 |
----------------------------------------
|
| 61 |
-
TOTAL: 20.70
|
| 62 |
};
|
| 63 |
|
| 64 |
const mockProcessReceiptText = async (text) => {
|
| 65 |
await new Promise(resolve => setTimeout(resolve, 1500));
|
| 66 |
const lines = text.split('\n');
|
| 67 |
const products = [];
|
| 68 |
-
let
|
| 69 |
-
let
|
| 70 |
-
let
|
| 71 |
|
| 72 |
lines.forEach(line => {
|
| 73 |
-
if (line.includes('SUPERMARKET') || line.includes('
|
| 74 |
-
|
| 75 |
}
|
| 76 |
if (line.includes('Address:')) {
|
| 77 |
-
|
| 78 |
}
|
| 79 |
if (line.includes('Date:')) {
|
| 80 |
-
|
| 81 |
}
|
| 82 |
|
| 83 |
-
const priceMatch = line.match(/(.+?)\s+(\d+[,.]?\d*
|
| 84 |
if (priceMatch && !line.includes('TOTAL') && !line.includes('---')) {
|
| 85 |
-
const
|
| 86 |
-
const
|
| 87 |
-
if (
|
| 88 |
products.push({
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
});
|
| 94 |
}
|
| 95 |
}
|
|
@@ -139,36 +139,36 @@ const Sidebar = ({ activeTab, setActiveTab }) => {
|
|
| 139 |
const HomeTab = () => {
|
| 140 |
const { products } = useStore();
|
| 141 |
|
| 142 |
-
const
|
| 143 |
|
| 144 |
-
const
|
| 145 |
-
const key = p.
|
| 146 |
if (!acc[key]) acc[key] = [];
|
| 147 |
acc[key].push(p);
|
| 148 |
return acc;
|
| 149 |
}, {});
|
| 150 |
|
| 151 |
-
const
|
| 152 |
-
if (!acc[p.
|
| 153 |
-
acc[p.
|
| 154 |
return acc;
|
| 155 |
}, {});
|
| 156 |
|
| 157 |
-
const
|
| 158 |
.filter(([_, prods]) => prods.length > 1)
|
| 159 |
-
.map(([
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
}))
|
| 165 |
-
.sort((a, b) => b.
|
| 166 |
.slice(0, 5);
|
| 167 |
|
| 168 |
-
const
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
}));
|
| 173 |
|
| 174 |
const COLORS = ['#3b82f6', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6'];
|
|
@@ -179,77 +179,77 @@ const HomeTab = () => {
|
|
| 179 |
|
| 180 |
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
| 181 |
<div className="bg-white rounded-lg shadow p-6">
|
| 182 |
-
<h3 className="text-lg font-semibold text-gray-700 mb-2">Total
|
| 183 |
-
<p className="text-3xl font-bold text-blue-600">{
|
| 184 |
</div>
|
| 185 |
|
| 186 |
<div className="bg-white rounded-lg shadow p-6">
|
| 187 |
-
<h3 className="text-lg font-semibold text-gray-700 mb-2">
|
| 188 |
<p className="text-3xl font-bold text-green-600">{products.length}</p>
|
| 189 |
</div>
|
| 190 |
|
| 191 |
<div className="bg-white rounded-lg shadow p-6">
|
| 192 |
-
<h3 className="text-lg font-semibold text-gray-700 mb-2">
|
| 193 |
-
<p className="text-3xl font-bold text-purple-600">{Object.keys(
|
| 194 |
</div>
|
| 195 |
</div>
|
| 196 |
|
| 197 |
-
{
|
| 198 |
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
| 199 |
<div className="bg-white rounded-lg shadow p-6">
|
| 200 |
-
<h3 className="text-lg font-semibold text-gray-700 mb-4">
|
| 201 |
<ResponsiveContainer width="100%" height={300}>
|
| 202 |
-
<BarChart data={
|
| 203 |
<CartesianGrid strokeDasharray="3 3" />
|
| 204 |
-
<XAxis dataKey="
|
| 205 |
<YAxis />
|
| 206 |
-
<Tooltip formatter={(value) => [`${value.toFixed(2)}
|
| 207 |
-
<Bar dataKey="
|
| 208 |
</BarChart>
|
| 209 |
</ResponsiveContainer>
|
| 210 |
</div>
|
| 211 |
|
| 212 |
<div className="bg-white rounded-lg shadow p-6">
|
| 213 |
-
<h3 className="text-lg font-semibold text-gray-700 mb-4">
|
| 214 |
<ResponsiveContainer width="100%" height={300}>
|
| 215 |
<PieChart>
|
| 216 |
<Pie
|
| 217 |
-
data={
|
| 218 |
cx="50%"
|
| 219 |
cy="50%"
|
| 220 |
labelLine={false}
|
| 221 |
-
label={({
|
| 222 |
outerRadius={80}
|
| 223 |
fill="#8884d8"
|
| 224 |
-
dataKey="
|
| 225 |
>
|
| 226 |
-
{
|
| 227 |
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
|
| 228 |
))}
|
| 229 |
</Pie>
|
| 230 |
-
<Tooltip formatter={(value) => [`${value.toFixed(2)}
|
| 231 |
</PieChart>
|
| 232 |
</ResponsiveContainer>
|
| 233 |
</div>
|
| 234 |
</div>
|
| 235 |
)}
|
| 236 |
|
| 237 |
-
{
|
| 238 |
<div className="bg-white rounded-lg shadow p-6">
|
| 239 |
<h3 className="text-lg font-semibold text-gray-700 mb-4 flex items-center">
|
| 240 |
<TrendingUp className="mr-2" />
|
| 241 |
-
|
| 242 |
</h3>
|
| 243 |
<div className="space-y-3">
|
| 244 |
-
{
|
| 245 |
<div key={index} className="flex justify-between items-center p-3 bg-gray-50 rounded">
|
| 246 |
-
<span className="font-medium">{
|
| 247 |
<div className="text-right">
|
| 248 |
<span className="text-sm text-gray-500">
|
| 249 |
-
{
|
| 250 |
</span>
|
| 251 |
<span className="ml-2 text-red-600 font-semibold">
|
| 252 |
-
+{
|
| 253 |
</span>
|
| 254 |
</div>
|
| 255 |
</div>
|
|
@@ -293,7 +293,7 @@ const UploadTab = () => {
|
|
| 293 |
setStep('review');
|
| 294 |
} catch (error) {
|
| 295 |
console.error('Error processing receipt:', error);
|
| 296 |
-
alert('Error
|
| 297 |
} finally {
|
| 298 |
setLoading(false);
|
| 299 |
}
|
|
@@ -305,12 +305,12 @@ const UploadTab = () => {
|
|
| 305 |
setExtractedText('');
|
| 306 |
setProcessedProducts([]);
|
| 307 |
setStep('upload');
|
| 308 |
-
alert('
|
| 309 |
};
|
| 310 |
|
| 311 |
return (
|
| 312 |
<div className="p-6">
|
| 313 |
-
<h2 className="text-3xl font-bold text-gray-800 mb-6">
|
| 314 |
|
| 315 |
<div className="bg-white rounded-lg shadow p-6">
|
| 316 |
{step === 'upload' && (
|
|
@@ -319,7 +319,7 @@ const UploadTab = () => {
|
|
| 319 |
<Upload className="mx-auto h-12 w-12 text-gray-400 mb-4" />
|
| 320 |
<label className="cursor-pointer">
|
| 321 |
<span className="text-lg font-medium text-gray-700">
|
| 322 |
-
|
| 323 |
</span>
|
| 324 |
<input
|
| 325 |
type="file"
|
|
@@ -330,7 +330,7 @@ const UploadTab = () => {
|
|
| 330 |
</label>
|
| 331 |
{file && (
|
| 332 |
<p className="mt-2 text-sm text-gray-500">
|
| 333 |
-
|
| 334 |
</p>
|
| 335 |
)}
|
| 336 |
</div>
|
|
@@ -341,7 +341,7 @@ const UploadTab = () => {
|
|
| 341 |
disabled={loading}
|
| 342 |
className="w-full bg-blue-600 text-white py-3 px-4 rounded-lg hover:bg-blue-700 disabled:opacity-50"
|
| 343 |
>
|
| 344 |
-
{loading ? '
|
| 345 |
</button>
|
| 346 |
)}
|
| 347 |
</div>
|
|
@@ -350,30 +350,30 @@ const UploadTab = () => {
|
|
| 350 |
{step === 'extracting' && (
|
| 351 |
<div className="text-center py-8">
|
| 352 |
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4"></div>
|
| 353 |
-
<p className="text-lg font-medium text-gray-700">
|
| 354 |
</div>
|
| 355 |
)}
|
| 356 |
|
| 357 |
{step === 'processing' && (
|
| 358 |
<div className="text-center py-8">
|
| 359 |
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-green-600 mx-auto mb-4"></div>
|
| 360 |
-
<p className="text-lg font-medium text-gray-700">
|
| 361 |
</div>
|
| 362 |
)}
|
| 363 |
|
| 364 |
{step === 'review' && (
|
| 365 |
<div className="space-y-4">
|
| 366 |
-
<h3 className="text-xl font-semibold text-gray-800">
|
| 367 |
<div className="bg-gray-50 rounded-lg p-4 max-h-96 overflow-y-auto">
|
| 368 |
{processedProducts.map((product, index) => (
|
| 369 |
<div key={index} className="flex justify-between items-center py-2 border-b border-gray-200 last:border-b-0">
|
| 370 |
<div>
|
| 371 |
-
<span className="font-medium">{product.
|
| 372 |
<span className="text-sm text-gray-500 ml-2">
|
| 373 |
-
({product.
|
| 374 |
</span>
|
| 375 |
</div>
|
| 376 |
-
<span className="font-bold text-green-600">{product.
|
| 377 |
</div>
|
| 378 |
))}
|
| 379 |
</div>
|
|
@@ -383,13 +383,13 @@ const UploadTab = () => {
|
|
| 383 |
onClick={handleSaveProducts}
|
| 384 |
className="flex-1 bg-green-600 text-white py-3 px-4 rounded-lg hover:bg-green-700"
|
| 385 |
>
|
| 386 |
-
|
| 387 |
</button>
|
| 388 |
<button
|
| 389 |
onClick={() => setStep('upload')}
|
| 390 |
className="flex-1 bg-gray-600 text-white py-3 px-4 rounded-lg hover:bg-gray-700"
|
| 391 |
>
|
| 392 |
-
|
| 393 |
</button>
|
| 394 |
</div>
|
| 395 |
</div>
|
|
@@ -421,14 +421,14 @@ const ListTab = () => {
|
|
| 421 |
};
|
| 422 |
|
| 423 |
const handleDelete = (id) => {
|
| 424 |
-
if (window.confirm('
|
| 425 |
deleteProduct(id);
|
| 426 |
}
|
| 427 |
};
|
| 428 |
|
| 429 |
return (
|
| 430 |
<div className="p-6">
|
| 431 |
-
<h2 className="text-3xl font-bold text-gray-800 mb-6">
|
| 432 |
|
| 433 |
<div className="bg-white rounded-lg shadow overflow-hidden">
|
| 434 |
<div className="overflow-x-auto">
|
|
@@ -436,19 +436,19 @@ const ListTab = () => {
|
|
| 436 |
<thead className="bg-gray-50">
|
| 437 |
<tr>
|
| 438 |
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
| 439 |
-
|
| 440 |
</th>
|
| 441 |
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
| 442 |
-
|
| 443 |
</th>
|
| 444 |
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
| 445 |
-
|
| 446 |
</th>
|
| 447 |
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
| 448 |
-
|
| 449 |
</th>
|
| 450 |
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
| 451 |
-
|
| 452 |
</th>
|
| 453 |
</tr>
|
| 454 |
</thead>
|
|
@@ -459,12 +459,12 @@ const ListTab = () => {
|
|
| 459 |
{editingId === product.id ? (
|
| 460 |
<input
|
| 461 |
type="text"
|
| 462 |
-
value={editForm.
|
| 463 |
-
onChange={(e) => setEditForm({...editForm,
|
| 464 |
className="w-full px-3 py-1 border border-gray-300 rounded"
|
| 465 |
/>
|
| 466 |
) : (
|
| 467 |
-
<div className="text-sm font-medium text-gray-900">{product.
|
| 468 |
)}
|
| 469 |
</td>
|
| 470 |
<td className="px-6 py-4 whitespace-nowrap">
|
|
@@ -472,28 +472,28 @@ const ListTab = () => {
|
|
| 472 |
<input
|
| 473 |
type="number"
|
| 474 |
step="0.01"
|
| 475 |
-
value={editForm.
|
| 476 |
-
onChange={(e) => setEditForm({...editForm,
|
| 477 |
className="w-full px-3 py-1 border border-gray-300 rounded"
|
| 478 |
/>
|
| 479 |
) : (
|
| 480 |
-
<div className="text-sm text-gray-900">{product.
|
| 481 |
)}
|
| 482 |
</td>
|
| 483 |
<td className="px-6 py-4 whitespace-nowrap">
|
| 484 |
-
<div className="text-sm text-gray-900">{product.
|
| 485 |
-
<div className="text-sm text-gray-500">{product.
|
| 486 |
</td>
|
| 487 |
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
| 488 |
{editingId === product.id ? (
|
| 489 |
<input
|
| 490 |
type="date"
|
| 491 |
-
value={editForm.
|
| 492 |
-
onChange={(e) => setEditForm({...editForm,
|
| 493 |
className="w-full px-3 py-1 border border-gray-300 rounded"
|
| 494 |
/>
|
| 495 |
) : (
|
| 496 |
-
product.
|
| 497 |
)}
|
| 498 |
</td>
|
| 499 |
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium space-x-2">
|
|
@@ -536,7 +536,7 @@ const ListTab = () => {
|
|
| 536 |
|
| 537 |
{products.length === 0 && (
|
| 538 |
<div className="text-center py-8">
|
| 539 |
-
<p className="text-gray-500">No
|
| 540 |
</div>
|
| 541 |
)}
|
| 542 |
</div>
|
|
|
|
| 47 |
|
| 48 |
const mockExtractTextFromImage = async (file) => {
|
| 49 |
await new Promise(resolve => setTimeout(resolve, 2000));
|
| 50 |
+
return `SUPERMARKET WALMART
|
| 51 |
+
Address: 123 Main Street, New York
|
| 52 |
Date: ${new Date().toLocaleDateString()}
|
| 53 |
----------------------------------------
|
| 54 |
+
Whole Milk 1L $2.50
|
| 55 |
+
Whole Wheat Bread 500g $1.80
|
| 56 |
+
Tomatoes 1kg $3.20
|
| 57 |
+
Chicken Fillets 500g $4.50
|
| 58 |
+
Olive Oil 500ml $5.80
|
| 59 |
+
Dozen Eggs $2.90
|
| 60 |
----------------------------------------
|
| 61 |
+
TOTAL: $20.70`;
|
| 62 |
};
|
| 63 |
|
| 64 |
const mockProcessReceiptText = async (text) => {
|
| 65 |
await new Promise(resolve => setTimeout(resolve, 1500));
|
| 66 |
const lines = text.split('\n');
|
| 67 |
const products = [];
|
| 68 |
+
let store = '';
|
| 69 |
+
let address = '';
|
| 70 |
+
let date = '';
|
| 71 |
|
| 72 |
lines.forEach(line => {
|
| 73 |
+
if (line.includes('SUPERMARKET') || line.includes('WALMART') || line.includes('TARGET')) {
|
| 74 |
+
store = line.replace('SUPERMARKET ', '').trim();
|
| 75 |
}
|
| 76 |
if (line.includes('Address:')) {
|
| 77 |
+
address = line.replace('Address:', '').trim();
|
| 78 |
}
|
| 79 |
if (line.includes('Date:')) {
|
| 80 |
+
date = line.replace('Date:', '').trim();
|
| 81 |
}
|
| 82 |
|
| 83 |
+
const priceMatch = line.match(/(.+?)\s+(\$\d+[,.]?\d*)$/);
|
| 84 |
if (priceMatch && !line.includes('TOTAL') && !line.includes('---')) {
|
| 85 |
+
const name = priceMatch[1].trim();
|
| 86 |
+
const price = parseFloat(priceMatch[2].replace('$', '').replace(',', '.'));
|
| 87 |
+
if (name && price > 0) {
|
| 88 |
products.push({
|
| 89 |
+
name,
|
| 90 |
+
price,
|
| 91 |
+
store: { name: store, address },
|
| 92 |
+
date
|
| 93 |
});
|
| 94 |
}
|
| 95 |
}
|
|
|
|
| 139 |
const HomeTab = () => {
|
| 140 |
const { products } = useStore();
|
| 141 |
|
| 142 |
+
const totalSpent = products.reduce((sum, p) => sum + p.price, 0);
|
| 143 |
|
| 144 |
+
const productsByStore = products.reduce((acc, p) => {
|
| 145 |
+
const key = p.store.name;
|
| 146 |
if (!acc[key]) acc[key] = [];
|
| 147 |
acc[key].push(p);
|
| 148 |
return acc;
|
| 149 |
}, {});
|
| 150 |
|
| 151 |
+
const priceVariations = products.reduce((acc, p) => {
|
| 152 |
+
if (!acc[p.name]) acc[p.name] = [];
|
| 153 |
+
acc[p.name].push(p);
|
| 154 |
return acc;
|
| 155 |
}, {});
|
| 156 |
|
| 157 |
+
const productsWithVariation = Object.entries(priceVariations)
|
| 158 |
.filter(([_, prods]) => prods.length > 1)
|
| 159 |
+
.map(([name, prods]) => ({
|
| 160 |
+
name,
|
| 161 |
+
minPrice: Math.min(...prods.map(p => p.price)),
|
| 162 |
+
maxPrice: Math.max(...prods.map(p => p.price)),
|
| 163 |
+
difference: Math.max(...prods.map(p => p.price)) - Math.min(...prods.map(p => p.price))
|
| 164 |
}))
|
| 165 |
+
.sort((a, b) => b.difference - a.difference)
|
| 166 |
.slice(0, 5);
|
| 167 |
|
| 168 |
+
const spendingByStore = Object.entries(productsByStore).map(([name, prods]) => ({
|
| 169 |
+
name,
|
| 170 |
+
spending: prods.reduce((sum, p) => sum + p.price, 0),
|
| 171 |
+
products: prods.length
|
| 172 |
}));
|
| 173 |
|
| 174 |
const COLORS = ['#3b82f6', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6'];
|
|
|
|
| 179 |
|
| 180 |
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
| 181 |
<div className="bg-white rounded-lg shadow p-6">
|
| 182 |
+
<h3 className="text-lg font-semibold text-gray-700 mb-2">Total Spent</h3>
|
| 183 |
+
<p className="text-3xl font-bold text-blue-600">${totalSpent.toFixed(2)}</p>
|
| 184 |
</div>
|
| 185 |
|
| 186 |
<div className="bg-white rounded-lg shadow p-6">
|
| 187 |
+
<h3 className="text-lg font-semibold text-gray-700 mb-2">Registered Products</h3>
|
| 188 |
<p className="text-3xl font-bold text-green-600">{products.length}</p>
|
| 189 |
</div>
|
| 190 |
|
| 191 |
<div className="bg-white rounded-lg shadow p-6">
|
| 192 |
+
<h3 className="text-lg font-semibold text-gray-700 mb-2">Stores</h3>
|
| 193 |
+
<p className="text-3xl font-bold text-purple-600">{Object.keys(productsByStore).length}</p>
|
| 194 |
</div>
|
| 195 |
</div>
|
| 196 |
|
| 197 |
+
{spendingByStore.length > 0 && (
|
| 198 |
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
| 199 |
<div className="bg-white rounded-lg shadow p-6">
|
| 200 |
+
<h3 className="text-lg font-semibold text-gray-700 mb-4">Spending by Store</h3>
|
| 201 |
<ResponsiveContainer width="100%" height={300}>
|
| 202 |
+
<BarChart data={spendingByStore}>
|
| 203 |
<CartesianGrid strokeDasharray="3 3" />
|
| 204 |
+
<XAxis dataKey="name" />
|
| 205 |
<YAxis />
|
| 206 |
+
<Tooltip formatter={(value) => [`$${value.toFixed(2)}`, 'Spending']} />
|
| 207 |
+
<Bar dataKey="spending" fill="#3b82f6" />
|
| 208 |
</BarChart>
|
| 209 |
</ResponsiveContainer>
|
| 210 |
</div>
|
| 211 |
|
| 212 |
<div className="bg-white rounded-lg shadow p-6">
|
| 213 |
+
<h3 className="text-lg font-semibold text-gray-700 mb-4">Spending Distribution</h3>
|
| 214 |
<ResponsiveContainer width="100%" height={300}>
|
| 215 |
<PieChart>
|
| 216 |
<Pie
|
| 217 |
+
data={spendingByStore}
|
| 218 |
cx="50%"
|
| 219 |
cy="50%"
|
| 220 |
labelLine={false}
|
| 221 |
+
label={({ name, percent }) => `${name} ${(percent * 100).toFixed(0)}%`}
|
| 222 |
outerRadius={80}
|
| 223 |
fill="#8884d8"
|
| 224 |
+
dataKey="spending"
|
| 225 |
>
|
| 226 |
+
{spendingByStore.map((entry, index) => (
|
| 227 |
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
|
| 228 |
))}
|
| 229 |
</Pie>
|
| 230 |
+
<Tooltip formatter={(value) => [`$${value.toFixed(2)}`, 'Spending']} />
|
| 231 |
</PieChart>
|
| 232 |
</ResponsiveContainer>
|
| 233 |
</div>
|
| 234 |
</div>
|
| 235 |
)}
|
| 236 |
|
| 237 |
+
{productsWithVariation.length > 0 && (
|
| 238 |
<div className="bg-white rounded-lg shadow p-6">
|
| 239 |
<h3 className="text-lg font-semibold text-gray-700 mb-4 flex items-center">
|
| 240 |
<TrendingUp className="mr-2" />
|
| 241 |
+
Products with Highest Price Variation
|
| 242 |
</h3>
|
| 243 |
<div className="space-y-3">
|
| 244 |
+
{productsWithVariation.map((product, index) => (
|
| 245 |
<div key={index} className="flex justify-between items-center p-3 bg-gray-50 rounded">
|
| 246 |
+
<span className="font-medium">{product.name}</span>
|
| 247 |
<div className="text-right">
|
| 248 |
<span className="text-sm text-gray-500">
|
| 249 |
+
${product.minPrice.toFixed(2)} - ${product.maxPrice.toFixed(2)}
|
| 250 |
</span>
|
| 251 |
<span className="ml-2 text-red-600 font-semibold">
|
| 252 |
+
+${product.difference.toFixed(2)}
|
| 253 |
</span>
|
| 254 |
</div>
|
| 255 |
</div>
|
|
|
|
| 293 |
setStep('review');
|
| 294 |
} catch (error) {
|
| 295 |
console.error('Error processing receipt:', error);
|
| 296 |
+
alert('Error processing receipt');
|
| 297 |
} finally {
|
| 298 |
setLoading(false);
|
| 299 |
}
|
|
|
|
| 305 |
setExtractedText('');
|
| 306 |
setProcessedProducts([]);
|
| 307 |
setStep('upload');
|
| 308 |
+
alert('Products saved successfully');
|
| 309 |
};
|
| 310 |
|
| 311 |
return (
|
| 312 |
<div className="p-6">
|
| 313 |
+
<h2 className="text-3xl font-bold text-gray-800 mb-6">Upload Receipt</h2>
|
| 314 |
|
| 315 |
<div className="bg-white rounded-lg shadow p-6">
|
| 316 |
{step === 'upload' && (
|
|
|
|
| 319 |
<Upload className="mx-auto h-12 w-12 text-gray-400 mb-4" />
|
| 320 |
<label className="cursor-pointer">
|
| 321 |
<span className="text-lg font-medium text-gray-700">
|
| 322 |
+
Select a receipt image
|
| 323 |
</span>
|
| 324 |
<input
|
| 325 |
type="file"
|
|
|
|
| 330 |
</label>
|
| 331 |
{file && (
|
| 332 |
<p className="mt-2 text-sm text-gray-500">
|
| 333 |
+
Selected file: {file.name}
|
| 334 |
</p>
|
| 335 |
)}
|
| 336 |
</div>
|
|
|
|
| 341 |
disabled={loading}
|
| 342 |
className="w-full bg-blue-600 text-white py-3 px-4 rounded-lg hover:bg-blue-700 disabled:opacity-50"
|
| 343 |
>
|
| 344 |
+
{loading ? 'Processing...' : 'Process Receipt'}
|
| 345 |
</button>
|
| 346 |
)}
|
| 347 |
</div>
|
|
|
|
| 350 |
{step === 'extracting' && (
|
| 351 |
<div className="text-center py-8">
|
| 352 |
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4"></div>
|
| 353 |
+
<p className="text-lg font-medium text-gray-700">Extracting text from image...</p>
|
| 354 |
</div>
|
| 355 |
)}
|
| 356 |
|
| 357 |
{step === 'processing' && (
|
| 358 |
<div className="text-center py-8">
|
| 359 |
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-green-600 mx-auto mb-4"></div>
|
| 360 |
+
<p className="text-lg font-medium text-gray-700">Processing products...</p>
|
| 361 |
</div>
|
| 362 |
)}
|
| 363 |
|
| 364 |
{step === 'review' && (
|
| 365 |
<div className="space-y-4">
|
| 366 |
+
<h3 className="text-xl font-semibold text-gray-800">Detected Products</h3>
|
| 367 |
<div className="bg-gray-50 rounded-lg p-4 max-h-96 overflow-y-auto">
|
| 368 |
{processedProducts.map((product, index) => (
|
| 369 |
<div key={index} className="flex justify-between items-center py-2 border-b border-gray-200 last:border-b-0">
|
| 370 |
<div>
|
| 371 |
+
<span className="font-medium">{product.name}</span>
|
| 372 |
<span className="text-sm text-gray-500 ml-2">
|
| 373 |
+
({product.store.name})
|
| 374 |
</span>
|
| 375 |
</div>
|
| 376 |
+
<span className="font-bold text-green-600">${product.price.toFixed(2)}</span>
|
| 377 |
</div>
|
| 378 |
))}
|
| 379 |
</div>
|
|
|
|
| 383 |
onClick={handleSaveProducts}
|
| 384 |
className="flex-1 bg-green-600 text-white py-3 px-4 rounded-lg hover:bg-green-700"
|
| 385 |
>
|
| 386 |
+
Save Products
|
| 387 |
</button>
|
| 388 |
<button
|
| 389 |
onClick={() => setStep('upload')}
|
| 390 |
className="flex-1 bg-gray-600 text-white py-3 px-4 rounded-lg hover:bg-gray-700"
|
| 391 |
>
|
| 392 |
+
Cancel
|
| 393 |
</button>
|
| 394 |
</div>
|
| 395 |
</div>
|
|
|
|
| 421 |
};
|
| 422 |
|
| 423 |
const handleDelete = (id) => {
|
| 424 |
+
if (window.confirm('Are you sure you want to delete this product?')) {
|
| 425 |
deleteProduct(id);
|
| 426 |
}
|
| 427 |
};
|
| 428 |
|
| 429 |
return (
|
| 430 |
<div className="p-6">
|
| 431 |
+
<h2 className="text-3xl font-bold text-gray-800 mb-6">Product List</h2>
|
| 432 |
|
| 433 |
<div className="bg-white rounded-lg shadow overflow-hidden">
|
| 434 |
<div className="overflow-x-auto">
|
|
|
|
| 436 |
<thead className="bg-gray-50">
|
| 437 |
<tr>
|
| 438 |
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
| 439 |
+
Product
|
| 440 |
</th>
|
| 441 |
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
| 442 |
+
Price
|
| 443 |
</th>
|
| 444 |
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
| 445 |
+
Store
|
| 446 |
</th>
|
| 447 |
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
| 448 |
+
Date
|
| 449 |
</th>
|
| 450 |
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
| 451 |
+
Actions
|
| 452 |
</th>
|
| 453 |
</tr>
|
| 454 |
</thead>
|
|
|
|
| 459 |
{editingId === product.id ? (
|
| 460 |
<input
|
| 461 |
type="text"
|
| 462 |
+
value={editForm.name}
|
| 463 |
+
onChange={(e) => setEditForm({...editForm, name: e.target.value})}
|
| 464 |
className="w-full px-3 py-1 border border-gray-300 rounded"
|
| 465 |
/>
|
| 466 |
) : (
|
| 467 |
+
<div className="text-sm font-medium text-gray-900">{product.name}</div>
|
| 468 |
)}
|
| 469 |
</td>
|
| 470 |
<td className="px-6 py-4 whitespace-nowrap">
|
|
|
|
| 472 |
<input
|
| 473 |
type="number"
|
| 474 |
step="0.01"
|
| 475 |
+
value={editForm.price}
|
| 476 |
+
onChange={(e) => setEditForm({...editForm, price: parseFloat(e.target.value)})}
|
| 477 |
className="w-full px-3 py-1 border border-gray-300 rounded"
|
| 478 |
/>
|
| 479 |
) : (
|
| 480 |
+
<div className="text-sm text-gray-900">${product.price.toFixed(2)}</div>
|
| 481 |
)}
|
| 482 |
</td>
|
| 483 |
<td className="px-6 py-4 whitespace-nowrap">
|
| 484 |
+
<div className="text-sm text-gray-900">{product.store.name}</div>
|
| 485 |
+
<div className="text-sm text-gray-500">{product.store.address}</div>
|
| 486 |
</td>
|
| 487 |
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
| 488 |
{editingId === product.id ? (
|
| 489 |
<input
|
| 490 |
type="date"
|
| 491 |
+
value={editForm.date}
|
| 492 |
+
onChange={(e) => setEditForm({...editForm, date: e.target.value})}
|
| 493 |
className="w-full px-3 py-1 border border-gray-300 rounded"
|
| 494 |
/>
|
| 495 |
) : (
|
| 496 |
+
product.date
|
| 497 |
)}
|
| 498 |
</td>
|
| 499 |
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium space-x-2">
|
|
|
|
| 536 |
|
| 537 |
{products.length === 0 && (
|
| 538 |
<div className="text-center py-8">
|
| 539 |
+
<p className="text-gray-500">No products registered</p>
|
| 540 |
</div>
|
| 541 |
)}
|
| 542 |
</div>
|