@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.
- package/README.md +102 -0
- package/package.json +37 -6
- package/src/components/ActivityTimeline.stories.js +55 -0
- package/src/components/ActivityTimeline.vue +83 -66
- package/src/components/AdminDashboard.stories.js +1095 -0
- package/src/components/AgentLoadIndicator.stories.js +37 -0
- package/src/components/AgentLoadIndicator.vue +49 -0
- package/src/components/AgentPerformanceCard.vue +44 -0
- package/src/components/ArticleEditor.vue +82 -0
- package/src/components/AssigneeSelect.vue +37 -23
- package/src/components/AttachmentList.vue +53 -37
- package/src/components/AuditLogEntry.vue +95 -0
- package/src/components/BulkActionBar.vue +99 -30
- package/src/components/CategoryTree.vue +89 -0
- package/src/components/ChartWidget.vue +46 -0
- package/src/components/CollisionWarning.stories.js +30 -0
- package/src/components/CollisionWarning.vue +68 -0
- package/src/components/ConditionalFieldRules.vue +135 -0
- package/src/components/ContextPanel.vue +66 -0
- package/src/components/ContextPanelSection.vue +65 -0
- package/src/components/CustomFieldRenderer.vue +159 -0
- package/src/components/EscalatedLayout.vue +400 -69
- package/src/components/FileDropzone.vue +57 -41
- package/src/components/FollowButton.stories.js +30 -0
- package/src/components/FollowButton.vue +53 -28
- package/src/components/KeyboardShortcutHelp.vue +129 -39
- package/src/components/KnowledgePanel.vue +122 -0
- package/src/components/KpiCard.stories.js +32 -0
- package/src/components/KpiCard.vue +88 -0
- package/src/components/MacroDropdown.vue +95 -35
- package/src/components/PermissionMatrix.vue +107 -0
- package/src/components/PinnedNotes.vue +75 -33
- package/src/components/PresenceIndicator.vue +93 -27
- package/src/components/PriorityBadge.stories.js +33 -0
- package/src/components/PriorityBadge.vue +46 -39
- package/src/components/QuickFilters.vue +65 -49
- package/src/components/ReplyComposer.vue +273 -159
- package/src/components/ReplyThread.vue +78 -23
- package/src/components/SatisfactionRating.vue +98 -46
- package/src/components/ScheduleEditor.vue +104 -0
- package/src/components/SideConversation.vue +165 -0
- package/src/components/SideConversationList.vue +147 -0
- package/src/components/SkillTagManager.vue +79 -0
- package/src/components/SlaComplianceChart.vue +53 -0
- package/src/components/SlaTimer.stories.js +35 -0
- package/src/components/SlaTimer.vue +62 -49
- package/src/components/StatsCard.stories.js +24 -0
- package/src/components/StatsCard.vue +58 -46
- package/src/components/StatusBadge.stories.js +53 -0
- package/src/components/StatusBadge.vue +78 -46
- package/src/components/TagSelect.vue +84 -53
- package/src/components/TicketFilters.vue +225 -69
- package/src/components/TicketLinkPanel.vue +193 -0
- package/src/components/TicketList.vue +594 -193
- package/src/components/TicketMergeDialog.vue +157 -0
- package/src/components/TicketSidebar.vue +167 -99
- package/src/components/TicketTypeSelector.vue +21 -0
- package/src/components/TotpInput.stories.js +9 -0
- package/src/components/TotpInput.vue +81 -0
- package/src/components/TwoFactorSetup.vue +113 -0
- package/src/composables/useDebouncedSearch.js +38 -0
- package/src/composables/useI18n.js +80 -0
- package/src/composables/usePluginExtensions.js +95 -3
- package/src/composables/usePluginHooks.js +65 -0
- package/src/defineEscalatedPlugin.js +34 -8
- package/src/index.js +72 -35
- package/src/locales/de.json +442 -0
- package/src/locales/en.json +467 -0
- package/src/locales/es.json +442 -0
- package/src/locales/fr.json +442 -0
- package/src/locales/index.js +6 -0
- package/src/pages/Admin/ApiTokens/Index.vue +231 -67
- package/src/pages/Admin/AuditLog/Index.vue +154 -0
- package/src/pages/Admin/Automations/Form.vue +184 -0
- package/src/pages/Admin/Automations/Index.vue +129 -0
- package/src/pages/Admin/BusinessHours/Form.vue +172 -0
- package/src/pages/Admin/BusinessHours/Index.vue +113 -0
- package/src/pages/Admin/CannedResponses/Index.vue +127 -70
- package/src/pages/Admin/Capacity/Index.vue +134 -0
- package/src/pages/Admin/CustomFields/Form.vue +195 -0
- package/src/pages/Admin/CustomFields/Index.vue +134 -0
- package/src/pages/Admin/CustomObjects/Form.vue +202 -0
- package/src/pages/Admin/CustomObjects/Index.vue +127 -0
- package/src/pages/Admin/CustomObjects/Records.vue +228 -0
- package/src/pages/Admin/Departments/Form.vue +75 -51
- package/src/pages/Admin/Departments/Index.vue +109 -57
- package/src/pages/Admin/EscalationRules/Form.vue +157 -96
- package/src/pages/Admin/EscalationRules/Index.vue +178 -57
- package/src/pages/Admin/Import/Connect.vue +96 -0
- package/src/pages/Admin/Import/Index.vue +102 -0
- package/src/pages/Admin/Import/Mapping.vue +110 -0
- package/src/pages/Admin/Import/Progress.vue +153 -0
- package/src/pages/Admin/Import/Review.vue +76 -0
- package/src/pages/Admin/KnowledgeBase/Articles/Form.vue +142 -0
- package/src/pages/Admin/KnowledgeBase/Articles/Index.vue +195 -0
- package/src/pages/Admin/KnowledgeBase/Categories/Index.vue +193 -0
- package/src/pages/Admin/Macros/Index.vue +250 -89
- package/src/pages/Admin/Plugins/Index.vue +205 -75
- package/src/pages/Admin/Reports/AgentMetrics.vue +115 -0
- package/src/pages/Admin/Reports/CsatReport.vue +127 -0
- package/src/pages/Admin/Reports/Dashboard.vue +211 -0
- package/src/pages/Admin/Reports/SlaReport.vue +166 -0
- package/src/pages/Admin/Reports.vue +139 -34
- package/src/pages/Admin/Roles/Form.vue +71 -0
- package/src/pages/Admin/Roles/Index.vue +122 -0
- package/src/pages/Admin/Settings/CsatSettings.vue +165 -0
- package/src/pages/Admin/Settings/DataRetention.vue +182 -0
- package/src/pages/Admin/Settings/EmailSettings.vue +243 -0
- package/src/pages/Admin/Settings/Sandbox.vue +135 -0
- package/src/pages/Admin/Settings/SsoSettings.vue +218 -0
- package/src/pages/Admin/Settings/TwoFactor.vue +110 -0
- package/src/pages/Admin/Settings.vue +468 -260
- package/src/pages/Admin/Skills/Form.vue +53 -0
- package/src/pages/Admin/Skills/Index.vue +94 -0
- package/src/pages/Admin/SlaPolicies/Form.vue +118 -75
- package/src/pages/Admin/SlaPolicies/Index.vue +120 -59
- package/src/pages/Admin/Statuses/Form.vue +114 -0
- package/src/pages/Admin/Statuses/Index.vue +153 -0
- package/src/pages/Admin/Tags/Index.vue +191 -105
- package/src/pages/Admin/Tickets/Index.vue +91 -39
- package/src/pages/Admin/Tickets/Show.vue +101 -37
- package/src/pages/Admin/Webhooks/DeliveryLog.vue +120 -0
- package/src/pages/Admin/Webhooks/Form.vue +110 -0
- package/src/pages/Admin/Webhooks/Index.vue +136 -0
- package/src/pages/Agent/Dashboard.vue +311 -186
- package/src/pages/Agent/TicketIndex.vue +88 -38
- package/src/pages/Agent/TicketShow.vue +258 -51
- package/src/pages/Customer/Create.vue +36 -9
- package/src/pages/Customer/Index.vue +31 -24
- package/src/pages/Customer/KnowledgeBase/Article.vue +127 -0
- package/src/pages/Customer/KnowledgeBase/Index.vue +151 -0
- package/src/plugin.js +86 -1
- package/src/utils/constants.js +30 -0
- package/src/utils/formatting.js +35 -0
- package/src/utils/sanitizeHtml.js +64 -0
- 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)]">→</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
|
+
← 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>
|