Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions frontend/src/lib/api/integrations.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,10 @@ export const zammadTickets = {
resolve: (correlationKey) =>
fetchAPI(`/zammad-ticket-links/resolve/${encodeURIComponent(correlationKey)}`),
forItem: (itemId) => fetchAPI(`/items/${itemId}/zammad-links`),
history: (itemId, { limit = 6 } = {}) =>
fetchAPI(`/items/${itemId}/zammad-history?limit=${encodeURIComponent(limit)}`),
workspaceOverview: (workspaceId, { limit = 5 } = {}) =>
fetchAPI(`/workspaces/${workspaceId}/zammad-overview?limit=${encodeURIComponent(limit)}`),
create: (itemId, data) =>
fetchAPI(`/items/${itemId}/zammad-tickets`, {
method: 'POST',
Expand Down
299 changes: 267 additions & 32 deletions frontend/src/lib/features/items/ZammadItemPanel.svelte

Large diffs are not rendered by default.

18 changes: 18 additions & 0 deletions frontend/src/lib/features/items/zammadPanelContext.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,21 @@ export function isCurrentZammadMetadataRequest(
export function isUsableZammadGroup(group) {
return group?.active !== false && Boolean(group?.name?.trim());
}

export function isCurrentZammadTimelineRequest(
requestVersion,
currentVersion,
requestItemId,
itemId,
requestWorkspaceId,
workspaceId
) {
return isCurrentZammadPanelContext(
requestVersion,
currentVersion,
requestItemId,
itemId,
requestWorkspaceId,
workspaceId
);
}
2 changes: 1 addition & 1 deletion frontend/src/lib/locales/de/navigation.js
Original file line number Diff line number Diff line change
Expand Up @@ -427,10 +427,10 @@ export default {
selectWorkspace: 'Wählen Sie einen Workspace zum Starten',
noWorkspacesAvailable: 'Es sind noch keine Workspaces verfügbar. Bitte kontaktieren Sie Ihren Administrator, um Zugang zu einem Workspace zu erhalten.',
},

workspaceDashboard: {
widgets: {
supportMetrics: { name: 'Support-Kennzahlen', description: 'Backlog, Erstantwort, Lösung und SLA-Einhaltung für Kundentickets' },
zammadSupportOverview: { name: 'Zammad-Supportübersicht', description: 'Workspaceweiter Stand verknüpfter Tickets und beobachtete Änderungen' },
},
},
};
30 changes: 30 additions & 0 deletions frontend/src/lib/locales/de/zammad.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ export default { zammad: {
editTicket: 'Zammad-Ticket bearbeiten',
ticketUpdated: 'Zammad-Ticket aktualisiert',
ticketUpdateFailed: 'Zammad-Ticket konnte nicht aktualisiert werden',
ticketReloadAfterChangeFailed: 'Die Zammad-Änderung war erfolgreich, die aktuellen Ticketdaten konnten jedoch nicht neu geladen werden.',
ticketRefreshed: 'Zammad-Ticketstatus aktualisiert',
ticketRefreshFailed: 'Zammad-Ticket konnte nicht aktualisiert werden',
retrySync: 'Synchronisation wiederholen',
Expand All @@ -97,6 +98,35 @@ export default { zammad: {
ticketLinkRemoveFailed: 'Zammad-Ticketzuordnung konnte nicht entfernt werden',
ticketNumber: 'Zammad #{number}',
lastSynced: 'Synchronisiert: {time}',
timeline: {
toggle: 'Beobachtete Änderungen',
observedHint: 'Von Windshift bei der Synchronisierung beobachtete Änderungen. Dies ist kein vollständiger Zammad-Audit-Verlauf.',
empty: 'Bisher keine Ticketänderungen beobachtet.',
loadFailed: 'Beobachtete Zammad-Änderungen konnten nicht geladen werden',
initialValue: 'nicht gesetzt',
valueIdFallback: 'ID {id}',
change: '{field}: von {from} auf {to}',
field: { status: 'Status', group: 'Gruppe', owner: 'Verantwortlich', changed: 'Ticketfeld' },
},
overview: {
loading: 'Workspaceweite Zammad-Supportübersicht wird geladen…',
loadFailed: 'Zammad-Supportübersicht konnte nicht geladen werden',
emptyTitle: 'Keine verknüpften Zammad-Tickets',
emptyDescription: 'Dieser Workspace hat keine verknüpften Zammad-Tickets.',
observedHint: 'Workspaceweiter, von Windshift bei der Synchronisierung beobachteter Ticketstand.',
refresh: 'Aktualisieren',
byStatus: 'Statusverteilung',
statusWithConnection: '{status} ({connection})',
statusIdFallback: 'Status-ID {id}',
unknownStatusBucket: 'Unbekannter Status',
recentChanges: 'Zuletzt beobachtete Änderungen',
noRecentChanges: 'Bisher keine Änderungen beobachtet.',
syncStateHint: 'Einige Ticketverknüpfungen benötigen Nacharbeit in Zammad oder den Verbindungseinstellungen.',
metrics: {
total: 'Gesamt', active: 'Aktiv', closed: 'Geschlossen', unassigned: 'Nicht zugewiesen',
syncFailed: 'Synchronisierung fehlgeschlagen', creationUncertain: 'Erstellung unklar', unknownStatus: 'Unbekannter Status',
},
},
returnLinkResolving: 'Der verknüpfte Windshift-Eintrag wird geöffnet...',
returnLinkFailed: 'Der verknüpfte Windshift-Eintrag konnte nicht geöffnet werden. Er existiert möglicherweise nicht mehr oder du hast keinen Zugriff.',
syncState: {
Expand Down
1 change: 1 addition & 0 deletions frontend/src/lib/locales/en/navigation.js
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,7 @@ export default {
iterationTimeline: { name: 'Iteration Timeline', description: 'Current and upcoming iteration schedule' },
testCoverage: { name: 'Test Coverage', description: 'Requirements covered by test cases' },
supportMetrics: { name: 'Support Metrics', description: 'Backlog, first response, resolution, and SLA compliance for customer tickets' },
zammadSupportOverview: { name: 'Zammad support overview', description: 'Workspace-wide linked-ticket status and observed changes' },
},
customization: {
widgets: 'Widgets', builtIn: 'Built-in widgets', builtInDescription: 'Core workspace metrics and charts',
Expand Down
30 changes: 30 additions & 0 deletions frontend/src/lib/locales/en/zammad.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ export default { zammad: {
editTicket: 'Edit Zammad ticket',
ticketUpdated: 'Zammad ticket updated',
ticketUpdateFailed: 'Failed to update Zammad ticket',
ticketReloadAfterChangeFailed: 'The Zammad change succeeded, but the current ticket data could not be reloaded.',
ticketRefreshed: 'Zammad ticket status refreshed',
ticketRefreshFailed: 'Failed to refresh Zammad ticket',
retrySync: 'Retry synchronization',
Expand All @@ -97,6 +98,35 @@ export default { zammad: {
ticketLinkRemoveFailed: 'Failed to remove Zammad ticket assignment',
ticketNumber: 'Zammad #{number}',
lastSynced: 'Synced {time}',
timeline: {
toggle: 'Observed changes',
observedHint: 'Changes observed by Windshift during synchronization. This is not a complete Zammad audit trail.',
empty: 'No observed ticket changes yet.',
loadFailed: 'Failed to load observed Zammad changes',
initialValue: 'not set',
valueIdFallback: 'ID {id}',
change: '{field}: {from} -> {to}',
field: { status: 'Status', group: 'Group', owner: 'Owner', changed: 'Ticket field' },
},
overview: {
loading: 'Loading workspace-wide Zammad support status…',
loadFailed: 'Failed to load Zammad support overview',
emptyTitle: 'No linked Zammad tickets',
emptyDescription: 'This workspace has no linked Zammad tickets.',
observedHint: 'Workspace-wide ticket status observed by Windshift during synchronization.',
refresh: 'Refresh',
byStatus: 'Status distribution',
statusWithConnection: '{connection} · {status}',
statusIdFallback: 'Status ID {id}',
unknownStatusBucket: 'Unknown status',
recentChanges: 'Recent observed changes',
noRecentChanges: 'No observed changes yet.',
syncStateHint: 'Some ticket links need follow-up in Zammad or its connection settings.',
metrics: {
total: 'Total', active: 'Active', closed: 'Closed', unassigned: 'Unassigned',
syncFailed: 'Sync failed', creationUncertain: 'Creation uncertain', unknownStatus: 'Unknown status',
},
},
returnLinkResolving: 'Opening the linked Windshift item...',
returnLinkFailed: 'The linked Windshift item could not be opened. It may no longer exist or you may not have access.',
syncState: {
Expand Down
1 change: 1 addition & 0 deletions frontend/src/lib/locales/ru/navigation.js
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,7 @@ export default {
iterationTimeline: { name: 'График итераций', description: 'Текущие и ближайшие итерации' },
testCoverage: { name: 'Покрытие тестами', description: 'Требования, связанные с тест-кейсами' },
supportMetrics: { name: 'Метрики поддержки', description: 'Бэклог, первый ответ, решение и соблюдение SLA для клиентских заявок' },
zammadSupportOverview: { name: 'Обзор поддержки Zammad', description: 'Статус связанных заявок всего пространства и наблюдаемые изменения' },
},
customization: {
widgets: 'Виджеты', builtIn: 'Основные виджеты', builtInDescription: 'Основные показатели и графики пространства',
Expand Down
30 changes: 30 additions & 0 deletions frontend/src/lib/locales/ru/zammad.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ export default { zammad: {
editTicket: 'Изменить заявку Zammad',
ticketUpdated: 'Заявка Zammad обновлена',
ticketUpdateFailed: 'Не удалось обновить заявку Zammad',
ticketReloadAfterChangeFailed: 'Изменение в Zammad выполнено, но актуальные данные заявки не удалось загрузить повторно.',
ticketRefreshed: 'Статус заявки Zammad обновлён',
ticketRefreshFailed: 'Не удалось обновить заявку Zammad',
retrySync: 'Повторить синхронизацию',
Expand All @@ -97,6 +98,35 @@ export default { zammad: {
ticketLinkRemoveFailed: 'Не удалось удалить связь с заявкой Zammad',
ticketNumber: 'Zammad №{number}',
lastSynced: 'Синхронизировано: {time}',
timeline: {
toggle: 'Наблюдаемые изменения',
observedHint: 'Изменения, замеченные Windshift во время синхронизации. Это не полный журнал аудита Zammad.',
empty: 'Наблюдаемых изменений заявки пока нет.',
loadFailed: 'Не удалось загрузить наблюдаемые изменения Zammad',
initialValue: 'не задано',
valueIdFallback: 'ID {id}',
change: '{field}: с {from} на {to}',
field: { status: 'Статус', group: 'Группа', owner: 'Ответственный', changed: 'Поле заявки' },
},
overview: {
loading: 'Загружается обзор поддержки Zammad для рабочего пространства…',
loadFailed: 'Не удалось загрузить обзор поддержки Zammad',
emptyTitle: 'Нет связанных заявок Zammad',
emptyDescription: 'В этом рабочем пространстве нет связанных заявок Zammad.',
observedHint: 'Состояние заявок всего рабочего пространства, наблюдаемое Windshift при синхронизации.',
refresh: 'Обновить',
byStatus: 'Распределение по статусам',
statusWithConnection: '{status} ({connection})',
statusIdFallback: 'ID статуса {id}',
unknownStatusBucket: 'Неизвестный статус',
recentChanges: 'Последние наблюдаемые изменения',
noRecentChanges: 'Наблюдаемых изменений пока нет.',
syncStateHint: 'Некоторые связи заявок требуют проверки в Zammad или настройках подключения.',
metrics: {
total: 'Всего', active: 'Активные', closed: 'Закрытые', unassigned: 'Не назначены',
syncFailed: 'Ошибка синхронизации', creationUncertain: 'Создание не подтверждено', unknownStatus: 'Неизвестный статус',
},
},
returnLinkResolving: 'Открывается связанный элемент Windshift...',
returnLinkFailed: 'Не удалось открыть связанный элемент Windshift. Возможно, он больше не существует или у вас нет доступа.',
syncState: {
Expand Down
12 changes: 12 additions & 0 deletions frontend/src/lib/services/widgetRegistry.js
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,18 @@ export const widgetRegistry = [
defaultWidth: 2,
maxWidth: 3,
},
{
type: 'zammad-support-overview',
name: 'Zammad support overview',
description: 'Current linked-ticket status and recent observed changes',
nameKey: 'workspaceDashboard.widgets.zammadSupportOverview.name',
descriptionKey: 'workspaceDashboard.widgets.zammadSupportOverview.description',
category: widgetCategories.ADDITIONAL,
icon: 'TicketCheck',
minWidth: 2,
defaultWidth: 3,
maxWidth: 3,
},

// Additional widgets (calendar/timeline widgets)
{
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/lib/utils/icons.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ import {
Tag,
Target,
Terminal,
TicketCheck,
Trash,
Truck,
Upload,
Expand Down Expand Up @@ -262,6 +263,7 @@ export const workspaceIconMap = {
Watch,
Cloud,
BarChart,
TicketCheck,
};

/**
Expand Down
63 changes: 63 additions & 0 deletions frontend/src/lib/utils/zammadObservations.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
function nonEmptyName(value) {
return typeof value?.name === 'string' ? value.name.trim() : '';
}

function positiveId(value) {
const id = Number(value?.id);
return Number.isFinite(id) && id > 0 ? value.id : null;
}

export function getZammadObservedValueLabel(value, translate, field = null) {
if (field === 'owner' && Number(value?.id) === 1) {
return translate('zammad.unassignedOwner');
}

const name = nonEmptyName(value);
if (name) return name;

const id = positiveId(value);
if (id !== null) return translate('zammad.timeline.valueIdFallback', { id });

return translate('zammad.timeline.initialValue');
}

export function getZammadStatusBucketLabel(status, translate) {
const name = nonEmptyName(status);
if (name) return name;

const id = positiveId(status);
if (id !== null) return translate('zammad.overview.statusIdFallback', { id });

return translate('zammad.overview.unknownStatusBucket');
}

export function getZammadStatusBucketDisplayLabel(status, translate) {
const statusLabel = getZammadStatusBucketLabel(status, translate);
const connectionName =
typeof status?.connection_name === 'string' ? status.connection_name.trim() : '';
if (!connectionName) return statusLabel;

return translate('zammad.overview.statusWithConnection', {
connection: connectionName,
status: statusLabel,
});
}

export function getZammadStatusAppearance(status, closed = status?.closed === true) {
if (closed) return 'success';
if (positiveId(status) === null) return 'default';

const normalizedName = nonEmptyName(status).toLowerCase();
if (/^pending(?:[\s_-]|$)/.test(normalizedName)) return 'warning';

return 'info';
}

export function isCurrentZammadWorkspaceOverviewRequest(
requestVersion,
currentVersion,
requestWorkspaceId,
workspaceId
) {
return requestVersion === currentVersion && String(requestWorkspaceId) === String(workspaceId);
}
Loading
Loading