finito spero
This commit is contained in:
		
							parent
							
								
									3ea07942c6
								
							
						
					
					
						commit
						54d4faca9d
					
				
					 12 changed files with 7315 additions and 70 deletions
				
			
		|  | @ -29,13 +29,13 @@ export const AddItemDialog = ({ open, onOpenChange, tags, onItemAdded }: AddItem | |||
|   const [description, setDescription] = useState(''); | ||||
|   const [selectedTagIds, setSelectedTagIds] = useState<string[]>([]); | ||||
|   const [isLoading, setIsLoading] = useState(false); | ||||
|    | ||||
| 
 | ||||
|   const { user } = useAuth(); | ||||
|   const { toast } = useToast(); | ||||
| 
 | ||||
|   const handleSubmit = async (e: React.FormEvent) => { | ||||
|     e.preventDefault(); | ||||
|      | ||||
| 
 | ||||
|     if (!name.trim()) { | ||||
|       toast({ | ||||
|         title: "Errore", | ||||
|  | @ -46,16 +46,17 @@ export const AddItemDialog = ({ open, onOpenChange, tags, onItemAdded }: AddItem | |||
|     } | ||||
| 
 | ||||
|     setIsLoading(true); | ||||
|      | ||||
| 
 | ||||
|     try { | ||||
|       const newItem = await inventoryService.createItem({ | ||||
|         name: name.trim(), | ||||
|         description: description.trim(), | ||||
|         quantity: 1, // Default quantity set to 1
 | ||||
|         tagIds: selectedTagIds | ||||
|       }); | ||||
|        | ||||
| 
 | ||||
|       onItemAdded(newItem); | ||||
|        | ||||
| 
 | ||||
|       // Reset form
 | ||||
|       setName(''); | ||||
|       setDescription(''); | ||||
|  | @ -88,7 +89,7 @@ export const AddItemDialog = ({ open, onOpenChange, tags, onItemAdded }: AddItem | |||
|             Inserisci i dettagli del nuovo oggetto per l'inventario | ||||
|           </DialogDescription> | ||||
|         </DialogHeader> | ||||
|          | ||||
| 
 | ||||
|         <form onSubmit={handleSubmit} className="space-y-4"> | ||||
|           <div className="space-y-2"> | ||||
|             <label className="text-sm font-medium text-gray-700">Nome *</label> | ||||
|  | @ -99,7 +100,7 @@ export const AddItemDialog = ({ open, onOpenChange, tags, onItemAdded }: AddItem | |||
|               required | ||||
|             /> | ||||
|           </div> | ||||
|            | ||||
| 
 | ||||
|           <div className="space-y-2"> | ||||
|             <label className="text-sm font-medium text-gray-700">Descrizione</label> | ||||
|             <Textarea | ||||
|  | @ -109,7 +110,7 @@ export const AddItemDialog = ({ open, onOpenChange, tags, onItemAdded }: AddItem | |||
|               rows={3} | ||||
|             /> | ||||
|           </div> | ||||
|            | ||||
| 
 | ||||
|           <div className="space-y-2"> | ||||
|             <label className="text-sm font-medium text-gray-700">Tag</label> | ||||
|             <div className="flex flex-wrap gap-2 p-3 border rounded-md bg-gray-50 min-h-[60px]"> | ||||
|  | @ -117,17 +118,17 @@ export const AddItemDialog = ({ open, onOpenChange, tags, onItemAdded }: AddItem | |||
|                 <Badge | ||||
|                   key={tag._id} | ||||
|                   variant="outline" | ||||
|                   className={`cursor-pointer transition-all border`} | ||||
|                   style={{ | ||||
|                     backgroundColor: selectedTagIds.includes(tag._id) ? '#3b82f6' : tag.color, | ||||
|                     color: '#fff', | ||||
|                   }} | ||||
|                   className={`cursor-pointer transition-all ${selectedTagIds.includes(tag._id) | ||||
|                       ? 'bg-blue-100 border-blue-300' | ||||
|                       : 'hover:bg-gray-100' | ||||
|                     }`}
 | ||||
|                   onClick={() => toggleTag(tag._id)} | ||||
|                 > | ||||
|                   <div | ||||
|                     className="w-3 h-3 rounded-full mr-2" | ||||
|                     style={{ backgroundColor: tag.color }} | ||||
|                   /> | ||||
|                   {tag.name} | ||||
|                   {selectedTagIds.includes(tag._id) && ( | ||||
|                     <span className="ml-1 font-bold">✓</span> | ||||
|                   )} | ||||
|                 </Badge> | ||||
|               ))} | ||||
|             </div> | ||||
|  |  | |||
							
								
								
									
										171
									
								
								frontend/src/components/EditItemDialog.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										171
									
								
								frontend/src/components/EditItemDialog.tsx
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,171 @@ | |||
| 
 | ||||
| import { useState, useEffect } from 'react'; | ||||
| import { Button } from '@/components/ui/button'; | ||||
| import { Input } from '@/components/ui/input'; | ||||
| import { Textarea } from '@/components/ui/textarea'; | ||||
| import { Badge } from '@/components/ui/badge'; | ||||
| import { | ||||
|   Dialog, | ||||
|   DialogContent, | ||||
|   DialogHeader, | ||||
|   DialogTitle, | ||||
|   DialogFooter, | ||||
| } from '@/components/ui/dialog'; | ||||
| import { Label } from '@/components/ui/label'; | ||||
| import { InventoryItem, Tag, UpdateItemData } from '@/types/inventory'; | ||||
| import { inventoryService } from '@/services/inventoryService'; | ||||
| import { useToast } from '@/hooks/use-toast'; | ||||
| 
 | ||||
| interface EditItemDialogProps { | ||||
|   open: boolean; | ||||
|   onOpenChange: (open: boolean) => void; | ||||
|   item: InventoryItem | null; | ||||
|   tags: Tag[]; | ||||
|   onItemUpdated: (updatedItem: InventoryItem) => void; | ||||
| } | ||||
| 
 | ||||
| export const EditItemDialog = ({ | ||||
|   open, | ||||
|   onOpenChange, | ||||
|   item, | ||||
|   tags, | ||||
|   onItemUpdated, | ||||
| }: EditItemDialogProps) => { | ||||
|   const [formData, setFormData] = useState({ | ||||
|     name: '', | ||||
|     description: '', | ||||
|     quantity: 0, | ||||
|     selectedTagIds: [] as string[], | ||||
|   }); | ||||
|   const [isLoading, setIsLoading] = useState(false); | ||||
|   const { toast } = useToast(); | ||||
| 
 | ||||
|   useEffect(() => { | ||||
|     if (item) { | ||||
|       setFormData({ | ||||
|         name: item.name, | ||||
|         description: item.description, | ||||
|         quantity: item.quantity, | ||||
|         selectedTagIds: item.tags.map(tag => tag._id), | ||||
|       }); | ||||
|     } | ||||
|   }, [item]); | ||||
| 
 | ||||
|   const handleSubmit = async (e: React.FormEvent) => { | ||||
|     e.preventDefault(); | ||||
|     if (!item) return; | ||||
| 
 | ||||
|     setIsLoading(true); | ||||
|     try { | ||||
|       const updateData: UpdateItemData = { | ||||
|         name: formData.name, | ||||
|         description: formData.description, | ||||
|         quantity: formData.quantity, | ||||
|         tagIds: formData.selectedTagIds, | ||||
|       }; | ||||
| 
 | ||||
|       const updatedItem = await inventoryService.updateItem(item._id, updateData); | ||||
|       onItemUpdated(updatedItem); | ||||
|       onOpenChange(false); | ||||
|       toast({ | ||||
|         title: "Oggetto aggiornato", | ||||
|         description: `${updatedItem.name} è stato modificato con successo`, | ||||
|       }); | ||||
|     } catch (error) { | ||||
|       toast({ | ||||
|         title: "Errore", | ||||
|         description: "Impossibile aggiornare l'oggetto", | ||||
|         variant: "destructive", | ||||
|       }); | ||||
|     } finally { | ||||
|       setIsLoading(false); | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
|   const toggleTag = (tagId: string) => { | ||||
|     setFormData(prev => ({ | ||||
|       ...prev, | ||||
|       selectedTagIds: prev.selectedTagIds.includes(tagId) | ||||
|         ? prev.selectedTagIds.filter(id => id !== tagId) | ||||
|         : [...prev.selectedTagIds, tagId] | ||||
|     })); | ||||
|   }; | ||||
| 
 | ||||
|   if (!item) return null; | ||||
| 
 | ||||
|   return ( | ||||
|     <Dialog open={open} onOpenChange={onOpenChange}> | ||||
|       <DialogContent className="sm:max-w-[425px]"> | ||||
|         <DialogHeader> | ||||
|           <DialogTitle>Modifica Oggetto</DialogTitle> | ||||
|         </DialogHeader> | ||||
|         <form onSubmit={handleSubmit} className="space-y-4"> | ||||
|           <div className="space-y-2"> | ||||
|             <Label htmlFor="name">Nome</Label> | ||||
|             <Input | ||||
|               id="name" | ||||
|               value={formData.name} | ||||
|               onChange={(e) => setFormData(prev => ({ ...prev, name: e.target.value }))} | ||||
|               required | ||||
|             /> | ||||
|           </div> | ||||
| 
 | ||||
|           <div className="space-y-2"> | ||||
|             <Label htmlFor="description">Descrizione</Label> | ||||
|             <Textarea | ||||
|               id="description" | ||||
|               value={formData.description} | ||||
|               onChange={(e) => setFormData(prev => ({ ...prev, description: e.target.value }))} | ||||
|               rows={3} | ||||
|             /> | ||||
|           </div> | ||||
| 
 | ||||
|           <div className="space-y-2"> | ||||
|             <Label htmlFor="quantity">Quantità</Label> | ||||
|             <Input | ||||
|               id="quantity" | ||||
|               type="number" | ||||
|               min="0" | ||||
|               value={formData.quantity} | ||||
|               onChange={(e) => setFormData(prev => ({ ...prev, quantity: parseInt(e.target.value) || 0 }))} | ||||
|               required | ||||
|             /> | ||||
|           </div> | ||||
| 
 | ||||
|           <div className="space-y-2"> | ||||
|             <Label>Tag</Label> | ||||
|             <div className="flex flex-wrap gap-2"> | ||||
|               {tags.map(tag => ( | ||||
|                 <Badge | ||||
|                   key={tag._id} | ||||
|                   variant="outline" | ||||
|                   className={`cursor-pointer transition-all ${ | ||||
|                     formData.selectedTagIds.includes(tag._id) | ||||
|                       ? 'bg-blue-100 border-blue-300' | ||||
|                       : 'hover:bg-gray-100' | ||||
|                   }`}
 | ||||
|                   onClick={() => toggleTag(tag._id)} | ||||
|                 > | ||||
|                   <div | ||||
|                     className="w-3 h-3 rounded-full mr-2" | ||||
|                     style={{ backgroundColor: tag.color }} | ||||
|                   /> | ||||
|                   {tag.name} | ||||
|                 </Badge> | ||||
|               ))} | ||||
|             </div> | ||||
|           </div> | ||||
| 
 | ||||
|           <DialogFooter> | ||||
|             <Button type="button" variant="outline" onClick={() => onOpenChange(false)}> | ||||
|               Annulla | ||||
|             </Button> | ||||
|             <Button type="submit" disabled={isLoading}> | ||||
|               {isLoading ? 'Salvataggio...' : 'Salva Modifiche'} | ||||
|             </Button> | ||||
|           </DialogFooter> | ||||
|         </form> | ||||
|       </DialogContent> | ||||
|     </Dialog> | ||||
|   ); | ||||
| }; | ||||
|  | @ -1,17 +1,18 @@ | |||
| 
 | ||||
| import { useState, useEffect, useMemo } from 'react'; | ||||
| import { Button } from '@/components/ui/button'; | ||||
| import { Input } from '@/components/ui/input'; | ||||
| import { Badge } from '@/components/ui/badge'; | ||||
| import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; | ||||
| import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'; | ||||
| import { Search, Plus, Tag as TagIcon, User, Trash2 } from 'lucide-react'; | ||||
| import { Search, Plus, Tag as TagIcon, User, Trash2, Edit } from 'lucide-react'; | ||||
| import { InventoryItem, Tag } from '@/types/inventory'; | ||||
| import { inventoryService } from '@/services/inventoryService'; | ||||
| import { useAuth } from '@/hooks/useAuth'; | ||||
| import { AddItemDialog } from '@/components/AddItemDialog'; | ||||
| import { ManageTagsDialog } from '@/components/ManageTagsDialog'; | ||||
| import { DeleteItemDialog } from '@/components/DeleteItemDialog'; | ||||
| import { EditItemDialog } from '@/components/EditItemDialog'; | ||||
| import { QuantityControls } from '@/components/QuantityControls'; | ||||
| import { useToast } from '@/hooks/use-toast'; | ||||
| 
 | ||||
| export const InventoryDashboard = () => { | ||||
|  | @ -24,6 +25,7 @@ export const InventoryDashboard = () => { | |||
|   const [showAddDialog, setShowAddDialog] = useState(false); | ||||
|   const [showTagsDialog, setShowTagsDialog] = useState(false); | ||||
|   const [itemToDelete, setItemToDelete] = useState<InventoryItem | null>(null); | ||||
|   const [itemToEdit, setItemToEdit] = useState<InventoryItem | null>(null); | ||||
|   const [isDeletingItem, setIsDeletingItem] = useState(false); | ||||
| 
 | ||||
|   const { user, logout } = useAuth(); | ||||
|  | @ -122,6 +124,18 @@ export const InventoryDashboard = () => { | |||
|     ); | ||||
|   }; | ||||
| 
 | ||||
|   const handleItemUpdated = (updatedItem: InventoryItem) => { | ||||
|     setItems(prev => prev.map(item =>  | ||||
|       item._id === updatedItem._id ? updatedItem : item | ||||
|     )); | ||||
|   }; | ||||
| 
 | ||||
|   const handleQuantityUpdate = (updatedItem: InventoryItem) => { | ||||
|     setItems(prev => prev.map(item =>  | ||||
|       item._id === updatedItem._id ? updatedItem : item | ||||
|     )); | ||||
|   }; | ||||
| 
 | ||||
|   const filteredItems = useMemo(() => { | ||||
|     if (!searchQuery && selectedTagIds.length === 0) return items; | ||||
| 
 | ||||
|  | @ -244,20 +258,21 @@ export const InventoryDashboard = () => { | |||
|                   {tags.map(tag => { | ||||
|                     console.log(tag); | ||||
|                     return <Badge | ||||
|                       key={tag._id} | ||||
|                       variant="outline" | ||||
|                       className={`cursor-pointer transition-all border`} | ||||
|                       style={{ | ||||
|                         backgroundColor: selectedTagIds.includes(tag._id) ? '#3b82f6' : tag.color, | ||||
|                         color: '#fff', | ||||
|                       }} | ||||
|                       onClick={() => toggleTagFilter(tag._id)} | ||||
|                     > | ||||
|                       {tag.name} | ||||
|                       {selectedTagIds.includes(tag._id) && ( | ||||
|                         <span className="ml-1 font-bold">✓</span> | ||||
|                       )} | ||||
|                     </Badge> | ||||
|                     key={tag._id} | ||||
|                     variant="outline" | ||||
|                     className={`cursor-pointer transition-all ${ | ||||
|                       selectedTagIds.includes(tag._id) | ||||
|                         ? 'bg-blue-100 border-blue-300' | ||||
|                         : 'hover:bg-gray-100' | ||||
|                     }`}
 | ||||
|                     onClick={() => toggleTagFilter(tag._id)} | ||||
|                   > | ||||
|                     <div | ||||
|                       className="w-3 h-3 rounded-full mr-2" | ||||
|                       style={{ backgroundColor: tag.color }} | ||||
|                     /> | ||||
|                     {tag.name} | ||||
|                   </Badge> | ||||
|                   })} | ||||
|                 </div> | ||||
|               </div> | ||||
|  | @ -289,6 +304,7 @@ export const InventoryDashboard = () => { | |||
|                   <TableRow className="bg-slate-50"> | ||||
|                     <TableHead className="font-semibold">Nome</TableHead> | ||||
|                     <TableHead className="font-semibold">Descrizione</TableHead> | ||||
|                     <TableHead className="font-semibold">Quantità</TableHead> | ||||
|                     <TableHead className="font-semibold">Tag</TableHead> | ||||
|                     <TableHead className="font-semibold">Data Aggiunta</TableHead> | ||||
|                     <TableHead className="font-semibold">Aggiunto da</TableHead> | ||||
|  | @ -298,7 +314,7 @@ export const InventoryDashboard = () => { | |||
|                 <TableBody> | ||||
|                   {filteredItems.length === 0 ? ( | ||||
|                     <TableRow> | ||||
|                       <TableCell colSpan={6} className="text-center py-8 text-gray-500"> | ||||
|                       <TableCell colSpan={7} className="text-center py-8 text-gray-500"> | ||||
|                         {searchQuery || selectedTagIds.length > 0 | ||||
|                           ? "Nessun oggetto trovato con i filtri applicati" | ||||
|                           : "Nessun oggetto nell'inventario" | ||||
|  | @ -310,26 +326,49 @@ export const InventoryDashboard = () => { | |||
|                       <TableRow key={item._id} className="hover:bg-slate-50 transition-colors"> | ||||
|                         <TableCell className="font-medium">{item.name}</TableCell> | ||||
|                         <TableCell className="text-gray-600">{item.description}</TableCell> | ||||
|                         <TableCell> | ||||
|                           <QuantityControls  | ||||
|                             item={item}  | ||||
|                             onQuantityUpdate={handleQuantityUpdate} | ||||
|                           /> | ||||
|                         </TableCell> | ||||
|                         <TableCell> | ||||
|                           <div className="flex flex-wrap gap-1"> | ||||
|                             {item.tags.map(tag => ( | ||||
|                               <Badge key={tag._id} variant="outline" className={`text-xs text-white`} style={{ backgroundColor: tag.color }}> | ||||
|                                 {tag.name} | ||||
|                               </Badge> | ||||
|                               <Badge | ||||
|                               key={tag._id} | ||||
|                               variant="outline" | ||||
|                             > | ||||
|                               <div | ||||
|                                 className="w-3 h-3 rounded-full mr-2" | ||||
|                                 style={{ backgroundColor: tag.color }} | ||||
|                               /> | ||||
|                               {tag.name} | ||||
|                             </Badge> | ||||
|                             ))} | ||||
|                           </div> | ||||
|                         </TableCell> | ||||
|                         <TableCell className="text-gray-600">{formatDate(item.dateAdded)}</TableCell> | ||||
|                         <TableCell className="text-gray-600">{item.addedBy}</TableCell> | ||||
|                         <TableCell> | ||||
|                           <Button | ||||
|                             variant="ghost" | ||||
|                             size="sm" | ||||
|                             onClick={() => setItemToDelete(item)} | ||||
|                             className="text-red-600 hover:text-red-700 hover:bg-red-50" | ||||
|                           > | ||||
|                             <Trash2 className="w-4 h-4" /> | ||||
|                           </Button> | ||||
|                           <div className="flex gap-1"> | ||||
|                             <Button | ||||
|                               variant="ghost" | ||||
|                               size="sm" | ||||
|                               onClick={() => setItemToEdit(item)} | ||||
|                               className="text-blue-600 hover:text-blue-700 hover:bg-blue-50" | ||||
|                             > | ||||
|                               <Edit className="w-4 h-4" /> | ||||
|                             </Button> | ||||
|                             <Button | ||||
|                               variant="ghost" | ||||
|                               size="sm" | ||||
|                               onClick={() => setItemToDelete(item)} | ||||
|                               className="text-red-600 hover:text-red-700 hover:bg-red-50" | ||||
|                             > | ||||
|                               <Trash2 className="w-4 h-4" /> | ||||
|                             </Button> | ||||
|                           </div> | ||||
|                         </TableCell> | ||||
|                       </TableRow> | ||||
|                     )) | ||||
|  | @ -363,6 +402,14 @@ export const InventoryDashboard = () => { | |||
|         onConfirm={handleDeleteItem} | ||||
|         isLoading={isDeletingItem} | ||||
|       /> | ||||
| 
 | ||||
|       <EditItemDialog | ||||
|         open={!!itemToEdit} | ||||
|         onOpenChange={() => setItemToEdit(null)} | ||||
|         item={itemToEdit} | ||||
|         tags={tags} | ||||
|         onItemUpdated={handleItemUpdated} | ||||
|       /> | ||||
|     </div> | ||||
|   ); | ||||
| }; | ||||
|  |  | |||
|  | @ -32,24 +32,24 @@ interface ManageTagsDialogProps { | |||
|   onTagDeleted: (tagId: string) => void; | ||||
| } | ||||
| 
 | ||||
| export const ManageTagsDialog = ({  | ||||
|   open,  | ||||
|   onOpenChange,  | ||||
|   tags,  | ||||
|   onTagCreated,  | ||||
|   onTagDeleted  | ||||
| export const ManageTagsDialog = ({ | ||||
|   open, | ||||
|   onOpenChange, | ||||
|   tags, | ||||
|   onTagCreated, | ||||
|   onTagDeleted | ||||
| }: ManageTagsDialogProps) => { | ||||
|   const [newTagName, setNewTagName] = useState(''); | ||||
|   const [newTagColor, setNewTagColor] = useState('#000000'); | ||||
|   const [isLoading, setIsLoading] = useState(false); | ||||
|   const [tagToDelete, setTagToDelete] = useState<Tag | null>(null); | ||||
|   const [isDeletingTag, setIsDeletingTag] = useState(false); | ||||
|    | ||||
| 
 | ||||
|   const { toast } = useToast(); | ||||
| 
 | ||||
|   const handleCreateTag = async (e: React.FormEvent) => { | ||||
|     e.preventDefault(); | ||||
|      | ||||
| 
 | ||||
|     if (!newTagName.trim()) { | ||||
|       toast({ | ||||
|         title: "Errore", | ||||
|  | @ -69,13 +69,13 @@ export const ManageTagsDialog = ({ | |||
|     } | ||||
| 
 | ||||
|     setIsLoading(true); | ||||
|      | ||||
| 
 | ||||
|     try { | ||||
|       const newTag = await inventoryService.createTag(newTagName.trim(), newTagColor.trim()); | ||||
|       onTagCreated(newTag); | ||||
|       setNewTagName(''); | ||||
|       setNewTagColor('#000000'); | ||||
|        | ||||
| 
 | ||||
|       toast({ | ||||
|         title: "Tag creato", | ||||
|         description: `Il tag "${newTag.name}" è stato aggiunto`, | ||||
|  | @ -95,12 +95,12 @@ export const ManageTagsDialog = ({ | |||
|     if (!tagToDelete) return; | ||||
| 
 | ||||
|     setIsDeletingTag(true); | ||||
|      | ||||
| 
 | ||||
|     try { | ||||
|       await inventoryService.deleteTag(tagToDelete._id); | ||||
|       onTagDeleted(tagToDelete._id); | ||||
|       setTagToDelete(null); | ||||
|        | ||||
| 
 | ||||
|       toast({ | ||||
|         title: "Tag eliminato", | ||||
|         description: `Il tag "${tagToDelete.name}" è stato eliminato`, | ||||
|  | @ -129,7 +129,7 @@ export const ManageTagsDialog = ({ | |||
|               Visualizza, crea ed elimina tag per organizzare l'inventario | ||||
|             </DialogDescription> | ||||
|           </DialogHeader> | ||||
|            | ||||
| 
 | ||||
|           <div className="space-y-6"> | ||||
|             {/* Create New Tag */} | ||||
|             <div className="space-y-3"> | ||||
|  | @ -167,10 +167,13 @@ export const ManageTagsDialog = ({ | |||
|                 {tags.map(tag => ( | ||||
|                   <div key={tag._id} className="flex items-center justify-between mb-2 p-2 bg-white rounded border"> | ||||
|                     <Badge | ||||
|                       key={tag._id} | ||||
|                       variant="outline" | ||||
|                       style={{ backgroundColor: tag.color }} | ||||
|                       className="text-white" | ||||
|                     > | ||||
|                       <div | ||||
|                         className="w-3 h-3 rounded-full mr-2" | ||||
|                         style={{ backgroundColor: tag.color }} | ||||
|                       /> | ||||
|                       {tag.name} | ||||
|                     </Badge> | ||||
|                     <Button | ||||
|  | @ -213,7 +216,7 @@ export const ManageTagsDialog = ({ | |||
|             <AlertDialogCancel disabled={isDeletingTag}> | ||||
|               Annulla | ||||
|             </AlertDialogCancel> | ||||
|             <AlertDialogAction  | ||||
|             <AlertDialogAction | ||||
|               onClick={handleDeleteTag} | ||||
|               disabled={isDeletingTag} | ||||
|               className="bg-red-600 hover:bg-red-700" | ||||
|  |  | |||
							
								
								
									
										65
									
								
								frontend/src/components/QuantityControls.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								frontend/src/components/QuantityControls.tsx
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,65 @@ | |||
| 
 | ||||
| import { useState } from 'react'; | ||||
| import { Button } from '@/components/ui/button'; | ||||
| import { Plus, Minus } from 'lucide-react'; | ||||
| import { InventoryItem } from '@/types/inventory'; | ||||
| import { inventoryService } from '@/services/inventoryService'; | ||||
| import { useToast } from '@/hooks/use-toast'; | ||||
| 
 | ||||
| interface QuantityControlsProps { | ||||
|   item: InventoryItem; | ||||
|   onQuantityUpdate: (updatedItem: InventoryItem) => void; | ||||
| } | ||||
| 
 | ||||
| export const QuantityControls = ({ item, onQuantityUpdate }: QuantityControlsProps) => { | ||||
|   const [isUpdating, setIsUpdating] = useState(false); | ||||
|   const { toast } = useToast(); | ||||
| 
 | ||||
|   const updateQuantity = async (newQuantity: number) => { | ||||
|     if (newQuantity < 0) return; | ||||
|      | ||||
|     setIsUpdating(true); | ||||
|     try { | ||||
|       const updatedItem = await inventoryService.updateItemQuantity(item._id, newQuantity); | ||||
|       onQuantityUpdate(updatedItem); | ||||
|       toast({ | ||||
|         title: "Quantità aggiornata", | ||||
|         description: `${item.name}: ${newQuantity} pezzi`, | ||||
|       }); | ||||
|     } catch (error) { | ||||
|       toast({ | ||||
|         title: "Errore", | ||||
|         description: "Impossibile aggiornare la quantità", | ||||
|         variant: "destructive", | ||||
|       }); | ||||
|     } finally { | ||||
|       setIsUpdating(false); | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
|   return ( | ||||
|     <div className="flex items-center space-x-2"> | ||||
|       <Button | ||||
|         variant="outline" | ||||
|         size="sm" | ||||
|         onClick={() => updateQuantity(item.quantity - 1)} | ||||
|         disabled={isUpdating || item.quantity <= 0} | ||||
|         className="h-8 w-8 p-0" | ||||
|       > | ||||
|         <Minus className="w-3 h-3" /> | ||||
|       </Button> | ||||
|       <span className="text-sm font-medium min-w-[2rem] text-center"> | ||||
|         {item.quantity} | ||||
|       </span> | ||||
|       <Button | ||||
|         variant="outline" | ||||
|         size="sm" | ||||
|         onClick={() => updateQuantity(item.quantity + 1)} | ||||
|         disabled={isUpdating} | ||||
|         className="h-8 w-8 p-0" | ||||
|       > | ||||
|         <Plus className="w-3 h-3" /> | ||||
|       </Button> | ||||
|     </div> | ||||
|   ); | ||||
| }; | ||||
|  | @ -5,6 +5,7 @@ import { | |||
|   InventoryItem, | ||||
|   Tag, | ||||
|   CreateItemData, | ||||
|   UpdateItemData, | ||||
| } from '@/types/inventory'; | ||||
| 
 | ||||
| const API_URL = import.meta.env.VITE_API_URL ?? 'http://localhost:6789/api'; | ||||
|  | @ -45,6 +46,40 @@ export const inventoryService = { | |||
|     return res.json(); | ||||
|   }, | ||||
| 
 | ||||
|   async updateItem(itemId: string, data: UpdateItemData): Promise<InventoryItem> { | ||||
|     const res = await fetch(`${API_URL}/items/${itemId}`, { | ||||
|       method: 'PUT', | ||||
|       headers: { | ||||
|         'Content-Type': 'application/json', | ||||
|         ...getAuthHeader(), | ||||
|       }, | ||||
|       body: JSON.stringify(data), | ||||
|     }); | ||||
| 
 | ||||
|     if (!res.ok) { | ||||
|       const msg = (await res.json())?.message ?? 'Errore aggiornamento item'; | ||||
|       throw new Error(msg); | ||||
|     } | ||||
|     return res.json(); | ||||
|   }, | ||||
| 
 | ||||
|   async updateItemQuantity(itemId: string, quantity: number): Promise<InventoryItem> { | ||||
|     const res = await fetch(`${API_URL}/items/${itemId}/quantity`, { | ||||
|       method: 'PATCH', | ||||
|       headers: { | ||||
|         'Content-Type': 'application/json', | ||||
|         ...getAuthHeader(), | ||||
|       }, | ||||
|       body: JSON.stringify({ quantity }), | ||||
|     }); | ||||
| 
 | ||||
|     if (!res.ok) { | ||||
|       const msg = (await res.json())?.message ?? 'Errore aggiornamento quantità'; | ||||
|       throw new Error(msg); | ||||
|     } | ||||
|     return res.json(); | ||||
|   }, | ||||
| 
 | ||||
|   async deleteItem(itemId: string): Promise<void> { | ||||
|     const res = await fetch(`${API_URL}/items/${itemId}`, { | ||||
|       method: 'DELETE', | ||||
|  |  | |||
|  | @ -9,6 +9,7 @@ export interface InventoryItem { | |||
|   _id: string; | ||||
|   name: string; | ||||
|   description: string; | ||||
|   quantity: number; | ||||
|   tags: Tag[]; | ||||
|   dateAdded: string; | ||||
|   addedBy: string; | ||||
|  | @ -17,5 +18,13 @@ export interface InventoryItem { | |||
| export interface CreateItemData { | ||||
|   name: string; | ||||
|   description: string; | ||||
|   quantity: number; | ||||
|   tagIds: string[]; | ||||
| } | ||||
| 
 | ||||
| export interface UpdateItemData { | ||||
|   name?: string; | ||||
|   description?: string; | ||||
|   quantity?: number; | ||||
|   tagIds?: string[]; | ||||
| } | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 StefanoPutelli
						StefanoPutelli