diff --git a/cmd/statuses.go b/cmd/statuses.go
index bccd9733..33f5aa37 100644
--- a/cmd/statuses.go
+++ b/cmd/statuses.go
@@ -33,7 +33,7 @@ func handleCreateStatus(r *fastglue.Request) error {
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.empty", "name", "`name`"), nil, envelope.InputError)
}
- createdStatus, err := app.status.Create(status.Name)
+ createdStatus, err := app.status.Create(status.Name, status.Category)
if err != nil {
return sendErrorEnvelope(r, err)
}
@@ -74,7 +74,7 @@ func handleUpdateStatus(r *fastglue.Request) error {
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.empty", "name", "`name`"), nil, envelope.InputError)
}
- updatedStatus, err := app.status.Update(id, status.Name)
+ updatedStatus, err := app.status.Update(id, status.Name, status.Category)
if err != nil {
return sendErrorEnvelope(r, err)
}
diff --git a/cmd/upgrade.go b/cmd/upgrade.go
index 7514d9eb..cfb5ddd0 100644
--- a/cmd/upgrade.go
+++ b/cmd/upgrade.go
@@ -41,6 +41,7 @@ var migList = []migFunc{
{"v0.10.0", migrations.V0_10_0},
{"v1.0.1", migrations.V1_0_1},
{"v2.0.0", migrations.V2_0_0},
+ {"v2.2.0", migrations.V2_2_0},
}
// upgrade upgrades the database to the current version by running SQL migration files
diff --git a/frontend/apps/main/src/features/admin/status/StatusForm.vue b/frontend/apps/main/src/features/admin/status/StatusForm.vue
index 110ee3d8..cfb0a50d 100644
--- a/frontend/apps/main/src/features/admin/status/StatusForm.vue
+++ b/frontend/apps/main/src/features/admin/status/StatusForm.vue
@@ -6,11 +6,31 @@
-
-
+
+
+
+ {{ $t('globals.terms.category') }}
+
+
+
+
+
+
+
@@ -18,11 +38,18 @@
diff --git a/frontend/apps/main/src/features/admin/status/dataTableColumns.js b/frontend/apps/main/src/features/admin/status/dataTableColumns.js
index e4c7845d..4bbd9b7e 100644
--- a/frontend/apps/main/src/features/admin/status/dataTableColumns.js
+++ b/frontend/apps/main/src/features/admin/status/dataTableColumns.js
@@ -3,6 +3,13 @@ import dropdown from './dataTableDropdown.vue'
import { format } from 'date-fns'
import { CONVERSATION_DEFAULT_STATUSES_LIST } from '@/constants/conversation.js'
+const DEFAULT_STATUS_KEY = {
+ Open: 'globals.terms.open',
+ Snoozed: 'globals.terms.snoozed',
+ Resolved: 'globals.terms.resolved',
+ Closed: 'globals.terms.closed'
+}
+
export const createColumns = (t, { onEdit } = {}) => [
{
accessorKey: 'name',
@@ -10,17 +17,28 @@ export const createColumns = (t, { onEdit } = {}) => [
return h('div', { class: 'text-center' }, t('globals.terms.name'))
},
cell: function ({ row }) {
- const isDefault = CONVERSATION_DEFAULT_STATUSES_LIST.includes(row.getValue('name'))
+ const name = row.getValue('name')
+ const isDefault = CONVERSATION_DEFAULT_STATUSES_LIST.includes(name)
+ const label = isDefault ? t(DEFAULT_STATUS_KEY[name]) : name
return h('div', { class: 'text-center' },
onEdit && !isDefault
? h('span', {
class: 'text-primary hover:underline cursor-pointer',
onClick: () => onEdit(row.original)
- }, row.getValue('name'))
- : row.getValue('name')
+ }, label)
+ : label
)
}
},
+ {
+ accessorKey: 'category',
+ header: function () {
+ return h('div', { class: 'text-center' }, t('globals.terms.category'))
+ },
+ cell: function ({ row }) {
+ return h('div', { class: 'text-center' }, t(`globals.terms.${row.getValue('category')}`))
+ }
+ },
{
accessorKey: 'created_at',
header: function () {
diff --git a/frontend/apps/main/src/features/admin/status/formSchema.js b/frontend/apps/main/src/features/admin/status/formSchema.js
index 812577ba..ee9e8a1f 100644
--- a/frontend/apps/main/src/features/admin/status/formSchema.js
+++ b/frontend/apps/main/src/features/admin/status/formSchema.js
@@ -15,5 +15,8 @@ export const createFormSchema = (t) => z.object({
min: 1,
max: 25,
})
- })
+ }),
+ category: z.enum(['open', 'waiting', 'resolved'], {
+ required_error: t('globals.messages.required'),
+ })
})
diff --git a/i18n/da.json b/i18n/da.json
index 56dc8b30..bb83379f 100644
--- a/i18n/da.json
+++ b/i18n/da.json
@@ -90,6 +90,7 @@
"admin.businessHours.setBusinessHours": "Indstil åbningstider",
"admin.contextLink.help.description": "Kontekstlinks vises i samtalens sidebjælke og lader agenter åbne eksterne værktøjer som CRM-systemer, faktureringssystemer eller interne dashboards, hvor kontaktens oplysninger automatisk sendes med.",
"admin.contextLink.help.detail": "Brug {'{{token}}'} til en fuldt krypteret payload, som den eksterne app dekrypterer med en delt hemmelighed. Eller brug individuelle variabler som {'{{email}}'}, {'{{phone}}'}, {'{{external_user_id}}'} til almindelige URL-links til betroede interne værktøjer.",
+ "admin.conversationStatus.category.placeholder": "Vælg kategori",
"admin.conversationStatus.name.description": "Opsæt statusnavn. Klik på Gem når færdig.",
"admin.conversationTags.edit.description": "Skift tagnavn. Klik på Gem når færdig.",
"admin.conversationTags.name.valid": "Tagnavn skal udgøre mindst 3 tegn",
@@ -365,7 +366,7 @@
"admin.team.help.description": "Konfigurer teamindstillinger inkl. arbejdstider og SLA-politikker.",
"admin.team.help.detail": "Administrer grænser for automatisk agenttildeling og mere.",
"admin.team.maxAutoAssigned": "Maks. automatisk tildelte samtaler",
- "admin.team.maxAutoAssigned.description": "Maks. antal samtaler, der kan tildeles automatisk til en agent. Samtaler med status \"Løst\" eller \"Lukket\" tæller ikke med. Sæt til 0 for ubegrænset.",
+ "admin.team.maxAutoAssigned.description": "Maks. antal samtaler, der kan tildeles automatisk til en agent. Kun samtaler med status i kategorien \"Åben\" tæller med i denne grænse. Sæt til 0 for ubegrænset.",
"admin.team.noPermissionBusinessHours": "Du har ikke tilladelse til at se åbningstider.",
"admin.team.slaPolicy.description": "SLA-politik, der automatisk anvendes på samtaler, når de tildeles dette team.",
"admin.team.slaPolicy.placeholder": "Vælg politik",
@@ -657,9 +658,11 @@
"globals.terms.brandName": "Brandnavn",
"globals.terms.businessHour": "Åbningstid | Åbningstider",
"globals.terms.callbackURL": "Tilbagekalds-URL",
+ "globals.terms.category": "Kategori",
"globals.terms.channel": "Kanal",
"globals.terms.clientID": "Klient-ID",
"globals.terms.clientSecret": "Klienthemmelighed",
+ "globals.terms.closed": "Lukket",
"globals.terms.closedAt": "Lukket kl.",
"globals.terms.collapse": "Sammenfold",
"globals.terms.contact": "Kontakt | Kontakter",
@@ -769,6 +772,7 @@
"globals.terms.report": "Rapport | Rapporter",
"globals.terms.required": "Obligatorisk",
"globals.terms.resolve": "Løs",
+ "globals.terms.resolved": "Løst",
"globals.terms.resolvedAt": "Løst kl.",
"globals.terms.role": "Rolle | Roller",
"globals.terms.rootURL": "Rod URL",
@@ -786,6 +790,7 @@
"globals.terms.smtpHost": "SMTP-vært | SMTP-værter",
"globals.terms.smtpPort": "SMTP-port | SMTP-porte",
"globals.terms.snooze": "Slumre",
+ "globals.terms.snoozed": "Slumret",
"globals.terms.solid": "Ensfarvet",
"globals.terms.someone": "Nogen",
"globals.terms.sso": "SSO | SSO'er",
@@ -819,6 +824,7 @@
"globals.terms.view": "Visning | Visninger",
"globals.terms.visibility": "Synlighed | Synligheder",
"globals.terms.visitor": "Besøgende | Besøgende",
+ "globals.terms.waiting": "Afventer",
"globals.terms.warning": "Advarsel | Advarsler",
"globals.terms.webhook": "WebHook | Webhooks",
"globals.terms.week": "Uge | Uger",
diff --git a/i18n/de.json b/i18n/de.json
index 526b07f9..34e6ab09 100644
--- a/i18n/de.json
+++ b/i18n/de.json
@@ -90,6 +90,7 @@
"admin.businessHours.setBusinessHours": "Öffnungszeiten auswählen",
"admin.contextLink.help.description": "Kontextlinks erscheinen in der Seitenleiste der Konversation und ermöglichen es Agenten, externe Tools wie CRMs, Abrechnungssysteme oder interne Dashboards zu öffnen, wobei die Kontaktdaten automatisch übergeben werden.",
"admin.contextLink.help.detail": "Verwenden Sie {'{{token}}'} für eine vollständig verschlüsselte Nutzlast, die die externe App mit einem gemeinsamen Schlüssel entschlüsselt. Oder verwenden Sie einzelne Variablen wie {'{{email}}'}, {'{{phone}}'}, {'{{external_user_id}}'} für einfache URL-Links zu vertrauenswürdigen internen Tools.",
+ "admin.conversationStatus.category.placeholder": "Kategorie auswählen",
"admin.conversationStatus.name.description": "Status-Name festlegen. Klicken auf Speichern, wenn du fertig bist.",
"admin.conversationTags.edit.description": "Ändere den Tag-Namen. Klicke auf Speichern, wenn du fertig bist.",
"admin.conversationTags.name.valid": "Tag-Name muss mindestens 3 Zeichen lang sein",
@@ -365,7 +366,7 @@
"admin.team.help.description": "Konfigurieren Sie Teameinstellungen einschließlich Arbeitszeiten und SLA-Richtlinien.",
"admin.team.help.detail": "Verwalten Sie Limits für die automatische Agentenzuweisung und mehr.",
"admin.team.maxAutoAssigned": "Maximale automatisch zugewiesene Konversationen",
- "admin.team.maxAutoAssigned.description": "Maximale Anzahl von Konversationen, die einem Agenten automatisch zugewiesen werden können. Konversationen mit dem Status \"Gelöst\" oder \"Geschlossen\" zählen nicht zu diesem Limit. Auf 0 setzen für unbegrenzt.",
+ "admin.team.maxAutoAssigned.description": "Maximale Anzahl von Konversationen, die einem Agenten automatisch zugewiesen werden können. Nur Konversationen mit Status in der Kategorie \"Offen\" zählen zu diesem Limit. Auf 0 setzen für unbegrenzt.",
"admin.team.noPermissionBusinessHours": "Sie haben keine Berechtigung, Geschäftszeiten anzuzeigen.",
"admin.team.slaPolicy.description": "SLA-Richtlinie, die automatisch auf Konversationen angewendet wird, wenn Konversationen diesem Team zugewiesen werden.",
"admin.team.slaPolicy.placeholder": "Richtlinie auswählen",
@@ -657,9 +658,11 @@
"globals.terms.brandName": "Markenname",
"globals.terms.businessHour": "Öffnungszeiten | Öffnungszeiten",
"globals.terms.callbackURL": "Callback-URL",
+ "globals.terms.category": "Kategorie",
"globals.terms.channel": "Kanal",
"globals.terms.clientID": "Client-ID",
"globals.terms.clientSecret": "Client-Geheimnis",
+ "globals.terms.closed": "Geschlossen",
"globals.terms.closedAt": "Geschlossen am",
"globals.terms.collapse": "Einklappen",
"globals.terms.contact": "Kontakt | Kontakte",
@@ -769,6 +772,7 @@
"globals.terms.report": "Bericht | Berichte",
"globals.terms.required": "Erforderlich",
"globals.terms.resolve": "Erledigt",
+ "globals.terms.resolved": "Gelöst",
"globals.terms.resolvedAt": "Gelöst am",
"globals.terms.role": "Rolle | Rollen",
"globals.terms.rootURL": "Basis-URL",
@@ -786,6 +790,7 @@
"globals.terms.smtpHost": "SMTP-Host | SMTP-Hosts",
"globals.terms.smtpPort": "SMTP-Port | SMTP-Ports",
"globals.terms.snooze": "Später erinnern",
+ "globals.terms.snoozed": "Zurückgestellt",
"globals.terms.solid": "Einfarbig",
"globals.terms.someone": "Jemand",
"globals.terms.sso": "SSO | SSO",
@@ -819,6 +824,7 @@
"globals.terms.view": "Ansicht | Ansichten",
"globals.terms.visibility": "Sichtbarkeit | Sichtbarkeit",
"globals.terms.visitor": "Besucher | Besucher",
+ "globals.terms.waiting": "Wartend",
"globals.terms.warning": "Warnung | Warnungen",
"globals.terms.webhook": "Webhook | Webhook",
"globals.terms.week": "Woche | Wochen",
diff --git a/i18n/en.json b/i18n/en.json
index 58ae7c04..9647cc4f 100644
--- a/i18n/en.json
+++ b/i18n/en.json
@@ -90,6 +90,7 @@
"admin.businessHours.setBusinessHours": "Set business hours",
"admin.contextLink.help.description": "Context links appear in the conversation sidebar, letting agents open external tools like CRMs, billing systems, or internal dashboards with the contact's details passed automatically.",
"admin.contextLink.help.detail": "Use {'{{token}}'} for a fully encrypted payload that the external app decrypts with a shared secret. Or use individual variables like {'{{email}}'}, {'{{phone}}'}, {'{{external_user_id}}'} for plain URL links to trusted internal tools.",
+ "admin.conversationStatus.category.placeholder": "Select category",
"admin.conversationStatus.name.description": "Set status name. Click save when you're done.",
"admin.conversationTags.edit.description": "Change the tag name. Click save when you're done.",
"admin.conversationTags.name.valid": "Tag name should at least 3 characters",
@@ -365,7 +366,7 @@
"admin.team.help.description": "Configure team settings including working hours and SLA policies.",
"admin.team.help.detail": "Manage agent auto-assignment limits and more.",
"admin.team.maxAutoAssigned": "Maximum auto-assigned conversations",
- "admin.team.maxAutoAssigned.description": "Maximum number of conversations that can be auto-assigned to an agent, conversations in \"Resolved\" or \"Closed\" states do not count toward this limit. Set to 0 for unlimited.",
+ "admin.team.maxAutoAssigned.description": "Maximum number of conversations that can be auto-assigned to an agent. Only conversations in statuses with the \"Open\" category count toward this limit. Set to 0 for unlimited.",
"admin.team.noPermissionBusinessHours": "You do not have permission to view business hours.",
"admin.team.slaPolicy.description": "SLA policy to be auto applied to conversations, when conversations are assigned to this team.",
"admin.team.slaPolicy.placeholder": "Select policy",
@@ -657,9 +658,11 @@
"globals.terms.brandName": "Brand name",
"globals.terms.businessHour": "Business hour | Business hours",
"globals.terms.callbackURL": "Callback URL",
+ "globals.terms.category": "Category",
"globals.terms.channel": "Channel",
"globals.terms.clientID": "Client ID",
"globals.terms.clientSecret": "Client secret",
+ "globals.terms.closed": "Closed",
"globals.terms.closedAt": "Closed at",
"globals.terms.collapse": "Collapse",
"globals.terms.contact": "Contact | Contacts",
@@ -769,6 +772,7 @@
"globals.terms.report": "Report | Reports",
"globals.terms.required": "Required",
"globals.terms.resolve": "Resolve",
+ "globals.terms.resolved": "Resolved",
"globals.terms.resolvedAt": "Resolved at",
"globals.terms.role": "Role | Roles",
"globals.terms.rootURL": "Root URL",
@@ -786,6 +790,7 @@
"globals.terms.smtpHost": "SMTP Host | SMTP Hosts",
"globals.terms.smtpPort": "SMTP Port | SMTP Ports",
"globals.terms.snooze": "Snooze",
+ "globals.terms.snoozed": "Snoozed",
"globals.terms.solid": "Solid",
"globals.terms.someone": "Someone",
"globals.terms.sso": "SSO | SSOs",
@@ -819,6 +824,7 @@
"globals.terms.view": "View | Views",
"globals.terms.visibility": "Visibility | Visibilities",
"globals.terms.visitor": "Visitor | Visitors",
+ "globals.terms.waiting": "Waiting",
"globals.terms.warning": "Warning | Warnings",
"globals.terms.webhook": "Webhook | Webhooks",
"globals.terms.week": "Week | Weeks",
diff --git a/i18n/es.json b/i18n/es.json
index 60e949c9..5041df16 100644
--- a/i18n/es.json
+++ b/i18n/es.json
@@ -90,6 +90,7 @@
"admin.businessHours.setBusinessHours": "Establecer horario de atención",
"admin.contextLink.help.description": "Los enlaces de contexto aparecen en la barra lateral de la conversación, permitiendo a los agentes abrir herramientas externas como CRMs, sistemas de facturación o paneles internos con los datos del contacto pasados automáticamente.",
"admin.contextLink.help.detail": "Usa {'{{token}}'} para un payload completamente cifrado que la aplicación externa descifra con un secreto compartido. O usa variables individuales como {'{{email}}'}, {'{{phone}}'}, {'{{external_user_id}}'} para enlaces URL en texto plano hacia herramientas internas de confianza.",
+ "admin.conversationStatus.category.placeholder": "Selecciona una categoría",
"admin.conversationStatus.name.description": "Establece el nombre del estado. Haz clic en guardar cuando termines.",
"admin.conversationTags.edit.description": "Cambia el nombre de la etiqueta. Haz clic en guardar cuando termines.",
"admin.conversationTags.name.valid": "El nombre de la etiqueta debe tener al menos 3 caracteres",
@@ -365,7 +366,7 @@
"admin.team.help.description": "Configura los ajustes del equipo incluyendo horario de trabajo y políticas SLA.",
"admin.team.help.detail": "Administra los límites de asignación automática de agentes y más.",
"admin.team.maxAutoAssigned": "Conversaciones máximas asignadas automáticamente",
- "admin.team.maxAutoAssigned.description": "Número máximo de conversaciones que se pueden asignar automáticamente a un agente. Las conversaciones en estado \"Resuelto\" o \"Cerrado\" no cuentan para este límite. Establece 0 para ilimitado.",
+ "admin.team.maxAutoAssigned.description": "Número máximo de conversaciones que se pueden asignar automáticamente a un agente. Solo las conversaciones en estados con la categoría \"Abierto\" cuentan para este límite. Establece 0 para ilimitado.",
"admin.team.noPermissionBusinessHours": "No tienes permiso para ver el horario de atención.",
"admin.team.slaPolicy.description": "Política SLA a aplicar automáticamente a las conversaciones cuando se asignen a este equipo.",
"admin.team.slaPolicy.placeholder": "Seleccionar política",
@@ -657,9 +658,11 @@
"globals.terms.brandName": "Nombre de marca",
"globals.terms.businessHour": "Horario de atención | Horario de trabajo",
"globals.terms.callbackURL": "URL de retorno (callback)",
+ "globals.terms.category": "Categoría",
"globals.terms.channel": "Canal",
"globals.terms.clientID": "ID del cliente",
"globals.terms.clientSecret": "Secreto del cliente",
+ "globals.terms.closed": "Cerrada",
"globals.terms.closedAt": "Cerrado el",
"globals.terms.collapse": "Contraer",
"globals.terms.contact": "Contacto | Contactos",
@@ -769,6 +772,7 @@
"globals.terms.report": "Informe | Informes",
"globals.terms.required": "Requerido",
"globals.terms.resolve": "Resolver",
+ "globals.terms.resolved": "Resuelto",
"globals.terms.resolvedAt": "Resuelto el",
"globals.terms.role": "Rol | Roles",
"globals.terms.rootURL": "URL raíz",
@@ -786,6 +790,7 @@
"globals.terms.smtpHost": "Host SMTP | Hosts SMTP",
"globals.terms.smtpPort": "Puerto SMTP | Puertos SMTP",
"globals.terms.snooze": "Posponer",
+ "globals.terms.snoozed": "Pospuesta",
"globals.terms.solid": "Sólido",
"globals.terms.someone": "Alguien",
"globals.terms.sso": "SSO | SSO",
@@ -819,6 +824,7 @@
"globals.terms.view": "Vista | Vistas",
"globals.terms.visibility": "Visibilidad | Visibilidades",
"globals.terms.visitor": "Visitante | Visitantes",
+ "globals.terms.waiting": "En espera",
"globals.terms.warning": "Advertencia | Advertencias",
"globals.terms.webhook": "Web Hook | Hooks web",
"globals.terms.week": "Semana | Semanas",
diff --git a/i18n/fa.json b/i18n/fa.json
index 04125c48..bc097f4d 100644
--- a/i18n/fa.json
+++ b/i18n/fa.json
@@ -90,6 +90,7 @@
"admin.businessHours.setBusinessHours": "تنظیم ساعات کاری",
"admin.contextLink.help.description": "لینکهای زمینهای در نوار کناری مکالمه نمایش داده میشوند و به نمایندگان امکان میدهند ابزارهای خارجی مانند CRM، سیستمهای صورتحساب یا داشبوردهای داخلی را با ارسال خودکار اطلاعات مخاطب باز کنند.",
"admin.contextLink.help.detail": "از {'{{token}}'} برای ارسال بار رمزگذاریشده استفاده کنید که اپلیکیشن خارجی آن را با یک رمز مشترک رمزگشایی میکند. یا از متغیرهای جداگانه مانند {'{{email}}'}، {'{{phone}}'}، {'{{external_user_id}}'} برای لینکهای URL ساده به ابزارهای داخلی مورد اعتماد استفاده کنید.",
+ "admin.conversationStatus.category.placeholder": "انتخاب دستهبندی",
"admin.conversationStatus.name.description": "نام وضعیت را تنظیم کنید. پس از اتمام، ذخیره را کلیک کنید.",
"admin.conversationTags.edit.description": "نام برچسب را تغییر دهید. پس از اتمام، ذخیره را کلیک کنید.",
"admin.conversationTags.name.valid": "نام برچسب باید حداقل 3 کاراکتر باشد",
@@ -365,7 +366,7 @@
"admin.team.help.description": "تنظیمات تیم از جمله ساعات کاری و سیاستهای SLA را پیکربندی کنید.",
"admin.team.help.detail": "محدودیتهای تخصیص خودکار کارشناسان و موارد دیگر را مدیریت کنید.",
"admin.team.maxAutoAssigned": "حداکثر مکالمات تخصیص خودکار",
- "admin.team.maxAutoAssigned.description": "حداکثر تعداد مکالماتی که میتواند به صورت خودکار به یک کارشناس تخصیص یابد. مکالمات در وضعیت «حلشده» یا «بسته» در این محدودیت محاسبه نمیشوند. برای نامحدود 0 تنظیم کنید.",
+ "admin.team.maxAutoAssigned.description": "حداکثر تعداد مکالماتی که میتواند به صورت خودکار به یک کارشناس تخصیص یابد. فقط مکالماتی که وضعیت آنها در دستهبندی «باز» قرار دارد در این محدودیت محاسبه میشوند. برای نامحدود 0 تنظیم کنید.",
"admin.team.noPermissionBusinessHours": "شما مجوز مشاهده ساعات کاری را ندارید.",
"admin.team.slaPolicy.description": "سیاست SLA که به صورت خودکار هنگام تخصیص مکالمات به این تیم اعمال میشود.",
"admin.team.slaPolicy.placeholder": "انتخاب سیاست",
@@ -657,9 +658,11 @@
"globals.terms.brandName": "نام برند",
"globals.terms.businessHour": "ساعت کاری | ساعات کاری",
"globals.terms.callbackURL": "URL بازگشت",
+ "globals.terms.category": "دستهبندی",
"globals.terms.channel": "کانال",
"globals.terms.clientID": "شناسه کلاینت",
"globals.terms.clientSecret": "رمز کلاینت",
+ "globals.terms.closed": "بستهشده",
"globals.terms.closedAt": "بسته شده در",
"globals.terms.collapse": "جمع کردن",
"globals.terms.contact": "مخاطب | مخاطبان",
@@ -769,6 +772,7 @@
"globals.terms.report": "گزارش | گزارشها",
"globals.terms.required": "الزامی",
"globals.terms.resolve": "حل کردن",
+ "globals.terms.resolved": "حلشده",
"globals.terms.resolvedAt": "حل شده در",
"globals.terms.role": "نقش | نقشها",
"globals.terms.rootURL": "URL ریشه",
@@ -786,6 +790,7 @@
"globals.terms.smtpHost": "میزبان SMTP | میزبانهای SMTP",
"globals.terms.smtpPort": "پورت SMTP | پورتهای SMTP",
"globals.terms.snooze": "تعویق",
+ "globals.terms.snoozed": "به تعویق افتاده",
"globals.terms.solid": "تکرنگ",
"globals.terms.someone": "شخصی",
"globals.terms.sso": "SSO | SSO",
@@ -819,6 +824,7 @@
"globals.terms.view": "نمایش | نمایشها",
"globals.terms.visibility": "قابلیت مشاهده | قابلیت مشاهده",
"globals.terms.visitor": "بازدیدکننده | بازدیدکنندگان",
+ "globals.terms.waiting": "در انتظار",
"globals.terms.warning": "هشدار | هشدارها",
"globals.terms.webhook": "وبهوک | وبهوک ها",
"globals.terms.week": "هفته | هفتهها",
diff --git a/i18n/fr.json b/i18n/fr.json
index 6faaf110..99dcf5ee 100644
--- a/i18n/fr.json
+++ b/i18n/fr.json
@@ -90,6 +90,7 @@
"admin.businessHours.setBusinessHours": "Fixer les heures d'ouverture",
"admin.contextLink.help.description": "Les liens contextuels apparaissent dans la barre latérale de la conversation, permettant aux agents d'ouvrir des outils externes comme des CRM, des systèmes de facturation ou des tableaux de bord internes avec les coordonnées du contact transmises automatiquement.",
"admin.contextLink.help.detail": "Utilisez {'{{token}}'} pour un payload entièrement chiffré que l'application externe déchiffre avec un secret partagé. Ou utilisez des variables individuelles comme {'{{email}}'}, {'{{phone}}'}, {'{{external_user_id}}'} pour des liens URL en clair vers des outils internes de confiance.",
+ "admin.conversationStatus.category.placeholder": "Sélectionner une catégorie",
"admin.conversationStatus.name.description": "Définissez le nom de l'état. Cliquez sur enregistrer lorsque vous avez terminé.",
"admin.conversationTags.edit.description": "Modifiez le nom de la balise. Cliquez sur enregistrer lorsque vous avez terminé.",
"admin.conversationTags.name.valid": "Le nom de l'étiquette doit comporter au moins 3 caractères",
@@ -365,7 +366,7 @@
"admin.team.help.description": "Configurez les paramètres de l'équipe, y compris les heures de travail et les politiques SLA.",
"admin.team.help.detail": "Gérez les limites d'auto-assignation des agents et plus encore.",
"admin.team.maxAutoAssigned": "Conversations auto-assignées maximum",
- "admin.team.maxAutoAssigned.description": "Nombre maximum de conversations pouvant être auto-assignées à un agent. Les conversations en statut « Résolu » ou « Fermé » ne comptent pas. Définir à 0 pour illimité.",
+ "admin.team.maxAutoAssigned.description": "Nombre maximum de conversations pouvant être auto-assignées à un agent. Seules les conversations dont le statut appartient à la catégorie « Ouvert » comptent dans cette limite. Définir à 0 pour illimité.",
"admin.team.noPermissionBusinessHours": "Vous n'avez pas la permission de voir les horaires d'ouverture.",
"admin.team.slaPolicy.description": "Politique SLA appliquée automatiquement aux conversations lorsqu'elles sont assignées à cette équipe.",
"admin.team.slaPolicy.placeholder": "Sélectionner une politique",
@@ -657,9 +658,11 @@
"globals.terms.brandName": "Nom de marque",
"globals.terms.businessHour": "Horaire d’ouverture | Horaires d'ouverture",
"globals.terms.callbackURL": "URL de callback",
+ "globals.terms.category": "Catégorie",
"globals.terms.channel": "Canal",
"globals.terms.clientID": "ID du client",
"globals.terms.clientSecret": "Clé secrète du client",
+ "globals.terms.closed": "Fermée",
"globals.terms.closedAt": "Fermé le",
"globals.terms.collapse": "Réduire",
"globals.terms.contact": "Contact | Contacts",
@@ -769,6 +772,7 @@
"globals.terms.report": "Rapport | Rapports",
"globals.terms.required": "Requis",
"globals.terms.resolve": "Résoudre",
+ "globals.terms.resolved": "Résolu",
"globals.terms.resolvedAt": "Résolu le",
"globals.terms.role": "Rôle | Rôles",
"globals.terms.rootURL": "URL racine",
@@ -786,6 +790,7 @@
"globals.terms.smtpHost": "Hôte SMTP | Hôtes SMTP",
"globals.terms.smtpPort": "Port SMTP | Ports SMTP",
"globals.terms.snooze": "Reporter",
+ "globals.terms.snoozed": "En pause",
"globals.terms.solid": "Uni",
"globals.terms.someone": "Quelqu'un",
"globals.terms.sso": "SSO | SSO",
@@ -819,6 +824,7 @@
"globals.terms.view": "Vue | Vues",
"globals.terms.visibility": "Visibilité | Visibilités",
"globals.terms.visitor": "Visiteur | Visiteurs",
+ "globals.terms.waiting": "En attente",
"globals.terms.warning": "Avertissement | Avertissements",
"globals.terms.webhook": "Webhook | Webhooks",
"globals.terms.week": "Semaine | Semaines",
diff --git a/i18n/it.json b/i18n/it.json
index d7ee5e09..11136d71 100644
--- a/i18n/it.json
+++ b/i18n/it.json
@@ -90,6 +90,7 @@
"admin.businessHours.setBusinessHours": "Imposta l'orario di lavoro",
"admin.contextLink.help.description": "I link contestuali appaiono nella barra laterale della conversazione, consentendo agli agenti di aprire strumenti esterni come CRM, sistemi di fatturazione o dashboard interne con i dettagli del contatto passati automaticamente.",
"admin.contextLink.help.detail": "Usa {'{{token}}'} per un payload completamente crittografato che l'app esterna decrittografa con un segreto condiviso. Oppure usa variabili individuali come {'{{email}}'}, {'{{phone}}'}, {'{{external_user_id}}'} per link URL in chiaro verso strumenti interni affidabili.",
+ "admin.conversationStatus.category.placeholder": "Seleziona categoria",
"admin.conversationStatus.name.description": "Imposta il nome di stato. Fai clic su Salva quando hai finito.",
"admin.conversationTags.edit.description": "Cambia il nome del tag. Fai clic su Salva quando hai finito.",
"admin.conversationTags.name.valid": "Il nome del sito deve essere di almeno 3 carattere",
@@ -365,7 +366,7 @@
"admin.team.help.description": "Configura le impostazioni del team inclusi orari lavorativi e policy SLA.",
"admin.team.help.detail": "Gestisci i limiti di assegnazione automatica degli agenti e altro.",
"admin.team.maxAutoAssigned": "Conversazioni massime auto-assegnate",
- "admin.team.maxAutoAssigned.description": "Numero massimo di conversazioni che possono essere auto-assegnate a un agente. Le conversazioni in stato \"Risolto\" o \"Chiuso\" non contano per questo limite. Imposta 0 per illimitato.",
+ "admin.team.maxAutoAssigned.description": "Numero massimo di conversazioni che possono essere auto-assegnate a un agente. Solo le conversazioni in stati con categoria \"Aperto\" contano per questo limite. Imposta 0 per illimitato.",
"admin.team.noPermissionBusinessHours": "Non hai i permessi per visualizzare gli orari di attività.",
"admin.team.slaPolicy.description": "Policy SLA da applicare automaticamente alle conversazioni quando vengono assegnate a questo team.",
"admin.team.slaPolicy.placeholder": "Seleziona policy",
@@ -657,9 +658,11 @@
"globals.terms.brandName": "Nome del marchio",
"globals.terms.businessHour": "Orario attività | Orari attività",
"globals.terms.callbackURL": "URL di callback",
+ "globals.terms.category": "Categoria",
"globals.terms.channel": "Canale",
"globals.terms.clientID": "ID client",
"globals.terms.clientSecret": "Client Secret",
+ "globals.terms.closed": "Chiusa",
"globals.terms.closedAt": "Chiuso il ",
"globals.terms.collapse": "Riduci",
"globals.terms.contact": "Contatto | Contatti",
@@ -769,6 +772,7 @@
"globals.terms.report": "Relazione | Relazioni",
"globals.terms.required": "Richiesto",
"globals.terms.resolve": "Risolvi",
+ "globals.terms.resolved": "Risolto",
"globals.terms.resolvedAt": "Risolto il",
"globals.terms.role": "Ruolo | Ruoli",
"globals.terms.rootURL": "URL base",
@@ -786,6 +790,7 @@
"globals.terms.smtpHost": "Host SMTP | Hosts SMTP",
"globals.terms.smtpPort": "Porta SMTP | Porta SMTP",
"globals.terms.snooze": "Posticipa",
+ "globals.terms.snoozed": "Posticipata",
"globals.terms.solid": "Tinta unita",
"globals.terms.someone": "Qualcuno",
"globals.terms.sso": "SSO | SSO",
@@ -819,6 +824,7 @@
"globals.terms.view": "Vista | Viste",
"globals.terms.visibility": "Visibilità | Visibilità",
"globals.terms.visitor": "Visitatore | Visitatori",
+ "globals.terms.waiting": "In attesa",
"globals.terms.warning": "Attenzione | Avvisi",
"globals.terms.webhook": "Webhook | Webhook",
"globals.terms.week": "Settimana | Settimane",
diff --git a/i18n/ja.json b/i18n/ja.json
index cc1024a0..286fff2b 100644
--- a/i18n/ja.json
+++ b/i18n/ja.json
@@ -90,6 +90,7 @@
"admin.businessHours.setBusinessHours": "営業時間を設定",
"admin.contextLink.help.description": "コンテキストリンクは会話サイドバーに表示され、エージェントがCRM、請求システム、社内ダッシュボードなどの外部ツールを、コンタクトの情報を自動的に渡して開くことができます。",
"admin.contextLink.help.detail": "{'{{token}}'} を使用すると、外部アプリが共有シークレットで復号する完全に暗号化されたペイロードになります。または {'{{email}}'}、{'{{phone}}'}、{'{{external_user_id}}'} などの個別変数を使用して、信頼できる社内ツールへのプレーンURLリンクを作成できます。",
+ "admin.conversationStatus.category.placeholder": "カテゴリーを選択",
"admin.conversationStatus.name.description": "ステータス名を設定してください。設定が終わったら「保存」をクリックしてください。",
"admin.conversationTags.edit.description": "タグ名を変更してください。変更が終わったら「保存」をクリックしてください。",
"admin.conversationTags.name.valid": "タグ名は少なくとも 3 文字必要です",
@@ -365,7 +366,7 @@
"admin.team.help.description": "営業時間やSLAポリシーを含むチーム設定を構成します。",
"admin.team.help.detail": "エージェントの自動割り当て上限などを管理します。",
"admin.team.maxAutoAssigned": "自動割り当て会話の最大数",
- "admin.team.maxAutoAssigned.description": "エージェントに自動割り当てできる会話の最大数。「解決済み」または「クローズ」状態の会話はこの上限にカウントされません。0に設定すると無制限です。",
+ "admin.team.maxAutoAssigned.description": "エージェントに自動割り当てできる会話の最大数。「オープン」カテゴリーのステータスの会話のみがこの上限にカウントされます。0に設定すると無制限です。",
"admin.team.noPermissionBusinessHours": "営業時間を表示する権限がありません。",
"admin.team.slaPolicy.description": "会話がこのチームに割り当てられた際に自動適用されるSLAポリシー。",
"admin.team.slaPolicy.placeholder": "ポリシーを選択",
@@ -657,9 +658,11 @@
"globals.terms.brandName": "ブランド名",
"globals.terms.businessHour": "営業時間",
"globals.terms.callbackURL": "コールバック URL",
+ "globals.terms.category": "カテゴリー",
"globals.terms.channel": "チャンネル",
"globals.terms.clientID": "クライアントID",
"globals.terms.clientSecret": "クライアントシークレット",
+ "globals.terms.closed": "クローズ済み",
"globals.terms.closedAt": "クローズ日時",
"globals.terms.collapse": "折りたたむ",
"globals.terms.contact": "連絡先",
@@ -769,6 +772,7 @@
"globals.terms.report": "レポート",
"globals.terms.required": "必須",
"globals.terms.resolve": "解決",
+ "globals.terms.resolved": "解決済み",
"globals.terms.resolvedAt": "解決日時",
"globals.terms.role": "役割",
"globals.terms.rootURL": "ルート URL",
@@ -786,6 +790,7 @@
"globals.terms.smtpHost": "SMTP ホスト",
"globals.terms.smtpPort": "SMTP ポート",
"globals.terms.snooze": "スヌーズ",
+ "globals.terms.snoozed": "スヌーズ中",
"globals.terms.solid": "単色",
"globals.terms.someone": "誰か",
"globals.terms.sso": "SSO",
@@ -819,6 +824,7 @@
"globals.terms.view": "表示",
"globals.terms.visibility": "公開範囲",
"globals.terms.visitor": "訪問者",
+ "globals.terms.waiting": "待機中",
"globals.terms.warning": "警告",
"globals.terms.webhook": "Webhook",
"globals.terms.week": "週",
diff --git a/i18n/mr.json b/i18n/mr.json
index a6a1c45e..7ed3649d 100644
--- a/i18n/mr.json
+++ b/i18n/mr.json
@@ -90,6 +90,7 @@
"admin.businessHours.setBusinessHours": "व्यवसाय तास सेट करा",
"admin.contextLink.help.description": "संदर्भ दुवे संभाषण साइडबारमध्ये दिसतात, ज्यामुळे एजंट संपर्काची माहिती आपोआप पाठवून CRM, बिलिंग प्रणाली किंवा अंतर्गत डॅशबोर्ड यांसारखी बाह्य साधने उघडू शकतात.",
"admin.contextLink.help.detail": "संपूर्ण एन्क्रिप्टेड पेलोडसाठी {'{{token}}'} वापरा जो बाह्य अॅप सामायिक गुपिताने डिक्रिप्ट करतो. किंवा विश्वसनीय अंतर्गत साधनांसाठी सादा URL दुव्यांमध्ये {'{{email}}'}, {'{{phone}}'}, {'{{external_user_id}}'} सारखे वैयक्तिक व्हेरिएबल्स वापरा.",
+ "admin.conversationStatus.category.placeholder": "श्रेणी निवडा",
"admin.conversationStatus.name.description": "स्थिती नाव सेट करा. पूर्ण झाल्यावर जतन करा क्लिक करा.",
"admin.conversationTags.edit.description": "टॅग नाव बदला. पूर्ण झाल्यावर जतन करा क्लिक करा.",
"admin.conversationTags.name.valid": "टॅग नाव किमान 3 अक्षरे असावे",
@@ -365,7 +366,7 @@
"admin.team.help.description": "कामाचे तास आणि SLA धोरणांसह संघ सेटिंग्ज कॉन्फिगर करा.",
"admin.team.help.detail": "एजंट स्वयं-नियुक्ती मर्यादा आणि बरेच काही व्यवस्थापित करा.",
"admin.team.maxAutoAssigned": "कमाल स्वयं-नियुक्त संभाषणे",
- "admin.team.maxAutoAssigned.description": "एजंटला स्वयं-नियुक्त करता येणाऱ्या संभाषणांची कमाल संख्या, \"सोडवलेली\" किंवा \"बंद\" स्थितीतील संभाषणे या मर्यादेत गणली जात नाहीत. अमर्यादित करण्यासाठी 0 सेट करा.",
+ "admin.team.maxAutoAssigned.description": "एजंटला स्वयं-नियुक्त करता येणाऱ्या संभाषणांची कमाल संख्या. फक्त \"उघडे\" श्रेणीतील स्थितीत असलेली संभाषणे या मर्यादेत गणली जातात. अमर्यादित करण्यासाठी 0 सेट करा.",
"admin.team.noPermissionBusinessHours": "व्यवसाय तास पाहण्याची तुम्हाला परवानगी नाही.",
"admin.team.slaPolicy.description": "संभाषणे या संघाला नियुक्त केली जातात तेव्हा स्वयं लागू होणारे SLA धोरण.",
"admin.team.slaPolicy.placeholder": "धोरण निवडा",
@@ -657,9 +658,11 @@
"globals.terms.brandName": "ब्रँड नाव",
"globals.terms.businessHour": "व्यावसायिक तास | व्यावसायिक तास",
"globals.terms.callbackURL": "कॉलबॅक URL",
+ "globals.terms.category": "श्रेणी",
"globals.terms.channel": "चॅनेल",
"globals.terms.clientID": "क्लायंट ID",
"globals.terms.clientSecret": "क्लायंट गुप्त",
+ "globals.terms.closed": "बंद",
"globals.terms.closedAt": "बंद केले",
"globals.terms.collapse": "कोलॅप्स करा",
"globals.terms.contact": "संपर्क | संपर्क",
@@ -769,6 +772,7 @@
"globals.terms.report": "रिपोर्ट | रिपोर्ट",
"globals.terms.required": "आवश्यक",
"globals.terms.resolve": "सोडवा",
+ "globals.terms.resolved": "सोडवलेले",
"globals.terms.resolvedAt": "सोडवले",
"globals.terms.role": "भूमिका | भूमिका",
"globals.terms.rootURL": "रूट URL",
@@ -786,6 +790,7 @@
"globals.terms.smtpHost": "SMTP होस्ट | SMTP होस्ट्स",
"globals.terms.smtpPort": "SMTP पोर्ट | SMTP पोर्ट्स",
"globals.terms.snooze": "स्नूझ",
+ "globals.terms.snoozed": "स्नूझ केलेले",
"globals.terms.solid": "सॉलिड",
"globals.terms.someone": "कोणीतरी",
"globals.terms.sso": "एसएसओ | एसएसओ",
@@ -819,6 +824,7 @@
"globals.terms.view": "दृश्य | दृश्ये",
"globals.terms.visibility": "दृश्यता | दृश्यता",
"globals.terms.visitor": "व्हिजिटर | व्हिजिटर",
+ "globals.terms.waiting": "प्रतीक्षा",
"globals.terms.warning": "वॉर्निंग | वॉर्निंग",
"globals.terms.webhook": "वेबहुक | वेबहुक्स",
"globals.terms.week": "आठवडा | आठवडे",
diff --git a/internal/conversation/models/models.go b/internal/conversation/models/models.go
index e3c0eaa5..98bde15a 100644
--- a/internal/conversation/models/models.go
+++ b/internal/conversation/models/models.go
@@ -497,6 +497,7 @@ type Status struct {
ID int `db:"id" json:"id"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
Name string `db:"name" json:"name"`
+ Category string `db:"category" json:"category"`
}
type Priority struct {
diff --git a/internal/conversation/queries.sql b/internal/conversation/queries.sql
index 9e5951e0..50ae6d69 100644
--- a/internal/conversation/queries.sql
+++ b/internal/conversation/queries.sql
@@ -331,16 +331,19 @@ WHERE uuid = $1;
-- name: update-conversation-status
+WITH new_status AS (
+ SELECT id, category FROM conversation_statuses WHERE name = $2
+)
UPDATE conversations
-SET status_id = (SELECT id FROM conversation_statuses WHERE name = $2),
- resolved_at = COALESCE(resolved_at, CASE WHEN $2 IN ('Resolved', 'Closed') THEN NOW() END),
- closed_at = COALESCE(closed_at, CASE WHEN $2 = 'Closed' THEN NOW() END),
+SET status_id = (SELECT id FROM new_status),
+ resolved_at = COALESCE(resolved_at, CASE WHEN (SELECT category FROM new_status) = 'resolved' THEN NOW() END),
+ closed_at = COALESCE(closed_at, CASE WHEN $2 = 'Closed' THEN NOW() END),
snoozed_until = CASE WHEN $2 = 'Snoozed' THEN $3::timestamptz ELSE snoozed_until END,
- updated_at = NOW()
+ updated_at = NOW()
WHERE uuid = $1;
-- name: get-user-active-conversations-count
-SELECT COUNT(*) FROM conversations WHERE status_id IN (SELECT id FROM conversation_statuses WHERE name NOT IN ('Resolved', 'Closed')) and assigned_user_id = $1;
+SELECT COUNT(*) FROM conversations WHERE status_id IN (SELECT id FROM conversation_statuses WHERE category = 'open') AND assigned_user_id = $1;
-- name: update-conversation-priority
UPDATE conversations
@@ -449,7 +452,7 @@ WHERE m.uuid = $1;
UPDATE conversations
SET assigned_user_id = NULL,
updated_at = NOW()
-WHERE assigned_user_id = $1 AND status_id in (SELECT id FROM conversation_statuses WHERE name NOT IN ('Resolved', 'Closed'));
+WHERE assigned_user_id = $1 AND status_id IN (SELECT id FROM conversation_statuses WHERE category != 'resolved');
-- name: update-conversation-custom-attributes
UPDATE conversations
diff --git a/internal/conversation/status/models/models.go b/internal/conversation/status/models/models.go
index e5b50109..286bb06a 100644
--- a/internal/conversation/status/models/models.go
+++ b/internal/conversation/status/models/models.go
@@ -9,8 +9,21 @@ var DefaultStatuses = []string{
"Closed",
}
+const (
+ CategoryOpen = "open"
+ CategoryWaiting = "waiting"
+ CategoryResolved = "resolved"
+)
+
+var ValidCategories = []string{
+ CategoryOpen,
+ CategoryWaiting,
+ CategoryResolved,
+}
+
type Status struct {
ID int `db:"id" json:"id"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
Name string `db:"name" json:"name"`
+ Category string `db:"category" json:"category"`
}
diff --git a/internal/conversation/status/queries.sql b/internal/conversation/status/queries.sql
index 8fb7158b..83a09955 100644
--- a/internal/conversation/status/queries.sql
+++ b/internal/conversation/status/queries.sql
@@ -1,21 +1,23 @@
-- name: get-status
-select id,
+select id,
created_at,
- name
+ name,
+ category
from conversation_statuses
where id = $1;
-- name: get-all-statuses
-select id,
+select id,
created_at,
- name
+ name,
+ category
from conversation_statuses;
-- name: insert-status
-INSERT into conversation_statuses(name) values ($1) RETURNING *;
+INSERT into conversation_statuses(name, category) values ($1, $2) RETURNING *;
-- name: delete-status
DELETE from conversation_statuses where id = $1;
-- name: update-status
-UPDATE conversation_statuses set name = $2 where id = $1 RETURNING *;
\ No newline at end of file
+UPDATE conversation_statuses set name = $2, category = $3 where id = $1 RETURNING *;
diff --git a/internal/conversation/status/status.go b/internal/conversation/status/status.go
index f08dd27c..a0a3df70 100644
--- a/internal/conversation/status/status.go
+++ b/internal/conversation/status/status.go
@@ -70,12 +70,15 @@ func (m *Manager) GetAll() ([]models.Status, error) {
}
// Create creates a new status.
-func (m *Manager) Create(name string) (models.Status, error) {
+func (m *Manager) Create(name, category string) (models.Status, error) {
var status models.Status
if err := m.validateStatusName(name); err != nil {
return status, err
}
- if err := m.q.InsertStatus.Get(&status, name); err != nil {
+ if err := m.validateCategory(category); err != nil {
+ return status, err
+ }
+ if err := m.q.InsertStatus.Get(&status, name, category); err != nil {
m.lo.Error("error inserting status", "error", err)
return status, envelope.NewError(envelope.GeneralError, m.i18n.T("globals.messages.somethingWentWrong"), nil)
}
@@ -105,22 +108,25 @@ func (m *Manager) Delete(id int) error {
}
// Update updates a status by id.
-func (m *Manager) Update(id int, name string) (models.Status, error) {
+func (m *Manager) Update(id int, name, category string) (models.Status, error) {
var updatedStatus models.Status
if err := m.validateStatusName(name); err != nil {
return updatedStatus, err
}
- // Disallow updating of default statuses.
+ if err := m.validateCategory(category); err != nil {
+ return updatedStatus, err
+ }
status, err := m.Get(id)
if err != nil {
return updatedStatus, envelope.NewError(envelope.GeneralError, m.i18n.T("globals.messages.somethingWentWrong"), nil)
}
+ // Default statuses are locked. Silently no-op and return the existing row.
if slices.Contains(models.DefaultStatuses, status.Name) {
- return updatedStatus, envelope.NewError(envelope.InputError, m.i18n.T("conversationStatus.cannotUpdateDefault"), nil)
+ return status, nil
}
- if err := m.q.UpdateStatus.Get(&updatedStatus, id, name); err != nil {
+ if err := m.q.UpdateStatus.Get(&updatedStatus, id, name, category); err != nil {
m.lo.Error("error updating status", "error", err)
return updatedStatus, envelope.NewError(envelope.GeneralError, m.i18n.T("globals.messages.somethingWentWrong"), nil)
}
@@ -147,3 +153,11 @@ func (m *Manager) validateStatusName(name string) error {
}
return nil
}
+
+// validateCategory checks that the category is one of the allowed values.
+func (m *Manager) validateCategory(category string) error {
+ if !slices.Contains(models.ValidCategories, category) {
+ return envelope.NewError(envelope.InputError, m.i18n.Ts("validation.invalidFields", "name", "`category`"), nil)
+ }
+ return nil
+}
diff --git a/internal/migrations/v2.2.0.go b/internal/migrations/v2.2.0.go
new file mode 100644
index 00000000..49c734c2
--- /dev/null
+++ b/internal/migrations/v2.2.0.go
@@ -0,0 +1,41 @@
+package migrations
+
+import (
+ "github.com/jmoiron/sqlx"
+ "github.com/knadh/koanf/v2"
+ "github.com/knadh/stuffbin"
+)
+
+func V2_2_0(db *sqlx.DB, fs stuffbin.FileSystem, ko *koanf.Koanf) error {
+ _, err := db.Exec(`
+ DO $$
+ BEGIN
+ IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'conversation_status_category') THEN
+ CREATE TYPE conversation_status_category AS ENUM ('open', 'waiting', 'resolved');
+ END IF;
+ END$$;
+ `)
+ if err != nil {
+ return err
+ }
+
+ _, err = db.Exec(`
+ ALTER TABLE conversation_statuses
+ ADD COLUMN IF NOT EXISTS category conversation_status_category NOT NULL DEFAULT 'open';
+ `)
+ if err != nil {
+ return err
+ }
+
+ _, err = db.Exec(`UPDATE conversation_statuses SET category = 'waiting' WHERE name = 'Snoozed'`)
+ if err != nil {
+ return err
+ }
+
+ _, err = db.Exec(`UPDATE conversation_statuses SET category = 'resolved' WHERE name IN ('Resolved', 'Closed')`)
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
diff --git a/internal/report/queries.sql b/internal/report/queries.sql
index 017c0ff7..e86e6189 100644
--- a/internal/report/queries.sql
+++ b/internal/report/queries.sql
@@ -70,7 +70,7 @@ FROM
conversations c
INNER JOIN conversation_statuses s ON c.status_id = s.id
WHERE
- s.name not in ('Resolved', 'Closed');
+ s.category != 'resolved';
-- name: get-overview-sla-counts
WITH first_and_resolution AS (
diff --git a/internal/sla/models/models.go b/internal/sla/models/models.go
index 547ad231..ded6b61e 100644
--- a/internal/sla/models/models.go
+++ b/internal/sla/models/models.go
@@ -90,6 +90,7 @@ type AppliedSLA struct {
ConversationSubject string `db:"conversation_subject"`
ConversationAssignedUserID null.Int `db:"conversation_assigned_user_id"`
ConversationStatus string `db:"conversation_status"`
+ ConversationStatusCategory string `db:"conversation_status_category"`
}
type SLAEvent struct {
diff --git a/internal/sla/queries.sql b/internal/sla/queries.sql
index cedb362c..75268d8c 100644
--- a/internal/sla/queries.sql
+++ b/internal/sla/queries.sql
@@ -74,8 +74,8 @@ WHERE id = $1;
-- name: update-conversation-sla-deadline
UPDATE conversations c
SET next_sla_deadline_at = CASE
- -- If resolved or closed, clear the deadline
- WHEN c.status_id IN (SELECT id FROM conversation_statuses WHERE name IN ('Resolved', 'Closed')) THEN NULL
+ -- If the conversation is in a resolved-category status, clear the deadline
+ WHEN c.status_id IN (SELECT id FROM conversation_statuses WHERE category = 'resolved') THEN NULL
-- If an external timestamp ($2) is provided (e.g. next_response), use the earliest of $2.
WHEN $2::TIMESTAMPTZ IS NOT NULL THEN LEAST(
@@ -149,7 +149,8 @@ SELECT a.id,
c.reference_number as conversation_reference_number,
c.subject as conversation_subject,
c.assigned_user_id as conversation_assigned_user_id,
- s.name as conversation_status
+ s.name as conversation_status,
+ s.category as conversation_status_category
FROM applied_slas a INNER JOIN conversations c on a.conversation_id = c.id
LEFT JOIN conversation_statuses s ON c.status_id = s.id
WHERE a.id = $1;
diff --git a/internal/sla/sla.go b/internal/sla/sla.go
index 887af081..b2a7da63 100644
--- a/internal/sla/sla.go
+++ b/internal/sla/sla.go
@@ -13,7 +13,7 @@ import (
businesshours "github.com/abhinavxd/libredesk/internal/business_hours"
bmodels "github.com/abhinavxd/libredesk/internal/business_hours/models"
- cmodels "github.com/abhinavxd/libredesk/internal/conversation/models"
+ cstatusmodels "github.com/abhinavxd/libredesk/internal/conversation/status/models"
"github.com/abhinavxd/libredesk/internal/dbutil"
"github.com/abhinavxd/libredesk/internal/envelope"
notifier "github.com/abhinavxd/libredesk/internal/notification"
@@ -544,9 +544,9 @@ func (m *Manager) SendNotification(scheduledNotification models.ScheduledSLANoti
return fmt.Errorf("fetching applied SLA for notification: %w", err)
}
- // If conversation is `Resolved` / `Closed`, mark the notification as processed and return.
- if appliedSLA.ConversationStatus == cmodels.StatusResolved || appliedSLA.ConversationStatus == cmodels.StatusClosed {
- m.lo.Info("marking sla notification as processed as the conversation is resolved/closed", "status", appliedSLA.ConversationStatus, "scheduled_notification_id", scheduledNotification.ID)
+ // Any status in the resolved category is terminal for SLA tracking.
+ if appliedSLA.ConversationStatusCategory == cstatusmodels.CategoryResolved {
+ m.lo.Info("marking sla notification as processed as the conversation is in a resolved-category status", "status", appliedSLA.ConversationStatus, "scheduled_notification_id", scheduledNotification.ID)
if _, err := m.q.UpdateSLANotificationProcessed.Exec(scheduledNotification.ID); err != nil {
m.lo.Error("error marking notification as processed", "error", err)
}
diff --git a/schema.sql b/schema.sql
index 8e91fd0b..7fcfff6d 100644
--- a/schema.sql
+++ b/schema.sql
@@ -23,6 +23,7 @@ DROP TYPE IF EXISTS "sla_notification_type" CASCADE; CREATE TYPE "sla_notificati
DROP TYPE IF EXISTS "activity_log_type" CASCADE; CREATE TYPE "activity_log_type" AS ENUM ('agent_login', 'agent_logout', 'agent_away', 'agent_away_reassigned', 'agent_online', 'agent_password_set', 'agent_role_permissions_changed');
DROP TYPE IF EXISTS "macro_visible_when" CASCADE; CREATE TYPE "macro_visible_when" AS ENUM ('replying', 'starting_conversation', 'adding_private_note');
DROP TYPE IF EXISTS "user_notification_type" CASCADE; CREATE TYPE "user_notification_type" AS ENUM ('mention', 'assignment', 'sla_warning', 'sla_breach');
+DROP TYPE IF EXISTS "conversation_status_category" CASCADE; CREATE TYPE "conversation_status_category" AS ENUM ('open', 'waiting', 'resolved');
DROP TYPE IF EXISTS "webhook_event" CASCADE; CREATE TYPE webhook_event AS ENUM (
'conversation.created',
'conversation.status_changed',
@@ -189,7 +190,8 @@ CREATE TABLE conversation_statuses (
id SERIAL PRIMARY KEY,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
- "name" TEXT NOT NULL UNIQUE
+ "name" TEXT NOT NULL UNIQUE,
+ category conversation_status_category NOT NULL DEFAULT 'open'
);
DROP TABLE IF EXISTS conversation_priorities CASCADE;
@@ -741,11 +743,11 @@ INSERT INTO conversation_priorities (name) VALUES
('High');
-- Default conversation statuses
-INSERT INTO conversation_statuses (name) VALUES
-('Open'),
-('Snoozed'),
-('Resolved'),
-('Closed');
+INSERT INTO conversation_statuses (name, category) VALUES
+('Open', 'open'),
+('Snoozed', 'waiting'),
+('Resolved', 'resolved'),
+('Closed', 'resolved');
-- Default roles
INSERT INTO