ar-saas 0.4.3 → 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.
Files changed (117) hide show
  1. package/package.json +1 -1
  2. package/templates/backend/.env.example +13 -3
  3. package/templates/backend/README.md +22 -3
  4. package/templates/backend/package-lock.json +165 -2
  5. package/templates/backend/package.json +2 -0
  6. package/templates/backend/src/app.module.ts +14 -0
  7. package/templates/backend/src/common/guards/github-auth.guard.ts +5 -0
  8. package/templates/backend/src/main.ts +2 -2
  9. package/templates/backend/src/modules/auth/auth.controller.ts +51 -3
  10. package/templates/backend/src/modules/auth/auth.module.ts +2 -1
  11. package/templates/backend/src/modules/auth/auth.service.ts +96 -11
  12. package/templates/backend/src/modules/auth/strategies/github.strategy.ts +46 -0
  13. package/templates/backend/src/modules/clients/clients.controller.ts +91 -0
  14. package/templates/backend/src/modules/clients/clients.module.ts +16 -0
  15. package/templates/backend/src/modules/clients/clients.repository.ts +14 -0
  16. package/templates/backend/src/modules/clients/clients.service.ts +52 -0
  17. package/templates/backend/src/modules/clients/dto/create-client.dto.ts +40 -0
  18. package/templates/backend/src/modules/clients/dto/query-client.dto.ts +30 -0
  19. package/templates/backend/src/modules/clients/dto/update-client.dto.ts +4 -0
  20. package/templates/backend/src/modules/clients/schemas/client.schema.ts +32 -0
  21. package/templates/backend/src/modules/invoices/dto/create-invoice.dto.ts +79 -0
  22. package/templates/backend/src/modules/invoices/dto/invoice-item.dto.ts +23 -0
  23. package/templates/backend/src/modules/invoices/dto/query-invoice.dto.ts +40 -0
  24. package/templates/backend/src/modules/invoices/dto/update-invoice.dto.ts +4 -0
  25. package/templates/backend/src/modules/invoices/invoices.controller.ts +91 -0
  26. package/templates/backend/src/modules/invoices/invoices.module.ts +18 -0
  27. package/templates/backend/src/modules/invoices/invoices.repository.ts +14 -0
  28. package/templates/backend/src/modules/invoices/invoices.service.ts +104 -0
  29. package/templates/backend/src/modules/invoices/schemas/invoice.schema.ts +75 -0
  30. package/templates/backend/src/modules/notifications/dto/create-notification.dto.ts +45 -0
  31. package/templates/backend/src/modules/notifications/dto/query-notification.dto.ts +30 -0
  32. package/templates/backend/src/modules/notifications/dto/update-notification.dto.ts +4 -0
  33. package/templates/backend/src/modules/notifications/notifications.controller.ts +119 -0
  34. package/templates/backend/src/modules/notifications/notifications.module.ts +16 -0
  35. package/templates/backend/src/modules/notifications/notifications.repository.ts +31 -0
  36. package/templates/backend/src/modules/notifications/notifications.service.ts +64 -0
  37. package/templates/backend/src/modules/notifications/schemas/notification.schema.ts +38 -0
  38. package/templates/backend/src/modules/pipeline/dto/create-deal.dto.ts +40 -0
  39. package/templates/backend/src/modules/pipeline/dto/query-deal.dto.ts +35 -0
  40. package/templates/backend/src/modules/pipeline/dto/update-deal.dto.ts +4 -0
  41. package/templates/backend/src/modules/pipeline/pipeline.controller.ts +91 -0
  42. package/templates/backend/src/modules/pipeline/pipeline.module.ts +18 -0
  43. package/templates/backend/src/modules/pipeline/pipeline.repository.ts +14 -0
  44. package/templates/backend/src/modules/pipeline/pipeline.service.ts +64 -0
  45. package/templates/backend/src/modules/pipeline/schemas/deal.schema.ts +39 -0
  46. package/templates/backend/src/modules/planner/dto/create-planner-block.dto.ts +66 -0
  47. package/templates/backend/src/modules/planner/dto/query-planner-block.dto.ts +48 -0
  48. package/templates/backend/src/modules/planner/dto/update-block-status.dto.ts +10 -0
  49. package/templates/backend/src/modules/planner/dto/update-planner-block.dto.ts +4 -0
  50. package/templates/backend/src/modules/planner/planner.controller.ts +124 -0
  51. package/templates/backend/src/modules/planner/planner.module.ts +16 -0
  52. package/templates/backend/src/modules/planner/planner.repository.ts +45 -0
  53. package/templates/backend/src/modules/planner/planner.service.ts +104 -0
  54. package/templates/backend/src/modules/planner/schemas/planner-block.schema.ts +56 -0
  55. package/templates/backend/src/modules/task-columns/dto/create-task-column.dto.ts +20 -0
  56. package/templates/backend/src/modules/task-columns/dto/reorder-columns.dto.ts +9 -0
  57. package/templates/backend/src/modules/task-columns/dto/update-task-column.dto.ts +4 -0
  58. package/templates/backend/src/modules/task-columns/schemas/task-column.schema.ts +21 -0
  59. package/templates/backend/src/modules/task-columns/task-columns.controller.ts +86 -0
  60. package/templates/backend/src/modules/task-columns/task-columns.module.ts +16 -0
  61. package/templates/backend/src/modules/task-columns/task-columns.repository.ts +15 -0
  62. package/templates/backend/src/modules/task-columns/task-columns.service.ts +49 -0
  63. package/templates/backend/src/modules/tasks/dto/checklist-item.dto.ts +13 -0
  64. package/templates/backend/src/modules/tasks/dto/create-task.dto.ts +67 -0
  65. package/templates/backend/src/modules/tasks/dto/label.dto.ts +12 -0
  66. package/templates/backend/src/modules/tasks/dto/move-task.dto.ts +15 -0
  67. package/templates/backend/src/modules/tasks/dto/query-task.dto.ts +40 -0
  68. package/templates/backend/src/modules/tasks/dto/update-task.dto.ts +4 -0
  69. package/templates/backend/src/modules/tasks/schemas/task.schema.ts +66 -0
  70. package/templates/backend/src/modules/tasks/tasks.controller.ts +104 -0
  71. package/templates/backend/src/modules/tasks/tasks.module.ts +18 -0
  72. package/templates/backend/src/modules/tasks/tasks.repository.ts +14 -0
  73. package/templates/backend/src/modules/tasks/tasks.service.ts +76 -0
  74. package/templates/backend/src/modules/users/schemas/user.schema.ts +3 -0
  75. package/templates/backend/src/modules/users/users.repository.ts +8 -0
  76. package/templates/backend/src/modules/users/users.service.ts +34 -0
  77. package/templates/frontend/.env.local.example +1 -1
  78. package/templates/frontend/README.md +43 -1
  79. package/templates/frontend/package.json +48 -45
  80. package/templates/frontend/pnpm-lock.yaml +5096 -5012
  81. package/templates/frontend/src/app/(auth)/layout.tsx +7 -1
  82. package/templates/frontend/src/app/(auth)/login/page.tsx +13 -0
  83. package/templates/frontend/src/app/(auth)/register/page.tsx +13 -0
  84. package/templates/frontend/src/app/(dashboard)/clients/page.tsx +295 -0
  85. package/templates/frontend/src/app/(dashboard)/invoices/page.tsx +305 -0
  86. package/templates/frontend/src/app/(dashboard)/notifications/page.tsx +173 -0
  87. package/templates/frontend/src/app/(dashboard)/pipeline/page.tsx +244 -0
  88. package/templates/frontend/src/app/(dashboard)/planner/page.tsx +287 -0
  89. package/templates/frontend/src/app/(dashboard)/settings/page.tsx +165 -128
  90. package/templates/frontend/src/app/(dashboard)/tasks/page.tsx +366 -0
  91. package/templates/frontend/src/app/auth/github/callback/page.tsx +82 -0
  92. package/templates/frontend/src/app/landing/page.tsx +21 -0
  93. package/templates/frontend/src/app/page.tsx +5 -5
  94. package/templates/frontend/src/app/setup/page.tsx +15 -14
  95. package/templates/frontend/src/components/auth/github-button.tsx +25 -0
  96. package/templates/frontend/src/components/dashboard/sidebar.tsx +90 -71
  97. package/templates/frontend/src/components/ui/alert-dialog.tsx +141 -0
  98. package/templates/frontend/src/components/ui/button.tsx +56 -52
  99. package/templates/frontend/src/components/ui/popover.tsx +31 -0
  100. package/templates/frontend/src/components/ui/select.tsx +160 -0
  101. package/templates/frontend/src/components/ui/sheet.tsx +140 -0
  102. package/templates/frontend/src/lib/api/auth.ts +7 -0
  103. package/templates/frontend/src/lib/api/clients.ts +17 -0
  104. package/templates/frontend/src/lib/api/invoices.ts +18 -0
  105. package/templates/frontend/src/lib/api/notifications.ts +27 -0
  106. package/templates/frontend/src/lib/api/pipeline.ts +18 -0
  107. package/templates/frontend/src/lib/api/planner.ts +26 -0
  108. package/templates/frontend/src/lib/api/task-columns.ts +17 -0
  109. package/templates/frontend/src/lib/api/tasks.ts +21 -0
  110. package/templates/frontend/src/lib/hooks/use-unread-notifications.ts +23 -0
  111. package/templates/frontend/src/providers/auth-provider.tsx +7 -1
  112. package/templates/frontend/src/types/clients.ts +38 -0
  113. package/templates/frontend/src/types/invoices.ts +51 -0
  114. package/templates/frontend/src/types/notifications.ts +30 -0
  115. package/templates/frontend/src/types/pipeline.ts +35 -0
  116. package/templates/frontend/src/types/planner.ts +49 -0
  117. package/templates/frontend/src/types/tasks.ts +65 -0
@@ -1,155 +1,192 @@
1
1
  'use client'
2
2
 
3
- import { useState } from 'react'
4
- import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
5
- import { Switch } from '@/components/ui/switch'
6
- import { Separator } from '@/components/ui/separator'
3
+ import { useEffect } from 'react'
4
+ import { useForm } from 'react-hook-form'
5
+ import { Settings, User, Lock, Building2 } from 'lucide-react'
6
+ import apiClient from '@/lib/api/client'
7
+ import { useAuth } from '@/lib/hooks/use-auth'
7
8
  import { Button } from '@/components/ui/button'
8
- import { Label } from '@/components/ui/label'
9
- import { Badge } from '@/components/ui/badge'
10
-
11
- interface NotificationSettings {
12
- emailMarketing: boolean
13
- emailProduct: boolean
14
- emailSecurity: boolean
15
- pushAll: boolean
9
+ import { Input } from '@/components/ui/input'
10
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
11
+ import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'
12
+ import { Separator } from '@/components/ui/separator'
13
+ import { useToast } from '@/hooks/use-toast'
14
+
15
+ interface ProfileForm {
16
+ name: string
17
+ phone: string
18
+ }
19
+
20
+ interface WorkspaceForm {
21
+ name: string
16
22
  }
17
23
 
18
- interface WorkspaceSettings {
19
- publicProfile: boolean
20
- allowInvites: boolean
24
+ interface PasswordForm {
25
+ currentPassword: string
26
+ newPassword: string
27
+ confirmPassword: string
21
28
  }
22
29
 
23
30
  export default function SettingsPage() {
24
- const [notifications, setNotifications] = useState<NotificationSettings>({
25
- emailMarketing: false,
26
- emailProduct: true,
27
- emailSecurity: true,
28
- pushAll: false,
31
+ const { toast } = useToast()
32
+ const { user, refreshUser } = useAuth()
33
+
34
+ const profileForm = useForm<ProfileForm>({
35
+ defaultValues: { name: '', phone: '' },
36
+ })
37
+
38
+ const workspaceForm = useForm<WorkspaceForm>({
39
+ defaultValues: { name: '' },
29
40
  })
30
41
 
31
- const [workspace, setWorkspace] = useState<WorkspaceSettings>({
32
- publicProfile: false,
33
- allowInvites: true,
42
+ const passwordForm = useForm<PasswordForm>({
43
+ defaultValues: { currentPassword: '', newPassword: '', confirmPassword: '' },
34
44
  })
35
45
 
36
- function toggleNotification(key: keyof NotificationSettings) {
37
- setNotifications((prev) => ({ ...prev, [key]: !prev[key] }))
46
+ useEffect(() => {
47
+ if (user) {
48
+ profileForm.reset({ name: user.name ?? '', phone: '' })
49
+ }
50
+ }, [user, profileForm])
51
+
52
+ useEffect(() => {
53
+ const fetchWorkspace = async () => {
54
+ try {
55
+ const ws = await apiClient.get('/api/workspaces/me') as { name: string }
56
+ workspaceForm.reset({ name: ws.name ?? '' })
57
+ } catch { /* workspace may not exist yet */ }
58
+ }
59
+ fetchWorkspace()
60
+ }, [workspaceForm])
61
+
62
+ const onProfileSubmit = async (data: ProfileForm) => {
63
+ try {
64
+ await apiClient.patch('/api/users/me', data)
65
+ await refreshUser()
66
+ toast({ title: 'Perfil actualizado' })
67
+ } catch {
68
+ toast({ title: 'Error al actualizar perfil', variant: 'destructive' })
69
+ }
70
+ }
71
+
72
+ const onWorkspaceSubmit = async (data: WorkspaceForm) => {
73
+ try {
74
+ await apiClient.patch('/api/workspaces/me', data)
75
+ toast({ title: 'Workspace actualizado' })
76
+ } catch {
77
+ toast({ title: 'Error al actualizar workspace', variant: 'destructive' })
78
+ }
38
79
  }
39
80
 
40
- function toggleWorkspace(key: keyof WorkspaceSettings) {
41
- setWorkspace((prev) => ({ ...prev, [key]: !prev[key] }))
81
+ const onPasswordSubmit = async (data: PasswordForm) => {
82
+ if (data.newPassword !== data.confirmPassword) {
83
+ passwordForm.setError('confirmPassword', { message: 'Las contraseñas no coinciden' })
84
+ return
85
+ }
86
+ try {
87
+ await apiClient.patch('/api/users/me', {
88
+ currentPassword: data.currentPassword,
89
+ newPassword: data.newPassword,
90
+ })
91
+ passwordForm.reset()
92
+ toast({ title: 'Contraseña actualizada' })
93
+ } catch {
94
+ toast({ title: 'Error al cambiar contraseña', variant: 'destructive' })
95
+ }
42
96
  }
43
97
 
44
98
  return (
45
- <div className="max-w-2xl space-y-6">
46
- <div className="rounded-lg border border-amber-200 bg-amber-50 p-3">
47
- <p className="text-xs text-amber-800 font-medium">Próximamente</p>
48
- <p className="text-xs text-amber-700 mt-0.5">La configuración de notificaciones y workspace estará disponible en la próxima actualización.</p>
99
+ <div className="flex flex-col gap-6 p-6 max-w-2xl">
100
+ <div className="flex items-center gap-2">
101
+ <Settings className="size-5" />
102
+ <h1 className="text-xl font-semibold">Configuración</h1>
49
103
  </div>
50
104
 
51
- {/* Notificaciones */}
52
- <Card>
53
- <CardHeader>
54
- <CardTitle>Notificaciones por email</CardTitle>
55
- <CardDescription>Elegí qué correos querés recibir de nuestra parte.</CardDescription>
56
- </CardHeader>
57
- <CardContent className="space-y-4">
58
- {[
59
- {
60
- key: 'emailSecurity' as const,
61
- label: 'Seguridad',
62
- description: 'Alertas de inicio de sesión y cambios de contraseña.',
63
- badge: 'Recomendado',
64
- },
65
- {
66
- key: 'emailProduct' as const,
67
- label: 'Actualizaciones del producto',
68
- description: 'Nuevas funcionalidades, mejoras y fixes importantes.',
69
- badge: null,
70
- },
71
- {
72
- key: 'emailMarketing' as const,
73
- label: 'Marketing y promociones',
74
- description: 'Ofertas, descuentos y contenido educativo.',
75
- badge: null,
76
- },
77
- ].map(({ key, label, description, badge }) => (
78
- <div key={key} className="flex items-start justify-between gap-4">
79
- <div className="space-y-0.5">
80
- <div className="flex items-center gap-2">
81
- <Label className="text-sm font-medium">{label}</Label>
82
- {badge && <Badge variant="secondary" className="text-xs">{badge}</Badge>}
105
+ <Tabs defaultValue="profile">
106
+ <TabsList>
107
+ <TabsTrigger value="profile">
108
+ <User className="mr-2 size-4" />Perfil
109
+ </TabsTrigger>
110
+ <TabsTrigger value="security">
111
+ <Lock className="mr-2 size-4" />Seguridad
112
+ </TabsTrigger>
113
+ <TabsTrigger value="workspace">
114
+ <Building2 className="mr-2 size-4" />Workspace
115
+ </TabsTrigger>
116
+ </TabsList>
117
+
118
+ {/* Perfil */}
119
+ <TabsContent value="profile" className="mt-6">
120
+ <div className="rounded-xl border p-6">
121
+ <h2 className="text-base font-semibold mb-4">Información de perfil</h2>
122
+ <Form {...profileForm}>
123
+ <form onSubmit={profileForm.handleSubmit(onProfileSubmit)} className="flex flex-col gap-4">
124
+ <FormField control={profileForm.control} name="name" rules={{ required: 'El nombre es obligatorio' }} render={({ field }) => (
125
+ <FormItem><FormLabel>Nombre</FormLabel><FormControl><Input {...field} /></FormControl><FormMessage /></FormItem>
126
+ )} />
127
+ <FormItem>
128
+ <FormLabel>Email</FormLabel>
129
+ <Input value={user?.email ?? ''} readOnly className="bg-muted/40 cursor-not-allowed" />
130
+ <p className="text-xs text-muted-foreground">El email no se puede modificar.</p>
131
+ </FormItem>
132
+ <FormField control={profileForm.control} name="phone" render={({ field }) => (
133
+ <FormItem><FormLabel>Teléfono</FormLabel><FormControl><Input {...field} /></FormControl></FormItem>
134
+ )} />
135
+ <div className="flex justify-end">
136
+ <Button type="submit" disabled={profileForm.formState.isSubmitting}>
137
+ {profileForm.formState.isSubmitting ? 'Guardando...' : 'Guardar cambios'}
138
+ </Button>
83
139
  </div>
84
- <p className="text-xs text-muted-foreground">{description}</p>
85
- </div>
86
- <Switch
87
- checked={notifications[key]}
88
- onCheckedChange={() => toggleNotification(key)}
89
- disabled
90
- />
91
- </div>
92
- ))}
93
- </CardContent>
94
- </Card>
95
-
96
- {/* Workspace */}
97
- <Card>
98
- <CardHeader>
99
- <CardTitle>Workspace</CardTitle>
100
- <CardDescription>Configuración de tu espacio de trabajo.</CardDescription>
101
- </CardHeader>
102
- <CardContent className="space-y-4">
103
- <div className="flex items-start justify-between gap-4">
104
- <div className="space-y-0.5">
105
- <Label className="text-sm font-medium">Perfil público</Label>
106
- <p className="text-xs text-muted-foreground">
107
- Permite que otros usuarios encuentren tu perfil.
108
- </p>
109
- </div>
110
- <Switch
111
- checked={workspace.publicProfile}
112
- onCheckedChange={() => toggleWorkspace('publicProfile')}
113
- disabled
114
- />
140
+ </form>
141
+ </Form>
115
142
  </div>
143
+ </TabsContent>
116
144
 
117
- <Separator />
118
-
119
- <div className="flex items-start justify-between gap-4">
120
- <div className="space-y-0.5">
121
- <Label className="text-sm font-medium">Invitaciones de equipo</Label>
122
- <p className="text-xs text-muted-foreground">
123
- Permite que otros te inviten a sus workspaces.
124
- </p>
125
- </div>
126
- <Switch
127
- checked={workspace.allowInvites}
128
- onCheckedChange={() => toggleWorkspace('allowInvites')}
129
- disabled
130
- />
131
- </div>
132
- </CardContent>
133
- </Card>
134
-
135
- {/* Danger zone */}
136
- <Card className="border-destructive/40">
137
- <CardHeader>
138
- <CardTitle className="text-destructive">Zona peligrosa</CardTitle>
139
- <CardDescription>Acciones irreversibles sobre tu cuenta.</CardDescription>
140
- </CardHeader>
141
- <CardContent className="space-y-3">
142
- <div className="flex items-center justify-between">
143
- <div>
144
- <p className="text-sm font-medium">Exportar mis datos</p>
145
- <p className="text-xs text-muted-foreground">Descargá toda tu información en formato JSON.</p>
146
- </div>
147
- <Button variant="outline" size="sm" disabled>Exportar</Button>
145
+ {/* Seguridad */}
146
+ <TabsContent value="security" className="mt-6">
147
+ <div className="rounded-xl border p-6">
148
+ <h2 className="text-base font-semibold mb-4">Cambiar contraseña</h2>
149
+ <Form {...passwordForm}>
150
+ <form onSubmit={passwordForm.handleSubmit(onPasswordSubmit)} className="flex flex-col gap-4">
151
+ <FormField control={passwordForm.control} name="currentPassword" rules={{ required: 'Requerida' }} render={({ field }) => (
152
+ <FormItem><FormLabel>Contraseña actual</FormLabel><FormControl><Input type="password" {...field} /></FormControl><FormMessage /></FormItem>
153
+ )} />
154
+ <Separator />
155
+ <FormField control={passwordForm.control} name="newPassword" rules={{ required: 'Requerida', minLength: { value: 8, message: 'Mínimo 8 caracteres' } }} render={({ field }) => (
156
+ <FormItem><FormLabel>Nueva contraseña</FormLabel><FormControl><Input type="password" {...field} /></FormControl><FormMessage /></FormItem>
157
+ )} />
158
+ <FormField control={passwordForm.control} name="confirmPassword" rules={{ required: 'Requerida' }} render={({ field }) => (
159
+ <FormItem><FormLabel>Confirmar contraseña</FormLabel><FormControl><Input type="password" {...field} /></FormControl><FormMessage /></FormItem>
160
+ )} />
161
+ <div className="flex justify-end">
162
+ <Button type="submit" disabled={passwordForm.formState.isSubmitting}>
163
+ {passwordForm.formState.isSubmitting ? 'Actualizando...' : 'Cambiar contraseña'}
164
+ </Button>
165
+ </div>
166
+ </form>
167
+ </Form>
148
168
  </div>
149
- </CardContent>
150
- </Card>
169
+ </TabsContent>
151
170
 
152
- <Button disabled>Guardar ajustes</Button>
171
+ {/* Workspace */}
172
+ <TabsContent value="workspace" className="mt-6">
173
+ <div className="rounded-xl border p-6">
174
+ <h2 className="text-base font-semibold mb-4">Configuración del workspace</h2>
175
+ <Form {...workspaceForm}>
176
+ <form onSubmit={workspaceForm.handleSubmit(onWorkspaceSubmit)} className="flex flex-col gap-4">
177
+ <FormField control={workspaceForm.control} name="name" rules={{ required: 'El nombre es obligatorio' }} render={({ field }) => (
178
+ <FormItem><FormLabel>Nombre del workspace</FormLabel><FormControl><Input {...field} /></FormControl><FormMessage /></FormItem>
179
+ )} />
180
+ <div className="flex justify-end">
181
+ <Button type="submit" disabled={workspaceForm.formState.isSubmitting}>
182
+ {workspaceForm.formState.isSubmitting ? 'Guardando...' : 'Guardar cambios'}
183
+ </Button>
184
+ </div>
185
+ </form>
186
+ </Form>
187
+ </div>
188
+ </TabsContent>
189
+ </Tabs>
153
190
  </div>
154
191
  )
155
192
  }