create-crm-tmp 1.0.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/bin/create-crm-tmp.js +93 -0
- package/package.json +25 -0
- package/template/.prettierignore +33 -0
- package/template/.prettierrc.json +25 -0
- package/template/README.md +173 -0
- package/template/eslint.config.mjs +18 -0
- package/template/exemple-contacts.csv +11 -0
- package/template/next.config.ts +8 -0
- package/template/package.json +64 -0
- package/template/postcss.config.mjs +7 -0
- package/template/prisma/migrations/20251126144728_init/migration.sql +78 -0
- package/template/prisma/migrations/20251126155204_add_user_roles/migration.sql +5 -0
- package/template/prisma/migrations/20251128095126_add_company_info/migration.sql +19 -0
- package/template/prisma/migrations/20251128123321_add_smtp_config/migration.sql +22 -0
- package/template/prisma/migrations/20251128132303_add_status/migration.sql +23 -0
- package/template/prisma/migrations/20251201102207_add_user_active/migration.sql +75 -0
- package/template/prisma/migrations/20251201105507_add_email_signature/migration.sql +2 -0
- package/template/prisma/migrations/20251201151122_add_tasks/migration.sql +45 -0
- package/template/prisma/migrations/20251202111854_add_task_reminder/migration.sql +2 -0
- package/template/prisma/migrations/20251202135859_add_google_meet_integration/migration.sql +27 -0
- package/template/prisma/migrations/20251203103317_add_meta_lead_integration/migration.sql +20 -0
- package/template/prisma/migrations/20251203104002_add_google_ads_integration/migration.sql +18 -0
- package/template/prisma/migrations/20251203112122_add_google_sheet_integration/migration.sql +32 -0
- package/template/prisma/migrations/20251203153853_allow_multiple_integration_configs/migration.sql +20 -0
- package/template/prisma/migrations/20251205141705_update_user_roles/migration.sql +12 -0
- package/template/prisma/migrations/20251205150000_add_commercial_and_telepro_assignment/migration.sql +21 -0
- package/template/prisma/migrations/20251205160000_add_interaction_logging/migration.sql +11 -0
- package/template/prisma/migrations/20251208090314_add_automatic_interaction_types/migration.sql +12 -0
- package/template/prisma/migrations/20251208094843_mg/migration.sql +14 -0
- package/template/prisma/migrations/20251208100000_add_company_support/migration.sql +14 -0
- package/template/prisma/migrations/20251208110000_add_templates/migration.sql +26 -0
- package/template/prisma/migrations/20251208141304_add_video_conference_task_type/migration.sql +2 -0
- package/template/prisma/migrations/20251209104759_add_internal_note_to_task/migration.sql +2 -0
- package/template/prisma/migrations/20251209134803_add_company_field/migration.sql +2 -0
- package/template/prisma/migrations/20251209150000_rename_company_to_company_name/migration.sql +3 -0
- package/template/prisma/migrations/20251209150016_add_email_tracking/migration.sql +21 -0
- package/template/prisma/migrations/20251209155908_add_notify_contact_to_task/migration.sql +2 -0
- package/template/prisma/migrations/20251210110019_add_appointment_types/migration.sql +10 -0
- package/template/prisma/migrations/20251210113928_add_contact_files/migration.sql +26 -0
- package/template/prisma/migrations/20251212132339_add_custom_roles/migration.sql +24 -0
- package/template/prisma/migrations/20251215104448_add_file_interaction_types/migration.sql +11 -0
- package/template/prisma/migrations/20251215145616_add_closing_reasons/migration.sql +12 -0
- package/template/prisma/migrations/20251216140850_add_log_users/migration.sql +25 -0
- package/template/prisma/migrations/20251216151000_rename_perdu_to_ferme/migration.sql +8 -0
- package/template/prisma/migrations/20251216162318_add_column_mappings_to_google_sheet/migration.sql +2 -0
- package/template/prisma/migrations/20251216185127_add_workflows/migration.sql +80 -0
- package/template/prisma/migrations/20251216192237_add_scheduled_workflow_actions/migration.sql +32 -0
- package/template/prisma/migrations/migration_lock.toml +3 -0
- package/template/prisma/schema.prisma +582 -0
- package/template/prisma.config.ts +14 -0
- package/template/src/app/(auth)/invite/[token]/page.tsx +200 -0
- package/template/src/app/(auth)/layout.tsx +3 -0
- package/template/src/app/(auth)/reset-password/complete/page.tsx +213 -0
- package/template/src/app/(auth)/reset-password/page.tsx +146 -0
- package/template/src/app/(auth)/reset-password/verify/page.tsx +183 -0
- package/template/src/app/(auth)/signin/page.tsx +166 -0
- package/template/src/app/(dashboard)/agenda/page.tsx +3051 -0
- package/template/src/app/(dashboard)/automatisation/[id]/page.tsx +24 -0
- package/template/src/app/(dashboard)/automatisation/_components/workflow-editor.tsx +905 -0
- package/template/src/app/(dashboard)/automatisation/new/page.tsx +20 -0
- package/template/src/app/(dashboard)/automatisation/page.tsx +337 -0
- package/template/src/app/(dashboard)/closing/page.tsx +1052 -0
- package/template/src/app/(dashboard)/contacts/[id]/page.tsx +6028 -0
- package/template/src/app/(dashboard)/contacts/page.tsx +3713 -0
- package/template/src/app/(dashboard)/dashboard/page.tsx +186 -0
- package/template/src/app/(dashboard)/layout.tsx +30 -0
- package/template/src/app/(dashboard)/settings/page.tsx +4070 -0
- package/template/src/app/(dashboard)/templates/page.tsx +567 -0
- package/template/src/app/(dashboard)/users/list/page.tsx +507 -0
- package/template/src/app/(dashboard)/users/page.tsx +457 -0
- package/template/src/app/(dashboard)/users/permissions/page.tsx +181 -0
- package/template/src/app/(dashboard)/users/roles/page.tsx +434 -0
- package/template/src/app/api/audit-logs/route.ts +57 -0
- package/template/src/app/api/auth/[...all]/route.ts +4 -0
- package/template/src/app/api/auth/check-active/route.ts +31 -0
- package/template/src/app/api/auth/google/callback/route.ts +94 -0
- package/template/src/app/api/auth/google/disconnect/route.ts +32 -0
- package/template/src/app/api/auth/google/route.ts +34 -0
- package/template/src/app/api/auth/google/status/route.ts +32 -0
- package/template/src/app/api/closing-reasons/route.ts +27 -0
- package/template/src/app/api/contacts/[id]/files/[fileId]/route.ts +94 -0
- package/template/src/app/api/contacts/[id]/files/route.ts +269 -0
- package/template/src/app/api/contacts/[id]/interactions/[interactionId]/route.ts +91 -0
- package/template/src/app/api/contacts/[id]/interactions/route.ts +103 -0
- package/template/src/app/api/contacts/[id]/meet/route.ts +296 -0
- package/template/src/app/api/contacts/[id]/route.ts +322 -0
- package/template/src/app/api/contacts/[id]/send-email/route.ts +254 -0
- package/template/src/app/api/contacts/export/route.ts +270 -0
- package/template/src/app/api/contacts/import/route.ts +381 -0
- package/template/src/app/api/contacts/route.ts +283 -0
- package/template/src/app/api/dashboard/stats/route.ts +299 -0
- package/template/src/app/api/email/track/[id]/route.ts +68 -0
- package/template/src/app/api/integrations/google-sheet/sync/route.ts +526 -0
- package/template/src/app/api/invite/complete/route.ts +88 -0
- package/template/src/app/api/invite/validate/route.ts +55 -0
- package/template/src/app/api/reminders/route.ts +95 -0
- package/template/src/app/api/reset-password/complete/route.ts +73 -0
- package/template/src/app/api/reset-password/request/route.ts +84 -0
- package/template/src/app/api/reset-password/validate/route.ts +49 -0
- package/template/src/app/api/reset-password/verify/route.ts +74 -0
- package/template/src/app/api/roles/[id]/route.ts +183 -0
- package/template/src/app/api/roles/route.ts +140 -0
- package/template/src/app/api/send/route.ts +282 -0
- package/template/src/app/api/settings/change-password/route.ts +95 -0
- package/template/src/app/api/settings/closing-reasons/[id]/route.ts +84 -0
- package/template/src/app/api/settings/closing-reasons/route.ts +74 -0
- package/template/src/app/api/settings/company/route.ts +121 -0
- package/template/src/app/api/settings/google-ads/[id]/route.ts +117 -0
- package/template/src/app/api/settings/google-ads/route.ts +122 -0
- package/template/src/app/api/settings/google-sheet/[id]/route.ts +230 -0
- package/template/src/app/api/settings/google-sheet/auto-map/route.ts +196 -0
- package/template/src/app/api/settings/google-sheet/route.ts +254 -0
- package/template/src/app/api/settings/meta-leads/[id]/route.ts +123 -0
- package/template/src/app/api/settings/meta-leads/route.ts +132 -0
- package/template/src/app/api/settings/profile/route.ts +42 -0
- package/template/src/app/api/settings/smtp/route.ts +130 -0
- package/template/src/app/api/settings/smtp/test/route.ts +121 -0
- package/template/src/app/api/settings/statuses/[id]/route.ts +101 -0
- package/template/src/app/api/settings/statuses/route.ts +83 -0
- package/template/src/app/api/statuses/route.ts +25 -0
- package/template/src/app/api/tasks/[id]/attendees/route.ts +76 -0
- package/template/src/app/api/tasks/[id]/route.ts +728 -0
- package/template/src/app/api/tasks/meet/route.ts +240 -0
- package/template/src/app/api/tasks/route.ts +417 -0
- package/template/src/app/api/templates/[id]/route.ts +140 -0
- package/template/src/app/api/templates/route.ts +91 -0
- package/template/src/app/api/users/[id]/route.ts +168 -0
- package/template/src/app/api/users/list/route.ts +45 -0
- package/template/src/app/api/users/me/route.ts +48 -0
- package/template/src/app/api/users/route.ts +250 -0
- package/template/src/app/api/webhooks/google-ads/route.ts +208 -0
- package/template/src/app/api/webhooks/meta-leads/route.ts +258 -0
- package/template/src/app/api/workflows/[id]/route.ts +192 -0
- package/template/src/app/api/workflows/process/route.ts +293 -0
- package/template/src/app/api/workflows/route.ts +124 -0
- package/template/src/app/favicon.ico +0 -0
- package/template/src/app/globals.css +1416 -0
- package/template/src/app/layout.tsx +31 -0
- package/template/src/app/page.tsx +32 -0
- package/template/src/components/dashboard/activity-chart.tsx +67 -0
- package/template/src/components/dashboard/contacts-chart.tsx +63 -0
- package/template/src/components/dashboard/recent-activity.tsx +164 -0
- package/template/src/components/dashboard/sales-analytics-chart.tsx +81 -0
- package/template/src/components/dashboard/stat-card.tsx +61 -0
- package/template/src/components/dashboard/status-distribution-chart.tsx +45 -0
- package/template/src/components/dashboard/tasks-pie-chart.tsx +88 -0
- package/template/src/components/dashboard/top-contacts-list.tsx +129 -0
- package/template/src/components/dashboard/upcoming-tasks-list.tsx +126 -0
- package/template/src/components/editor.tsx +856 -0
- package/template/src/components/email-template.tsx +35 -0
- package/template/src/components/header.tsx +320 -0
- package/template/src/components/invitation-email-template.tsx +79 -0
- package/template/src/components/meet-cancellation-email-template.tsx +120 -0
- package/template/src/components/meet-confirmation-email-template.tsx +156 -0
- package/template/src/components/meet-update-email-template.tsx +209 -0
- package/template/src/components/page-header.tsx +61 -0
- package/template/src/components/reset-password-email-template.tsx +79 -0
- package/template/src/components/sidebar.tsx +294 -0
- package/template/src/components/skeleton.tsx +380 -0
- package/template/src/components/ui/commands.tsx +396 -0
- package/template/src/components/ui/components.tsx +150 -0
- package/template/src/components/ui/theme.tsx +5 -0
- package/template/src/components/view-as-banner.tsx +45 -0
- package/template/src/components/view-as-modal.tsx +186 -0
- package/template/src/contexts/mobile-menu-context.tsx +31 -0
- package/template/src/contexts/sidebar-context.tsx +107 -0
- package/template/src/contexts/task-reminder-context.tsx +239 -0
- package/template/src/contexts/view-as-context.tsx +84 -0
- package/template/src/hooks/use-user-role.ts +82 -0
- package/template/src/lib/audit-log.ts +45 -0
- package/template/src/lib/auth-client.ts +16 -0
- package/template/src/lib/auth.ts +35 -0
- package/template/src/lib/check-permission.ts +193 -0
- package/template/src/lib/contact-duplicate.ts +112 -0
- package/template/src/lib/contact-interactions.ts +371 -0
- package/template/src/lib/encryption.ts +99 -0
- package/template/src/lib/google-calendar.ts +300 -0
- package/template/src/lib/google-drive.ts +372 -0
- package/template/src/lib/permissions.ts +412 -0
- package/template/src/lib/prisma.ts +32 -0
- package/template/src/lib/roles.ts +120 -0
- package/template/src/lib/template-variables.ts +76 -0
- package/template/src/lib/utils.ts +46 -0
- package/template/src/lib/workflow-executor.ts +482 -0
- package/template/src/proxy.ts +91 -0
- package/template/tsconfig.json +34 -0
- package/template/vercel.json +8 -0
|
@@ -0,0 +1,905 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect, useRef, useState } from 'react';
|
|
4
|
+
import { useRouter } from 'next/navigation';
|
|
5
|
+
import {
|
|
6
|
+
Plus,
|
|
7
|
+
Trash2,
|
|
8
|
+
Mail,
|
|
9
|
+
MessageSquare,
|
|
10
|
+
Tag,
|
|
11
|
+
CheckSquare,
|
|
12
|
+
Clock,
|
|
13
|
+
} from 'lucide-react';
|
|
14
|
+
import { cn } from '@/lib/utils';
|
|
15
|
+
|
|
16
|
+
interface Status {
|
|
17
|
+
id: string;
|
|
18
|
+
name: string;
|
|
19
|
+
color: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface Template {
|
|
23
|
+
id: string;
|
|
24
|
+
name: string;
|
|
25
|
+
type: 'EMAIL' | 'SMS' | 'NOTE';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface WorkflowAction {
|
|
29
|
+
id?: string;
|
|
30
|
+
actionType: 'SEND_EMAIL' | 'SEND_SMS' | 'CHANGE_STATUS' | 'CREATE_TASK' | 'WAIT';
|
|
31
|
+
order: number;
|
|
32
|
+
delayDays: number;
|
|
33
|
+
delayHours: number;
|
|
34
|
+
emailTemplateId?: string | null;
|
|
35
|
+
smsMessage?: string | null;
|
|
36
|
+
newStatusId?: string | null;
|
|
37
|
+
taskTitle?: string | null;
|
|
38
|
+
taskDescription?: string | null;
|
|
39
|
+
conditionOperator?: 'EQUALS' | 'NOT_EQUALS' | null;
|
|
40
|
+
conditionStatusId?: string | null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface WorkflowEditorInitialData {
|
|
44
|
+
id?: string;
|
|
45
|
+
name: string;
|
|
46
|
+
description: string | null;
|
|
47
|
+
active: boolean;
|
|
48
|
+
triggerType: 'CONTACT_CREATED' | 'STATUS_CHANGED' | 'TIME_BASED' | 'MANUAL';
|
|
49
|
+
triggerFromStatusId: string | null;
|
|
50
|
+
triggerToStatusId: string | null;
|
|
51
|
+
triggerTimeDays: number | null;
|
|
52
|
+
triggerTimeHours: number | null;
|
|
53
|
+
actions: WorkflowAction[];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
interface WorkflowEditorProps {
|
|
57
|
+
workflowId?: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function WorkflowEditor({ workflowId }: WorkflowEditorProps) {
|
|
61
|
+
const router = useRouter();
|
|
62
|
+
|
|
63
|
+
const [loading, setLoading] = useState<boolean>(!!workflowId);
|
|
64
|
+
const [saving, setSaving] = useState(false);
|
|
65
|
+
const [error, setError] = useState('');
|
|
66
|
+
const [success, setSuccess] = useState('');
|
|
67
|
+
|
|
68
|
+
const [statuses, setStatuses] = useState<Status[]>([]);
|
|
69
|
+
const [emailTemplates, setEmailTemplates] = useState<Template[]>([]);
|
|
70
|
+
|
|
71
|
+
const [formData, setFormData] = useState<WorkflowEditorInitialData>({
|
|
72
|
+
id: undefined,
|
|
73
|
+
name: '',
|
|
74
|
+
description: '',
|
|
75
|
+
active: true,
|
|
76
|
+
triggerType: 'CONTACT_CREATED',
|
|
77
|
+
triggerFromStatusId: null,
|
|
78
|
+
triggerToStatusId: null,
|
|
79
|
+
triggerTimeDays: null,
|
|
80
|
+
triggerTimeHours: null,
|
|
81
|
+
actions: [],
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const [actions, setActions] = useState<WorkflowAction[]>([]);
|
|
85
|
+
const [showActionMenu, setShowActionMenu] = useState(false);
|
|
86
|
+
const actionMenuRef = useRef<HTMLDivElement>(null);
|
|
87
|
+
|
|
88
|
+
useEffect(() => {
|
|
89
|
+
fetchStatuses();
|
|
90
|
+
fetchEmailTemplates();
|
|
91
|
+
}, []);
|
|
92
|
+
|
|
93
|
+
useEffect(() => {
|
|
94
|
+
if (!workflowId) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const fetchWorkflow = async () => {
|
|
99
|
+
try {
|
|
100
|
+
setLoading(true);
|
|
101
|
+
const response = await fetch(`/api/workflows/${workflowId}`);
|
|
102
|
+
const data = await response.json();
|
|
103
|
+
|
|
104
|
+
if (!response.ok) {
|
|
105
|
+
throw new Error(data.error || 'Erreur lors du chargement du workflow');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
setFormData({
|
|
109
|
+
id: data.id,
|
|
110
|
+
name: data.name,
|
|
111
|
+
description: data.description,
|
|
112
|
+
active: data.active,
|
|
113
|
+
triggerType: data.triggerType,
|
|
114
|
+
triggerFromStatusId: data.triggerFromStatusId,
|
|
115
|
+
triggerToStatusId: data.triggerToStatusId,
|
|
116
|
+
triggerTimeDays: data.triggerTimeDays,
|
|
117
|
+
triggerTimeHours: data.triggerTimeHours,
|
|
118
|
+
actions: data.actions,
|
|
119
|
+
});
|
|
120
|
+
setActions(data.actions || []);
|
|
121
|
+
} catch (err: any) {
|
|
122
|
+
setError(err.message || 'Erreur lors du chargement du workflow');
|
|
123
|
+
} finally {
|
|
124
|
+
setLoading(false);
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
fetchWorkflow();
|
|
129
|
+
}, [workflowId]);
|
|
130
|
+
|
|
131
|
+
// Fermer le menu d'actions en cliquant en dehors
|
|
132
|
+
useEffect(() => {
|
|
133
|
+
const handleClickOutside = (event: MouseEvent) => {
|
|
134
|
+
if (
|
|
135
|
+
actionMenuRef.current &&
|
|
136
|
+
!actionMenuRef.current.contains(event.target as Node)
|
|
137
|
+
) {
|
|
138
|
+
setShowActionMenu(false);
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
if (showActionMenu) {
|
|
143
|
+
document.addEventListener('mousedown', handleClickOutside);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return () => {
|
|
147
|
+
document.removeEventListener('mousedown', handleClickOutside);
|
|
148
|
+
};
|
|
149
|
+
}, [showActionMenu]);
|
|
150
|
+
|
|
151
|
+
const fetchStatuses = async () => {
|
|
152
|
+
try {
|
|
153
|
+
const response = await fetch('/api/settings/statuses');
|
|
154
|
+
if (response.ok) {
|
|
155
|
+
const data = await response.json();
|
|
156
|
+
setStatuses(data);
|
|
157
|
+
}
|
|
158
|
+
} catch (err) {
|
|
159
|
+
console.error('Erreur lors du chargement des statuts:', err);
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const fetchEmailTemplates = async () => {
|
|
164
|
+
try {
|
|
165
|
+
const response = await fetch('/api/templates?type=EMAIL');
|
|
166
|
+
if (response.ok) {
|
|
167
|
+
const data = await response.json();
|
|
168
|
+
setEmailTemplates(data);
|
|
169
|
+
}
|
|
170
|
+
} catch (err) {
|
|
171
|
+
console.error('Erreur lors du chargement des templates:', err);
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
176
|
+
e.preventDefault();
|
|
177
|
+
setError('');
|
|
178
|
+
setSuccess('');
|
|
179
|
+
|
|
180
|
+
if (!formData.name) {
|
|
181
|
+
setError('Le nom du workflow est requis');
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
try {
|
|
186
|
+
setSaving(true);
|
|
187
|
+
const isEdit = !!formData.id;
|
|
188
|
+
const url = isEdit ? `/api/workflows/${formData.id}` : '/api/workflows';
|
|
189
|
+
const method = isEdit ? 'PUT' : 'POST';
|
|
190
|
+
|
|
191
|
+
const response = await fetch(url, {
|
|
192
|
+
method,
|
|
193
|
+
headers: { 'Content-Type': 'application/json' },
|
|
194
|
+
body: JSON.stringify({
|
|
195
|
+
name: formData.name,
|
|
196
|
+
description: formData.description,
|
|
197
|
+
active: formData.active,
|
|
198
|
+
triggerType: formData.triggerType,
|
|
199
|
+
triggerFromStatusId: formData.triggerFromStatusId || null,
|
|
200
|
+
triggerToStatusId: formData.triggerToStatusId || null,
|
|
201
|
+
triggerTimeDays: formData.triggerTimeDays || null,
|
|
202
|
+
triggerTimeHours: formData.triggerTimeHours || null,
|
|
203
|
+
actions: actions.map((action, index) => ({
|
|
204
|
+
...action,
|
|
205
|
+
order: index,
|
|
206
|
+
})),
|
|
207
|
+
}),
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
const data = await response.json();
|
|
211
|
+
|
|
212
|
+
if (!response.ok) {
|
|
213
|
+
throw new Error(data.error || 'Erreur lors de la sauvegarde du workflow');
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
setSuccess(isEdit ? 'Workflow mis à jour avec succès' : 'Workflow créé avec succès');
|
|
217
|
+
setTimeout(() => setSuccess(''), 5000);
|
|
218
|
+
|
|
219
|
+
router.push('/automatisation');
|
|
220
|
+
} catch (err: any) {
|
|
221
|
+
setError(err.message || 'Erreur lors de la sauvegarde du workflow');
|
|
222
|
+
} finally {
|
|
223
|
+
setSaving(false);
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
const addAction = (actionType: WorkflowAction['actionType']) => {
|
|
228
|
+
const newAction: WorkflowAction = {
|
|
229
|
+
actionType,
|
|
230
|
+
order: actions.length,
|
|
231
|
+
delayDays: 0,
|
|
232
|
+
delayHours: 0,
|
|
233
|
+
};
|
|
234
|
+
setActions([...actions, newAction]);
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
const removeAction = (index: number) => {
|
|
238
|
+
setActions(actions.filter((_, i) => i !== index));
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
const updateAction = (index: number, updates: Partial<WorkflowAction>) => {
|
|
242
|
+
const newActions = [...actions];
|
|
243
|
+
newActions[index] = { ...newActions[index], ...updates };
|
|
244
|
+
setActions(newActions);
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
const getActionIcon = (actionType: WorkflowAction['actionType']) => {
|
|
248
|
+
switch (actionType) {
|
|
249
|
+
case 'SEND_EMAIL':
|
|
250
|
+
return <Mail className="h-5 w-5 text-indigo-600" />;
|
|
251
|
+
case 'SEND_SMS':
|
|
252
|
+
return <MessageSquare className="h-5 w-5 text-indigo-600" />;
|
|
253
|
+
case 'CHANGE_STATUS':
|
|
254
|
+
return <Tag className="h-5 w-5 text-indigo-600" />;
|
|
255
|
+
case 'CREATE_TASK':
|
|
256
|
+
return <CheckSquare className="h-5 w-5 text-indigo-600" />;
|
|
257
|
+
case 'WAIT':
|
|
258
|
+
return <Clock className="h-5 w-5 text-indigo-600" />;
|
|
259
|
+
default:
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
const getActionLabel = (actionType: WorkflowAction['actionType']) => {
|
|
265
|
+
switch (actionType) {
|
|
266
|
+
case 'SEND_EMAIL':
|
|
267
|
+
return 'Envoyer un email';
|
|
268
|
+
case 'SEND_SMS':
|
|
269
|
+
return 'Envoyer un SMS';
|
|
270
|
+
case 'CHANGE_STATUS':
|
|
271
|
+
return 'Changer le statut';
|
|
272
|
+
case 'CREATE_TASK':
|
|
273
|
+
return 'Créer une tâche';
|
|
274
|
+
case 'WAIT':
|
|
275
|
+
return 'Attendre';
|
|
276
|
+
default:
|
|
277
|
+
return '';
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
const getTriggerLabel = (triggerType: WorkflowEditorInitialData['triggerType']) => {
|
|
282
|
+
switch (triggerType) {
|
|
283
|
+
case 'CONTACT_CREATED':
|
|
284
|
+
return 'Nouveau contact créé';
|
|
285
|
+
case 'STATUS_CHANGED':
|
|
286
|
+
return 'Changement de statut';
|
|
287
|
+
case 'TIME_BASED':
|
|
288
|
+
return 'Basé sur le temps';
|
|
289
|
+
case 'MANUAL':
|
|
290
|
+
return 'Déclencheur manuel';
|
|
291
|
+
default:
|
|
292
|
+
return '';
|
|
293
|
+
}
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
return (
|
|
297
|
+
<form onSubmit={handleSubmit} className="h-full">
|
|
298
|
+
<div className="grid gap-6 lg:grid-cols-[minmax(0,2fr)_minmax(320px,1fr)]">
|
|
299
|
+
{/* Colonne centrale : flux d'actions */}
|
|
300
|
+
<div className="space-y-6">
|
|
301
|
+
<div className="rounded-2xl border border-dashed border-indigo-200 bg-indigo-50/40 p-4 text-sm text-indigo-900">
|
|
302
|
+
<p className="font-medium">Flux du workflow</p>
|
|
303
|
+
<p className="mt-1 text-xs text-indigo-700">
|
|
304
|
+
Visualisez l’enchaînement du déclencheur et des actions. Ajoutez des étapes pour
|
|
305
|
+
construire votre automatisation.
|
|
306
|
+
</p>
|
|
307
|
+
</div>
|
|
308
|
+
|
|
309
|
+
<div className="relative space-y-4">
|
|
310
|
+
{/* Ligne verticale */}
|
|
311
|
+
<div className="pointer-events-none absolute left-6 top-0 h-full w-px bg-linear-to-b from-indigo-300 via-gray-200 to-gray-200" />
|
|
312
|
+
|
|
313
|
+
{/* Déclencheur */}
|
|
314
|
+
<div className="relative pl-10">
|
|
315
|
+
<div className="absolute left-5 top-6 h-3 w-3 -translate-x-1/2 rounded-full border border-white bg-indigo-500 shadow-sm" />
|
|
316
|
+
<div className="rounded-xl border border-indigo-100 bg-white px-4 py-3 shadow-sm">
|
|
317
|
+
<div className="flex items-center justify-between">
|
|
318
|
+
<div>
|
|
319
|
+
<p className="text-xs font-semibold uppercase tracking-wide text-indigo-600">
|
|
320
|
+
Déclencheur
|
|
321
|
+
</p>
|
|
322
|
+
<p className="mt-1 text-sm font-medium text-gray-900">
|
|
323
|
+
{getTriggerLabel(formData.triggerType)}
|
|
324
|
+
</p>
|
|
325
|
+
</div>
|
|
326
|
+
</div>
|
|
327
|
+
</div>
|
|
328
|
+
</div>
|
|
329
|
+
|
|
330
|
+
{/* Actions */}
|
|
331
|
+
{actions.length === 0 ? (
|
|
332
|
+
<div className="relative pl-10">
|
|
333
|
+
<div className="absolute left-5 top-6 h-3 w-3 -translate-x-1/2 rounded-full border border-dashed border-gray-300 bg-white" />
|
|
334
|
+
<div className="flex flex-col items-center justify-center rounded-xl border border-dashed border-gray-300 bg-gray-50 px-4 py-8 text-center">
|
|
335
|
+
<Clock className="mb-2 h-8 w-8 text-gray-300" />
|
|
336
|
+
<p className="text-sm font-medium text-gray-900">Aucune action définie</p>
|
|
337
|
+
<p className="mt-1 text-xs text-gray-500">
|
|
338
|
+
Ajoutez une première action pour démarrer votre séquence.
|
|
339
|
+
</p>
|
|
340
|
+
</div>
|
|
341
|
+
</div>
|
|
342
|
+
) : (
|
|
343
|
+
actions.map((action, index) => (
|
|
344
|
+
<div key={index} className="relative pl-10">
|
|
345
|
+
<div className="absolute left-5 top-6 h-3 w-3 -translate-x-1/2 rounded-full border border-white bg-indigo-500 shadow-sm" />
|
|
346
|
+
<div className="rounded-xl border border-gray-200 bg-white p-4 shadow-sm transition-shadow hover:shadow-md">
|
|
347
|
+
<div className="mb-3 flex items-center justify-between">
|
|
348
|
+
<div className="flex items-center gap-3">
|
|
349
|
+
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-indigo-50 text-xs font-semibold text-indigo-700">
|
|
350
|
+
{index + 1}
|
|
351
|
+
</div>
|
|
352
|
+
<div>
|
|
353
|
+
<div className="flex items-center gap-2 text-gray-900">
|
|
354
|
+
{getActionIcon(action.actionType)}
|
|
355
|
+
<span className="text-sm font-medium">
|
|
356
|
+
{getActionLabel(action.actionType)}
|
|
357
|
+
</span>
|
|
358
|
+
</div>
|
|
359
|
+
<p className="mt-0.5 text-xs text-gray-500">
|
|
360
|
+
Délai de {action.delayDays || 0}j {action.delayHours || 0}h après
|
|
361
|
+
l’étape précédente
|
|
362
|
+
</p>
|
|
363
|
+
</div>
|
|
364
|
+
</div>
|
|
365
|
+
<button
|
|
366
|
+
type="button"
|
|
367
|
+
onClick={() => removeAction(index)}
|
|
368
|
+
className="cursor-pointer rounded-lg p-1.5 text-gray-400 transition-colors hover:bg-red-50 hover:text-red-600"
|
|
369
|
+
>
|
|
370
|
+
<Trash2 className="h-4 w-4" />
|
|
371
|
+
</button>
|
|
372
|
+
</div>
|
|
373
|
+
|
|
374
|
+
<div className="grid gap-4 md:grid-cols-2">
|
|
375
|
+
<div>
|
|
376
|
+
<label className="block text-xs font-medium text-gray-700">
|
|
377
|
+
Type d'action
|
|
378
|
+
</label>
|
|
379
|
+
<select
|
|
380
|
+
value={action.actionType}
|
|
381
|
+
onChange={(e) =>
|
|
382
|
+
updateAction(index, {
|
|
383
|
+
actionType: e.target.value as WorkflowAction['actionType'],
|
|
384
|
+
})
|
|
385
|
+
}
|
|
386
|
+
className="mt-1 block w-full rounded-lg border border-gray-300 px-3 py-2 text-xs text-gray-900 focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500 focus:outline-none"
|
|
387
|
+
>
|
|
388
|
+
<option value="SEND_EMAIL">Envoyer un email</option>
|
|
389
|
+
<option value="SEND_SMS">Envoyer un SMS</option>
|
|
390
|
+
<option value="CHANGE_STATUS">Changer le statut</option>
|
|
391
|
+
<option value="CREATE_TASK">Créer une tâche</option>
|
|
392
|
+
<option value="WAIT">Attendre</option>
|
|
393
|
+
</select>
|
|
394
|
+
</div>
|
|
395
|
+
<div className="grid grid-cols-2 gap-2">
|
|
396
|
+
<div>
|
|
397
|
+
<label className="block text-xs font-medium text-gray-700">
|
|
398
|
+
Délai (jours)
|
|
399
|
+
</label>
|
|
400
|
+
<input
|
|
401
|
+
type="number"
|
|
402
|
+
min="0"
|
|
403
|
+
value={action.delayDays}
|
|
404
|
+
onChange={(e) =>
|
|
405
|
+
updateAction(index, {
|
|
406
|
+
delayDays: parseInt(e.target.value, 10) || 0,
|
|
407
|
+
})
|
|
408
|
+
}
|
|
409
|
+
className="mt-1 block w-full rounded-lg border border-gray-300 px-3 py-2 text-xs text-gray-900 focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500 focus:outline-none"
|
|
410
|
+
/>
|
|
411
|
+
</div>
|
|
412
|
+
<div>
|
|
413
|
+
<label className="block text-xs font-medium text-gray-700">
|
|
414
|
+
Heures
|
|
415
|
+
</label>
|
|
416
|
+
<input
|
|
417
|
+
type="number"
|
|
418
|
+
min="0"
|
|
419
|
+
value={action.delayHours}
|
|
420
|
+
onChange={(e) =>
|
|
421
|
+
updateAction(index, {
|
|
422
|
+
delayHours: parseInt(e.target.value, 10) || 0,
|
|
423
|
+
})
|
|
424
|
+
}
|
|
425
|
+
className="mt-1 block w-full rounded-lg border border-gray-300 px-3 py-2 text-xs text-gray-900 focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500 focus:outline-none"
|
|
426
|
+
/>
|
|
427
|
+
</div>
|
|
428
|
+
</div>
|
|
429
|
+
</div>
|
|
430
|
+
|
|
431
|
+
{/* Config spécifique */}
|
|
432
|
+
{action.actionType === 'SEND_EMAIL' && (
|
|
433
|
+
<div className="mt-4">
|
|
434
|
+
<label className="block text-xs font-medium text-gray-700">
|
|
435
|
+
Template email
|
|
436
|
+
</label>
|
|
437
|
+
<select
|
|
438
|
+
value={action.emailTemplateId || ''}
|
|
439
|
+
onChange={(e) =>
|
|
440
|
+
updateAction(index, {
|
|
441
|
+
emailTemplateId: e.target.value || null,
|
|
442
|
+
})
|
|
443
|
+
}
|
|
444
|
+
className="mt-1 block w-full rounded-lg border border-gray-300 px-3 py-2 text-xs text-gray-900 focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500 focus:outline-none"
|
|
445
|
+
>
|
|
446
|
+
<option value="">Sélectionner un template</option>
|
|
447
|
+
{emailTemplates.map((template) => (
|
|
448
|
+
<option key={template.id} value={template.id}>
|
|
449
|
+
{template.name}
|
|
450
|
+
</option>
|
|
451
|
+
))}
|
|
452
|
+
</select>
|
|
453
|
+
</div>
|
|
454
|
+
)}
|
|
455
|
+
|
|
456
|
+
{action.actionType === 'SEND_SMS' && (
|
|
457
|
+
<div className="mt-4">
|
|
458
|
+
<label className="block text-xs font-medium text-gray-700">
|
|
459
|
+
Message SMS
|
|
460
|
+
</label>
|
|
461
|
+
<textarea
|
|
462
|
+
value={action.smsMessage || ''}
|
|
463
|
+
onChange={(e) =>
|
|
464
|
+
updateAction(index, {
|
|
465
|
+
smsMessage: e.target.value,
|
|
466
|
+
})
|
|
467
|
+
}
|
|
468
|
+
rows={3}
|
|
469
|
+
className="mt-1 block w-full rounded-lg border border-gray-300 px-3 py-2 text-xs text-gray-900 focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500 focus:outline-none"
|
|
470
|
+
placeholder="Votre message SMS..."
|
|
471
|
+
/>
|
|
472
|
+
</div>
|
|
473
|
+
)}
|
|
474
|
+
|
|
475
|
+
{action.actionType === 'CHANGE_STATUS' && (
|
|
476
|
+
<div className="mt-4">
|
|
477
|
+
<label className="block text-xs font-medium text-gray-700">
|
|
478
|
+
Nouveau statut
|
|
479
|
+
</label>
|
|
480
|
+
<select
|
|
481
|
+
value={action.newStatusId || ''}
|
|
482
|
+
onChange={(e) =>
|
|
483
|
+
updateAction(index, {
|
|
484
|
+
newStatusId: e.target.value || null,
|
|
485
|
+
})
|
|
486
|
+
}
|
|
487
|
+
className="mt-1 block w-full rounded-lg border border-gray-300 px-3 py-2 text-xs text-gray-900 focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500 focus:outline-none"
|
|
488
|
+
>
|
|
489
|
+
<option value="">Sélectionner un statut</option>
|
|
490
|
+
{statuses.map((status) => (
|
|
491
|
+
<option key={status.id} value={status.id}>
|
|
492
|
+
{status.name}
|
|
493
|
+
</option>
|
|
494
|
+
))}
|
|
495
|
+
</select>
|
|
496
|
+
</div>
|
|
497
|
+
)}
|
|
498
|
+
|
|
499
|
+
{action.actionType === 'CREATE_TASK' && (
|
|
500
|
+
<div className="mt-4 space-y-3">
|
|
501
|
+
<div>
|
|
502
|
+
<label className="block text-xs font-medium text-gray-700">
|
|
503
|
+
Titre de la tâche
|
|
504
|
+
</label>
|
|
505
|
+
<input
|
|
506
|
+
type="text"
|
|
507
|
+
value={action.taskTitle || ''}
|
|
508
|
+
onChange={(e) =>
|
|
509
|
+
updateAction(index, {
|
|
510
|
+
taskTitle: e.target.value,
|
|
511
|
+
})
|
|
512
|
+
}
|
|
513
|
+
className="mt-1 block w-full rounded-lg border border-gray-300 px-3 py-2 text-xs text-gray-900 focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500 focus:outline-none"
|
|
514
|
+
placeholder="Titre de la tâche..."
|
|
515
|
+
/>
|
|
516
|
+
</div>
|
|
517
|
+
<div>
|
|
518
|
+
<label className="block text-xs font-medium text-gray-700">
|
|
519
|
+
Description
|
|
520
|
+
</label>
|
|
521
|
+
<textarea
|
|
522
|
+
value={action.taskDescription || ''}
|
|
523
|
+
onChange={(e) =>
|
|
524
|
+
updateAction(index, {
|
|
525
|
+
taskDescription: e.target.value,
|
|
526
|
+
})
|
|
527
|
+
}
|
|
528
|
+
rows={3}
|
|
529
|
+
className="mt-1 block w-full rounded-lg border border-gray-300 px-3 py-2 text-xs text-gray-900 focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500 focus:outline-none"
|
|
530
|
+
placeholder="Description de la tâche..."
|
|
531
|
+
/>
|
|
532
|
+
</div>
|
|
533
|
+
</div>
|
|
534
|
+
)}
|
|
535
|
+
|
|
536
|
+
{/* Condition */}
|
|
537
|
+
<div className="mt-4 border-t border-dashed border-gray-200 pt-3">
|
|
538
|
+
<label className="block text-xs font-medium text-gray-700">
|
|
539
|
+
Condition (optionnel)
|
|
540
|
+
</label>
|
|
541
|
+
<div className="mt-2 grid grid-cols-2 gap-2">
|
|
542
|
+
<select
|
|
543
|
+
value={action.conditionOperator || ''}
|
|
544
|
+
onChange={(e) =>
|
|
545
|
+
updateAction(index, {
|
|
546
|
+
conditionOperator:
|
|
547
|
+
(e.target.value as 'EQUALS' | 'NOT_EQUALS') || null,
|
|
548
|
+
})
|
|
549
|
+
}
|
|
550
|
+
className="block w-full rounded-lg border border-gray-300 px-3 py-2 text-xs text-gray-900 focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500 focus:outline-none"
|
|
551
|
+
>
|
|
552
|
+
<option value="">Aucune condition</option>
|
|
553
|
+
<option value="EQUALS">Statut est égal à</option>
|
|
554
|
+
<option value="NOT_EQUALS">Statut n'est pas égal à</option>
|
|
555
|
+
</select>
|
|
556
|
+
{action.conditionOperator && (
|
|
557
|
+
<select
|
|
558
|
+
value={action.conditionStatusId || ''}
|
|
559
|
+
onChange={(e) =>
|
|
560
|
+
updateAction(index, {
|
|
561
|
+
conditionStatusId: e.target.value || null,
|
|
562
|
+
})
|
|
563
|
+
}
|
|
564
|
+
className="block w-full rounded-lg border border-gray-300 px-3 py-2 text-xs text-gray-900 focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500 focus:outline-none"
|
|
565
|
+
>
|
|
566
|
+
<option value="">Sélectionner...</option>
|
|
567
|
+
{statuses.map((status) => (
|
|
568
|
+
<option key={status.id} value={status.id}>
|
|
569
|
+
{status.name}
|
|
570
|
+
</option>
|
|
571
|
+
))}
|
|
572
|
+
</select>
|
|
573
|
+
)}
|
|
574
|
+
</div>
|
|
575
|
+
</div>
|
|
576
|
+
</div>
|
|
577
|
+
</div>
|
|
578
|
+
))
|
|
579
|
+
)}
|
|
580
|
+
|
|
581
|
+
{/* Bouton ajouter une action */}
|
|
582
|
+
<div className="relative pl-10">
|
|
583
|
+
<div className="absolute left-5 top-6 h-3 w-3 -translate-x-1/2 rounded-full border border-dashed border-indigo-300 bg-white" />
|
|
584
|
+
<div className="flex items-center justify-center">
|
|
585
|
+
<div className="relative inline-flex items-center" ref={actionMenuRef}>
|
|
586
|
+
<button
|
|
587
|
+
type="button"
|
|
588
|
+
onClick={() => setShowActionMenu(!showActionMenu)}
|
|
589
|
+
className="inline-flex cursor-pointer items-center gap-2 rounded-full border border-indigo-200 bg-white px-4 py-2 text-xs font-medium text-indigo-700 shadow-sm transition-colors hover:border-indigo-300 hover:bg-indigo-50"
|
|
590
|
+
>
|
|
591
|
+
<Plus className="h-4 w-4" />
|
|
592
|
+
Ajouter une action
|
|
593
|
+
</button>
|
|
594
|
+
{showActionMenu && (
|
|
595
|
+
<div className="absolute left-1/2 top-full z-10 mt-2 w-60 -translate-x-1/2 rounded-xl border border-gray-200 bg-white p-2 text-xs shadow-lg">
|
|
596
|
+
<p className="px-2 pb-1 text-[11px] font-medium uppercase tracking-wide text-gray-500">
|
|
597
|
+
Types d’actions
|
|
598
|
+
</p>
|
|
599
|
+
<button
|
|
600
|
+
type="button"
|
|
601
|
+
onClick={() => {
|
|
602
|
+
addAction('SEND_EMAIL');
|
|
603
|
+
setShowActionMenu(false);
|
|
604
|
+
}}
|
|
605
|
+
className="flex w-full cursor-pointer items-center gap-2 rounded-lg px-3 py-2 text-gray-700 transition-colors hover:bg-gray-50"
|
|
606
|
+
>
|
|
607
|
+
<Mail className="h-4 w-4" />
|
|
608
|
+
Envoyer un email
|
|
609
|
+
</button>
|
|
610
|
+
<button
|
|
611
|
+
type="button"
|
|
612
|
+
onClick={() => {
|
|
613
|
+
addAction('SEND_SMS');
|
|
614
|
+
setShowActionMenu(false);
|
|
615
|
+
}}
|
|
616
|
+
className="flex w-full cursor-pointer items-center gap-2 rounded-lg px-3 py-2 text-gray-700 transition-colors hover:bg-gray-50"
|
|
617
|
+
>
|
|
618
|
+
<MessageSquare className="h-4 w-4" />
|
|
619
|
+
Envoyer un SMS
|
|
620
|
+
</button>
|
|
621
|
+
<button
|
|
622
|
+
type="button"
|
|
623
|
+
onClick={() => {
|
|
624
|
+
addAction('CHANGE_STATUS');
|
|
625
|
+
setShowActionMenu(false);
|
|
626
|
+
}}
|
|
627
|
+
className="flex w-full cursor-pointer items-center gap-2 rounded-lg px-3 py-2 text-gray-700 transition-colors hover:bg-gray-50"
|
|
628
|
+
>
|
|
629
|
+
<Tag className="h-4 w-4" />
|
|
630
|
+
Changer le statut
|
|
631
|
+
</button>
|
|
632
|
+
<button
|
|
633
|
+
type="button"
|
|
634
|
+
onClick={() => {
|
|
635
|
+
addAction('CREATE_TASK');
|
|
636
|
+
setShowActionMenu(false);
|
|
637
|
+
}}
|
|
638
|
+
className="flex w-full cursor-pointer items-center gap-2 rounded-lg px-3 py-2 text-gray-700 transition-colors hover:bg-gray-50"
|
|
639
|
+
>
|
|
640
|
+
<CheckSquare className="h-4 w-4" />
|
|
641
|
+
Créer une tâche
|
|
642
|
+
</button>
|
|
643
|
+
<button
|
|
644
|
+
type="button"
|
|
645
|
+
onClick={() => {
|
|
646
|
+
addAction('WAIT');
|
|
647
|
+
setShowActionMenu(false);
|
|
648
|
+
}}
|
|
649
|
+
className="flex w-full cursor-pointer items-center gap-2 rounded-lg px-3 py-2 text-gray-700 transition-colors hover:bg-gray-50"
|
|
650
|
+
>
|
|
651
|
+
<Clock className="h-4 w-4" />
|
|
652
|
+
Attendre
|
|
653
|
+
</button>
|
|
654
|
+
</div>
|
|
655
|
+
)}
|
|
656
|
+
</div>
|
|
657
|
+
</div>
|
|
658
|
+
</div>
|
|
659
|
+
</div>
|
|
660
|
+
</div>
|
|
661
|
+
|
|
662
|
+
{/* Colonne droite : formulaire */}
|
|
663
|
+
<div className="space-y-4">
|
|
664
|
+
<div className="rounded-2xl border border-gray-200 bg-white p-5 shadow-sm">
|
|
665
|
+
<div className="mb-4 flex items-center justify-between">
|
|
666
|
+
<div>
|
|
667
|
+
<p className="text-xs font-semibold uppercase tracking-wide text-gray-500">
|
|
668
|
+
Paramètres du workflow
|
|
669
|
+
</p>
|
|
670
|
+
<p className="mt-1 text-sm font-medium text-gray-900">
|
|
671
|
+
{formData.id ? 'Modifier le workflow' : 'Nouvelle automatisation'}
|
|
672
|
+
</p>
|
|
673
|
+
</div>
|
|
674
|
+
<span
|
|
675
|
+
className={cn(
|
|
676
|
+
'inline-flex items-center rounded-full px-2.5 py-0.5 text-[11px] font-medium',
|
|
677
|
+
formData.active
|
|
678
|
+
? 'bg-green-50 text-green-700'
|
|
679
|
+
: 'bg-gray-100 text-gray-600',
|
|
680
|
+
)}
|
|
681
|
+
>
|
|
682
|
+
{formData.active ? 'Actif' : 'Brouillon'}
|
|
683
|
+
</span>
|
|
684
|
+
</div>
|
|
685
|
+
|
|
686
|
+
<div className="space-y-4">
|
|
687
|
+
<div>
|
|
688
|
+
<label className="block text-xs font-medium text-gray-700">
|
|
689
|
+
Nom du workflow *
|
|
690
|
+
</label>
|
|
691
|
+
<input
|
|
692
|
+
type="text"
|
|
693
|
+
required
|
|
694
|
+
value={formData.name}
|
|
695
|
+
onChange={(e) =>
|
|
696
|
+
setFormData((prev) => ({
|
|
697
|
+
...prev,
|
|
698
|
+
name: e.target.value,
|
|
699
|
+
}))
|
|
700
|
+
}
|
|
701
|
+
className="mt-1 block w-full rounded-lg border border-gray-300 px-3 py-2 text-sm text-gray-900 focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500 focus:outline-none"
|
|
702
|
+
placeholder="Ex : Séquence de bienvenue nouveau lead"
|
|
703
|
+
/>
|
|
704
|
+
</div>
|
|
705
|
+
|
|
706
|
+
<div>
|
|
707
|
+
<label className="block text-xs font-medium text-gray-700">
|
|
708
|
+
Description
|
|
709
|
+
</label>
|
|
710
|
+
<textarea
|
|
711
|
+
value={formData.description ?? ''}
|
|
712
|
+
onChange={(e) =>
|
|
713
|
+
setFormData((prev) => ({
|
|
714
|
+
...prev,
|
|
715
|
+
description: e.target.value,
|
|
716
|
+
}))
|
|
717
|
+
}
|
|
718
|
+
rows={3}
|
|
719
|
+
className="mt-1 block w-full rounded-lg border border-gray-300 px-3 py-2 text-sm text-gray-900 focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500 focus:outline-none"
|
|
720
|
+
placeholder="Expliquez brièvement ce que fait ce workflow..."
|
|
721
|
+
/>
|
|
722
|
+
</div>
|
|
723
|
+
|
|
724
|
+
<div className="flex items-center justify-between rounded-xl bg-gray-50 px-3 py-2.5">
|
|
725
|
+
<div>
|
|
726
|
+
<p className="text-xs font-medium text-gray-700">Statut</p>
|
|
727
|
+
<p className="text-xs text-gray-500">
|
|
728
|
+
Activez pour commencer à traiter automatiquement les contacts.
|
|
729
|
+
</p>
|
|
730
|
+
</div>
|
|
731
|
+
<button
|
|
732
|
+
type="button"
|
|
733
|
+
onClick={() =>
|
|
734
|
+
setFormData((prev) => ({
|
|
735
|
+
...prev,
|
|
736
|
+
active: !prev.active,
|
|
737
|
+
}))
|
|
738
|
+
}
|
|
739
|
+
className={cn(
|
|
740
|
+
'relative inline-flex h-6 w-11 cursor-pointer items-center rounded-full border transition-colors',
|
|
741
|
+
formData.active
|
|
742
|
+
? 'border-indigo-500 bg-indigo-500'
|
|
743
|
+
: 'border-gray-300 bg-gray-200',
|
|
744
|
+
)}
|
|
745
|
+
>
|
|
746
|
+
<span
|
|
747
|
+
className={cn(
|
|
748
|
+
'inline-block h-4 w-4 transform rounded-full bg-white shadow transition-transform',
|
|
749
|
+
formData.active ? 'translate-x-5' : 'translate-x-1',
|
|
750
|
+
)}
|
|
751
|
+
/>
|
|
752
|
+
</button>
|
|
753
|
+
</div>
|
|
754
|
+
</div>
|
|
755
|
+
</div>
|
|
756
|
+
|
|
757
|
+
<div className="rounded-2xl border border-gray-200 bg-white p-5 shadow-sm space-y-4">
|
|
758
|
+
<div>
|
|
759
|
+
<label className="block text-xs font-medium text-gray-700">
|
|
760
|
+
Type de déclencheur *
|
|
761
|
+
</label>
|
|
762
|
+
<select
|
|
763
|
+
value={formData.triggerType}
|
|
764
|
+
onChange={(e) =>
|
|
765
|
+
setFormData((prev) => ({
|
|
766
|
+
...prev,
|
|
767
|
+
triggerType: e.target.value as WorkflowEditorInitialData['triggerType'],
|
|
768
|
+
}))
|
|
769
|
+
}
|
|
770
|
+
className="mt-1 block w-full rounded-lg border border-gray-300 px-3 py-2 text-sm text-gray-900 focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500 focus:outline-none"
|
|
771
|
+
>
|
|
772
|
+
<option value="CONTACT_CREATED">Nouveau contact créé</option>
|
|
773
|
+
<option value="STATUS_CHANGED">Changement de statut</option>
|
|
774
|
+
<option value="TIME_BASED">Basé sur le temps</option>
|
|
775
|
+
<option value="MANUAL">Déclencheur manuel</option>
|
|
776
|
+
</select>
|
|
777
|
+
</div>
|
|
778
|
+
|
|
779
|
+
{formData.triggerType === 'STATUS_CHANGED' && (
|
|
780
|
+
<div className="grid gap-3 md:grid-cols-2">
|
|
781
|
+
<div>
|
|
782
|
+
<label className="block text-xs font-medium text-gray-700">
|
|
783
|
+
Du statut (optionnel)
|
|
784
|
+
</label>
|
|
785
|
+
<select
|
|
786
|
+
value={formData.triggerFromStatusId ?? ''}
|
|
787
|
+
onChange={(e) =>
|
|
788
|
+
setFormData((prev) => ({
|
|
789
|
+
...prev,
|
|
790
|
+
triggerFromStatusId: e.target.value || null,
|
|
791
|
+
}))
|
|
792
|
+
}
|
|
793
|
+
className="mt-1 block w-full rounded-lg border border-gray-300 px-3 py-2 text-sm text-gray-900 focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500 focus:outline-none"
|
|
794
|
+
>
|
|
795
|
+
<option value="">Tous les statuts</option>
|
|
796
|
+
{statuses.map((status) => (
|
|
797
|
+
<option key={status.id} value={status.id}>
|
|
798
|
+
{status.name}
|
|
799
|
+
</option>
|
|
800
|
+
))}
|
|
801
|
+
</select>
|
|
802
|
+
</div>
|
|
803
|
+
<div>
|
|
804
|
+
<label className="block text-xs font-medium text-gray-700">
|
|
805
|
+
Vers le statut
|
|
806
|
+
</label>
|
|
807
|
+
<select
|
|
808
|
+
value={formData.triggerToStatusId ?? ''}
|
|
809
|
+
onChange={(e) =>
|
|
810
|
+
setFormData((prev) => ({
|
|
811
|
+
...prev,
|
|
812
|
+
triggerToStatusId: e.target.value || null,
|
|
813
|
+
}))
|
|
814
|
+
}
|
|
815
|
+
className="mt-1 block w-full rounded-lg border border-gray-300 px-3 py-2 text-sm text-gray-900 focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500 focus:outline-none"
|
|
816
|
+
>
|
|
817
|
+
<option value="">Tous les statuts</option>
|
|
818
|
+
{statuses.map((status) => (
|
|
819
|
+
<option key={status.id} value={status.id}>
|
|
820
|
+
{status.name}
|
|
821
|
+
</option>
|
|
822
|
+
))}
|
|
823
|
+
</select>
|
|
824
|
+
</div>
|
|
825
|
+
</div>
|
|
826
|
+
)}
|
|
827
|
+
|
|
828
|
+
{formData.triggerType === 'TIME_BASED' && (
|
|
829
|
+
<div className="grid gap-3 md:grid-cols-2">
|
|
830
|
+
<div>
|
|
831
|
+
<label className="block text-xs font-medium text-gray-700">
|
|
832
|
+
Délai (jours)
|
|
833
|
+
</label>
|
|
834
|
+
<input
|
|
835
|
+
type="number"
|
|
836
|
+
min="0"
|
|
837
|
+
value={formData.triggerTimeDays ?? 0}
|
|
838
|
+
onChange={(e) =>
|
|
839
|
+
setFormData((prev) => ({
|
|
840
|
+
...prev,
|
|
841
|
+
triggerTimeDays: parseInt(e.target.value, 10) || 0,
|
|
842
|
+
}))
|
|
843
|
+
}
|
|
844
|
+
className="mt-1 block w-full rounded-lg border border-gray-300 px-3 py-2 text-sm text-gray-900 focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500 focus:outline-none"
|
|
845
|
+
/>
|
|
846
|
+
</div>
|
|
847
|
+
<div>
|
|
848
|
+
<label className="block text-xs font-medium text-gray-700">
|
|
849
|
+
Délai (heures)
|
|
850
|
+
</label>
|
|
851
|
+
<input
|
|
852
|
+
type="number"
|
|
853
|
+
min="0"
|
|
854
|
+
value={formData.triggerTimeHours ?? 0}
|
|
855
|
+
onChange={(e) =>
|
|
856
|
+
setFormData((prev) => ({
|
|
857
|
+
...prev,
|
|
858
|
+
triggerTimeHours: parseInt(e.target.value, 10) || 0,
|
|
859
|
+
}))
|
|
860
|
+
}
|
|
861
|
+
className="mt-1 block w-full rounded-lg border border-gray-300 px-3 py-2 text-sm text-gray-900 focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500 focus:outline-none"
|
|
862
|
+
/>
|
|
863
|
+
</div>
|
|
864
|
+
</div>
|
|
865
|
+
)}
|
|
866
|
+
</div>
|
|
867
|
+
|
|
868
|
+
{(error || success) && (
|
|
869
|
+
<div className="space-y-2 text-xs">
|
|
870
|
+
{error && (
|
|
871
|
+
<div className="rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-red-700">
|
|
872
|
+
{error}
|
|
873
|
+
</div>
|
|
874
|
+
)}
|
|
875
|
+
{success && (
|
|
876
|
+
<div className="rounded-lg border border-green-200 bg-green-50 px-3 py-2 text-green-700">
|
|
877
|
+
{success}
|
|
878
|
+
</div>
|
|
879
|
+
)}
|
|
880
|
+
</div>
|
|
881
|
+
)}
|
|
882
|
+
|
|
883
|
+
<div className="flex flex-col gap-2 pt-2 sm:flex-row sm:justify-end">
|
|
884
|
+
<button
|
|
885
|
+
type="button"
|
|
886
|
+
onClick={() => router.push('/automatisation')}
|
|
887
|
+
className="w-full cursor-pointer rounded-xl border border-gray-300 bg-white px-4 py-2.5 text-sm font-medium text-gray-700 transition-colors hover:bg-gray-50 sm:w-auto"
|
|
888
|
+
>
|
|
889
|
+
Annuler
|
|
890
|
+
</button>
|
|
891
|
+
<button
|
|
892
|
+
type="submit"
|
|
893
|
+
disabled={saving || loading}
|
|
894
|
+
className="w-full cursor-pointer rounded-xl bg-indigo-600 px-4 py-2.5 text-sm font-medium text-white shadow-sm transition-colors hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 disabled:cursor-not-allowed disabled:bg-indigo-400 sm:w-auto"
|
|
895
|
+
>
|
|
896
|
+
{saving ? 'Enregistrement...' : 'Enregistrer le workflow'}
|
|
897
|
+
</button>
|
|
898
|
+
</div>
|
|
899
|
+
</div>
|
|
900
|
+
</div>
|
|
901
|
+
</form>
|
|
902
|
+
);
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
|