create-crm-tmp 1.1.2 → 2.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/package.json +1 -1
- package/template/.prettierignore +2 -0
- package/template/README.md +53 -67
- package/template/components.json +22 -0
- package/template/exemple-contacts.csv +54 -0
- package/template/next.config.ts +27 -1
- package/template/package.json +64 -27
- package/template/prisma/schema.prisma +821 -72
- package/template/skills-lock.json +25 -0
- package/template/src/app/(auth)/invite/[token]/page.tsx +21 -24
- package/template/src/app/(auth)/reset-password/complete/page.tsx +12 -21
- package/template/src/app/(auth)/reset-password/page.tsx +12 -8
- package/template/src/app/(auth)/reset-password/verify/page.tsx +12 -8
- package/template/src/app/(auth)/signin/page.tsx +20 -17
- package/template/src/app/(dashboard)/agenda/page.tsx +2231 -2188
- package/template/src/app/(dashboard)/automatisation/[id]/page.tsx +10 -7
- package/template/src/app/(dashboard)/automatisation/_components/workflow-editor.tsx +680 -323
- package/template/src/app/(dashboard)/automatisation/new/page.tsx +11 -8
- package/template/src/app/(dashboard)/automatisation/page.tsx +473 -180
- package/template/src/app/(dashboard)/closing/page.tsx +500 -468
- package/template/src/app/(dashboard)/contacts/[id]/page.tsx +5035 -4126
- package/template/src/app/(dashboard)/contacts/companies/[id]/page.tsx +1703 -0
- package/template/src/app/(dashboard)/contacts/loading.tsx +13 -0
- package/template/src/app/(dashboard)/contacts/page.tsx +3776 -2064
- package/template/src/app/(dashboard)/dashboard/page.tsx +37 -519
- package/template/src/app/(dashboard)/error.tsx +37 -0
- package/template/src/app/(dashboard)/layout.tsx +1 -1
- package/template/src/app/(dashboard)/loading.tsx +5 -0
- package/template/src/app/(dashboard)/settings/loading.tsx +19 -0
- package/template/src/app/(dashboard)/settings/page.tsx +2685 -2489
- package/template/src/app/(dashboard)/templates/page.tsx +500 -300
- package/template/src/app/(dashboard)/users/list/page.tsx +356 -350
- package/template/src/app/(dashboard)/users/page.tsx +279 -310
- package/template/src/app/(dashboard)/users/permissions/page.tsx +104 -99
- package/template/src/app/(dashboard)/users/roles/page.tsx +164 -137
- package/template/src/app/api/audit-logs/route.ts +1 -1
- package/template/src/app/api/auth/google/callback/route.ts +8 -5
- package/template/src/app/api/auth/google/disconnect/route.ts +2 -2
- package/template/src/app/api/companies/[id]/activities/route.ts +131 -0
- package/template/src/app/api/companies/[id]/route.ts +195 -0
- package/template/src/app/api/companies/export/route.ts +206 -0
- package/template/src/app/api/companies/route.ts +166 -0
- package/template/src/app/api/contact-views/[id]/pin/route.ts +69 -0
- package/template/src/app/api/contact-views/[id]/route.ts +197 -0
- package/template/src/app/api/contact-views/route.ts +146 -0
- package/template/src/app/api/contacts/[id]/files/[fileId]/preview/route.ts +77 -0
- package/template/src/app/api/contacts/[id]/files/[fileId]/route.ts +7 -17
- package/template/src/app/api/contacts/[id]/files/route.ts +83 -44
- package/template/src/app/api/contacts/[id]/interactions/route.ts +37 -0
- package/template/src/app/api/contacts/[id]/kyc/route.ts +71 -0
- package/template/src/app/api/contacts/[id]/meet/route.ts +38 -29
- package/template/src/app/api/contacts/[id]/route.ts +111 -20
- package/template/src/app/api/contacts/[id]/send-email/route.ts +6 -0
- package/template/src/app/api/contacts/[id]/workflows/run/route.ts +61 -0
- package/template/src/app/api/contacts/export/route.ts +12 -17
- package/template/src/app/api/contacts/import/route.ts +22 -19
- package/template/src/app/api/contacts/import-preview/route.ts +139 -0
- package/template/src/app/api/contacts/route.ts +202 -49
- package/template/src/app/api/dashboard/stats/route.ts +9 -292
- package/template/src/app/api/integrations/google-sheet/sync/route.ts +203 -185
- package/template/src/app/api/invite/complete/route.ts +20 -23
- package/template/src/app/api/reminders/route.ts +1 -0
- package/template/src/app/api/reset-password/complete/route.ts +11 -13
- package/template/src/app/api/send/route.ts +9 -85
- package/template/src/app/api/settings/closing-reasons/[id]/route.ts +10 -21
- package/template/src/app/api/settings/closing-reasons/route.ts +10 -21
- package/template/src/app/api/settings/company/route.ts +19 -26
- package/template/src/app/api/settings/google-ads/[id]/route.ts +20 -23
- package/template/src/app/api/settings/google-ads/route.ts +20 -23
- package/template/src/app/api/settings/google-sheet/[id]/route.ts +20 -23
- package/template/src/app/api/settings/google-sheet/auto-map/route.ts +23 -32
- package/template/src/app/api/settings/google-sheet/preview/route.ts +104 -0
- package/template/src/app/api/settings/google-sheet/route.ts +20 -23
- package/template/src/app/api/settings/meta-leads/[id]/route.ts +20 -23
- package/template/src/app/api/settings/meta-leads/route.ts +20 -23
- package/template/src/app/api/settings/statuses/[id]/route.ts +33 -23
- package/template/src/app/api/settings/statuses/route.ts +24 -22
- package/template/src/app/api/statuses/route.ts +2 -5
- package/template/src/app/api/tasks/[id]/attendees/route.ts +14 -7
- package/template/src/app/api/tasks/[id]/route.ts +161 -137
- package/template/src/app/api/tasks/meet/route.ts +11 -8
- package/template/src/app/api/tasks/route.ts +155 -95
- package/template/src/app/api/templates/[id]/route.ts +22 -13
- package/template/src/app/api/templates/route.ts +22 -5
- package/template/src/app/api/users/[id]/resend-invite/route.ts +95 -0
- package/template/src/app/api/users/[id]/route.ts +16 -1
- package/template/src/app/api/users/commercials/route.ts +38 -0
- package/template/src/app/api/users/for-agenda/route.ts +1 -2
- package/template/src/app/api/users/route.ts +94 -55
- package/template/src/app/api/webhooks/google-ads/route.ts +20 -1
- package/template/src/app/api/webhooks/meta-leads/route.ts +18 -1
- package/template/src/app/api/workflows/[id]/route.ts +33 -6
- package/template/src/app/api/workflows/process/route.ts +509 -146
- package/template/src/app/api/workflows/route.ts +46 -4
- package/template/src/app/globals.css +210 -101
- package/template/src/app/layout.tsx +19 -8
- package/template/src/app/page.tsx +37 -7
- package/template/src/components/address-autocomplete.tsx +232 -0
- package/template/src/components/contacts/filter-bar.tsx +181 -0
- package/template/src/components/contacts/filter-builder.tsx +589 -0
- package/template/src/components/contacts/save-view-dialog.tsx +160 -0
- package/template/src/components/contacts/views-tab-bar.tsx +440 -0
- package/template/src/components/dashboard/activity-chart.tsx +31 -39
- package/template/src/components/dashboard/dashboard-content.tsx +79 -0
- package/template/src/components/dashboard/stat-card.tsx +40 -42
- package/template/src/components/dashboard/tasks-pie-chart.tsx +34 -37
- package/template/src/components/dashboard/upcoming-tasks-list.tsx +78 -72
- package/template/src/components/date-picker.tsx +396 -0
- package/template/src/components/editor.tsx +27 -13
- package/template/src/components/email-template.tsx +4 -2
- package/template/src/components/global-search.tsx +358 -0
- package/template/src/components/header.tsx +57 -62
- package/template/src/components/invitation-email-template.tsx +4 -2
- package/template/src/components/lazy-editor.tsx +11 -0
- package/template/src/components/meet-cancellation-email-template.tsx +11 -3
- package/template/src/components/meet-confirmation-email-template.tsx +10 -3
- package/template/src/components/meet-update-email-template.tsx +10 -3
- package/template/src/components/page-header.tsx +19 -15
- package/template/src/components/protected-page.tsx +94 -0
- package/template/src/components/reset-password-email-template.tsx +4 -2
- package/template/src/components/sidebar.tsx +92 -94
- package/template/src/components/skeleton.tsx +128 -42
- package/template/src/components/ui/accordion.tsx +64 -0
- package/template/src/components/ui/alert-dialog.tsx +139 -0
- package/template/src/components/ui/button.tsx +60 -0
- package/template/src/components/view-as-banner.tsx +1 -1
- package/template/src/components/view-as-modal.tsx +21 -16
- package/template/src/config/nav-pages.ts +108 -0
- package/template/src/contexts/app-toast-context.tsx +174 -0
- package/template/src/contexts/sidebar-context.tsx +16 -47
- package/template/src/contexts/task-reminder-context.tsx +6 -6
- package/template/src/contexts/view-as-context.tsx +11 -16
- package/template/src/hooks/use-alert.tsx +65 -0
- package/template/src/hooks/use-confirm.tsx +87 -0
- package/template/src/hooks/use-contact-views.ts +140 -0
- package/template/src/hooks/use-contacts.ts +69 -0
- package/template/src/hooks/use-fetch.ts +17 -0
- package/template/src/hooks/use-focus-trap.ts +73 -0
- package/template/src/hooks/use-statuses.ts +22 -0
- package/template/src/lib/address-api.ts +155 -0
- package/template/src/lib/cache.ts +73 -0
- package/template/src/lib/check-permission.ts +12 -177
- package/template/src/lib/contact-interactions.ts +3 -1
- package/template/src/lib/contact-view-filters.ts +341 -0
- package/template/src/lib/dashboard-stats.ts +224 -0
- package/template/src/lib/date-utils.ts +49 -0
- package/template/src/lib/get-auth-user.ts +25 -0
- package/template/src/lib/google-calendar.ts +54 -12
- package/template/src/lib/google-drive.ts +796 -75
- package/template/src/lib/google-fetch.ts +63 -0
- package/template/src/lib/local-storage.ts +34 -0
- package/template/src/lib/permissions.ts +245 -47
- package/template/src/lib/prisma.ts +11 -11
- package/template/src/lib/roles.ts +14 -39
- package/template/src/lib/template-variables.ts +67 -33
- package/template/src/lib/utils.ts +26 -2
- package/template/src/lib/workflow-executor.ts +445 -229
- package/template/src/proxy.ts +34 -73
- package/template/src/types/contact-views.ts +351 -0
- package/template/src/types/yousign.ts +52 -0
- package/template/vercel.json +12 -0
- package/template/WORKFLOWS_CRON.md +0 -185
- package/template/prisma/migrations/20251126144728_init/migration.sql +0 -78
- package/template/prisma/migrations/20251126155204_add_user_roles/migration.sql +0 -5
- package/template/prisma/migrations/20251128095126_add_company_info/migration.sql +0 -19
- package/template/prisma/migrations/20251128123321_add_smtp_config/migration.sql +0 -22
- package/template/prisma/migrations/20251128132303_add_status/migration.sql +0 -23
- package/template/prisma/migrations/20251201102207_add_user_active/migration.sql +0 -75
- package/template/prisma/migrations/20251201105507_add_email_signature/migration.sql +0 -2
- package/template/prisma/migrations/20251201151122_add_tasks/migration.sql +0 -45
- package/template/prisma/migrations/20251202111854_add_task_reminder/migration.sql +0 -2
- package/template/prisma/migrations/20251202135859_add_google_meet_integration/migration.sql +0 -27
- package/template/prisma/migrations/20251203103317_add_meta_lead_integration/migration.sql +0 -20
- package/template/prisma/migrations/20251203104002_add_google_ads_integration/migration.sql +0 -18
- package/template/prisma/migrations/20251203112122_add_google_sheet_integration/migration.sql +0 -32
- package/template/prisma/migrations/20251203153853_allow_multiple_integration_configs/migration.sql +0 -20
- package/template/prisma/migrations/20251205141705_update_user_roles/migration.sql +0 -12
- package/template/prisma/migrations/20251205150000_add_commercial_and_telepro_assignment/migration.sql +0 -21
- package/template/prisma/migrations/20251205160000_add_interaction_logging/migration.sql +0 -11
- package/template/prisma/migrations/20251208090314_add_automatic_interaction_types/migration.sql +0 -12
- package/template/prisma/migrations/20251208094843_mg/migration.sql +0 -14
- package/template/prisma/migrations/20251208100000_add_company_support/migration.sql +0 -14
- package/template/prisma/migrations/20251208110000_add_templates/migration.sql +0 -26
- package/template/prisma/migrations/20251208141304_add_video_conference_task_type/migration.sql +0 -2
- package/template/prisma/migrations/20251209104759_add_internal_note_to_task/migration.sql +0 -2
- package/template/prisma/migrations/20251209134803_add_company_field/migration.sql +0 -2
- package/template/prisma/migrations/20251209150000_rename_company_to_company_name/migration.sql +0 -3
- package/template/prisma/migrations/20251209150016_add_email_tracking/migration.sql +0 -21
- package/template/prisma/migrations/20251209155908_add_notify_contact_to_task/migration.sql +0 -2
- package/template/prisma/migrations/20251210110019_add_appointment_types/migration.sql +0 -10
- package/template/prisma/migrations/20251210113928_add_contact_files/migration.sql +0 -26
- package/template/prisma/migrations/20251212132339_add_custom_roles/migration.sql +0 -24
- package/template/prisma/migrations/20251215104448_add_file_interaction_types/migration.sql +0 -11
- package/template/prisma/migrations/20251215145616_add_closing_reasons/migration.sql +0 -12
- package/template/prisma/migrations/20251216140850_add_log_users/migration.sql +0 -25
- package/template/prisma/migrations/20251216151000_rename_perdu_to_ferme/migration.sql +0 -8
- package/template/prisma/migrations/20251216162318_add_column_mappings_to_google_sheet/migration.sql +0 -2
- package/template/prisma/migrations/20251216185127_add_workflows/migration.sql +0 -80
- package/template/prisma/migrations/20251216192237_add_scheduled_workflow_actions/migration.sql +0 -32
- package/template/prisma/migrations/20251220000000_add_task_interaction_type/migration.sql +0 -4
- package/template/prisma/migrations/20251221000000_add_task_type/migration.sql +0 -3
- package/template/prisma/migrations/20251221000001_add_event_color/migration.sql +0 -23
- package/template/prisma/migrations/20260210114913_add_dashboard_widget/migration.sql +0 -20
- package/template/prisma/migrations/migration_lock.toml +0 -3
- package/template/src/app/(dashboard)/users/layout.tsx +0 -30
- package/template/src/app/api/dashboard/widgets/[id]/route.ts +0 -47
- package/template/src/app/api/dashboard/widgets/route.ts +0 -181
- package/template/src/components/dashboard/add-widget-dialog.tsx +0 -161
- package/template/src/components/dashboard/color-picker.tsx +0 -65
- package/template/src/components/dashboard/contacts-chart.tsx +0 -69
- package/template/src/components/dashboard/interactions-by-type-chart.tsx +0 -121
- package/template/src/components/dashboard/recent-activity.tsx +0 -157
- package/template/src/components/dashboard/sales-analytics-chart.tsx +0 -77
- package/template/src/components/dashboard/status-distribution-chart.tsx +0 -82
- package/template/src/components/dashboard/top-contacts-list.tsx +0 -119
- package/template/src/components/dashboard/widget-wrapper.tsx +0 -39
- package/template/src/contexts/dashboard-theme-context.tsx +0 -58
- package/template/src/lib/dashboard-themes.ts +0 -140
- package/template/src/lib/default-widgets.ts +0 -14
- package/template/src/lib/widget-registry.ts +0 -177
|
@@ -0,0 +1,1703 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect, useCallback, useRef } from 'react';
|
|
4
|
+
import { useParams, useRouter } from 'next/navigation';
|
|
5
|
+
import Link from 'next/link';
|
|
6
|
+
import {
|
|
7
|
+
ArrowLeft,
|
|
8
|
+
Building2,
|
|
9
|
+
Users,
|
|
10
|
+
Trash2,
|
|
11
|
+
RefreshCw,
|
|
12
|
+
Plus,
|
|
13
|
+
Activity,
|
|
14
|
+
X,
|
|
15
|
+
Phone,
|
|
16
|
+
Mail,
|
|
17
|
+
} from 'lucide-react';
|
|
18
|
+
import { cn, normalizePhoneNumber } from '@/lib/utils';
|
|
19
|
+
import { useConfirm } from '@/hooks/use-confirm';
|
|
20
|
+
import { useUserRole } from '@/hooks/use-user-role';
|
|
21
|
+
import { ProtectedPage } from '@/components/protected-page';
|
|
22
|
+
import AddressAutocomplete from '@/components/address-autocomplete';
|
|
23
|
+
import { Spinner } from '@/components/skeleton';
|
|
24
|
+
import { useAppToast } from '@/contexts/app-toast-context';
|
|
25
|
+
|
|
26
|
+
interface CompanyContact {
|
|
27
|
+
id: string;
|
|
28
|
+
firstName: string | null;
|
|
29
|
+
lastName: string | null;
|
|
30
|
+
jobTitle: string | null;
|
|
31
|
+
phone: string;
|
|
32
|
+
email: string | null;
|
|
33
|
+
status: { id: string; name: string; color: string } | null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface CompanyDetail {
|
|
37
|
+
id: string;
|
|
38
|
+
name: string;
|
|
39
|
+
phone: string | null;
|
|
40
|
+
email: string | null;
|
|
41
|
+
address: string | null;
|
|
42
|
+
city: string | null;
|
|
43
|
+
postalCode: string | null;
|
|
44
|
+
website: string | null;
|
|
45
|
+
siret: string | null;
|
|
46
|
+
industry: string | null;
|
|
47
|
+
notes: string | null;
|
|
48
|
+
assignedCommercialId: string | null;
|
|
49
|
+
assignedCommercial: { id: string; name: string; email: string } | null;
|
|
50
|
+
assignedTeleproId: string | null;
|
|
51
|
+
assignedTelepro: { id: string; name: string; email: string } | null;
|
|
52
|
+
createdById: string | null;
|
|
53
|
+
createdBy: { id: string; name: string; email: string } | null;
|
|
54
|
+
createdAt: string;
|
|
55
|
+
updatedAt: string;
|
|
56
|
+
contacts: CompanyContact[];
|
|
57
|
+
_count: { contacts: number };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
interface CompanyActivityItem {
|
|
61
|
+
id: string;
|
|
62
|
+
type: string;
|
|
63
|
+
title: string | null;
|
|
64
|
+
content: string;
|
|
65
|
+
metadata: unknown;
|
|
66
|
+
userId: string | null;
|
|
67
|
+
user: { id: string; name: string } | null;
|
|
68
|
+
createdAt: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
interface Status {
|
|
72
|
+
id: string;
|
|
73
|
+
name: string;
|
|
74
|
+
color: string;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
interface UserOption {
|
|
78
|
+
id: string;
|
|
79
|
+
name: string;
|
|
80
|
+
email: string;
|
|
81
|
+
role?: string;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
type TabId = 'contacts' | 'activities';
|
|
85
|
+
|
|
86
|
+
export default function CompanyDetailPage() {
|
|
87
|
+
const params = useParams();
|
|
88
|
+
const router = useRouter();
|
|
89
|
+
const { hasPermission, isAdmin } = useUserRole();
|
|
90
|
+
const { confirm, ConfirmDialog } = useConfirm();
|
|
91
|
+
const toast = useAppToast();
|
|
92
|
+
const id = params.id as string;
|
|
93
|
+
|
|
94
|
+
const [company, setCompany] = useState<CompanyDetail | null>(null);
|
|
95
|
+
const [loading, setLoading] = useState(true);
|
|
96
|
+
const [error, setError] = useState('');
|
|
97
|
+
const [activeTab, setActiveTab] = useState<TabId>('contacts');
|
|
98
|
+
const [activities, setActivities] = useState<CompanyActivityItem[]>([]);
|
|
99
|
+
const [companyFormData, setCompanyFormData] = useState({
|
|
100
|
+
name: '',
|
|
101
|
+
phone: '',
|
|
102
|
+
email: '',
|
|
103
|
+
address: '',
|
|
104
|
+
city: '',
|
|
105
|
+
postalCode: '',
|
|
106
|
+
website: '',
|
|
107
|
+
siret: '',
|
|
108
|
+
industry: '',
|
|
109
|
+
notes: '',
|
|
110
|
+
assignedCommercialId: '',
|
|
111
|
+
assignedTeleproId: '',
|
|
112
|
+
});
|
|
113
|
+
const [isSavingCompany, setIsSavingCompany] = useState(false);
|
|
114
|
+
const [lastSavedCompany, setLastSavedCompany] = useState<Date | null>(null);
|
|
115
|
+
const [showAddContactModal, setShowAddContactModal] = useState(false);
|
|
116
|
+
const [contactModalMode, setContactModalMode] = useState<'create' | 'search'>('create');
|
|
117
|
+
const [newContactForm, setNewContactForm] = useState({
|
|
118
|
+
civility: '',
|
|
119
|
+
firstName: '',
|
|
120
|
+
lastName: '',
|
|
121
|
+
jobTitle: '',
|
|
122
|
+
phone: '',
|
|
123
|
+
email: '',
|
|
124
|
+
address: '',
|
|
125
|
+
city: '',
|
|
126
|
+
postalCode: '',
|
|
127
|
+
origin: '',
|
|
128
|
+
statusId: '',
|
|
129
|
+
assignedCommercialId: '',
|
|
130
|
+
assignedTeleproId: '',
|
|
131
|
+
});
|
|
132
|
+
const [savingContact, setSavingContact] = useState(false);
|
|
133
|
+
const [contactFormError, setContactFormError] = useState('');
|
|
134
|
+
const [statuses, setStatuses] = useState<Status[]>([]);
|
|
135
|
+
const [users, setUsers] = useState<UserOption[]>([]);
|
|
136
|
+
const [existingContactSearch, setExistingContactSearch] = useState('');
|
|
137
|
+
const [existingContactResults, setExistingContactResults] = useState<
|
|
138
|
+
{
|
|
139
|
+
id: string;
|
|
140
|
+
firstName: string | null;
|
|
141
|
+
lastName: string | null;
|
|
142
|
+
phone: string;
|
|
143
|
+
email: string | null;
|
|
144
|
+
company: { id: string; name: string } | null;
|
|
145
|
+
}[]
|
|
146
|
+
>([]);
|
|
147
|
+
const [addExistingSelectedId, setAddExistingSelectedId] = useState<string | null>(null);
|
|
148
|
+
const [addExistingJobTitle, setAddExistingJobTitle] = useState('');
|
|
149
|
+
const [addExistingLoading, setAddExistingLoading] = useState(false);
|
|
150
|
+
const [addExistingSearching, setAddExistingSearching] = useState(false);
|
|
151
|
+
const [addExistingError, setAddExistingError] = useState('');
|
|
152
|
+
|
|
153
|
+
const originalFormDataRef = useRef<typeof companyFormData | null>(null);
|
|
154
|
+
|
|
155
|
+
const canEdit = hasPermission('companies.edit') || hasPermission('contacts.edit');
|
|
156
|
+
const canDelete = hasPermission('companies.delete') || hasPermission('contacts.delete');
|
|
157
|
+
const canCreate = hasPermission('companies.create') || hasPermission('contacts.create');
|
|
158
|
+
const canViewActivities = hasPermission('companies.view_activities');
|
|
159
|
+
|
|
160
|
+
const fetchCompany = useCallback(async () => {
|
|
161
|
+
try {
|
|
162
|
+
setLoading(true);
|
|
163
|
+
const res = await fetch(`/api/companies/${id}`);
|
|
164
|
+
if (!res.ok) throw new Error('Entreprise non trouvée');
|
|
165
|
+
const data = await res.json();
|
|
166
|
+
setCompany(data);
|
|
167
|
+
const initialForm = {
|
|
168
|
+
name: data.name ?? '',
|
|
169
|
+
phone: data.phone ?? '',
|
|
170
|
+
email: data.email ?? '',
|
|
171
|
+
address: data.address ?? '',
|
|
172
|
+
city: data.city ?? '',
|
|
173
|
+
postalCode: data.postalCode ?? '',
|
|
174
|
+
website: data.website ?? '',
|
|
175
|
+
siret: data.siret ?? '',
|
|
176
|
+
industry: data.industry ?? '',
|
|
177
|
+
notes: data.notes ?? '',
|
|
178
|
+
assignedCommercialId: data.assignedCommercialId ?? '',
|
|
179
|
+
assignedTeleproId: data.assignedTeleproId ?? '',
|
|
180
|
+
};
|
|
181
|
+
setCompanyFormData(initialForm);
|
|
182
|
+
originalFormDataRef.current = { ...initialForm };
|
|
183
|
+
} catch (err: unknown) {
|
|
184
|
+
setError(err instanceof Error ? err.message : 'Erreur');
|
|
185
|
+
} finally {
|
|
186
|
+
setLoading(false);
|
|
187
|
+
}
|
|
188
|
+
}, [id]);
|
|
189
|
+
|
|
190
|
+
const fetchActivities = useCallback(async () => {
|
|
191
|
+
if (!canViewActivities) return;
|
|
192
|
+
try {
|
|
193
|
+
const res = await fetch(`/api/companies/${id}/activities`);
|
|
194
|
+
if (res.ok) {
|
|
195
|
+
const data = await res.json();
|
|
196
|
+
setActivities(data);
|
|
197
|
+
}
|
|
198
|
+
} catch {
|
|
199
|
+
// ignore
|
|
200
|
+
}
|
|
201
|
+
}, [id, canViewActivities]);
|
|
202
|
+
|
|
203
|
+
useEffect(() => {
|
|
204
|
+
fetchCompany();
|
|
205
|
+
}, [fetchCompany]);
|
|
206
|
+
|
|
207
|
+
useEffect(() => {
|
|
208
|
+
if (company) fetchActivities();
|
|
209
|
+
}, [company, fetchActivities]);
|
|
210
|
+
|
|
211
|
+
useEffect(() => {
|
|
212
|
+
if (!canViewActivities && activeTab === 'activities') setActiveTab('contacts');
|
|
213
|
+
}, [canViewActivities, activeTab]);
|
|
214
|
+
|
|
215
|
+
useEffect(() => {
|
|
216
|
+
if (!error) return;
|
|
217
|
+
toast.error(error);
|
|
218
|
+
setError('');
|
|
219
|
+
}, [error, toast]);
|
|
220
|
+
|
|
221
|
+
useEffect(() => {
|
|
222
|
+
if (!contactFormError) return;
|
|
223
|
+
toast.error(contactFormError);
|
|
224
|
+
setContactFormError('');
|
|
225
|
+
}, [contactFormError, toast]);
|
|
226
|
+
|
|
227
|
+
useEffect(() => {
|
|
228
|
+
if (!addExistingError) return;
|
|
229
|
+
toast.error(addExistingError);
|
|
230
|
+
setAddExistingError('');
|
|
231
|
+
}, [addExistingError, toast]);
|
|
232
|
+
|
|
233
|
+
// Entreprise non trouvée → redirection
|
|
234
|
+
useEffect(() => {
|
|
235
|
+
if (loading) return;
|
|
236
|
+
if (error && !company) {
|
|
237
|
+
router.replace('/contacts?entity=companies');
|
|
238
|
+
}
|
|
239
|
+
}, [loading, error, company, router]);
|
|
240
|
+
|
|
241
|
+
useEffect(() => {
|
|
242
|
+
if (company && canEdit) {
|
|
243
|
+
void (async () => {
|
|
244
|
+
const response = await fetch('/api/users/list');
|
|
245
|
+
if (!response.ok) return;
|
|
246
|
+
const data = await response.json();
|
|
247
|
+
setUsers(data || []);
|
|
248
|
+
})();
|
|
249
|
+
}
|
|
250
|
+
}, [company, canEdit]);
|
|
251
|
+
|
|
252
|
+
const handleSaveCompany = useCallback(
|
|
253
|
+
async (silent = false) => {
|
|
254
|
+
if (!company || !canEdit) return;
|
|
255
|
+
setIsSavingCompany(true);
|
|
256
|
+
if (!silent) setError('');
|
|
257
|
+
try {
|
|
258
|
+
const payload = {
|
|
259
|
+
name: companyFormData.name || undefined,
|
|
260
|
+
phone: companyFormData.phone || null,
|
|
261
|
+
email: companyFormData.email || null,
|
|
262
|
+
address: companyFormData.address || null,
|
|
263
|
+
city: companyFormData.city || null,
|
|
264
|
+
postalCode: companyFormData.postalCode || null,
|
|
265
|
+
website: companyFormData.website || null,
|
|
266
|
+
siret: companyFormData.siret || null,
|
|
267
|
+
industry: companyFormData.industry || null,
|
|
268
|
+
notes: companyFormData.notes || null,
|
|
269
|
+
assignedCommercialId: companyFormData.assignedCommercialId || null,
|
|
270
|
+
assignedTeleproId: companyFormData.assignedTeleproId || null,
|
|
271
|
+
};
|
|
272
|
+
const res = await fetch(`/api/companies/${id}`, {
|
|
273
|
+
method: 'PUT',
|
|
274
|
+
headers: { 'Content-Type': 'application/json' },
|
|
275
|
+
body: JSON.stringify(payload),
|
|
276
|
+
});
|
|
277
|
+
if (!res.ok) throw new Error("Erreur lors de l'enregistrement");
|
|
278
|
+
const updated = await res.json();
|
|
279
|
+
setCompany(updated);
|
|
280
|
+
setLastSavedCompany(new Date());
|
|
281
|
+
originalFormDataRef.current = { ...companyFormData };
|
|
282
|
+
await fetchActivities();
|
|
283
|
+
} catch (err: unknown) {
|
|
284
|
+
if (!silent) setError(err instanceof Error ? err.message : 'Erreur');
|
|
285
|
+
} finally {
|
|
286
|
+
setIsSavingCompany(false);
|
|
287
|
+
}
|
|
288
|
+
},
|
|
289
|
+
[company, canEdit, id, companyFormData, fetchActivities],
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
const hasFormDataChanged = useCallback(() => {
|
|
293
|
+
if (!originalFormDataRef.current) return false;
|
|
294
|
+
const o = originalFormDataRef.current;
|
|
295
|
+
return (
|
|
296
|
+
companyFormData.name !== o.name ||
|
|
297
|
+
companyFormData.phone !== o.phone ||
|
|
298
|
+
companyFormData.email !== o.email ||
|
|
299
|
+
companyFormData.address !== o.address ||
|
|
300
|
+
companyFormData.city !== o.city ||
|
|
301
|
+
companyFormData.postalCode !== o.postalCode ||
|
|
302
|
+
companyFormData.website !== o.website ||
|
|
303
|
+
companyFormData.siret !== o.siret ||
|
|
304
|
+
companyFormData.industry !== o.industry ||
|
|
305
|
+
companyFormData.notes !== o.notes ||
|
|
306
|
+
companyFormData.assignedCommercialId !== o.assignedCommercialId ||
|
|
307
|
+
companyFormData.assignedTeleproId !== o.assignedTeleproId
|
|
308
|
+
);
|
|
309
|
+
}, [companyFormData]);
|
|
310
|
+
|
|
311
|
+
const saveIfDirty = useCallback(async () => {
|
|
312
|
+
if (hasFormDataChanged()) await handleSaveCompany(true);
|
|
313
|
+
}, [hasFormDataChanged, handleSaveCompany]);
|
|
314
|
+
|
|
315
|
+
useEffect(() => {
|
|
316
|
+
const onBeforeUnload = (e: BeforeUnloadEvent) => {
|
|
317
|
+
if (hasFormDataChanged()) e.preventDefault();
|
|
318
|
+
};
|
|
319
|
+
globalThis.addEventListener('beforeunload', onBeforeUnload);
|
|
320
|
+
return () => globalThis.removeEventListener('beforeunload', onBeforeUnload);
|
|
321
|
+
}, [hasFormDataChanged]);
|
|
322
|
+
|
|
323
|
+
useEffect(() => {
|
|
324
|
+
return () => {
|
|
325
|
+
const original = originalFormDataRef.current;
|
|
326
|
+
if (!original) return;
|
|
327
|
+
const changed =
|
|
328
|
+
companyFormData.name !== original.name ||
|
|
329
|
+
companyFormData.phone !== original.phone ||
|
|
330
|
+
companyFormData.email !== original.email ||
|
|
331
|
+
companyFormData.address !== original.address ||
|
|
332
|
+
companyFormData.city !== original.city ||
|
|
333
|
+
companyFormData.postalCode !== original.postalCode ||
|
|
334
|
+
companyFormData.website !== original.website ||
|
|
335
|
+
companyFormData.siret !== original.siret ||
|
|
336
|
+
companyFormData.industry !== original.industry ||
|
|
337
|
+
companyFormData.notes !== original.notes ||
|
|
338
|
+
companyFormData.assignedCommercialId !== original.assignedCommercialId ||
|
|
339
|
+
companyFormData.assignedTeleproId !== original.assignedTeleproId;
|
|
340
|
+
if (!changed) return;
|
|
341
|
+
fetch(`/api/companies/${id}`, {
|
|
342
|
+
method: 'PUT',
|
|
343
|
+
headers: { 'Content-Type': 'application/json' },
|
|
344
|
+
body: JSON.stringify({
|
|
345
|
+
name: companyFormData.name || undefined,
|
|
346
|
+
phone: companyFormData.phone || null,
|
|
347
|
+
email: companyFormData.email || null,
|
|
348
|
+
address: companyFormData.address || null,
|
|
349
|
+
city: companyFormData.city || null,
|
|
350
|
+
postalCode: companyFormData.postalCode || null,
|
|
351
|
+
website: companyFormData.website || null,
|
|
352
|
+
siret: companyFormData.siret || null,
|
|
353
|
+
industry: companyFormData.industry || null,
|
|
354
|
+
notes: companyFormData.notes || null,
|
|
355
|
+
assignedCommercialId: companyFormData.assignedCommercialId || null,
|
|
356
|
+
assignedTeleproId: companyFormData.assignedTeleproId || null,
|
|
357
|
+
}),
|
|
358
|
+
keepalive: true,
|
|
359
|
+
});
|
|
360
|
+
};
|
|
361
|
+
}, [id, companyFormData]);
|
|
362
|
+
|
|
363
|
+
useEffect(() => {
|
|
364
|
+
originalFormDataRef.current = null;
|
|
365
|
+
setLastSavedCompany(null);
|
|
366
|
+
}, [id]);
|
|
367
|
+
|
|
368
|
+
const openAddContactModal = async (mode: 'create' | 'search' = 'create') => {
|
|
369
|
+
await saveIfDirty();
|
|
370
|
+
setContactModalMode(mode);
|
|
371
|
+
if (mode === 'create') {
|
|
372
|
+
setNewContactForm({
|
|
373
|
+
civility: '',
|
|
374
|
+
firstName: '',
|
|
375
|
+
lastName: '',
|
|
376
|
+
jobTitle: '',
|
|
377
|
+
phone: '',
|
|
378
|
+
email: '',
|
|
379
|
+
address: company?.address ?? '',
|
|
380
|
+
city: company?.city ?? '',
|
|
381
|
+
postalCode: company?.postalCode ?? '',
|
|
382
|
+
origin: '',
|
|
383
|
+
statusId: '',
|
|
384
|
+
assignedCommercialId: '',
|
|
385
|
+
assignedTeleproId: '',
|
|
386
|
+
});
|
|
387
|
+
setContactFormError('');
|
|
388
|
+
void (async () => {
|
|
389
|
+
const [statusesResponse, usersResponse] = await Promise.all([
|
|
390
|
+
fetch('/api/statuses'),
|
|
391
|
+
fetch('/api/users/list'),
|
|
392
|
+
]);
|
|
393
|
+
if (statusesResponse.ok) {
|
|
394
|
+
const statusesData = await statusesResponse.json();
|
|
395
|
+
setStatuses(statusesData || []);
|
|
396
|
+
}
|
|
397
|
+
if (usersResponse.ok) {
|
|
398
|
+
const usersData = await usersResponse.json();
|
|
399
|
+
setUsers(usersData || []);
|
|
400
|
+
}
|
|
401
|
+
})();
|
|
402
|
+
} else {
|
|
403
|
+
setExistingContactSearch('');
|
|
404
|
+
setExistingContactResults([]);
|
|
405
|
+
setAddExistingSelectedId(null);
|
|
406
|
+
setAddExistingJobTitle('');
|
|
407
|
+
setAddExistingError('');
|
|
408
|
+
}
|
|
409
|
+
setShowAddContactModal(true);
|
|
410
|
+
};
|
|
411
|
+
|
|
412
|
+
const searchExistingContacts = useCallback(async () => {
|
|
413
|
+
if (!existingContactSearch.trim()) {
|
|
414
|
+
setExistingContactResults([]);
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
setAddExistingSearching(true);
|
|
418
|
+
setAddExistingError('');
|
|
419
|
+
try {
|
|
420
|
+
const res = await fetch(
|
|
421
|
+
`/api/contacts?search=${encodeURIComponent(existingContactSearch.trim())}&limit=30&page=1`,
|
|
422
|
+
);
|
|
423
|
+
if (!res.ok) throw new Error('Recherche impossible');
|
|
424
|
+
const data = await res.json();
|
|
425
|
+
const alreadyInCompany = new Set((company?.contacts ?? []).map((c) => c.id));
|
|
426
|
+
const filtered = (data.contacts ?? []).filter(
|
|
427
|
+
(c: { id: string }) => !alreadyInCompany.has(c.id),
|
|
428
|
+
);
|
|
429
|
+
setExistingContactResults(filtered);
|
|
430
|
+
} catch {
|
|
431
|
+
setAddExistingError('Erreur lors de la recherche');
|
|
432
|
+
setExistingContactResults([]);
|
|
433
|
+
} finally {
|
|
434
|
+
setAddExistingSearching(false);
|
|
435
|
+
}
|
|
436
|
+
}, [existingContactSearch, company?.contacts]);
|
|
437
|
+
|
|
438
|
+
// Recherche en direct (debounce) quand on tape dans l'onglet « Rechercher un contact existant »
|
|
439
|
+
const searchDebounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
440
|
+
useEffect(() => {
|
|
441
|
+
if (!showAddContactModal || contactModalMode !== 'search') return;
|
|
442
|
+
if (searchDebounceRef.current) clearTimeout(searchDebounceRef.current);
|
|
443
|
+
searchDebounceRef.current = setTimeout(() => {
|
|
444
|
+
searchDebounceRef.current = null;
|
|
445
|
+
searchExistingContacts();
|
|
446
|
+
}, 400);
|
|
447
|
+
return () => {
|
|
448
|
+
if (searchDebounceRef.current) clearTimeout(searchDebounceRef.current);
|
|
449
|
+
};
|
|
450
|
+
}, [existingContactSearch, showAddContactModal, contactModalMode, searchExistingContacts]);
|
|
451
|
+
|
|
452
|
+
const handleAddExistingContact = async () => {
|
|
453
|
+
if (!addExistingSelectedId || !company) return;
|
|
454
|
+
setAddExistingLoading(true);
|
|
455
|
+
setAddExistingError('');
|
|
456
|
+
try {
|
|
457
|
+
const res = await fetch(`/api/contacts/${addExistingSelectedId}`, {
|
|
458
|
+
method: 'PUT',
|
|
459
|
+
headers: { 'Content-Type': 'application/json' },
|
|
460
|
+
body: JSON.stringify({
|
|
461
|
+
companyId: id,
|
|
462
|
+
jobTitle: addExistingJobTitle.trim() || null,
|
|
463
|
+
}),
|
|
464
|
+
});
|
|
465
|
+
if (!res.ok) {
|
|
466
|
+
const err = await res.json().catch(() => ({}));
|
|
467
|
+
throw new Error(err.error || "Impossible d'ajouter le contact");
|
|
468
|
+
}
|
|
469
|
+
const updated = await res.json();
|
|
470
|
+
const contactName =
|
|
471
|
+
[updated.firstName, updated.lastName].filter(Boolean).join(' ') || 'Contact';
|
|
472
|
+
await fetch(`/api/companies/${id}/activities`, {
|
|
473
|
+
method: 'POST',
|
|
474
|
+
headers: { 'Content-Type': 'application/json' },
|
|
475
|
+
body: JSON.stringify({
|
|
476
|
+
type: 'CONTACT_ADDED',
|
|
477
|
+
title: 'Contact associé',
|
|
478
|
+
content: `${contactName} a été associé à l'entreprise.`,
|
|
479
|
+
metadata: { contactId: updated.id },
|
|
480
|
+
}),
|
|
481
|
+
});
|
|
482
|
+
await fetchCompany();
|
|
483
|
+
await fetchActivities();
|
|
484
|
+
setShowAddContactModal(false);
|
|
485
|
+
} catch (err: unknown) {
|
|
486
|
+
setAddExistingError(err instanceof Error ? err.message : 'Erreur');
|
|
487
|
+
} finally {
|
|
488
|
+
setAddExistingLoading(false);
|
|
489
|
+
}
|
|
490
|
+
};
|
|
491
|
+
|
|
492
|
+
const handleCreateContact = async (e: React.FormEvent) => {
|
|
493
|
+
e.preventDefault();
|
|
494
|
+
setContactFormError('');
|
|
495
|
+
await saveIfDirty();
|
|
496
|
+
if (!newContactForm.phone?.trim()) {
|
|
497
|
+
setContactFormError('Le téléphone est obligatoire');
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
setSavingContact(true);
|
|
501
|
+
try {
|
|
502
|
+
const res = await fetch('/api/contacts', {
|
|
503
|
+
method: 'POST',
|
|
504
|
+
headers: { 'Content-Type': 'application/json' },
|
|
505
|
+
body: JSON.stringify({
|
|
506
|
+
...newContactForm,
|
|
507
|
+
companyId: id,
|
|
508
|
+
civility: newContactForm.civility || null,
|
|
509
|
+
jobTitle: newContactForm.jobTitle || null,
|
|
510
|
+
assignedCommercialId: newContactForm.assignedCommercialId || null,
|
|
511
|
+
assignedTeleproId: newContactForm.assignedTeleproId || null,
|
|
512
|
+
statusId: newContactForm.statusId || null,
|
|
513
|
+
}),
|
|
514
|
+
});
|
|
515
|
+
const data = await res.json();
|
|
516
|
+
if (!res.ok) throw new Error(data.error || 'Erreur');
|
|
517
|
+
const contactName =
|
|
518
|
+
[newContactForm.firstName, newContactForm.lastName].filter(Boolean).join(' ') ||
|
|
519
|
+
'Nouveau contact';
|
|
520
|
+
await fetch(`/api/companies/${id}/activities`, {
|
|
521
|
+
method: 'POST',
|
|
522
|
+
headers: { 'Content-Type': 'application/json' },
|
|
523
|
+
body: JSON.stringify({
|
|
524
|
+
type: 'CONTACT_ADDED',
|
|
525
|
+
title: 'Contact ajouté',
|
|
526
|
+
content: `${contactName} a été ajouté à l'entreprise.`,
|
|
527
|
+
metadata: { contactId: data.id },
|
|
528
|
+
}),
|
|
529
|
+
});
|
|
530
|
+
await fetchCompany();
|
|
531
|
+
await fetchActivities();
|
|
532
|
+
setShowAddContactModal(false);
|
|
533
|
+
} catch (err: unknown) {
|
|
534
|
+
setContactFormError(err instanceof Error ? err.message : 'Erreur');
|
|
535
|
+
} finally {
|
|
536
|
+
setSavingContact(false);
|
|
537
|
+
}
|
|
538
|
+
};
|
|
539
|
+
|
|
540
|
+
const handleDelete = async () => {
|
|
541
|
+
const ok = await confirm({
|
|
542
|
+
title: "Supprimer l'entreprise",
|
|
543
|
+
description: `Êtes-vous sûr de vouloir supprimer "${company?.name}" ? Les contacts liés ne seront pas supprimés, mais le lien sera retiré.`,
|
|
544
|
+
confirmText: 'Supprimer',
|
|
545
|
+
variant: 'destructive',
|
|
546
|
+
});
|
|
547
|
+
if (!ok) return;
|
|
548
|
+
|
|
549
|
+
try {
|
|
550
|
+
const res = await fetch(`/api/companies/${id}`, { method: 'DELETE' });
|
|
551
|
+
if (!res.ok) throw new Error('Erreur lors de la suppression');
|
|
552
|
+
router.push('/contacts?entity=companies');
|
|
553
|
+
} catch (err: unknown) {
|
|
554
|
+
setError(err instanceof Error ? err.message : 'Erreur');
|
|
555
|
+
}
|
|
556
|
+
};
|
|
557
|
+
|
|
558
|
+
if (loading && !company) {
|
|
559
|
+
return (
|
|
560
|
+
<div className="flex h-full items-center justify-center">
|
|
561
|
+
<div className="h-12 w-12 animate-spin rounded-full border-4 border-blue-200 border-t-blue-600" />
|
|
562
|
+
</div>
|
|
563
|
+
);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
if (error && !company) return null;
|
|
567
|
+
|
|
568
|
+
if (!company) return null;
|
|
569
|
+
|
|
570
|
+
return (
|
|
571
|
+
<ProtectedPage
|
|
572
|
+
requiredPermission={[
|
|
573
|
+
'contacts.view_all',
|
|
574
|
+
'contacts.view_own',
|
|
575
|
+
'companies.view_all',
|
|
576
|
+
'companies.view_own',
|
|
577
|
+
]}
|
|
578
|
+
>
|
|
579
|
+
<div className="flex h-full flex-col">
|
|
580
|
+
<ConfirmDialog />
|
|
581
|
+
|
|
582
|
+
{/* Header (même style que page contact) */}
|
|
583
|
+
<div className="border-b border-gray-200 bg-white px-4 py-3 sm:px-6 sm:py-3.5 lg:px-8">
|
|
584
|
+
<div className="flex items-center justify-between gap-3">
|
|
585
|
+
<div className="flex items-center gap-2 sm:gap-4">
|
|
586
|
+
<Link
|
|
587
|
+
href="/contacts?entity=companies"
|
|
588
|
+
className="flex cursor-pointer items-center gap-2 text-sm text-gray-600 hover:text-gray-900"
|
|
589
|
+
>
|
|
590
|
+
<ArrowLeft />
|
|
591
|
+
</Link>
|
|
592
|
+
<h1 className="text-base font-semibold text-gray-900 sm:text-lg">Entreprises</h1>
|
|
593
|
+
</div>
|
|
594
|
+
<div className="flex items-center gap-1 sm:gap-2">
|
|
595
|
+
<button
|
|
596
|
+
onClick={() => saveIfDirty().then(() => fetchCompany())}
|
|
597
|
+
className="cursor-pointer rounded-lg p-2 text-gray-600 transition-colors hover:bg-gray-100"
|
|
598
|
+
title="Actualiser"
|
|
599
|
+
>
|
|
600
|
+
<RefreshCw className="h-5 w-5" />
|
|
601
|
+
</button>
|
|
602
|
+
{canDelete && (
|
|
603
|
+
<button
|
|
604
|
+
onClick={handleDelete}
|
|
605
|
+
className="cursor-pointer rounded-lg p-2 text-red-600 transition-colors hover:bg-red-50"
|
|
606
|
+
title="Supprimer l'entreprise"
|
|
607
|
+
>
|
|
608
|
+
<Trash2 className="h-5 w-5" />
|
|
609
|
+
</button>
|
|
610
|
+
)}
|
|
611
|
+
</div>
|
|
612
|
+
</div>
|
|
613
|
+
</div>
|
|
614
|
+
|
|
615
|
+
{/* Section Profil (avatar + nom + boutons d'action, comme contact) */}
|
|
616
|
+
<div className="border-b border-gray-200 bg-white px-4 py-3 sm:px-6 sm:py-4 lg:px-8">
|
|
617
|
+
<div className="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
|
|
618
|
+
<div className="flex items-center gap-3 sm:gap-4">
|
|
619
|
+
<div className="flex h-12 w-12 shrink-0 items-center justify-center rounded-full bg-blue-100 text-lg font-semibold text-blue-800 sm:h-16 sm:w-16 sm:text-xl">
|
|
620
|
+
<Building2 className="h-6 w-6 sm:h-8 sm:w-8" />
|
|
621
|
+
</div>
|
|
622
|
+
<div className="min-w-0">
|
|
623
|
+
<h2 className="truncate text-base font-bold text-gray-900 sm:text-lg">
|
|
624
|
+
{company.name}
|
|
625
|
+
</h2>
|
|
626
|
+
<p className="mt-1 truncate text-xs text-gray-500 sm:text-sm">
|
|
627
|
+
{company._count.contacts} contact{company._count.contacts !== 1 ? 's' : ''}{' '}
|
|
628
|
+
associé
|
|
629
|
+
{company._count.contacts !== 1 ? 's' : ''}
|
|
630
|
+
</p>
|
|
631
|
+
</div>
|
|
632
|
+
</div>
|
|
633
|
+
<div className="flex flex-wrap items-center gap-1.5 sm:gap-2">
|
|
634
|
+
{canCreate && (
|
|
635
|
+
<div className="flex flex-wrap items-center gap-1.5">
|
|
636
|
+
{(canCreate || canEdit) && (
|
|
637
|
+
<button
|
|
638
|
+
type="button"
|
|
639
|
+
onClick={() => openAddContactModal('create')}
|
|
640
|
+
className="inline-flex cursor-pointer items-center gap-1.5 rounded-lg bg-blue-600 px-2 py-1.5 text-xs font-medium text-white transition-colors hover:bg-blue-700 sm:px-4 sm:py-2 sm:text-sm"
|
|
641
|
+
>
|
|
642
|
+
<Plus className="h-3 w-3 sm:h-4 sm:w-4" />
|
|
643
|
+
<span className="hidden sm:inline">Ajouter un contact</span>
|
|
644
|
+
<span className="sm:hidden">Ajouter</span>
|
|
645
|
+
</button>
|
|
646
|
+
)}
|
|
647
|
+
</div>
|
|
648
|
+
)}
|
|
649
|
+
</div>
|
|
650
|
+
</div>
|
|
651
|
+
</div>
|
|
652
|
+
|
|
653
|
+
{/* Contenu principal - Deux colonnes (comme page contact) */}
|
|
654
|
+
<div className="flex-1 overflow-y-auto">
|
|
655
|
+
<div className="flex flex-col gap-6 p-4 lg:flex-row lg:p-6">
|
|
656
|
+
{/* Colonne gauche - Formulaire entreprise éditable */}
|
|
657
|
+
<div className="w-full space-y-4 lg:sticky lg:top-6 lg:max-h-[calc(100vh-3rem)] lg:w-1/3 lg:self-start lg:overflow-y-auto lg:pb-4">
|
|
658
|
+
<div className="rounded-lg bg-white p-3 shadow sm:p-4">
|
|
659
|
+
{canEdit ? (
|
|
660
|
+
<form
|
|
661
|
+
className="space-y-3"
|
|
662
|
+
onSubmit={(e) => {
|
|
663
|
+
e.preventDefault();
|
|
664
|
+
handleSaveCompany();
|
|
665
|
+
}}
|
|
666
|
+
>
|
|
667
|
+
<div>
|
|
668
|
+
<label
|
|
669
|
+
htmlFor="company-name"
|
|
670
|
+
className="mb-1 block text-sm font-medium text-gray-700"
|
|
671
|
+
>
|
|
672
|
+
Nom *
|
|
673
|
+
</label>
|
|
674
|
+
<input
|
|
675
|
+
id="company-name"
|
|
676
|
+
type="text"
|
|
677
|
+
value={companyFormData.name}
|
|
678
|
+
onChange={(e) =>
|
|
679
|
+
setCompanyFormData({ ...companyFormData, name: e.target.value })
|
|
680
|
+
}
|
|
681
|
+
className="w-full rounded-lg border border-gray-300 px-2 py-1.5 text-sm text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
|
|
682
|
+
required
|
|
683
|
+
/>
|
|
684
|
+
</div>
|
|
685
|
+
<div className="grid grid-cols-2 gap-2">
|
|
686
|
+
<div>
|
|
687
|
+
<label
|
|
688
|
+
htmlFor="company-phone"
|
|
689
|
+
className="mb-1 block text-sm font-medium text-gray-700"
|
|
690
|
+
>
|
|
691
|
+
Téléphone
|
|
692
|
+
</label>
|
|
693
|
+
<input
|
|
694
|
+
id="company-phone"
|
|
695
|
+
type="tel"
|
|
696
|
+
value={companyFormData.phone}
|
|
697
|
+
onChange={(e) =>
|
|
698
|
+
setCompanyFormData({ ...companyFormData, phone: e.target.value })
|
|
699
|
+
}
|
|
700
|
+
className="w-full rounded-lg border border-gray-300 px-2 py-1.5 text-sm text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
|
|
701
|
+
/>
|
|
702
|
+
</div>
|
|
703
|
+
<div>
|
|
704
|
+
<label
|
|
705
|
+
htmlFor="company-email"
|
|
706
|
+
className="mb-1 block text-sm font-medium text-gray-700"
|
|
707
|
+
>
|
|
708
|
+
Email
|
|
709
|
+
</label>
|
|
710
|
+
<input
|
|
711
|
+
id="company-email"
|
|
712
|
+
type="email"
|
|
713
|
+
value={companyFormData.email}
|
|
714
|
+
onChange={(e) =>
|
|
715
|
+
setCompanyFormData({ ...companyFormData, email: e.target.value })
|
|
716
|
+
}
|
|
717
|
+
className="w-full rounded-lg border border-gray-300 px-2 py-1.5 text-sm text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
|
|
718
|
+
/>
|
|
719
|
+
</div>
|
|
720
|
+
</div>
|
|
721
|
+
<div>
|
|
722
|
+
<label
|
|
723
|
+
htmlFor="company-address"
|
|
724
|
+
className="mb-1 block text-sm font-medium text-gray-700"
|
|
725
|
+
>
|
|
726
|
+
Adresse
|
|
727
|
+
</label>
|
|
728
|
+
<AddressAutocomplete
|
|
729
|
+
value={companyFormData.address}
|
|
730
|
+
onChange={(address, components) => {
|
|
731
|
+
if (components) {
|
|
732
|
+
setCompanyFormData({
|
|
733
|
+
...companyFormData,
|
|
734
|
+
address: components.street,
|
|
735
|
+
city: components.city,
|
|
736
|
+
postalCode: components.postalCode,
|
|
737
|
+
});
|
|
738
|
+
} else {
|
|
739
|
+
setCompanyFormData({ ...companyFormData, address });
|
|
740
|
+
}
|
|
741
|
+
}}
|
|
742
|
+
placeholder="Rechercher une adresse..."
|
|
743
|
+
/>
|
|
744
|
+
</div>
|
|
745
|
+
<div className="grid grid-cols-2 gap-2">
|
|
746
|
+
<div>
|
|
747
|
+
<label
|
|
748
|
+
htmlFor="company-city"
|
|
749
|
+
className="mb-1 block text-sm font-medium text-gray-700"
|
|
750
|
+
>
|
|
751
|
+
Ville
|
|
752
|
+
</label>
|
|
753
|
+
<input
|
|
754
|
+
id="company-city"
|
|
755
|
+
type="text"
|
|
756
|
+
value={companyFormData.city}
|
|
757
|
+
onChange={(e) =>
|
|
758
|
+
setCompanyFormData({ ...companyFormData, city: e.target.value })
|
|
759
|
+
}
|
|
760
|
+
className="w-full rounded-lg border border-gray-300 px-2 py-1.5 text-sm text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
|
|
761
|
+
/>
|
|
762
|
+
</div>
|
|
763
|
+
<div>
|
|
764
|
+
<label
|
|
765
|
+
htmlFor="company-postalCode"
|
|
766
|
+
className="mb-1 block text-sm font-medium text-gray-700"
|
|
767
|
+
>
|
|
768
|
+
Code postal
|
|
769
|
+
</label>
|
|
770
|
+
<input
|
|
771
|
+
id="company-postalCode"
|
|
772
|
+
type="text"
|
|
773
|
+
value={companyFormData.postalCode}
|
|
774
|
+
onChange={(e) =>
|
|
775
|
+
setCompanyFormData({ ...companyFormData, postalCode: e.target.value })
|
|
776
|
+
}
|
|
777
|
+
className="w-full rounded-lg border border-gray-300 px-2 py-1.5 text-sm text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
|
|
778
|
+
/>
|
|
779
|
+
</div>
|
|
780
|
+
</div>
|
|
781
|
+
<div>
|
|
782
|
+
<label
|
|
783
|
+
htmlFor="company-website"
|
|
784
|
+
className="mb-1 block text-sm font-medium text-gray-700"
|
|
785
|
+
>
|
|
786
|
+
Site web
|
|
787
|
+
</label>
|
|
788
|
+
<input
|
|
789
|
+
id="company-website"
|
|
790
|
+
type="url"
|
|
791
|
+
value={companyFormData.website}
|
|
792
|
+
onChange={(e) =>
|
|
793
|
+
setCompanyFormData({ ...companyFormData, website: e.target.value })
|
|
794
|
+
}
|
|
795
|
+
placeholder="https://"
|
|
796
|
+
className="w-full rounded-lg border border-gray-300 px-2 py-1.5 text-sm text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
|
|
797
|
+
/>
|
|
798
|
+
</div>
|
|
799
|
+
<div className="grid grid-cols-2 gap-2">
|
|
800
|
+
<div>
|
|
801
|
+
<label
|
|
802
|
+
htmlFor="company-siret"
|
|
803
|
+
className="mb-1 block text-sm font-medium text-gray-700"
|
|
804
|
+
>
|
|
805
|
+
SIRET
|
|
806
|
+
</label>
|
|
807
|
+
<input
|
|
808
|
+
id="company-siret"
|
|
809
|
+
type="text"
|
|
810
|
+
value={companyFormData.siret}
|
|
811
|
+
onChange={(e) =>
|
|
812
|
+
setCompanyFormData({ ...companyFormData, siret: e.target.value })
|
|
813
|
+
}
|
|
814
|
+
className="w-full rounded-lg border border-gray-300 px-2 py-1.5 text-sm text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
|
|
815
|
+
/>
|
|
816
|
+
</div>
|
|
817
|
+
<div>
|
|
818
|
+
<label
|
|
819
|
+
htmlFor="company-industry"
|
|
820
|
+
className="mb-1 block text-sm font-medium text-gray-700"
|
|
821
|
+
>
|
|
822
|
+
Secteur
|
|
823
|
+
</label>
|
|
824
|
+
<input
|
|
825
|
+
id="company-industry"
|
|
826
|
+
type="text"
|
|
827
|
+
value={companyFormData.industry}
|
|
828
|
+
onChange={(e) =>
|
|
829
|
+
setCompanyFormData({ ...companyFormData, industry: e.target.value })
|
|
830
|
+
}
|
|
831
|
+
className="w-full rounded-lg border border-gray-300 px-2 py-1.5 text-sm text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
|
|
832
|
+
/>
|
|
833
|
+
</div>
|
|
834
|
+
</div>
|
|
835
|
+
<div>
|
|
836
|
+
<label
|
|
837
|
+
htmlFor="company-notes"
|
|
838
|
+
className="mb-1 block text-sm font-medium text-gray-700"
|
|
839
|
+
>
|
|
840
|
+
Notes
|
|
841
|
+
</label>
|
|
842
|
+
<textarea
|
|
843
|
+
id="company-notes"
|
|
844
|
+
value={companyFormData.notes}
|
|
845
|
+
onChange={(e) =>
|
|
846
|
+
setCompanyFormData({ ...companyFormData, notes: e.target.value })
|
|
847
|
+
}
|
|
848
|
+
rows={3}
|
|
849
|
+
className="w-full rounded-lg border border-gray-300 px-2 py-1.5 text-sm text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
|
|
850
|
+
/>
|
|
851
|
+
</div>
|
|
852
|
+
<div className="grid grid-cols-2 gap-2 border-t border-gray-100 pt-3">
|
|
853
|
+
<div>
|
|
854
|
+
<label
|
|
855
|
+
htmlFor="company-commercial"
|
|
856
|
+
className="mb-1 block text-xs font-medium text-gray-500"
|
|
857
|
+
>
|
|
858
|
+
Commercial
|
|
859
|
+
</label>
|
|
860
|
+
<select
|
|
861
|
+
id="company-commercial"
|
|
862
|
+
value={companyFormData.assignedCommercialId}
|
|
863
|
+
onChange={(e) =>
|
|
864
|
+
setCompanyFormData({
|
|
865
|
+
...companyFormData,
|
|
866
|
+
assignedCommercialId: e.target.value,
|
|
867
|
+
})
|
|
868
|
+
}
|
|
869
|
+
className="w-full rounded-lg border border-gray-300 px-2 py-1.5 text-sm text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
|
|
870
|
+
>
|
|
871
|
+
<option value="">Non attribué</option>
|
|
872
|
+
{users.map((u) => (
|
|
873
|
+
<option key={u.id} value={u.id}>
|
|
874
|
+
{u.name}
|
|
875
|
+
</option>
|
|
876
|
+
))}
|
|
877
|
+
</select>
|
|
878
|
+
</div>
|
|
879
|
+
<div>
|
|
880
|
+
<label
|
|
881
|
+
htmlFor="company-telepro"
|
|
882
|
+
className="mb-1 block text-xs font-medium text-gray-500"
|
|
883
|
+
>
|
|
884
|
+
Télépro
|
|
885
|
+
</label>
|
|
886
|
+
<select
|
|
887
|
+
id="company-telepro"
|
|
888
|
+
value={companyFormData.assignedTeleproId}
|
|
889
|
+
onChange={(e) =>
|
|
890
|
+
setCompanyFormData({
|
|
891
|
+
...companyFormData,
|
|
892
|
+
assignedTeleproId: e.target.value,
|
|
893
|
+
})
|
|
894
|
+
}
|
|
895
|
+
className="w-full rounded-lg border border-gray-300 px-2 py-1.5 text-sm text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
|
|
896
|
+
>
|
|
897
|
+
<option value="">Non attribué</option>
|
|
898
|
+
{users.map((u) => (
|
|
899
|
+
<option key={u.id} value={u.id}>
|
|
900
|
+
{u.name}
|
|
901
|
+
</option>
|
|
902
|
+
))}
|
|
903
|
+
</select>
|
|
904
|
+
</div>
|
|
905
|
+
</div>
|
|
906
|
+
<div className="flex items-center justify-between border-t border-gray-100 pt-3">
|
|
907
|
+
<div className="text-xs text-gray-500">
|
|
908
|
+
{company.createdBy?.name && `Créé par ${company.createdBy.name}`}
|
|
909
|
+
</div>
|
|
910
|
+
<div className="flex items-center gap-2">
|
|
911
|
+
{lastSavedCompany && (
|
|
912
|
+
<span className="text-xs text-green-600">
|
|
913
|
+
Enregistré à{' '}
|
|
914
|
+
{lastSavedCompany.toLocaleTimeString('fr-FR', {
|
|
915
|
+
hour: '2-digit',
|
|
916
|
+
minute: '2-digit',
|
|
917
|
+
})}
|
|
918
|
+
</span>
|
|
919
|
+
)}
|
|
920
|
+
<button
|
|
921
|
+
type="submit"
|
|
922
|
+
disabled={isSavingCompany}
|
|
923
|
+
className="cursor-pointer rounded-lg bg-blue-600 px-3 py-1.5 text-sm font-medium text-white hover:bg-blue-700 disabled:opacity-50"
|
|
924
|
+
>
|
|
925
|
+
{isSavingCompany ? <Spinner size="sm" /> : 'Enregistrer'}
|
|
926
|
+
</button>
|
|
927
|
+
</div>
|
|
928
|
+
</div>
|
|
929
|
+
</form>
|
|
930
|
+
) : (
|
|
931
|
+
<div className="space-y-3">
|
|
932
|
+
<div>
|
|
933
|
+
<span className="mb-1 block text-sm font-medium text-gray-700">Nom</span>
|
|
934
|
+
<p className="rounded-lg border border-gray-200 bg-gray-50 px-2 py-1.5 text-sm text-gray-900">
|
|
935
|
+
{company.name}
|
|
936
|
+
</p>
|
|
937
|
+
</div>
|
|
938
|
+
<div className="grid grid-cols-2 gap-2">
|
|
939
|
+
<div>
|
|
940
|
+
<span className="mb-1 block text-sm font-medium text-gray-700">
|
|
941
|
+
Téléphone
|
|
942
|
+
</span>
|
|
943
|
+
<p className="rounded-lg border border-gray-200 bg-gray-50 px-2 py-1.5 text-sm text-gray-900">
|
|
944
|
+
{company.phone || '-'}
|
|
945
|
+
</p>
|
|
946
|
+
</div>
|
|
947
|
+
<div>
|
|
948
|
+
<span className="mb-1 block text-sm font-medium text-gray-700">Email</span>
|
|
949
|
+
<p className="truncate rounded-lg border border-gray-200 bg-gray-50 px-2 py-1.5 text-sm text-gray-900">
|
|
950
|
+
{company.email || '-'}
|
|
951
|
+
</p>
|
|
952
|
+
</div>
|
|
953
|
+
</div>
|
|
954
|
+
{(company.address || company.city) && (
|
|
955
|
+
<div>
|
|
956
|
+
<span className="mb-1 block text-sm font-medium text-gray-700">
|
|
957
|
+
Adresse
|
|
958
|
+
</span>
|
|
959
|
+
<p className="rounded-lg border border-gray-200 bg-gray-50 px-2 py-1.5 text-sm text-gray-900">
|
|
960
|
+
{[company.address, company.city, company.postalCode]
|
|
961
|
+
.filter(Boolean)
|
|
962
|
+
.join(', ')}
|
|
963
|
+
</p>
|
|
964
|
+
</div>
|
|
965
|
+
)}
|
|
966
|
+
{company.website && (
|
|
967
|
+
<div>
|
|
968
|
+
<span className="mb-1 block text-sm font-medium text-gray-700">
|
|
969
|
+
Site web
|
|
970
|
+
</span>
|
|
971
|
+
<a
|
|
972
|
+
href={company.website}
|
|
973
|
+
target="_blank"
|
|
974
|
+
rel="noopener noreferrer"
|
|
975
|
+
className="block truncate text-sm text-blue-600 hover:underline"
|
|
976
|
+
>
|
|
977
|
+
{company.website}
|
|
978
|
+
</a>
|
|
979
|
+
</div>
|
|
980
|
+
)}
|
|
981
|
+
{(company.siret || company.industry) && (
|
|
982
|
+
<div className="grid grid-cols-2 gap-2">
|
|
983
|
+
{company.siret && (
|
|
984
|
+
<div>
|
|
985
|
+
<span className="mb-1 block text-xs font-medium text-gray-500">
|
|
986
|
+
SIRET
|
|
987
|
+
</span>
|
|
988
|
+
<p className="text-sm text-gray-900">{company.siret}</p>
|
|
989
|
+
</div>
|
|
990
|
+
)}
|
|
991
|
+
{company.industry && (
|
|
992
|
+
<div>
|
|
993
|
+
<span className="mb-1 block text-xs font-medium text-gray-500">
|
|
994
|
+
Secteur
|
|
995
|
+
</span>
|
|
996
|
+
<p className="text-sm text-gray-900">{company.industry}</p>
|
|
997
|
+
</div>
|
|
998
|
+
)}
|
|
999
|
+
</div>
|
|
1000
|
+
)}
|
|
1001
|
+
{company.notes && (
|
|
1002
|
+
<div>
|
|
1003
|
+
<span className="mb-1 block text-sm font-medium text-gray-700">Notes</span>
|
|
1004
|
+
<p className="rounded-lg border border-gray-200 bg-gray-50 px-2 py-1.5 text-sm whitespace-pre-wrap text-gray-900">
|
|
1005
|
+
{company.notes}
|
|
1006
|
+
</p>
|
|
1007
|
+
</div>
|
|
1008
|
+
)}
|
|
1009
|
+
<div className="grid grid-cols-2 gap-2 border-t border-gray-100 pt-3">
|
|
1010
|
+
<div>
|
|
1011
|
+
<span className="mb-1 block text-xs font-medium text-gray-500">
|
|
1012
|
+
Commercial
|
|
1013
|
+
</span>
|
|
1014
|
+
<p className="text-sm text-gray-900">
|
|
1015
|
+
{company.assignedCommercial?.name || 'Non attribué'}
|
|
1016
|
+
</p>
|
|
1017
|
+
</div>
|
|
1018
|
+
<div>
|
|
1019
|
+
<span className="mb-1 block text-xs font-medium text-gray-500">
|
|
1020
|
+
Télépro
|
|
1021
|
+
</span>
|
|
1022
|
+
<p className="text-sm text-gray-900">
|
|
1023
|
+
{company.assignedTelepro?.name || 'Non attribué'}
|
|
1024
|
+
</p>
|
|
1025
|
+
</div>
|
|
1026
|
+
</div>
|
|
1027
|
+
<div className="border-t border-gray-100 pt-3">
|
|
1028
|
+
<span className="mb-1 block text-xs font-medium text-gray-500">Créé par</span>
|
|
1029
|
+
<p className="text-sm text-gray-900">{company.createdBy?.name || '-'}</p>
|
|
1030
|
+
</div>
|
|
1031
|
+
</div>
|
|
1032
|
+
)}
|
|
1033
|
+
</div>
|
|
1034
|
+
</div>
|
|
1035
|
+
|
|
1036
|
+
{/* Colonne droite - Onglets (Contacts associés / Activités) */}
|
|
1037
|
+
<div className="flex-1">
|
|
1038
|
+
<div className="rounded-lg bg-white shadow">
|
|
1039
|
+
<div className="border-b border-gray-200">
|
|
1040
|
+
<nav className="flex flex-wrap" aria-label="Tabs">
|
|
1041
|
+
{[
|
|
1042
|
+
{ id: 'contacts' as const, label: 'Contacts associés', icon: Users },
|
|
1043
|
+
...(canViewActivities
|
|
1044
|
+
? [{ id: 'activities' as const, label: 'Activités', icon: Activity }]
|
|
1045
|
+
: []),
|
|
1046
|
+
].map((tab) => {
|
|
1047
|
+
const TabIcon = tab.icon;
|
|
1048
|
+
const contactsList = company?.contacts ?? [];
|
|
1049
|
+
return (
|
|
1050
|
+
<button
|
|
1051
|
+
key={tab.id}
|
|
1052
|
+
onClick={() => setActiveTab(tab.id)}
|
|
1053
|
+
className={cn(
|
|
1054
|
+
'flex cursor-pointer items-center gap-1.5 border-b-2 px-4 py-4 text-sm font-medium transition-colors sm:gap-2 sm:px-6',
|
|
1055
|
+
activeTab === tab.id
|
|
1056
|
+
? 'border-blue-600 text-blue-600'
|
|
1057
|
+
: 'border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700',
|
|
1058
|
+
)}
|
|
1059
|
+
>
|
|
1060
|
+
<TabIcon className="h-4 w-4 shrink-0" />
|
|
1061
|
+
<span className="whitespace-nowrap">{tab.label}</span>
|
|
1062
|
+
{tab.id === 'contacts' && contactsList.length > 0 && (
|
|
1063
|
+
<span className="rounded-full bg-gray-200 px-2 py-0.5 text-xs">
|
|
1064
|
+
{contactsList.length}
|
|
1065
|
+
</span>
|
|
1066
|
+
)}
|
|
1067
|
+
</button>
|
|
1068
|
+
);
|
|
1069
|
+
})}
|
|
1070
|
+
</nav>
|
|
1071
|
+
</div>
|
|
1072
|
+
|
|
1073
|
+
<div className="p-4 sm:p-6">
|
|
1074
|
+
{activeTab === 'contacts' && (
|
|
1075
|
+
<div>
|
|
1076
|
+
<div className="mb-4 flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
|
1077
|
+
<h2 className="text-lg font-semibold text-gray-900">Contacts associés</h2>
|
|
1078
|
+
</div>
|
|
1079
|
+
|
|
1080
|
+
{(company.contacts ?? []).length === 0 ? (
|
|
1081
|
+
<div className="rounded-lg border border-gray-200 bg-gray-50 py-12 text-center">
|
|
1082
|
+
<Users className="mx-auto h-12 w-12 text-gray-400" />
|
|
1083
|
+
<p className="mt-2 text-sm text-gray-600">
|
|
1084
|
+
Aucun contact associé à cette entreprise
|
|
1085
|
+
</p>
|
|
1086
|
+
{(canCreate || canEdit) && (
|
|
1087
|
+
<div className="mt-4 flex flex-wrap justify-center gap-2">
|
|
1088
|
+
{canCreate && (
|
|
1089
|
+
<button
|
|
1090
|
+
type="button"
|
|
1091
|
+
onClick={() => openAddContactModal('create')}
|
|
1092
|
+
className="inline-flex cursor-pointer items-center gap-2 rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700"
|
|
1093
|
+
>
|
|
1094
|
+
<Plus className="h-4 w-4" />
|
|
1095
|
+
Ajouter un contact
|
|
1096
|
+
</button>
|
|
1097
|
+
)}
|
|
1098
|
+
{canEdit && (
|
|
1099
|
+
<button
|
|
1100
|
+
type="button"
|
|
1101
|
+
onClick={() => openAddContactModal('search')}
|
|
1102
|
+
className="inline-flex cursor-pointer items-center gap-2 rounded-lg border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50"
|
|
1103
|
+
>
|
|
1104
|
+
<Plus className="h-4 w-4" />
|
|
1105
|
+
Rechercher un contact existant
|
|
1106
|
+
</button>
|
|
1107
|
+
)}
|
|
1108
|
+
</div>
|
|
1109
|
+
)}
|
|
1110
|
+
</div>
|
|
1111
|
+
) : (
|
|
1112
|
+
<div className="overflow-hidden rounded-lg border border-gray-200">
|
|
1113
|
+
<table className="min-w-full divide-y divide-gray-200 text-sm">
|
|
1114
|
+
<thead className="bg-gray-50">
|
|
1115
|
+
<tr>
|
|
1116
|
+
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase sm:px-6">
|
|
1117
|
+
Contact
|
|
1118
|
+
</th>
|
|
1119
|
+
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase sm:px-6">
|
|
1120
|
+
Poste
|
|
1121
|
+
</th>
|
|
1122
|
+
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase sm:px-6">
|
|
1123
|
+
Téléphone
|
|
1124
|
+
</th>
|
|
1125
|
+
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase sm:px-6">
|
|
1126
|
+
Email
|
|
1127
|
+
</th>
|
|
1128
|
+
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase sm:px-6">
|
|
1129
|
+
Statut
|
|
1130
|
+
</th>
|
|
1131
|
+
</tr>
|
|
1132
|
+
</thead>
|
|
1133
|
+
<tbody className="divide-y divide-gray-200 bg-white">
|
|
1134
|
+
{(company.contacts ?? []).map((c) => (
|
|
1135
|
+
<tr
|
|
1136
|
+
key={c.id}
|
|
1137
|
+
onClick={() => router.push(`/contacts/${c.id}`)}
|
|
1138
|
+
className="cursor-pointer transition-colors hover:bg-blue-50/50"
|
|
1139
|
+
>
|
|
1140
|
+
<td className="px-4 py-3 font-medium text-gray-900 sm:px-6">
|
|
1141
|
+
{c.firstName} {c.lastName}
|
|
1142
|
+
</td>
|
|
1143
|
+
<td className="px-4 py-3 text-gray-600 sm:px-6">
|
|
1144
|
+
{c.jobTitle || '-'}
|
|
1145
|
+
</td>
|
|
1146
|
+
<td className="px-4 py-3 text-gray-600 sm:px-6">{c.phone}</td>
|
|
1147
|
+
<td className="px-4 py-3 text-gray-600 sm:px-6">
|
|
1148
|
+
{c.email || '-'}
|
|
1149
|
+
</td>
|
|
1150
|
+
<td className="px-4 py-3 sm:px-6">
|
|
1151
|
+
{c.status ? (
|
|
1152
|
+
<span
|
|
1153
|
+
className="inline-flex rounded-full px-2 py-0.5 text-xs font-medium"
|
|
1154
|
+
style={{
|
|
1155
|
+
backgroundColor: `${c.status.color}20`,
|
|
1156
|
+
color: c.status.color,
|
|
1157
|
+
}}
|
|
1158
|
+
>
|
|
1159
|
+
{c.status.name}
|
|
1160
|
+
</span>
|
|
1161
|
+
) : (
|
|
1162
|
+
'-'
|
|
1163
|
+
)}
|
|
1164
|
+
</td>
|
|
1165
|
+
</tr>
|
|
1166
|
+
))}
|
|
1167
|
+
</tbody>
|
|
1168
|
+
</table>
|
|
1169
|
+
</div>
|
|
1170
|
+
)}
|
|
1171
|
+
</div>
|
|
1172
|
+
)}
|
|
1173
|
+
|
|
1174
|
+
{activeTab === 'activities' && (
|
|
1175
|
+
<div>
|
|
1176
|
+
<h2 className="mb-4 text-lg font-semibold text-gray-900">Activités</h2>
|
|
1177
|
+
{activities.length === 0 ? (
|
|
1178
|
+
<div className="py-12 text-center">
|
|
1179
|
+
<Activity className="mx-auto h-12 w-12 text-gray-400" />
|
|
1180
|
+
<p className="mt-2 text-sm text-gray-600">
|
|
1181
|
+
Aucune activité pour le moment.
|
|
1182
|
+
</p>
|
|
1183
|
+
<p className="mt-1 text-xs text-gray-500">
|
|
1184
|
+
Cliquez sur un contact dans l’onglet « Contacts associés » pour voir ses
|
|
1185
|
+
activités.
|
|
1186
|
+
</p>
|
|
1187
|
+
</div>
|
|
1188
|
+
) : (
|
|
1189
|
+
<div className="space-y-2">
|
|
1190
|
+
{activities.map((act) => (
|
|
1191
|
+
<div
|
|
1192
|
+
key={act.id}
|
|
1193
|
+
className="rounded-lg border border-gray-200 bg-gray-50/50 p-3"
|
|
1194
|
+
>
|
|
1195
|
+
<div className="flex items-center justify-between gap-2">
|
|
1196
|
+
<p className="text-sm font-medium text-gray-900">
|
|
1197
|
+
{act.title ||
|
|
1198
|
+
(act.type === 'COMPANY_UPDATE'
|
|
1199
|
+
? 'Informations modifiées'
|
|
1200
|
+
: act.type === 'CONTACT_ADDED'
|
|
1201
|
+
? 'Contact ajouté'
|
|
1202
|
+
: act.type)}
|
|
1203
|
+
</p>
|
|
1204
|
+
<span className="text-xs text-gray-500">
|
|
1205
|
+
{new Date(act.createdAt).toLocaleDateString('fr-FR', {
|
|
1206
|
+
day: 'numeric',
|
|
1207
|
+
month: 'short',
|
|
1208
|
+
year: 'numeric',
|
|
1209
|
+
hour: '2-digit',
|
|
1210
|
+
minute: '2-digit',
|
|
1211
|
+
})}
|
|
1212
|
+
</span>
|
|
1213
|
+
</div>
|
|
1214
|
+
{act.content && (
|
|
1215
|
+
<p className="mt-1 text-sm text-gray-700">{act.content}</p>
|
|
1216
|
+
)}
|
|
1217
|
+
{act.user && (
|
|
1218
|
+
<p className="mt-1 text-xs text-gray-500">Par {act.user.name}</p>
|
|
1219
|
+
)}
|
|
1220
|
+
</div>
|
|
1221
|
+
))}
|
|
1222
|
+
</div>
|
|
1223
|
+
)}
|
|
1224
|
+
</div>
|
|
1225
|
+
)}
|
|
1226
|
+
</div>
|
|
1227
|
+
</div>
|
|
1228
|
+
</div>
|
|
1229
|
+
</div>
|
|
1230
|
+
</div>
|
|
1231
|
+
|
|
1232
|
+
{showAddContactModal && (
|
|
1233
|
+
<div className="fixed inset-0 z-50 flex items-center justify-center bg-gray-500/20 p-4 backdrop-blur-sm">
|
|
1234
|
+
<div className="flex max-h-[90vh] w-full max-w-lg flex-col rounded-lg bg-white shadow-xl">
|
|
1235
|
+
<div className="shrink-0 border-b border-gray-200 px-6 py-4">
|
|
1236
|
+
<div className="flex items-center justify-between">
|
|
1237
|
+
<h2 className="text-lg font-semibold text-gray-900">Ajouter un contact</h2>
|
|
1238
|
+
<button
|
|
1239
|
+
type="button"
|
|
1240
|
+
onClick={() => setShowAddContactModal(false)}
|
|
1241
|
+
className="cursor-pointer rounded-lg p-2 text-gray-400 hover:bg-gray-100 hover:text-gray-600"
|
|
1242
|
+
>
|
|
1243
|
+
<X className="h-5 w-5" />
|
|
1244
|
+
</button>
|
|
1245
|
+
</div>
|
|
1246
|
+
<div className="mt-3 flex gap-1 border-b border-gray-200">
|
|
1247
|
+
<button
|
|
1248
|
+
type="button"
|
|
1249
|
+
onClick={() => setContactModalMode('create')}
|
|
1250
|
+
className={cn(
|
|
1251
|
+
'cursor-pointer border-b-2 px-4 py-2 text-sm font-medium transition-colors',
|
|
1252
|
+
contactModalMode === 'create'
|
|
1253
|
+
? 'border-blue-600 text-blue-600'
|
|
1254
|
+
: 'border-transparent text-gray-500 hover:text-gray-700',
|
|
1255
|
+
)}
|
|
1256
|
+
>
|
|
1257
|
+
Créer un nouveau contact
|
|
1258
|
+
</button>
|
|
1259
|
+
<button
|
|
1260
|
+
type="button"
|
|
1261
|
+
onClick={() => setContactModalMode('search')}
|
|
1262
|
+
className={cn(
|
|
1263
|
+
'cursor-pointer border-b-2 px-4 py-2 text-sm font-medium transition-colors',
|
|
1264
|
+
contactModalMode === 'search'
|
|
1265
|
+
? 'border-blue-600 text-blue-600'
|
|
1266
|
+
: 'border-transparent text-gray-500 hover:text-gray-700',
|
|
1267
|
+
)}
|
|
1268
|
+
>
|
|
1269
|
+
Rechercher un contact existant
|
|
1270
|
+
</button>
|
|
1271
|
+
</div>
|
|
1272
|
+
</div>
|
|
1273
|
+
<div className="flex-1 overflow-y-auto">
|
|
1274
|
+
{contactModalMode === 'create' ? (
|
|
1275
|
+
<form onSubmit={handleCreateContact} className="space-y-4 p-6">
|
|
1276
|
+
<p className="text-sm text-gray-600">
|
|
1277
|
+
Entreprise : <strong>{company.name}</strong>
|
|
1278
|
+
</p>
|
|
1279
|
+
<div className="grid grid-cols-2 gap-2">
|
|
1280
|
+
<div>
|
|
1281
|
+
<label
|
|
1282
|
+
htmlFor="new-civility"
|
|
1283
|
+
className="mb-1 block text-sm font-medium text-gray-700"
|
|
1284
|
+
>
|
|
1285
|
+
Civilité
|
|
1286
|
+
</label>
|
|
1287
|
+
<select
|
|
1288
|
+
id="new-civility"
|
|
1289
|
+
value={newContactForm.civility}
|
|
1290
|
+
onChange={(e) =>
|
|
1291
|
+
setNewContactForm({ ...newContactForm, civility: e.target.value })
|
|
1292
|
+
}
|
|
1293
|
+
className="w-full rounded-lg border border-gray-300 px-2 py-1.5 text-sm text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
|
|
1294
|
+
>
|
|
1295
|
+
<option value="">-</option>
|
|
1296
|
+
<option value="M">M.</option>
|
|
1297
|
+
<option value="MME">Mme</option>
|
|
1298
|
+
<option value="MLLE">Mlle</option>
|
|
1299
|
+
</select>
|
|
1300
|
+
</div>
|
|
1301
|
+
</div>
|
|
1302
|
+
<div className="grid grid-cols-2 gap-2">
|
|
1303
|
+
<div>
|
|
1304
|
+
<label
|
|
1305
|
+
htmlFor="new-firstName"
|
|
1306
|
+
className="mb-1 block text-sm font-medium text-gray-700"
|
|
1307
|
+
>
|
|
1308
|
+
Prénom
|
|
1309
|
+
</label>
|
|
1310
|
+
<input
|
|
1311
|
+
id="new-firstName"
|
|
1312
|
+
type="text"
|
|
1313
|
+
value={newContactForm.firstName}
|
|
1314
|
+
onChange={(e) =>
|
|
1315
|
+
setNewContactForm({ ...newContactForm, firstName: e.target.value })
|
|
1316
|
+
}
|
|
1317
|
+
className="w-full rounded-lg border border-gray-300 px-2 py-1.5 text-sm text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
|
|
1318
|
+
/>
|
|
1319
|
+
</div>
|
|
1320
|
+
<div>
|
|
1321
|
+
<label
|
|
1322
|
+
htmlFor="new-lastName"
|
|
1323
|
+
className="mb-1 block text-sm font-medium text-gray-700"
|
|
1324
|
+
>
|
|
1325
|
+
Nom
|
|
1326
|
+
</label>
|
|
1327
|
+
<input
|
|
1328
|
+
id="new-lastName"
|
|
1329
|
+
type="text"
|
|
1330
|
+
value={newContactForm.lastName}
|
|
1331
|
+
onChange={(e) =>
|
|
1332
|
+
setNewContactForm({ ...newContactForm, lastName: e.target.value })
|
|
1333
|
+
}
|
|
1334
|
+
className="w-full rounded-lg border border-gray-300 px-2 py-1.5 text-sm text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
|
|
1335
|
+
/>
|
|
1336
|
+
</div>
|
|
1337
|
+
</div>
|
|
1338
|
+
<div>
|
|
1339
|
+
<label
|
|
1340
|
+
htmlFor="new-jobTitle"
|
|
1341
|
+
className="mb-1 block text-sm font-medium text-gray-700"
|
|
1342
|
+
>
|
|
1343
|
+
Intitulé du poste
|
|
1344
|
+
</label>
|
|
1345
|
+
<input
|
|
1346
|
+
id="new-jobTitle"
|
|
1347
|
+
type="text"
|
|
1348
|
+
value={newContactForm.jobTitle}
|
|
1349
|
+
onChange={(e) =>
|
|
1350
|
+
setNewContactForm({ ...newContactForm, jobTitle: e.target.value })
|
|
1351
|
+
}
|
|
1352
|
+
placeholder="Ex. Directeur commercial, Assistant..."
|
|
1353
|
+
className="w-full rounded-lg border border-gray-300 px-2 py-1.5 text-sm text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
|
|
1354
|
+
/>
|
|
1355
|
+
</div>
|
|
1356
|
+
<div className="grid grid-cols-2 gap-2">
|
|
1357
|
+
<div>
|
|
1358
|
+
<label
|
|
1359
|
+
htmlFor="new-phone"
|
|
1360
|
+
className="mb-1 block text-sm font-medium text-gray-700"
|
|
1361
|
+
>
|
|
1362
|
+
Téléphone *
|
|
1363
|
+
</label>
|
|
1364
|
+
<input
|
|
1365
|
+
id="new-phone"
|
|
1366
|
+
type="tel"
|
|
1367
|
+
required
|
|
1368
|
+
value={newContactForm.phone}
|
|
1369
|
+
onChange={(e) =>
|
|
1370
|
+
setNewContactForm({ ...newContactForm, phone: e.target.value })
|
|
1371
|
+
}
|
|
1372
|
+
onBlur={(e) => {
|
|
1373
|
+
const n = normalizePhoneNumber(e.target.value);
|
|
1374
|
+
if (n) setNewContactForm((prev) => ({ ...prev, phone: n }));
|
|
1375
|
+
}}
|
|
1376
|
+
className="w-full rounded-lg border border-gray-300 px-2 py-1.5 text-sm text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
|
|
1377
|
+
/>
|
|
1378
|
+
</div>
|
|
1379
|
+
<div>
|
|
1380
|
+
<label
|
|
1381
|
+
htmlFor="new-email"
|
|
1382
|
+
className="mb-1 block text-sm font-medium text-gray-700"
|
|
1383
|
+
>
|
|
1384
|
+
Email
|
|
1385
|
+
</label>
|
|
1386
|
+
<input
|
|
1387
|
+
id="new-email"
|
|
1388
|
+
type="email"
|
|
1389
|
+
value={newContactForm.email}
|
|
1390
|
+
onChange={(e) =>
|
|
1391
|
+
setNewContactForm({ ...newContactForm, email: e.target.value })
|
|
1392
|
+
}
|
|
1393
|
+
className="w-full rounded-lg border border-gray-300 px-2 py-1.5 text-sm text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
|
|
1394
|
+
/>
|
|
1395
|
+
</div>
|
|
1396
|
+
</div>
|
|
1397
|
+
<div>
|
|
1398
|
+
<label
|
|
1399
|
+
htmlFor="new-address"
|
|
1400
|
+
className="mb-1 block text-sm font-medium text-gray-700"
|
|
1401
|
+
>
|
|
1402
|
+
Adresse
|
|
1403
|
+
</label>
|
|
1404
|
+
<AddressAutocomplete
|
|
1405
|
+
value={newContactForm.address}
|
|
1406
|
+
onChange={(address, components) =>
|
|
1407
|
+
setNewContactForm({
|
|
1408
|
+
...newContactForm,
|
|
1409
|
+
address,
|
|
1410
|
+
city: components?.city ?? newContactForm.city,
|
|
1411
|
+
postalCode: components?.postalCode ?? newContactForm.postalCode,
|
|
1412
|
+
})
|
|
1413
|
+
}
|
|
1414
|
+
placeholder="Rechercher une adresse..."
|
|
1415
|
+
/>
|
|
1416
|
+
</div>
|
|
1417
|
+
<div className="grid grid-cols-2 gap-2">
|
|
1418
|
+
<div>
|
|
1419
|
+
<label
|
|
1420
|
+
htmlFor="new-city"
|
|
1421
|
+
className="mb-1 block text-sm font-medium text-gray-700"
|
|
1422
|
+
>
|
|
1423
|
+
Ville
|
|
1424
|
+
</label>
|
|
1425
|
+
<input
|
|
1426
|
+
id="new-city"
|
|
1427
|
+
type="text"
|
|
1428
|
+
value={newContactForm.city}
|
|
1429
|
+
onChange={(e) =>
|
|
1430
|
+
setNewContactForm({ ...newContactForm, city: e.target.value })
|
|
1431
|
+
}
|
|
1432
|
+
className="w-full rounded-lg border border-gray-300 px-2 py-1.5 text-sm text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
|
|
1433
|
+
/>
|
|
1434
|
+
</div>
|
|
1435
|
+
<div>
|
|
1436
|
+
<label
|
|
1437
|
+
htmlFor="new-postalCode"
|
|
1438
|
+
className="mb-1 block text-sm font-medium text-gray-700"
|
|
1439
|
+
>
|
|
1440
|
+
Code postal
|
|
1441
|
+
</label>
|
|
1442
|
+
<input
|
|
1443
|
+
id="new-postalCode"
|
|
1444
|
+
type="text"
|
|
1445
|
+
value={newContactForm.postalCode}
|
|
1446
|
+
onChange={(e) =>
|
|
1447
|
+
setNewContactForm({ ...newContactForm, postalCode: e.target.value })
|
|
1448
|
+
}
|
|
1449
|
+
className="w-full rounded-lg border border-gray-300 px-2 py-1.5 text-sm text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
|
|
1450
|
+
/>
|
|
1451
|
+
</div>
|
|
1452
|
+
</div>
|
|
1453
|
+
<div>
|
|
1454
|
+
<label
|
|
1455
|
+
htmlFor="new-status"
|
|
1456
|
+
className="mb-1 block text-sm font-medium text-gray-700"
|
|
1457
|
+
>
|
|
1458
|
+
Statut
|
|
1459
|
+
</label>
|
|
1460
|
+
<select
|
|
1461
|
+
id="new-status"
|
|
1462
|
+
value={newContactForm.statusId}
|
|
1463
|
+
onChange={(e) =>
|
|
1464
|
+
setNewContactForm({ ...newContactForm, statusId: e.target.value })
|
|
1465
|
+
}
|
|
1466
|
+
className="w-full rounded-lg border border-gray-300 px-2 py-1.5 text-sm text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
|
|
1467
|
+
>
|
|
1468
|
+
<option value="">Aucun statut</option>
|
|
1469
|
+
{statuses.map((s) => (
|
|
1470
|
+
<option key={s.id} value={s.id}>
|
|
1471
|
+
{s.name}
|
|
1472
|
+
</option>
|
|
1473
|
+
))}
|
|
1474
|
+
</select>
|
|
1475
|
+
</div>
|
|
1476
|
+
<div className="grid grid-cols-2 gap-2">
|
|
1477
|
+
<div>
|
|
1478
|
+
<label
|
|
1479
|
+
htmlFor="new-commercial"
|
|
1480
|
+
className="mb-1 block text-sm font-medium text-gray-700"
|
|
1481
|
+
>
|
|
1482
|
+
Commercial
|
|
1483
|
+
</label>
|
|
1484
|
+
<select
|
|
1485
|
+
id="new-commercial"
|
|
1486
|
+
value={newContactForm.assignedCommercialId}
|
|
1487
|
+
onChange={(e) =>
|
|
1488
|
+
setNewContactForm({
|
|
1489
|
+
...newContactForm,
|
|
1490
|
+
assignedCommercialId: e.target.value,
|
|
1491
|
+
})
|
|
1492
|
+
}
|
|
1493
|
+
className="w-full rounded-lg border border-gray-300 px-2 py-1.5 text-sm text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
|
|
1494
|
+
>
|
|
1495
|
+
<option value="">Non attribué</option>
|
|
1496
|
+
{(isAdmin
|
|
1497
|
+
? users
|
|
1498
|
+
: users.filter(
|
|
1499
|
+
(u) =>
|
|
1500
|
+
u.role === 'COMMERCIAL' ||
|
|
1501
|
+
u.role === 'ADMIN' ||
|
|
1502
|
+
u.role === 'MANAGER',
|
|
1503
|
+
)
|
|
1504
|
+
).map((u) => (
|
|
1505
|
+
<option key={u.id} value={u.id}>
|
|
1506
|
+
{u.name}
|
|
1507
|
+
</option>
|
|
1508
|
+
))}
|
|
1509
|
+
</select>
|
|
1510
|
+
</div>
|
|
1511
|
+
<div>
|
|
1512
|
+
<label
|
|
1513
|
+
htmlFor="new-telepro"
|
|
1514
|
+
className="mb-1 block text-sm font-medium text-gray-700"
|
|
1515
|
+
>
|
|
1516
|
+
Télépro
|
|
1517
|
+
</label>
|
|
1518
|
+
<select
|
|
1519
|
+
id="new-telepro"
|
|
1520
|
+
value={newContactForm.assignedTeleproId}
|
|
1521
|
+
onChange={(e) =>
|
|
1522
|
+
setNewContactForm({
|
|
1523
|
+
...newContactForm,
|
|
1524
|
+
assignedTeleproId: e.target.value,
|
|
1525
|
+
})
|
|
1526
|
+
}
|
|
1527
|
+
className="w-full rounded-lg border border-gray-300 px-2 py-1.5 text-sm text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
|
|
1528
|
+
>
|
|
1529
|
+
<option value="">Non attribué</option>
|
|
1530
|
+
{(isAdmin
|
|
1531
|
+
? users
|
|
1532
|
+
: users.filter(
|
|
1533
|
+
(u) =>
|
|
1534
|
+
u.role === 'TELEPRO' ||
|
|
1535
|
+
u.role === 'ADMIN' ||
|
|
1536
|
+
u.role === 'MANAGER',
|
|
1537
|
+
)
|
|
1538
|
+
).map((u) => (
|
|
1539
|
+
<option key={u.id} value={u.id}>
|
|
1540
|
+
{u.name}
|
|
1541
|
+
</option>
|
|
1542
|
+
))}
|
|
1543
|
+
</select>
|
|
1544
|
+
</div>
|
|
1545
|
+
</div>
|
|
1546
|
+
<div className="flex justify-end gap-2 border-t border-gray-100 pt-4">
|
|
1547
|
+
<button
|
|
1548
|
+
type="button"
|
|
1549
|
+
onClick={() => setShowAddContactModal(false)}
|
|
1550
|
+
className="cursor-pointer rounded-lg border border-gray-300 px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50"
|
|
1551
|
+
>
|
|
1552
|
+
Annuler
|
|
1553
|
+
</button>
|
|
1554
|
+
<button
|
|
1555
|
+
type="submit"
|
|
1556
|
+
disabled={savingContact}
|
|
1557
|
+
className="cursor-pointer rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700 disabled:opacity-50"
|
|
1558
|
+
>
|
|
1559
|
+
{savingContact ? <Spinner size="sm" /> : 'Créer'}
|
|
1560
|
+
</button>
|
|
1561
|
+
</div>
|
|
1562
|
+
</form>
|
|
1563
|
+
) : (
|
|
1564
|
+
<div className="space-y-4 p-6">
|
|
1565
|
+
<div>
|
|
1566
|
+
<label className="block text-sm font-medium text-gray-700">
|
|
1567
|
+
Rechercher un contact
|
|
1568
|
+
</label>
|
|
1569
|
+
<input
|
|
1570
|
+
type="text"
|
|
1571
|
+
value={existingContactSearch}
|
|
1572
|
+
onChange={(e) => setExistingContactSearch(e.target.value)}
|
|
1573
|
+
placeholder="Rechercher par nom, téléphone ou email..."
|
|
1574
|
+
className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
|
|
1575
|
+
/>
|
|
1576
|
+
<p className="mt-1 text-xs text-gray-500">
|
|
1577
|
+
Tapez pour rechercher parmi les contacts existants
|
|
1578
|
+
</p>
|
|
1579
|
+
</div>
|
|
1580
|
+
{existingContactSearch.trim() &&
|
|
1581
|
+
!addExistingSearching &&
|
|
1582
|
+
existingContactResults.length === 0 && (
|
|
1583
|
+
<p className="text-sm text-gray-500">
|
|
1584
|
+
Aucun contact trouvé ou tous sont déjà associés à cette entreprise.
|
|
1585
|
+
</p>
|
|
1586
|
+
)}
|
|
1587
|
+
{existingContactResults.length > 0 && (
|
|
1588
|
+
<div className="space-y-2">
|
|
1589
|
+
<p className="text-xs text-gray-500">
|
|
1590
|
+
Sélectionnez le contact à associer dans la liste, ou tapez ci-dessus pour
|
|
1591
|
+
filtrer.
|
|
1592
|
+
</p>
|
|
1593
|
+
<p className="text-sm font-medium text-gray-700">
|
|
1594
|
+
{existingContactResults.length} contact(s) trouvé(s)
|
|
1595
|
+
</p>
|
|
1596
|
+
<div className="max-h-72 overflow-y-auto rounded-lg border border-gray-200">
|
|
1597
|
+
{existingContactResults.map((c) => {
|
|
1598
|
+
const isSelected = addExistingSelectedId === c.id;
|
|
1599
|
+
return (
|
|
1600
|
+
<div
|
|
1601
|
+
key={c.id}
|
|
1602
|
+
className={cn(
|
|
1603
|
+
'flex cursor-pointer items-center gap-3 border-b border-gray-100 p-4 transition-colors last:border-b-0',
|
|
1604
|
+
isSelected
|
|
1605
|
+
? 'bg-blue-50 hover:bg-blue-100'
|
|
1606
|
+
: 'hover:bg-gray-50',
|
|
1607
|
+
)}
|
|
1608
|
+
onClick={() => setAddExistingSelectedId(isSelected ? null : c.id)}
|
|
1609
|
+
>
|
|
1610
|
+
<input
|
|
1611
|
+
type="checkbox"
|
|
1612
|
+
checked={isSelected}
|
|
1613
|
+
onChange={() =>
|
|
1614
|
+
setAddExistingSelectedId(isSelected ? null : c.id)
|
|
1615
|
+
}
|
|
1616
|
+
onClick={(e) => e.stopPropagation()}
|
|
1617
|
+
className="h-4 w-4 cursor-pointer rounded border-gray-300 text-blue-600 focus:ring-gray-400/30"
|
|
1618
|
+
/>
|
|
1619
|
+
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-blue-100 text-sm font-semibold text-blue-800">
|
|
1620
|
+
{(c.firstName?.[0] || c.lastName?.[0] || '?').toUpperCase()}
|
|
1621
|
+
</div>
|
|
1622
|
+
<div className="min-w-0 flex-1">
|
|
1623
|
+
<p className="truncate text-sm font-medium text-gray-900">
|
|
1624
|
+
{[c.firstName, c.lastName].filter(Boolean).join(' ') ||
|
|
1625
|
+
'Sans nom'}
|
|
1626
|
+
</p>
|
|
1627
|
+
<div className="mt-1 flex flex-wrap items-center gap-2 text-xs text-gray-500">
|
|
1628
|
+
{c.phone && (
|
|
1629
|
+
<span className="flex items-center gap-1">
|
|
1630
|
+
<Phone className="h-3 w-3" />
|
|
1631
|
+
{c.phone}
|
|
1632
|
+
</span>
|
|
1633
|
+
)}
|
|
1634
|
+
{c.email && (
|
|
1635
|
+
<span className="flex items-center gap-1">
|
|
1636
|
+
<Mail className="h-3 w-3" />
|
|
1637
|
+
<span className="max-w-[200px] truncate">{c.email}</span>
|
|
1638
|
+
</span>
|
|
1639
|
+
)}
|
|
1640
|
+
</div>
|
|
1641
|
+
</div>
|
|
1642
|
+
</div>
|
|
1643
|
+
);
|
|
1644
|
+
})}
|
|
1645
|
+
</div>
|
|
1646
|
+
{addExistingSelectedId && (
|
|
1647
|
+
<div className="border-t border-gray-100 pt-4">
|
|
1648
|
+
<label
|
|
1649
|
+
htmlFor="add-existing-jobTitle"
|
|
1650
|
+
className="mb-1 block text-sm font-medium text-gray-700"
|
|
1651
|
+
>
|
|
1652
|
+
Intitulé du poste (optionnel)
|
|
1653
|
+
</label>
|
|
1654
|
+
<input
|
|
1655
|
+
id="add-existing-jobTitle"
|
|
1656
|
+
type="text"
|
|
1657
|
+
value={addExistingJobTitle}
|
|
1658
|
+
onChange={(e) => setAddExistingJobTitle(e.target.value)}
|
|
1659
|
+
placeholder="Ex. Directeur commercial..."
|
|
1660
|
+
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
|
|
1661
|
+
/>
|
|
1662
|
+
</div>
|
|
1663
|
+
)}
|
|
1664
|
+
</div>
|
|
1665
|
+
)}
|
|
1666
|
+
{!existingContactSearch.trim() && existingContactResults.length === 0 && (
|
|
1667
|
+
<div className="rounded-lg border border-gray-200 bg-gray-50 p-8 text-center">
|
|
1668
|
+
<p className="text-sm text-gray-500">
|
|
1669
|
+
Tapez ci-dessus pour rechercher parmi les contacts existants.
|
|
1670
|
+
</p>
|
|
1671
|
+
</div>
|
|
1672
|
+
)}
|
|
1673
|
+
</div>
|
|
1674
|
+
)}
|
|
1675
|
+
</div>
|
|
1676
|
+
{contactModalMode === 'search' && (
|
|
1677
|
+
<div className="shrink-0 border-t border-gray-100 px-6 pt-4 pb-6">
|
|
1678
|
+
<div className="flex justify-end gap-2">
|
|
1679
|
+
<button
|
|
1680
|
+
type="button"
|
|
1681
|
+
onClick={() => setShowAddContactModal(false)}
|
|
1682
|
+
className="cursor-pointer rounded-lg border border-gray-300 px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50"
|
|
1683
|
+
>
|
|
1684
|
+
Fermer
|
|
1685
|
+
</button>
|
|
1686
|
+
<button
|
|
1687
|
+
type="button"
|
|
1688
|
+
onClick={handleAddExistingContact}
|
|
1689
|
+
disabled={!addExistingSelectedId || addExistingLoading}
|
|
1690
|
+
className="cursor-pointer rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700 disabled:opacity-50"
|
|
1691
|
+
>
|
|
1692
|
+
{addExistingLoading ? <Spinner size="sm" /> : 'Associer'}
|
|
1693
|
+
</button>
|
|
1694
|
+
</div>
|
|
1695
|
+
</div>
|
|
1696
|
+
)}
|
|
1697
|
+
</div>
|
|
1698
|
+
</div>
|
|
1699
|
+
)}
|
|
1700
|
+
</div>
|
|
1701
|
+
</ProtectedPage>
|
|
1702
|
+
);
|
|
1703
|
+
}
|