@consilioweb/payload-support 5.0.0 → 6.0.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.
Files changed (120) hide show
  1. package/README.md +249 -22
  2. package/dist/components/RichTextEditor/index.js +1 -1
  3. package/dist/components/TicketConversation/RewriteDropdown.js +1 -1
  4. package/dist/components/TicketConversation/components/AISummaryPanel.js +3 -0
  5. package/dist/components/TicketConversation/components/ActionPanels.js +13 -8
  6. package/dist/components/TicketConversation/components/ClientHistory.js +1 -0
  7. package/dist/components/TicketConversation/components/CodeBlock.js +1 -0
  8. package/dist/components/TicketConversation/components/CodeBlockInserter.js +1 -1
  9. package/dist/components/TicketConversation/components/QuickActions.js +10 -8
  10. package/dist/components/TicketConversation/components/TicketHeader.js +2 -0
  11. package/dist/components/TicketConversation/components/TimeTrackingPanel.js +15 -9
  12. package/dist/components/TicketConversation/index.d.ts +10 -0
  13. package/dist/components/TicketConversation/index.js +25 -7
  14. package/dist/index.cjs +306 -60
  15. package/dist/index.d.cts +107 -38
  16. package/dist/index.d.ts +107 -38
  17. package/dist/index.js +300 -59
  18. package/dist/styles/BillingView.module.scss +11 -11
  19. package/dist/styles/ChatView.module.scss +7 -7
  20. package/dist/styles/CommandPalette.module.scss +1 -1
  21. package/dist/styles/CrmView.module.scss +9 -9
  22. package/dist/styles/EmailTracking.module.scss +6 -6
  23. package/dist/styles/ImportConversation.module.scss +5 -5
  24. package/dist/styles/Logs.module.scss +5 -5
  25. package/dist/styles/NewTicket.module.scss +2 -2
  26. package/dist/styles/PendingEmails.module.scss +13 -13
  27. package/dist/styles/SupportDashboard.module.scss +24 -8
  28. package/dist/styles/TicketDetail.module.scss +20 -20
  29. package/dist/styles/TicketInbox.module.scss +6 -6
  30. package/dist/styles/TicketingSettings.module.scss +10 -10
  31. package/dist/styles/TimeDashboard.module.scss +6 -6
  32. package/dist/styles/_tokens.scss +3 -1
  33. package/dist/utils/db.js +20 -0
  34. package/dist/utils/readSettings.js +150 -0
  35. package/dist/views/BillingView/client.js +15 -8
  36. package/dist/views/ChatView/client.js +2 -0
  37. package/dist/views/CrmView/client.js +2 -0
  38. package/dist/views/EmailTrackingView/client.js +23 -11
  39. package/dist/views/ImportConversationView/client.js +1 -0
  40. package/dist/views/LogsView/client.js +1 -1
  41. package/dist/views/NewTicketView/client.js +20 -13
  42. package/dist/views/PendingEmailsView/client.js +9 -3
  43. package/dist/views/SupportDashboardView/client.js +17 -19
  44. package/dist/views/TicketDetailView/client.js +23 -11
  45. package/dist/views/TicketInboxView/client.js +52 -26
  46. package/dist/views/TicketingSettingsView/TicketingSettings.module.scss +7 -7
  47. package/dist/views/TicketingSettingsView/client.js +44 -26
  48. package/dist/views/TimeDashboardView/client.js +10 -7
  49. package/dist/views/shared/ErrorBoundary.d.ts +36 -1
  50. package/dist/views/shared/ErrorBoundary.js +59 -28
  51. package/dist/views/shared/adminTokens.d.ts +15 -5
  52. package/dist/views/shared/adminTokens.js +23 -7
  53. package/dist/views/shared/icons.d.ts +49 -0
  54. package/dist/views/shared/icons.js +92 -0
  55. package/dist/views/shared/locales/en.json +15 -1
  56. package/dist/views/shared/locales/fr.json +15 -1
  57. package/package.json +9 -4
  58. package/scripts/copy-subpath-types.mjs +103 -0
  59. package/scripts/uninstall-data.mjs +178 -0
  60. package/scripts/uninstall.mjs +184 -0
  61. package/scripts/verify-dist-imports.mjs +84 -0
  62. package/src/collections/Tickets.ts +7 -0
  63. package/src/collections/TimeEntries.ts +11 -3
  64. package/src/components/RichTextEditor/index.tsx +1 -1
  65. package/src/components/TicketConversation/RewriteDropdown.tsx +1 -1
  66. package/src/components/TicketConversation/components/AISummaryPanel.tsx +3 -0
  67. package/src/components/TicketConversation/components/ActionPanels.tsx +13 -8
  68. package/src/components/TicketConversation/components/ClientHistory.tsx +1 -0
  69. package/src/components/TicketConversation/components/CodeBlock.tsx +1 -0
  70. package/src/components/TicketConversation/components/CodeBlockInserter.tsx +1 -1
  71. package/src/components/TicketConversation/components/QuickActions.tsx +10 -8
  72. package/src/components/TicketConversation/components/TicketHeader.tsx +2 -0
  73. package/src/components/TicketConversation/components/TimeTrackingPanel.tsx +17 -9
  74. package/src/components/TicketConversation/index.tsx +41 -7
  75. package/src/endpoints/delete-account.ts +191 -47
  76. package/src/endpoints/export-data.ts +104 -4
  77. package/src/endpoints/purge-logs.ts +6 -15
  78. package/src/index.ts +2 -0
  79. package/src/plugin.ts +51 -1
  80. package/src/portal/LiveChat.tsx +1 -1
  81. package/src/portal/auth/profile/page.tsx +3 -2
  82. package/src/portal/icons.tsx +91 -0
  83. package/src/styles/BillingView.module.scss +11 -11
  84. package/src/styles/ChatView.module.scss +7 -7
  85. package/src/styles/CommandPalette.module.scss +1 -1
  86. package/src/styles/CrmView.module.scss +9 -9
  87. package/src/styles/EmailTracking.module.scss +6 -6
  88. package/src/styles/ImportConversation.module.scss +5 -5
  89. package/src/styles/Logs.module.scss +5 -5
  90. package/src/styles/NewTicket.module.scss +2 -2
  91. package/src/styles/PendingEmails.module.scss +13 -13
  92. package/src/styles/SupportDashboard.module.scss +24 -8
  93. package/src/styles/TicketDetail.module.scss +20 -20
  94. package/src/styles/TicketInbox.module.scss +6 -6
  95. package/src/styles/TicketingSettings.module.scss +10 -10
  96. package/src/styles/TimeDashboard.module.scss +6 -6
  97. package/src/styles/_tokens.scss +3 -1
  98. package/src/types.ts +20 -0
  99. package/src/utils/retention.ts +111 -0
  100. package/src/utils/slugs.ts +15 -0
  101. package/src/views/BillingView/client.tsx +17 -8
  102. package/src/views/ChatView/client.tsx +2 -0
  103. package/src/views/CrmView/client.tsx +2 -0
  104. package/src/views/EmailTrackingView/client.tsx +18 -7
  105. package/src/views/ImportConversationView/client.tsx +1 -0
  106. package/src/views/LogsView/client.tsx +1 -1
  107. package/src/views/NewTicketView/client.tsx +22 -13
  108. package/src/views/PendingEmailsView/client.tsx +9 -3
  109. package/src/views/SupportDashboardView/client.tsx +11 -5
  110. package/src/views/TicketDetailView/client.tsx +25 -11
  111. package/src/views/TicketInboxView/client.tsx +47 -7
  112. package/src/views/TicketingSettingsView/TicketingSettings.module.scss +7 -7
  113. package/src/views/TicketingSettingsView/client.tsx +201 -144
  114. package/src/views/TimeDashboardView/client.tsx +10 -7
  115. package/src/views/shared/ErrorBoundary.tsx +95 -31
  116. package/src/views/shared/adminTokens.ts +28 -11
  117. package/src/views/shared/icons.tsx +146 -0
  118. package/src/views/shared/locales/en.json +15 -1
  119. package/src/views/shared/locales/fr.json +15 -1
  120. package/src/types/lucide-react.d.ts +0 -42
@@ -1,7 +1,7 @@
1
1
  'use client'
2
2
 
3
- import React, { useState, useEffect } from 'react'
4
- import { Settings, Mail, Bot, Clock, Timer, Globe, FileSignature } from 'lucide-react'
3
+ import React, { useState, useEffect, useId } from 'react'
4
+ import { Settings, Mail, Bot, Clock, Timer, Globe, FileSignature } from '../shared/icons'
5
5
  import { V, btnStyle } from '../shared/adminTokens'
6
6
  import { AdminViewHeader } from '../shared/AdminViewHeader'
7
7
  import { DEFAULT_FEATURES, type TicketingFeatures } from '../../components/TicketConversation/config'
@@ -280,19 +280,33 @@ const CollapsibleSection: React.FC<{
280
280
  )
281
281
  }
282
282
 
283
+ /**
284
+ * Row of the settings form.
285
+ *
286
+ * It owns the id of the control it labels: `children` is called with a
287
+ * generated id that the control must carry, which turns the row title into a
288
+ * real `<label>` instead of a floating `<div>`. The id is generated with
289
+ * `useId` because several settings views can be mounted at once.
290
+ */
283
291
  const FieldRow: React.FC<{
284
292
  label: string
285
293
  description?: string
286
- children: React.ReactNode
287
- }> = ({ label, description, children }) => (
288
- <div className={ts.fieldRow}>
289
- <div className={ts.fieldLabel}>
290
- {label}
291
- {description && <div className={ts.fieldDescription}>{description}</div>}
294
+ children: React.ReactNode | ((id: string) => React.ReactNode)
295
+ }> = ({ label, description, children }) => {
296
+ const id = useId()
297
+
298
+ return (
299
+ <div className={ts.fieldRow}>
300
+ <div className={ts.fieldLabel}>
301
+ <label htmlFor={id}>{label}</label>
302
+ {description && <div className={ts.fieldDescription}>{description}</div>}
303
+ </div>
304
+ <div className={ts.fieldContent}>
305
+ {typeof children === 'function' ? children(id) : children}
306
+ </div>
292
307
  </div>
293
- <div className={ts.fieldContent}>{children}</div>
294
- </div>
295
- )
308
+ )
309
+ }
296
310
 
297
311
  /* ============================================
298
312
  * Main Component
@@ -393,7 +407,7 @@ export const TicketingSettingsClient: React.FC = () => {
393
407
  subtitle={t('settings.subtitle', { enabled: String(enabledCount), total: String(totalCount) })}
394
408
  actions={
395
409
  <div style={{ display: 'flex', gap: 8 }}>
396
- <button onClick={handleReset} style={btnStyle('var(--theme-elevation-400)', { small: true })}>
410
+ <button type="button" onClick={handleReset} style={btnStyle(V.neutralBg, { small: true, fg: V.neutralFg })}>
397
411
  {t('settingsView.reset')}
398
412
  </button>
399
413
  <button
@@ -477,54 +491,69 @@ export const TicketingSettingsClient: React.FC = () => {
477
491
  </p>
478
492
 
479
493
  <FieldRow label={t('settingsView.senderAddress')} description={t('settingsView.senderAddressDesc')}>
480
- <input
481
- type="email"
482
- value={settings.email.fromAddress}
483
- onChange={(e) => updateEmail('fromAddress', e.target.value)}
484
- placeholder="support@example.com"
485
- className={ts.input}
486
- />
494
+ {(id) => (
495
+ <input
496
+ id={id}
497
+ type="email"
498
+ value={settings.email.fromAddress}
499
+ onChange={(e) => updateEmail('fromAddress', e.target.value)}
500
+ placeholder="support@example.com"
501
+ className={ts.input}
502
+ />
503
+ )}
487
504
  </FieldRow>
488
505
 
489
506
  <FieldRow label={t('settingsView.senderName')} description={t('settingsView.senderNameDesc')}>
490
- <input
491
- type="text"
492
- value={settings.email.fromName}
493
- onChange={(e) => updateEmail('fromName', e.target.value)}
494
- placeholder="Support ConsilioWEB"
495
- className={ts.input}
496
- />
507
+ {(id) => (
508
+ <input
509
+ id={id}
510
+ type="text"
511
+ value={settings.email.fromName}
512
+ onChange={(e) => updateEmail('fromName', e.target.value)}
513
+ placeholder="Support ConsilioWEB"
514
+ className={ts.input}
515
+ />
516
+ )}
497
517
  </FieldRow>
498
518
 
499
519
  <FieldRow label={t('settingsView.replyTo')} description={t('settingsView.replyToDesc')}>
500
- <input
501
- type="email"
502
- value={settings.email.replyToAddress}
503
- onChange={(e) => updateEmail('replyToAddress', e.target.value)}
504
- placeholder="(identique a l'expediteur)"
505
- className={ts.input}
506
- />
520
+ {(id) => (
521
+ <input
522
+ id={id}
523
+ type="email"
524
+ value={settings.email.replyToAddress}
525
+ onChange={(e) => updateEmail('replyToAddress', e.target.value)}
526
+ placeholder="(identique a l'expediteur)"
527
+ className={ts.input}
528
+ />
529
+ )}
507
530
  </FieldRow>
508
531
 
509
532
  <div className={ts.separator} />
510
533
 
511
534
  <FieldRow label={t('settingsView.smtpServer')} description={t('settingsView.smtpServerDesc')}>
512
- <input
513
- type="text"
514
- value={smtpHost}
515
- readOnly
516
- className={ts.inputReadonly}
517
- />
535
+ {(id) => (
536
+ <input
537
+ id={id}
538
+ type="text"
539
+ value={smtpHost}
540
+ readOnly
541
+ className={ts.inputReadonly}
542
+ />
543
+ )}
518
544
  </FieldRow>
519
545
 
520
546
  <FieldRow label={t('settingsView.smtpPort')}>
521
- <input
522
- type="text"
523
- value={smtpPort}
524
- readOnly
525
- className={ts.inputReadonly}
526
- style={{ maxWidth: 100 }}
527
- />
547
+ {(id) => (
548
+ <input
549
+ id={id}
550
+ type="text"
551
+ value={smtpPort}
552
+ readOnly
553
+ className={ts.inputReadonly}
554
+ style={{ maxWidth: 100 }}
555
+ />
556
+ )}
528
557
  </FieldRow>
529
558
  </CollapsibleSection>
530
559
 
@@ -547,47 +576,56 @@ export const TicketingSettingsClient: React.FC = () => {
547
576
  </p>
548
577
 
549
578
  <FieldRow label={t('settingsView.aiProvider')} description={t('settingsView.aiProviderDesc')}>
550
- <select
551
- value={settings.ai.provider}
552
- onChange={(e) => updateAI('provider', e.target.value as AISettings['provider'])}
553
- className={ts.select}
554
- >
555
- <option value="ollama">Ollama (local / tunnel)</option>
556
- <option value="anthropic">Anthropic (Claude)</option>
557
- <option value="openai">OpenAI (GPT)</option>
558
- <option value="gemini">Google (Gemini)</option>
559
- </select>
579
+ {(id) => (
580
+ <select
581
+ id={id}
582
+ value={settings.ai.provider}
583
+ onChange={(e) => updateAI('provider', e.target.value as AISettings['provider'])}
584
+ className={ts.select}
585
+ >
586
+ <option value="ollama">Ollama (local / tunnel)</option>
587
+ <option value="anthropic">Anthropic (Claude)</option>
588
+ <option value="openai">OpenAI (GPT)</option>
589
+ <option value="gemini">Google (Gemini)</option>
590
+ </select>
591
+ )}
560
592
  </FieldRow>
561
593
 
562
594
  {settings.ai.provider !== 'ollama' && (
563
595
  <FieldRow label={t('settingsView.aiApiKey')} description={t('settingsView.aiApiKeyDesc')}>
564
- <div className={ts.apiKeyRow}>
565
- <input
566
- type={showApiKey ? 'text' : 'password'}
567
- value={settings.ai.apiKey}
568
- onChange={(e) => updateAI('apiKey', e.target.value)}
569
- placeholder="sk-..."
570
- className={ts.input}
571
- style={{ flex: 1 }}
572
- />
573
- <button
574
- onClick={() => setShowApiKey(!showApiKey)}
575
- className={ts.apiKeyToggle}
576
- >
577
- {showApiKey ? t('settingsView.hideKey') : t('settingsView.showKey')}
578
- </button>
579
- </div>
596
+ {(id) => (
597
+ <div className={ts.apiKeyRow}>
598
+ <input
599
+ id={id}
600
+ type={showApiKey ? 'text' : 'password'}
601
+ value={settings.ai.apiKey}
602
+ onChange={(e) => updateAI('apiKey', e.target.value)}
603
+ placeholder="sk-..."
604
+ className={ts.input}
605
+ style={{ flex: 1 }}
606
+ />
607
+ <button
608
+ onClick={() => setShowApiKey(!showApiKey)}
609
+ className={ts.apiKeyToggle}
610
+ >
611
+ {showApiKey ? t('settingsView.hideKey') : t('settingsView.showKey')}
612
+ </button>
613
+ </div>
614
+ )}
580
615
  </FieldRow>
581
616
  )}
582
617
 
583
618
  <FieldRow label={t('settingsView.aiModel')} description={t('settingsView.aiModelDesc')}>
584
- <input
585
- type="text"
586
- value={settings.ai.model}
587
- onChange={(e) => updateAI('model', e.target.value)}
588
- placeholder="qwen2.5:32b"
589
- className={ts.input}
590
- />
619
+ {(id) => (
620
+ <input
621
+ id={id}
622
+ type="text"
623
+ value={settings.ai.model}
624
+ onChange={(e) => updateAI('model', e.target.value)}
625
+ placeholder="qwen2.5:32b"
626
+ className={ts.input}
627
+ />
628
+ )}
591
629
  </FieldRow>
592
630
 
593
631
  <div className={ts.separator} />
@@ -631,33 +669,39 @@ export const TicketingSettingsClient: React.FC = () => {
631
669
  </p>
632
670
 
633
671
  <FieldRow label={t('settingsView.slaFirstResponse')} description={t('settingsView.slaFirstResponseDesc')}>
634
- <div className={ts.slaInline}>
635
- <input
636
- type="number"
637
- min={1}
638
- value={settings.sla.firstResponseMinutes}
639
- onChange={(e) => updateSLA('firstResponseMinutes', parseInt(e.target.value) || 0)}
640
- className={ts.numberInput}
641
- />
642
- <span className={ts.slaHint}>
643
- {t('settingsView.minutes')} ({Math.floor(settings.sla.firstResponseMinutes / 60)}h{String(settings.sla.firstResponseMinutes % 60).padStart(2, '0')})
644
- </span>
645
- </div>
672
+ {(id) => (
673
+ <div className={ts.slaInline}>
674
+ <input
675
+ id={id}
676
+ type="number"
677
+ min={1}
678
+ value={settings.sla.firstResponseMinutes}
679
+ onChange={(e) => updateSLA('firstResponseMinutes', parseInt(e.target.value) || 0)}
680
+ className={ts.numberInput}
681
+ />
682
+ <span className={ts.slaHint}>
683
+ {t('settingsView.minutes')} ({Math.floor(settings.sla.firstResponseMinutes / 60)}h{String(settings.sla.firstResponseMinutes % 60).padStart(2, '0')})
684
+ </span>
685
+ </div>
686
+ )}
646
687
  </FieldRow>
647
688
 
648
689
  <FieldRow label={t('settingsView.slaResolution')} description={t('settingsView.slaResolutionDesc')}>
649
- <div className={ts.slaInline}>
650
- <input
651
- type="number"
652
- min={1}
653
- value={settings.sla.resolutionMinutes}
654
- onChange={(e) => updateSLA('resolutionMinutes', parseInt(e.target.value) || 0)}
655
- className={ts.numberInput}
656
- />
657
- <span className={ts.slaHint}>
658
- {t('settingsView.minutes')} ({Math.floor(settings.sla.resolutionMinutes / 60)}h{String(settings.sla.resolutionMinutes % 60).padStart(2, '0')})
659
- </span>
660
- </div>
690
+ {(id) => (
691
+ <div className={ts.slaInline}>
692
+ <input
693
+ id={id}
694
+ type="number"
695
+ min={1}
696
+ value={settings.sla.resolutionMinutes}
697
+ onChange={(e) => updateSLA('resolutionMinutes', parseInt(e.target.value) || 0)}
698
+ className={ts.numberInput}
699
+ />
700
+ <span className={ts.slaHint}>
701
+ {t('settingsView.minutes')} ({Math.floor(settings.sla.resolutionMinutes / 60)}h{String(settings.sla.resolutionMinutes % 60).padStart(2, '0')})
702
+ </span>
703
+ </div>
704
+ )}
661
705
  </FieldRow>
662
706
 
663
707
  <div className={ts.toggleRow}>
@@ -678,13 +722,16 @@ export const TicketingSettingsClient: React.FC = () => {
678
722
  <div className={ts.separator} />
679
723
 
680
724
  <FieldRow label={t('settingsView.escalationEmail')} description={t('settingsView.escalationEmailDesc')}>
681
- <input
682
- type="email"
683
- value={settings.sla.escalationEmail}
684
- onChange={(e) => updateSLA('escalationEmail', e.target.value)}
685
- placeholder="admin@example.com"
686
- className={ts.input}
687
- />
725
+ {(id) => (
726
+ <input
727
+ id={id}
728
+ type="email"
729
+ value={settings.sla.escalationEmail}
730
+ onChange={(e) => updateSLA('escalationEmail', e.target.value)}
731
+ placeholder="admin@example.com"
732
+ className={ts.input}
733
+ />
734
+ )}
688
735
  </FieldRow>
689
736
  </CollapsibleSection>
690
737
 
@@ -720,33 +767,39 @@ export const TicketingSettingsClient: React.FC = () => {
720
767
  {settings.autoClose.enabled && (
721
768
  <>
722
769
  <FieldRow label={t('settingsView.autoCloseDelay')} description={t('settingsView.autoCloseDelayDesc')}>
723
- <div className={ts.slaInline}>
724
- <input
725
- type="number"
726
- min={1}
727
- max={90}
728
- value={settings.autoClose.daysBeforeClose}
729
- onChange={(e) => updateAutoClose('daysBeforeClose', parseInt(e.target.value) || 7)}
730
- className={ts.numberInput}
731
- />
732
- <span className={ts.slaHint}>{t('settingsView.days')}</span>
733
- </div>
770
+ {(id) => (
771
+ <div className={ts.slaInline}>
772
+ <input
773
+ id={id}
774
+ type="number"
775
+ min={1}
776
+ max={90}
777
+ value={settings.autoClose.daysBeforeClose}
778
+ onChange={(e) => updateAutoClose('daysBeforeClose', parseInt(e.target.value) || 7)}
779
+ className={ts.numberInput}
780
+ />
781
+ <span className={ts.slaHint}>{t('settingsView.days')}</span>
782
+ </div>
783
+ )}
734
784
  </FieldRow>
735
785
 
736
786
  <FieldRow label={t('settingsView.autoCloseReminder')} description={t('settingsView.autoCloseReminderDesc')}>
737
- <div className={ts.slaInline}>
738
- <input
739
- type="number"
740
- min={1}
741
- max={settings.autoClose.daysBeforeClose - 1}
742
- value={settings.autoClose.reminderDaysBefore}
743
- onChange={(e) => updateAutoClose('reminderDaysBefore', parseInt(e.target.value) || 2)}
744
- className={ts.numberInput}
745
- />
746
- <span className={ts.slaHint}>
747
- {t('settingsView.autoCloseDaysBeforeReminder', { count: String(settings.autoClose.reminderDaysBefore) })}
748
- </span>
749
- </div>
787
+ {(id) => (
788
+ <div className={ts.slaInline}>
789
+ <input
790
+ id={id}
791
+ type="number"
792
+ min={1}
793
+ max={settings.autoClose.daysBeforeClose - 1}
794
+ value={settings.autoClose.reminderDaysBefore}
795
+ onChange={(e) => updateAutoClose('reminderDaysBefore', parseInt(e.target.value) || 2)}
796
+ className={ts.numberInput}
797
+ />
798
+ <span className={ts.slaHint}>
799
+ {t('settingsView.autoCloseDaysBeforeReminder', { count: String(settings.autoClose.reminderDaysBefore) })}
800
+ </span>
801
+ </div>
802
+ )}
750
803
  </FieldRow>
751
804
  </>
752
805
  )}
@@ -783,14 +836,17 @@ export const TicketingSettingsClient: React.FC = () => {
783
836
  </p>
784
837
 
785
838
  <FieldRow label={t('settingsView.language')} description={t('settingsView.languageDesc')}>
786
- <select
787
- value={settings.locale.language}
788
- onChange={(e) => updateLocale('language', e.target.value as 'fr' | 'en')}
789
- className={ts.select}
790
- >
791
- <option value="fr">{t('settingsView.french')}</option>
792
- <option value="en">{t('settingsView.english')}</option>
793
- </select>
839
+ {(id) => (
840
+ <select
841
+ id={id}
842
+ value={settings.locale.language}
843
+ onChange={(e) => updateLocale('language', e.target.value as 'fr' | 'en')}
844
+ className={ts.select}
845
+ >
846
+ <option value="fr">{t('settingsView.french')}</option>
847
+ <option value="en">{t('settingsView.english')}</option>
848
+ </select>
849
+ )}
794
850
  </FieldRow>
795
851
  </CollapsibleSection>
796
852
 
@@ -808,6 +864,7 @@ export const TicketingSettingsClient: React.FC = () => {
808
864
  </p>
809
865
 
810
866
  <textarea
867
+ aria-label={t('settingsView.signatureTitle')}
811
868
  value={signature}
812
869
  onChange={(e) => { setSignature(e.target.value); setSaved(false) }}
813
870
  placeholder={t('settingsView.signaturePlaceholder')}
@@ -843,7 +900,7 @@ export const TicketingSettingsClient: React.FC = () => {
843
900
  if (res.ok) { const d = await res.json(); alert(`${d.purged} ${t('settingsView.purgeDone')}`) }
844
901
  } catch { alert(t('import.errorTitle')) }
845
902
  }}
846
- style={{ ...btnStyle(opt.days === 0 ? '#ef4444' : 'var(--theme-elevation-500)', { small: true }), fontSize: 11 }}
903
+ style={{ ...btnStyle(opt.days === 0 ? V.red : V.neutralBg, { small: true, fg: opt.days === 0 ? undefined : V.neutralFg }), fontSize: 11 }}
847
904
  >
848
905
  {opt.label}
849
906
  </button>
@@ -856,7 +913,7 @@ export const TicketingSettingsClient: React.FC = () => {
856
913
 
857
914
  {/* Bottom save bar */}
858
915
  <div className={ts.bottomBar}>
859
- <button onClick={handleReset} style={btnStyle('var(--theme-elevation-400)', { small: true })}>
916
+ <button type="button" onClick={handleReset} style={btnStyle(V.neutralBg, { small: true, fg: V.neutralFg })}>
860
917
  {t('settingsView.resetAll')}
861
918
  </button>
862
919
  <button
@@ -1,7 +1,7 @@
1
1
  /* eslint-disable @next/next/no-html-link-for-pages */
2
2
  'use client'
3
3
 
4
- import React, { useState, useEffect, useCallback } from 'react'
4
+ import React, { useState, useEffect, useCallback, useId } from 'react'
5
5
  import { SkeletonDashboard } from '../shared/Skeleton'
6
6
  import { useTranslation } from '../../components/TicketConversation/hooks/useTranslation'
7
7
  import styles from '../../styles/TimeDashboard.module.scss'
@@ -44,6 +44,9 @@ const MONTHS_FR = ['Jan', 'Fev', 'Mar', 'Avr', 'Mai', 'Jun', 'Jul', 'Aou', 'Sep'
44
44
 
45
45
  export const TimeDashboardClient: React.FC = () => {
46
46
  const { t } = useTranslation()
47
+ const fromId = useId()
48
+ const toId = useId()
49
+ const groupById = useId()
47
50
  const [entries, setEntries] = useState<TimeEntry[]>([])
48
51
  const [loading, setLoading] = useState(true)
49
52
  const [from, setFrom] = useState(() => getMonthRange(0).from)
@@ -149,16 +152,16 @@ export const TimeDashboardClient: React.FC = () => {
149
152
  </div>
150
153
  <div className={styles.filterRow}>
151
154
  <div className={styles.fieldGroup}>
152
- <label className={styles.label}>{t('timeDashboard.filters.from')}</label>
153
- <input type="date" value={from} onChange={(e) => setFrom(e.target.value)} className={styles.input} />
155
+ <label className={styles.label} htmlFor={fromId}>{t('timeDashboard.filters.from')}</label>
156
+ <input id={fromId} type="date" value={from} onChange={(e) => setFrom(e.target.value)} className={styles.input} />
154
157
  </div>
155
158
  <div className={styles.fieldGroup}>
156
- <label className={styles.label}>{t('timeDashboard.filters.to')}</label>
157
- <input type="date" value={to} onChange={(e) => setTo(e.target.value)} className={styles.input} />
159
+ <label className={styles.label} htmlFor={toId}>{t('timeDashboard.filters.to')}</label>
160
+ <input id={toId} type="date" value={to} onChange={(e) => setTo(e.target.value)} className={styles.input} />
158
161
  </div>
159
162
  <div className={styles.fieldGroup}>
160
- <label className={styles.label}>{t('timeDashboard.filters.groupBy')}</label>
161
- <select value={groupBy} onChange={(e) => setGroupBy(e.target.value as 'day' | 'week' | 'project')} className={styles.select}>
163
+ <label className={styles.label} htmlFor={groupById}>{t('timeDashboard.filters.groupBy')}</label>
164
+ <select id={groupById} value={groupBy} onChange={(e) => setGroupBy(e.target.value as 'day' | 'week' | 'project')} className={styles.select}>
162
165
  <option value="day">{t('timeDashboard.filters.day')}</option>
163
166
  <option value="week">{t('timeDashboard.filters.week')}</option>
164
167
  <option value="project">{t('timeDashboard.filters.project')}</option>