@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.
- package/README.md +249 -22
- package/dist/components/RichTextEditor/index.js +1 -1
- package/dist/components/TicketConversation/RewriteDropdown.js +1 -1
- package/dist/components/TicketConversation/components/AISummaryPanel.js +3 -0
- package/dist/components/TicketConversation/components/ActionPanels.js +13 -8
- package/dist/components/TicketConversation/components/ClientHistory.js +1 -0
- package/dist/components/TicketConversation/components/CodeBlock.js +1 -0
- package/dist/components/TicketConversation/components/CodeBlockInserter.js +1 -1
- package/dist/components/TicketConversation/components/QuickActions.js +10 -8
- package/dist/components/TicketConversation/components/TicketHeader.js +2 -0
- package/dist/components/TicketConversation/components/TimeTrackingPanel.js +15 -9
- package/dist/components/TicketConversation/index.d.ts +10 -0
- package/dist/components/TicketConversation/index.js +25 -7
- package/dist/index.cjs +306 -60
- package/dist/index.d.cts +107 -38
- package/dist/index.d.ts +107 -38
- package/dist/index.js +300 -59
- package/dist/styles/BillingView.module.scss +11 -11
- package/dist/styles/ChatView.module.scss +7 -7
- package/dist/styles/CommandPalette.module.scss +1 -1
- package/dist/styles/CrmView.module.scss +9 -9
- package/dist/styles/EmailTracking.module.scss +6 -6
- package/dist/styles/ImportConversation.module.scss +5 -5
- package/dist/styles/Logs.module.scss +5 -5
- package/dist/styles/NewTicket.module.scss +2 -2
- package/dist/styles/PendingEmails.module.scss +13 -13
- package/dist/styles/SupportDashboard.module.scss +24 -8
- package/dist/styles/TicketDetail.module.scss +20 -20
- package/dist/styles/TicketInbox.module.scss +6 -6
- package/dist/styles/TicketingSettings.module.scss +10 -10
- package/dist/styles/TimeDashboard.module.scss +6 -6
- package/dist/styles/_tokens.scss +3 -1
- package/dist/utils/db.js +20 -0
- package/dist/utils/readSettings.js +150 -0
- package/dist/views/BillingView/client.js +15 -8
- package/dist/views/ChatView/client.js +2 -0
- package/dist/views/CrmView/client.js +2 -0
- package/dist/views/EmailTrackingView/client.js +23 -11
- package/dist/views/ImportConversationView/client.js +1 -0
- package/dist/views/LogsView/client.js +1 -1
- package/dist/views/NewTicketView/client.js +20 -13
- package/dist/views/PendingEmailsView/client.js +9 -3
- package/dist/views/SupportDashboardView/client.js +17 -19
- package/dist/views/TicketDetailView/client.js +23 -11
- package/dist/views/TicketInboxView/client.js +52 -26
- package/dist/views/TicketingSettingsView/TicketingSettings.module.scss +7 -7
- package/dist/views/TicketingSettingsView/client.js +44 -26
- package/dist/views/TimeDashboardView/client.js +10 -7
- package/dist/views/shared/ErrorBoundary.d.ts +36 -1
- package/dist/views/shared/ErrorBoundary.js +59 -28
- package/dist/views/shared/adminTokens.d.ts +15 -5
- package/dist/views/shared/adminTokens.js +23 -7
- package/dist/views/shared/icons.d.ts +49 -0
- package/dist/views/shared/icons.js +92 -0
- package/dist/views/shared/locales/en.json +15 -1
- package/dist/views/shared/locales/fr.json +15 -1
- package/package.json +9 -4
- package/scripts/copy-subpath-types.mjs +103 -0
- package/scripts/uninstall-data.mjs +178 -0
- package/scripts/uninstall.mjs +184 -0
- package/scripts/verify-dist-imports.mjs +84 -0
- package/src/collections/Tickets.ts +7 -0
- package/src/collections/TimeEntries.ts +11 -3
- package/src/components/RichTextEditor/index.tsx +1 -1
- package/src/components/TicketConversation/RewriteDropdown.tsx +1 -1
- package/src/components/TicketConversation/components/AISummaryPanel.tsx +3 -0
- package/src/components/TicketConversation/components/ActionPanels.tsx +13 -8
- package/src/components/TicketConversation/components/ClientHistory.tsx +1 -0
- package/src/components/TicketConversation/components/CodeBlock.tsx +1 -0
- package/src/components/TicketConversation/components/CodeBlockInserter.tsx +1 -1
- package/src/components/TicketConversation/components/QuickActions.tsx +10 -8
- package/src/components/TicketConversation/components/TicketHeader.tsx +2 -0
- package/src/components/TicketConversation/components/TimeTrackingPanel.tsx +17 -9
- package/src/components/TicketConversation/index.tsx +41 -7
- package/src/endpoints/delete-account.ts +191 -47
- package/src/endpoints/export-data.ts +104 -4
- package/src/endpoints/purge-logs.ts +6 -15
- package/src/index.ts +2 -0
- package/src/plugin.ts +51 -1
- package/src/portal/LiveChat.tsx +1 -1
- package/src/portal/auth/profile/page.tsx +3 -2
- package/src/portal/icons.tsx +91 -0
- package/src/styles/BillingView.module.scss +11 -11
- package/src/styles/ChatView.module.scss +7 -7
- package/src/styles/CommandPalette.module.scss +1 -1
- package/src/styles/CrmView.module.scss +9 -9
- package/src/styles/EmailTracking.module.scss +6 -6
- package/src/styles/ImportConversation.module.scss +5 -5
- package/src/styles/Logs.module.scss +5 -5
- package/src/styles/NewTicket.module.scss +2 -2
- package/src/styles/PendingEmails.module.scss +13 -13
- package/src/styles/SupportDashboard.module.scss +24 -8
- package/src/styles/TicketDetail.module.scss +20 -20
- package/src/styles/TicketInbox.module.scss +6 -6
- package/src/styles/TicketingSettings.module.scss +10 -10
- package/src/styles/TimeDashboard.module.scss +6 -6
- package/src/styles/_tokens.scss +3 -1
- package/src/types.ts +20 -0
- package/src/utils/retention.ts +111 -0
- package/src/utils/slugs.ts +15 -0
- package/src/views/BillingView/client.tsx +17 -8
- package/src/views/ChatView/client.tsx +2 -0
- package/src/views/CrmView/client.tsx +2 -0
- package/src/views/EmailTrackingView/client.tsx +18 -7
- package/src/views/ImportConversationView/client.tsx +1 -0
- package/src/views/LogsView/client.tsx +1 -1
- package/src/views/NewTicketView/client.tsx +22 -13
- package/src/views/PendingEmailsView/client.tsx +9 -3
- package/src/views/SupportDashboardView/client.tsx +11 -5
- package/src/views/TicketDetailView/client.tsx +25 -11
- package/src/views/TicketInboxView/client.tsx +47 -7
- package/src/views/TicketingSettingsView/TicketingSettings.module.scss +7 -7
- package/src/views/TicketingSettingsView/client.tsx +201 -144
- package/src/views/TimeDashboardView/client.tsx +10 -7
- package/src/views/shared/ErrorBoundary.tsx +95 -31
- package/src/views/shared/adminTokens.ts +28 -11
- package/src/views/shared/icons.tsx +146 -0
- package/src/views/shared/locales/en.json +15 -1
- package/src/views/shared/locales/fr.json +15 -1
- package/src/types/lucide-react.d.ts +0 -42
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `support-uninstall` — removes this plugin from a host application.
|
|
5
|
+
*
|
|
6
|
+
* Contract, deliberately identical to the other ConsilioWEB plugins:
|
|
7
|
+
* - DRY RUN by default. Nothing is written without `--confirm`.
|
|
8
|
+
* - a detection gate: the destructive step refuses to run unless this project
|
|
9
|
+
* really uses the plugin (a source file references it, or it is a declared
|
|
10
|
+
* dependency). `--force-db` overrides it.
|
|
11
|
+
* - `--keep-data` cleans the code and the preferences but keeps every row.
|
|
12
|
+
*
|
|
13
|
+
* Usage:
|
|
14
|
+
* npx support-uninstall list what would be removed
|
|
15
|
+
* npx support-uninstall --confirm do it
|
|
16
|
+
* npx support-uninstall --confirm --keep-data code only, rows kept
|
|
17
|
+
* npx support-uninstall --confirm --force-db delete rows even if undetected
|
|
18
|
+
* npx support-uninstall --extra-slugs=a,b add renamed collections
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import fs from 'node:fs'
|
|
22
|
+
import path from 'node:path'
|
|
23
|
+
import { execSync, spawnSync } from 'node:child_process'
|
|
24
|
+
import { fileURLToPath } from 'node:url'
|
|
25
|
+
|
|
26
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url))
|
|
27
|
+
|
|
28
|
+
function readOwnPackageName() {
|
|
29
|
+
try {
|
|
30
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(HERE, '..', 'package.json'), 'utf-8'))
|
|
31
|
+
if (typeof pkg.name === 'string' && pkg.name) return pkg.name
|
|
32
|
+
} catch {
|
|
33
|
+
// fall through
|
|
34
|
+
}
|
|
35
|
+
return '@consilioweb/payload-support'
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const PACKAGE_NAME = readOwnPackageName()
|
|
39
|
+
|
|
40
|
+
function detectPackageManager(dir) {
|
|
41
|
+
if (fs.existsSync(path.join(dir, 'pnpm-lock.yaml'))) return 'pnpm'
|
|
42
|
+
if (fs.existsSync(path.join(dir, 'yarn.lock'))) return 'yarn'
|
|
43
|
+
if (fs.existsSync(path.join(dir, 'bun.lockb')) || fs.existsSync(path.join(dir, 'bun.lock'))) return 'bun'
|
|
44
|
+
return 'npm'
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function findSourceFiles(dir, acc = []) {
|
|
48
|
+
if (!fs.existsSync(dir)) return acc
|
|
49
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
50
|
+
const full = path.join(dir, entry.name)
|
|
51
|
+
if (entry.isDirectory()) {
|
|
52
|
+
if (entry.name === 'node_modules' || entry.name === '.next' || entry.name.startsWith('.')) continue
|
|
53
|
+
findSourceFiles(full, acc)
|
|
54
|
+
} else if (/\.(?:ts|tsx|js|jsx|mjs|cjs)$/.test(entry.name)) {
|
|
55
|
+
acc.push(full)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return acc
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function isDeclaredDependency(projectDir) {
|
|
62
|
+
try {
|
|
63
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(projectDir, 'package.json'), 'utf-8'))
|
|
64
|
+
return Boolean(
|
|
65
|
+
pkg.dependencies?.[PACKAGE_NAME]
|
|
66
|
+
|| pkg.devDependencies?.[PACKAGE_NAME]
|
|
67
|
+
|| pkg.peerDependencies?.[PACKAGE_NAME]
|
|
68
|
+
|| pkg.optionalDependencies?.[PACKAGE_NAME],
|
|
69
|
+
)
|
|
70
|
+
} catch {
|
|
71
|
+
return false
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function run(cmd, cwd) {
|
|
76
|
+
console.log(` $ ${cmd}`)
|
|
77
|
+
try {
|
|
78
|
+
execSync(cmd, { cwd, stdio: 'inherit' })
|
|
79
|
+
return true
|
|
80
|
+
} catch {
|
|
81
|
+
return false
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function main() {
|
|
86
|
+
const projectDir = process.env.INIT_CWD || process.cwd()
|
|
87
|
+
const argv = process.argv.slice(2)
|
|
88
|
+
const confirm = argv.includes('--confirm')
|
|
89
|
+
const keepData = argv.includes('--keep-data')
|
|
90
|
+
const forceDb = argv.includes('--force-db')
|
|
91
|
+
const extraSlugs = argv.find((a) => a.startsWith('--extra-slugs='))
|
|
92
|
+
const pm = detectPackageManager(projectDir)
|
|
93
|
+
|
|
94
|
+
console.log('')
|
|
95
|
+
console.log(` ${PACKAGE_NAME} — uninstall`)
|
|
96
|
+
console.log(' ─────────────────────────────────────────────')
|
|
97
|
+
console.log(` Project: ${projectDir}`)
|
|
98
|
+
console.log(` Package manager: ${pm}`)
|
|
99
|
+
console.log(` Mode: ${confirm ? 'APPLY' : 'DRY RUN — nothing will be written'}`)
|
|
100
|
+
if (keepData) console.log(' --keep-data: rows will be kept')
|
|
101
|
+
console.log('')
|
|
102
|
+
|
|
103
|
+
// ── 1. Where is the plugin referenced? ───────────────────
|
|
104
|
+
const sourceFiles = findSourceFiles(path.join(projectDir, 'src'))
|
|
105
|
+
const referencing = sourceFiles.filter((file) => {
|
|
106
|
+
try {
|
|
107
|
+
return fs.readFileSync(file, 'utf-8').includes(PACKAGE_NAME)
|
|
108
|
+
} catch {
|
|
109
|
+
return false
|
|
110
|
+
}
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
console.log(' [1/3] Source references')
|
|
114
|
+
if (referencing.length === 0) {
|
|
115
|
+
console.log(' none found under src/')
|
|
116
|
+
} else {
|
|
117
|
+
for (const file of referencing) console.log(` ${path.relative(projectDir, file)}`)
|
|
118
|
+
console.log('')
|
|
119
|
+
console.log(' Remove the `supportPlugin(...)` call and its import yourself.')
|
|
120
|
+
console.log(' This script never rewrites your source: the plugin call carries')
|
|
121
|
+
console.log(' your configuration, and a regex that guesses where it ends is how')
|
|
122
|
+
console.log(' a config file gets silently corrupted.')
|
|
123
|
+
}
|
|
124
|
+
console.log('')
|
|
125
|
+
|
|
126
|
+
const declared = isDeclaredDependency(projectDir)
|
|
127
|
+
const detected = referencing.length > 0 || declared || forceDb
|
|
128
|
+
|
|
129
|
+
// ── 2. Data ──────────────────────────────────────────────
|
|
130
|
+
console.log(' [2/3] Data')
|
|
131
|
+
if (!detected) {
|
|
132
|
+
console.log(` ${PACKAGE_NAME} was not found in this project`)
|
|
133
|
+
console.log(' (no source reference, not in package.json dependencies).')
|
|
134
|
+
console.log(' Skipping the data step. Force it with --force-db.')
|
|
135
|
+
console.log('')
|
|
136
|
+
} else {
|
|
137
|
+
// The Payload local API needs the host's TypeScript config loaded, which is
|
|
138
|
+
// exactly what `payload run` sets up. Spawning it beats reimplementing a
|
|
139
|
+
// TypeScript loader here.
|
|
140
|
+
const dataScript = path.join(HERE, 'uninstall-data.mjs')
|
|
141
|
+
const args = ['payload', 'run', dataScript]
|
|
142
|
+
if (confirm) args.push('--confirm')
|
|
143
|
+
if (keepData) args.push('--keep-data')
|
|
144
|
+
if (extraSlugs) args.push(extraSlugs)
|
|
145
|
+
|
|
146
|
+
const result = spawnSync('npx', args, { cwd: projectDir, stdio: 'inherit' })
|
|
147
|
+
if (result.status !== 0) {
|
|
148
|
+
console.log('')
|
|
149
|
+
console.log(' Could not run the data step through `npx payload run`.')
|
|
150
|
+
console.log(' Run it by hand from your project root:')
|
|
151
|
+
console.log(` npx payload run ${dataScript} ${confirm ? '--confirm' : ''}`)
|
|
152
|
+
console.log('')
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ── 3. The package ───────────────────────────────────────
|
|
157
|
+
console.log(' [3/3] Package')
|
|
158
|
+
if (!confirm) {
|
|
159
|
+
const removeCmd = pm === 'npm' ? 'npm uninstall' : `${pm} remove`
|
|
160
|
+
console.log(` would run: ${removeCmd} ${PACKAGE_NAME}`)
|
|
161
|
+
console.log(` would run: ${pm === 'npm' ? 'npx' : pm} payload generate:importmap`)
|
|
162
|
+
console.log('')
|
|
163
|
+
console.log(' Dry run finished. Nothing was changed. Re-run with --confirm.')
|
|
164
|
+
console.log('')
|
|
165
|
+
return
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (referencing.length > 0) {
|
|
169
|
+
console.log(' Skipped: the plugin is still imported by the files listed above.')
|
|
170
|
+
console.log(' Remove those references first, then re-run.')
|
|
171
|
+
console.log('')
|
|
172
|
+
return
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const removeCmd = pm === 'npm' ? 'npm uninstall' : `${pm} remove`
|
|
176
|
+
run(`${removeCmd} ${PACKAGE_NAME}`, projectDir)
|
|
177
|
+
run(`${pm === 'npm' ? 'npx' : pm} payload generate:importmap`, projectDir)
|
|
178
|
+
|
|
179
|
+
console.log('')
|
|
180
|
+
console.log(' Done.')
|
|
181
|
+
console.log('')
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
main()
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Every relative import in `dist/` must resolve to a file that is actually there.
|
|
4
|
+
*
|
|
5
|
+
* This catches one specific, silent failure mode of a multi-pass tsup build.
|
|
6
|
+
* The `bundle: false` pass preserves the source tree and rewrites nothing, so a
|
|
7
|
+
* relative import survives into the emitted file. If the target module is not
|
|
8
|
+
* itself in that pass's `entry` list, it is never emitted as a standalone file —
|
|
9
|
+
* it only exists inlined inside the bundled `dist/index.js`. The import then
|
|
10
|
+
* points at nothing.
|
|
11
|
+
*
|
|
12
|
+
* Nothing in the normal toolchain sees it. `tsc` type-checks the SOURCE tree,
|
|
13
|
+
* where the module exists. Vitest imports from `src/`, same thing. The build
|
|
14
|
+
* itself succeeds — tsup has no reason to object. The failure surfaces only when
|
|
15
|
+
* the HOST application's bundler resolves the published package, which is to say
|
|
16
|
+
* after publication, in someone else's build.
|
|
17
|
+
*
|
|
18
|
+
* `import type` is invisible here on purpose: TypeScript erases it, so it leaves
|
|
19
|
+
* no trace in the emitted file and cannot break anything. Only value imports do.
|
|
20
|
+
* That is why the defect hid for two releases in this package — every other
|
|
21
|
+
* importer of the same module used `import type`.
|
|
22
|
+
*/
|
|
23
|
+
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs'
|
|
24
|
+
import { dirname, join, resolve } from 'node:path'
|
|
25
|
+
|
|
26
|
+
const DIST = resolve(process.cwd(), 'dist')
|
|
27
|
+
|
|
28
|
+
if (!existsSync(DIST)) {
|
|
29
|
+
console.error('verify-dist-imports: dist/ is missing — run the build first.')
|
|
30
|
+
process.exit(1)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Every emitted JavaScript file, at any depth. */
|
|
34
|
+
function walk(dir) {
|
|
35
|
+
return readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
|
|
36
|
+
const path = join(dir, entry.name)
|
|
37
|
+
if (entry.isDirectory()) return walk(path)
|
|
38
|
+
return /\.(js|cjs|mjs)$/.test(entry.name) ? [path] : []
|
|
39
|
+
})
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Relative specifiers only. Bare specifiers are the consumer's dependency
|
|
44
|
+
* problem, not ours, and `node:` builtins always resolve.
|
|
45
|
+
*/
|
|
46
|
+
const SPECIFIER =
|
|
47
|
+
/(?:^|[\s;{(])(?:import|export)\s+(?:[^'"]*?\sfrom\s+)?['"](\.[^'"]+)['"]|require\(\s*['"](\.[^'"]+)['"]\s*\)|import\(\s*['"](\.[^'"]+)['"]\s*\)/g
|
|
48
|
+
|
|
49
|
+
/** Resolve the way Node and a bundler would: as written, then the usual suffixes. */
|
|
50
|
+
function resolves(fromFile, specifier) {
|
|
51
|
+
const base = resolve(dirname(fromFile), specifier)
|
|
52
|
+
const candidates = [
|
|
53
|
+
base,
|
|
54
|
+
`${base}.js`, `${base}.cjs`, `${base}.mjs`,
|
|
55
|
+
join(base, 'index.js'), join(base, 'index.cjs'), join(base, 'index.mjs'),
|
|
56
|
+
]
|
|
57
|
+
return candidates.some((c) => existsSync(c) && statSync(c).isFile())
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const broken = []
|
|
61
|
+
for (const file of walk(DIST)) {
|
|
62
|
+
const source = readFileSync(file, 'utf8')
|
|
63
|
+
for (const match of source.matchAll(SPECIFIER)) {
|
|
64
|
+
const specifier = match[1] || match[2] || match[3]
|
|
65
|
+
if (!specifier || resolves(file, specifier)) continue
|
|
66
|
+
broken.push({ file: file.replace(`${process.cwd()}/`, ''), specifier })
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (broken.length === 0) {
|
|
71
|
+
console.log('verify-dist-imports: every relative import in dist/ resolves.')
|
|
72
|
+
process.exit(0)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
console.error(`verify-dist-imports: ${broken.length} unresolved relative import(s) in dist/.\n`)
|
|
76
|
+
for (const { file, specifier } of broken) {
|
|
77
|
+
console.error(` ${file}\n imports '${specifier}' — not emitted`)
|
|
78
|
+
}
|
|
79
|
+
console.error(
|
|
80
|
+
'\nThe target is almost certainly missing from the `entry` list of the\n' +
|
|
81
|
+
'`bundle: false` pass in tsup.config.ts. Add it there, or move the value\n' +
|
|
82
|
+
'being imported into a module that pass already emits.',
|
|
83
|
+
)
|
|
84
|
+
process.exit(1)
|
|
@@ -525,9 +525,16 @@ function createCascadeDelete(slugs: CollectionSlugs): CollectionBeforeDeleteHook
|
|
|
525
525
|
return async ({ id, req }) => {
|
|
526
526
|
const collections = [slugs.ticketMessages, slugs.ticketActivityLog, slugs.timeEntries, slugs.satisfactionSurveys]
|
|
527
527
|
for (const slug of collections) {
|
|
528
|
+
// `req` must be forwarded: it carries the ambient transaction. Without
|
|
529
|
+
// it these cascades open their own connection and, on SQLite, deadlock
|
|
530
|
+
// against the transaction that is deleting the ticket — the delete then
|
|
531
|
+
// fails silently (Payload collects per-document errors instead of
|
|
532
|
+
// throwing) and the children survive the parent.
|
|
533
|
+
if (!req.payload.collections?.[slug as never]) continue
|
|
528
534
|
await dbDelete(req.payload, slug, {
|
|
529
535
|
where: { ticket: { equals: id } },
|
|
530
536
|
overrideAccess: true,
|
|
537
|
+
req,
|
|
531
538
|
})
|
|
532
539
|
}
|
|
533
540
|
}
|
|
@@ -1,13 +1,19 @@
|
|
|
1
|
-
import type { CollectionConfig, CollectionAfterChangeHook, CollectionAfterDeleteHook } from 'payload'
|
|
1
|
+
import type { CollectionConfig, CollectionAfterChangeHook, CollectionAfterDeleteHook, PayloadRequest } from 'payload'
|
|
2
2
|
import type { Payload } from 'payload'
|
|
3
3
|
import type { CollectionSlugs } from '../utils/slugs'
|
|
4
4
|
|
|
5
5
|
// ─── Hooks ───────────────────────────────────────────────
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* `req` is forwarded to every operation so the rollup joins the caller's
|
|
9
|
+
* transaction. Reading or writing outside it would see a stale total and, on
|
|
10
|
+
* SQLite, contend with the very transaction that triggered the hook.
|
|
11
|
+
*/
|
|
7
12
|
async function recalculateTicketTime(
|
|
8
13
|
payload: Payload,
|
|
9
14
|
slugs: CollectionSlugs,
|
|
10
15
|
ticketId: number | string,
|
|
16
|
+
req?: PayloadRequest,
|
|
11
17
|
): Promise<void> {
|
|
12
18
|
// Sum all time entries for this ticket, paginating to avoid loading all at once
|
|
13
19
|
let totalMinutes = 0
|
|
@@ -23,6 +29,7 @@ async function recalculateTicketTime(
|
|
|
23
29
|
depth: 0,
|
|
24
30
|
overrideAccess: true,
|
|
25
31
|
select: { duration: true },
|
|
32
|
+
req,
|
|
26
33
|
})
|
|
27
34
|
|
|
28
35
|
for (const entry of entries.docs) {
|
|
@@ -38,6 +45,7 @@ async function recalculateTicketTime(
|
|
|
38
45
|
id: ticketId,
|
|
39
46
|
data: { totalTimeMinutes: totalMinutes },
|
|
40
47
|
overrideAccess: true,
|
|
48
|
+
req,
|
|
41
49
|
})
|
|
42
50
|
}
|
|
43
51
|
|
|
@@ -45,7 +53,7 @@ function createRecalculateTicketTime(slugs: CollectionSlugs): CollectionAfterCha
|
|
|
45
53
|
return async ({ doc, req }) => {
|
|
46
54
|
if (!doc.ticket) return
|
|
47
55
|
const ticketId = typeof doc.ticket === 'object' ? doc.ticket.id : doc.ticket
|
|
48
|
-
await recalculateTicketTime(req.payload, slugs, ticketId)
|
|
56
|
+
await recalculateTicketTime(req.payload, slugs, ticketId, req)
|
|
49
57
|
}
|
|
50
58
|
}
|
|
51
59
|
|
|
@@ -59,7 +67,7 @@ function createRecalculateTicketTimeOnDelete(slugs: CollectionSlugs): Collection
|
|
|
59
67
|
if (!doc?.ticket) return
|
|
60
68
|
const ticketId = typeof doc.ticket === 'object' ? doc.ticket.id : doc.ticket
|
|
61
69
|
try {
|
|
62
|
-
await recalculateTicketTime(req.payload, slugs, ticketId)
|
|
70
|
+
await recalculateTicketTime(req.payload, slugs, ticketId, req)
|
|
63
71
|
} catch (err) {
|
|
64
72
|
// The entry is already gone; never fail the delete on the rollup refresh.
|
|
65
73
|
console.error('[support] Failed to recalculate ticket time after delete:', err)
|
|
@@ -287,7 +287,6 @@ export const RichTextEditor = forwardRef<RichTextEditorHandle, Props>(function R
|
|
|
287
287
|
fontSize: '14px',
|
|
288
288
|
lineHeight: 1.6,
|
|
289
289
|
color: '#1f2937',
|
|
290
|
-
outline: 'none',
|
|
291
290
|
overflowY: 'auto',
|
|
292
291
|
}}
|
|
293
292
|
/>
|
|
@@ -300,6 +299,7 @@ export const RichTextEditor = forwardRef<RichTextEditorHandle, Props>(function R
|
|
|
300
299
|
type="file"
|
|
301
300
|
accept="image/*"
|
|
302
301
|
multiple
|
|
302
|
+
aria-label="Inserer une image"
|
|
303
303
|
onChange={handleFileChange}
|
|
304
304
|
style={{ display: 'none' }}
|
|
305
305
|
/>
|
|
@@ -76,7 +76,7 @@ export const RewriteDropdown: React.FC<{
|
|
|
76
76
|
onMouseLeave={(e) => { (e.target as HTMLElement).style.background = 'transparent' }}
|
|
77
77
|
>
|
|
78
78
|
<span style={{ fontSize: 12, fontWeight: 600, color: 'var(--theme-text, #111)' }}>{style.label}</span>
|
|
79
|
-
<span style={{ fontSize: 10, color: 'var(--theme-elevation-
|
|
79
|
+
<span style={{ fontSize: 10, color: 'var(--theme-elevation-650, #6b7280)' }}>{style.desc}</span>
|
|
80
80
|
</button>
|
|
81
81
|
))}
|
|
82
82
|
</div>
|
|
@@ -39,6 +39,7 @@ export function AISummaryPanel({
|
|
|
39
39
|
return (
|
|
40
40
|
<div style={{ marginBottom: '14px' }}>
|
|
41
41
|
<button
|
|
42
|
+
type="button"
|
|
42
43
|
onClick={() => { setShowAiSummary(!showAiSummary); if (!showAiSummary && !aiSummary) handleAiGenerate() }}
|
|
43
44
|
style={{
|
|
44
45
|
...s.ghostBtn('#17807c', false),
|
|
@@ -61,6 +62,7 @@ export function AISummaryPanel({
|
|
|
61
62
|
</h4>
|
|
62
63
|
<div style={{ display: 'flex', gap: '6px' }}>
|
|
63
64
|
<button
|
|
65
|
+
type="button"
|
|
64
66
|
onClick={handleAiGenerate}
|
|
65
67
|
disabled={aiGenerating}
|
|
66
68
|
style={{ ...s.outlineBtn('#17807c', aiGenerating), fontSize: '11px', padding: '4px 10px' }}
|
|
@@ -69,6 +71,7 @@ export function AISummaryPanel({
|
|
|
69
71
|
</button>
|
|
70
72
|
{aiSummary && !aiGenerating && (
|
|
71
73
|
<button
|
|
74
|
+
type="button"
|
|
72
75
|
onClick={handleAiSave}
|
|
73
76
|
disabled={aiSaving || aiSaved}
|
|
74
77
|
style={{ ...s.btn(aiSaved ? '#16a34a' : '#2563eb', aiSaving), fontSize: '11px', padding: '4px 10px' }}
|
|
@@ -26,12 +26,13 @@ export function MergePanel({
|
|
|
26
26
|
<div style={{ display: 'flex', gap: '8px', alignItems: 'center', marginBottom: '8px' }}>
|
|
27
27
|
<input
|
|
28
28
|
type="text"
|
|
29
|
+
aria-label="Numero du ticket cible"
|
|
29
30
|
value={mergeTarget}
|
|
30
31
|
onChange={(e) => { setMergeTarget(e.target.value); setMergeTargetInfo(null); setMergeError('') }}
|
|
31
32
|
placeholder="TK-0001"
|
|
32
33
|
style={{ ...s.input, width: '130px' }}
|
|
33
34
|
/>
|
|
34
|
-
<button onClick={handleMergeLookup} style={{ ...s.outlineBtn('#ec4899'), fontSize: '12px', padding: '6px 14px' }}>
|
|
35
|
+
<button type="button" onClick={handleMergeLookup} style={{ ...s.outlineBtn('#ec4899'), fontSize: '12px', padding: '6px 14px' }}>
|
|
35
36
|
Rechercher
|
|
36
37
|
</button>
|
|
37
38
|
{mergeError && <span style={{ fontSize: '12px', color: '#be185d', fontWeight: 600 }}>{mergeError}</span>}
|
|
@@ -41,7 +42,7 @@ export function MergePanel({
|
|
|
41
42
|
<span style={{ fontSize: '13px', fontWeight: 600 }}>
|
|
42
43
|
{mergeTargetInfo.ticketNumber} — {mergeTargetInfo.subject}
|
|
43
44
|
</span>
|
|
44
|
-
<button onClick={handleMerge} disabled={merging} style={{ ...s.btn('#ec4899', merging), color: '#fff', fontSize: '12px', padding: '6px 14px' }}>
|
|
45
|
+
<button type="button" onClick={handleMerge} disabled={merging} style={{ ...s.btn('#ec4899', merging), color: '#fff', fontSize: '12px', padding: '6px 14px' }}>
|
|
45
46
|
{merging ? 'Fusion...' : 'Confirmer la fusion'}
|
|
46
47
|
</button>
|
|
47
48
|
</div>
|
|
@@ -76,6 +77,7 @@ export function ExtMessagePanel({
|
|
|
76
77
|
<div style={{ padding: '14px 18px', borderRadius: '8px', backgroundColor: '#eef2ff', border: '1px solid #c7d2fe', marginBottom: '14px' }}>
|
|
77
78
|
<h4 style={{ fontSize: '13px', fontWeight: 600, marginBottom: '10px', color: '#312e81' }}>Ajouter un message reçu (email, SMS, WhatsApp...)</h4>
|
|
78
79
|
<textarea
|
|
80
|
+
aria-label="Contenu du message recu"
|
|
79
81
|
value={extMsgBody}
|
|
80
82
|
onChange={(e) => setExtMsgBody(e.target.value)}
|
|
81
83
|
placeholder="Coller le contenu du message reçu..."
|
|
@@ -83,17 +85,18 @@ export function ExtMessagePanel({
|
|
|
83
85
|
style={{ ...s.input, width: '100%', resize: 'vertical', marginBottom: '10px' }}
|
|
84
86
|
/>
|
|
85
87
|
<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' }}>
|
|
88
|
+
<select aria-label="Auteur du message recu" value={extMsgAuthor} onChange={(e) => setExtMsgAuthor(e.target.value as 'client' | 'admin')} style={{ ...s.input, fontSize: '12px' }}>
|
|
87
89
|
<option value="client">Envoyé par le client</option>
|
|
88
90
|
<option value="admin">Envoyé par le support</option>
|
|
89
91
|
</select>
|
|
90
92
|
<input
|
|
91
93
|
type="datetime-local"
|
|
94
|
+
aria-label="Date du message recu"
|
|
92
95
|
value={extMsgDate}
|
|
93
96
|
onChange={(e) => setExtMsgDate(e.target.value)}
|
|
94
97
|
style={{ ...s.input, fontSize: '12px' }}
|
|
95
98
|
/>
|
|
96
|
-
<input ref={extFileInputRef} type="file" multiple onChange={handleExtFileChange} style={{ display: 'none' }} accept="image/*,.pdf,.doc,.docx,.txt,.zip" />
|
|
99
|
+
<input ref={extFileInputRef} type="file" multiple aria-label="Joindre des fichiers au message recu" onChange={handleExtFileChange} style={{ display: 'none' }} accept="image/*,.pdf,.doc,.docx,.txt,.zip" />
|
|
97
100
|
<button type="button" onClick={() => extFileInputRef.current?.click()} style={{ ...s.ghostBtn('#6b7280'), fontSize: '12px', padding: '6px 12px' }}>
|
|
98
101
|
+ PJ
|
|
99
102
|
</button>
|
|
@@ -108,7 +111,7 @@ export function ExtMessagePanel({
|
|
|
108
111
|
))}
|
|
109
112
|
</div>
|
|
110
113
|
)}
|
|
111
|
-
<button onClick={handleSendExtMsg} disabled={sendingExtMsg || !extMsgBody.trim()} style={s.btn('#1d2b4d', sendingExtMsg || !extMsgBody.trim())}>
|
|
114
|
+
<button type="button" onClick={handleSendExtMsg} disabled={sendingExtMsg || !extMsgBody.trim()} style={s.btn('#1d2b4d', sendingExtMsg || !extMsgBody.trim())}>
|
|
112
115
|
{sendingExtMsg ? 'Ajout...' : 'Ajouter (sans notification)'}
|
|
113
116
|
</button>
|
|
114
117
|
</div>
|
|
@@ -186,6 +189,7 @@ export function ReminderPanel({
|
|
|
186
189
|
)}
|
|
187
190
|
|
|
188
191
|
<button
|
|
192
|
+
type="button"
|
|
189
193
|
onClick={handleSendReminder}
|
|
190
194
|
disabled={reminderSending || !clientEmail}
|
|
191
195
|
style={{ ...s.btn(C.orange, reminderSending || !clientEmail), color: '#fff', fontSize: '13px', padding: '8px 18px' }}
|
|
@@ -207,11 +211,12 @@ export function SnoozePanel({ snoozeSaving, handleSnooze }: SnoozePanelProps) {
|
|
|
207
211
|
<div style={{ padding: '14px 18px', borderRadius: '8px', backgroundColor: '#eef8f7', border: '1px solid #b8dcda', marginBottom: '14px' }}>
|
|
208
212
|
<h4 style={{ fontSize: '13px', fontWeight: 600, marginBottom: '10px', color: '#5b21b6' }}>Snooze — masquer temporairement</h4>
|
|
209
213
|
<div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap', alignItems: 'center' }}>
|
|
210
|
-
<button onClick={() => handleSnooze(1)} disabled={snoozeSaving} style={{ ...s.outlineBtn('#17807c', snoozeSaving), fontSize: '12px' }}>1 jour</button>
|
|
211
|
-
<button onClick={() => handleSnooze(3)} disabled={snoozeSaving} style={{ ...s.outlineBtn('#17807c', snoozeSaving), fontSize: '12px' }}>3 jours</button>
|
|
212
|
-
<button onClick={() => handleSnooze(7)} disabled={snoozeSaving} style={{ ...s.outlineBtn('#17807c', snoozeSaving), fontSize: '12px' }}>1 semaine</button>
|
|
214
|
+
<button type="button" onClick={() => handleSnooze(1)} disabled={snoozeSaving} style={{ ...s.outlineBtn('#17807c', snoozeSaving), fontSize: '12px' }}>1 jour</button>
|
|
215
|
+
<button type="button" onClick={() => handleSnooze(3)} disabled={snoozeSaving} style={{ ...s.outlineBtn('#17807c', snoozeSaving), fontSize: '12px' }}>3 jours</button>
|
|
216
|
+
<button type="button" onClick={() => handleSnooze(7)} disabled={snoozeSaving} style={{ ...s.outlineBtn('#17807c', snoozeSaving), fontSize: '12px' }}>1 semaine</button>
|
|
213
217
|
<input
|
|
214
218
|
type="datetime-local"
|
|
219
|
+
aria-label="Snoozer jusqu'a une date precise"
|
|
215
220
|
onChange={(e) => { if (e.target.value) handleSnooze(null, e.target.value) }}
|
|
216
221
|
style={{ ...s.input, fontSize: '12px' }}
|
|
217
222
|
/>
|
|
@@ -96,6 +96,7 @@ export function ClientHistory({
|
|
|
96
96
|
{/* Notes */}
|
|
97
97
|
<h5 style={{ fontSize: '12px', fontWeight: 600, color: C.textSecondary, marginBottom: '8px' }}>Notes internes</h5>
|
|
98
98
|
<textarea
|
|
99
|
+
aria-label="Notes internes sur le client"
|
|
99
100
|
value={clientNotes}
|
|
100
101
|
onChange={(e) => onNotesChange(e.target.value)}
|
|
101
102
|
rows={3}
|
|
@@ -109,6 +109,7 @@ export function CodeBlockInserter({ onInsert, className, style }: CodeBlockInser
|
|
|
109
109
|
<input
|
|
110
110
|
ref={inputRef}
|
|
111
111
|
type="text"
|
|
112
|
+
aria-label="Rechercher un langage"
|
|
112
113
|
value={filter}
|
|
113
114
|
onChange={(e) => setFilter(e.target.value)}
|
|
114
115
|
onKeyDown={(e) => {
|
|
@@ -124,7 +125,6 @@ export function CodeBlockInserter({ onInsert, className, style }: CodeBlockInser
|
|
|
124
125
|
border: '1px solid #334155',
|
|
125
126
|
borderRadius: 6,
|
|
126
127
|
color: '#e2e8f0',
|
|
127
|
-
outline: 'none',
|
|
128
128
|
boxSizing: 'border-box',
|
|
129
129
|
}}
|
|
130
130
|
/>
|
|
@@ -48,6 +48,7 @@ export function QuickActions({
|
|
|
48
48
|
<div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap', marginBottom: '16px', alignItems: 'center' }}>
|
|
49
49
|
{statusTransitions.map((a) => (
|
|
50
50
|
<button
|
|
51
|
+
type="button"
|
|
51
52
|
key={a.status}
|
|
52
53
|
onClick={() => onStatusChange(a.status)}
|
|
53
54
|
disabled={statusUpdating}
|
|
@@ -57,22 +58,22 @@ export function QuickActions({
|
|
|
57
58
|
</button>
|
|
58
59
|
))}
|
|
59
60
|
{snoozeUntil && new Date(snoozeUntil) > new Date() && (
|
|
60
|
-
<button onClick={onCancelSnooze} disabled={snoozeSaving} style={{ ...s.ghostBtn('#17807c', snoozeSaving), fontSize: '12px', padding: '5px 10px' }}>
|
|
61
|
+
<button type="button" onClick={onCancelSnooze} disabled={snoozeSaving} style={{ ...s.ghostBtn('#17807c', snoozeSaving), fontSize: '12px', padding: '5px 10px' }}>
|
|
61
62
|
Annuler snooze
|
|
62
63
|
</button>
|
|
63
64
|
)}
|
|
64
65
|
<span style={{ borderLeft: `1px solid ${C.border}`, height: '20px', margin: '0 4px' }} />
|
|
65
|
-
<button onClick={onToggleMerge} style={s.ghostBtn('#be185d')}>Fusionner</button>
|
|
66
|
-
<button onClick={onToggleExtMsg} style={s.ghostBtn('#4f46e5')}>+ Message reçu</button>
|
|
67
|
-
<button onClick={onToggleSnooze} style={s.ghostBtn('#17807c')}>Snooze</button>
|
|
66
|
+
<button type="button" onClick={onToggleMerge} style={s.ghostBtn('#be185d')}>Fusionner</button>
|
|
67
|
+
<button type="button" onClick={onToggleExtMsg} style={s.ghostBtn('#4f46e5')}>+ Message reçu</button>
|
|
68
|
+
<button type="button" onClick={onToggleSnooze} style={s.ghostBtn('#17807c')}>Snooze</button>
|
|
68
69
|
{showReminderButton && (
|
|
69
|
-
<button onClick={onToggleReminder} style={s.ghostBtn('#ea580c')}>{'⏰'} Relancer</button>
|
|
70
|
+
<button type="button" onClick={onToggleReminder} style={s.ghostBtn('#ea580c')}>{'⏰'} Relancer</button>
|
|
70
71
|
)}
|
|
71
|
-
<button onClick={onNextTicket} style={s.ghostBtn('#16a34a')}>Ticket suivant</button>
|
|
72
|
+
<button type="button" onClick={onNextTicket} style={s.ghostBtn('#16a34a')}>Ticket suivant</button>
|
|
72
73
|
{scheduledClose && (
|
|
73
74
|
<span style={{ display: 'inline-flex', alignItems: 'center', gap: '8px', padding: '5px 10px', borderRadius: '6px', backgroundColor: '#fff7ed', border: '1px solid #fed7aa', fontSize: '12px', color: '#9a3412', fontWeight: 600 }}>
|
|
74
75
|
{'⏰'} Fermeture auto le {scheduledClose}
|
|
75
|
-
<button onClick={onCancelScheduledClose} disabled={cancelingClose} style={{ border: 'none', background: 'none', color: '#b91c1c', cursor: cancelingClose ? 'not-allowed' : 'pointer', fontWeight: 700, fontSize: '12px', textDecoration: 'underline', padding: 0, opacity: cancelingClose ? 0.5 : 1 }}>
|
|
76
|
+
<button type="button" onClick={onCancelScheduledClose} disabled={cancelingClose} style={{ border: 'none', background: 'none', color: '#b91c1c', cursor: cancelingClose ? 'not-allowed' : 'pointer', fontWeight: 700, fontSize: '12px', textDecoration: 'underline', padding: 0, opacity: cancelingClose ? 0.5 : 1 }}>
|
|
76
77
|
{cancelingClose ? '...' : 'Annuler'}
|
|
77
78
|
</button>
|
|
78
79
|
</span>
|
|
@@ -90,6 +91,7 @@ export function QuickActions({
|
|
|
90
91
|
Ticket suivant : <strong>{nextTicketInfo}</strong>
|
|
91
92
|
</span>
|
|
92
93
|
<button
|
|
94
|
+
type="button"
|
|
93
95
|
onClick={() => { window.location.href = `/admin/support/ticket?id=${nextTicketId}` }}
|
|
94
96
|
style={{ ...s.btn(C.statusResolved), color: C.white, fontSize: '12px', padding: '5px 14px' }}
|
|
95
97
|
>
|
|
@@ -99,7 +101,7 @@ export function QuickActions({
|
|
|
99
101
|
) : (
|
|
100
102
|
<span style={{ fontSize: '13px', color: '#166534', fontWeight: 700 }}>{nextTicketInfo}</span>
|
|
101
103
|
)}
|
|
102
|
-
<button onClick={onCloseNextTicket} style={{ border: 'none', background: 'none', color: C.textMuted, cursor: 'pointer', fontSize: '16px', fontWeight: 700, marginLeft: '8px' }}>×</button>
|
|
104
|
+
<button type="button" onClick={onCloseNextTicket} style={{ border: 'none', background: 'none', color: C.textMuted, cursor: 'pointer', fontSize: '16px', fontWeight: 700, marginLeft: '8px' }}>×</button>
|
|
103
105
|
</div>
|
|
104
106
|
)}
|
|
105
107
|
</>
|
|
@@ -71,6 +71,7 @@ export function TicketHeader({
|
|
|
71
71
|
)}
|
|
72
72
|
<span style={{ marginLeft: 'auto', display: 'inline-flex', alignItems: 'center', gap: '4px' }}>
|
|
73
73
|
<button
|
|
74
|
+
type="button"
|
|
74
75
|
onClick={() => onCopyLink('admin')}
|
|
75
76
|
title="Copier le lien admin"
|
|
76
77
|
style={{ background: 'none', border: 'none', cursor: 'pointer', fontSize: '13px', opacity: copiedLink === 'admin' ? 1 : 0.5, padding: '2px 4px' }}
|
|
@@ -78,6 +79,7 @@ export function TicketHeader({
|
|
|
78
79
|
{copiedLink === 'admin' ? '\u2705' : '\uD83D\uDCCB'}
|
|
79
80
|
</button>
|
|
80
81
|
<button
|
|
82
|
+
type="button"
|
|
81
83
|
onClick={() => onCopyLink('client')}
|
|
82
84
|
title="Copier le lien client"
|
|
83
85
|
style={{ background: 'none', border: 'none', cursor: 'pointer', fontSize: '13px', opacity: copiedLink === 'client' ? 1 : 0.5, padding: '2px 4px' }}
|