ar-saas 0.4.2 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -1
- package/templates/backend/.env.example +13 -3
- package/templates/backend/README.md +22 -3
- package/templates/backend/package-lock.json +165 -2
- package/templates/backend/package.json +2 -0
- package/templates/backend/src/app.module.ts +14 -0
- package/templates/backend/src/common/guards/github-auth.guard.ts +5 -0
- package/templates/backend/src/main.ts +2 -2
- package/templates/backend/src/modules/auth/auth.controller.ts +51 -3
- package/templates/backend/src/modules/auth/auth.module.ts +2 -1
- package/templates/backend/src/modules/auth/auth.service.ts +96 -11
- package/templates/backend/src/modules/auth/strategies/github.strategy.ts +46 -0
- package/templates/backend/src/modules/auth/strategies/jwt.strategy.ts +1 -1
- package/templates/backend/src/modules/clients/clients.controller.ts +91 -0
- package/templates/backend/src/modules/clients/clients.module.ts +16 -0
- package/templates/backend/src/modules/clients/clients.repository.ts +14 -0
- package/templates/backend/src/modules/clients/clients.service.ts +52 -0
- package/templates/backend/src/modules/clients/dto/create-client.dto.ts +40 -0
- package/templates/backend/src/modules/clients/dto/query-client.dto.ts +30 -0
- package/templates/backend/src/modules/clients/dto/update-client.dto.ts +4 -0
- package/templates/backend/src/modules/clients/schemas/client.schema.ts +32 -0
- package/templates/backend/src/modules/invoices/dto/create-invoice.dto.ts +79 -0
- package/templates/backend/src/modules/invoices/dto/invoice-item.dto.ts +23 -0
- package/templates/backend/src/modules/invoices/dto/query-invoice.dto.ts +40 -0
- package/templates/backend/src/modules/invoices/dto/update-invoice.dto.ts +4 -0
- package/templates/backend/src/modules/invoices/invoices.controller.ts +91 -0
- package/templates/backend/src/modules/invoices/invoices.module.ts +18 -0
- package/templates/backend/src/modules/invoices/invoices.repository.ts +14 -0
- package/templates/backend/src/modules/invoices/invoices.service.ts +104 -0
- package/templates/backend/src/modules/invoices/schemas/invoice.schema.ts +75 -0
- package/templates/backend/src/modules/notifications/dto/create-notification.dto.ts +45 -0
- package/templates/backend/src/modules/notifications/dto/query-notification.dto.ts +30 -0
- package/templates/backend/src/modules/notifications/dto/update-notification.dto.ts +4 -0
- package/templates/backend/src/modules/notifications/notifications.controller.ts +119 -0
- package/templates/backend/src/modules/notifications/notifications.module.ts +16 -0
- package/templates/backend/src/modules/notifications/notifications.repository.ts +31 -0
- package/templates/backend/src/modules/notifications/notifications.service.ts +64 -0
- package/templates/backend/src/modules/notifications/schemas/notification.schema.ts +38 -0
- package/templates/backend/src/modules/pipeline/dto/create-deal.dto.ts +40 -0
- package/templates/backend/src/modules/pipeline/dto/query-deal.dto.ts +35 -0
- package/templates/backend/src/modules/pipeline/dto/update-deal.dto.ts +4 -0
- package/templates/backend/src/modules/pipeline/pipeline.controller.ts +91 -0
- package/templates/backend/src/modules/pipeline/pipeline.module.ts +18 -0
- package/templates/backend/src/modules/pipeline/pipeline.repository.ts +14 -0
- package/templates/backend/src/modules/pipeline/pipeline.service.ts +64 -0
- package/templates/backend/src/modules/pipeline/schemas/deal.schema.ts +39 -0
- package/templates/backend/src/modules/planner/dto/create-planner-block.dto.ts +66 -0
- package/templates/backend/src/modules/planner/dto/query-planner-block.dto.ts +48 -0
- package/templates/backend/src/modules/planner/dto/update-block-status.dto.ts +10 -0
- package/templates/backend/src/modules/planner/dto/update-planner-block.dto.ts +4 -0
- package/templates/backend/src/modules/planner/planner.controller.ts +124 -0
- package/templates/backend/src/modules/planner/planner.module.ts +16 -0
- package/templates/backend/src/modules/planner/planner.repository.ts +45 -0
- package/templates/backend/src/modules/planner/planner.service.ts +104 -0
- package/templates/backend/src/modules/planner/schemas/planner-block.schema.ts +56 -0
- package/templates/backend/src/modules/task-columns/dto/create-task-column.dto.ts +20 -0
- package/templates/backend/src/modules/task-columns/dto/reorder-columns.dto.ts +9 -0
- package/templates/backend/src/modules/task-columns/dto/update-task-column.dto.ts +4 -0
- package/templates/backend/src/modules/task-columns/schemas/task-column.schema.ts +21 -0
- package/templates/backend/src/modules/task-columns/task-columns.controller.ts +86 -0
- package/templates/backend/src/modules/task-columns/task-columns.module.ts +16 -0
- package/templates/backend/src/modules/task-columns/task-columns.repository.ts +15 -0
- package/templates/backend/src/modules/task-columns/task-columns.service.ts +49 -0
- package/templates/backend/src/modules/tasks/dto/checklist-item.dto.ts +13 -0
- package/templates/backend/src/modules/tasks/dto/create-task.dto.ts +67 -0
- package/templates/backend/src/modules/tasks/dto/label.dto.ts +12 -0
- package/templates/backend/src/modules/tasks/dto/move-task.dto.ts +15 -0
- package/templates/backend/src/modules/tasks/dto/query-task.dto.ts +40 -0
- package/templates/backend/src/modules/tasks/dto/update-task.dto.ts +4 -0
- package/templates/backend/src/modules/tasks/schemas/task.schema.ts +66 -0
- package/templates/backend/src/modules/tasks/tasks.controller.ts +104 -0
- package/templates/backend/src/modules/tasks/tasks.module.ts +18 -0
- package/templates/backend/src/modules/tasks/tasks.repository.ts +14 -0
- package/templates/backend/src/modules/tasks/tasks.service.ts +76 -0
- package/templates/backend/src/modules/users/schemas/user.schema.ts +3 -0
- package/templates/backend/src/modules/users/users.repository.ts +8 -0
- package/templates/backend/src/modules/users/users.service.ts +34 -0
- package/templates/frontend/.env.local.example +1 -1
- package/templates/frontend/README.md +43 -1
- package/templates/frontend/package.json +48 -45
- package/templates/frontend/pnpm-lock.yaml +5096 -5012
- package/templates/frontend/src/app/(auth)/layout.tsx +7 -1
- package/templates/frontend/src/app/(auth)/login/page.tsx +13 -0
- package/templates/frontend/src/app/(auth)/register/page.tsx +13 -0
- package/templates/frontend/src/app/(dashboard)/clients/page.tsx +295 -0
- package/templates/frontend/src/app/(dashboard)/invoices/page.tsx +305 -0
- package/templates/frontend/src/app/(dashboard)/notifications/page.tsx +173 -0
- package/templates/frontend/src/app/(dashboard)/pipeline/page.tsx +244 -0
- package/templates/frontend/src/app/(dashboard)/planner/page.tsx +287 -0
- package/templates/frontend/src/app/(dashboard)/settings/page.tsx +165 -128
- package/templates/frontend/src/app/(dashboard)/tasks/page.tsx +366 -0
- package/templates/frontend/src/app/auth/github/callback/page.tsx +82 -0
- package/templates/frontend/src/app/landing/page.tsx +21 -0
- package/templates/frontend/src/app/page.tsx +5 -5
- package/templates/frontend/src/app/setup/page.tsx +15 -14
- package/templates/frontend/src/components/auth/github-button.tsx +25 -0
- package/templates/frontend/src/components/dashboard/sidebar.tsx +90 -71
- package/templates/frontend/src/components/ui/alert-dialog.tsx +141 -0
- package/templates/frontend/src/components/ui/button.tsx +56 -52
- package/templates/frontend/src/components/ui/popover.tsx +31 -0
- package/templates/frontend/src/components/ui/select.tsx +160 -0
- package/templates/frontend/src/components/ui/sheet.tsx +140 -0
- package/templates/frontend/src/lib/api/auth.ts +7 -0
- package/templates/frontend/src/lib/api/clients.ts +17 -0
- package/templates/frontend/src/lib/api/invoices.ts +18 -0
- package/templates/frontend/src/lib/api/notifications.ts +27 -0
- package/templates/frontend/src/lib/api/pipeline.ts +18 -0
- package/templates/frontend/src/lib/api/planner.ts +26 -0
- package/templates/frontend/src/lib/api/task-columns.ts +17 -0
- package/templates/frontend/src/lib/api/tasks.ts +21 -0
- package/templates/frontend/src/lib/hooks/use-unread-notifications.ts +23 -0
- package/templates/frontend/src/providers/auth-provider.tsx +7 -1
- package/templates/frontend/src/types/clients.ts +38 -0
- package/templates/frontend/src/types/invoices.ts +51 -0
- package/templates/frontend/src/types/notifications.ts +30 -0
- package/templates/frontend/src/types/pipeline.ts +35 -0
- package/templates/frontend/src/types/planner.ts +49 -0
- package/templates/frontend/src/types/tasks.ts +65 -0
|
@@ -15,7 +15,13 @@ export default function AuthLayout({ children }: { children: React.ReactNode })
|
|
|
15
15
|
}
|
|
16
16
|
}, [isAuthenticated, isLoading, router])
|
|
17
17
|
|
|
18
|
-
if (isLoading)
|
|
18
|
+
if (isLoading) {
|
|
19
|
+
return (
|
|
20
|
+
<div className="min-h-screen flex items-center justify-center bg-muted/40">
|
|
21
|
+
<div className="text-sm text-muted-foreground">Cargando...</div>
|
|
22
|
+
</div>
|
|
23
|
+
)
|
|
24
|
+
}
|
|
19
25
|
|
|
20
26
|
return (
|
|
21
27
|
<div className="min-h-screen flex flex-col items-center justify-center bg-muted/40 px-4">
|
|
@@ -9,6 +9,8 @@ import { Button } from '@/components/ui/button'
|
|
|
9
9
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
|
10
10
|
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'
|
|
11
11
|
import { Input } from '@/components/ui/input'
|
|
12
|
+
import { Separator } from '@/components/ui/separator'
|
|
13
|
+
import { GitHubButton } from '@/components/auth/github-button'
|
|
12
14
|
|
|
13
15
|
interface LoginForm {
|
|
14
16
|
email: string
|
|
@@ -99,6 +101,17 @@ export default function LoginPage() {
|
|
|
99
101
|
</Button>
|
|
100
102
|
</form>
|
|
101
103
|
</Form>
|
|
104
|
+
<div className="mt-4 space-y-3">
|
|
105
|
+
<div className="relative">
|
|
106
|
+
<div className="absolute inset-0 flex items-center">
|
|
107
|
+
<Separator />
|
|
108
|
+
</div>
|
|
109
|
+
<div className="relative flex justify-center text-xs uppercase">
|
|
110
|
+
<span className="bg-card px-2 text-muted-foreground">o continuá con</span>
|
|
111
|
+
</div>
|
|
112
|
+
</div>
|
|
113
|
+
<GitHubButton />
|
|
114
|
+
</div>
|
|
102
115
|
<p className="mt-4 text-center text-sm text-muted-foreground">
|
|
103
116
|
¿No tenés cuenta?{' '}
|
|
104
117
|
<Link href="/register" className="underline underline-offset-4 hover:text-foreground">
|
|
@@ -9,6 +9,8 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com
|
|
|
9
9
|
import { Checkbox } from '@/components/ui/checkbox'
|
|
10
10
|
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'
|
|
11
11
|
import { Input } from '@/components/ui/input'
|
|
12
|
+
import { Separator } from '@/components/ui/separator'
|
|
13
|
+
import { GitHubButton } from '@/components/auth/github-button'
|
|
12
14
|
|
|
13
15
|
interface RegisterForm {
|
|
14
16
|
name: string
|
|
@@ -149,6 +151,17 @@ export default function RegisterPage() {
|
|
|
149
151
|
</Button>
|
|
150
152
|
</form>
|
|
151
153
|
</Form>
|
|
154
|
+
<div className="mt-4 space-y-3">
|
|
155
|
+
<div className="relative">
|
|
156
|
+
<div className="absolute inset-0 flex items-center">
|
|
157
|
+
<Separator />
|
|
158
|
+
</div>
|
|
159
|
+
<div className="relative flex justify-center text-xs uppercase">
|
|
160
|
+
<span className="bg-card px-2 text-muted-foreground">o continuá con</span>
|
|
161
|
+
</div>
|
|
162
|
+
</div>
|
|
163
|
+
<GitHubButton />
|
|
164
|
+
</div>
|
|
152
165
|
<p className="mt-4 text-center text-sm text-muted-foreground">
|
|
153
166
|
¿Ya tenés cuenta?{' '}
|
|
154
167
|
<Link href="/login" className="underline underline-offset-4 hover:text-foreground">
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect, useRef, useState } from 'react'
|
|
4
|
+
import { useForm } from 'react-hook-form'
|
|
5
|
+
import { Building2, Pencil, Trash2, Plus } from 'lucide-react'
|
|
6
|
+
import { createClient, deleteClient, getClients, updateClient } from '@/lib/api/clients'
|
|
7
|
+
import type { Client, CreateClientDto, QueryClientDto } from '@/types/clients'
|
|
8
|
+
import { Button } from '@/components/ui/button'
|
|
9
|
+
import { Input } from '@/components/ui/input'
|
|
10
|
+
import { Badge } from '@/components/ui/badge'
|
|
11
|
+
import { Skeleton } from '@/components/ui/skeleton'
|
|
12
|
+
import { Sheet, SheetContent, SheetHeader, SheetTitle } from '@/components/ui/sheet'
|
|
13
|
+
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from '@/components/ui/alert-dialog'
|
|
14
|
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
|
15
|
+
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'
|
|
16
|
+
import { useToast } from '@/hooks/use-toast'
|
|
17
|
+
|
|
18
|
+
export default function ClientsPage() {
|
|
19
|
+
const { toast } = useToast()
|
|
20
|
+
const [clients, setClients] = useState<Client[]>([])
|
|
21
|
+
const [loading, setLoading] = useState(true)
|
|
22
|
+
const [search, setSearch] = useState('')
|
|
23
|
+
const [statusFilter, setStatusFilter] = useState<string>('all')
|
|
24
|
+
const [sheetOpen, setSheetOpen] = useState(false)
|
|
25
|
+
const [editing, setEditing] = useState<Client | null>(null)
|
|
26
|
+
const [deleteTarget, setDeleteTarget] = useState<Client | null>(null)
|
|
27
|
+
const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
28
|
+
|
|
29
|
+
const form = useForm<CreateClientDto>({
|
|
30
|
+
defaultValues: { name: '', email: '', phone: '', address: '', notes: '', status: 'active' },
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
const load = useCallback(async (q: QueryClientDto = {}) => {
|
|
34
|
+
setLoading(true)
|
|
35
|
+
try {
|
|
36
|
+
const res = await getClients(q)
|
|
37
|
+
setClients(res.data)
|
|
38
|
+
} catch {
|
|
39
|
+
toast({ title: 'Error al cargar clientes', variant: 'destructive' })
|
|
40
|
+
} finally {
|
|
41
|
+
setLoading(false)
|
|
42
|
+
}
|
|
43
|
+
}, [toast])
|
|
44
|
+
|
|
45
|
+
useEffect(() => { load() }, [load])
|
|
46
|
+
|
|
47
|
+
const handleSearch = (value: string) => {
|
|
48
|
+
setSearch(value)
|
|
49
|
+
if (debounceRef.current) clearTimeout(debounceRef.current)
|
|
50
|
+
debounceRef.current = setTimeout(() => {
|
|
51
|
+
load({ search: value || undefined, status: statusFilter !== 'all' ? (statusFilter as 'active' | 'archived') : undefined })
|
|
52
|
+
}, 300)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const handleStatusFilter = (value: string) => {
|
|
56
|
+
setStatusFilter(value)
|
|
57
|
+
load({ search: search || undefined, status: value !== 'all' ? (value as 'active' | 'archived') : undefined })
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const openCreate = () => {
|
|
61
|
+
setEditing(null)
|
|
62
|
+
form.reset({ name: '', email: '', phone: '', address: '', notes: '', status: 'active' })
|
|
63
|
+
setSheetOpen(true)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const openEdit = (client: Client) => {
|
|
67
|
+
setEditing(client)
|
|
68
|
+
form.reset({
|
|
69
|
+
name: client.name,
|
|
70
|
+
email: client.email ?? '',
|
|
71
|
+
phone: client.phone ?? '',
|
|
72
|
+
address: client.address ?? '',
|
|
73
|
+
notes: client.notes ?? '',
|
|
74
|
+
status: client.status,
|
|
75
|
+
})
|
|
76
|
+
setSheetOpen(true)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const onSubmit = async (data: CreateClientDto) => {
|
|
80
|
+
try {
|
|
81
|
+
if (editing) {
|
|
82
|
+
await updateClient(editing._id, data)
|
|
83
|
+
toast({ title: 'Cliente actualizado' })
|
|
84
|
+
} else {
|
|
85
|
+
await createClient(data)
|
|
86
|
+
toast({ title: 'Cliente creado' })
|
|
87
|
+
}
|
|
88
|
+
setSheetOpen(false)
|
|
89
|
+
load({ search: search || undefined, status: statusFilter !== 'all' ? (statusFilter as 'active' | 'archived') : undefined })
|
|
90
|
+
} catch {
|
|
91
|
+
toast({ title: 'Error al guardar cliente', variant: 'destructive' })
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const handleDelete = async () => {
|
|
96
|
+
if (!deleteTarget) return
|
|
97
|
+
try {
|
|
98
|
+
await deleteClient(deleteTarget._id)
|
|
99
|
+
toast({ title: 'Cliente eliminado' })
|
|
100
|
+
setDeleteTarget(null)
|
|
101
|
+
load()
|
|
102
|
+
} catch {
|
|
103
|
+
toast({ title: 'Error al eliminar cliente', variant: 'destructive' })
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return (
|
|
108
|
+
<div className="flex flex-col gap-6 p-6">
|
|
109
|
+
<div className="flex items-center justify-between">
|
|
110
|
+
<div className="flex items-center gap-2">
|
|
111
|
+
<Building2 className="size-5" />
|
|
112
|
+
<h1 className="text-xl font-semibold">Clientes</h1>
|
|
113
|
+
</div>
|
|
114
|
+
<Button onClick={openCreate}>
|
|
115
|
+
<Plus className="mr-2 size-4" />
|
|
116
|
+
Nuevo cliente
|
|
117
|
+
</Button>
|
|
118
|
+
</div>
|
|
119
|
+
|
|
120
|
+
<div className="flex gap-3">
|
|
121
|
+
<Input
|
|
122
|
+
placeholder="Buscar por nombre o email..."
|
|
123
|
+
value={search}
|
|
124
|
+
onChange={e => handleSearch(e.target.value)}
|
|
125
|
+
className="max-w-sm"
|
|
126
|
+
/>
|
|
127
|
+
<Select value={statusFilter} onValueChange={handleStatusFilter}>
|
|
128
|
+
<SelectTrigger className="w-44">
|
|
129
|
+
<SelectValue placeholder="Estado" />
|
|
130
|
+
</SelectTrigger>
|
|
131
|
+
<SelectContent>
|
|
132
|
+
<SelectItem value="all">Todos</SelectItem>
|
|
133
|
+
<SelectItem value="active">Activos</SelectItem>
|
|
134
|
+
<SelectItem value="archived">Archivados</SelectItem>
|
|
135
|
+
</SelectContent>
|
|
136
|
+
</Select>
|
|
137
|
+
</div>
|
|
138
|
+
|
|
139
|
+
<div className="rounded-lg border">
|
|
140
|
+
<table className="w-full text-sm">
|
|
141
|
+
<thead className="border-b bg-muted/40">
|
|
142
|
+
<tr>
|
|
143
|
+
<th className="px-4 py-3 text-left font-medium">Nombre</th>
|
|
144
|
+
<th className="px-4 py-3 text-left font-medium">Email</th>
|
|
145
|
+
<th className="px-4 py-3 text-left font-medium">Teléfono</th>
|
|
146
|
+
<th className="px-4 py-3 text-left font-medium">Estado</th>
|
|
147
|
+
<th className="px-4 py-3 text-left font-medium">Creado</th>
|
|
148
|
+
<th className="px-4 py-3 text-right font-medium">Acciones</th>
|
|
149
|
+
</tr>
|
|
150
|
+
</thead>
|
|
151
|
+
<tbody>
|
|
152
|
+
{loading ? (
|
|
153
|
+
Array.from({ length: 5 }).map((_, i) => (
|
|
154
|
+
<tr key={i} className="border-b">
|
|
155
|
+
{Array.from({ length: 6 }).map((__, j) => (
|
|
156
|
+
<td key={j} className="px-4 py-3"><Skeleton className="h-4 w-full" /></td>
|
|
157
|
+
))}
|
|
158
|
+
</tr>
|
|
159
|
+
))
|
|
160
|
+
) : clients.length === 0 ? (
|
|
161
|
+
<tr>
|
|
162
|
+
<td colSpan={6} className="px-4 py-12 text-center text-muted-foreground">
|
|
163
|
+
No hay clientes. Creá el primero.
|
|
164
|
+
</td>
|
|
165
|
+
</tr>
|
|
166
|
+
) : (
|
|
167
|
+
clients.map(client => (
|
|
168
|
+
<tr key={client._id} className="border-b last:border-0 hover:bg-muted/20">
|
|
169
|
+
<td className="px-4 py-3 font-medium">{client.name}</td>
|
|
170
|
+
<td className="px-4 py-3 text-muted-foreground">{client.email ?? '—'}</td>
|
|
171
|
+
<td className="px-4 py-3 text-muted-foreground">{client.phone ?? '—'}</td>
|
|
172
|
+
<td className="px-4 py-3">
|
|
173
|
+
<Badge variant={client.status === 'active' ? 'default' : 'secondary'}>
|
|
174
|
+
{client.status === 'active' ? 'Activo' : 'Archivado'}
|
|
175
|
+
</Badge>
|
|
176
|
+
</td>
|
|
177
|
+
<td className="px-4 py-3 text-muted-foreground">
|
|
178
|
+
{new Date(client.createdAt).toLocaleDateString('es-AR')}
|
|
179
|
+
</td>
|
|
180
|
+
<td className="px-4 py-3 text-right">
|
|
181
|
+
<Button variant="ghost" size="icon" onClick={() => openEdit(client)}>
|
|
182
|
+
<Pencil className="size-4" />
|
|
183
|
+
</Button>
|
|
184
|
+
<Button variant="ghost" size="icon" onClick={() => setDeleteTarget(client)}>
|
|
185
|
+
<Trash2 className="size-4 text-destructive" />
|
|
186
|
+
</Button>
|
|
187
|
+
</td>
|
|
188
|
+
</tr>
|
|
189
|
+
))
|
|
190
|
+
)}
|
|
191
|
+
</tbody>
|
|
192
|
+
</table>
|
|
193
|
+
</div>
|
|
194
|
+
|
|
195
|
+
{/* Sheet formulario */}
|
|
196
|
+
<Sheet open={sheetOpen} onOpenChange={setSheetOpen}>
|
|
197
|
+
<SheetContent className="w-full sm:max-w-md overflow-y-auto">
|
|
198
|
+
<SheetHeader>
|
|
199
|
+
<SheetTitle>{editing ? 'Editar cliente' : 'Nuevo cliente'}</SheetTitle>
|
|
200
|
+
</SheetHeader>
|
|
201
|
+
<Form {...form}>
|
|
202
|
+
<form onSubmit={form.handleSubmit(onSubmit)} className="mt-6 flex flex-col gap-4">
|
|
203
|
+
<FormField control={form.control} name="name" rules={{ required: 'El nombre es obligatorio' }}
|
|
204
|
+
render={({ field }) => (
|
|
205
|
+
<FormItem>
|
|
206
|
+
<FormLabel>Nombre *</FormLabel>
|
|
207
|
+
<FormControl><Input {...field} /></FormControl>
|
|
208
|
+
<FormMessage />
|
|
209
|
+
</FormItem>
|
|
210
|
+
)}
|
|
211
|
+
/>
|
|
212
|
+
<FormField control={form.control} name="email"
|
|
213
|
+
render={({ field }) => (
|
|
214
|
+
<FormItem>
|
|
215
|
+
<FormLabel>Email</FormLabel>
|
|
216
|
+
<FormControl><Input type="email" {...field} /></FormControl>
|
|
217
|
+
<FormMessage />
|
|
218
|
+
</FormItem>
|
|
219
|
+
)}
|
|
220
|
+
/>
|
|
221
|
+
<FormField control={form.control} name="phone"
|
|
222
|
+
render={({ field }) => (
|
|
223
|
+
<FormItem>
|
|
224
|
+
<FormLabel>Teléfono</FormLabel>
|
|
225
|
+
<FormControl><Input {...field} /></FormControl>
|
|
226
|
+
<FormMessage />
|
|
227
|
+
</FormItem>
|
|
228
|
+
)}
|
|
229
|
+
/>
|
|
230
|
+
<FormField control={form.control} name="address"
|
|
231
|
+
render={({ field }) => (
|
|
232
|
+
<FormItem>
|
|
233
|
+
<FormLabel>Dirección</FormLabel>
|
|
234
|
+
<FormControl><Input {...field} /></FormControl>
|
|
235
|
+
<FormMessage />
|
|
236
|
+
</FormItem>
|
|
237
|
+
)}
|
|
238
|
+
/>
|
|
239
|
+
<FormField control={form.control} name="notes"
|
|
240
|
+
render={({ field }) => (
|
|
241
|
+
<FormItem>
|
|
242
|
+
<FormLabel>Notas</FormLabel>
|
|
243
|
+
<FormControl><textarea className="w-full rounded-md border px-3 py-2 text-sm" rows={3} {...field} /></FormControl>
|
|
244
|
+
<FormMessage />
|
|
245
|
+
</FormItem>
|
|
246
|
+
)}
|
|
247
|
+
/>
|
|
248
|
+
<FormField control={form.control} name="status"
|
|
249
|
+
render={({ field }) => (
|
|
250
|
+
<FormItem>
|
|
251
|
+
<FormLabel>Estado</FormLabel>
|
|
252
|
+
<Select value={field.value} onValueChange={field.onChange}>
|
|
253
|
+
<FormControl>
|
|
254
|
+
<SelectTrigger><SelectValue /></SelectTrigger>
|
|
255
|
+
</FormControl>
|
|
256
|
+
<SelectContent>
|
|
257
|
+
<SelectItem value="active">Activo</SelectItem>
|
|
258
|
+
<SelectItem value="archived">Archivado</SelectItem>
|
|
259
|
+
</SelectContent>
|
|
260
|
+
</Select>
|
|
261
|
+
<FormMessage />
|
|
262
|
+
</FormItem>
|
|
263
|
+
)}
|
|
264
|
+
/>
|
|
265
|
+
<div className="flex justify-end gap-2 pt-2">
|
|
266
|
+
<Button type="button" variant="outline" onClick={() => setSheetOpen(false)}>Cancelar</Button>
|
|
267
|
+
<Button type="submit" disabled={form.formState.isSubmitting}>
|
|
268
|
+
{form.formState.isSubmitting ? 'Guardando...' : 'Guardar'}
|
|
269
|
+
</Button>
|
|
270
|
+
</div>
|
|
271
|
+
</form>
|
|
272
|
+
</Form>
|
|
273
|
+
</SheetContent>
|
|
274
|
+
</Sheet>
|
|
275
|
+
|
|
276
|
+
{/* AlertDialog eliminar */}
|
|
277
|
+
<AlertDialog open={!!deleteTarget} onOpenChange={open => !open && setDeleteTarget(null)}>
|
|
278
|
+
<AlertDialogContent>
|
|
279
|
+
<AlertDialogHeader>
|
|
280
|
+
<AlertDialogTitle>¿Eliminar cliente?</AlertDialogTitle>
|
|
281
|
+
<AlertDialogDescription>
|
|
282
|
+
Esta acción eliminará a <strong>{deleteTarget?.name}</strong>. Se puede recuperar más tarde.
|
|
283
|
+
</AlertDialogDescription>
|
|
284
|
+
</AlertDialogHeader>
|
|
285
|
+
<AlertDialogFooter>
|
|
286
|
+
<AlertDialogCancel>Cancelar</AlertDialogCancel>
|
|
287
|
+
<AlertDialogAction onClick={handleDelete} className="bg-destructive text-destructive-foreground hover:bg-destructive/90">
|
|
288
|
+
Eliminar
|
|
289
|
+
</AlertDialogAction>
|
|
290
|
+
</AlertDialogFooter>
|
|
291
|
+
</AlertDialogContent>
|
|
292
|
+
</AlertDialog>
|
|
293
|
+
</div>
|
|
294
|
+
)
|
|
295
|
+
}
|