@consilioweb/payload-support 0.5.1
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/LICENSE +21 -0
- package/README.md +525 -0
- package/dist/client.cjs +7 -0
- package/dist/client.d.cts +3 -0
- package/dist/client.d.ts +3 -0
- package/dist/client.js +5 -0
- package/dist/index.cjs +7766 -0
- package/dist/index.d.cts +384 -0
- package/dist/index.d.ts +384 -0
- package/dist/index.js +7730 -0
- package/dist/views.d.cts +30 -0
- package/dist/views.d.ts +30 -0
- package/package.json +131 -0
- package/src/client.ts +1 -0
- package/src/collections/AuthLogs.ts +65 -0
- package/src/collections/CannedResponses.ts +69 -0
- package/src/collections/ChatMessages.ts +98 -0
- package/src/collections/EmailLogs.ts +94 -0
- package/src/collections/KnowledgeBase.ts +99 -0
- package/src/collections/Macros.ts +98 -0
- package/src/collections/PendingEmails.ts +122 -0
- package/src/collections/SatisfactionSurveys.ts +98 -0
- package/src/collections/SlaPolicies.ts +123 -0
- package/src/collections/SupportClients.ts +210 -0
- package/src/collections/TicketActivityLog.ts +81 -0
- package/src/collections/TicketMessages.ts +364 -0
- package/src/collections/TicketStatuses.ts +108 -0
- package/src/collections/Tickets.ts +704 -0
- package/src/collections/TimeEntries.ts +105 -0
- package/src/collections/WebhookEndpoints.ts +96 -0
- package/src/collections/index.ts +16 -0
- package/src/components/TicketConversation/components/AISummaryPanel.tsx +85 -0
- package/src/components/TicketConversation/components/ActionPanels.tsx +140 -0
- package/src/components/TicketConversation/components/ActivityLog.tsx +39 -0
- package/src/components/TicketConversation/components/ClientBar.tsx +37 -0
- package/src/components/TicketConversation/components/ClientHistory.tsx +117 -0
- package/src/components/TicketConversation/components/CodeBlock.tsx +186 -0
- package/src/components/TicketConversation/components/CodeBlockInserter.tsx +166 -0
- package/src/components/TicketConversation/components/QuickActions.tsx +82 -0
- package/src/components/TicketConversation/components/TicketHeader.tsx +91 -0
- package/src/components/TicketConversation/components/TimeTrackingPanel.tsx +161 -0
- package/src/components/TicketConversation/config.ts +82 -0
- package/src/components/TicketConversation/constants.ts +74 -0
- package/src/components/TicketConversation/context.ts +63 -0
- package/src/components/TicketConversation/hooks/useAI.ts +180 -0
- package/src/components/TicketConversation/hooks/useMessageActions.ts +131 -0
- package/src/components/TicketConversation/hooks/useReply.ts +190 -0
- package/src/components/TicketConversation/hooks/useTicketActions.ts +205 -0
- package/src/components/TicketConversation/hooks/useTimeTracking.ts +107 -0
- package/src/components/TicketConversation/hooks/useTranslation.ts +116 -0
- package/src/components/TicketConversation/index.tsx +1110 -0
- package/src/components/TicketConversation/locales/en.json +878 -0
- package/src/components/TicketConversation/locales/fr.json +878 -0
- package/src/components/TicketConversation/types.ts +54 -0
- package/src/components/TicketConversation/utils.ts +25 -0
- package/src/endpoints/admin-chat-stream.ts +238 -0
- package/src/endpoints/admin-chat.ts +263 -0
- package/src/endpoints/admin-stats.ts +200 -0
- package/src/endpoints/ai.ts +199 -0
- package/src/endpoints/apply-macro.ts +144 -0
- package/src/endpoints/auth-2fa.ts +163 -0
- package/src/endpoints/auto-close.ts +175 -0
- package/src/endpoints/billing.ts +167 -0
- package/src/endpoints/bulk-action.ts +103 -0
- package/src/endpoints/chat-stream.ts +127 -0
- package/src/endpoints/chat.ts +188 -0
- package/src/endpoints/chatbot.ts +113 -0
- package/src/endpoints/delete-account.ts +129 -0
- package/src/endpoints/email-stats.ts +109 -0
- package/src/endpoints/export-csv.ts +84 -0
- package/src/endpoints/export-data.ts +104 -0
- package/src/endpoints/import-conversation.ts +307 -0
- package/src/endpoints/index.ts +154 -0
- package/src/endpoints/login.ts +92 -0
- package/src/endpoints/merge-clients.ts +132 -0
- package/src/endpoints/merge-tickets.ts +137 -0
- package/src/endpoints/oauth-google.ts +179 -0
- package/src/endpoints/pending-emails-process.ts +224 -0
- package/src/endpoints/presence.ts +104 -0
- package/src/endpoints/process-scheduled.ts +144 -0
- package/src/endpoints/purge-logs.ts +58 -0
- package/src/endpoints/resend-notification.ts +99 -0
- package/src/endpoints/round-robin-config.ts +92 -0
- package/src/endpoints/satisfaction.ts +93 -0
- package/src/endpoints/search.ts +106 -0
- package/src/endpoints/seed-kb.ts +153 -0
- package/src/endpoints/settings.ts +144 -0
- package/src/endpoints/signature.ts +93 -0
- package/src/endpoints/sla-check.ts +124 -0
- package/src/endpoints/split-ticket.ts +131 -0
- package/src/endpoints/statuses.ts +45 -0
- package/src/endpoints/track-open.ts +154 -0
- package/src/endpoints/typing.ts +101 -0
- package/src/endpoints/user-prefs.ts +125 -0
- package/src/hooks/checkSLA.ts +414 -0
- package/src/hooks/ticketStatusEmail.ts +182 -0
- package/src/index.ts +51 -0
- package/src/plugin.ts +157 -0
- package/src/portal/LiveChat.tsx +1353 -0
- package/src/portal/auth/ChatWidget.tsx +350 -0
- package/src/portal/auth/ChatbotWidget.tsx +285 -0
- package/src/portal/auth/SupportHeader.tsx +409 -0
- package/src/portal/auth/dashboard/DashboardClient.tsx +650 -0
- package/src/portal/auth/dashboard/page.tsx +84 -0
- package/src/portal/auth/faq/FAQSearch.tsx +117 -0
- package/src/portal/auth/faq/page.tsx +199 -0
- package/src/portal/auth/layout.tsx +61 -0
- package/src/portal/auth/profile/page.tsx +705 -0
- package/src/portal/auth/tickets/detail/CloseTicketButton.tsx +74 -0
- package/src/portal/auth/tickets/detail/CollapsibleMessages.tsx +46 -0
- package/src/portal/auth/tickets/detail/MarkSolutionButton.tsx +50 -0
- package/src/portal/auth/tickets/detail/MessageActions.tsx +158 -0
- package/src/portal/auth/tickets/detail/PrintButton.tsx +16 -0
- package/src/portal/auth/tickets/detail/ReadReceipt.tsx +34 -0
- package/src/portal/auth/tickets/detail/ReopenTicketButton.tsx +74 -0
- package/src/portal/auth/tickets/detail/SatisfactionForm.tsx +156 -0
- package/src/portal/auth/tickets/detail/TicketPolling.tsx +57 -0
- package/src/portal/auth/tickets/detail/TicketReplyForm.tsx +294 -0
- package/src/portal/auth/tickets/detail/TypingIndicator.tsx +58 -0
- package/src/portal/auth/tickets/detail/page.tsx +738 -0
- package/src/portal/auth/tickets/new/page.tsx +515 -0
- package/src/portal/forgot-password/page.tsx +114 -0
- package/src/portal/layout.tsx +26 -0
- package/src/portal/locales/en.json +374 -0
- package/src/portal/locales/fr.json +374 -0
- package/src/portal/login/page.tsx +351 -0
- package/src/portal/page.tsx +162 -0
- package/src/portal/register/page.tsx +281 -0
- package/src/portal/reset-password/page.tsx +152 -0
- package/src/styles/BillingView.module.scss +311 -0
- package/src/styles/ChatView.module.scss +438 -0
- package/src/styles/CommandPalette.module.scss +160 -0
- package/src/styles/CrmView.module.scss +554 -0
- package/src/styles/EmailTracking.module.scss +238 -0
- package/src/styles/ImportConversation.module.scss +267 -0
- package/src/styles/Layout.module.scss +55 -0
- package/src/styles/Logs.module.scss +164 -0
- package/src/styles/NewTicket.module.scss +143 -0
- package/src/styles/PendingEmails.module.scss +629 -0
- package/src/styles/SupportDashboard.module.scss +649 -0
- package/src/styles/TicketDetail.module.scss +1043 -0
- package/src/styles/TicketInbox.module.scss +296 -0
- package/src/styles/TicketingSettings.module.scss +358 -0
- package/src/styles/TimeDashboard.module.scss +287 -0
- package/src/styles/_tokens.scss +78 -0
- package/src/styles/theme.css +633 -0
- package/src/types.ts +255 -0
- package/src/utils/adminNotification.ts +38 -0
- package/src/utils/auth.ts +46 -0
- package/src/utils/emailTemplate.ts +343 -0
- package/src/utils/fireWebhooks.ts +84 -0
- package/src/utils/index.ts +22 -0
- package/src/utils/rateLimiter.ts +52 -0
- package/src/utils/readSettings.ts +67 -0
- package/src/utils/slugs.ts +54 -0
- package/src/utils/webhookDispatcher.ts +120 -0
- package/src/views/BillingView/client.tsx +137 -0
- package/src/views/BillingView/index.tsx +33 -0
- package/src/views/ChatView/client.tsx +294 -0
- package/src/views/ChatView/index.tsx +33 -0
- package/src/views/CrmView/client.tsx +206 -0
- package/src/views/CrmView/index.tsx +33 -0
- package/src/views/EmailTrackingView/client.tsx +124 -0
- package/src/views/EmailTrackingView/index.tsx +33 -0
- package/src/views/ImportConversationView/client.tsx +133 -0
- package/src/views/ImportConversationView/index.tsx +33 -0
- package/src/views/LogsView/client.tsx +151 -0
- package/src/views/LogsView/index.tsx +30 -0
- package/src/views/NewTicketView/client.tsx +227 -0
- package/src/views/NewTicketView/index.tsx +30 -0
- package/src/views/PendingEmailsView/client.tsx +177 -0
- package/src/views/PendingEmailsView/index.tsx +33 -0
- package/src/views/SupportDashboardView/client.tsx +424 -0
- package/src/views/SupportDashboardView/index.tsx +33 -0
- package/src/views/TicketDetailView/client.tsx +775 -0
- package/src/views/TicketDetailView/index.tsx +33 -0
- package/src/views/TicketInboxView/client.tsx +313 -0
- package/src/views/TicketInboxView/index.tsx +30 -0
- package/src/views/TicketingSettingsView/client.tsx +866 -0
- package/src/views/TicketingSettingsView/index.tsx +33 -0
- package/src/views/TimeDashboardView/client.tsx +144 -0
- package/src/views/TimeDashboardView/index.tsx +33 -0
- package/src/views/shared/AdminViewHeader.tsx +69 -0
- package/src/views/shared/ErrorBoundary.tsx +68 -0
- package/src/views/shared/Skeleton.tsx +125 -0
- package/src/views/shared/adminTokens.ts +37 -0
- package/src/views/shared/config.ts +82 -0
- package/src/views/shared/index.ts +6 -0
- package/src/views.ts +16 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import type { CollectionConfig, CollectionAfterChangeHook } from 'payload'
|
|
2
|
+
import type { CollectionSlugs } from '../utils/slugs'
|
|
3
|
+
|
|
4
|
+
// ─── Hooks ───────────────────────────────────────────────
|
|
5
|
+
|
|
6
|
+
function createRecalculateTicketTime(slugs: CollectionSlugs): CollectionAfterChangeHook {
|
|
7
|
+
return async ({ doc, req }) => {
|
|
8
|
+
if (!doc.ticket) return
|
|
9
|
+
|
|
10
|
+
const { payload } = req
|
|
11
|
+
const ticketId = typeof doc.ticket === 'object' ? doc.ticket.id : doc.ticket
|
|
12
|
+
|
|
13
|
+
// Sum all time entries for this ticket, paginating to avoid loading all at once
|
|
14
|
+
let totalMinutes = 0
|
|
15
|
+
let page = 1
|
|
16
|
+
let hasMore = true
|
|
17
|
+
|
|
18
|
+
while (hasMore) {
|
|
19
|
+
const entries = await payload.find({
|
|
20
|
+
collection: slugs.timeEntries,
|
|
21
|
+
where: { ticket: { equals: ticketId } },
|
|
22
|
+
limit: 100,
|
|
23
|
+
page,
|
|
24
|
+
depth: 0,
|
|
25
|
+
overrideAccess: true,
|
|
26
|
+
select: { duration: true },
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
for (const entry of entries.docs) {
|
|
30
|
+
totalMinutes += ((entry.duration as number) || 0)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
hasMore = entries.hasNextPage ?? false
|
|
34
|
+
page++
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
await payload.update({
|
|
38
|
+
collection: slugs.tickets,
|
|
39
|
+
id: ticketId,
|
|
40
|
+
data: { totalTimeMinutes: totalMinutes },
|
|
41
|
+
overrideAccess: true,
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ─── Collection factory ──────────────────────────────────
|
|
47
|
+
|
|
48
|
+
export function createTimeEntriesCollection(slugs: CollectionSlugs): CollectionConfig {
|
|
49
|
+
return {
|
|
50
|
+
slug: slugs.timeEntries,
|
|
51
|
+
labels: {
|
|
52
|
+
singular: 'Entrée de temps',
|
|
53
|
+
plural: 'Entrées de temps',
|
|
54
|
+
},
|
|
55
|
+
admin: {
|
|
56
|
+
group: 'Gestion',
|
|
57
|
+
defaultColumns: ['ticket', 'duration', 'description', 'date'],
|
|
58
|
+
},
|
|
59
|
+
fields: [
|
|
60
|
+
{
|
|
61
|
+
name: 'ticket',
|
|
62
|
+
type: 'relationship',
|
|
63
|
+
relationTo: slugs.tickets,
|
|
64
|
+
required: true,
|
|
65
|
+
label: 'Ticket',
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
type: 'row',
|
|
69
|
+
fields: [
|
|
70
|
+
{
|
|
71
|
+
name: 'duration',
|
|
72
|
+
type: 'number',
|
|
73
|
+
required: true,
|
|
74
|
+
label: 'Durée (minutes)',
|
|
75
|
+
min: 1,
|
|
76
|
+
admin: { width: '50%' },
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
name: 'date',
|
|
80
|
+
type: 'date',
|
|
81
|
+
required: true,
|
|
82
|
+
label: 'Date',
|
|
83
|
+
defaultValue: () => new Date().toISOString(),
|
|
84
|
+
admin: { width: '50%', date: { displayFormat: 'dd/MM/yyyy' } },
|
|
85
|
+
},
|
|
86
|
+
],
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
name: 'description',
|
|
90
|
+
type: 'textarea',
|
|
91
|
+
label: 'Description du travail',
|
|
92
|
+
},
|
|
93
|
+
],
|
|
94
|
+
hooks: {
|
|
95
|
+
afterChange: [createRecalculateTicketTime(slugs)],
|
|
96
|
+
},
|
|
97
|
+
access: {
|
|
98
|
+
create: ({ req }) => req.user?.collection === slugs.users,
|
|
99
|
+
read: ({ req }) => req.user?.collection === slugs.users,
|
|
100
|
+
update: ({ req }) => req.user?.collection === slugs.users,
|
|
101
|
+
delete: ({ req }) => req.user?.collection === slugs.users,
|
|
102
|
+
},
|
|
103
|
+
timestamps: true,
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import type { CollectionConfig } from 'payload'
|
|
2
|
+
import type { CollectionSlugs } from '../utils/slugs'
|
|
3
|
+
|
|
4
|
+
// ─── Collection factory ──────────────────────────────────
|
|
5
|
+
|
|
6
|
+
export function createWebhookEndpointsCollection(slugs: CollectionSlugs): CollectionConfig {
|
|
7
|
+
return {
|
|
8
|
+
slug: slugs.webhookEndpoints,
|
|
9
|
+
labels: {
|
|
10
|
+
singular: 'Webhook',
|
|
11
|
+
plural: 'Webhooks',
|
|
12
|
+
},
|
|
13
|
+
admin: {
|
|
14
|
+
useAsTitle: 'name',
|
|
15
|
+
group: 'Support',
|
|
16
|
+
defaultColumns: ['name', 'url', 'events', 'active', 'lastTriggeredAt', 'lastStatus'],
|
|
17
|
+
},
|
|
18
|
+
access: {
|
|
19
|
+
create: ({ req }) => req.user?.collection === slugs.users,
|
|
20
|
+
read: ({ req }) => req.user?.collection === slugs.users,
|
|
21
|
+
update: ({ req }) => req.user?.collection === slugs.users,
|
|
22
|
+
delete: ({ req }) => req.user?.collection === slugs.users,
|
|
23
|
+
},
|
|
24
|
+
fields: [
|
|
25
|
+
{
|
|
26
|
+
name: 'name',
|
|
27
|
+
type: 'text',
|
|
28
|
+
required: true,
|
|
29
|
+
label: 'Nom',
|
|
30
|
+
admin: {
|
|
31
|
+
description: 'Ex: Slack notifications, n8n workflow…',
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
name: 'url',
|
|
36
|
+
type: 'text',
|
|
37
|
+
required: true,
|
|
38
|
+
label: 'URL',
|
|
39
|
+
admin: {
|
|
40
|
+
description: 'URL du webhook à appeler (POST)',
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
name: 'secret',
|
|
45
|
+
type: 'text',
|
|
46
|
+
label: 'Secret HMAC',
|
|
47
|
+
admin: {
|
|
48
|
+
description: 'Secret optionnel pour signer les payloads (HMAC-SHA256, header X-Webhook-Signature)',
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
name: 'events',
|
|
53
|
+
type: 'select',
|
|
54
|
+
hasMany: true,
|
|
55
|
+
required: true,
|
|
56
|
+
label: 'Événements',
|
|
57
|
+
options: [
|
|
58
|
+
{ label: 'Ticket créé', value: 'ticket_created' },
|
|
59
|
+
{ label: 'Ticket résolu', value: 'ticket_resolved' },
|
|
60
|
+
{ label: 'Réponse au ticket', value: 'ticket_replied' },
|
|
61
|
+
{ label: 'Ticket assigné', value: 'ticket_assigned' },
|
|
62
|
+
{ label: 'SLA dépassé', value: 'sla_breached' },
|
|
63
|
+
],
|
|
64
|
+
admin: {
|
|
65
|
+
description: 'Événements qui déclenchent ce webhook',
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
name: 'active',
|
|
70
|
+
type: 'checkbox',
|
|
71
|
+
defaultValue: true,
|
|
72
|
+
label: 'Actif',
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
name: 'lastTriggeredAt',
|
|
76
|
+
type: 'date',
|
|
77
|
+
label: 'Dernier déclenchement',
|
|
78
|
+
admin: {
|
|
79
|
+
readOnly: true,
|
|
80
|
+
date: { displayFormat: 'dd/MM/yyyy HH:mm' },
|
|
81
|
+
position: 'sidebar',
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
name: 'lastStatus',
|
|
86
|
+
type: 'number',
|
|
87
|
+
label: 'Dernier statut HTTP',
|
|
88
|
+
admin: {
|
|
89
|
+
readOnly: true,
|
|
90
|
+
position: 'sidebar',
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
],
|
|
94
|
+
timestamps: true,
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export { createTicketsCollection } from './Tickets'
|
|
2
|
+
export { createTicketMessagesCollection } from './TicketMessages'
|
|
3
|
+
export { createSupportClientsCollection } from './SupportClients'
|
|
4
|
+
export { createTimeEntriesCollection } from './TimeEntries'
|
|
5
|
+
export { createCannedResponsesCollection } from './CannedResponses'
|
|
6
|
+
export { createTicketActivityLogCollection } from './TicketActivityLog'
|
|
7
|
+
export { createSatisfactionSurveysCollection } from './SatisfactionSurveys'
|
|
8
|
+
export { createKnowledgeBaseCollection } from './KnowledgeBase'
|
|
9
|
+
export { createChatMessagesCollection } from './ChatMessages'
|
|
10
|
+
export { createPendingEmailsCollection } from './PendingEmails'
|
|
11
|
+
export { createEmailLogsCollection } from './EmailLogs'
|
|
12
|
+
export { createAuthLogsCollection } from './AuthLogs'
|
|
13
|
+
export { createWebhookEndpointsCollection } from './WebhookEndpoints'
|
|
14
|
+
export { createSlaPoliciesCollection } from './SlaPolicies'
|
|
15
|
+
export { createMacrosCollection } from './Macros'
|
|
16
|
+
export { createTicketStatusesCollection } from './TicketStatuses'
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import React from 'react'
|
|
4
|
+
import { s } from '../constants'
|
|
5
|
+
|
|
6
|
+
interface AISummaryPanelProps {
|
|
7
|
+
showAiSummary: boolean
|
|
8
|
+
setShowAiSummary: (v: boolean) => void
|
|
9
|
+
aiSummary: string
|
|
10
|
+
aiGenerating: boolean
|
|
11
|
+
aiSaving: boolean
|
|
12
|
+
aiSaved: boolean
|
|
13
|
+
handleAiGenerate: () => void
|
|
14
|
+
handleAiSave: () => void
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function AISummaryPanel({
|
|
18
|
+
showAiSummary, setShowAiSummary, aiSummary, aiGenerating, aiSaving, aiSaved,
|
|
19
|
+
handleAiGenerate, handleAiSave,
|
|
20
|
+
}: AISummaryPanelProps) {
|
|
21
|
+
return (
|
|
22
|
+
<div style={{ marginBottom: '14px' }}>
|
|
23
|
+
<button
|
|
24
|
+
onClick={() => { setShowAiSummary(!showAiSummary); if (!showAiSummary && !aiSummary) handleAiGenerate() }}
|
|
25
|
+
style={{
|
|
26
|
+
...s.ghostBtn('#7c3aed', false),
|
|
27
|
+
fontSize: '12px',
|
|
28
|
+
display: 'inline-flex',
|
|
29
|
+
alignItems: 'center',
|
|
30
|
+
gap: '6px',
|
|
31
|
+
}}
|
|
32
|
+
>
|
|
33
|
+
{showAiSummary ? 'Masquer la synthèse IA' : 'Synthèse IA'}
|
|
34
|
+
</button>
|
|
35
|
+
{showAiSummary && (
|
|
36
|
+
<div style={{
|
|
37
|
+
marginTop: '10px', padding: '14px 18px', borderRadius: '8px',
|
|
38
|
+
backgroundColor: '#faf5ff', border: '1px solid #e9d5ff',
|
|
39
|
+
}}>
|
|
40
|
+
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '10px' }}>
|
|
41
|
+
<h4 style={{ fontSize: '13px', fontWeight: 600, color: '#7c3aed', margin: 0 }}>
|
|
42
|
+
Synthèse IA
|
|
43
|
+
</h4>
|
|
44
|
+
<div style={{ display: 'flex', gap: '6px' }}>
|
|
45
|
+
<button
|
|
46
|
+
onClick={handleAiGenerate}
|
|
47
|
+
disabled={aiGenerating}
|
|
48
|
+
style={{ ...s.outlineBtn('#7c3aed', aiGenerating), fontSize: '11px', padding: '4px 10px' }}
|
|
49
|
+
>
|
|
50
|
+
{aiGenerating ? 'Génération...' : 'Régénérer'}
|
|
51
|
+
</button>
|
|
52
|
+
{aiSummary && !aiGenerating && (
|
|
53
|
+
<button
|
|
54
|
+
onClick={handleAiSave}
|
|
55
|
+
disabled={aiSaving || aiSaved}
|
|
56
|
+
style={{ ...s.btn(aiSaved ? '#16a34a' : '#2563eb', aiSaving), fontSize: '11px', padding: '4px 10px' }}
|
|
57
|
+
>
|
|
58
|
+
{aiSaved ? 'Sauvegardé' : aiSaving ? 'Sauvegarde...' : 'Sauvegarder (note interne)'}
|
|
59
|
+
</button>
|
|
60
|
+
)}
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
{aiGenerating ? (
|
|
64
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: '10px', color: '#7c3aed', fontSize: '13px' }}>
|
|
65
|
+
Analyse de la conversation en cours...
|
|
66
|
+
</div>
|
|
67
|
+
) : aiSummary ? (
|
|
68
|
+
<div
|
|
69
|
+
style={{ fontSize: '13px', lineHeight: '1.7', color: '#1e1b4b', whiteSpace: 'pre-wrap' }}
|
|
70
|
+
dangerouslySetInnerHTML={{
|
|
71
|
+
__html: aiSummary
|
|
72
|
+
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
|
|
73
|
+
.replace(/\n/g, '<br/>'),
|
|
74
|
+
}}
|
|
75
|
+
/>
|
|
76
|
+
) : (
|
|
77
|
+
<p style={{ color: '#999', fontStyle: 'italic', fontSize: '13px', margin: 0 }}>
|
|
78
|
+
Cliquez sur "Régénérer" pour lancer l'analyse
|
|
79
|
+
</p>
|
|
80
|
+
)}
|
|
81
|
+
</div>
|
|
82
|
+
)}
|
|
83
|
+
</div>
|
|
84
|
+
)
|
|
85
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import React, { useRef } from 'react'
|
|
4
|
+
import { s } from '../constants'
|
|
5
|
+
|
|
6
|
+
// ========== MERGE PANEL ==========
|
|
7
|
+
interface MergePanelProps {
|
|
8
|
+
mergeTarget: string
|
|
9
|
+
setMergeTarget: (v: string) => void
|
|
10
|
+
mergeTargetInfo: { id: number; ticketNumber: string; subject: string } | null
|
|
11
|
+
setMergeTargetInfo: (v: null) => void
|
|
12
|
+
mergeError: string
|
|
13
|
+
setMergeError: (v: string) => void
|
|
14
|
+
merging: boolean
|
|
15
|
+
handleMergeLookup: () => void
|
|
16
|
+
handleMerge: () => void
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function MergePanel({
|
|
20
|
+
mergeTarget, setMergeTarget, mergeTargetInfo, setMergeTargetInfo,
|
|
21
|
+
mergeError, setMergeError, merging, handleMergeLookup, handleMerge,
|
|
22
|
+
}: MergePanelProps) {
|
|
23
|
+
return (
|
|
24
|
+
<div style={{ padding: '14px 18px', borderRadius: '8px', backgroundColor: '#fdf2f8', border: '1px solid #fbcfe8', marginBottom: '14px' }}>
|
|
25
|
+
<h4 style={{ fontSize: '13px', fontWeight: 600, marginBottom: '10px', color: '#831843' }}>Fusionner ce ticket dans un autre</h4>
|
|
26
|
+
<div style={{ display: 'flex', gap: '8px', alignItems: 'center', marginBottom: '8px' }}>
|
|
27
|
+
<input
|
|
28
|
+
type="text"
|
|
29
|
+
value={mergeTarget}
|
|
30
|
+
onChange={(e) => { setMergeTarget(e.target.value); setMergeTargetInfo(null); setMergeError('') }}
|
|
31
|
+
placeholder="TK-0001"
|
|
32
|
+
style={{ ...s.input, width: '130px' }}
|
|
33
|
+
/>
|
|
34
|
+
<button onClick={handleMergeLookup} style={{ ...s.outlineBtn('#ec4899'), fontSize: '12px', padding: '6px 14px' }}>
|
|
35
|
+
Rechercher
|
|
36
|
+
</button>
|
|
37
|
+
{mergeError && <span style={{ fontSize: '12px', color: '#be185d', fontWeight: 600 }}>{mergeError}</span>}
|
|
38
|
+
</div>
|
|
39
|
+
{mergeTargetInfo && (
|
|
40
|
+
<div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
|
|
41
|
+
<span style={{ fontSize: '13px', fontWeight: 600 }}>
|
|
42
|
+
{mergeTargetInfo.ticketNumber} — {mergeTargetInfo.subject}
|
|
43
|
+
</span>
|
|
44
|
+
<button onClick={handleMerge} disabled={merging} style={{ ...s.btn('#ec4899', merging), color: '#fff', fontSize: '12px', padding: '6px 14px' }}>
|
|
45
|
+
{merging ? 'Fusion...' : 'Confirmer la fusion'}
|
|
46
|
+
</button>
|
|
47
|
+
</div>
|
|
48
|
+
)}
|
|
49
|
+
</div>
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ========== EXTERNAL MESSAGE PANEL ==========
|
|
54
|
+
interface ExtMessagePanelProps {
|
|
55
|
+
extMsgBody: string
|
|
56
|
+
setExtMsgBody: (v: string) => void
|
|
57
|
+
extMsgAuthor: 'client' | 'admin'
|
|
58
|
+
setExtMsgAuthor: (v: 'client' | 'admin') => void
|
|
59
|
+
extMsgDate: string
|
|
60
|
+
setExtMsgDate: (v: string) => void
|
|
61
|
+
extMsgFiles: File[]
|
|
62
|
+
setExtMsgFiles: React.Dispatch<React.SetStateAction<File[]>>
|
|
63
|
+
sendingExtMsg: boolean
|
|
64
|
+
handleSendExtMsg: () => void
|
|
65
|
+
handleExtFileChange: (e: React.ChangeEvent<HTMLInputElement>) => void
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function ExtMessagePanel({
|
|
69
|
+
extMsgBody, setExtMsgBody, extMsgAuthor, setExtMsgAuthor,
|
|
70
|
+
extMsgDate, setExtMsgDate, extMsgFiles, setExtMsgFiles,
|
|
71
|
+
sendingExtMsg, handleSendExtMsg, handleExtFileChange,
|
|
72
|
+
}: ExtMessagePanelProps) {
|
|
73
|
+
const extFileInputRef = useRef<HTMLInputElement>(null)
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
<div style={{ padding: '14px 18px', borderRadius: '8px', backgroundColor: '#eef2ff', border: '1px solid #c7d2fe', marginBottom: '14px' }}>
|
|
77
|
+
<h4 style={{ fontSize: '13px', fontWeight: 600, marginBottom: '10px', color: '#312e81' }}>Ajouter un message reçu (email, SMS, WhatsApp...)</h4>
|
|
78
|
+
<textarea
|
|
79
|
+
value={extMsgBody}
|
|
80
|
+
onChange={(e) => setExtMsgBody(e.target.value)}
|
|
81
|
+
placeholder="Coller le contenu du message reçu..."
|
|
82
|
+
rows={3}
|
|
83
|
+
style={{ ...s.input, width: '100%', resize: 'vertical', marginBottom: '10px' }}
|
|
84
|
+
/>
|
|
85
|
+
<div style={{ display: 'flex', gap: '8px', alignItems: 'center', flexWrap: 'wrap', marginBottom: '10px' }}>
|
|
86
|
+
<select value={extMsgAuthor} onChange={(e) => setExtMsgAuthor(e.target.value as 'client' | 'admin')} style={{ ...s.input, fontSize: '12px' }}>
|
|
87
|
+
<option value="client">Envoyé par le client</option>
|
|
88
|
+
<option value="admin">Envoyé par le support</option>
|
|
89
|
+
</select>
|
|
90
|
+
<input
|
|
91
|
+
type="datetime-local"
|
|
92
|
+
value={extMsgDate}
|
|
93
|
+
onChange={(e) => setExtMsgDate(e.target.value)}
|
|
94
|
+
style={{ ...s.input, fontSize: '12px' }}
|
|
95
|
+
/>
|
|
96
|
+
<input ref={extFileInputRef} type="file" multiple onChange={handleExtFileChange} style={{ display: 'none' }} accept="image/*,.pdf,.doc,.docx,.txt,.zip" />
|
|
97
|
+
<button type="button" onClick={() => extFileInputRef.current?.click()} style={{ ...s.ghostBtn('#6b7280'), fontSize: '12px', padding: '6px 12px' }}>
|
|
98
|
+
+ PJ
|
|
99
|
+
</button>
|
|
100
|
+
</div>
|
|
101
|
+
{extMsgFiles.length > 0 && (
|
|
102
|
+
<div style={{ marginBottom: '10px', display: 'flex', gap: '6px', flexWrap: 'wrap' }}>
|
|
103
|
+
{extMsgFiles.map((file, i) => (
|
|
104
|
+
<span key={i} style={{ ...s.badge('#f1f5f9', '#374151'), display: 'inline-flex', alignItems: 'center', gap: '4px' }}>
|
|
105
|
+
{file.name}
|
|
106
|
+
<button type="button" onClick={() => setExtMsgFiles((prev) => prev.filter((_, idx) => idx !== i))} style={{ border: 'none', background: 'none', color: '#ef4444', fontWeight: 700, cursor: 'pointer', fontSize: '14px', lineHeight: 1 }}>×</button>
|
|
107
|
+
</span>
|
|
108
|
+
))}
|
|
109
|
+
</div>
|
|
110
|
+
)}
|
|
111
|
+
<button onClick={handleSendExtMsg} disabled={sendingExtMsg || !extMsgBody.trim()} style={s.btn('#818cf8', sendingExtMsg || !extMsgBody.trim())}>
|
|
112
|
+
{sendingExtMsg ? 'Ajout...' : 'Ajouter (sans notification)'}
|
|
113
|
+
</button>
|
|
114
|
+
</div>
|
|
115
|
+
)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ========== SNOOZE PANEL ==========
|
|
119
|
+
interface SnoozePanelProps {
|
|
120
|
+
snoozeSaving: boolean
|
|
121
|
+
handleSnooze: (days: number | null, customDate?: string) => void
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function SnoozePanel({ snoozeSaving, handleSnooze }: SnoozePanelProps) {
|
|
125
|
+
return (
|
|
126
|
+
<div style={{ padding: '14px 18px', borderRadius: '8px', backgroundColor: '#faf5ff', border: '1px solid #e9d5ff', marginBottom: '14px' }}>
|
|
127
|
+
<h4 style={{ fontSize: '13px', fontWeight: 600, marginBottom: '10px', color: '#5b21b6' }}>Snooze — masquer temporairement</h4>
|
|
128
|
+
<div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap', alignItems: 'center' }}>
|
|
129
|
+
<button onClick={() => handleSnooze(1)} disabled={snoozeSaving} style={{ ...s.outlineBtn('#8b5cf6', snoozeSaving), fontSize: '12px' }}>1 jour</button>
|
|
130
|
+
<button onClick={() => handleSnooze(3)} disabled={snoozeSaving} style={{ ...s.outlineBtn('#8b5cf6', snoozeSaving), fontSize: '12px' }}>3 jours</button>
|
|
131
|
+
<button onClick={() => handleSnooze(7)} disabled={snoozeSaving} style={{ ...s.outlineBtn('#8b5cf6', snoozeSaving), fontSize: '12px' }}>1 semaine</button>
|
|
132
|
+
<input
|
|
133
|
+
type="datetime-local"
|
|
134
|
+
onChange={(e) => { if (e.target.value) handleSnooze(null, e.target.value) }}
|
|
135
|
+
style={{ ...s.input, fontSize: '12px' }}
|
|
136
|
+
/>
|
|
137
|
+
</div>
|
|
138
|
+
</div>
|
|
139
|
+
)
|
|
140
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import React, { useState } from 'react'
|
|
4
|
+
import type { ActivityEntry } from '../types'
|
|
5
|
+
import { C, s } from '../constants'
|
|
6
|
+
|
|
7
|
+
export function ActivityLog({ activityLog }: { activityLog: ActivityEntry[] }) {
|
|
8
|
+
const [showActivity, setShowActivity] = useState(false)
|
|
9
|
+
|
|
10
|
+
return (
|
|
11
|
+
<div style={s.section}>
|
|
12
|
+
<h4 style={{ ...s.sectionTitle, cursor: 'pointer' }} onClick={() => setShowActivity(!showActivity)}>
|
|
13
|
+
Historique <span style={s.badge('#f1f5f9', '#475569')}>{activityLog.length}</span>
|
|
14
|
+
<span style={{ fontSize: '12px', color: C.textMuted, transition: 'transform 0.2s', display: 'inline-block', transform: showActivity ? 'rotate(90deg)' : 'none' }}>▶</span>
|
|
15
|
+
</h4>
|
|
16
|
+
|
|
17
|
+
{showActivity && (
|
|
18
|
+
activityLog.length === 0 ? (
|
|
19
|
+
<p style={{ fontSize: '12px', color: C.textMuted, fontStyle: 'italic' }}>Aucune activité enregistrée.</p>
|
|
20
|
+
) : (
|
|
21
|
+
<div style={{ fontSize: '12px', display: 'flex', flexDirection: 'column', gap: '2px', borderLeft: '2px solid #bfdbfe', paddingLeft: '14px', marginLeft: '4px' }}>
|
|
22
|
+
{activityLog.map((entry) => (
|
|
23
|
+
<div key={entry.id} style={{ display: 'flex', gap: '10px', alignItems: 'center', padding: '5px 0', borderBottom: '1px solid #f8fafc' }}>
|
|
24
|
+
<span style={{ color: C.textMuted, fontSize: '11px', whiteSpace: 'nowrap', fontWeight: 500 }}>
|
|
25
|
+
{new Date(entry.createdAt).toLocaleString('fr-FR', { day: 'numeric', month: 'short', hour: '2-digit', minute: '2-digit' })}
|
|
26
|
+
</span>
|
|
27
|
+
<span style={s.badge(entry.actorType === 'admin' ? '#eff6ff' : '#dcfce7', entry.actorType === 'admin' ? '#1e40af' : '#166534')}>
|
|
28
|
+
{entry.actorType || 'system'}
|
|
29
|
+
</span>
|
|
30
|
+
<span style={{ color: '#374151', fontWeight: 500 }}>{(entry.detail || entry.action).replace(/\[object Object\]/g, '(utilisateur)')}</span>
|
|
31
|
+
<span style={{ color: C.textMuted, fontSize: '11px' }}>{entry.actorEmail}</span>
|
|
32
|
+
</div>
|
|
33
|
+
))}
|
|
34
|
+
</div>
|
|
35
|
+
)
|
|
36
|
+
)}
|
|
37
|
+
</div>
|
|
38
|
+
)
|
|
39
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import React from 'react'
|
|
4
|
+
import type { ClientInfo } from '../types'
|
|
5
|
+
import { C, s } from '../constants'
|
|
6
|
+
|
|
7
|
+
export function ClientBar({ client }: { client: ClientInfo }) {
|
|
8
|
+
return (
|
|
9
|
+
<div style={{
|
|
10
|
+
display: 'flex', alignItems: 'center', gap: '12px', padding: '10px 16px',
|
|
11
|
+
borderRadius: '8px', backgroundColor: 'var(--theme-elevation-100)',
|
|
12
|
+
marginBottom: '16px', flexWrap: 'wrap',
|
|
13
|
+
}}>
|
|
14
|
+
<span style={{ fontSize: '16px' }}>👤</span>
|
|
15
|
+
<span style={{ fontWeight: 700, fontSize: '13px', color: C.textPrimary }}>{client.company}</span>
|
|
16
|
+
<span style={{ color: C.textSecondary, fontSize: '13px' }}>
|
|
17
|
+
{client.firstName} {client.lastName}
|
|
18
|
+
</span>
|
|
19
|
+
<span style={{ color: C.textMuted, fontSize: '12px' }}>|</span>
|
|
20
|
+
<a href={`mailto:${client.email}`} style={{ color: '#2563eb', fontSize: '12px', fontWeight: 600, textDecoration: 'none' }}>{client.email}</a>
|
|
21
|
+
{client.phone && <span style={{ color: C.textSecondary, fontSize: '12px' }}>{client.phone}</span>}
|
|
22
|
+
<span style={{ marginLeft: 'auto', display: 'inline-flex', gap: '8px', alignItems: 'center' }}>
|
|
23
|
+
<a href={`/admin/collections/support-clients/${client.id}`} style={{ ...s.ghostBtn('#475569'), fontSize: '11px', padding: '4px 10px', textDecoration: 'none' }}>
|
|
24
|
+
Fiche client
|
|
25
|
+
</a>
|
|
26
|
+
<button
|
|
27
|
+
type="button"
|
|
28
|
+
onClick={() => window.open(`/api/admin/impersonate?clientId=${client.id}`, '_blank')}
|
|
29
|
+
style={{ ...s.ghostBtn('#7c3aed'), fontSize: '11px', padding: '4px 10px' }}
|
|
30
|
+
title="Se connecter au portail support en tant que ce client"
|
|
31
|
+
>
|
|
32
|
+
Voir en tant que client
|
|
33
|
+
</button>
|
|
34
|
+
</span>
|
|
35
|
+
</div>
|
|
36
|
+
)
|
|
37
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import React, { useState } from 'react'
|
|
4
|
+
import type { ClientInfo } from '../types'
|
|
5
|
+
import { statusLabels, projectStatusLabels, C, s } from '../constants'
|
|
6
|
+
|
|
7
|
+
interface ClientHistoryProps {
|
|
8
|
+
client: ClientInfo
|
|
9
|
+
clientTickets: Array<{ id: number; ticketNumber: string; subject: string; status: string; createdAt: string }>
|
|
10
|
+
clientProjects: Array<{ id: number; name: string; status: string }>
|
|
11
|
+
clientNotes: string
|
|
12
|
+
onNotesChange: (notes: string) => void
|
|
13
|
+
onNotesSave: () => void
|
|
14
|
+
savingNotes: boolean
|
|
15
|
+
notesSaved: boolean
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function ClientHistory({
|
|
19
|
+
client: _client, clientTickets, clientProjects, clientNotes, onNotesChange, onNotesSave, savingNotes, notesSaved,
|
|
20
|
+
}: ClientHistoryProps) {
|
|
21
|
+
const [showClientHistory, setShowClientHistory] = useState(false)
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<div style={s.section}>
|
|
25
|
+
<button
|
|
26
|
+
type="button"
|
|
27
|
+
onClick={() => setShowClientHistory(!showClientHistory)}
|
|
28
|
+
style={{
|
|
29
|
+
background: 'none', border: `1px dashed ${C.border}`, borderRadius: '6px',
|
|
30
|
+
padding: '8px 14px', cursor: 'pointer', color: C.textSecondary, fontSize: '13px',
|
|
31
|
+
fontWeight: 600, width: '100%', textAlign: 'left',
|
|
32
|
+
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
|
33
|
+
}}
|
|
34
|
+
>
|
|
35
|
+
<span>Historique client ({clientTickets.length} ticket{clientTickets.length !== 1 ? 's' : ''}, {clientProjects.length} projet{clientProjects.length !== 1 ? 's' : ''})</span>
|
|
36
|
+
<span style={{ fontSize: '12px', transition: 'transform 0.2s', display: 'inline-block', transform: showClientHistory ? 'rotate(90deg)' : 'none' }}>▶</span>
|
|
37
|
+
</button>
|
|
38
|
+
{showClientHistory && (
|
|
39
|
+
<div style={{ marginTop: '10px', padding: '14px 18px', borderRadius: '8px', backgroundColor: C.white, border: `1px solid ${C.border}` }}>
|
|
40
|
+
{/* Past tickets */}
|
|
41
|
+
<h5 style={{ fontSize: '12px', fontWeight: 600, color: C.textSecondary, marginBottom: '8px' }}>Derniers tickets</h5>
|
|
42
|
+
{clientTickets.length === 0 ? (
|
|
43
|
+
<p style={{ fontSize: '12px', color: C.textMuted, fontStyle: 'italic', marginBottom: '14px' }}>Aucun autre ticket</p>
|
|
44
|
+
) : (
|
|
45
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: '4px', marginBottom: '14px' }}>
|
|
46
|
+
{clientTickets.map((t) => {
|
|
47
|
+
const st = statusLabels[t.status] || statusLabels.open
|
|
48
|
+
return (
|
|
49
|
+
<a
|
|
50
|
+
key={t.id}
|
|
51
|
+
href={`/admin/support/ticket?id=${t.id}`}
|
|
52
|
+
style={{
|
|
53
|
+
display: 'flex', alignItems: 'center', gap: '8px', padding: '6px 10px',
|
|
54
|
+
borderRadius: '6px', border: `1px solid ${C.border}`, textDecoration: 'none',
|
|
55
|
+
fontSize: '12px', color: '#374151', backgroundColor: '#fafafa',
|
|
56
|
+
}}
|
|
57
|
+
>
|
|
58
|
+
<span style={{ fontFamily: "'SF Mono', 'Fira Code', 'JetBrains Mono', 'Cascadia Code', monospace", fontWeight: 600, color: C.textMuted, fontSize: '11px', whiteSpace: 'nowrap' }}>{t.ticketNumber}</span>
|
|
59
|
+
<span style={{ flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', fontWeight: 600 }}>{t.subject}</span>
|
|
60
|
+
<span style={s.badge(st.bg, st.color)}>{st.label}</span>
|
|
61
|
+
<span style={{ fontSize: '10px', color: C.textMuted, whiteSpace: 'nowrap' }}>
|
|
62
|
+
{new Date(t.createdAt).toLocaleDateString('fr-FR', { day: 'numeric', month: 'short' })}
|
|
63
|
+
</span>
|
|
64
|
+
</a>
|
|
65
|
+
)
|
|
66
|
+
})}
|
|
67
|
+
</div>
|
|
68
|
+
)}
|
|
69
|
+
|
|
70
|
+
{/* Projects */}
|
|
71
|
+
<h5 style={{ fontSize: '12px', fontWeight: 600, color: C.textSecondary, marginBottom: '8px' }}>Projets</h5>
|
|
72
|
+
{clientProjects.length === 0 ? (
|
|
73
|
+
<p style={{ fontSize: '12px', color: C.textMuted, fontStyle: 'italic', marginBottom: '14px' }}>Aucun projet</p>
|
|
74
|
+
) : (
|
|
75
|
+
<div style={{ display: 'flex', gap: '6px', flexWrap: 'wrap', marginBottom: '14px' }}>
|
|
76
|
+
{clientProjects.map((p) => {
|
|
77
|
+
const ps = projectStatusLabels[p.status] || projectStatusLabels.active
|
|
78
|
+
return (
|
|
79
|
+
<a
|
|
80
|
+
key={p.id}
|
|
81
|
+
href={`/admin/collections/projects/${p.id}`}
|
|
82
|
+
style={{
|
|
83
|
+
display: 'inline-flex', alignItems: 'center', gap: '6px', padding: '5px 10px',
|
|
84
|
+
borderRadius: '6px', border: `1px solid ${C.border}`, textDecoration: 'none',
|
|
85
|
+
fontSize: '12px', fontWeight: 600, color: '#374151', backgroundColor: '#fafafa',
|
|
86
|
+
}}
|
|
87
|
+
>
|
|
88
|
+
{p.name}
|
|
89
|
+
<span style={s.badge(ps.bg, ps.color)}>{ps.label}</span>
|
|
90
|
+
</a>
|
|
91
|
+
)
|
|
92
|
+
})}
|
|
93
|
+
</div>
|
|
94
|
+
)}
|
|
95
|
+
|
|
96
|
+
{/* Notes */}
|
|
97
|
+
<h5 style={{ fontSize: '12px', fontWeight: 600, color: C.textSecondary, marginBottom: '8px' }}>Notes internes</h5>
|
|
98
|
+
<textarea
|
|
99
|
+
value={clientNotes}
|
|
100
|
+
onChange={(e) => onNotesChange(e.target.value)}
|
|
101
|
+
rows={3}
|
|
102
|
+
style={{ ...s.input, width: '100%', resize: 'vertical', fontSize: '12px', marginBottom: '8px' }}
|
|
103
|
+
placeholder="Notes sur ce client..."
|
|
104
|
+
/>
|
|
105
|
+
<button
|
|
106
|
+
type="button"
|
|
107
|
+
onClick={onNotesSave}
|
|
108
|
+
disabled={savingNotes}
|
|
109
|
+
style={{ ...s.btn(notesSaved ? '#16a34a' : C.blue, savingNotes), fontSize: '11px', padding: '5px 12px' }}
|
|
110
|
+
>
|
|
111
|
+
{notesSaved ? 'Sauvegarde OK' : savingNotes ? 'Sauvegarde...' : 'Sauvegarder les notes'}
|
|
112
|
+
</button>
|
|
113
|
+
</div>
|
|
114
|
+
)}
|
|
115
|
+
</div>
|
|
116
|
+
)
|
|
117
|
+
}
|