376 lines
14 KiB
TypeScript
376 lines
14 KiB
TypeScript
import { useEffect, useState } from 'react';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { shoppingItemApi } from '../api/client';
|
|
import type { ShoppingItem } from '../types';
|
|
import {
|
|
X,
|
|
Plus,
|
|
Trash2,
|
|
ShoppingCart,
|
|
Check,
|
|
Loader2,
|
|
Pencil,
|
|
} from 'lucide-react';
|
|
import ConfirmModal from './ConfirmModal';
|
|
|
|
interface ShoppingListModalProps {
|
|
familyId: number;
|
|
onClose: () => void;
|
|
}
|
|
|
|
type ConfirmAction =
|
|
| { type: 'delete-item'; itemId: number }
|
|
| { type: 'mark-all' }
|
|
| { type: 'clear-all' };
|
|
|
|
export default function ShoppingListModal({ familyId, onClose }: ShoppingListModalProps) {
|
|
const { t } = useTranslation();
|
|
const [items, setItems] = useState<ShoppingItem[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [newItemName, setNewItemName] = useState('');
|
|
const [editingId, setEditingId] = useState<number | null>(null);
|
|
const [editingName, setEditingName] = useState('');
|
|
const [confirmAction, setConfirmAction] = useState<ConfirmAction | null>(null);
|
|
|
|
useEffect(() => {
|
|
loadItems();
|
|
}, [familyId]);
|
|
|
|
const loadItems = async () => {
|
|
try {
|
|
setLoading(true);
|
|
const response = await shoppingItemApi.getAllByFamily(familyId);
|
|
setItems(response.data);
|
|
} catch (err) {
|
|
console.error('Error loading shopping items:', err);
|
|
alert(t('shopping.loadError'));
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleAddItem = async () => {
|
|
if (!newItemName.trim()) return;
|
|
|
|
try {
|
|
await shoppingItemApi.create(familyId, { name: newItemName });
|
|
setNewItemName('');
|
|
loadItems();
|
|
} catch (err) {
|
|
console.error('Error adding item:', err);
|
|
alert(t('shopping.addError'));
|
|
}
|
|
};
|
|
|
|
const handleTogglePurchased = async (itemId: number, currentStatus: boolean) => {
|
|
try {
|
|
await shoppingItemApi.markAsPurchased(familyId, itemId, { is_purchased: !currentStatus });
|
|
loadItems();
|
|
} catch (err) {
|
|
console.error('Error toggling purchased status:', err);
|
|
alert(t('shopping.toggleError'));
|
|
}
|
|
};
|
|
|
|
const handleDeleteItem = (itemId: number) => {
|
|
setConfirmAction({ type: 'delete-item', itemId });
|
|
};
|
|
|
|
const executeDeleteItem = async (itemId: number) => {
|
|
try {
|
|
await shoppingItemApi.delete(familyId, itemId);
|
|
loadItems();
|
|
} catch (err) {
|
|
console.error('Error deleting item:', err);
|
|
alert(t('shopping.deleteError'));
|
|
}
|
|
};
|
|
|
|
const handleStartEdit = (item: ShoppingItem) => {
|
|
setEditingId(item.id);
|
|
setEditingName(item.name);
|
|
};
|
|
|
|
const handleSaveEdit = async (itemId: number) => {
|
|
if (!editingName.trim()) return;
|
|
|
|
try {
|
|
await shoppingItemApi.update(familyId, itemId, { name: editingName });
|
|
setEditingId(null);
|
|
setEditingName('');
|
|
loadItems();
|
|
} catch (err) {
|
|
console.error('Error updating item:', err);
|
|
alert(t('shopping.updateError'));
|
|
}
|
|
};
|
|
|
|
const handleCancelEdit = () => {
|
|
setEditingId(null);
|
|
setEditingName('');
|
|
};
|
|
|
|
const handleMarkAllPurchased = () => {
|
|
setConfirmAction({ type: 'mark-all' });
|
|
};
|
|
|
|
const executeMarkAllPurchased = async () => {
|
|
try {
|
|
await shoppingItemApi.markAllAsPurchased(familyId);
|
|
loadItems();
|
|
} catch (err) {
|
|
console.error('Error marking all as purchased:', err);
|
|
alert(t('shopping.markAllError'));
|
|
}
|
|
};
|
|
|
|
const handleClearAll = () => {
|
|
setConfirmAction({ type: 'clear-all' });
|
|
};
|
|
|
|
const executeClearAll = async () => {
|
|
try {
|
|
await shoppingItemApi.clearAll(familyId);
|
|
loadItems();
|
|
} catch (err) {
|
|
console.error('Error clearing all items:', err);
|
|
alert(t('shopping.clearError'));
|
|
}
|
|
};
|
|
|
|
const handleConfirm = () => {
|
|
if (!confirmAction) return;
|
|
|
|
switch (confirmAction.type) {
|
|
case 'delete-item':
|
|
executeDeleteItem(confirmAction.itemId);
|
|
break;
|
|
case 'mark-all':
|
|
executeMarkAllPurchased();
|
|
break;
|
|
case 'clear-all':
|
|
executeClearAll();
|
|
break;
|
|
}
|
|
|
|
setConfirmAction(null);
|
|
};
|
|
|
|
const getConfirmModalContent = () => {
|
|
if (!confirmAction) return null;
|
|
|
|
switch (confirmAction.type) {
|
|
case 'delete-item':
|
|
return {
|
|
title: t('confirm.deleteItem'),
|
|
message: t('confirm.deleteItemMessage'),
|
|
confirmText: t('common.delete'),
|
|
};
|
|
case 'mark-all':
|
|
return {
|
|
title: t('confirm.markAll'),
|
|
message: t('confirm.markAllMessage'),
|
|
confirmText: t('confirm.markButton'),
|
|
variant: 'info' as const,
|
|
};
|
|
case 'clear-all':
|
|
return {
|
|
title: t('confirm.clearAll'),
|
|
message: t('confirm.clearAllMessage'),
|
|
confirmText: t('shopping.clear'),
|
|
};
|
|
}
|
|
};
|
|
|
|
const unpurchasedItems = items.filter(item => !item.is_purchased);
|
|
const purchasedItems = items.filter(item => item.is_purchased);
|
|
const confirmContent = getConfirmModalContent();
|
|
|
|
return (
|
|
<>
|
|
<div className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-50 p-4">
|
|
<div className="bg-white rounded-3xl shadow-2xl max-w-2xl w-full max-h-[90vh] overflow-hidden flex flex-col">
|
|
<div className="bg-gradient-to-r from-green-500 to-emerald-600 p-6 flex items-center justify-between">
|
|
<div className="flex items-center gap-3">
|
|
<div className="p-3 bg-white/20 backdrop-blur-md rounded-2xl">
|
|
<ShoppingCart className="w-8 h-8 text-white" />
|
|
</div>
|
|
<h2 className="text-3xl font-bold text-white">{t('shopping.title')}</h2>
|
|
</div>
|
|
<button
|
|
onClick={onClose}
|
|
className="p-2 hover:bg-white/20 rounded-xl transition-all"
|
|
>
|
|
<X className="w-6 h-6 text-white" />
|
|
</button>
|
|
</div>
|
|
|
|
<div className="p-6 flex-1 overflow-y-auto">
|
|
<div className="mb-6">
|
|
<div className="flex gap-2">
|
|
<input
|
|
type="text"
|
|
placeholder={t('shopping.addPlaceholder')}
|
|
value={newItemName}
|
|
onChange={(e) => setNewItemName(e.target.value)}
|
|
onKeyPress={(e) => e.key === 'Enter' && handleAddItem()}
|
|
className="flex-1 px-4 py-3 border-2 border-gray-300 rounded-2xl focus:border-green-500 focus:ring-2 focus:ring-green-200 transition-all"
|
|
/>
|
|
<button
|
|
onClick={handleAddItem}
|
|
className="px-6 py-3 bg-gradient-to-r from-green-500 to-green-600 text-white rounded-2xl hover:shadow-lg transition-all font-semibold flex items-center gap-2"
|
|
>
|
|
<Plus className="w-5 h-5" />
|
|
<span className="hidden sm:inline">{t('common.add')}</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{loading ? (
|
|
<div className="flex items-center justify-center py-12">
|
|
<Loader2 className="w-8 h-8 animate-spin text-green-500" />
|
|
</div>
|
|
) : (
|
|
<div className="space-y-6">
|
|
{unpurchasedItems.length > 0 && (
|
|
<div>
|
|
<h3 className="text-lg font-semibold text-gray-700 mb-3">{t('shopping.toBuy')}</h3>
|
|
<div className="space-y-2">
|
|
{unpurchasedItems.map((item) => (
|
|
<div
|
|
key={item.id}
|
|
className="bg-gray-50 p-4 rounded-2xl border-2 border-gray-200 hover:border-green-300 transition-all"
|
|
>
|
|
{editingId === item.id ? (
|
|
<div className="flex gap-2">
|
|
<input
|
|
type="text"
|
|
value={editingName}
|
|
onChange={(e) => setEditingName(e.target.value)}
|
|
onKeyPress={(e) => e.key === 'Enter' && handleSaveEdit(item.id)}
|
|
className="flex-1 px-3 py-2 border-2 border-green-300 rounded-xl focus:border-green-500 focus:ring-2 focus:ring-green-200"
|
|
autoFocus
|
|
/>
|
|
<button
|
|
onClick={() => handleSaveEdit(item.id)}
|
|
className="px-4 py-2 bg-green-500 text-white rounded-xl hover:bg-green-600 transition-all"
|
|
>
|
|
<Check className="w-5 h-5" />
|
|
</button>
|
|
<button
|
|
onClick={handleCancelEdit}
|
|
className="px-4 py-2 bg-gray-300 text-gray-700 rounded-xl hover:bg-gray-400 transition-all"
|
|
>
|
|
<X className="w-5 h-5" />
|
|
</button>
|
|
</div>
|
|
) : (
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-3">
|
|
<button
|
|
onClick={() => handleTogglePurchased(item.id, item.is_purchased)}
|
|
className="w-6 h-6 border-2 border-gray-400 rounded-lg hover:border-green-500 transition-all"
|
|
/>
|
|
<span className="text-gray-800 font-medium">{item.name}</span>
|
|
</div>
|
|
<div className="flex gap-2">
|
|
<button
|
|
onClick={() => handleStartEdit(item)}
|
|
className="p-2 hover:bg-gray-200 rounded-xl transition-all"
|
|
>
|
|
<Pencil className="w-4 h-4 text-gray-600" />
|
|
</button>
|
|
<button
|
|
onClick={() => handleDeleteItem(item.id)}
|
|
className="p-2 hover:bg-red-100 rounded-xl transition-all"
|
|
>
|
|
<Trash2 className="w-4 h-4 text-red-500" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{purchasedItems.length > 0 && (
|
|
<div>
|
|
<h3 className="text-lg font-semibold text-gray-700 mb-3">{t('shopping.purchased')}</h3>
|
|
<div className="space-y-2">
|
|
{purchasedItems.map((item) => (
|
|
<div
|
|
key={item.id}
|
|
className="bg-green-50 p-4 rounded-2xl border-2 border-green-200"
|
|
>
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-3">
|
|
<button
|
|
onClick={() => handleTogglePurchased(item.id, item.is_purchased)}
|
|
className="w-6 h-6 bg-green-500 border-2 border-green-500 rounded-lg flex items-center justify-center hover:bg-green-600 transition-all"
|
|
>
|
|
<Check className="w-4 h-4 text-white" />
|
|
</button>
|
|
<span className="text-gray-500 line-through">{item.name}</span>
|
|
</div>
|
|
<button
|
|
onClick={() => handleDeleteItem(item.id)}
|
|
className="p-2 hover:bg-red-100 rounded-xl transition-all"
|
|
>
|
|
<Trash2 className="w-4 h-4 text-red-500" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{items.length === 0 && (
|
|
<div className="text-center py-12 text-gray-500">
|
|
<ShoppingCart className="w-16 h-16 mx-auto mb-4 opacity-30" />
|
|
<p className="text-lg">{t('shopping.empty')}</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{items.length > 0 && (
|
|
<div className="p-6 border-t-2 border-gray-200 bg-gray-50">
|
|
<div className="flex gap-3">
|
|
<button
|
|
onClick={handleMarkAllPurchased}
|
|
className="flex-1 flex items-center justify-center gap-2 px-4 py-3 bg-gradient-to-r from-green-500 to-green-600 text-white rounded-2xl hover:shadow-lg transition-all font-semibold"
|
|
>
|
|
<Check className="w-5 h-5" />
|
|
{t('shopping.allPurchased')}
|
|
</button>
|
|
<button
|
|
onClick={handleClearAll}
|
|
className="flex-1 flex items-center justify-center gap-2 px-4 py-3 bg-gradient-to-r from-red-500 to-red-600 text-white rounded-2xl hover:shadow-lg transition-all font-semibold"
|
|
>
|
|
<Trash2 className="w-5 h-5" />
|
|
{t('shopping.clear')}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{confirmContent && (
|
|
<ConfirmModal
|
|
title={confirmContent.title}
|
|
message={confirmContent.message}
|
|
confirmText={confirmContent.confirmText}
|
|
onConfirm={handleConfirm}
|
|
onCancel={() => setConfirmAction(null)}
|
|
variant={confirmContent.variant || 'danger'}
|
|
/>
|
|
)}
|
|
</>
|
|
);
|
|
}
|