@weaver-apps/service-desk 0.0.0-stage → 0.1.2
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.
|
@@ -0,0 +1,773 @@
|
|
|
1
|
+
import * as i0 from "@angular/core";
|
|
2
|
+
import { CUSTOM_ELEMENTS_SCHEMA, ChangeDetectionStrategy, Component, DestroyRef, InjectionToken, computed, effect, inject, input, output, resource, signal } from "@angular/core";
|
|
3
|
+
import { TranslocoPipe, TranslocoService } from "@jsverse/transloco";
|
|
4
|
+
import { toSignal } from "@angular/core/rxjs-interop";
|
|
5
|
+
import { ActivatedRoute } from "@angular/router";
|
|
6
|
+
import { map } from "rxjs";
|
|
7
|
+
import { DialogRef } from "@loomweaver/plugin-sdk";
|
|
8
|
+
var ServiceDeskGatewayError = class extends Error {
|
|
9
|
+
status;
|
|
10
|
+
constructor(status) {
|
|
11
|
+
super(`The service desk answered ${status}.`);
|
|
12
|
+
this.status = status;
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
const SERVICE_DESK_GATEWAY = new InjectionToken("weaver-apps.service-desk.gateway");
|
|
16
|
+
function provideServiceDeskGateway(gateway) {
|
|
17
|
+
return {
|
|
18
|
+
provide: SERVICE_DESK_GATEWAY,
|
|
19
|
+
useValue: gateway
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
var ServiceDeskHost = class {
|
|
23
|
+
ctx;
|
|
24
|
+
bind(ctx) {
|
|
25
|
+
this.ctx = ctx;
|
|
26
|
+
}
|
|
27
|
+
unbind() {
|
|
28
|
+
this.ctx = void 0;
|
|
29
|
+
}
|
|
30
|
+
get ui() {
|
|
31
|
+
if (!this.ctx) throw new Error("The service-desk plugin is not active.");
|
|
32
|
+
return this.ctx.ui;
|
|
33
|
+
}
|
|
34
|
+
navigate(path) {
|
|
35
|
+
this.ctx?.navigateContent(path);
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
const serviceDeskHost = new ServiceDeskHost();
|
|
39
|
+
const deskListPath = "service-desk";
|
|
40
|
+
function deskPath(deskId) {
|
|
41
|
+
return `${deskListPath}/${deskId}`;
|
|
42
|
+
}
|
|
43
|
+
var DeskListPage = class DeskListPage {
|
|
44
|
+
gateway = inject(SERVICE_DESK_GATEWAY);
|
|
45
|
+
desks = resource({
|
|
46
|
+
...ngDevMode ? { debugName: "desks" } : /* istanbul ignore next */ {},
|
|
47
|
+
loader: () => this.gateway.desks()
|
|
48
|
+
});
|
|
49
|
+
open(deskId) {
|
|
50
|
+
serviceDeskHost.navigate(deskPath(deskId));
|
|
51
|
+
}
|
|
52
|
+
async create() {
|
|
53
|
+
const name = await serviceDeskHost.ui.prompt({
|
|
54
|
+
title: "service-desk.desks.create",
|
|
55
|
+
message: "service-desk.desks.createMessage",
|
|
56
|
+
confirmLabel: "service-desk.desks.createConfirm"
|
|
57
|
+
});
|
|
58
|
+
if (!name?.trim()) return;
|
|
59
|
+
try {
|
|
60
|
+
const desk = await this.gateway.saveDesk(crypto.randomUUID(), name.trim());
|
|
61
|
+
this.open(desk.id);
|
|
62
|
+
} catch {
|
|
63
|
+
serviceDeskHost.ui.toast({
|
|
64
|
+
message: "service-desk.errors.create",
|
|
65
|
+
kind: "error"
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
static ɵfac = i0.ɵɵngDeclareFactory({
|
|
70
|
+
minVersion: "12.0.0",
|
|
71
|
+
version: "22.2.3",
|
|
72
|
+
ngImport: i0,
|
|
73
|
+
type: DeskListPage,
|
|
74
|
+
deps: [],
|
|
75
|
+
target: i0.ɵɵFactoryTarget.Component
|
|
76
|
+
});
|
|
77
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({
|
|
78
|
+
minVersion: "17.0.0",
|
|
79
|
+
version: "22.2.3",
|
|
80
|
+
type: DeskListPage,
|
|
81
|
+
isStandalone: true,
|
|
82
|
+
selector: "wa-desk-list-page",
|
|
83
|
+
ngImport: i0,
|
|
84
|
+
template: "<div class=\"flex h-full flex-col gap-4 overflow-auto p-6\">\n <div class=\"flex items-center justify-between gap-4\">\n <h2 class=\"text-lg font-semibold text-content\">{{ 'service-desk.desks.title' | transloco }}</h2>\n <button type=\"button\" class=\"lw-btn lw-btn--primary\" (click)=\"create()\">{{ 'service-desk.desks.create' | transloco }}</button>\n </div>\n @if (desks.error()) {\n <p class=\"text-sm text-negative\" role=\"alert\">{{ 'service-desk.errors.load' | transloco }}</p>\n } @else if (desks.hasValue()) {\n <ul role=\"list\" class=\"divide-y divide-border rounded-xl border border-border bg-surface\">\n @for (desk of desks.value(); track desk.id) {\n <li>\n <button type=\"button\" class=\"flex w-full items-center gap-3 px-4 py-3 text-left text-sm text-content hover:bg-surface-overlay\" (click)=\"open(desk.id)\">\n <lw-icon name=\"serviceDesk\" size=\"1.25rem\"></lw-icon>\n <span>{{ desk.name }}</span>\n </button>\n </li>\n } @empty {\n <li class=\"px-4 py-8 text-center text-sm text-content-faint\">{{ 'service-desk.desks.empty' | transloco }}</li>\n }\n </ul>\n }\n</div>\n",
|
|
85
|
+
dependencies: [{
|
|
86
|
+
kind: "pipe",
|
|
87
|
+
type: TranslocoPipe,
|
|
88
|
+
name: "transloco"
|
|
89
|
+
}],
|
|
90
|
+
changeDetection: i0.ChangeDetectionStrategy.OnPush
|
|
91
|
+
});
|
|
92
|
+
};
|
|
93
|
+
i0.ɵɵngDeclareClassMetadata({
|
|
94
|
+
minVersion: "12.0.0",
|
|
95
|
+
version: "22.2.3",
|
|
96
|
+
ngImport: i0,
|
|
97
|
+
type: DeskListPage,
|
|
98
|
+
decorators: [{
|
|
99
|
+
type: Component,
|
|
100
|
+
args: [{
|
|
101
|
+
selector: "wa-desk-list-page",
|
|
102
|
+
imports: [TranslocoPipe],
|
|
103
|
+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
|
104
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
105
|
+
template: "<div class=\"flex h-full flex-col gap-4 overflow-auto p-6\">\n <div class=\"flex items-center justify-between gap-4\">\n <h2 class=\"text-lg font-semibold text-content\">{{ 'service-desk.desks.title' | transloco }}</h2>\n <button type=\"button\" class=\"lw-btn lw-btn--primary\" (click)=\"create()\">{{ 'service-desk.desks.create' | transloco }}</button>\n </div>\n @if (desks.error()) {\n <p class=\"text-sm text-negative\" role=\"alert\">{{ 'service-desk.errors.load' | transloco }}</p>\n } @else if (desks.hasValue()) {\n <ul role=\"list\" class=\"divide-y divide-border rounded-xl border border-border bg-surface\">\n @for (desk of desks.value(); track desk.id) {\n <li>\n <button type=\"button\" class=\"flex w-full items-center gap-3 px-4 py-3 text-left text-sm text-content hover:bg-surface-overlay\" (click)=\"open(desk.id)\">\n <lw-icon name=\"serviceDesk\" size=\"1.25rem\"></lw-icon>\n <span>{{ desk.name }}</span>\n </button>\n </li>\n } @empty {\n <li class=\"px-4 py-8 text-center text-sm text-content-faint\">{{ 'service-desk.desks.empty' | transloco }}</li>\n }\n </ul>\n }\n</div>\n"
|
|
106
|
+
}]
|
|
107
|
+
}]
|
|
108
|
+
});
|
|
109
|
+
function activeLocale() {
|
|
110
|
+
const transloco = inject(TranslocoService);
|
|
111
|
+
return toSignal(transloco.langChanges$, { initialValue: transloco.getActiveLang() });
|
|
112
|
+
}
|
|
113
|
+
const ServiceRequestOrigin = {
|
|
114
|
+
manual: "manual",
|
|
115
|
+
voiceChat: "voiceChat",
|
|
116
|
+
textChat: "textChat",
|
|
117
|
+
widget: "widget",
|
|
118
|
+
email: "email",
|
|
119
|
+
phone: "phone"
|
|
120
|
+
};
|
|
121
|
+
const ServiceRequestPriority = {
|
|
122
|
+
low: "low",
|
|
123
|
+
normal: "normal",
|
|
124
|
+
high: "high",
|
|
125
|
+
urgent: "urgent"
|
|
126
|
+
};
|
|
127
|
+
const ServiceRequestStatus = {
|
|
128
|
+
open: "open",
|
|
129
|
+
inProgress: "inProgress",
|
|
130
|
+
completed: "completed",
|
|
131
|
+
cancelled: "cancelled"
|
|
132
|
+
};
|
|
133
|
+
const ServiceRequestType = {
|
|
134
|
+
inquiry: "inquiry",
|
|
135
|
+
task: "task",
|
|
136
|
+
callbackRequest: "callbackRequest",
|
|
137
|
+
conversationSummary: "conversationSummary",
|
|
138
|
+
complaint: "complaint",
|
|
139
|
+
quote: "quote"
|
|
140
|
+
};
|
|
141
|
+
const requestTypes = Object.values(ServiceRequestType);
|
|
142
|
+
const requestPriorities = Object.values(ServiceRequestPriority);
|
|
143
|
+
const requestOrigins = Object.values(ServiceRequestOrigin);
|
|
144
|
+
const settableStatuses = [ServiceRequestStatus.open, ServiceRequestStatus.inProgress];
|
|
145
|
+
const statusFilters = [
|
|
146
|
+
"active",
|
|
147
|
+
"open",
|
|
148
|
+
"inProgress",
|
|
149
|
+
"completed",
|
|
150
|
+
"cancelled",
|
|
151
|
+
"all"
|
|
152
|
+
];
|
|
153
|
+
function isClosed(status) {
|
|
154
|
+
return status === ServiceRequestStatus.completed || status === ServiceRequestStatus.cancelled;
|
|
155
|
+
}
|
|
156
|
+
function localDate(value) {
|
|
157
|
+
const pad = (part) => `${part}`.padStart(2, "0");
|
|
158
|
+
return `${value.getFullYear()}-${pad(value.getMonth() + 1)}-${pad(value.getDate())}`;
|
|
159
|
+
}
|
|
160
|
+
function pick(choices, value) {
|
|
161
|
+
return choices.find((choice) => choice === value);
|
|
162
|
+
}
|
|
163
|
+
var RequestDialog = class RequestDialog {
|
|
164
|
+
dialog = inject(DialogRef);
|
|
165
|
+
gateway = inject(SERVICE_DESK_GATEWAY);
|
|
166
|
+
data = this.dialog.data;
|
|
167
|
+
existing = this.data.request;
|
|
168
|
+
editing = this.existing !== null;
|
|
169
|
+
closed = this.existing !== null && isClosed(this.existing.status);
|
|
170
|
+
types = requestTypes;
|
|
171
|
+
priorities = requestPriorities;
|
|
172
|
+
origins = requestOrigins;
|
|
173
|
+
statuses = settableStatuses;
|
|
174
|
+
title = signal(this.existing?.title ?? "", ...ngDevMode ? [{ debugName: "title" }] : /* istanbul ignore next */ []);
|
|
175
|
+
type = signal(this.existing?.type ?? ServiceRequestType.inquiry, ...ngDevMode ? [{ debugName: "type" }] : /* istanbul ignore next */ []);
|
|
176
|
+
priority = signal(this.existing?.priority ?? ServiceRequestPriority.normal, ...ngDevMode ? [{ debugName: "priority" }] : /* istanbul ignore next */ []);
|
|
177
|
+
origin = signal(this.existing?.origin ?? ServiceRequestOrigin.manual, ...ngDevMode ? [{ debugName: "origin" }] : /* istanbul ignore next */ []);
|
|
178
|
+
status = signal(this.existing?.status ?? "open", ...ngDevMode ? [{ debugName: "status" }] : /* istanbul ignore next */ []);
|
|
179
|
+
contactName = signal(this.existing?.contactName ?? "", ...ngDevMode ? [{ debugName: "contactName" }] : /* istanbul ignore next */ []);
|
|
180
|
+
contactPhone = signal(this.existing?.contactPhone ?? "", ...ngDevMode ? [{ debugName: "contactPhone" }] : /* istanbul ignore next */ []);
|
|
181
|
+
contactEmail = signal(this.existing?.contactEmail ?? "", ...ngDevMode ? [{ debugName: "contactEmail" }] : /* istanbul ignore next */ []);
|
|
182
|
+
assignee = signal(this.existing?.assignee ?? "", ...ngDevMode ? [{ debugName: "assignee" }] : /* istanbul ignore next */ []);
|
|
183
|
+
dueDate = signal(this.existing?.dueAt ? localDate(new Date(this.existing.dueAt)) : "", ...ngDevMode ? [{ debugName: "dueDate" }] : /* istanbul ignore next */ []);
|
|
184
|
+
description = signal(this.existing?.description ?? "", ...ngDevMode ? [{ debugName: "description" }] : /* istanbul ignore next */ []);
|
|
185
|
+
cancellationReason = this.existing?.cancellationReason ?? null;
|
|
186
|
+
saving = signal(false, ...ngDevMode ? [{ debugName: "saving" }] : /* istanbul ignore next */ []);
|
|
187
|
+
problem = signal("", ...ngDevMode ? [{ debugName: "problem" }] : /* istanbul ignore next */ []);
|
|
188
|
+
persisted = false;
|
|
189
|
+
bind(target, event) {
|
|
190
|
+
target.set(event.target.value);
|
|
191
|
+
}
|
|
192
|
+
chooseType(event) {
|
|
193
|
+
this.choose(this.type, requestTypes, event);
|
|
194
|
+
}
|
|
195
|
+
choosePriority(event) {
|
|
196
|
+
this.choose(this.priority, requestPriorities, event);
|
|
197
|
+
}
|
|
198
|
+
chooseOrigin(event) {
|
|
199
|
+
this.choose(this.origin, requestOrigins, event);
|
|
200
|
+
}
|
|
201
|
+
chooseStatus(event) {
|
|
202
|
+
this.choose(this.status, settableStatuses, event);
|
|
203
|
+
}
|
|
204
|
+
close() {
|
|
205
|
+
this.dialog.close(this.persisted);
|
|
206
|
+
}
|
|
207
|
+
async save() {
|
|
208
|
+
if (!this.title().trim()) {
|
|
209
|
+
this.problem.set("service-desk.dialog.titleRequired");
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
this.saving.set(true);
|
|
213
|
+
try {
|
|
214
|
+
const changes = this.changes();
|
|
215
|
+
const outcome = this.existing ? await this.gateway.edit(this.data.deskId, this.existing.id, changes) : await this.gateway.raise(this.data.deskId, {
|
|
216
|
+
...changes,
|
|
217
|
+
origin: this.origin()
|
|
218
|
+
});
|
|
219
|
+
this.settle(await this.applyStatus(outcome));
|
|
220
|
+
} catch {
|
|
221
|
+
this.problem.set("service-desk.errors.save");
|
|
222
|
+
} finally {
|
|
223
|
+
this.saving.set(false);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
changes() {
|
|
227
|
+
const keptDue = this.existing?.dueAt && localDate(new Date(this.existing.dueAt)) === this.dueDate() ? this.existing.dueAt : null;
|
|
228
|
+
if (keptDue) return {
|
|
229
|
+
...this.fields(),
|
|
230
|
+
dueAt: keptDue
|
|
231
|
+
};
|
|
232
|
+
const due = this.dueDate() ? /* @__PURE__ */ new Date(`${this.dueDate()}T12:00`) : null;
|
|
233
|
+
return {
|
|
234
|
+
...this.fields(),
|
|
235
|
+
dueAt: due && !Number.isNaN(due.getTime()) ? due.toISOString() : null
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
fields() {
|
|
239
|
+
return {
|
|
240
|
+
title: this.title().trim(),
|
|
241
|
+
description: this.optional(this.description()),
|
|
242
|
+
type: this.type(),
|
|
243
|
+
priority: this.priority(),
|
|
244
|
+
contactName: this.optional(this.contactName()),
|
|
245
|
+
contactPhone: this.optional(this.contactPhone()),
|
|
246
|
+
contactEmail: this.optional(this.contactEmail()),
|
|
247
|
+
assignee: this.optional(this.assignee())
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
async applyStatus(outcome) {
|
|
251
|
+
if (outcome.kind === "saved") {
|
|
252
|
+
this.existing = outcome.request;
|
|
253
|
+
this.persisted = true;
|
|
254
|
+
}
|
|
255
|
+
if (outcome.kind !== "saved" || outcome.request.status === this.status()) return outcome;
|
|
256
|
+
return this.gateway.setStatus(this.data.deskId, outcome.request.id, this.status());
|
|
257
|
+
}
|
|
258
|
+
settle(outcome) {
|
|
259
|
+
const messages = {
|
|
260
|
+
closed: "service-desk.dialog.closed",
|
|
261
|
+
changed: "service-desk.dialog.changed",
|
|
262
|
+
invalid: "service-desk.dialog.invalid",
|
|
263
|
+
missing: "service-desk.dialog.missing"
|
|
264
|
+
};
|
|
265
|
+
if (outcome.kind === "saved") {
|
|
266
|
+
this.dialog.close(true);
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
this.problem.set(messages[outcome.kind]);
|
|
270
|
+
}
|
|
271
|
+
choose(target, choices, event) {
|
|
272
|
+
const chosen = pick(choices, event.target.value);
|
|
273
|
+
if (chosen) target.set(chosen);
|
|
274
|
+
}
|
|
275
|
+
optional(value) {
|
|
276
|
+
return value.trim() ? value.trim() : null;
|
|
277
|
+
}
|
|
278
|
+
static ɵfac = i0.ɵɵngDeclareFactory({
|
|
279
|
+
minVersion: "12.0.0",
|
|
280
|
+
version: "22.2.3",
|
|
281
|
+
ngImport: i0,
|
|
282
|
+
type: RequestDialog,
|
|
283
|
+
deps: [],
|
|
284
|
+
target: i0.ɵɵFactoryTarget.Component
|
|
285
|
+
});
|
|
286
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({
|
|
287
|
+
minVersion: "17.0.0",
|
|
288
|
+
version: "22.2.3",
|
|
289
|
+
type: RequestDialog,
|
|
290
|
+
isStandalone: true,
|
|
291
|
+
selector: "wa-request-dialog",
|
|
292
|
+
ngImport: i0,
|
|
293
|
+
template: "<form class=\"flex flex-col gap-4\" (submit)=\"$event.preventDefault(); save()\">\n @if (closed) {\n <p class=\"text-sm text-content-faint\" role=\"status\">\n {{ 'service-desk.dialog.closedNote' | transloco }}\n @if (cancellationReason) {\n {{ 'service-desk.dialog.reason' | transloco: { reason: cancellationReason } }}\n }\n </p>\n }\n\n <label class=\"flex flex-col gap-1 text-sm text-content\">\n {{ 'service-desk.fields.title' | transloco }}\n <input class=\"lw-field\" required [disabled]=\"closed\" [value]=\"title()\" (input)=\"bind(title, $event)\" />\n </label>\n\n <div class=\"grid grid-cols-1 gap-4 sm:grid-cols-3\">\n <label class=\"flex flex-col gap-1 text-sm text-content\">\n {{ 'service-desk.fields.type' | transloco }}\n <select class=\"lw-field\" [disabled]=\"closed\" (change)=\"chooseType($event)\">\n @for (choice of types; track choice) {\n <option [value]=\"choice\" [selected]=\"choice === type()\">{{ 'service-desk.type.' + choice | transloco }}</option>\n }\n </select>\n </label>\n <label class=\"flex flex-col gap-1 text-sm text-content\">\n {{ 'service-desk.fields.priority' | transloco }}\n <select class=\"lw-field\" [disabled]=\"closed\" (change)=\"choosePriority($event)\">\n @for (choice of priorities; track choice) {\n <option [value]=\"choice\" [selected]=\"choice === priority()\">{{ 'service-desk.priority.' + choice | transloco }}</option>\n }\n </select>\n </label>\n @if (editing) {\n <label class=\"flex flex-col gap-1 text-sm text-content\">\n {{ 'service-desk.fields.status' | transloco }}\n <select class=\"lw-field\" [disabled]=\"closed\" (change)=\"chooseStatus($event)\">\n @if (closed) {\n <option selected>{{ 'service-desk.status.' + status() | transloco }}</option>\n } @else {\n @for (choice of statuses; track choice) {\n <option [value]=\"choice\" [selected]=\"choice === status()\">{{ 'service-desk.status.' + choice | transloco }}</option>\n }\n }\n </select>\n </label>\n } @else {\n <label class=\"flex flex-col gap-1 text-sm text-content\">\n {{ 'service-desk.fields.origin' | transloco }}\n <select class=\"lw-field\" (change)=\"chooseOrigin($event)\">\n @for (choice of origins; track choice) {\n <option [value]=\"choice\" [selected]=\"choice === origin()\">{{ 'service-desk.origin.' + choice | transloco }}</option>\n }\n </select>\n </label>\n }\n </div>\n\n <div class=\"grid grid-cols-1 gap-4 sm:grid-cols-3\">\n <label class=\"flex flex-col gap-1 text-sm text-content\">\n {{ 'service-desk.fields.contactName' | transloco }}\n <input class=\"lw-field\" [disabled]=\"closed\" [value]=\"contactName()\" (input)=\"bind(contactName, $event)\" />\n </label>\n <label class=\"flex flex-col gap-1 text-sm text-content\">\n {{ 'service-desk.fields.contactPhone' | transloco }}\n <input class=\"lw-field\" type=\"tel\" [disabled]=\"closed\" [value]=\"contactPhone()\" (input)=\"bind(contactPhone, $event)\" />\n </label>\n <label class=\"flex flex-col gap-1 text-sm text-content\">\n {{ 'service-desk.fields.contactEmail' | transloco }}\n <input class=\"lw-field\" type=\"email\" [disabled]=\"closed\" [value]=\"contactEmail()\" (input)=\"bind(contactEmail, $event)\" />\n </label>\n </div>\n\n <div class=\"grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <label class=\"flex flex-col gap-1 text-sm text-content\">\n {{ 'service-desk.fields.assignee' | transloco }}\n <input class=\"lw-field\" [disabled]=\"closed\" [value]=\"assignee()\" (input)=\"bind(assignee, $event)\" />\n </label>\n <label class=\"flex flex-col gap-1 text-sm text-content\">\n {{ 'service-desk.fields.due' | transloco }}\n <input class=\"lw-field\" type=\"date\" [disabled]=\"closed\" [value]=\"dueDate()\" (input)=\"bind(dueDate, $event)\" />\n </label>\n </div>\n\n <label class=\"flex flex-col gap-1 text-sm text-content\">\n {{ 'service-desk.fields.description' | transloco }}\n <textarea class=\"lw-field\" rows=\"3\" [disabled]=\"closed\" [value]=\"description()\" (input)=\"bind(description, $event)\"></textarea>\n </label>\n\n @if (problem()) {\n <p class=\"text-sm text-negative\" role=\"alert\">{{ problem() | transloco }}</p>\n }\n\n <div class=\"flex justify-end gap-2\">\n <button type=\"button\" class=\"lw-btn lw-btn--default\" (click)=\"close()\">{{ (closed ? 'service-desk.dialog.close' : 'service-desk.dialog.cancel') | transloco }}</button>\n @if (!closed) {\n <button type=\"submit\" class=\"lw-btn lw-btn--primary\" [disabled]=\"saving()\">{{ 'service-desk.dialog.save' | transloco }}</button>\n }\n </div>\n</form>\n",
|
|
294
|
+
dependencies: [{
|
|
295
|
+
kind: "pipe",
|
|
296
|
+
type: TranslocoPipe,
|
|
297
|
+
name: "transloco"
|
|
298
|
+
}],
|
|
299
|
+
changeDetection: i0.ChangeDetectionStrategy.OnPush
|
|
300
|
+
});
|
|
301
|
+
};
|
|
302
|
+
i0.ɵɵngDeclareClassMetadata({
|
|
303
|
+
minVersion: "12.0.0",
|
|
304
|
+
version: "22.2.3",
|
|
305
|
+
ngImport: i0,
|
|
306
|
+
type: RequestDialog,
|
|
307
|
+
decorators: [{
|
|
308
|
+
type: Component,
|
|
309
|
+
args: [{
|
|
310
|
+
selector: "wa-request-dialog",
|
|
311
|
+
imports: [TranslocoPipe],
|
|
312
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
313
|
+
template: "<form class=\"flex flex-col gap-4\" (submit)=\"$event.preventDefault(); save()\">\n @if (closed) {\n <p class=\"text-sm text-content-faint\" role=\"status\">\n {{ 'service-desk.dialog.closedNote' | transloco }}\n @if (cancellationReason) {\n {{ 'service-desk.dialog.reason' | transloco: { reason: cancellationReason } }}\n }\n </p>\n }\n\n <label class=\"flex flex-col gap-1 text-sm text-content\">\n {{ 'service-desk.fields.title' | transloco }}\n <input class=\"lw-field\" required [disabled]=\"closed\" [value]=\"title()\" (input)=\"bind(title, $event)\" />\n </label>\n\n <div class=\"grid grid-cols-1 gap-4 sm:grid-cols-3\">\n <label class=\"flex flex-col gap-1 text-sm text-content\">\n {{ 'service-desk.fields.type' | transloco }}\n <select class=\"lw-field\" [disabled]=\"closed\" (change)=\"chooseType($event)\">\n @for (choice of types; track choice) {\n <option [value]=\"choice\" [selected]=\"choice === type()\">{{ 'service-desk.type.' + choice | transloco }}</option>\n }\n </select>\n </label>\n <label class=\"flex flex-col gap-1 text-sm text-content\">\n {{ 'service-desk.fields.priority' | transloco }}\n <select class=\"lw-field\" [disabled]=\"closed\" (change)=\"choosePriority($event)\">\n @for (choice of priorities; track choice) {\n <option [value]=\"choice\" [selected]=\"choice === priority()\">{{ 'service-desk.priority.' + choice | transloco }}</option>\n }\n </select>\n </label>\n @if (editing) {\n <label class=\"flex flex-col gap-1 text-sm text-content\">\n {{ 'service-desk.fields.status' | transloco }}\n <select class=\"lw-field\" [disabled]=\"closed\" (change)=\"chooseStatus($event)\">\n @if (closed) {\n <option selected>{{ 'service-desk.status.' + status() | transloco }}</option>\n } @else {\n @for (choice of statuses; track choice) {\n <option [value]=\"choice\" [selected]=\"choice === status()\">{{ 'service-desk.status.' + choice | transloco }}</option>\n }\n }\n </select>\n </label>\n } @else {\n <label class=\"flex flex-col gap-1 text-sm text-content\">\n {{ 'service-desk.fields.origin' | transloco }}\n <select class=\"lw-field\" (change)=\"chooseOrigin($event)\">\n @for (choice of origins; track choice) {\n <option [value]=\"choice\" [selected]=\"choice === origin()\">{{ 'service-desk.origin.' + choice | transloco }}</option>\n }\n </select>\n </label>\n }\n </div>\n\n <div class=\"grid grid-cols-1 gap-4 sm:grid-cols-3\">\n <label class=\"flex flex-col gap-1 text-sm text-content\">\n {{ 'service-desk.fields.contactName' | transloco }}\n <input class=\"lw-field\" [disabled]=\"closed\" [value]=\"contactName()\" (input)=\"bind(contactName, $event)\" />\n </label>\n <label class=\"flex flex-col gap-1 text-sm text-content\">\n {{ 'service-desk.fields.contactPhone' | transloco }}\n <input class=\"lw-field\" type=\"tel\" [disabled]=\"closed\" [value]=\"contactPhone()\" (input)=\"bind(contactPhone, $event)\" />\n </label>\n <label class=\"flex flex-col gap-1 text-sm text-content\">\n {{ 'service-desk.fields.contactEmail' | transloco }}\n <input class=\"lw-field\" type=\"email\" [disabled]=\"closed\" [value]=\"contactEmail()\" (input)=\"bind(contactEmail, $event)\" />\n </label>\n </div>\n\n <div class=\"grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <label class=\"flex flex-col gap-1 text-sm text-content\">\n {{ 'service-desk.fields.assignee' | transloco }}\n <input class=\"lw-field\" [disabled]=\"closed\" [value]=\"assignee()\" (input)=\"bind(assignee, $event)\" />\n </label>\n <label class=\"flex flex-col gap-1 text-sm text-content\">\n {{ 'service-desk.fields.due' | transloco }}\n <input class=\"lw-field\" type=\"date\" [disabled]=\"closed\" [value]=\"dueDate()\" (input)=\"bind(dueDate, $event)\" />\n </label>\n </div>\n\n <label class=\"flex flex-col gap-1 text-sm text-content\">\n {{ 'service-desk.fields.description' | transloco }}\n <textarea class=\"lw-field\" rows=\"3\" [disabled]=\"closed\" [value]=\"description()\" (input)=\"bind(description, $event)\"></textarea>\n </label>\n\n @if (problem()) {\n <p class=\"text-sm text-negative\" role=\"alert\">{{ problem() | transloco }}</p>\n }\n\n <div class=\"flex justify-end gap-2\">\n <button type=\"button\" class=\"lw-btn lw-btn--default\" (click)=\"close()\">{{ (closed ? 'service-desk.dialog.close' : 'service-desk.dialog.cancel') | transloco }}</button>\n @if (!closed) {\n <button type=\"submit\" class=\"lw-btn lw-btn--primary\" [disabled]=\"saving()\">{{ 'service-desk.dialog.save' | transloco }}</button>\n }\n </div>\n</form>\n"
|
|
314
|
+
}]
|
|
315
|
+
}]
|
|
316
|
+
});
|
|
317
|
+
var RequestTable = class RequestTable {
|
|
318
|
+
requests = input.required(...ngDevMode ? [{ debugName: "requests" }] : /* istanbul ignore next */ []);
|
|
319
|
+
locale = input.required(...ngDevMode ? [{ debugName: "locale" }] : /* istanbul ignore next */ []);
|
|
320
|
+
requestActions = output();
|
|
321
|
+
rows = computed(() => {
|
|
322
|
+
const date = new Intl.DateTimeFormat(this.locale(), {
|
|
323
|
+
day: "2-digit",
|
|
324
|
+
month: "2-digit",
|
|
325
|
+
year: "numeric"
|
|
326
|
+
});
|
|
327
|
+
return this.requests().map((request) => ({
|
|
328
|
+
request,
|
|
329
|
+
raised: date.format(new Date(request.raisedAt)),
|
|
330
|
+
due: request.dueAt ? date.format(new Date(request.dueAt)) : null,
|
|
331
|
+
closed: isClosed(request.status)
|
|
332
|
+
}));
|
|
333
|
+
}, ...ngDevMode ? [{ debugName: "rows" }] : /* istanbul ignore next */ []);
|
|
334
|
+
static ɵfac = i0.ɵɵngDeclareFactory({
|
|
335
|
+
minVersion: "12.0.0",
|
|
336
|
+
version: "22.2.3",
|
|
337
|
+
ngImport: i0,
|
|
338
|
+
type: RequestTable,
|
|
339
|
+
deps: [],
|
|
340
|
+
target: i0.ɵɵFactoryTarget.Component
|
|
341
|
+
});
|
|
342
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({
|
|
343
|
+
minVersion: "17.0.0",
|
|
344
|
+
version: "22.2.3",
|
|
345
|
+
type: RequestTable,
|
|
346
|
+
isStandalone: true,
|
|
347
|
+
selector: "wa-request-table",
|
|
348
|
+
inputs: {
|
|
349
|
+
requests: {
|
|
350
|
+
classPropertyName: "requests",
|
|
351
|
+
publicName: "requests",
|
|
352
|
+
isSignal: true,
|
|
353
|
+
isRequired: true,
|
|
354
|
+
transformFunction: null
|
|
355
|
+
},
|
|
356
|
+
locale: {
|
|
357
|
+
classPropertyName: "locale",
|
|
358
|
+
publicName: "locale",
|
|
359
|
+
isSignal: true,
|
|
360
|
+
isRequired: true,
|
|
361
|
+
transformFunction: null
|
|
362
|
+
}
|
|
363
|
+
},
|
|
364
|
+
outputs: { requestActions: "requestActions" },
|
|
365
|
+
ngImport: i0,
|
|
366
|
+
template: "<table class=\"w-full border-collapse text-sm\">\n <thead class=\"border-y border-border bg-surface-raised\">\n <tr>\n <th scope=\"col\" class=\"px-4 py-2 text-left text-xs font-medium uppercase tracking-wide text-content-faint\">{{ 'service-desk.table.title' | transloco }}</th>\n <th scope=\"col\" class=\"px-4 py-2 text-left text-xs font-medium uppercase tracking-wide text-content-faint\">{{ 'service-desk.table.type' | transloco }}</th>\n <th scope=\"col\" class=\"px-4 py-2 text-left text-xs font-medium uppercase tracking-wide text-content-faint\">{{ 'service-desk.table.priority' | transloco }}</th>\n <th scope=\"col\" class=\"px-4 py-2 text-left text-xs font-medium uppercase tracking-wide text-content-faint\">{{ 'service-desk.table.status' | transloco }}</th>\n <th scope=\"col\" class=\"px-4 py-2 text-left text-xs font-medium uppercase tracking-wide text-content-faint\">{{ 'service-desk.table.contact' | transloco }}</th>\n <th scope=\"col\" class=\"px-4 py-2 text-left text-xs font-medium uppercase tracking-wide text-content-faint\">{{ 'service-desk.table.dates' | transloco }}</th>\n <th scope=\"col\" class=\"px-4 py-2\"><span class=\"sr-only\">{{ 'service-desk.table.actions' | transloco }}</span></th>\n </tr>\n </thead>\n <tbody>\n @for (row of rows(); track row.request.id) {\n <tr class=\"border-b border-border\" [class.text-content-faint]=\"row.closed\" data-testid=\"request-row\">\n <td class=\"min-w-48 px-4 py-3 font-medium text-content\" [class.line-through]=\"row.request.status === 'cancelled'\">{{ row.request.title }}</td>\n <td class=\"px-4 py-3 whitespace-nowrap\">\n <span class=\"rounded-full border border-border px-2 py-0.5 text-xs text-content\">{{ 'service-desk.type.' + row.request.type | transloco }}</span>\n </td>\n <td class=\"px-4 py-3 whitespace-nowrap\">\n <span class=\"rounded-full border border-border px-2 py-0.5 text-xs\" [class.text-negative]=\"row.request.priority === 'urgent'\" [class.font-semibold]=\"row.request.priority === 'urgent' || row.request.priority === 'high'\" data-testid=\"request-priority\">{{ 'service-desk.priority.' + row.request.priority | transloco }}</span>\n </td>\n <td class=\"px-4 py-3 whitespace-nowrap\">\n <span class=\"rounded-full border border-border px-2 py-0.5 text-xs text-content\" data-testid=\"request-status\">{{ 'service-desk.status.' + row.request.status | transloco }}</span>\n </td>\n <td class=\"px-4 py-3\">\n <div class=\"text-content\">{{ row.request.contactName ?? '–' }}</div>\n @if (row.request.contactPhone ?? row.request.contactEmail; as reach) {\n <div class=\"text-content-faint\">{{ reach }}</div>\n }\n </td>\n <td class=\"px-4 py-3 whitespace-nowrap\">\n <div class=\"text-content\">{{ row.raised }}</div>\n @if (row.due) {\n <div class=\"text-content-faint\">{{ 'service-desk.table.dueOn' | transloco: { date: row.due } }}</div>\n }\n </td>\n <td class=\"px-4 py-3 text-right whitespace-nowrap\">\n <button type=\"button\" class=\"relative lw-btn lw-btn--ghost lw-btn--sm lw-btn--icon\" [attr.aria-label]=\"'service-desk.actions.more' | transloco\" (click)=\"requestActions.emit({ request: row.request, x: $event.clientX, y: $event.clientY })\">\n <lw-icon name=\"more\" size=\"1rem\"></lw-icon>\n <lw-tooltip [attr.text]=\"'service-desk.actions.more' | transloco\" position=\"top\"></lw-tooltip>\n </button>\n </td>\n </tr>\n } @empty {\n <tr>\n <td colspan=\"7\" class=\"px-4 py-8 text-center text-content-faint\">{{ 'service-desk.table.empty' | transloco }}</td>\n </tr>\n }\n </tbody>\n</table>\n",
|
|
367
|
+
dependencies: [{
|
|
368
|
+
kind: "pipe",
|
|
369
|
+
type: TranslocoPipe,
|
|
370
|
+
name: "transloco"
|
|
371
|
+
}],
|
|
372
|
+
changeDetection: i0.ChangeDetectionStrategy.OnPush
|
|
373
|
+
});
|
|
374
|
+
};
|
|
375
|
+
i0.ɵɵngDeclareClassMetadata({
|
|
376
|
+
minVersion: "12.0.0",
|
|
377
|
+
version: "22.2.3",
|
|
378
|
+
ngImport: i0,
|
|
379
|
+
type: RequestTable,
|
|
380
|
+
decorators: [{
|
|
381
|
+
type: Component,
|
|
382
|
+
args: [{
|
|
383
|
+
selector: "wa-request-table",
|
|
384
|
+
imports: [TranslocoPipe],
|
|
385
|
+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
|
386
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
387
|
+
template: "<table class=\"w-full border-collapse text-sm\">\n <thead class=\"border-y border-border bg-surface-raised\">\n <tr>\n <th scope=\"col\" class=\"px-4 py-2 text-left text-xs font-medium uppercase tracking-wide text-content-faint\">{{ 'service-desk.table.title' | transloco }}</th>\n <th scope=\"col\" class=\"px-4 py-2 text-left text-xs font-medium uppercase tracking-wide text-content-faint\">{{ 'service-desk.table.type' | transloco }}</th>\n <th scope=\"col\" class=\"px-4 py-2 text-left text-xs font-medium uppercase tracking-wide text-content-faint\">{{ 'service-desk.table.priority' | transloco }}</th>\n <th scope=\"col\" class=\"px-4 py-2 text-left text-xs font-medium uppercase tracking-wide text-content-faint\">{{ 'service-desk.table.status' | transloco }}</th>\n <th scope=\"col\" class=\"px-4 py-2 text-left text-xs font-medium uppercase tracking-wide text-content-faint\">{{ 'service-desk.table.contact' | transloco }}</th>\n <th scope=\"col\" class=\"px-4 py-2 text-left text-xs font-medium uppercase tracking-wide text-content-faint\">{{ 'service-desk.table.dates' | transloco }}</th>\n <th scope=\"col\" class=\"px-4 py-2\"><span class=\"sr-only\">{{ 'service-desk.table.actions' | transloco }}</span></th>\n </tr>\n </thead>\n <tbody>\n @for (row of rows(); track row.request.id) {\n <tr class=\"border-b border-border\" [class.text-content-faint]=\"row.closed\" data-testid=\"request-row\">\n <td class=\"min-w-48 px-4 py-3 font-medium text-content\" [class.line-through]=\"row.request.status === 'cancelled'\">{{ row.request.title }}</td>\n <td class=\"px-4 py-3 whitespace-nowrap\">\n <span class=\"rounded-full border border-border px-2 py-0.5 text-xs text-content\">{{ 'service-desk.type.' + row.request.type | transloco }}</span>\n </td>\n <td class=\"px-4 py-3 whitespace-nowrap\">\n <span class=\"rounded-full border border-border px-2 py-0.5 text-xs\" [class.text-negative]=\"row.request.priority === 'urgent'\" [class.font-semibold]=\"row.request.priority === 'urgent' || row.request.priority === 'high'\" data-testid=\"request-priority\">{{ 'service-desk.priority.' + row.request.priority | transloco }}</span>\n </td>\n <td class=\"px-4 py-3 whitespace-nowrap\">\n <span class=\"rounded-full border border-border px-2 py-0.5 text-xs text-content\" data-testid=\"request-status\">{{ 'service-desk.status.' + row.request.status | transloco }}</span>\n </td>\n <td class=\"px-4 py-3\">\n <div class=\"text-content\">{{ row.request.contactName ?? '–' }}</div>\n @if (row.request.contactPhone ?? row.request.contactEmail; as reach) {\n <div class=\"text-content-faint\">{{ reach }}</div>\n }\n </td>\n <td class=\"px-4 py-3 whitespace-nowrap\">\n <div class=\"text-content\">{{ row.raised }}</div>\n @if (row.due) {\n <div class=\"text-content-faint\">{{ 'service-desk.table.dueOn' | transloco: { date: row.due } }}</div>\n }\n </td>\n <td class=\"px-4 py-3 text-right whitespace-nowrap\">\n <button type=\"button\" class=\"relative lw-btn lw-btn--ghost lw-btn--sm lw-btn--icon\" [attr.aria-label]=\"'service-desk.actions.more' | transloco\" (click)=\"requestActions.emit({ request: row.request, x: $event.clientX, y: $event.clientY })\">\n <lw-icon name=\"more\" size=\"1rem\"></lw-icon>\n <lw-tooltip [attr.text]=\"'service-desk.actions.more' | transloco\" position=\"top\"></lw-tooltip>\n </button>\n </td>\n </tr>\n } @empty {\n <tr>\n <td colspan=\"7\" class=\"px-4 py-8 text-center text-content-faint\">{{ 'service-desk.table.empty' | transloco }}</td>\n </tr>\n }\n </tbody>\n</table>\n"
|
|
388
|
+
}]
|
|
389
|
+
}],
|
|
390
|
+
propDecorators: {
|
|
391
|
+
requests: [{
|
|
392
|
+
type: i0.Input,
|
|
393
|
+
args: [{
|
|
394
|
+
isSignal: true,
|
|
395
|
+
alias: "requests",
|
|
396
|
+
required: true
|
|
397
|
+
}]
|
|
398
|
+
}],
|
|
399
|
+
locale: [{
|
|
400
|
+
type: i0.Input,
|
|
401
|
+
args: [{
|
|
402
|
+
isSignal: true,
|
|
403
|
+
alias: "locale",
|
|
404
|
+
required: true
|
|
405
|
+
}]
|
|
406
|
+
}],
|
|
407
|
+
requestActions: [{
|
|
408
|
+
type: i0.Output,
|
|
409
|
+
args: ["requestActions"]
|
|
410
|
+
}]
|
|
411
|
+
}
|
|
412
|
+
});
|
|
413
|
+
const pageSize = 25;
|
|
414
|
+
const searchDelayMs = 300;
|
|
415
|
+
var DeskPage = class DeskPage {
|
|
416
|
+
gateway = inject(SERVICE_DESK_GATEWAY);
|
|
417
|
+
route = inject(ActivatedRoute);
|
|
418
|
+
searchTimer;
|
|
419
|
+
locale = activeLocale();
|
|
420
|
+
filters = statusFilters;
|
|
421
|
+
deskId = toSignal(this.route.paramMap.pipe(map((params) => params.get("deskId") ?? "")), { initialValue: this.route.snapshot.paramMap.get("deskId") ?? "" });
|
|
422
|
+
text = signal("", ...ngDevMode ? [{ debugName: "text" }] : /* istanbul ignore next */ []);
|
|
423
|
+
filter = signal("active", ...ngDevMode ? [{ debugName: "filter" }] : /* istanbul ignore next */ []);
|
|
424
|
+
offset = signal(0, ...ngDevMode ? [{ debugName: "offset" }] : /* istanbul ignore next */ []);
|
|
425
|
+
desk = resource({
|
|
426
|
+
...ngDevMode ? { debugName: "desk" } : /* istanbul ignore next */ {},
|
|
427
|
+
params: () => this.deskId(),
|
|
428
|
+
loader: ({ params }) => this.gateway.desk(params)
|
|
429
|
+
});
|
|
430
|
+
page = resource({
|
|
431
|
+
...ngDevMode ? { debugName: "page" } : /* istanbul ignore next */ {},
|
|
432
|
+
params: () => ({
|
|
433
|
+
deskId: this.deskId(),
|
|
434
|
+
text: this.text().trim(),
|
|
435
|
+
filter: this.filter(),
|
|
436
|
+
offset: this.offset()
|
|
437
|
+
}),
|
|
438
|
+
loader: ({ params, abortSignal }) => this.gateway.requests(params.deskId, {
|
|
439
|
+
text: params.text || void 0,
|
|
440
|
+
...params.filter === "active" ? {} : params.filter === "all" ? { includeCancelled: true } : { status: params.filter },
|
|
441
|
+
offset: params.offset,
|
|
442
|
+
limit: pageSize
|
|
443
|
+
}, abortSignal)
|
|
444
|
+
});
|
|
445
|
+
constructor() {
|
|
446
|
+
inject(DestroyRef).onDestroy(() => clearTimeout(this.searchTimer));
|
|
447
|
+
effect(() => {
|
|
448
|
+
const total = this.total();
|
|
449
|
+
if (this.page.hasValue() && this.offset() > 0 && this.offset() >= total) this.offset.set(Math.max(0, Math.ceil(total / pageSize) - 1) * pageSize);
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
total = computed(() => Number(this.page.value()?.total ?? 0), ...ngDevMode ? [{ debugName: "total" }] : /* istanbul ignore next */ []);
|
|
453
|
+
range = computed(() => {
|
|
454
|
+
const total = this.total();
|
|
455
|
+
return {
|
|
456
|
+
first: total === 0 ? 0 : this.offset() + 1,
|
|
457
|
+
last: Math.min(this.offset() + pageSize, total),
|
|
458
|
+
total
|
|
459
|
+
};
|
|
460
|
+
}, ...ngDevMode ? [{ debugName: "range" }] : /* istanbul ignore next */ []);
|
|
461
|
+
hasPrevious = computed(() => this.offset() > 0, ...ngDevMode ? [{ debugName: "hasPrevious" }] : /* istanbul ignore next */ []);
|
|
462
|
+
hasNext = computed(() => this.offset() + pageSize < this.total(), ...ngDevMode ? [{ debugName: "hasNext" }] : /* istanbul ignore next */ []);
|
|
463
|
+
search(event) {
|
|
464
|
+
const value = event.target.value;
|
|
465
|
+
clearTimeout(this.searchTimer);
|
|
466
|
+
this.searchTimer = setTimeout(() => {
|
|
467
|
+
this.text.set(value);
|
|
468
|
+
this.offset.set(0);
|
|
469
|
+
}, searchDelayMs);
|
|
470
|
+
}
|
|
471
|
+
chooseFilter(event) {
|
|
472
|
+
const chosen = statusFilters.find((filter) => filter === event.target.value);
|
|
473
|
+
if (chosen) {
|
|
474
|
+
this.filter.set(chosen);
|
|
475
|
+
this.offset.set(0);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
previous() {
|
|
479
|
+
this.offset.update((offset) => Math.max(0, offset - pageSize));
|
|
480
|
+
}
|
|
481
|
+
next() {
|
|
482
|
+
this.offset.update((offset) => offset + pageSize);
|
|
483
|
+
}
|
|
484
|
+
add() {
|
|
485
|
+
this.openDialog({
|
|
486
|
+
deskId: this.deskId(),
|
|
487
|
+
request: null
|
|
488
|
+
}, "service-desk.dialog.addTitle");
|
|
489
|
+
}
|
|
490
|
+
actions({ request, x, y }) {
|
|
491
|
+
const closed = isClosed(request.status);
|
|
492
|
+
serviceDeskHost.ui.openMenu([
|
|
493
|
+
{
|
|
494
|
+
label: closed ? "service-desk.actions.view" : "service-desk.actions.edit",
|
|
495
|
+
icon: closed ? "preview" : "edit",
|
|
496
|
+
run: () => this.open(request)
|
|
497
|
+
},
|
|
498
|
+
...closed ? [] : [{
|
|
499
|
+
label: "service-desk.actions.complete",
|
|
500
|
+
icon: "serviceDeskComplete",
|
|
501
|
+
run: () => void this.complete(request)
|
|
502
|
+
}, {
|
|
503
|
+
label: "service-desk.actions.cancel",
|
|
504
|
+
icon: "serviceDeskCancel",
|
|
505
|
+
run: () => void this.cancel(request)
|
|
506
|
+
}],
|
|
507
|
+
{
|
|
508
|
+
label: "service-desk.actions.delete",
|
|
509
|
+
icon: "trash",
|
|
510
|
+
run: () => void this.remove(request)
|
|
511
|
+
}
|
|
512
|
+
], {
|
|
513
|
+
x,
|
|
514
|
+
y
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
open(request) {
|
|
518
|
+
const closed = isClosed(request.status);
|
|
519
|
+
this.openDialog({
|
|
520
|
+
deskId: this.deskId(),
|
|
521
|
+
request
|
|
522
|
+
}, closed ? "service-desk.dialog.viewTitle" : "service-desk.dialog.editTitle");
|
|
523
|
+
}
|
|
524
|
+
async complete(request) {
|
|
525
|
+
await this.attempt(async () => this.report(await this.gateway.complete(this.deskId(), request.id), "service-desk.complete.done"));
|
|
526
|
+
}
|
|
527
|
+
async cancel(request) {
|
|
528
|
+
const reason = await serviceDeskHost.ui.prompt({
|
|
529
|
+
title: "service-desk.cancel.title",
|
|
530
|
+
message: "service-desk.cancel.message",
|
|
531
|
+
confirmLabel: "service-desk.cancel.confirm",
|
|
532
|
+
tone: "warning"
|
|
533
|
+
});
|
|
534
|
+
if (reason === null) return;
|
|
535
|
+
await this.attempt(async () => this.report(await this.gateway.cancel(this.deskId(), request.id, reason.trim() || null), "service-desk.cancel.done"));
|
|
536
|
+
}
|
|
537
|
+
async remove(request) {
|
|
538
|
+
if (!await serviceDeskHost.ui.confirm({
|
|
539
|
+
title: "service-desk.delete.title",
|
|
540
|
+
message: "service-desk.delete.message",
|
|
541
|
+
confirmLabel: "service-desk.delete.confirm",
|
|
542
|
+
tone: "danger"
|
|
543
|
+
})) return;
|
|
544
|
+
await this.attempt(async () => {
|
|
545
|
+
await this.gateway.remove(this.deskId(), request.id);
|
|
546
|
+
serviceDeskHost.ui.toast({
|
|
547
|
+
message: "service-desk.delete.done",
|
|
548
|
+
kind: "success"
|
|
549
|
+
});
|
|
550
|
+
this.page.reload();
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
async attempt(work) {
|
|
554
|
+
try {
|
|
555
|
+
await work();
|
|
556
|
+
} catch {
|
|
557
|
+
serviceDeskHost.ui.toast({
|
|
558
|
+
message: "service-desk.errors.save",
|
|
559
|
+
kind: "error"
|
|
560
|
+
});
|
|
561
|
+
this.page.reload();
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
openDialog(data, title) {
|
|
565
|
+
serviceDeskHost.ui.open(RequestDialog, {
|
|
566
|
+
data,
|
|
567
|
+
title,
|
|
568
|
+
size: "lg",
|
|
569
|
+
dismiss: "explicit"
|
|
570
|
+
}).closed.then((saved) => {
|
|
571
|
+
if (saved) {
|
|
572
|
+
serviceDeskHost.ui.toast({
|
|
573
|
+
message: "service-desk.dialog.saved",
|
|
574
|
+
kind: "success"
|
|
575
|
+
});
|
|
576
|
+
this.page.reload();
|
|
577
|
+
}
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
report(outcome, done) {
|
|
581
|
+
const saved = outcome.kind === "saved";
|
|
582
|
+
serviceDeskHost.ui.toast({
|
|
583
|
+
message: saved ? done : "service-desk.errors.save",
|
|
584
|
+
kind: saved ? "success" : "error"
|
|
585
|
+
});
|
|
586
|
+
this.page.reload();
|
|
587
|
+
}
|
|
588
|
+
static ɵfac = i0.ɵɵngDeclareFactory({
|
|
589
|
+
minVersion: "12.0.0",
|
|
590
|
+
version: "22.2.3",
|
|
591
|
+
ngImport: i0,
|
|
592
|
+
type: DeskPage,
|
|
593
|
+
deps: [],
|
|
594
|
+
target: i0.ɵɵFactoryTarget.Component
|
|
595
|
+
});
|
|
596
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({
|
|
597
|
+
minVersion: "17.0.0",
|
|
598
|
+
version: "22.2.3",
|
|
599
|
+
type: DeskPage,
|
|
600
|
+
isStandalone: true,
|
|
601
|
+
selector: "wa-desk-page",
|
|
602
|
+
ngImport: i0,
|
|
603
|
+
template: "<div class=\"flex h-full flex-col gap-4 overflow-auto p-6\">\n <div class=\"flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between\">\n <h2 class=\"text-lg font-semibold text-content\">{{ desk.value()?.name ?? ('service-desk.title' | transloco) }}</h2>\n <div class=\"flex flex-wrap items-center gap-2\">\n <input\n type=\"search\"\n class=\"lw-field w-56\"\n [attr.aria-label]=\"'service-desk.search.label' | transloco\"\n [attr.placeholder]=\"'service-desk.search.placeholder' | transloco\"\n (input)=\"search($event)\"\n />\n <select class=\"lw-field w-64\" [attr.aria-label]=\"'service-desk.filter.label' | transloco\" (change)=\"chooseFilter($event)\">\n @for (choice of filters; track choice) {\n <option [value]=\"choice\" [selected]=\"choice === filter()\">{{ 'service-desk.filter.' + choice | transloco }}</option>\n }\n </select>\n <button type=\"button\" class=\"lw-btn lw-btn--primary\" (click)=\"add()\">\n <lw-icon name=\"add\" size=\"1rem\"></lw-icon>\n {{ 'service-desk.actions.add' | transloco }}\n </button>\n </div>\n </div>\n\n @if (page.error()) {\n <p class=\"text-sm text-negative\" role=\"alert\">{{ 'service-desk.errors.load' | transloco }}</p>\n } @else {\n <div class=\"overflow-x-auto rounded-xl border border-border bg-surface\">\n <wa-request-table\n [requests]=\"page.value()?.items ?? []\"\n [locale]=\"locale()\"\n (requestActions)=\"actions($event)\"\n />\n </div>\n <div class=\"flex items-center justify-end gap-2 text-sm text-content-faint\">\n <span data-testid=\"page-range\">{{ 'service-desk.pager.range' | transloco: range() }}</span>\n <button type=\"button\" class=\"relative lw-btn lw-btn--default lw-btn--sm lw-btn--icon\" [disabled]=\"!hasPrevious()\" [attr.aria-label]=\"'service-desk.pager.previous' | transloco\" (click)=\"previous()\">\n <lw-icon name=\"serviceDeskPrevious\" size=\"1rem\"></lw-icon>\n </button>\n <button type=\"button\" class=\"relative lw-btn lw-btn--default lw-btn--sm lw-btn--icon\" [disabled]=\"!hasNext()\" [attr.aria-label]=\"'service-desk.pager.next' | transloco\" (click)=\"next()\">\n <lw-icon name=\"serviceDeskNext\" size=\"1rem\"></lw-icon>\n </button>\n </div>\n }\n</div>\n",
|
|
604
|
+
dependencies: [{
|
|
605
|
+
kind: "component",
|
|
606
|
+
type: RequestTable,
|
|
607
|
+
selector: "wa-request-table",
|
|
608
|
+
inputs: ["requests", "locale"],
|
|
609
|
+
outputs: ["requestActions"]
|
|
610
|
+
}, {
|
|
611
|
+
kind: "pipe",
|
|
612
|
+
type: TranslocoPipe,
|
|
613
|
+
name: "transloco"
|
|
614
|
+
}],
|
|
615
|
+
changeDetection: i0.ChangeDetectionStrategy.OnPush
|
|
616
|
+
});
|
|
617
|
+
};
|
|
618
|
+
i0.ɵɵngDeclareClassMetadata({
|
|
619
|
+
minVersion: "12.0.0",
|
|
620
|
+
version: "22.2.3",
|
|
621
|
+
ngImport: i0,
|
|
622
|
+
type: DeskPage,
|
|
623
|
+
decorators: [{
|
|
624
|
+
type: Component,
|
|
625
|
+
args: [{
|
|
626
|
+
selector: "wa-desk-page",
|
|
627
|
+
imports: [TranslocoPipe, RequestTable],
|
|
628
|
+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
|
629
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
630
|
+
template: "<div class=\"flex h-full flex-col gap-4 overflow-auto p-6\">\n <div class=\"flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between\">\n <h2 class=\"text-lg font-semibold text-content\">{{ desk.value()?.name ?? ('service-desk.title' | transloco) }}</h2>\n <div class=\"flex flex-wrap items-center gap-2\">\n <input\n type=\"search\"\n class=\"lw-field w-56\"\n [attr.aria-label]=\"'service-desk.search.label' | transloco\"\n [attr.placeholder]=\"'service-desk.search.placeholder' | transloco\"\n (input)=\"search($event)\"\n />\n <select class=\"lw-field w-64\" [attr.aria-label]=\"'service-desk.filter.label' | transloco\" (change)=\"chooseFilter($event)\">\n @for (choice of filters; track choice) {\n <option [value]=\"choice\" [selected]=\"choice === filter()\">{{ 'service-desk.filter.' + choice | transloco }}</option>\n }\n </select>\n <button type=\"button\" class=\"lw-btn lw-btn--primary\" (click)=\"add()\">\n <lw-icon name=\"add\" size=\"1rem\"></lw-icon>\n {{ 'service-desk.actions.add' | transloco }}\n </button>\n </div>\n </div>\n\n @if (page.error()) {\n <p class=\"text-sm text-negative\" role=\"alert\">{{ 'service-desk.errors.load' | transloco }}</p>\n } @else {\n <div class=\"overflow-x-auto rounded-xl border border-border bg-surface\">\n <wa-request-table\n [requests]=\"page.value()?.items ?? []\"\n [locale]=\"locale()\"\n (requestActions)=\"actions($event)\"\n />\n </div>\n <div class=\"flex items-center justify-end gap-2 text-sm text-content-faint\">\n <span data-testid=\"page-range\">{{ 'service-desk.pager.range' | transloco: range() }}</span>\n <button type=\"button\" class=\"relative lw-btn lw-btn--default lw-btn--sm lw-btn--icon\" [disabled]=\"!hasPrevious()\" [attr.aria-label]=\"'service-desk.pager.previous' | transloco\" (click)=\"previous()\">\n <lw-icon name=\"serviceDeskPrevious\" size=\"1rem\"></lw-icon>\n </button>\n <button type=\"button\" class=\"relative lw-btn lw-btn--default lw-btn--sm lw-btn--icon\" [disabled]=\"!hasNext()\" [attr.aria-label]=\"'service-desk.pager.next' | transloco\" (click)=\"next()\">\n <lw-icon name=\"serviceDeskNext\" size=\"1rem\"></lw-icon>\n </button>\n </div>\n }\n</div>\n"
|
|
631
|
+
}]
|
|
632
|
+
}],
|
|
633
|
+
ctorParameters: () => []
|
|
634
|
+
});
|
|
635
|
+
const stroke = "fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"";
|
|
636
|
+
const serviceDeskIcons = {
|
|
637
|
+
serviceDesk: `<svg viewBox="0 0 24 24" ${stroke}><path d="M4 13.5V7a2.5 2.5 0 0 1 2.5-2.5h11A2.5 2.5 0 0 1 20 7v6.5"/><path d="M3 13.5h5l1.5 2.5h5l1.5-2.5h5V18a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/></svg>`,
|
|
638
|
+
serviceDeskComplete: `<svg viewBox="0 0 24 24" ${stroke}><circle cx="12" cy="12" r="8.5"/><path d="M8.5 12.5l2.5 2.5 4.5-5"/></svg>`,
|
|
639
|
+
serviceDeskCancel: `<svg viewBox="0 0 24 24" ${stroke}><circle cx="12" cy="12" r="8.5"/><path d="M9 9l6 6M15 9l-6 6"/></svg>`,
|
|
640
|
+
serviceDeskPrevious: `<svg viewBox="0 0 24 24" ${stroke}><path d="M15 5l-7 7 7 7"/></svg>`,
|
|
641
|
+
serviceDeskNext: `<svg viewBox="0 0 24 24" ${stroke}><path d="M9 5l7 7-7 7"/></svg>`
|
|
642
|
+
};
|
|
643
|
+
function createServiceDeskPlugin(options = {}) {
|
|
644
|
+
return {
|
|
645
|
+
manifest: {
|
|
646
|
+
id: "service-desk",
|
|
647
|
+
name: "Service desk",
|
|
648
|
+
capabilities: [
|
|
649
|
+
"contributions",
|
|
650
|
+
"navigation",
|
|
651
|
+
"ui"
|
|
652
|
+
]
|
|
653
|
+
},
|
|
654
|
+
activate(ctx) {
|
|
655
|
+
serviceDeskHost.bind(ctx);
|
|
656
|
+
ctx.contributeIcons(serviceDeskIcons);
|
|
657
|
+
ctx.registerSurface({
|
|
658
|
+
id: "service-desk.desks",
|
|
659
|
+
title: "service-desk.desks.title",
|
|
660
|
+
icon: "serviceDesk",
|
|
661
|
+
routable: { path: deskListPath },
|
|
662
|
+
component: DeskListPage
|
|
663
|
+
});
|
|
664
|
+
ctx.registerSurface({
|
|
665
|
+
id: "service-desk.desk",
|
|
666
|
+
title: "service-desk.title",
|
|
667
|
+
icon: "serviceDesk",
|
|
668
|
+
routable: { path: `${deskListPath}/:deskId` },
|
|
669
|
+
component: DeskPage
|
|
670
|
+
});
|
|
671
|
+
if (options.railItem) ctx.registerRailItem({
|
|
672
|
+
id: "service-desk.rail",
|
|
673
|
+
rail: "primary",
|
|
674
|
+
icon: "serviceDesk",
|
|
675
|
+
title: "service-desk.desks.title",
|
|
676
|
+
run: () => ctx.navigateContent(deskListPath)
|
|
677
|
+
});
|
|
678
|
+
},
|
|
679
|
+
deactivate() {
|
|
680
|
+
serviceDeskHost.unbind();
|
|
681
|
+
}
|
|
682
|
+
};
|
|
683
|
+
}
|
|
684
|
+
const serviceDeskPlugin = createServiceDeskPlugin();
|
|
685
|
+
const getServiceDeskDesksSaveUrl = (deskId) => {
|
|
686
|
+
return `/service-desk/desks/${deskId}`;
|
|
687
|
+
};
|
|
688
|
+
const getServiceDeskDesksGetUrl = (deskId) => {
|
|
689
|
+
return `/service-desk/desks/${deskId}`;
|
|
690
|
+
};
|
|
691
|
+
const getServiceDeskDesksListUrl = () => {
|
|
692
|
+
return `/service-desk/desks`;
|
|
693
|
+
};
|
|
694
|
+
const getServiceDeskRequestsRaiseUrl = (deskId) => {
|
|
695
|
+
return `/service-desk/desks/${deskId}/requests`;
|
|
696
|
+
};
|
|
697
|
+
const getServiceDeskRequestsListUrl = (deskId, params) => {
|
|
698
|
+
const normalizedParams = new URLSearchParams();
|
|
699
|
+
Object.entries(params || {}).forEach(([key, value]) => {
|
|
700
|
+
if (value !== void 0) normalizedParams.append(key, value === null ? "null" : String(value));
|
|
701
|
+
});
|
|
702
|
+
const stringifiedParams = normalizedParams.toString();
|
|
703
|
+
return stringifiedParams.length > 0 ? `/service-desk/desks/${deskId}/requests?${stringifiedParams}` : `/service-desk/desks/${deskId}/requests`;
|
|
704
|
+
};
|
|
705
|
+
const getServiceDeskRequestsEditUrl = (deskId, requestId) => {
|
|
706
|
+
return `/service-desk/desks/${deskId}/requests/${requestId}`;
|
|
707
|
+
};
|
|
708
|
+
const getServiceDeskRequestsDeleteUrl = (deskId, requestId) => {
|
|
709
|
+
return `/service-desk/desks/${deskId}/requests/${requestId}`;
|
|
710
|
+
};
|
|
711
|
+
const getServiceDeskRequestsStatusUrl = (deskId, requestId) => {
|
|
712
|
+
return `/service-desk/desks/${deskId}/requests/${requestId}/status`;
|
|
713
|
+
};
|
|
714
|
+
const getServiceDeskRequestsCompleteUrl = (deskId, requestId) => {
|
|
715
|
+
return `/service-desk/desks/${deskId}/requests/${requestId}/complete`;
|
|
716
|
+
};
|
|
717
|
+
const getServiceDeskRequestsCancelUrl = (deskId, requestId) => {
|
|
718
|
+
return `/service-desk/desks/${deskId}/requests/${requestId}/cancel`;
|
|
719
|
+
};
|
|
720
|
+
const notFound = 404;
|
|
721
|
+
const conflict = 409;
|
|
722
|
+
const badRequest = 400;
|
|
723
|
+
const closedCode = "request-closed";
|
|
724
|
+
function createHttpServiceDeskGateway(options) {
|
|
725
|
+
const base = options.baseUrl.replace(/\/+$/, "");
|
|
726
|
+
const run = options.fetch ?? ((input, init) => fetch(input, init));
|
|
727
|
+
const send = (path, method, body, signal) => run(base + path, {
|
|
728
|
+
method,
|
|
729
|
+
signal,
|
|
730
|
+
credentials: "same-origin",
|
|
731
|
+
headers: {
|
|
732
|
+
Accept: "application/json",
|
|
733
|
+
...body === void 0 ? {} : { "Content-Type": "application/json" },
|
|
734
|
+
...options.headers
|
|
735
|
+
},
|
|
736
|
+
body: body === void 0 ? void 0 : JSON.stringify(body)
|
|
737
|
+
});
|
|
738
|
+
const read = async (response) => {
|
|
739
|
+
if (!response.ok) throw new ServiceDeskGatewayError(response.status);
|
|
740
|
+
return await response.json();
|
|
741
|
+
};
|
|
742
|
+
const outcome = async (response) => {
|
|
743
|
+
if (response.ok) return {
|
|
744
|
+
kind: "saved",
|
|
745
|
+
request: await response.json()
|
|
746
|
+
};
|
|
747
|
+
if (response.status === notFound) return { kind: "missing" };
|
|
748
|
+
if (response.status === badRequest) return { kind: "invalid" };
|
|
749
|
+
if (response.status !== conflict) throw new ServiceDeskGatewayError(response.status);
|
|
750
|
+
return (await response.json().catch(() => ({}))).code === closedCode ? { kind: "closed" } : { kind: "changed" };
|
|
751
|
+
};
|
|
752
|
+
return {
|
|
753
|
+
desks: async () => read(await send(getServiceDeskDesksListUrl(), "GET")),
|
|
754
|
+
desk: async (deskId) => {
|
|
755
|
+
const response = await send(getServiceDeskDesksGetUrl(deskId), "GET");
|
|
756
|
+
return response.status === notFound ? null : read(response);
|
|
757
|
+
},
|
|
758
|
+
saveDesk: async (deskId, name) => read(await send(getServiceDeskDesksSaveUrl(deskId), "PUT", { name })),
|
|
759
|
+
requests: async (deskId, listing, signal) => read(await send(getServiceDeskRequestsListUrl(deskId, listing), "GET", void 0, signal)),
|
|
760
|
+
raise: async (deskId, request) => outcome(await send(getServiceDeskRequestsRaiseUrl(deskId), "POST", request)),
|
|
761
|
+
edit: async (deskId, requestId, changes) => outcome(await send(getServiceDeskRequestsEditUrl(deskId, requestId), "PUT", changes)),
|
|
762
|
+
setStatus: async (deskId, requestId, status) => outcome(await send(getServiceDeskRequestsStatusUrl(deskId, requestId), "PUT", { status })),
|
|
763
|
+
complete: async (deskId, requestId) => outcome(await send(getServiceDeskRequestsCompleteUrl(deskId, requestId), "POST")),
|
|
764
|
+
cancel: async (deskId, requestId, reason) => outcome(await send(getServiceDeskRequestsCancelUrl(deskId, requestId), "POST", { reason })),
|
|
765
|
+
remove: async (deskId, requestId) => {
|
|
766
|
+
const response = await send(getServiceDeskRequestsDeleteUrl(deskId, requestId), "DELETE");
|
|
767
|
+
if (!response.ok && response.status !== notFound) throw new ServiceDeskGatewayError(response.status);
|
|
768
|
+
}
|
|
769
|
+
};
|
|
770
|
+
}
|
|
771
|
+
export { SERVICE_DESK_GATEWAY, ServiceDeskGatewayError, createHttpServiceDeskGateway, createServiceDeskPlugin, deskListPath, deskPath, provideServiceDeskGateway, serviceDeskPlugin };
|
|
772
|
+
|
|
773
|
+
//# sourceMappingURL=weaver-apps-service-desk.mjs.map
|