|
|
@@ -0,0 +1,285 @@
|
|
|
+"use client"
|
|
|
+
|
|
|
+import { useState } from 'react'
|
|
|
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
|
|
+import { Button } from "@/components/ui/button"
|
|
|
+import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"
|
|
|
+import { Badge } from "@/components/ui/badge"
|
|
|
+import { Label } from "@/components/ui/label"
|
|
|
+import { Input } from "@/components/ui/input"
|
|
|
+import { useServicios, type Servicio } from '@/hooks/useServicios'
|
|
|
+import { ServicioForm } from './ServicioForm'
|
|
|
+import { Plus, Edit, Trash2, Save, Search } from 'lucide-react'
|
|
|
+
|
|
|
+export function ServicioManager() {
|
|
|
+ const {
|
|
|
+ servicios,
|
|
|
+ loading,
|
|
|
+ error,
|
|
|
+ createServicio,
|
|
|
+ updateServicio,
|
|
|
+ deleteServicio,
|
|
|
+ } = useServicios()
|
|
|
+
|
|
|
+ const [editingServicio, setEditingServicio] = useState<string | null>(null)
|
|
|
+ const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false)
|
|
|
+ const [searchTerm, setSearchTerm] = useState('')
|
|
|
+ const [formData, setFormData] = useState({
|
|
|
+ codigoPrincipal: '',
|
|
|
+ codigoAuxiliar: '',
|
|
|
+ descripcion: '',
|
|
|
+ precioUnitario: '',
|
|
|
+ codigoPorcentaje: '2',
|
|
|
+ tarifa: '15',
|
|
|
+ activo: true,
|
|
|
+ })
|
|
|
+
|
|
|
+ const resetForm = () => {
|
|
|
+ setFormData({
|
|
|
+ codigoPrincipal: '',
|
|
|
+ codigoAuxiliar: '',
|
|
|
+ descripcion: '',
|
|
|
+ precioUnitario: '',
|
|
|
+ codigoPorcentaje: '2',
|
|
|
+ tarifa: '15',
|
|
|
+ activo: true,
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ const handleCreate = async () => {
|
|
|
+ try {
|
|
|
+ const dataToSubmit = {
|
|
|
+ ...formData,
|
|
|
+ codigoAuxiliar: formData.codigoAuxiliar || undefined
|
|
|
+ }
|
|
|
+ await createServicio(dataToSubmit)
|
|
|
+ setIsCreateDialogOpen(false)
|
|
|
+ resetForm()
|
|
|
+ } catch (error) {
|
|
|
+ console.error('Error creating servicio:', error)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ const handleUpdate = async (id: string) => {
|
|
|
+ try {
|
|
|
+ const dataToSubmit = {
|
|
|
+ ...formData,
|
|
|
+ codigoAuxiliar: formData.codigoAuxiliar || undefined
|
|
|
+ }
|
|
|
+ await updateServicio(id, dataToSubmit)
|
|
|
+ setEditingServicio(null)
|
|
|
+ resetForm()
|
|
|
+ } catch (error) {
|
|
|
+ console.error('Error updating servicio:', error)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ const handleDelete = async (id: string) => {
|
|
|
+ if (confirm('¿Está seguro de que desea eliminar este servicio?')) {
|
|
|
+ try {
|
|
|
+ await deleteServicio(id)
|
|
|
+ } catch (error) {
|
|
|
+ console.error('Error deleting servicio:', error)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ const startEdit = (servicio: Servicio) => {
|
|
|
+ setEditingServicio(servicio.id)
|
|
|
+ setFormData({
|
|
|
+ codigoPrincipal: servicio.codigoPrincipal,
|
|
|
+ codigoAuxiliar: servicio.codigoAuxiliar || '',
|
|
|
+ descripcion: servicio.descripcion,
|
|
|
+ precioUnitario: servicio.precioUnitario,
|
|
|
+ codigoPorcentaje: servicio.codigoPorcentaje,
|
|
|
+ tarifa: servicio.tarifa,
|
|
|
+ activo: servicio.activo,
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ const cancelEdit = () => {
|
|
|
+ setEditingServicio(null)
|
|
|
+ resetForm()
|
|
|
+ }
|
|
|
+
|
|
|
+ const getCodigoPorcentajeLabel = (codigo: string) => {
|
|
|
+ const codigos: Record<string, string> = {
|
|
|
+ '0': 'No objeto de IVA',
|
|
|
+ '2': 'IVA',
|
|
|
+ }
|
|
|
+ return codigos[codigo] || codigo
|
|
|
+ }
|
|
|
+
|
|
|
+ // Filtrar servicios por búsqueda
|
|
|
+ const filteredServicios = servicios.filter(servicio =>
|
|
|
+ servicio.descripcion.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
|
+ servicio.codigoPrincipal.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
|
+ (servicio.codigoAuxiliar && servicio.codigoAuxiliar.toLowerCase().includes(searchTerm.toLowerCase()))
|
|
|
+ )
|
|
|
+
|
|
|
+ if (loading && servicios.length === 0) {
|
|
|
+ return <div className="flex justify-center items-center h-64">Cargando...</div>
|
|
|
+ }
|
|
|
+
|
|
|
+ if (error) {
|
|
|
+ return <div className="text-red-500 text-center p-4">Error: {error}</div>
|
|
|
+ }
|
|
|
+
|
|
|
+ return (
|
|
|
+ <div className="space-y-6">
|
|
|
+ <div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-4">
|
|
|
+ <div>
|
|
|
+ <h2 className="text-2xl font-bold">Servicios</h2>
|
|
|
+ <p className="mt-2 text-muted-foreground">Gestiona la información de tus servicios y productos</p>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div className="flex gap-2 w-full md:w-auto">
|
|
|
+ <div className="relative flex-1 md:flex-initial">
|
|
|
+ <Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
|
|
|
+ <Input
|
|
|
+ placeholder="Buscar servicio..."
|
|
|
+ value={searchTerm}
|
|
|
+ onChange={(e) => setSearchTerm(e.target.value)}
|
|
|
+ className="pl-8 w-full md:w-[300px]"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <Dialog open={isCreateDialogOpen} onOpenChange={setIsCreateDialogOpen}>
|
|
|
+ <DialogTrigger asChild>
|
|
|
+ <Button onClick={() => { resetForm(); setIsCreateDialogOpen(true); }}>
|
|
|
+ <Plus className="w-4 h-4 mr-2" />
|
|
|
+ Nuevo Servicio
|
|
|
+ </Button>
|
|
|
+ </DialogTrigger>
|
|
|
+ <DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
|
|
|
+ <DialogHeader>
|
|
|
+ <DialogTitle>Nuevo Servicio</DialogTitle>
|
|
|
+ <DialogDescription>
|
|
|
+ Ingresa los datos del servicio o producto
|
|
|
+ </DialogDescription>
|
|
|
+ </DialogHeader>
|
|
|
+ <ServicioForm
|
|
|
+ formData={formData}
|
|
|
+ setFormData={setFormData}
|
|
|
+ onSave={handleCreate}
|
|
|
+ onCancel={() => setIsCreateDialogOpen(false)}
|
|
|
+ />
|
|
|
+ </DialogContent>
|
|
|
+ </Dialog>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div className="grid gap-4">
|
|
|
+ {filteredServicios.map((servicio) => (
|
|
|
+ <Card key={servicio.id}>
|
|
|
+ <CardHeader>
|
|
|
+ <div className="flex justify-between items-start">
|
|
|
+ <div>
|
|
|
+ <CardTitle className="flex items-center gap-2">
|
|
|
+ {servicio.codigoPrincipal} - {servicio.descripcion}
|
|
|
+ <Badge variant={servicio.activo ? "default" : "secondary"}>
|
|
|
+ {servicio.activo ? "Activo" : "Inactivo"}
|
|
|
+ </Badge>
|
|
|
+ </CardTitle>
|
|
|
+ <CardDescription>
|
|
|
+ ${parseFloat(servicio.precioUnitario).toFixed(2)} • {getCodigoPorcentajeLabel(servicio.codigoPorcentaje)}
|
|
|
+ {servicio.codigoPorcentaje !== '0' && ` (${servicio.tarifa}%)`}
|
|
|
+ </CardDescription>
|
|
|
+ </div>
|
|
|
+ <div className="flex gap-2">
|
|
|
+ {editingServicio === servicio.id ? (
|
|
|
+ <>
|
|
|
+ <Button
|
|
|
+ size="sm"
|
|
|
+ onClick={() => handleUpdate(servicio.id)}
|
|
|
+ disabled={loading}
|
|
|
+ >
|
|
|
+ <Save className="w-4 h-4" />
|
|
|
+ </Button>
|
|
|
+ <Button
|
|
|
+ size="sm"
|
|
|
+ variant="outline"
|
|
|
+ onClick={cancelEdit}
|
|
|
+ disabled={loading}
|
|
|
+ >
|
|
|
+ Cancelar
|
|
|
+ </Button>
|
|
|
+ </>
|
|
|
+ ) : (
|
|
|
+ <>
|
|
|
+ <Button
|
|
|
+ size="sm"
|
|
|
+ variant="outline"
|
|
|
+ onClick={() => startEdit(servicio)}
|
|
|
+ >
|
|
|
+ <Edit className="w-4 h-4" />
|
|
|
+ </Button>
|
|
|
+ <Button
|
|
|
+ size="sm"
|
|
|
+ variant="destructive"
|
|
|
+ onClick={() => handleDelete(servicio.id)}
|
|
|
+ disabled={loading}
|
|
|
+ >
|
|
|
+ <Trash2 className="w-4 h-4" />
|
|
|
+ </Button>
|
|
|
+ </>
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </CardHeader>
|
|
|
+ <CardContent>
|
|
|
+ {editingServicio === servicio.id ? (
|
|
|
+ <ServicioForm
|
|
|
+ formData={formData}
|
|
|
+ setFormData={setFormData}
|
|
|
+ onSave={() => handleUpdate(servicio.id)}
|
|
|
+ onCancel={cancelEdit}
|
|
|
+ isEdit
|
|
|
+ />
|
|
|
+ ) : (
|
|
|
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-4 text-sm">
|
|
|
+ {servicio.codigoAuxiliar && (
|
|
|
+ <div>
|
|
|
+ <Label className="text-muted-foreground">Código Auxiliar</Label>
|
|
|
+ <p>{servicio.codigoAuxiliar}</p>
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ <div>
|
|
|
+ <Label className="text-muted-foreground">Precio Unitario</Label>
|
|
|
+ <p>${parseFloat(servicio.precioUnitario).toFixed(2)}</p>
|
|
|
+ </div>
|
|
|
+ <div>
|
|
|
+ <Label className="text-muted-foreground">IVA</Label>
|
|
|
+ <p>
|
|
|
+ {getCodigoPorcentajeLabel(servicio.codigoPorcentaje)}
|
|
|
+ {servicio.codigoPorcentaje !== '0' && ` (${servicio.tarifa}%)`}
|
|
|
+ </p>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ </CardContent>
|
|
|
+ </Card>
|
|
|
+ ))}
|
|
|
+
|
|
|
+ {filteredServicios.length === 0 && !loading && (
|
|
|
+ <Card>
|
|
|
+ <CardContent className="text-center py-8">
|
|
|
+ <p className="text-gray-500">
|
|
|
+ {searchTerm ? 'No se encontraron servicios con ese criterio de búsqueda' : 'No hay servicios registrados'}
|
|
|
+ </p>
|
|
|
+ {!searchTerm && (
|
|
|
+ <Button
|
|
|
+ className="mt-4"
|
|
|
+ onClick={() => setIsCreateDialogOpen(true)}
|
|
|
+ >
|
|
|
+ <Plus className="w-4 h-4 mr-2" />
|
|
|
+ Crear Primer Servicio
|
|
|
+ </Button>
|
|
|
+ )}
|
|
|
+ </CardContent>
|
|
|
+ </Card>
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ )
|
|
|
+}
|