From 173493a0edcebcba76a4faa81978ea632b35c546 Mon Sep 17 00:00:00 2001 From: Abhinav Raut Date: Fri, 9 Jan 2026 03:26:22 +0530 Subject: [PATCH] handle panics in importer, translate messages and fix bugs --- cmd/agent_import.go | 65 ++-- cmd/init.go | 9 + cmd/main.go | 4 +- frontend/src/api/index.js | 9 + frontend/src/components/button/CopyButton.vue | 31 ++ frontend/src/components/importer/Importer.vue | 267 ++++++++++++++++ .../src/components/ui/importer/Importer.vue | 297 ------------------ .../features/admin/oidc/dataTableColumns.js | 9 + frontend/src/views/admin/agents/AgentList.vue | 10 +- frontend/src/views/admin/agents/Agents.vue | 2 +- i18n/en.json | 13 +- internal/importer/importer.go | 112 +++++-- 12 files changed, 465 insertions(+), 363 deletions(-) create mode 100644 frontend/src/components/button/CopyButton.vue create mode 100644 frontend/src/components/importer/Importer.vue delete mode 100644 frontend/src/components/ui/importer/Importer.vue diff --git a/cmd/agent_import.go b/cmd/agent_import.go index 639ca903..3d0a09df 100644 --- a/cmd/agent_import.go +++ b/cmd/agent_import.go @@ -17,13 +17,13 @@ func handleImportAgents(r *fastglue.Request) error { file, err := r.RequestCtx.FormFile("file") if err != nil { - return r.SendErrorEnvelope(fasthttp.StatusBadRequest, "No file provided", nil, envelope.InputError) + return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.required", "name", "{globals.terms.file}"), nil, envelope.InputError) } fileContent, err := file.Open() if err != nil { app.lo.Error("error opening uploaded file", "error", err) - return r.SendErrorEnvelope(fasthttp.StatusInternalServerError, "Failed to read file", nil, envelope.GeneralError) + return r.SendErrorEnvelope(fasthttp.StatusInternalServerError, app.i18n.Ts("globals.messages.errorReading", "name", "{globals.terms.file}"), nil, envelope.GeneralError) } defer fileContent.Close() @@ -32,11 +32,11 @@ func handleImportAgents(r *fastglue.Request) error { records, err := reader.ReadAll() if err != nil { app.lo.Error("error parsing CSV", "error", err) - return r.SendErrorEnvelope(fasthttp.StatusBadRequest, "Invalid CSV format", nil, envelope.InputError) + return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.invalid", "name", "{globals.terms.csvFile}"), nil, envelope.InputError) } if len(records) < 2 { - return r.SendErrorEnvelope(fasthttp.StatusBadRequest, "CSV must contain headers and at least one data row", nil, envelope.InputError) + return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.T("importer.csvMustContainHeadersAndData"), nil, envelope.InputError) } err = app.importer.Submit("agents", func() error { @@ -44,23 +44,19 @@ func handleImportAgents(r *fastglue.Request) error { }) if err != nil { - return r.SendErrorEnvelope(fasthttp.StatusConflict, err.Error(), nil, envelope.GeneralError) + return r.SendErrorEnvelope(fasthttp.StatusConflict, app.i18n.T("importer.importAlreadyInProgress"), nil, envelope.GeneralError) } - return r.SendEnvelope(map[string]string{ - "message": "Import started", - }) + return r.SendEnvelope(true) } // handleGetAgentImportStatus returns current import status func handleGetAgentImportStatus(r *fastglue.Request) error { var app = r.Context.(*App) - status, err := app.importer.GetStatus("agents") if err != nil { - return r.SendErrorEnvelope(fasthttp.StatusNotFound, err.Error(), nil, envelope.NotFoundError) + return sendErrorEnvelope(r, err) } - return r.SendEnvelope(status) } @@ -72,7 +68,7 @@ func processAgentImport(app *App, records [][]string) error { } // Validate required columns - required := []string{"first_name", "last_name", "email", "roles", "teams"} + required := []string{"first_name", "last_name", "email", "roles"} for _, col := range required { if _, ok := headerMap[col]; !ok { return fmt.Errorf("missing required column: %s", col) @@ -107,7 +103,7 @@ func processAgentImport(app *App, records [][]string) error { // Process each row for i, record := range records[1:] { - rowNum := i + 2 + rowNum := i + 1 // Parse fields firstName := getField(record, headerMap, "first_name") @@ -117,7 +113,7 @@ func processAgentImport(app *App, records [][]string) error { teamsStr := getField(record, headerMap, "teams") // Validate required fields - if firstName == "" || lastName == "" || email == "" || rolesStr == "" || teamsStr == "" { + if firstName == "" || lastName == "" || email == "" || rolesStr == "" { app.importer.UpdateCounts("agents", 0, 0, 1) app.importer.AddLog("agents", fmt.Sprintf("Row %d: Error - missing required fields", rowNum)) continue @@ -145,18 +141,21 @@ func processAgentImport(app *App, records [][]string) error { continue } - // Parse and validate teams + // Parse and validate teams (optional) teams := parseList(teamsStr) - if len(teams) == 0 { - app.importer.UpdateCounts("agents", 0, 0, 1) - app.importer.AddLog("agents", fmt.Sprintf("Row %d: Error - at least one team required", rowNum)) - continue + if len(teams) > 0 { + invalidTeams := findInvalid(teams, validTeams) + if len(invalidTeams) > 0 { + app.importer.UpdateCounts("agents", 0, 0, 1) + app.importer.AddLog("agents", fmt.Sprintf("Row %d: Error - invalid team(s): %s", rowNum, strings.Join(invalidTeams, ", "))) + continue + } } - invalidTeams := findInvalid(teams, validTeams) - if len(invalidTeams) > 0 { + // Check if agent already exists + if _, err := app.user.GetAgent(0, email); err == nil { app.importer.UpdateCounts("agents", 0, 0, 1) - app.importer.AddLog("agents", fmt.Sprintf("Row %d: Error - invalid team(s): %s", rowNum, strings.Join(invalidTeams, ", "))) + app.importer.AddLog("agents", fmt.Sprintf("Row %d: Error - email already exists", rowNum)) continue } @@ -164,19 +163,17 @@ func processAgentImport(app *App, records [][]string) error { agent, err := app.user.CreateAgent(firstName, lastName, email, roles) if err != nil { app.importer.UpdateCounts("agents", 0, 0, 1) - if strings.Contains(strings.ToLower(err.Error()), "email") && strings.Contains(strings.ToLower(err.Error()), "exists") { - app.importer.AddLog("agents", fmt.Sprintf("Row %d: Error - email already exists", rowNum)) - } else { - app.importer.AddLog("agents", fmt.Sprintf("Row %d: Error - failed to create agent", rowNum)) - } + app.importer.AddLog("agents", fmt.Sprintf("Row %d: Error - failed to create agent", rowNum)) continue } - // Assign teams - if err := app.team.UpsertUserTeams(agent.ID, teams); err != nil { - app.importer.UpdateCounts("agents", 0, 0, 1) - app.importer.AddLog("agents", fmt.Sprintf("Row %d: Error - team assignment failed", rowNum)) - continue + // Assign teams (if provided) + if len(teams) > 0 { + if err := app.team.UpsertUserTeams(agent.ID, teams); err != nil { + app.importer.UpdateCounts("agents", 0, 0, 1) + app.importer.AddLog("agents", fmt.Sprintf("Row %d: Error - team assignment failed", rowNum)) + continue + } } app.importer.UpdateCounts("agents", 0, 1, 0) @@ -185,8 +182,8 @@ func processAgentImport(app *App, records [][]string) error { // Final summary status, _ := app.importer.GetStatus("agents") - app.importer.AddLog("agents", fmt.Sprintf("Import completed: %d successful, %d failed out of %d total", - status.Success, status.Errors, status.Total)) + app.importer.AddLog("agents", fmt.Sprintf("Import completed: %d of %d successful, %d failed", + status.Success, status.Total, status.Errors)) return nil } diff --git a/cmd/init.go b/cmd/init.go index 1859b4b6..ba214c82 100644 --- a/cmd/init.go +++ b/cmd/init.go @@ -26,6 +26,7 @@ import ( "github.com/abhinavxd/libredesk/internal/conversation/status" "github.com/abhinavxd/libredesk/internal/csat" customAttribute "github.com/abhinavxd/libredesk/internal/custom_attribute" + "github.com/abhinavxd/libredesk/internal/importer" "github.com/abhinavxd/libredesk/internal/inbox" "github.com/abhinavxd/libredesk/internal/inbox/channel/email" imodels "github.com/abhinavxd/libredesk/internal/inbox/models" @@ -954,6 +955,14 @@ func initUserNotification(db *sqlx.DB, i18n *i18n.I18n) *notifier.UserNotificati return m } +// initImporter inits the importer manager. +func initImporter(i18n *i18n.I18n) *importer.Importer { + return importer.New(importer.Opts{ + Lo: initLogger("importer"), + I18n: i18n, + }) +} + // initNotifDispatcher initializes the notification dispatcher. func initNotifDispatcher(userNotification *notifier.UserNotificationManager, outbound *notifier.Service, wsHub *ws.Hub) *notifier.Dispatcher { return notifier.NewDispatcher(notifier.DispatcherOpts{ diff --git a/cmd/main.go b/cmd/main.go index 795d2341..53c083bb 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -258,7 +258,7 @@ func main() { conversation: conversation, automation: automation, businessHours: businessHours, - importer: importer.NewImporter(), + importer: initImporter(i18n), activityLog: initActivityLog(db, i18n), customAttribute: initCustomAttribute(db, i18n), authz: initAuthz(i18n), @@ -320,6 +320,8 @@ func main() { conversation.Close() colorlog.Red("Shutting down SLA...") sla.Close() + colorlog.Red("Shutting down importer...") + app.importer.Close() colorlog.Red("Shutting down database...") db.Close() colorlog.Red("Shutting down redis...") diff --git a/frontend/src/api/index.js b/frontend/src/api/index.js index c83f98ab..6a078f32 100644 --- a/frontend/src/api/index.js +++ b/frontend/src/api/index.js @@ -251,6 +251,13 @@ const setPassword = (data) => http.post('/api/v1/agents/set-password', data, { } }) const deleteUser = (id) => http.delete(`/api/v1/agents/${id}`) +const importAgents = (data) => + http.post('/api/v1/agents/import', data, { + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +const getAgentImportStatus = () => http.get('/api/v1/agents/import/status') const createUser = (data) => http.post('/api/v1/agents', data, { headers: { @@ -483,6 +490,8 @@ const deleteAllNotifications = () => http.delete('/api/v1/notifications') export default { login, deleteUser, + importAgents, + getAgentImportStatus, resetPassword, setPassword, getTags, diff --git a/frontend/src/components/button/CopyButton.vue b/frontend/src/components/button/CopyButton.vue new file mode 100644 index 00000000..d9219d2a --- /dev/null +++ b/frontend/src/components/button/CopyButton.vue @@ -0,0 +1,31 @@ + + + diff --git a/frontend/src/components/importer/Importer.vue b/frontend/src/components/importer/Importer.vue new file mode 100644 index 00000000..9d87fb83 --- /dev/null +++ b/frontend/src/components/importer/Importer.vue @@ -0,0 +1,267 @@ + + + diff --git a/frontend/src/components/ui/importer/Importer.vue b/frontend/src/components/ui/importer/Importer.vue deleted file mode 100644 index 911ebd92..00000000 --- a/frontend/src/components/ui/importer/Importer.vue +++ /dev/null @@ -1,297 +0,0 @@ - - - \ No newline at end of file diff --git a/frontend/src/features/admin/oidc/dataTableColumns.js b/frontend/src/features/admin/oidc/dataTableColumns.js index f0436796..e4ce082a 100644 --- a/frontend/src/features/admin/oidc/dataTableColumns.js +++ b/frontend/src/features/admin/oidc/dataTableColumns.js @@ -29,6 +29,15 @@ export const createColumns = (t) => [ return h('div', { class: 'text-center' }, enabled ? t('globals.messages.yes') : t('globals.messages.no')) } }, + { + accessorKey: 'created_at', + header: function () { + return h('div', { class: 'text-center' }, t('globals.terms.createdAt')) + }, + cell: function ({ row }) { + return h('div', { class: 'text-center' }, format(row.getValue('created_at'), 'PPpp')) + } + }, { accessorKey: 'updated_at', header: function () { diff --git a/frontend/src/views/admin/agents/AgentList.vue b/frontend/src/views/admin/agents/AgentList.vue index 681bb7a1..aa9991c6 100644 --- a/frontend/src/views/admin/agents/AgentList.vue +++ b/frontend/src/views/admin/agents/AgentList.vue @@ -2,7 +2,12 @@
- +