mirror of
https://github.com/R0m1k3/Priceflow.git
synced 2026-10-19 05:09:17 +02:00
230 lines
9.1 KiB
React
230 lines
9.1 KiB
React
import React, { useState, useEffect } from 'react';
|
|
import axios from 'axios';
|
|
import { toast } from 'sonner';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { Plus, RefreshCw, Search, X, Filter } from 'lucide-react';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Input } from '@/components/ui/input';
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
|
import { ItemCard } from '@/components/dashboard/ItemCard';
|
|
import { ItemModal } from '@/components/dashboard/ItemModal';
|
|
import { DeleteConfirmationModal } from '@/components/dashboard/DeleteConfirmationModal';
|
|
import { PriceHistoryModal } from '@/components/dashboard/PriceHistoryModal';
|
|
|
|
const API_URL = '/api';
|
|
|
|
export default function Dashboard() {
|
|
const { t } = useTranslation();
|
|
const [items, setItems] = useState([]);
|
|
const [searchTerm, setSearchTerm] = useState('');
|
|
const [selectedCategory, setSelectedCategory] = useState('all');
|
|
const [categories, setCategories] = useState([]);
|
|
const [showAddModal, setShowAddModal] = useState(false);
|
|
const [editingItem, setEditingItem] = useState(null);
|
|
const [itemToDelete, setItemToDelete] = useState(null);
|
|
const [historyItem, setHistoryItem] = useState(null);
|
|
const [zoomedImage, setZoomedImage] = useState(null);
|
|
|
|
const refreshItems = async () => {
|
|
try {
|
|
const response = await axios.get(`${API_URL}/items`);
|
|
const sortedItems = response.data.sort((a, b) => a.id - b.id);
|
|
setItems(sortedItems);
|
|
} catch (error) {
|
|
console.error('Error fetching items:', error);
|
|
toast.error(t('toast.fetchError'));
|
|
}
|
|
};
|
|
|
|
const loadCategories = async () => {
|
|
try {
|
|
const response = await axios.get(`${API_URL}/items/categories/list`);
|
|
setCategories(response.data);
|
|
} catch (error) {
|
|
console.error('Error fetching categories:', error);
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
refreshItems();
|
|
loadCategories();
|
|
const interval = setInterval(refreshItems, 10000);
|
|
return () => clearInterval(interval);
|
|
}, []);
|
|
|
|
const handleCheck = async (id) => {
|
|
try {
|
|
toast.info(t('toast.refreshStarted'));
|
|
await axios.post(`${API_URL}/items/${id}/check`);
|
|
toast.success(t('toast.refreshComplete'));
|
|
refreshItems();
|
|
} catch (error) {
|
|
console.error('Error triggering check:', error);
|
|
toast.error(t('toast.refreshError'));
|
|
}
|
|
};
|
|
|
|
const handleRefreshAll = async () => {
|
|
try {
|
|
toast.info(t('toast.refreshStarted'));
|
|
setItems(prevItems => prevItems.map(item => ({ ...item, is_refreshing: true })));
|
|
await axios.post(`${API_URL}/jobs/refresh-all`);
|
|
toast.success(t('toast.refreshComplete'));
|
|
} catch (error) {
|
|
console.error('Error triggering refresh all:', error);
|
|
toast.error(t('toast.refreshError'));
|
|
refreshItems();
|
|
}
|
|
};
|
|
|
|
const handleDelete = async () => {
|
|
if (!itemToDelete) return;
|
|
try {
|
|
await axios.delete(`${API_URL}/items/${itemToDelete.id}`);
|
|
toast.success(t('toast.itemDeleted'));
|
|
refreshItems();
|
|
setItemToDelete(null);
|
|
} catch (error) {
|
|
console.error('Error deleting item:', error);
|
|
toast.error(t('toast.itemError'));
|
|
}
|
|
};
|
|
|
|
const handleCategoryUpdate = async (itemId, category) => {
|
|
try {
|
|
await axios.patch(`${API_URL}/items/${itemId}/category?category=${encodeURIComponent(category || '')}`);
|
|
toast.success('Catégorie mise à jour');
|
|
refreshItems();
|
|
loadCategories();
|
|
} catch (error) {
|
|
console.error('Error updating category:', error);
|
|
toast.error('Erreur lors de la mise à jour');
|
|
}
|
|
};
|
|
|
|
const filteredItems = items.filter(item => {
|
|
const term = searchTerm.toLowerCase();
|
|
const matchesSearch = (
|
|
item.name.toLowerCase().includes(term) ||
|
|
item.url.toLowerCase().includes(term) ||
|
|
(item.tags && item.tags.toLowerCase().includes(term)) ||
|
|
(item.description && item.description.toLowerCase().includes(term))
|
|
);
|
|
|
|
const matchesCategory = selectedCategory === 'all' ||
|
|
(selectedCategory === 'uncategorized' && !item.category) ||
|
|
item.category === selectedCategory;
|
|
|
|
return matchesSearch && matchesCategory;
|
|
});
|
|
|
|
return (
|
|
<div className="space-y-6 animate-in fade-in duration-500">
|
|
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
|
|
<div>
|
|
<h2 className="text-2xl font-bold tracking-tight">{t('dashboard.title')}</h2>
|
|
<p className="text-muted-foreground">{t('dashboard.description')}</p>
|
|
</div>
|
|
<div className="flex flex-wrap items-center gap-2">
|
|
<div className="relative w-full md:w-64">
|
|
<Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
|
|
<Input
|
|
placeholder={t('dashboard.search')}
|
|
className="pl-8"
|
|
value={searchTerm}
|
|
onChange={(e) => setSearchTerm(e.target.value)}
|
|
/>
|
|
</div>
|
|
<Select value={selectedCategory} onValueChange={setSelectedCategory}>
|
|
<SelectTrigger className="w-full md:w-[180px]">
|
|
<Filter className="mr-2 h-4 w-4" />
|
|
<SelectValue placeholder="Catégorie" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="all">Toutes</SelectItem>
|
|
<SelectItem value="uncategorized">Sans catégorie</SelectItem>
|
|
{categories.map(cat => (
|
|
<SelectItem key={cat} value={cat}>{cat}</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
<Button variant="outline" onClick={handleRefreshAll}>
|
|
<RefreshCw className="mr-2 h-4 w-4" />
|
|
{t('dashboard.refreshAll')}
|
|
</Button>
|
|
<Button onClick={() => setShowAddModal(true)}>
|
|
<Plus className="mr-2 h-4 w-4" />
|
|
{t('dashboard.addItem')}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
|
{filteredItems.map(item => (
|
|
<ItemCard
|
|
key={item.id}
|
|
item={item}
|
|
onEdit={setEditingItem}
|
|
onDelete={setItemToDelete}
|
|
onCheck={handleCheck}
|
|
onZoom={setZoomedImage}
|
|
onCategoryUpdate={handleCategoryUpdate}
|
|
onViewHistory={setHistoryItem}
|
|
categories={categories}
|
|
/>
|
|
))}
|
|
</div>
|
|
|
|
<ItemModal
|
|
open={showAddModal}
|
|
onClose={() => setShowAddModal(false)}
|
|
onSaved={refreshItems}
|
|
/>
|
|
|
|
<ItemModal
|
|
open={!!editingItem}
|
|
item={editingItem}
|
|
onClose={() => setEditingItem(null)}
|
|
onSaved={refreshItems}
|
|
/>
|
|
|
|
<DeleteConfirmationModal
|
|
open={!!itemToDelete}
|
|
item={itemToDelete}
|
|
onClose={() => setItemToDelete(null)}
|
|
onConfirm={handleDelete}
|
|
/>
|
|
|
|
<PriceHistoryModal
|
|
open={!!historyItem}
|
|
item={historyItem}
|
|
onClose={() => setHistoryItem(null)}
|
|
/>
|
|
|
|
{/* Image Zoom Modal */}
|
|
{zoomedImage && (
|
|
<div
|
|
className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm p-4 animate-in fade-in duration-200"
|
|
onClick={() => setZoomedImage(null)}
|
|
>
|
|
<div className="relative max-h-full max-w-full">
|
|
<img
|
|
src={zoomedImage}
|
|
alt="Zoomed screenshot"
|
|
className="max-h-[90vh] max-w-[90vw] rounded-lg shadow-2xl"
|
|
/>
|
|
<Button
|
|
size="icon"
|
|
variant="secondary"
|
|
className="absolute -right-4 -top-4 rounded-full shadow-lg"
|
|
onClick={() => setZoomedImage(null)}
|
|
>
|
|
<X className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|