@escalated-dev/escalated 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. package/README.md +102 -0
  2. package/package.json +37 -6
  3. package/src/components/ActivityTimeline.stories.js +55 -0
  4. package/src/components/ActivityTimeline.vue +83 -66
  5. package/src/components/AdminDashboard.stories.js +1095 -0
  6. package/src/components/AgentLoadIndicator.stories.js +37 -0
  7. package/src/components/AgentLoadIndicator.vue +49 -0
  8. package/src/components/AgentPerformanceCard.vue +44 -0
  9. package/src/components/ArticleEditor.vue +82 -0
  10. package/src/components/AssigneeSelect.vue +37 -23
  11. package/src/components/AttachmentList.vue +53 -37
  12. package/src/components/AuditLogEntry.vue +95 -0
  13. package/src/components/BulkActionBar.vue +99 -30
  14. package/src/components/CategoryTree.vue +89 -0
  15. package/src/components/ChartWidget.vue +46 -0
  16. package/src/components/CollisionWarning.stories.js +30 -0
  17. package/src/components/CollisionWarning.vue +68 -0
  18. package/src/components/ConditionalFieldRules.vue +135 -0
  19. package/src/components/ContextPanel.vue +66 -0
  20. package/src/components/ContextPanelSection.vue +65 -0
  21. package/src/components/CustomFieldRenderer.vue +159 -0
  22. package/src/components/EscalatedLayout.vue +400 -69
  23. package/src/components/FileDropzone.vue +57 -41
  24. package/src/components/FollowButton.stories.js +30 -0
  25. package/src/components/FollowButton.vue +53 -28
  26. package/src/components/KeyboardShortcutHelp.vue +129 -39
  27. package/src/components/KnowledgePanel.vue +122 -0
  28. package/src/components/KpiCard.stories.js +32 -0
  29. package/src/components/KpiCard.vue +88 -0
  30. package/src/components/MacroDropdown.vue +95 -35
  31. package/src/components/PermissionMatrix.vue +107 -0
  32. package/src/components/PinnedNotes.vue +75 -33
  33. package/src/components/PresenceIndicator.vue +93 -27
  34. package/src/components/PriorityBadge.stories.js +33 -0
  35. package/src/components/PriorityBadge.vue +46 -39
  36. package/src/components/QuickFilters.vue +65 -49
  37. package/src/components/ReplyComposer.vue +273 -159
  38. package/src/components/ReplyThread.vue +78 -23
  39. package/src/components/SatisfactionRating.vue +98 -46
  40. package/src/components/ScheduleEditor.vue +104 -0
  41. package/src/components/SideConversation.vue +165 -0
  42. package/src/components/SideConversationList.vue +147 -0
  43. package/src/components/SkillTagManager.vue +79 -0
  44. package/src/components/SlaComplianceChart.vue +53 -0
  45. package/src/components/SlaTimer.stories.js +35 -0
  46. package/src/components/SlaTimer.vue +62 -49
  47. package/src/components/StatsCard.stories.js +24 -0
  48. package/src/components/StatsCard.vue +58 -46
  49. package/src/components/StatusBadge.stories.js +53 -0
  50. package/src/components/StatusBadge.vue +78 -46
  51. package/src/components/TagSelect.vue +84 -53
  52. package/src/components/TicketFilters.vue +225 -69
  53. package/src/components/TicketLinkPanel.vue +193 -0
  54. package/src/components/TicketList.vue +594 -193
  55. package/src/components/TicketMergeDialog.vue +157 -0
  56. package/src/components/TicketSidebar.vue +167 -99
  57. package/src/components/TicketTypeSelector.vue +21 -0
  58. package/src/components/TotpInput.stories.js +9 -0
  59. package/src/components/TotpInput.vue +81 -0
  60. package/src/components/TwoFactorSetup.vue +113 -0
  61. package/src/composables/useDebouncedSearch.js +38 -0
  62. package/src/composables/useI18n.js +80 -0
  63. package/src/composables/usePluginExtensions.js +95 -3
  64. package/src/composables/usePluginHooks.js +65 -0
  65. package/src/defineEscalatedPlugin.js +34 -8
  66. package/src/index.js +72 -35
  67. package/src/locales/de.json +442 -0
  68. package/src/locales/en.json +467 -0
  69. package/src/locales/es.json +442 -0
  70. package/src/locales/fr.json +442 -0
  71. package/src/locales/index.js +6 -0
  72. package/src/pages/Admin/ApiTokens/Index.vue +231 -67
  73. package/src/pages/Admin/AuditLog/Index.vue +154 -0
  74. package/src/pages/Admin/Automations/Form.vue +184 -0
  75. package/src/pages/Admin/Automations/Index.vue +129 -0
  76. package/src/pages/Admin/BusinessHours/Form.vue +172 -0
  77. package/src/pages/Admin/BusinessHours/Index.vue +113 -0
  78. package/src/pages/Admin/CannedResponses/Index.vue +127 -70
  79. package/src/pages/Admin/Capacity/Index.vue +134 -0
  80. package/src/pages/Admin/CustomFields/Form.vue +195 -0
  81. package/src/pages/Admin/CustomFields/Index.vue +134 -0
  82. package/src/pages/Admin/CustomObjects/Form.vue +202 -0
  83. package/src/pages/Admin/CustomObjects/Index.vue +127 -0
  84. package/src/pages/Admin/CustomObjects/Records.vue +228 -0
  85. package/src/pages/Admin/Departments/Form.vue +75 -51
  86. package/src/pages/Admin/Departments/Index.vue +109 -57
  87. package/src/pages/Admin/EscalationRules/Form.vue +157 -96
  88. package/src/pages/Admin/EscalationRules/Index.vue +178 -57
  89. package/src/pages/Admin/Import/Connect.vue +96 -0
  90. package/src/pages/Admin/Import/Index.vue +102 -0
  91. package/src/pages/Admin/Import/Mapping.vue +110 -0
  92. package/src/pages/Admin/Import/Progress.vue +153 -0
  93. package/src/pages/Admin/Import/Review.vue +76 -0
  94. package/src/pages/Admin/KnowledgeBase/Articles/Form.vue +142 -0
  95. package/src/pages/Admin/KnowledgeBase/Articles/Index.vue +195 -0
  96. package/src/pages/Admin/KnowledgeBase/Categories/Index.vue +193 -0
  97. package/src/pages/Admin/Macros/Index.vue +250 -89
  98. package/src/pages/Admin/Plugins/Index.vue +205 -75
  99. package/src/pages/Admin/Reports/AgentMetrics.vue +115 -0
  100. package/src/pages/Admin/Reports/CsatReport.vue +127 -0
  101. package/src/pages/Admin/Reports/Dashboard.vue +211 -0
  102. package/src/pages/Admin/Reports/SlaReport.vue +166 -0
  103. package/src/pages/Admin/Reports.vue +139 -34
  104. package/src/pages/Admin/Roles/Form.vue +71 -0
  105. package/src/pages/Admin/Roles/Index.vue +122 -0
  106. package/src/pages/Admin/Settings/CsatSettings.vue +165 -0
  107. package/src/pages/Admin/Settings/DataRetention.vue +182 -0
  108. package/src/pages/Admin/Settings/EmailSettings.vue +243 -0
  109. package/src/pages/Admin/Settings/Sandbox.vue +135 -0
  110. package/src/pages/Admin/Settings/SsoSettings.vue +218 -0
  111. package/src/pages/Admin/Settings/TwoFactor.vue +110 -0
  112. package/src/pages/Admin/Settings.vue +468 -260
  113. package/src/pages/Admin/Skills/Form.vue +53 -0
  114. package/src/pages/Admin/Skills/Index.vue +94 -0
  115. package/src/pages/Admin/SlaPolicies/Form.vue +118 -75
  116. package/src/pages/Admin/SlaPolicies/Index.vue +120 -59
  117. package/src/pages/Admin/Statuses/Form.vue +114 -0
  118. package/src/pages/Admin/Statuses/Index.vue +153 -0
  119. package/src/pages/Admin/Tags/Index.vue +191 -105
  120. package/src/pages/Admin/Tickets/Index.vue +91 -39
  121. package/src/pages/Admin/Tickets/Show.vue +101 -37
  122. package/src/pages/Admin/Webhooks/DeliveryLog.vue +120 -0
  123. package/src/pages/Admin/Webhooks/Form.vue +110 -0
  124. package/src/pages/Admin/Webhooks/Index.vue +136 -0
  125. package/src/pages/Agent/Dashboard.vue +311 -186
  126. package/src/pages/Agent/TicketIndex.vue +88 -38
  127. package/src/pages/Agent/TicketShow.vue +258 -51
  128. package/src/pages/Customer/Create.vue +36 -9
  129. package/src/pages/Customer/Index.vue +31 -24
  130. package/src/pages/Customer/KnowledgeBase/Article.vue +127 -0
  131. package/src/pages/Customer/KnowledgeBase/Index.vue +151 -0
  132. package/src/plugin.js +86 -1
  133. package/src/utils/constants.js +30 -0
  134. package/src/utils/formatting.js +35 -0
  135. package/src/utils/sanitizeHtml.js +64 -0
  136. package/src/pages/Admin/Plugins.vue +0 -129
@@ -0,0 +1,102 @@
1
+ <script setup>
2
+ import { router } from '@inertiajs/vue3';
3
+
4
+ const props = defineProps({
5
+ adapters: { type: Array, default: () => [] },
6
+ jobs: { type: Array, default: () => [] },
7
+ });
8
+
9
+ function selectPlatform(platform) {
10
+ router.get(route('escalated.admin.import.connect', platform));
11
+ }
12
+
13
+ function viewJob(jobId) {
14
+ router.get(route('escalated.admin.import.progress', jobId));
15
+ }
16
+
17
+ const statusColors = {
18
+ pending: 'text-gray-400',
19
+ importing: 'text-blue-400',
20
+ paused: 'text-yellow-400',
21
+ completed: 'text-green-400',
22
+ failed: 'text-red-400',
23
+ };
24
+ </script>
25
+
26
+ <template>
27
+ <div class="space-y-8">
28
+ <div>
29
+ <h2 class="text-lg font-semibold text-[var(--esc-panel-text)]">Import Data</h2>
30
+ <p class="mt-1 text-sm text-[var(--esc-panel-text-secondary)]">
31
+ Migrate tickets, contacts, and history from another helpdesk platform.
32
+ </p>
33
+ </div>
34
+
35
+ <!-- Platform cards -->
36
+ <div v-if="adapters.length" class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
37
+ <button
38
+ v-for="adapter in adapters"
39
+ :key="adapter.name"
40
+ class="flex flex-col items-start rounded-xl border border-[var(--esc-panel-border)] bg-[var(--esc-panel-surface)] p-6 text-left transition hover:border-[var(--esc-panel-accent)] hover:bg-[var(--esc-panel-hover)]"
41
+ @click="selectPlatform(adapter.name)"
42
+ >
43
+ <span class="text-base font-medium text-[var(--esc-panel-text)]">{{ adapter.display_name }}</span>
44
+ <span class="mt-1 text-sm text-[var(--esc-panel-text-secondary)]"
45
+ >Import from {{ adapter.display_name }}</span
46
+ >
47
+ </button>
48
+ </div>
49
+
50
+ <div
51
+ v-else
52
+ class="rounded-xl border border-[var(--esc-panel-border)] bg-[var(--esc-panel-surface)] p-8 text-center"
53
+ >
54
+ <p class="text-sm text-[var(--esc-panel-text-secondary)]">
55
+ No import adapters installed. Install an import plugin (e.g., escalated-plugin-import-zendesk) to get
56
+ started.
57
+ </p>
58
+ </div>
59
+
60
+ <!-- Import history -->
61
+ <div v-if="jobs.length">
62
+ <h3 class="mb-3 text-sm font-semibold text-[var(--esc-panel-text)]">Import History</h3>
63
+ <div class="overflow-hidden rounded-xl border border-[var(--esc-panel-border)]">
64
+ <table class="w-full text-sm">
65
+ <thead class="bg-[var(--esc-panel-surface-alt)]">
66
+ <tr>
67
+ <th class="px-4 py-3 text-left font-medium text-[var(--esc-panel-text-secondary)]">
68
+ Platform
69
+ </th>
70
+ <th class="px-4 py-3 text-left font-medium text-[var(--esc-panel-text-secondary)]">
71
+ Status
72
+ </th>
73
+ <th class="px-4 py-3 text-left font-medium text-[var(--esc-panel-text-secondary)]">Date</th>
74
+ <th class="px-4 py-3 text-left font-medium text-[var(--esc-panel-text-secondary)]"></th>
75
+ </tr>
76
+ </thead>
77
+ <tbody class="divide-y divide-[var(--esc-panel-border)]">
78
+ <tr
79
+ v-for="job in jobs"
80
+ :key="job.id"
81
+ class="bg-[var(--esc-panel-surface)] hover:bg-[var(--esc-panel-hover)]"
82
+ >
83
+ <td class="px-4 py-3 text-[var(--esc-panel-text)]">{{ job.platform }}</td>
84
+ <td class="px-4 py-3">
85
+ <span :class="statusColors[job.status]" class="capitalize">{{ job.status }}</span>
86
+ </td>
87
+ <td class="px-4 py-3 text-[var(--esc-panel-text-secondary)]">{{ job.created_at }}</td>
88
+ <td class="px-4 py-3 text-right">
89
+ <button
90
+ class="text-sm text-[var(--esc-panel-accent)] hover:underline"
91
+ @click="viewJob(job.id)"
92
+ >
93
+ View
94
+ </button>
95
+ </td>
96
+ </tr>
97
+ </tbody>
98
+ </table>
99
+ </div>
100
+ </div>
101
+ </div>
102
+ </template>
@@ -0,0 +1,110 @@
1
+ <script setup>
2
+ import { useForm } from '@inertiajs/vue3';
3
+ import { ref } from 'vue';
4
+
5
+ const props = defineProps({
6
+ platform: String,
7
+ display_name: String,
8
+ entity_types: { type: Array, default: () => [] },
9
+ mappings: { type: Object, default: () => ({}) },
10
+ credentials: { type: Object, default: () => ({}) },
11
+ });
12
+
13
+ const activeEntity = ref(props.entity_types[0] || '');
14
+ const fieldMappings = ref({});
15
+ const skippedEntities = ref({});
16
+
17
+ // Initialize mappings from defaults
18
+ props.entity_types.forEach((type) => {
19
+ fieldMappings.value[type] = { ...(props.mappings[type]?.defaults || {}) };
20
+ skippedEntities.value[type] = false;
21
+ });
22
+
23
+ const form = useForm({
24
+ credentials: props.credentials,
25
+ field_mappings: {},
26
+ entity_types: [],
27
+ });
28
+
29
+ function proceed() {
30
+ form.field_mappings = fieldMappings.value;
31
+ form.entity_types = props.entity_types.filter((t) => !skippedEntities.value[t]);
32
+ form.post(route('escalated.admin.import.review', props.platform));
33
+ }
34
+ </script>
35
+
36
+ <template>
37
+ <div class="space-y-6">
38
+ <div>
39
+ <h2 class="text-lg font-semibold text-[var(--esc-panel-text)]">Field Mapping</h2>
40
+ <p class="mt-1 text-sm text-[var(--esc-panel-text-secondary)]">
41
+ Configure how {{ display_name }} fields map to Escalated. Defaults are pre-filled.
42
+ </p>
43
+ </div>
44
+
45
+ <!-- Entity type tabs -->
46
+ <div
47
+ class="flex gap-1 rounded-lg border border-[var(--esc-panel-border)] bg-[var(--esc-panel-surface-alt)] p-1"
48
+ >
49
+ <button
50
+ v-for="type in entity_types"
51
+ :key="type"
52
+ :class="[
53
+ 'rounded-md px-3 py-1.5 text-sm font-medium transition',
54
+ activeEntity === type
55
+ ? 'bg-[var(--esc-panel-surface)] text-[var(--esc-panel-text)]'
56
+ : 'text-[var(--esc-panel-text-secondary)] hover:text-[var(--esc-panel-text)]',
57
+ ]"
58
+ @click="activeEntity = type"
59
+ >
60
+ {{ type }}
61
+ </button>
62
+ </div>
63
+
64
+ <!-- Active entity mapping -->
65
+ <div v-for="type in entity_types" v-show="activeEntity === type" :key="type">
66
+ <div class="rounded-xl border border-[var(--esc-panel-border)] bg-[var(--esc-panel-surface)] p-6">
67
+ <!-- Skip toggle -->
68
+ <label class="mb-4 flex items-center gap-2">
69
+ <input
70
+ v-model="skippedEntities[type]"
71
+ type="checkbox"
72
+ class="rounded border-[var(--esc-panel-border)]"
73
+ />
74
+ <span class="text-sm text-[var(--esc-panel-text-secondary)]">Skip importing {{ type }}</span>
75
+ </label>
76
+
77
+ <div v-if="!skippedEntities[type]" class="space-y-3">
78
+ <div
79
+ v-for="(sourceFields, idx) in mappings[type]?.source_fields || []"
80
+ :key="idx"
81
+ class="flex items-center gap-4"
82
+ >
83
+ <span class="w-1/3 text-sm text-[var(--esc-panel-text)]">{{
84
+ sourceFields.label || sourceFields.name
85
+ }}</span>
86
+ <span class="text-[var(--esc-panel-text-muted)]">&rarr;</span>
87
+ <select
88
+ v-model="fieldMappings[type][sourceFields.name]"
89
+ class="w-1/3 rounded-lg border border-[var(--esc-panel-border-input)] bg-[var(--esc-panel-surface-alt)] px-3 py-2 text-sm text-[var(--esc-panel-text-secondary)]"
90
+ >
91
+ <option value="">Skip field</option>
92
+ <option v-for="opt in sourceFields.escalated_options || []" :key="opt" :value="opt">
93
+ {{ opt }}
94
+ </option>
95
+ </select>
96
+ </div>
97
+ </div>
98
+ </div>
99
+ </div>
100
+
101
+ <div class="flex justify-end">
102
+ <button
103
+ class="rounded-lg bg-gradient-to-r from-[var(--esc-panel-accent)] to-[var(--esc-panel-accent-secondary)] px-6 py-2 text-sm font-medium text-white"
104
+ @click="proceed"
105
+ >
106
+ Next: Review
107
+ </button>
108
+ </div>
109
+ </div>
110
+ </template>
@@ -0,0 +1,153 @@
1
+ <script setup>
2
+ import { ref, onMounted, onUnmounted, computed } from 'vue';
3
+ import { router } from '@inertiajs/vue3';
4
+
5
+ const props = defineProps({
6
+ job: { type: Object, required: true },
7
+ });
8
+
9
+ const jobData = ref({ ...props.job });
10
+ const polling = ref(null);
11
+
12
+ const isActive = computed(() => ['importing', 'authenticating', 'mapping'].includes(jobData.value.status));
13
+ const isFinished = computed(() => ['completed', 'failed'].includes(jobData.value.status));
14
+
15
+ async function pollStatus() {
16
+ try {
17
+ const response = await fetch(route('escalated.admin.import.status', jobData.value.id));
18
+ const data = await response.json();
19
+ Object.assign(jobData.value, data);
20
+
21
+ if (isFinished.value && polling.value) {
22
+ clearInterval(polling.value);
23
+ polling.value = null;
24
+ }
25
+ } catch (e) {
26
+ // Silently retry on next interval
27
+ }
28
+ }
29
+
30
+ onMounted(() => {
31
+ if (isActive.value) {
32
+ polling.value = setInterval(pollStatus, 3000);
33
+ }
34
+ });
35
+
36
+ onUnmounted(() => {
37
+ if (polling.value) clearInterval(polling.value);
38
+ });
39
+
40
+ const progressEntries = computed(() => {
41
+ if (!jobData.value.progress) return [];
42
+ return Object.entries(jobData.value.progress).map(([type, data]) => ({
43
+ type,
44
+ ...data,
45
+ percentage: data.total > 0 ? Math.round((data.processed / data.total) * 100) : 0,
46
+ }));
47
+ });
48
+
49
+ const recentErrors = computed(() => {
50
+ return (jobData.value.error_log || []).slice(-20);
51
+ });
52
+
53
+ function pause() {
54
+ router.post(route('escalated.admin.import.pause', jobData.value.id));
55
+ }
56
+
57
+ function resume() {
58
+ router.post(route('escalated.admin.import.resume', jobData.value.id));
59
+ }
60
+
61
+ function cancel() {
62
+ if (confirm('Are you sure you want to cancel this import?')) {
63
+ router.post(route('escalated.admin.import.cancel', jobData.value.id));
64
+ }
65
+ }
66
+ </script>
67
+
68
+ <template>
69
+ <div class="space-y-6">
70
+ <div class="flex items-center justify-between">
71
+ <div>
72
+ <h2 class="text-lg font-semibold text-[var(--esc-panel-text)]">
73
+ Import Progress
74
+ <span
75
+ class="ml-2 text-sm font-normal capitalize"
76
+ :class="{
77
+ 'text-blue-400': isActive,
78
+ 'text-green-400': jobData.status === 'completed',
79
+ 'text-red-400': jobData.status === 'failed',
80
+ 'text-yellow-400': jobData.status === 'paused',
81
+ }"
82
+ >{{ jobData.status }}</span
83
+ >
84
+ </h2>
85
+ <p class="mt-1 text-sm text-[var(--esc-panel-text-secondary)] capitalize">{{ jobData.platform }}</p>
86
+ </div>
87
+
88
+ <div class="flex gap-2">
89
+ <button
90
+ v-if="jobData.status === 'importing'"
91
+ class="rounded-lg border border-yellow-500/50 px-4 py-2 text-sm font-medium text-yellow-400 hover:bg-yellow-500/10"
92
+ @click="pause"
93
+ >
94
+ Pause
95
+ </button>
96
+ <button
97
+ v-if="jobData.status === 'paused'"
98
+ class="rounded-lg border border-blue-500/50 px-4 py-2 text-sm font-medium text-blue-400 hover:bg-blue-500/10"
99
+ @click="resume"
100
+ >
101
+ Resume
102
+ </button>
103
+ <button
104
+ v-if="isActive || jobData.status === 'paused'"
105
+ class="rounded-lg border border-red-500/50 px-4 py-2 text-sm font-medium text-red-400 hover:bg-red-500/10"
106
+ @click="cancel"
107
+ >
108
+ Cancel
109
+ </button>
110
+ </div>
111
+ </div>
112
+
113
+ <!-- Progress bars per entity type -->
114
+ <div class="space-y-4">
115
+ <div
116
+ v-for="entry in progressEntries"
117
+ :key="entry.type"
118
+ class="rounded-xl border border-[var(--esc-panel-border)] bg-[var(--esc-panel-surface)] p-4"
119
+ >
120
+ <div class="mb-2 flex items-center justify-between text-sm">
121
+ <span class="font-medium capitalize text-[var(--esc-panel-text)]">{{ entry.type }}</span>
122
+ <span class="text-[var(--esc-panel-text-secondary)]">
123
+ {{ entry.processed }} / {{ entry.total || '?' }}
124
+ <span v-if="entry.skipped" class="ml-2 text-yellow-400">{{ entry.skipped }} skipped</span>
125
+ <span v-if="entry.failed" class="ml-2 text-red-400">{{ entry.failed }} failed</span>
126
+ </span>
127
+ </div>
128
+ <div class="h-2 overflow-hidden rounded-full bg-[var(--esc-panel-surface-alt)]">
129
+ <div
130
+ class="h-full rounded-full bg-gradient-to-r from-[var(--esc-panel-accent)] to-[var(--esc-panel-accent-secondary)] transition-all duration-500"
131
+ :style="{ width: entry.percentage + '%' }"
132
+ ></div>
133
+ </div>
134
+ </div>
135
+ </div>
136
+
137
+ <!-- Error log -->
138
+ <div v-if="recentErrors.length" class="rounded-xl border border-red-500/20 bg-[var(--esc-panel-surface)] p-4">
139
+ <h3 class="mb-3 text-sm font-semibold text-red-400">Recent Errors</h3>
140
+ <div class="max-h-60 space-y-2 overflow-y-auto">
141
+ <div
142
+ v-for="(error, idx) in recentErrors"
143
+ :key="idx"
144
+ class="rounded-lg bg-[var(--esc-panel-surface-alt)] p-3 text-xs"
145
+ >
146
+ <span class="font-medium capitalize text-[var(--esc-panel-text)]">{{ error.entity_type }}</span>
147
+ <span class="mx-1 text-[var(--esc-panel-text-muted)]">#{{ error.source_id }}</span>
148
+ <span class="text-red-400">{{ error.error }}</span>
149
+ </div>
150
+ </div>
151
+ </div>
152
+ </div>
153
+ </template>
@@ -0,0 +1,76 @@
1
+ <script setup>
2
+ import { ref } from 'vue';
3
+ import { useForm } from '@inertiajs/vue3';
4
+
5
+ const props = defineProps({
6
+ platform: String,
7
+ field_mappings: { type: Object, default: () => ({}) },
8
+ entity_types: { type: Array, default: () => [] },
9
+ });
10
+
11
+ const form = useForm({
12
+ field_mappings: props.field_mappings,
13
+ });
14
+
15
+ const submitting = ref(false);
16
+
17
+ function startImport() {
18
+ submitting.value = true;
19
+ form.post(route('escalated.admin.import.start', props.platform));
20
+ }
21
+ </script>
22
+
23
+ <template>
24
+ <div class="mx-auto max-w-lg space-y-6">
25
+ <div>
26
+ <h2 class="text-lg font-semibold text-[var(--esc-panel-text)]">Review Import</h2>
27
+ <p class="mt-1 text-sm text-[var(--esc-panel-text-secondary)]">
28
+ Review your import settings before starting.
29
+ </p>
30
+ </div>
31
+
32
+ <div class="rounded-xl border border-[var(--esc-panel-border)] bg-[var(--esc-panel-surface)] p-6 space-y-4">
33
+ <div>
34
+ <span class="text-sm font-medium text-[var(--esc-panel-text)]">Platform:</span>
35
+ <span class="ml-2 text-sm text-[var(--esc-panel-text-secondary)] capitalize">{{ platform }}</span>
36
+ </div>
37
+
38
+ <div>
39
+ <span class="text-sm font-medium text-[var(--esc-panel-text)]">Entity types to import:</span>
40
+ <ul class="mt-1 ml-4 list-disc text-sm text-[var(--esc-panel-text-secondary)]">
41
+ <li v-for="type in entity_types" :key="type" class="capitalize">{{ type }}</li>
42
+ </ul>
43
+ </div>
44
+
45
+ <div>
46
+ <span class="text-sm font-medium text-[var(--esc-panel-text)]">Field mappings configured:</span>
47
+ <span class="ml-2 text-sm text-[var(--esc-panel-text-secondary)]">
48
+ {{ Object.keys(field_mappings).length }} entity types
49
+ </span>
50
+ </div>
51
+ </div>
52
+
53
+ <div class="rounded-xl border border-yellow-500/30 bg-yellow-500/10 p-4">
54
+ <p class="text-sm text-yellow-200">
55
+ This will create records in your Escalated instance. The import is resumable if interrupted.
56
+ Notifications and automations are suppressed during import.
57
+ </p>
58
+ </div>
59
+
60
+ <div class="flex justify-end gap-3">
61
+ <button
62
+ class="rounded-lg border border-[var(--esc-panel-border)] px-4 py-2 text-sm font-medium text-[var(--esc-panel-text)] hover:bg-[var(--esc-panel-hover)]"
63
+ @click="$inertia.visit(route('escalated.admin.import.index'))"
64
+ >
65
+ Cancel
66
+ </button>
67
+ <button
68
+ :disabled="submitting"
69
+ class="rounded-lg bg-gradient-to-r from-[var(--esc-panel-accent)] to-[var(--esc-panel-accent-secondary)] px-6 py-2 text-sm font-medium text-white disabled:opacity-50"
70
+ @click="startImport"
71
+ >
72
+ {{ submitting ? 'Starting...' : 'Start Import' }}
73
+ </button>
74
+ </div>
75
+ </div>
76
+ </template>
@@ -0,0 +1,142 @@
1
+ <script setup>
2
+ import EscalatedLayout from '../../../../components/EscalatedLayout.vue';
3
+ import ArticleEditor from '../../../../components/ArticleEditor.vue';
4
+ import { useForm, Link } from '@inertiajs/vue3';
5
+ import { computed } from 'vue';
6
+
7
+ const props = defineProps({
8
+ article: { type: Object, default: null },
9
+ categories: Array,
10
+ });
11
+
12
+ const isEditing = computed(() => !!props.article);
13
+
14
+ const form = useForm({
15
+ title: props.article?.title || '',
16
+ slug: props.article?.slug || '',
17
+ body: props.article?.body || '',
18
+ status: props.article?.status || 'draft',
19
+ category_id: props.article?.category_id || '',
20
+ });
21
+
22
+ function submit() {
23
+ if (isEditing.value) {
24
+ form.put(route('escalated.admin.kb-articles.update', props.article.id));
25
+ } else {
26
+ form.post(route('escalated.admin.kb-articles.store'));
27
+ }
28
+ }
29
+
30
+ function generateSlug() {
31
+ if (!form.slug && form.title) {
32
+ form.slug = form.title
33
+ .toLowerCase()
34
+ .replace(/[^a-z0-9]+/g, '-')
35
+ .replace(/(^-|-$)/g, '');
36
+ }
37
+ }
38
+ </script>
39
+
40
+ <template>
41
+ <EscalatedLayout :title="isEditing ? 'Edit Article' : 'New Article'">
42
+ <div class="mx-auto max-w-4xl">
43
+ <div class="mb-6">
44
+ <Link
45
+ :href="route('escalated.admin.kb-articles.index')"
46
+ class="text-sm text-[var(--esc-panel-text-muted)] hover:text-[var(--esc-panel-text-secondary)]"
47
+ >
48
+ &larr; Back to Articles
49
+ </Link>
50
+ </div>
51
+
52
+ <form class="space-y-6" @submit.prevent="submit">
53
+ <!-- Title -->
54
+ <div>
55
+ <label class="mb-1.5 block text-sm font-medium text-[var(--esc-panel-text-secondary)]">Title</label>
56
+ <input
57
+ v-model="form.title"
58
+ type="text"
59
+ class="w-full rounded-lg border border-[var(--esc-panel-border)] bg-[var(--esc-panel-hover)] px-3 py-2.5 text-sm text-[var(--esc-panel-text-secondary)] placeholder-[var(--esc-panel-text-muted)] focus:border-cyan-500/40 focus:outline-none focus:ring-1 focus:ring-cyan-500/40"
60
+ placeholder="Article title"
61
+ @blur="generateSlug"
62
+ />
63
+ <p v-if="form.errors.title" class="mt-1 text-xs text-red-400">{{ form.errors.title }}</p>
64
+ </div>
65
+
66
+ <!-- Slug -->
67
+ <div>
68
+ <label class="mb-1.5 block text-sm font-medium text-[var(--esc-panel-text-secondary)]"
69
+ >Slug (SEO URL)</label
70
+ >
71
+ <input
72
+ v-model="form.slug"
73
+ type="text"
74
+ class="w-full rounded-lg border border-[var(--esc-panel-border)] bg-[var(--esc-panel-hover)] px-3 py-2.5 text-sm text-[var(--esc-panel-text-secondary)] placeholder-[var(--esc-panel-text-muted)] focus:border-cyan-500/40 focus:outline-none focus:ring-1 focus:ring-cyan-500/40"
75
+ placeholder="article-slug"
76
+ />
77
+ <p v-if="form.errors.slug" class="mt-1 text-xs text-red-400">{{ form.errors.slug }}</p>
78
+ </div>
79
+
80
+ <div class="grid grid-cols-2 gap-4">
81
+ <!-- Category -->
82
+ <div>
83
+ <label class="mb-1.5 block text-sm font-medium text-[var(--esc-panel-text-secondary)]"
84
+ >Category</label
85
+ >
86
+ <select
87
+ v-model="form.category_id"
88
+ class="w-full rounded-lg border border-[var(--esc-panel-border)] bg-[var(--esc-panel-hover)] px-3 py-2.5 text-sm text-[var(--esc-panel-text-secondary)] focus:border-cyan-500/40 focus:outline-none focus:ring-1 focus:ring-cyan-500/40"
89
+ >
90
+ <option value="">No Category</option>
91
+ <option v-for="cat in categories" :key="cat.id" :value="cat.id">{{ cat.name }}</option>
92
+ </select>
93
+ <p v-if="form.errors.category_id" class="mt-1 text-xs text-red-400">
94
+ {{ form.errors.category_id }}
95
+ </p>
96
+ </div>
97
+
98
+ <!-- Status -->
99
+ <div>
100
+ <label class="mb-1.5 block text-sm font-medium text-[var(--esc-panel-text-secondary)]"
101
+ >Status</label
102
+ >
103
+ <select
104
+ v-model="form.status"
105
+ class="w-full rounded-lg border border-[var(--esc-panel-border)] bg-[var(--esc-panel-hover)] px-3 py-2.5 text-sm text-[var(--esc-panel-text-secondary)] focus:border-cyan-500/40 focus:outline-none focus:ring-1 focus:ring-cyan-500/40"
106
+ >
107
+ <option value="draft">Draft</option>
108
+ <option value="published">Published</option>
109
+ </select>
110
+ <p v-if="form.errors.status" class="mt-1 text-xs text-red-400">{{ form.errors.status }}</p>
111
+ </div>
112
+ </div>
113
+
114
+ <!-- Body -->
115
+ <div>
116
+ <label class="mb-1.5 block text-sm font-medium text-[var(--esc-panel-text-secondary)]"
117
+ >Content</label
118
+ >
119
+ <ArticleEditor v-model="form.body" />
120
+ <p v-if="form.errors.body" class="mt-1 text-xs text-red-400">{{ form.errors.body }}</p>
121
+ </div>
122
+
123
+ <!-- Actions -->
124
+ <div class="flex items-center gap-3 pt-2">
125
+ <button
126
+ type="submit"
127
+ :disabled="form.processing"
128
+ class="rounded-lg bg-gradient-to-r from-[var(--esc-panel-accent)] to-[var(--esc-panel-accent-secondary)] px-5 py-2.5 text-sm font-medium text-white shadow-lg shadow-[var(--esc-panel-bg)]/20 transition-opacity hover:opacity-90 disabled:opacity-50"
129
+ >
130
+ {{ isEditing ? 'Update Article' : 'Create Article' }}
131
+ </button>
132
+ <Link
133
+ :href="route('escalated.admin.kb-articles.index')"
134
+ class="rounded-lg border border-[var(--esc-panel-border)] px-5 py-2.5 text-sm font-medium text-[var(--esc-panel-text-tertiary)] hover:bg-[var(--esc-panel-hover)] hover:text-[var(--esc-panel-text-secondary)]"
135
+ >
136
+ Cancel
137
+ </Link>
138
+ </div>
139
+ </form>
140
+ </div>
141
+ </EscalatedLayout>
142
+ </template>