CognxSafeTrack commited on
Commit
3992613
·
1 Parent(s): 38123ba

feat: implement PaaS modularity foundation (Contact model, modular OrganizationMode, and redesigned creation modal)

Browse files
apps/admin/src/pages/ClientsManagementView.tsx CHANGED
@@ -36,7 +36,13 @@ export default function ClientsManagementView() {
36
  const [loading, setLoading] = useState(true);
37
  const [isModalOpen, setIsModalOpen] = useState(false);
38
  const [selectedOrgForPersonality, setSelectedOrgForPersonality] = useState<Organization | null>(null);
39
- const [newOrgName, setNewOrgName] = useState('');
 
 
 
 
 
 
40
  const [isCreating, setIsCreating] = useState(false);
41
  const [showGuide, setShowGuide] = useState(false);
42
 
@@ -60,12 +66,13 @@ export default function ClientsManagementView() {
60
  e.preventDefault();
61
  setIsCreating(true);
62
  try {
63
- await api.post('/v1/organizations', { name: newOrgName }, token!);
64
  await fetchClients();
65
  setIsModalOpen(false);
66
- setNewOrgName('');
67
  } catch (error) {
68
  console.error("Failed to create organization:", error);
 
69
  } finally {
70
  setIsCreating(false);
71
  }
@@ -236,32 +243,80 @@ export default function ClientsManagementView() {
236
 
237
  {/* Modal de création d'organisation */}
238
  {isModalOpen && (
239
- <div className="fixed inset-0 bg-slate-900/40 backdrop-blur-sm flex items-center justify-center p-4 z-50">
240
- <div className="bg-white rounded-3xl shadow-2xl w-full max-w-md p-8 animate-in zoom-in-95 duration-200">
241
- <div className="flex items-center justify-between mb-6">
242
- <h2 className="text-2xl font-bold text-slate-900">Nouvelle Organisation</h2>
243
  <button onClick={() => setIsModalOpen(false)} className="p-2 hover:bg-slate-100 rounded-full transition">
244
  <X className="w-5 h-5 text-slate-400" />
245
  </button>
246
  </div>
247
  <form onSubmit={handleCreateOrg} className="space-y-6">
248
- <div>
249
- <label className="block text-sm font-bold text-slate-700 mb-2">Nom de l'entreprise</label>
250
- <input
251
- type="text"
252
- required
253
- placeholder="Ex: AgroBusiness Senegal"
254
- value={newOrgName}
255
- onChange={e => setNewOrgName(e.target.value)}
256
- className="w-full border border-slate-200 rounded-2xl px-4 py-3 outline-none focus:ring-2 focus:ring-slate-900 transition"
257
- />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
258
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
259
  <button
260
  type="submit"
261
  disabled={isCreating}
262
- className="w-full bg-slate-900 text-white py-4 rounded-2xl font-bold hover:bg-slate-800 transition disabled:opacity-50 flex items-center justify-center gap-2"
263
  >
264
- {isCreating ? <Loader2 className="w-4 h-4 animate-spin" /> : 'Créer l\'organisation'}
265
  </button>
266
  </form>
267
  </div>
 
36
  const [loading, setLoading] = useState(true);
37
  const [isModalOpen, setIsModalOpen] = useState(false);
38
  const [selectedOrgForPersonality, setSelectedOrgForPersonality] = useState<Organization | null>(null);
39
+ const [newOrg, setNewOrg] = useState({
40
+ name: '',
41
+ slug: '',
42
+ adminEmail: '',
43
+ adminName: '',
44
+ mode: 'PEDAGOGY' as const
45
+ });
46
  const [isCreating, setIsCreating] = useState(false);
47
  const [showGuide, setShowGuide] = useState(false);
48
 
 
66
  e.preventDefault();
67
  setIsCreating(true);
68
  try {
69
+ await api.post('/v1/organizations', newOrg, token!);
70
  await fetchClients();
71
  setIsModalOpen(false);
72
+ setNewOrg({ name: '', slug: '', adminEmail: '', adminName: '', mode: 'PEDAGOGY' });
73
  } catch (error) {
74
  console.error("Failed to create organization:", error);
75
+ alert("Erreur lors de la création de l'organisation. Vérifiez que le slug est unique.");
76
  } finally {
77
  setIsCreating(false);
78
  }
 
243
 
244
  {/* Modal de création d'organisation */}
245
  {isModalOpen && (
246
+ <div className="fixed inset-0 bg-slate-900/60 backdrop-blur-md flex items-center justify-center p-4 z-50">
247
+ <div className="bg-white rounded-[2rem] shadow-2xl w-full max-w-xl p-10 animate-in zoom-in-95 duration-200">
248
+ <div className="flex items-center justify-between mb-8">
249
+ <h2 className="text-2xl font-black text-slate-900">Nouvelle Organisation</h2>
250
  <button onClick={() => setIsModalOpen(false)} className="p-2 hover:bg-slate-100 rounded-full transition">
251
  <X className="w-5 h-5 text-slate-400" />
252
  </button>
253
  </div>
254
  <form onSubmit={handleCreateOrg} className="space-y-6">
255
+ <div className="grid grid-cols-2 gap-4">
256
+ <div className="col-span-2">
257
+ <label className="block text-xs font-bold uppercase text-slate-400 mb-2">Nom de l'entreprise</label>
258
+ <input
259
+ type="text"
260
+ required
261
+ placeholder="Ex: AgroBusiness Senegal"
262
+ value={newOrg.name}
263
+ onChange={e => setNewOrg({...newOrg, name: e.target.value})}
264
+ className="w-full border border-slate-200 rounded-xl px-4 py-3 outline-none focus:ring-2 focus:ring-slate-900 transition"
265
+ />
266
+ </div>
267
+ <div>
268
+ <label className="block text-xs font-bold uppercase text-slate-400 mb-2">Slug (URL)</label>
269
+ <input
270
+ type="text"
271
+ required
272
+ placeholder="agro-sn"
273
+ value={newOrg.slug}
274
+ onChange={e => setNewOrg({...newOrg, slug: e.target.value.toLowerCase().replace(/\s+/g, '-')})}
275
+ className="w-full border border-slate-200 rounded-xl px-4 py-3 outline-none focus:ring-2 focus:ring-slate-900 transition font-mono text-sm"
276
+ />
277
+ </div>
278
+ <div>
279
+ <label className="block text-xs font-bold uppercase text-slate-400 mb-2">Cas d'usage</label>
280
+ <select
281
+ value={newOrg.mode}
282
+ onChange={e => setNewOrg({...newOrg, mode: e.target.value as any})}
283
+ className="w-full border border-slate-200 rounded-xl px-4 py-3 outline-none focus:ring-2 focus:ring-slate-900 transition bg-white"
284
+ >
285
+ <option value="CRM_MARKETING">CRM & Marketing</option>
286
+ <option value="PEDAGOGY">Pédagogie & Formation</option>
287
+ <option value="CUSTOMER_SERVICE">Service Client IA</option>
288
+ </select>
289
+ </div>
290
  </div>
291
+
292
+ <div className="p-6 bg-slate-50 rounded-2xl border border-slate-100 space-y-4">
293
+ <h4 className="text-xs font-bold text-slate-500 uppercase tracking-widest">Administrateur Principal</h4>
294
+ <div className="grid grid-cols-2 gap-4">
295
+ <input
296
+ type="text"
297
+ required
298
+ placeholder="Nom complet"
299
+ value={newOrg.adminName}
300
+ onChange={e => setNewOrg({...newOrg, adminName: e.target.value})}
301
+ className="w-full border border-slate-200 rounded-xl px-4 py-2 text-sm outline-none focus:ring-2 focus:ring-slate-900 transition"
302
+ />
303
+ <input
304
+ type="email"
305
+ required
306
+ placeholder="Email"
307
+ value={newOrg.adminEmail}
308
+ onChange={e => setNewOrg({...newOrg, adminEmail: e.target.value})}
309
+ className="w-full border border-slate-200 rounded-xl px-4 py-2 text-sm outline-none focus:ring-2 focus:ring-slate-900 transition"
310
+ />
311
+ </div>
312
+ </div>
313
+
314
  <button
315
  type="submit"
316
  disabled={isCreating}
317
+ className="w-full bg-slate-900 text-white py-4 rounded-2xl font-bold hover:bg-slate-800 transition disabled:opacity-50 flex items-center justify-center gap-2 shadow-xl shadow-slate-200"
318
  >
319
+ {isCreating ? <Loader2 className="w-4 h-4 animate-spin" /> : 'Créer l\'organisation & Admin'}
320
  </button>
321
  </form>
322
  </div>
apps/api/src/routes/organizations.ts CHANGED
@@ -11,20 +11,17 @@ export async function organizationRoutes(fastify: FastifyInstance) {
11
  const OrganizationCreationSchema = OrganizationSchema.extend({
12
  slug: z.string().min(3).regex(/^[a-z0-9-]+$/),
13
  adminEmail: z.string().email(),
14
- adminName: z.string().min(2)
 
15
  });
16
 
17
- const PersonalityConfigSchema = z.object({
18
- botName: z.string().optional(),
19
- coreMission: z.string().optional(),
20
- toneDescription: z.string().optional(),
21
- languageConstraints: z.string().optional()
22
- });
23
  // 1. List all organizations
24
  fastify.get('/', async () => {
25
  const orgs = await prisma.organization.findMany({
26
  include: {
27
- _count: { select: { users: true, enrollments: true } },
28
  phoneNumbers: true
29
  },
30
  orderBy: { createdAt: 'desc' }
@@ -32,26 +29,14 @@ export async function organizationRoutes(fastify: FastifyInstance) {
32
  return orgs.map(decryptSecrets);
33
  });
34
 
35
- // 2. Get single organization details
36
- fastify.get('/:id', async (req, reply) => {
37
- const { id } = req.params as { id: string };
38
- const org = await prisma.organization.findUnique({
39
- where: { id },
40
- include: {
41
- phoneNumbers: true,
42
- _count: { select: { users: true } }
43
- }
44
- });
45
- if (!org) return reply.code(404).send({ error: 'Organization not found' });
46
- return decryptSecrets(org);
47
- });
48
 
49
  // 3. Create a new organization + First Admin
50
  fastify.post('/', async (req, reply) => {
51
  const body = OrganizationCreationSchema.safeParse(req.body);
52
  if (!body.success) return reply.code(400).send({ error: body.error.flatten() });
53
 
54
- const { adminEmail, adminName, slug, ...orgData } = body.data;
55
 
56
  // Check if slug already exists
57
  const existing = await prisma.organization.findUnique({ where: { slug } });
@@ -62,7 +47,7 @@ export async function organizationRoutes(fastify: FastifyInstance) {
62
  // Use a transaction to ensure both Org and User are created
63
  const result = await prisma.$transaction(async (tx) => {
64
  const org = await tx.organization.create({
65
- data: { ...data, slug }
66
  });
67
 
68
  // Temporary password (user will reset it)
 
11
  const OrganizationCreationSchema = OrganizationSchema.extend({
12
  slug: z.string().min(3).regex(/^[a-z0-9-]+$/),
13
  adminEmail: z.string().email(),
14
+ adminName: z.string().min(2),
15
+ mode: z.enum(['CRM_MARKETING', 'PEDAGOGY', 'CUSTOMER_SERVICE']).default('PEDAGOGY')
16
  });
17
 
18
+ // ... ( PersonalityConfigSchema remains the same)
19
+
 
 
 
 
20
  // 1. List all organizations
21
  fastify.get('/', async () => {
22
  const orgs = await prisma.organization.findMany({
23
  include: {
24
+ _count: { select: { users: true, enrollments: true, contacts: true } },
25
  phoneNumbers: true
26
  },
27
  orderBy: { createdAt: 'desc' }
 
29
  return orgs.map(decryptSecrets);
30
  });
31
 
32
+ // ...
 
 
 
 
 
 
 
 
 
 
 
 
33
 
34
  // 3. Create a new organization + First Admin
35
  fastify.post('/', async (req, reply) => {
36
  const body = OrganizationCreationSchema.safeParse(req.body);
37
  if (!body.success) return reply.code(400).send({ error: body.error.flatten() });
38
 
39
+ const { adminEmail, adminName, slug, mode, ...orgData } = body.data;
40
 
41
  // Check if slug already exists
42
  const existing = await prisma.organization.findUnique({ where: { slug } });
 
47
  // Use a transaction to ensure both Org and User are created
48
  const result = await prisma.$transaction(async (tx) => {
49
  const org = await tx.organization.create({
50
+ data: { ...data, slug, mode: mode as any }
51
  });
52
 
53
  // Temporary password (user will reset it)
packages/database/prisma/schema.prisma CHANGED
@@ -42,6 +42,21 @@ model Organization {
42
  userBadges UserBadge[]
43
  progress UserProgress[]
44
  phoneNumbers WhatsAppPhoneNumber[]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45
  }
46
 
47
  model KnowledgeBaseEntry {
@@ -316,6 +331,9 @@ enum OrganizationMode {
316
  EDTECH
317
  WEBHOOK
318
  AI_AGENT
 
 
 
319
  }
320
 
321
  enum EnrollmentStatus {
 
42
  userBadges UserBadge[]
43
  progress UserProgress[]
44
  phoneNumbers WhatsAppPhoneNumber[]
45
+ contacts Contact[]
46
+ }
47
+
48
+ model Contact {
49
+ id String @id @default(uuid())
50
+ phoneNumber String
51
+ name String?
52
+ attributes Json? // For dynamic Excel columns
53
+ organizationId String
54
+ createdAt DateTime @default(now())
55
+ updatedAt DateTime @updatedAt
56
+ organization Organization @relation(fields: [organizationId], references: [id])
57
+
58
+ @@unique([phoneNumber, organizationId])
59
+ @@index([organizationId])
60
  }
61
 
62
  model KnowledgeBaseEntry {
 
331
  EDTECH
332
  WEBHOOK
333
  AI_AGENT
334
+ CRM_MARKETING
335
+ PEDAGOGY
336
+ CUSTOMER_SERVICE
337
  }
338
 
339
  enum EnrollmentStatus {
packages/shared-types/src/organization.ts CHANGED
@@ -26,7 +26,7 @@ export const OrganizationSchema = z.object({
26
  name: z.string().min(1),
27
  contactEmail: z.string().email().optional(),
28
  customPrompt: z.string().optional(),
29
- mode: z.enum(['EDTECH', 'WEBHOOK', 'AI_AGENT']).optional(),
30
  flowConfig: FlowConfigSchema.optional(),
31
  webhookUrl: z.string().url().optional().or(z.literal('')),
32
  webhookSecret: z.string().optional(),
 
26
  name: z.string().min(1),
27
  contactEmail: z.string().email().optional(),
28
  customPrompt: z.string().optional(),
29
+ mode: z.enum(['EDTECH', 'WEBHOOK', 'AI_AGENT', 'CRM_MARKETING', 'PEDAGOGY', 'CUSTOMER_SERVICE']).optional(),
30
  flowConfig: FlowConfigSchema.optional(),
31
  webhookUrl: z.string().url().optional().or(z.literal('')),
32
  webhookSecret: z.string().optional(),