From 9bfe014d1ee305ad13cf3f6b150c12b4ffc7a380 Mon Sep 17 00:00:00 2001 From: Abhinav Raut Date: Wed, 9 Apr 2025 16:52:25 +0530 Subject: [PATCH] WIP: manage contacts page --- cmd/auth.go | 2 +- cmd/contacts.go | 160 ++++++++ cmd/conversation.go | 24 +- cmd/handlers.go | 70 ++-- cmd/init.go | 13 +- cmd/macro.go | 4 +- cmd/main.go | 2 +- cmd/media.go | 2 +- cmd/messages.go | 8 +- cmd/middlewares.go | 6 +- cmd/tags.go | 15 +- cmd/users.go | 234 ++++++------ cmd/views.go | 8 +- frontend/components.json | 1 - frontend/package.json | 1 + frontend/pnpm-lock.yaml | 76 ++++ frontend/src/App.vue | 21 +- frontend/src/api/index.js | 73 ++-- frontend/src/components/sidebar/Sidebar.vue | 46 ++- .../src/components/ui/avatar/AvatarUpload.vue | 58 +++ frontend/src/components/ui/avatar/index.js | 1 + .../src/components/ui/combobox/ComboBox.vue | 8 +- .../ui/pagination/PaginationEllipsis.vue | 29 ++ .../ui/pagination/PaginationFirst.vue | 29 ++ .../ui/pagination/PaginationLast.vue | 29 ++ .../ui/pagination/PaginationNext.vue | 29 ++ .../ui/pagination/PaginationPrev.vue | 29 ++ .../src/components/ui/pagination/index.js | 10 + frontend/src/components/ui/select/Select.vue | 4 +- .../src/components/ui/select/SelectValue.vue | 2 +- frontend/src/constants/countries.js | 242 ++++++++++++ frontend/src/constants/navigation.js | 7 + .../src/features/contacts/ContactsList.vue | 273 ++++++++++++++ .../sidebar/ConversationSideBarContact.vue | 38 +- .../src/layouts/contact/ContactDetail.vue | 5 + frontend/src/layouts/contact/ContactList.vue | 5 + frontend/src/router/index.js | 12 + .../src/views/contact/ContactDetailView.vue | 346 ++++++++++++++++++ frontend/src/views/contact/ContactsView.vue | 9 + frontend/src/views/contact/formSchema.js | 31 ++ i18n/en.json | 27 +- i18n/mr.json | 2 - internal/conversation/conversation.go | 4 +- internal/conversation/message.go | 2 +- internal/conversation/queries.sql | 1 + internal/dbutil/builder.go | 6 +- internal/inbox/channel/email/email.go | 4 +- internal/inbox/channel/email/imap.go | 17 +- internal/inbox/inbox.go | 22 +- internal/migrations/v0.6.0.go | 32 ++ internal/sla/sla.go | 4 +- internal/user/models/models.go | 53 +-- internal/user/queries.sql | 70 ++-- internal/user/user.go | 154 +++++--- schema.sql | 2 + 55 files changed, 1992 insertions(+), 370 deletions(-) create mode 100644 cmd/contacts.go create mode 100644 frontend/src/components/ui/avatar/AvatarUpload.vue create mode 100644 frontend/src/components/ui/pagination/PaginationEllipsis.vue create mode 100644 frontend/src/components/ui/pagination/PaginationFirst.vue create mode 100644 frontend/src/components/ui/pagination/PaginationLast.vue create mode 100644 frontend/src/components/ui/pagination/PaginationNext.vue create mode 100644 frontend/src/components/ui/pagination/PaginationPrev.vue create mode 100644 frontend/src/components/ui/pagination/index.js create mode 100644 frontend/src/constants/countries.js create mode 100644 frontend/src/features/contacts/ContactsList.vue create mode 100644 frontend/src/layouts/contact/ContactDetail.vue create mode 100644 frontend/src/layouts/contact/ContactList.vue create mode 100644 frontend/src/views/contact/ContactDetailView.vue create mode 100644 frontend/src/views/contact/ContactsView.vue create mode 100644 frontend/src/views/contact/formSchema.js diff --git a/cmd/auth.go b/cmd/auth.go index fadc258..4308183 100644 --- a/cmd/auth.go +++ b/cmd/auth.go @@ -77,7 +77,7 @@ func handleOIDCCallback(r *fastglue.Request) error { } // Lookup the user by email and set the session. - user, err := app.user.GetAgentByEmail(claims.Email) + user, err := app.user.GetAgent(0, claims.Email) if err != nil { return sendErrorEnvelope(r, err) } diff --git a/cmd/contacts.go b/cmd/contacts.go new file mode 100644 index 0000000..2bb9687 --- /dev/null +++ b/cmd/contacts.go @@ -0,0 +1,160 @@ +package main + +import ( + "path/filepath" + "strconv" + + "github.com/abhinavxd/libredesk/internal/envelope" + "github.com/abhinavxd/libredesk/internal/user/models" + "github.com/valyala/fasthttp" + "github.com/volatiletech/null/v9" + "github.com/zerodha/fastglue" +) + +// handleGetContacts returns a list of contacts from the database. +func handleGetContacts(r *fastglue.Request) error { + var ( + app = r.Context.(*App) + order = string(r.RequestCtx.QueryArgs().Peek("order")) + orderBy = string(r.RequestCtx.QueryArgs().Peek("order_by")) + filters = string(r.RequestCtx.QueryArgs().Peek("filters")) + page, _ = strconv.Atoi(string(r.RequestCtx.QueryArgs().Peek("page"))) + pageSize, _ = strconv.Atoi(string(r.RequestCtx.QueryArgs().Peek("page_size"))) + total = 0 + ) + contacts, err := app.user.GetContacts(page, pageSize, order, orderBy, filters) + if err != nil { + return sendErrorEnvelope(r, err) + } + if len(contacts) > 0 { + total = contacts[0].Total + } + return r.SendEnvelope(envelope.PageResults{ + Results: contacts, + Total: total, + PerPage: pageSize, + TotalPages: (total + pageSize - 1) / pageSize, + Page: page, + }) +} + +// handleGetTags returns a contact from the database. +func handleGetContact(r *fastglue.Request) error { + var ( + app = r.Context.(*App) + id, _ = strconv.Atoi(r.RequestCtx.UserValue("id").(string)) + ) + if id <= 0 { + return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.invalid", "name", "`id`"), nil, envelope.InputError) + } + c, err := app.user.GetContact(id, "") + if err != nil { + return sendErrorEnvelope(r, err) + } + return r.SendEnvelope(c) +} + +// handleUpdateContact updates a contact in the database. +func handleUpdateContact(r *fastglue.Request) error { + var ( + app = r.Context.(*App) + id, _ = strconv.Atoi(r.RequestCtx.UserValue("id").(string)) + ) + if id <= 0 { + return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.invalid", "name", "`id`"), nil, envelope.InputError) + } + + form, err := r.RequestCtx.MultipartForm() + if err != nil { + app.lo.Error("error parsing form data", "error", err) + return r.SendErrorEnvelope(fasthttp.StatusInternalServerError, app.i18n.Ts("globals.messages.errorParsing", "name", "{globals.terms.request}"), nil, envelope.GeneralError) + } + + // Parse form data + firstName := "" + if v, ok := form.Value["first_name"]; ok && len(v) > 0 { + firstName = string(v[0]) + } + lastName := "" + if v, ok := form.Value["last_name"]; ok && len(v) > 0 { + lastName = string(v[0]) + } + email := "" + if v, ok := form.Value["email"]; ok && len(v) > 0 { + email = string(v[0]) + } + phoneNumber := "" + if v, ok := form.Value["phone_number"]; ok && len(v) > 0 { + phoneNumber = string(v[0]) + } + phoneNumberCallingCode := "" + if v, ok := form.Value["phone_number_calling_code"]; ok && len(v) > 0 { + phoneNumberCallingCode = string(v[0]) + } + enabled := false + if v, ok := form.Value["enabled"]; ok && len(v) > 0 { + enabled = string(v[0]) == "true" + } + + user := models.User{ + FirstName: firstName, + LastName: lastName, + Email: null.StringFrom(email), + PhoneNumber: null.StringFrom(phoneNumber), + PhoneNumberCallingCode: null.StringFrom(phoneNumberCallingCode), + Enabled: enabled, + } + + if err := app.user.UpdateContact(id, user); err != nil { + return sendErrorEnvelope(r, err) + } + + // Upload avatar? + files, ok := form.File["files"] + if ok && len(files) > 0 { + contact, err := app.user.GetContact(id, "") + if err != nil { + return sendErrorEnvelope(r, err) + } + if err := uploadUserAvatar(r, &contact, files); err != nil { + app.lo.Error("error uploading avatar", "error", err) + return err + } + } + return r.SendEnvelope(true) +} + +// handleDeleteContactAvatar deletes contact avatar from storage and database. +func handleDeleteContactAvatar(r *fastglue.Request) error { + var ( + app = r.Context.(*App) + id, _ = strconv.Atoi(r.RequestCtx.UserValue("id").(string)) + ) + + if id <= 0 { + return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.invalid", "name", "`id`"), nil, envelope.InputError) + } + + // Get user + contact, err := app.user.GetContact(id, "") + if err != nil { + return sendErrorEnvelope(r, err) + } + + // Valid str? + if contact.AvatarURL.String == "" { + return r.SendEnvelope(true) + } + + fileName := filepath.Base(contact.AvatarURL.String) + + // Delete file from the store. + if err := app.media.Delete(fileName); err != nil { + return sendErrorEnvelope(r, err) + } + + if err = app.user.UpdateAvatar(contact.ID, ""); err != nil { + return sendErrorEnvelope(r, err) + } + return r.SendEnvelope(true) +} diff --git a/cmd/conversation.go b/cmd/conversation.go index 19c78ff..973e98b 100644 --- a/cmd/conversation.go +++ b/cmd/conversation.go @@ -130,7 +130,7 @@ func handleGetViewConversations(r *fastglue.Request) error { return r.SendErrorEnvelope(fasthttp.StatusForbidden, app.i18n.T("conversation.viewPermissionDenied"), nil, envelope.PermissionError) } - user, err := app.user.GetAgent(auser.ID) + user, err := app.user.GetAgent(auser.ID, "") if err != nil { return sendErrorEnvelope(r, err) } @@ -229,7 +229,7 @@ func handleGetConversation(r *fastglue.Request) error { auser = r.RequestCtx.UserValue("user").(amodels.User) ) - user, err := app.user.GetAgent(auser.ID) + user, err := app.user.GetAgent(auser.ID, "") if err != nil { return sendErrorEnvelope(r, err) } @@ -251,7 +251,7 @@ func handleUpdateConversationAssigneeLastSeen(r *fastglue.Request) error { uuid = r.RequestCtx.UserValue("uuid").(string) auser = r.RequestCtx.UserValue("user").(amodels.User) ) - user, err := app.user.GetAgent(auser.ID) + user, err := app.user.GetAgent(auser.ID, "") if err != nil { return sendErrorEnvelope(r, err) } @@ -272,7 +272,7 @@ func handleGetConversationParticipants(r *fastglue.Request) error { uuid = r.RequestCtx.UserValue("uuid").(string) auser = r.RequestCtx.UserValue("user").(amodels.User) ) - user, err := app.user.GetAgent(auser.ID) + user, err := app.user.GetAgent(auser.ID, "") if err != nil { return sendErrorEnvelope(r, err) } @@ -299,7 +299,7 @@ func handleUpdateUserAssignee(r *fastglue.Request) error { return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.invalid", "name", "`assignee_id`"), nil, envelope.InputError) } - user, err := app.user.GetAgent(auser.ID) + user, err := app.user.GetAgent(auser.ID, "") if err != nil { return sendErrorEnvelope(r, err) } @@ -331,7 +331,7 @@ func handleUpdateTeamAssignee(r *fastglue.Request) error { return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.invalid", "name", "`assignee_id`"), nil, envelope.InputError) } - user, err := app.user.GetAgent(auser.ID) + user, err := app.user.GetAgent(auser.ID, "") if err != nil { return sendErrorEnvelope(r, err) } @@ -367,7 +367,7 @@ func handleUpdateConversationPriority(r *fastglue.Request) error { return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.empty", "name", "`priority`"), nil, envelope.InputError) } - user, err := app.user.GetAgent(auser.ID) + user, err := app.user.GetAgent(auser.ID, "") if err != nil { return sendErrorEnvelope(r, err) } @@ -410,7 +410,7 @@ func handleUpdateConversationStatus(r *fastglue.Request) error { } // Enforce conversation access. - user, err := app.user.GetAgent(auser.ID) + user, err := app.user.GetAgent(auser.ID, "") if err != nil { return sendErrorEnvelope(r, err) } @@ -463,7 +463,7 @@ func handleUpdateConversationtags(r *fastglue.Request) error { return r.SendErrorEnvelope(fasthttp.StatusInternalServerError, app.i18n.Ts("globals.messages.errorParsing", "name", "{globals.terms.request}"), nil, envelope.InputError) } - user, err := app.user.GetAgent(auser.ID) + user, err := app.user.GetAgent(auser.ID, "") if err != nil { return sendErrorEnvelope(r, err) } @@ -525,7 +525,7 @@ func handleRemoveUserAssignee(r *fastglue.Request) error { uuid = r.RequestCtx.UserValue("uuid").(string) auser = r.RequestCtx.UserValue("user").(amodels.User) ) - user, err := app.user.GetAgent(auser.ID) + user, err := app.user.GetAgent(auser.ID, "") if err != nil { return sendErrorEnvelope(r, err) } @@ -546,7 +546,7 @@ func handleRemoveTeamAssignee(r *fastglue.Request) error { uuid = r.RequestCtx.UserValue("uuid").(string) auser = r.RequestCtx.UserValue("user").(amodels.User) ) - user, err := app.user.GetAgent(auser.ID) + user, err := app.user.GetAgent(auser.ID, "") if err != nil { return sendErrorEnvelope(r, err) } @@ -601,7 +601,7 @@ func handleCreateConversation(r *fastglue.Request) error { return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.fieldRequired", "name", "`first_name`"), nil, envelope.InputError) } - user, err := app.user.GetAgent(auser.ID) + user, err := app.user.GetAgent(auser.ID, "") if err != nil { return sendErrorEnvelope(r, err) } diff --git a/cmd/handlers.go b/cmd/handlers.go index b4e6725..104a40e 100644 --- a/cmd/handlers.go +++ b/cmd/handlers.go @@ -79,7 +79,7 @@ func initHandlers(g *fastglue.Fastglue, hub *ws.Hub) { g.DELETE("/api/v1/statuses/{id}", perm(handleDeleteStatus, "status:manage")) g.GET("/api/v1/priorities", auth(handleGetPriorities)) - // Tag. + // Tags. g.GET("/api/v1/tags", auth(handleGetTags)) g.POST("/api/v1/tags", perm(handleCreateTag, "tags:manage")) g.PUT("/api/v1/tags/{id}", perm(handleUpdateTag, "tags:manage")) @@ -93,22 +93,30 @@ func initHandlers(g *fastglue.Fastglue, hub *ws.Hub) { g.DELETE("/api/v1/macros/{id}", perm(handleDeleteMacro, "macros:manage")) g.POST("/api/v1/conversations/{uuid}/macros/{id}/apply", auth(handleApplyMacro)) - // User. - g.GET("/api/v1/users/me", auth(handleGetCurrentUser)) - g.PUT("/api/v1/users/me", auth(handleUpdateCurrentUser)) - g.GET("/api/v1/users/me/teams", auth(handleGetCurrentUserTeams)) - g.PUT("/api/v1/users/me/availability", auth(handleUpdateUserAvailability)) - g.DELETE("/api/v1/users/me/avatar", auth(handleDeleteAvatar)) - g.GET("/api/v1/users/compact", auth(handleGetUsersCompact)) - g.GET("/api/v1/users", perm(handleGetUsers, "users:manage")) - g.GET("/api/v1/users/{id}", perm(handleGetUser, "users:manage")) - g.POST("/api/v1/users", perm(handleCreateUser, "users:manage")) - g.PUT("/api/v1/users/{id}", perm(handleUpdateUser, "users:manage")) - g.DELETE("/api/v1/users/{id}", perm(handleDeleteUser, "users:manage")) - g.POST("/api/v1/users/reset-password", tryAuth(handleResetPassword)) - g.POST("/api/v1/users/set-password", tryAuth(handleSetPassword)) + // Agents. + g.GET("/api/v1/agents/me", auth(handleGetCurrentAgent)) + g.PUT("/api/v1/agents/me", auth(handleUpdateCurrentAgent)) + g.GET("/api/v1/agents/me/teams", auth(handleGetCurrentAgentTeams)) + g.PUT("/api/v1/agents/me/availability", auth(handleUpdateAgentAvailability)) + g.DELETE("/api/v1/agents/me/avatar", auth(handleDeleteCurrentAgentAvatar)) - // Team. + g.GET("/api/v1/agents/compact", auth(handleGetAgentsCompact)) + g.GET("/api/v1/agents", perm(handleGetAgents, "users:manage")) + g.GET("/api/v1/agents/{id}", perm(handleGetAgent, "users:manage")) + g.POST("/api/v1/agents", perm(handleCreateAgent, "users:manage")) + g.PUT("/api/v1/agents/{id}", perm(handleUpdateAgent, "users:manage")) + g.DELETE("/api/v1/agents/{id}", perm(handleDeleteAgent, "users:manage")) + g.POST("/api/v1/agents/reset-password", tryAuth(handleResetPassword)) + g.POST("/api/v1/agents/set-password", tryAuth(handleSetPassword)) + + // Contacts. + // TODO: Add permission checks for contacts. + g.GET("/api/v1/contacts", handleGetContacts) + g.GET("/api/v1/contacts/{id}", handleGetContact) + g.PUT("/api/v1/contacts/{id}", handleUpdateContact) + g.DELETE("/api/v1/contacts/{id}/avatar", handleDeleteContactAvatar) + + // Teams. g.GET("/api/v1/teams/compact", auth(handleGetTeamsCompact)) g.GET("/api/v1/teams", perm(handleGetTeams, "teams:manage")) g.GET("/api/v1/teams/{id}", perm(handleGetTeam, "teams:manage")) @@ -119,17 +127,17 @@ func initHandlers(g *fastglue.Fastglue, hub *ws.Hub) { // i18n. g.GET("/api/v1/lang/{lang}", handleGetI18nLang) - // Automation. - g.GET("/api/v1/automation/rules", perm(handleGetAutomationRules, "automations:manage")) - g.GET("/api/v1/automation/rules/{id}", perm(handleGetAutomationRule, "automations:manage")) - g.POST("/api/v1/automation/rules", perm(handleCreateAutomationRule, "automations:manage")) - g.PUT("/api/v1/automation/rules/{id}/toggle", perm(handleToggleAutomationRule, "automations:manage")) - g.PUT("/api/v1/automation/rules/{id}", perm(handleUpdateAutomationRule, "automations:manage")) - g.PUT("/api/v1/automation/rules/weights", perm(handleUpdateAutomationRuleWeights, "automations:manage")) - g.PUT("/api/v1/automation/rules/execution-mode", perm(handleUpdateAutomationRuleExecutionMode, "automations:manage")) - g.DELETE("/api/v1/automation/rules/{id}", perm(handleDeleteAutomationRule, "automations:manage")) + // Automations. + g.GET("/api/v1/automations/rules", perm(handleGetAutomationRules, "automations:manage")) + g.GET("/api/v1/automations/rules/{id}", perm(handleGetAutomationRule, "automations:manage")) + g.POST("/api/v1/automations/rules", perm(handleCreateAutomationRule, "automations:manage")) + g.PUT("/api/v1/automations/rules/{id}/toggle", perm(handleToggleAutomationRule, "automations:manage")) + g.PUT("/api/v1/automations/rules/{id}", perm(handleUpdateAutomationRule, "automations:manage")) + g.PUT("/api/v1/automations/rules/weights", perm(handleUpdateAutomationRuleWeights, "automations:manage")) + g.PUT("/api/v1/automations/rules/execution-mode", perm(handleUpdateAutomationRuleExecutionMode, "automations:manage")) + g.DELETE("/api/v1/automations/rules/{id}", perm(handleDeleteAutomationRule, "automations:manage")) - // Inbox. + // Inboxes. g.GET("/api/v1/inboxes", auth(handleGetInboxes)) g.GET("/api/v1/inboxes/{id}", perm(handleGetInbox, "inboxes:manage")) g.POST("/api/v1/inboxes", perm(handleCreateInbox, "inboxes:manage")) @@ -137,18 +145,18 @@ func initHandlers(g *fastglue.Fastglue, hub *ws.Hub) { g.PUT("/api/v1/inboxes/{id}", perm(handleUpdateInbox, "inboxes:manage")) g.DELETE("/api/v1/inboxes/{id}", perm(handleDeleteInbox, "inboxes:manage")) - // Role. + // Roles. g.GET("/api/v1/roles", perm(handleGetRoles, "roles:manage")) g.GET("/api/v1/roles/{id}", perm(handleGetRole, "roles:manage")) g.POST("/api/v1/roles", perm(handleCreateRole, "roles:manage")) g.PUT("/api/v1/roles/{id}", perm(handleUpdateRole, "roles:manage")) g.DELETE("/api/v1/roles/{id}", perm(handleDeleteRole, "roles:manage")) - // Dashboard. + // Reports. g.GET("/api/v1/reports/overview/counts", perm(handleDashboardCounts, "reports:manage")) g.GET("/api/v1/reports/overview/charts", perm(handleDashboardCharts, "reports:manage")) - // Template. + // Templates. g.GET("/api/v1/templates", perm(handleGetTemplates, "templates:manage")) g.GET("/api/v1/templates/{id}", perm(handleGetTemplate, "templates:manage")) g.POST("/api/v1/templates", perm(handleCreateTemplate, "templates:manage")) @@ -162,14 +170,14 @@ func initHandlers(g *fastglue.Fastglue, hub *ws.Hub) { g.PUT("/api/v1/business-hours/{id}", perm(handleUpdateBusinessHours, "business_hours:manage")) g.DELETE("/api/v1/business-hours/{id}", perm(handleDeleteBusinessHour, "business_hours:manage")) - // SLA. + // SLAs. g.GET("/api/v1/sla", perm(handleGetSLAs, "sla:manage")) g.GET("/api/v1/sla/{id}", perm(handleGetSLA, "sla:manage")) g.POST("/api/v1/sla", perm(handleCreateSLA, "sla:manage")) g.PUT("/api/v1/sla/{id}", perm(handleUpdateSLA, "sla:manage")) g.DELETE("/api/v1/sla/{id}", perm(handleDeleteSLA, "sla:manage")) - // AI completion. + // AI completions. g.GET("/api/v1/ai/prompts", auth(handleGetAIPrompts)) g.POST("/api/v1/ai/completion", auth(handleAICompletion)) g.PUT("/api/v1/ai/provider", perm(handleUpdateAIProvider, "ai:manage")) diff --git a/cmd/init.go b/cmd/init.go index a288d20..3a3c4a9 100644 --- a/cmd/init.go +++ b/cmd/init.go @@ -526,7 +526,7 @@ func initNotifier() *notifier.Service { } // initEmailInbox initializes the email inbox. -func initEmailInbox(inboxRecord imodels.Inbox, store inbox.MessageStore) (inbox.Inbox, error) { +func initEmailInbox(inboxRecord imodels.Inbox, msgStore inbox.MessageStore, usrStore inbox.UserStore) (inbox.Inbox, error) { var config email.Config // Load JSON data into Koanf. @@ -552,7 +552,7 @@ func initEmailInbox(inboxRecord imodels.Inbox, store inbox.MessageStore) (inbox. log.Printf("WARNING: No `from` email address set for `%s` inbox: Name: `%s`", inboxRecord.Channel, inboxRecord.Name) } - inbox, err := email.New(store, email.Opts{ + inbox, err := email.New(msgStore, usrStore, email.Opts{ ID: inboxRecord.ID, Config: config, Lo: initLogger("email_inbox"), @@ -568,10 +568,10 @@ func initEmailInbox(inboxRecord imodels.Inbox, store inbox.MessageStore) (inbox. } // initializeInboxes handles inbox initialization. -func initializeInboxes(inboxR imodels.Inbox, store inbox.MessageStore) (inbox.Inbox, error) { +func initializeInboxes(inboxR imodels.Inbox, msgStore inbox.MessageStore, usrStore inbox.UserStore) (inbox.Inbox, error) { switch inboxR.Channel { case "email": - return initEmailInbox(inboxR, store) + return initEmailInbox(inboxR, msgStore, usrStore) default: return nil, fmt.Errorf("unknown inbox channel: %s", inboxR.Channel) } @@ -584,8 +584,9 @@ func reloadInboxes(app *App) error { } // startInboxes registers the active inboxes and starts receiver for each. -func startInboxes(ctx context.Context, mgr *inbox.Manager, store inbox.MessageStore) { - mgr.SetMessageStore(store) +func startInboxes(ctx context.Context, mgr *inbox.Manager, msgStore inbox.MessageStore, usrStore inbox.UserStore) { + mgr.SetMessageStore(msgStore) + mgr.SetUserStore(usrStore) if err := mgr.InitInboxes(initializeInboxes); err != nil { log.Fatalf("error initializing inboxes: %v", err) diff --git a/cmd/macro.go b/cmd/macro.go index 91cf84f..6a13974 100644 --- a/cmd/macro.go +++ b/cmd/macro.go @@ -139,7 +139,7 @@ func handleApplyMacro(r *fastglue.Request) error { id, _ = strconv.Atoi(r.RequestCtx.UserValue("id").(string)) incomingActions = []autoModels.RuleAction{} ) - user, err := app.user.GetAgent(auser.ID) + user, err := app.user.GetAgent(auser.ID, "") if err != nil { return sendErrorEnvelope(r, err) } @@ -233,7 +233,7 @@ func setDisplayValues(app *App, actions []autoModels.RuleAction) error { return t.Name, nil }, autoModels.ActionAssignUser: func(id int) (string, error) { - u, err := app.user.GetAgent(id) + u, err := app.user.GetAgent(id, "") if err != nil { app.lo.Warn("user not found for macro action", "user_id", id) return "", err diff --git a/cmd/main.go b/cmd/main.go index dd2cada..d3b4b41 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -185,7 +185,7 @@ func main() { ) automation.SetConversationStore(conversation) - startInboxes(ctx, inbox, conversation) + startInboxes(ctx, inbox, conversation, user) go automation.Run(ctx, automationWorkers) go autoassigner.Run(ctx, autoAssignInterval) go conversation.Run(ctx, messageIncomingQWorkers, messageOutgoingQWorkers, messageOutgoingScanInterval) diff --git a/cmd/media.go b/cmd/media.go index b5b55b3..f953d5d 100644 --- a/cmd/media.go +++ b/cmd/media.go @@ -150,7 +150,7 @@ func handleServeMedia(r *fastglue.Request) error { uuid = r.RequestCtx.UserValue("uuid").(string) ) - user, err := app.user.GetAgent(auser.ID) + user, err := app.user.GetAgent(auser.ID, "") if err != nil { return sendErrorEnvelope(r, err) } diff --git a/cmd/messages.go b/cmd/messages.go index 746576d..d146f74 100644 --- a/cmd/messages.go +++ b/cmd/messages.go @@ -30,7 +30,7 @@ func handleGetMessages(r *fastglue.Request) error { total = 0 ) - user, err := app.user.GetAgent(auser.ID) + user, err := app.user.GetAgent(auser.ID, "") if err != nil { return sendErrorEnvelope(r, err) } @@ -73,7 +73,7 @@ func handleGetMessage(r *fastglue.Request) error { cuuid = r.RequestCtx.UserValue("cuuid").(string) auser = r.RequestCtx.UserValue("user").(amodels.User) ) - user, err := app.user.GetAgent(auser.ID) + user, err := app.user.GetAgent(auser.ID, "") if err != nil { return sendErrorEnvelope(r, err) } @@ -108,7 +108,7 @@ func handleRetryMessage(r *fastglue.Request) error { auser = r.RequestCtx.UserValue("user").(amodels.User) ) - user, err := app.user.GetAgent(auser.ID) + user, err := app.user.GetAgent(auser.ID, "") if err != nil { return sendErrorEnvelope(r, err) } @@ -135,7 +135,7 @@ func handleSendMessage(r *fastglue.Request) error { req = messageReq{} ) - user, err := app.user.GetAgent(auser.ID) + user, err := app.user.GetAgent(auser.ID, "") if err != nil { return sendErrorEnvelope(r, err) } diff --git a/cmd/middlewares.go b/cmd/middlewares.go index b578b95..533e925 100644 --- a/cmd/middlewares.go +++ b/cmd/middlewares.go @@ -24,7 +24,7 @@ func tryAuth(handler fastglue.FastRequestHandler) fastglue.FastRequestHandler { } // Try to get user. - user, err := app.user.GetAgent(userSession.ID) + user, err := app.user.GetAgent(userSession.ID, "") if err != nil { return handler(r) } @@ -54,7 +54,7 @@ func auth(handler fastglue.FastRequestHandler) fastglue.FastRequestHandler { } // Set user in the request context. - user, err := app.user.GetAgent(userSession.ID) + user, err := app.user.GetAgent(userSession.ID, "") if err != nil { return sendErrorEnvelope(r, err) } @@ -92,7 +92,7 @@ func perm(handler fastglue.FastRequestHandler, perm string) fastglue.FastRequest } // Get user from DB. - user, err := app.user.GetAgent(sessUser.ID) + user, err := app.user.GetAgent(sessUser.ID, "") if err != nil { return sendErrorEnvelope(r, err) } diff --git a/cmd/tags.go b/cmd/tags.go index 638e894..92ac607 100644 --- a/cmd/tags.go +++ b/cmd/tags.go @@ -9,17 +9,19 @@ import ( "github.com/zerodha/fastglue" ) +// handleGetTags returns all tags from the database. func handleGetTags(r *fastglue.Request) error { var ( app = r.Context.(*App) ) t, err := app.tag.GetAll() if err != nil { - return r.SendErrorEnvelope(fasthttp.StatusInternalServerError, err.Error(), nil, "") + return sendErrorEnvelope(r, err) } return r.SendEnvelope(t) } +// handleCreateTag creates a new tag in the database. func handleCreateTag(r *fastglue.Request) error { var ( app = r.Context.(*App) @@ -33,14 +35,14 @@ func handleCreateTag(r *fastglue.Request) error { return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.empty", "name", "`name`"), nil, envelope.InputError) } - err := app.tag.Create(tag.Name) - if err != nil { + if err := app.tag.Create(tag.Name); err != nil { return sendErrorEnvelope(r, err) } return r.SendEnvelope(true) } +// handleDeleteTag deletes a tag from the database. func handleDeleteTag(r *fastglue.Request) error { var ( app = r.Context.(*App) @@ -50,14 +52,14 @@ func handleDeleteTag(r *fastglue.Request) error { return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.invalid", "name", "`id`"), nil, envelope.InputError) } - err = app.tag.Delete(id) - if err != nil { + if err = app.tag.Delete(id); err != nil { return sendErrorEnvelope(r, err) } return r.SendEnvelope(true) } +// handleUpdateTag updates an existing tag in the database. func handleUpdateTag(r *fastglue.Request) error { var ( app = r.Context.(*App) @@ -76,8 +78,7 @@ func handleUpdateTag(r *fastglue.Request) error { return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.empty", "name", "`name`"), nil, envelope.InputError) } - err = app.tag.Update(id, tag.Name) - if err != nil { + if err = app.tag.Update(id, tag.Name); err != nil { return sendErrorEnvelope(r, err) } diff --git a/cmd/users.go b/cmd/users.go index a797c75..7986ae1 100644 --- a/cmd/users.go +++ b/cmd/users.go @@ -2,6 +2,7 @@ package main import ( "fmt" + "mime/multipart" "net/http" "path/filepath" "slices" @@ -22,33 +23,33 @@ import ( ) const ( - maxAvatarSizeMB = 20 + maxAvatarSizeMB = 5 ) -// handleGetUsers returns all users. -func handleGetUsers(r *fastglue.Request) error { +// handleGetAgents returns all agents. +func handleGetAgents(r *fastglue.Request) error { var ( app = r.Context.(*App) ) - agents, err := app.user.GetAll() + agents, err := app.user.GetAgents() if err != nil { return sendErrorEnvelope(r, err) } return r.SendEnvelope(agents) } -// handleGetUsersCompact returns all users in a compact format. -func handleGetUsersCompact(r *fastglue.Request) error { +// handleGetAgentsCompact returns all agents in a compact format. +func handleGetAgentsCompact(r *fastglue.Request) error { var app = r.Context.(*App) - agents, err := app.user.GetAllCompact() + agents, err := app.user.GetAgentsCompact() if err != nil { return sendErrorEnvelope(r, err) } return r.SendEnvelope(agents) } -// handleGetUser returns a user. -func handleGetUser(r *fastglue.Request) error { +// handleGetAgent returns an agent. +func handleGetAgent(r *fastglue.Request) error { var ( app = r.Context.(*App) ) @@ -56,15 +57,15 @@ func handleGetUser(r *fastglue.Request) error { if err != nil || id <= 0 { return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.invalid", "name", "`id`"), nil, envelope.InputError) } - user, err := app.user.GetAgent(id) + agent, err := app.user.GetAgent(id, "") if err != nil { return sendErrorEnvelope(r, err) } - return r.SendEnvelope(user) + return r.SendEnvelope(agent) } -// handleUpdateUserAvailability updates the current user availability. -func handleUpdateUserAvailability(r *fastglue.Request) error { +// handleUpdateAgentAvailability updates the current agent availability. +func handleUpdateAgentAvailability(r *fastglue.Request) error { var ( app = r.Context.(*App) auser = r.RequestCtx.UserValue("user").(amodels.User) @@ -76,31 +77,31 @@ func handleUpdateUserAvailability(r *fastglue.Request) error { return r.SendEnvelope(true) } -// handleGetCurrentUserTeams returns the teams of a user. -func handleGetCurrentUserTeams(r *fastglue.Request) error { +// handleGetCurrentAgentTeams returns the teams of an agent. +func handleGetCurrentAgentTeams(r *fastglue.Request) error { var ( app = r.Context.(*App) auser = r.RequestCtx.UserValue("user").(amodels.User) ) - user, err := app.user.GetAgent(auser.ID) + agent, err := app.user.GetAgent(auser.ID, "") if err != nil { return sendErrorEnvelope(r, err) } - teams, err := app.team.GetUserTeams(user.ID) + teams, err := app.team.GetUserTeams(agent.ID) if err != nil { return sendErrorEnvelope(r, err) } return r.SendEnvelope(teams) } -// handleUpdateCurrentUser updates the current user. -func handleUpdateCurrentUser(r *fastglue.Request) error { +// handleUpdateCurrentAgent updates the current agent. +func handleUpdateCurrentAgent(r *fastglue.Request) error { var ( app = r.Context.(*App) auser = r.RequestCtx.UserValue("user").(amodels.User) ) - user, err := app.user.GetAgent(auser.ID) + agent, err := app.user.GetAgent(auser.ID, "") if err != nil { return sendErrorEnvelope(r, err) } @@ -115,69 +116,15 @@ func handleUpdateCurrentUser(r *fastglue.Request) error { // Upload avatar? if ok && len(files) > 0 { - fileHeader := files[0] - file, err := fileHeader.Open() - if err != nil { - app.lo.Error("error reading uploaded", "error", err) - return r.SendErrorEnvelope(fasthttp.StatusInternalServerError, app.i18n.Ts("globals.messages.errorReading", "name", "{globals.terms.file}"), nil, envelope.GeneralError) - } - defer file.Close() - - // Sanitize filename. - srcFileName := stringutil.SanitizeFilename(fileHeader.Filename) - srcContentType := fileHeader.Header.Get("Content-Type") - srcFileSize := fileHeader.Size - srcExt := strings.TrimPrefix(strings.ToLower(filepath.Ext(srcFileName)), ".") - - if !slices.Contains(image.Exts, srcExt) { - return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.T("globals.messages.fileTypeisNotAnImage"), nil, envelope.InputError) - } - - // Check file size - if bytesToMegabytes(srcFileSize) > maxAvatarSizeMB { - app.lo.Error("error uploaded file size is larger than max allowed", "size", bytesToMegabytes(srcFileSize), "max_allowed", maxAvatarSizeMB) - return r.SendErrorEnvelope( - http.StatusRequestEntityTooLarge, - app.i18n.Ts("media.fileSizeTooLarge", "size", fmt.Sprintf("%dMB", maxAvatarSizeMB)), - nil, - envelope.GeneralError, - ) - } - - // Reset ptr. - file.Seek(0, 0) - linkedModel := null.StringFrom(mmodels.ModelUser) - linkedID := null.IntFrom(user.ID) - disposition := null.NewString("", false) - contentID := "" - meta := []byte("{}") - media, err := app.media.UploadAndInsert(srcFileName, srcContentType, contentID, linkedModel, linkedID, file, int(srcFileSize), disposition, meta) - if err != nil { - app.lo.Error("error uploading file", "error", err) - return r.SendErrorEnvelope(fasthttp.StatusInternalServerError, app.i18n.Ts("globals.messages.errorUploading", "name", "{globals.terms.file}"), nil, envelope.GeneralError) - } - - // Delete current avatar. - if user.AvatarURL.Valid { - fileName := filepath.Base(user.AvatarURL.String) - app.media.Delete(fileName) - } - - // Save file path. - path, err := stringutil.GetPathFromURL(media.URL) - if err != nil { - app.lo.Debug("error getting path from URL", "url", media.URL, "error", err) - return r.SendErrorEnvelope(fasthttp.StatusInternalServerError, app.i18n.Ts("globals.messages.errorUploading", "name", "{globals.terms.file}"), nil, envelope.GeneralError) - } - if err := app.user.UpdateAvatar(user.ID, path); err != nil { - return sendErrorEnvelope(r, err) + if err := uploadUserAvatar(r, &agent, files); err != nil { + return err } } return r.SendEnvelope(true) } -// handleCreateUser creates a new user. -func handleCreateUser(r *fastglue.Request) error { +// handleCreateAgent creates a new agent. +func handleCreateAgent(r *fastglue.Request) error { var ( app = r.Context.(*App) user = models.User{} @@ -240,15 +187,15 @@ func handleCreateUser(r *fastglue.Request) error { return r.SendEnvelope(true) } -// handleUpdateUser updates a user. -func handleUpdateUser(r *fastglue.Request) error { +// handleUpdateAgent updates an agent. +func handleUpdateAgent(r *fastglue.Request) error { var ( app = r.Context.(*App) user = models.User{} ) id, err := strconv.Atoi(r.RequestCtx.UserValue("id").(string)) if err != nil || id == 0 { - return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.empty", "name", "{globals.terms.user} `id`"), nil, envelope.InputError) + return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.empty", "name", "`id`"), nil, envelope.InputError) } if err := r.Decode(&user, "json"); err != nil { @@ -267,12 +214,12 @@ func handleUpdateUser(r *fastglue.Request) error { return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.empty", "name", "`first_name`"), nil, envelope.InputError) } - // Update user. - if err = app.user.Update(id, user); err != nil { + // Update agent. + if err = app.user.UpdateAgent(id, user); err != nil { return sendErrorEnvelope(r, err) } - // Upsert user teams. + // Upsert agent teams. if err := app.team.UpsertUserTeams(id, user.Teams.Names()); err != nil { return sendErrorEnvelope(r, err) } @@ -280,18 +227,24 @@ func handleUpdateUser(r *fastglue.Request) error { return r.SendEnvelope(true) } -// handleDeleteUser soft deletes a user. -func handleDeleteUser(r *fastglue.Request) error { +// handleDeleteAgent soft deletes an agent. +func handleDeleteAgent(r *fastglue.Request) error { var ( app = r.Context.(*App) id, err = strconv.Atoi(r.RequestCtx.UserValue("id").(string)) + auser = r.RequestCtx.UserValue("user").(amodels.User) ) if err != nil || id == 0 { return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.empty", "name", "{globals.terms.user} `id`"), nil, envelope.InputError) } + // Disallow if self-deleting. + if id == auser.ID { + return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.T("user.userCannotDeleteSelf"), nil, envelope.InputError) + } + // Soft delete user. - if err = app.user.SoftDelete(id); err != nil { + if err = app.user.SoftDeleteAgent(id); err != nil { return sendErrorEnvelope(r, err) } @@ -300,54 +253,54 @@ func handleDeleteUser(r *fastglue.Request) error { return sendErrorEnvelope(r, err) } - return r.SendEnvelope("User deleted successfully.") + return r.SendEnvelope(true) } -// handleGetCurrentUser returns the current logged in user. -func handleGetCurrentUser(r *fastglue.Request) error { +// handleGetCurrentAgent returns the current logged in agent. +func handleGetCurrentAgent(r *fastglue.Request) error { var ( app = r.Context.(*App) auser = r.RequestCtx.UserValue("user").(amodels.User) ) - u, err := app.user.GetAgent(auser.ID) + u, err := app.user.GetAgent(auser.ID, "") if err != nil { return sendErrorEnvelope(r, err) } return r.SendEnvelope(u) } -// handleDeleteAvatar deletes a user avatar. -func handleDeleteAvatar(r *fastglue.Request) error { +// handleDeleteCurrentAgentAvatar deletes the current agent's avatar. +func handleDeleteCurrentAgentAvatar(r *fastglue.Request) error { var ( app = r.Context.(*App) auser = r.RequestCtx.UserValue("user").(amodels.User) ) // Get user - user, err := app.user.GetAgent(auser.ID) + agent, err := app.user.GetAgent(auser.ID, "") if err != nil { return sendErrorEnvelope(r, err) } // Valid str? - if user.AvatarURL.String == "" { - return r.SendEnvelope("Avatar deleted successfully.") + if agent.AvatarURL.String == "" { + return r.SendEnvelope(true) } - fileName := filepath.Base(user.AvatarURL.String) + fileName := filepath.Base(agent.AvatarURL.String) // Delete file from the store. if err := app.media.Delete(fileName); err != nil { return sendErrorEnvelope(r, err) } - if err = app.user.UpdateAvatar(user.ID, ""); err != nil { + if err = app.user.UpdateAvatar(agent.ID, ""); err != nil { return sendErrorEnvelope(r, err) } return r.SendEnvelope(true) } -// handleResetPassword generates a reset password token and sends an email to the user. +// handleResetPassword generates a reset password token and sends an email to the agent. func handleResetPassword(r *fastglue.Request) error { var ( app = r.Context.(*App) @@ -363,13 +316,13 @@ func handleResetPassword(r *fastglue.Request) error { return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.empty", "name", "`email`"), nil, envelope.InputError) } - user, err := app.user.GetAgentByEmail(email) + agent, err := app.user.GetAgent(0, email) if err != nil { // Send 200 even if user not found, to prevent email enumeration. return r.SendEnvelope("Reset password email sent successfully.") } - token, err := app.user.SetResetPasswordToken(user.ID) + token, err := app.user.SetResetPasswordToken(agent.ID) if err != nil { return sendErrorEnvelope(r, err) } @@ -384,7 +337,7 @@ func handleResetPassword(r *fastglue.Request) error { } if err := app.notifier.Send(notifier.Message{ - RecipientEmails: []string{user.Email.String}, + RecipientEmails: []string{agent.Email.String}, Subject: "Reset Password", Content: content, Provider: notifier.ProviderEmail, @@ -399,14 +352,14 @@ func handleResetPassword(r *fastglue.Request) error { // handleSetPassword resets the password with the provided token. func handleSetPassword(r *fastglue.Request) error { var ( - app = r.Context.(*App) - user, ok = r.RequestCtx.UserValue("user").(amodels.User) - p = r.RequestCtx.PostArgs() - password = string(p.Peek("password")) - token = string(p.Peek("token")) + app = r.Context.(*App) + agent, ok = r.RequestCtx.UserValue("user").(amodels.User) + p = r.RequestCtx.PostArgs() + password = string(p.Peek("password")) + token = string(p.Peek("token")) ) - if ok && user.ID > 0 { + if ok && agent.ID > 0 { return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.T("user.userAlreadyLoggedIn"), nil, envelope.InputError) } @@ -420,3 +373,68 @@ func handleSetPassword(r *fastglue.Request) error { return r.SendEnvelope(true) } + +// uploadUserAvatar uploads the user avatar. +func uploadUserAvatar(r *fastglue.Request, user *models.User, files []*multipart.FileHeader) error { + var app = r.Context.(*App) + + fileHeader := files[0] + file, err := fileHeader.Open() + if err != nil { + app.lo.Error("error opening uploaded file", "error", err) + return r.SendErrorEnvelope(fasthttp.StatusInternalServerError, app.i18n.Ts("globals.messages.errorReading", "name", "{globals.terms.file}"), nil, envelope.GeneralError) + } + defer file.Close() + + // Sanitize filename. + srcFileName := stringutil.SanitizeFilename(fileHeader.Filename) + srcContentType := fileHeader.Header.Get("Content-Type") + srcFileSize := fileHeader.Size + srcExt := strings.TrimPrefix(strings.ToLower(filepath.Ext(srcFileName)), ".") + + if !slices.Contains(image.Exts, srcExt) { + return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.T("globals.messages.fileTypeisNotAnImage"), nil, envelope.InputError) + } + + // Check file size + if bytesToMegabytes(srcFileSize) > maxAvatarSizeMB { + app.lo.Error("error uploaded file size is larger than max allowed", "size", bytesToMegabytes(srcFileSize), "max_allowed", maxAvatarSizeMB) + return r.SendErrorEnvelope( + http.StatusRequestEntityTooLarge, + app.i18n.Ts("media.fileSizeTooLarge", "size", fmt.Sprintf("%dMB", maxAvatarSizeMB)), + nil, + envelope.GeneralError, + ) + } + + // Reset ptr. + file.Seek(0, 0) + linkedModel := null.StringFrom(mmodels.ModelUser) + linkedID := null.IntFrom(user.ID) + disposition := null.NewString("", false) + contentID := "" + meta := []byte("{}") + media, err := app.media.UploadAndInsert(srcFileName, srcContentType, contentID, linkedModel, linkedID, file, int(srcFileSize), disposition, meta) + if err != nil { + app.lo.Error("error uploading file", "error", err) + return r.SendErrorEnvelope(fasthttp.StatusInternalServerError, app.i18n.Ts("globals.messages.errorUploading", "name", "{globals.terms.file}"), nil, envelope.GeneralError) + } + + // Delete current avatar. + if user.AvatarURL.Valid { + fileName := filepath.Base(user.AvatarURL.String) + app.media.Delete(fileName) + } + + // Save file path. + path, err := stringutil.GetPathFromURL(media.URL) + if err != nil { + app.lo.Debug("error getting path from URL", "url", media.URL, "error", err) + return r.SendErrorEnvelope(fasthttp.StatusInternalServerError, app.i18n.Ts("globals.messages.errorUploading", "name", "{globals.terms.file}"), nil, envelope.GeneralError) + } + fmt.Println("path", path) + if err := app.user.UpdateAvatar(user.ID, path); err != nil { + return sendErrorEnvelope(r, err) + } + return nil +} diff --git a/cmd/views.go b/cmd/views.go index 36ac978..5cb1657 100644 --- a/cmd/views.go +++ b/cmd/views.go @@ -16,7 +16,7 @@ func handleGetUserViews(r *fastglue.Request) error { app = r.Context.(*App) auser = r.RequestCtx.UserValue("user").(amodels.User) ) - user, err := app.user.GetAgent(auser.ID) + user, err := app.user.GetAgent(auser.ID, "") if err != nil { return sendErrorEnvelope(r, err) } @@ -37,7 +37,7 @@ func handleCreateUserView(r *fastglue.Request) error { if err := r.Decode(&view, "json"); err != nil { return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.errorParsing", "name", "{globals.terms.request}"), err.Error(), envelope.InputError) } - user, err := app.user.GetAgent(auser.ID) + user, err := app.user.GetAgent(auser.ID, "") if err != nil { return sendErrorEnvelope(r, err) } @@ -63,7 +63,7 @@ func handleDeleteUserView(r *fastglue.Request) error { if err != nil || id <= 0 { return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.invalid", "name", "`id`"), nil, envelope.InputError) } - user, err := app.user.GetAgent(auser.ID) + user, err := app.user.GetAgent(auser.ID, "") if err != nil { return sendErrorEnvelope(r, err) } @@ -94,7 +94,7 @@ func handleUpdateUserView(r *fastglue.Request) error { if err := r.Decode(&view, "json"); err != nil { return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.errorParsing", "name", "{globals.terms.request}"), err.Error(), envelope.InputError) } - user, err := app.user.GetAgent(auser.ID) + user, err := app.user.GetAgent(auser.ID, "") if err != nil { return sendErrorEnvelope(r, err) } diff --git a/frontend/components.json b/frontend/components.json index e223780..38566e4 100644 --- a/frontend/components.json +++ b/frontend/components.json @@ -8,7 +8,6 @@ "baseColor": "gray", "cssVariables": true }, - "framework": "vite", "aliases": { "components": "@/components", "utils": "@/lib/utils" diff --git a/frontend/package.json b/frontend/package.json index 37bffcb..572f79b 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -45,6 +45,7 @@ "pinia": "^2.1.7", "qs": "^6.12.1", "radix-vue": "^1.9.17", + "reka-ui": "^2.2.0", "tailwind-merge": "^2.3.0", "vee-validate": "^4.13.2", "vue": "^3.4.37", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 101e946..dac0782 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -95,6 +95,9 @@ importers: radix-vue: specifier: ^1.9.17 version: 1.9.17(vue@3.5.13(typescript@5.7.3)) + reka-ui: + specifier: ^2.2.0 + version: 2.2.0(typescript@5.7.3)(vue@3.5.13(typescript@5.7.3)) tailwind-merge: specifier: ^2.3.0 version: 2.6.0 @@ -764,6 +767,9 @@ packages: '@tanstack/virtual-core@3.11.2': resolution: {integrity: sha512-vTtpNt7mKCiZ1pwU9hfKPhpdVO2sVzFQsxoVBGtOSHxlrRRzYr8iQ2TlwbAcRYCcEiZ9ECAM8kBzH0v2+VzfKw==} + '@tanstack/virtual-core@3.13.6': + resolution: {integrity: sha512-cnQUeWnhNP8tJ4WsGcYiX24Gjkc9ALstLbHcBj1t3E7EimN6n6kHH+DPV4PpDnuw00NApQp+ViojMj1GRdwYQg==} + '@tanstack/vue-table@8.20.5': resolution: {integrity: sha512-2xixT3BEgSDw+jOSqPt6ylO/eutDI107t2WdFMVYIZZ45UmTHLySqNriNs0+dMaKR56K5z3t+97P6VuVnI2L+Q==} engines: {node: '>=12'} @@ -775,6 +781,11 @@ packages: peerDependencies: vue: ^2.7.0 || ^3.0.0 + '@tanstack/vue-virtual@3.13.6': + resolution: {integrity: sha512-GYdZ3SJBQPzgxhuCE2fvpiH46qzHiVx5XzBSdtESgiqh4poj8UgckjGWYEhxaBbcVt1oLzh1m3Ql4TyH32TOzQ==} + peerDependencies: + vue: ^2.7.0 || ^3.0.0 + '@tiptap/core@2.11.2': resolution: {integrity: sha512-Z437c/sQg31yrRVgLJVkQuih+7Og5tjRx6FE/zE47QgEayqQ9yXH0LrTAbPiY6IfY1X+f2A0h3e5Y/WGD6rC3Q==} peerDependencies: @@ -1123,6 +1134,9 @@ packages: '@types/web-bluetooth@0.0.20': resolution: {integrity: sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==} + '@types/web-bluetooth@0.0.21': + resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==} + '@types/yauzl@2.10.3': resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} @@ -1209,18 +1223,27 @@ packages: '@vueuse/core@12.4.0': resolution: {integrity: sha512-XnjQYcJwCsyXyIafyA6SvyN/OBtfPnjvJmbxNxQjCcyWD198urwm5TYvIUUyAxEAN0K7HJggOgT15cOlWFyLeA==} + '@vueuse/core@12.8.2': + resolution: {integrity: sha512-HbvCmZdzAu3VGi/pWYm5Ut+Kd9mn1ZHnn4L5G8kOQTPs/IwIAmJoBrmYk2ckLArgMXZj0AW3n5CAejLUO+PhdQ==} + '@vueuse/metadata@10.11.1': resolution: {integrity: sha512-IGa5FXd003Ug1qAZmyE8wF3sJ81xGLSqTqtQ6jaVfkeZ4i5kS2mwQF61yhVqojRnenVew5PldLyRgvdl4YYuSw==} '@vueuse/metadata@12.4.0': resolution: {integrity: sha512-AhPuHs/qtYrKHUlEoNO6zCXufu8OgbR8S/n2oMw1OQuBQJ3+HOLQ+EpvXs+feOlZMa0p8QVvDWNlmcJJY8rW2g==} + '@vueuse/metadata@12.8.2': + resolution: {integrity: sha512-rAyLGEuoBJ/Il5AmFHiziCPdQzRt88VxR+Y/A/QhJ1EWtWqPBBAxTAFaSkviwEuOEZNtW8pvkPgoCZQ+HxqW1A==} + '@vueuse/shared@10.11.1': resolution: {integrity: sha512-LHpC8711VFZlDaYUXEBbFBCQ7GS3dVU9mjOhhMhXP6txTV4EhYQg/KGnQuvt/sPAtoUKq7VVUnL6mVtFoL42sA==} '@vueuse/shared@12.4.0': resolution: {integrity: sha512-9yLgbHVIF12OSCojnjTIoZL1+UA10+O4E1aD6Hpfo/DKVm5o3SZIwz6CupqGy3+IcKI8d6Jnl26EQj/YucnW0Q==} + '@vueuse/shared@12.8.2': + resolution: {integrity: sha512-dznP38YzxZoNloI0qpEfpkms8knDtaoQ6Y/sfS0L7Yki4zh40LFHEhur0odJC6xTHG5dxWVPiUWBXn+wCG2s5w==} + '@withtypes/mime@0.1.2': resolution: {integrity: sha512-PB9BfZGzwblUONJY0LiOwsHCA6uV3DIPj/w9ReekdHxPOl0VdUFgI5s4avKycuuq9Gf5Nz2ZPA2O36GAUzlMPA==} @@ -2479,6 +2502,9 @@ packages: resolution: {integrity: sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==} engines: {node: '>= 0.4'} + ohash@2.0.11: + resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==} + once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} @@ -2770,6 +2796,11 @@ packages: regenerator-runtime@0.14.1: resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} + reka-ui@2.2.0: + resolution: {integrity: sha512-eeRrLI4LwJ6dkdwks6KFNKGs0+beqZlHO3JMHen7THDTh+yJ5Z0KNwONmOhhV/0hZC2uJCEExgG60QPzGstkQg==} + peerDependencies: + vue: '>= 3.2.0' + request-progress@3.0.0: resolution: {integrity: sha512-MnWzEHHaxHO2iWiQuHrUPBi/1WeBf5PkxQqNyNvLl9VAYSdXkP8tQ3pBSeCPD+yw0v0Aq1zosWLz0BdeXpWwZg==} @@ -3801,6 +3832,8 @@ snapshots: '@tanstack/virtual-core@3.11.2': {} + '@tanstack/virtual-core@3.13.6': {} + '@tanstack/vue-table@8.20.5(vue@3.5.13(typescript@5.7.3))': dependencies: '@tanstack/table-core': 8.20.5 @@ -3811,6 +3844,11 @@ snapshots: '@tanstack/virtual-core': 3.11.2 vue: 3.5.13(typescript@5.7.3) + '@tanstack/vue-virtual@3.13.6(vue@3.5.13(typescript@5.7.3))': + dependencies: + '@tanstack/virtual-core': 3.13.6 + vue: 3.5.13(typescript@5.7.3) + '@tiptap/core@2.11.2(@tiptap/pm@2.11.2)': dependencies: '@tiptap/pm': 2.11.2 @@ -4204,6 +4242,8 @@ snapshots: '@types/web-bluetooth@0.0.20': {} + '@types/web-bluetooth@0.0.21': {} + '@types/yauzl@2.10.3': dependencies: '@types/node': 22.10.5 @@ -4377,10 +4417,21 @@ snapshots: transitivePeerDependencies: - typescript + '@vueuse/core@12.8.2(typescript@5.7.3)': + dependencies: + '@types/web-bluetooth': 0.0.21 + '@vueuse/metadata': 12.8.2 + '@vueuse/shared': 12.8.2(typescript@5.7.3) + vue: 3.5.13(typescript@5.7.3) + transitivePeerDependencies: + - typescript + '@vueuse/metadata@10.11.1': {} '@vueuse/metadata@12.4.0': {} + '@vueuse/metadata@12.8.2': {} + '@vueuse/shared@10.11.1(vue@3.5.13(typescript@5.7.3))': dependencies: vue-demi: 0.14.10(vue@3.5.13(typescript@5.7.3)) @@ -4394,6 +4445,12 @@ snapshots: transitivePeerDependencies: - typescript + '@vueuse/shared@12.8.2(typescript@5.7.3)': + dependencies: + vue: 3.5.13(typescript@5.7.3) + transitivePeerDependencies: + - typescript + '@withtypes/mime@0.1.2': dependencies: mime: 3.0.0 @@ -5728,6 +5785,8 @@ snapshots: object-inspect@1.13.3: {} + ohash@2.0.11: {} + once@1.4.0: dependencies: wrappy: 1.0.2 @@ -6041,6 +6100,23 @@ snapshots: regenerator-runtime@0.14.1: {} + reka-ui@2.2.0(typescript@5.7.3)(vue@3.5.13(typescript@5.7.3)): + dependencies: + '@floating-ui/dom': 1.6.13 + '@floating-ui/vue': 1.1.6(vue@3.5.13(typescript@5.7.3)) + '@internationalized/date': 3.6.0 + '@internationalized/number': 3.6.0 + '@tanstack/vue-virtual': 3.13.6(vue@3.5.13(typescript@5.7.3)) + '@vueuse/core': 12.8.2(typescript@5.7.3) + '@vueuse/shared': 12.8.2(typescript@5.7.3) + aria-hidden: 1.2.4 + defu: 6.1.4 + ohash: 2.0.11 + vue: 3.5.13(typescript@5.7.3) + transitivePeerDependencies: + - '@vue/composition-api' + - typescript + request-progress@3.0.0: dependencies: throttleit: 1.0.1 diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 20b5837..3345368 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -14,12 +14,10 @@ - - - - + + + + @@ -30,6 +28,15 @@ + + + + + + + @@ -96,7 +103,7 @@ import { toast as sooner } from 'vue-sonner' import Sidebar from '@/components/sidebar/Sidebar.vue' import Command from '@/features/command/CommandBox.vue' import CreateConversation from '@/features/conversation/CreateConversation.vue' -import { Inbox, Shield, FileLineChart } from 'lucide-vue-next' +import { Inbox, Shield, FileLineChart, BookUser } from 'lucide-vue-next' import { useI18n } from 'vue-i18n' import { useRoute } from 'vue-router' import { diff --git a/frontend/src/api/index.js b/frontend/src/api/index.js index 93e0dd6..866200f 100644 --- a/frontend/src/api/index.js +++ b/frontend/src/api/index.js @@ -36,9 +36,6 @@ http.interceptors.request.use((request) => { const searchConversations = (params) => http.get('/api/v1/conversations/search', { params }) const searchMessages = (params) => http.get('/api/v1/messages/search', { params }) const searchContacts = (params) => http.get('/api/v1/contacts/search', { params }) -const resetPassword = (data) => http.post('/api/v1/users/reset-password', data) -const setPassword = (data) => http.post('/api/v1/users/set-password', data) -const deleteUser = (id) => http.delete(`/api/v1/users/${id}`) const getEmailNotificationSettings = () => http.get('/api/v1/settings/notifications/email') const updateEmailNotificationSettings = (data) => http.put('/api/v1/settings/notifications/email', data) const getPriorities = () => http.get('/api/v1/priorities') @@ -119,31 +116,31 @@ const updateSettings = (key, data) => const getSettings = (key) => http.get(`/api/v1/settings/${key}`) const login = (data) => http.post(`/api/v1/login`, data) const getAutomationRules = (type) => - http.get(`/api/v1/automation/rules`, { + http.get(`/api/v1/automations/rules`, { params: { type: type } }) -const toggleAutomationRule = (id) => http.put(`/api/v1/automation/rules/${id}/toggle`) -const getAutomationRule = (id) => http.get(`/api/v1/automation/rules/${id}`) +const toggleAutomationRule = (id) => http.put(`/api/v1/automations/rules/${id}/toggle`) +const getAutomationRule = (id) => http.get(`/api/v1/automations/rules/${id}`) const updateAutomationRule = (id, data) => - http.put(`/api/v1/automation/rules/${id}`, data, { + http.put(`/api/v1/automations/rules/${id}`, data, { headers: { 'Content-Type': 'application/json' } }) const createAutomationRule = (data) => - http.post(`/api/v1/automation/rules`, data, { + http.post(`/api/v1/automations/rules`, data, { headers: { 'Content-Type': 'application/json' } }) -const deleteAutomationRule = (id) => http.delete(`/api/v1/automation/rules/${id}`) +const deleteAutomationRule = (id) => http.delete(`/api/v1/automations/rules/${id}`) const updateAutomationRuleWeights = (data) => - http.put(`/api/v1/automation/rules/weights`, data, { + http.put(`/api/v1/automations/rules/weights`, data, { headers: { 'Content-Type': 'application/json' } }) -const updateAutomationRulesExecutionMode = (data) => http.put(`/api/v1/automation/rules/execution-mode`, data) +const updateAutomationRulesExecutionMode = (data) => http.put(`/api/v1/automations/rules/execution-mode`, data) const getRoles = () => http.get('/api/v1/roles') const getRole = (id) => http.get(`/api/v1/roles/${id}`) const createRole = (data) => @@ -159,26 +156,47 @@ const updateRole = (id, data) => } }) const deleteRole = (id) => http.delete(`/api/v1/roles/${id}`) -const getUser = (id) => http.get(`/api/v1/users/${id}`) +const getContacts = (params) => http.get('/api/v1/contacts', { params }) +const getContact = (id) => http.get(`/api/v1/contacts/${id}`) +const updateContact = (id, data) => http.put(`/api/v1/contacts/${id}`, data, { + headers: { + 'Content-Type': 'multipart/form-data' + } +}) const getTeam = (id) => http.get(`/api/v1/teams/${id}`) const getTeams = () => http.get('/api/v1/teams') const updateTeam = (id, data) => http.put(`/api/v1/teams/${id}`, data) const createTeam = (data) => http.post('/api/v1/teams', data) const getTeamsCompact = () => http.get('/api/v1/teams/compact') const deleteTeam = (id) => http.delete(`/api/v1/teams/${id}`) - -const getUsers = () => http.get('/api/v1/users') -const getUsersCompact = () => http.get('/api/v1/users/compact') +const updateUser = (id, data) => + http.put(`/api/v1/agents/${id}`, data, { + headers: { + 'Content-Type': 'application/json' + } + }) +const getUsers = () => http.get('/api/v1/agents') +const getUsersCompact = () => http.get('/api/v1/agents/compact') const updateCurrentUser = (data) => - http.put('/api/v1/users/me', data, { + http.put('/api/v1/agents/me', data, { headers: { 'Content-Type': 'multipart/form-data' } }) -const deleteUserAvatar = () => http.delete('/api/v1/users/me/avatar') -const getCurrentUser = () => http.get('/api/v1/users/me') -const getCurrentUserTeams = () => http.get('/api/v1/users/me/teams') -const updateCurrentUserAvailability = (data) => http.put('/api/v1/users/me/availability', data) +const getUser = (id) => http.get(`/api/v1/agents/${id}`) +const deleteUserAvatar = () => http.delete('/api/v1/agents/me/avatar') +const getCurrentUser = () => http.get('/api/v1/agents/me') +const getCurrentUserTeams = () => http.get('/api/v1/agents/me/teams') +const updateCurrentUserAvailability = (data) => http.put('/api/v1/agents/me/availability', data) +const resetPassword = (data) => http.post('/api/v1/agents/reset-password', data) +const setPassword = (data) => http.post('/api/v1/agents/set-password', data) +const deleteUser = (id) => http.delete(`/api/v1/agents/${id}`) +const createUser = (data) => + http.post('/api/v1/agents', data, { + headers: { + 'Content-Type': 'application/json' + } + }) const getTags = () => http.get('/api/v1/tags') const upsertTags = (uuid, data) => http.post(`/api/v1/conversations/${uuid}/tags`, data) const updateAssignee = (uuid, assignee_type, data) => http.put(`/api/v1/conversations/${uuid}/assignee/${assignee_type}`, data) @@ -231,18 +249,6 @@ const uploadMedia = (data) => const getOverviewCounts = () => http.get('/api/v1/reports/overview/counts') const getOverviewCharts = () => http.get('/api/v1/reports/overview/charts') const getLanguage = (lang) => http.get(`/api/v1/lang/${lang}`) -const createUser = (data) => - http.post('/api/v1/users', data, { - headers: { - 'Content-Type': 'application/json' - } - }) -const updateUser = (id, data) => - http.put(`/api/v1/users/${id}`, data, { - headers: { - 'Content-Type': 'application/json' - } - }) const createInbox = (data) => http.post('/api/v1/inboxes', data, { headers: { @@ -390,4 +396,7 @@ export default { searchMessages, searchContacts, removeAssignee, + getContacts, + getContact, + updateContact, } diff --git a/frontend/src/components/sidebar/Sidebar.vue b/frontend/src/components/sidebar/Sidebar.vue index 0e7116b..e730328 100644 --- a/frontend/src/components/sidebar/Sidebar.vue +++ b/frontend/src/components/sidebar/Sidebar.vue @@ -1,5 +1,10 @@ diff --git a/frontend/src/components/ui/avatar/index.js b/frontend/src/components/ui/avatar/index.js index e75e44b..3fc17e8 100644 --- a/frontend/src/components/ui/avatar/index.js +++ b/frontend/src/components/ui/avatar/index.js @@ -3,6 +3,7 @@ import { cva } from 'class-variance-authority' export { default as Avatar } from './Avatar.vue' export { default as AvatarImage } from './AvatarImage.vue' export { default as AvatarFallback } from './AvatarFallback.vue' +export { default as AvatarUpload } from './AvatarUpload.vue' export const avatarVariant = cva( 'inline-flex items-center justify-center font-normal text-foreground select-none shrink-0 bg-secondary overflow-hidden', diff --git a/frontend/src/components/ui/combobox/ComboBox.vue b/frontend/src/components/ui/combobox/ComboBox.vue index c3b7cd1..9355cae 100644 --- a/frontend/src/components/ui/combobox/ComboBox.vue +++ b/frontend/src/components/ui/combobox/ComboBox.vue @@ -5,7 +5,7 @@ variant="outline" role="combobox" :aria-expanded="open" - class="w-full justify-between" + :class="['w-full justify-between', buttonClass]" > {{ selectedLabel }} @@ -58,7 +58,11 @@ const props = defineProps({ required: true }, placeholder: String, - defaultLabel: String + defaultLabel: String, + buttonClass: { + type: String, + default: '' + } }) const emit = defineEmits(['select']) diff --git a/frontend/src/components/ui/pagination/PaginationEllipsis.vue b/frontend/src/components/ui/pagination/PaginationEllipsis.vue new file mode 100644 index 0000000..52b73d9 --- /dev/null +++ b/frontend/src/components/ui/pagination/PaginationEllipsis.vue @@ -0,0 +1,29 @@ + + + diff --git a/frontend/src/components/ui/pagination/PaginationFirst.vue b/frontend/src/components/ui/pagination/PaginationFirst.vue new file mode 100644 index 0000000..a248ba7 --- /dev/null +++ b/frontend/src/components/ui/pagination/PaginationFirst.vue @@ -0,0 +1,29 @@ + + + diff --git a/frontend/src/components/ui/pagination/PaginationLast.vue b/frontend/src/components/ui/pagination/PaginationLast.vue new file mode 100644 index 0000000..0863391 --- /dev/null +++ b/frontend/src/components/ui/pagination/PaginationLast.vue @@ -0,0 +1,29 @@ + + + diff --git a/frontend/src/components/ui/pagination/PaginationNext.vue b/frontend/src/components/ui/pagination/PaginationNext.vue new file mode 100644 index 0000000..feb7230 --- /dev/null +++ b/frontend/src/components/ui/pagination/PaginationNext.vue @@ -0,0 +1,29 @@ + + + diff --git a/frontend/src/components/ui/pagination/PaginationPrev.vue b/frontend/src/components/ui/pagination/PaginationPrev.vue new file mode 100644 index 0000000..cbe8765 --- /dev/null +++ b/frontend/src/components/ui/pagination/PaginationPrev.vue @@ -0,0 +1,29 @@ + + + diff --git a/frontend/src/components/ui/pagination/index.js b/frontend/src/components/ui/pagination/index.js new file mode 100644 index 0000000..f060fd0 --- /dev/null +++ b/frontend/src/components/ui/pagination/index.js @@ -0,0 +1,10 @@ +export { default as PaginationEllipsis } from './PaginationEllipsis.vue'; +export { default as PaginationFirst } from './PaginationFirst.vue'; +export { default as PaginationLast } from './PaginationLast.vue'; +export { default as PaginationNext } from './PaginationNext.vue'; +export { default as PaginationPrev } from './PaginationPrev.vue'; +export { + PaginationRoot as Pagination, + PaginationList, + PaginationListItem, +} from 'reka-ui'; diff --git a/frontend/src/components/ui/select/Select.vue b/frontend/src/components/ui/select/Select.vue index 0a7b02c..ee38eab 100644 --- a/frontend/src/components/ui/select/Select.vue +++ b/frontend/src/components/ui/select/Select.vue @@ -4,8 +4,8 @@ import { SelectRoot, useForwardPropsEmits } from 'radix-vue' const props = defineProps({ open: { type: Boolean, required: false }, defaultOpen: { type: Boolean, required: false }, - defaultValue: { type: String, required: false }, - modelValue: { type: String, required: false }, + defaultValue: { type: [String, Number], required: false }, + modelValue: { type: [String, Number], required: false }, dir: { type: String, required: false }, name: { type: String, required: false }, autocomplete: { type: String, required: false }, diff --git a/frontend/src/components/ui/select/SelectValue.vue b/frontend/src/components/ui/select/SelectValue.vue index 06b7b97..aa49d9b 100644 --- a/frontend/src/components/ui/select/SelectValue.vue +++ b/frontend/src/components/ui/select/SelectValue.vue @@ -2,7 +2,7 @@ import { SelectValue } from 'radix-vue' const props = defineProps({ - placeholder: { type: String, required: false }, + placeholder: { type: [String, Number], required: false }, asChild: { type: Boolean, required: false }, as: { type: null, required: false } }) diff --git a/frontend/src/constants/countries.js b/frontend/src/constants/countries.js new file mode 100644 index 0000000..768dca7 --- /dev/null +++ b/frontend/src/constants/countries.js @@ -0,0 +1,242 @@ +const countries = [ + { calling_code: '+93', name: 'Afghanistan', emoji: '🇦🇫', iso_2: 'AF' }, + { calling_code: '+355', name: 'Albania', emoji: '🇦🇱', iso_2: 'AL' }, + { calling_code: '+213', name: 'Algeria', emoji: '🇩🇿', iso_2: 'DZ' }, + { calling_code: '+1-684', name: 'American Samoa', emoji: '🇦🇸', iso_2: 'AS' }, + { calling_code: '+376', name: 'Andorra', emoji: '🇦🇩', iso_2: 'AD' }, + { calling_code: '+244', name: 'Angola', emoji: '🇦🇴', iso_2: 'AO' }, + { calling_code: '+1-264', name: 'Anguilla', emoji: '🇦🇮', iso_2: 'AI' }, + { calling_code: '+1-268', name: 'Antigua and Barbuda', emoji: '🇦🇬', iso_2: 'AG' }, + { calling_code: '+54', name: 'Argentina', emoji: '🇦🇷', iso_2: 'AR' }, + { calling_code: '+374', name: 'Armenia', emoji: '🇦🇲', iso_2: 'AM' }, + { calling_code: '+297', name: 'Aruba', emoji: '🇦🇼', iso_2: 'AW' }, + { calling_code: '+61', name: 'Australia', emoji: '🇦🇺', iso_2: 'AU' }, + { calling_code: '+43', name: 'Austria', emoji: '🇦🇹', iso_2: 'AT' }, + { calling_code: '+994', name: 'Azerbaijan', emoji: '🇦🇿', iso_2: 'AZ' }, + { calling_code: '+1-242', name: 'Bahamas', emoji: '🇧🇸', iso_2: 'BS' }, + { calling_code: '+973', name: 'Bahrain', emoji: '🇧🇭', iso_2: 'BH' }, + { calling_code: '+880', name: 'Bangladesh', emoji: '🇧🇩', iso_2: 'BD' }, + { calling_code: '+1-246', name: 'Barbados', emoji: '🇧🇧', iso_2: 'BB' }, + { calling_code: '+375', name: 'Belarus', emoji: '🇧🇾', iso_2: 'BY' }, + { calling_code: '+32', name: 'Belgium', emoji: '🇧🇪', iso_2: 'BE' }, + { calling_code: '+501', name: 'Belize', emoji: '🇧🇿', iso_2: 'BZ' }, + { calling_code: '+229', name: 'Benin', emoji: '🇧🇯', iso_2: 'BJ' }, + { calling_code: '+1-441', name: 'Bermuda', emoji: '🇧🇲', iso_2: 'BM' }, + { calling_code: '+975', name: 'Bhutan', emoji: '🇧🇹', iso_2: 'BT' }, + { calling_code: '+591', name: 'Bolivia', emoji: '🇧🇴', iso_2: 'BO' }, + { calling_code: '+387', name: 'Bosnia and Herzegovina', emoji: '🇧🇦', iso_2: 'BA' }, + { calling_code: '+267', name: 'Botswana', emoji: '🇧🇼', iso_2: 'BW' }, + { calling_code: '+55', name: 'Brazil', emoji: '🇧🇷', iso_2: 'BR' }, + { calling_code: '+246', name: 'British Indian Ocean Territory', emoji: '🇮🇴', iso_2: 'IO' }, + { calling_code: '+673', name: 'Brunei', emoji: '🇧🇳', iso_2: 'BN' }, + { calling_code: '+359', name: 'Bulgaria', emoji: '🇧🇬', iso_2: 'BG' }, + { calling_code: '+226', name: 'Burkina Faso', emoji: '🇧🇫', iso_2: 'BF' }, + { calling_code: '+257', name: 'Burundi', emoji: '🇧🇮', iso_2: 'BI' }, + { calling_code: '+855', name: 'Cambodia', emoji: '🇰🇭', iso_2: 'KH' }, + { calling_code: '+237', name: 'Cameroon', emoji: '🇨🇲', iso_2: 'CM' }, + { calling_code: '+1', name: 'Canada', emoji: '🇨🇦', iso_2: 'CA' }, + { calling_code: '+238', name: 'Cape Verde', emoji: '🇨🇻', iso_2: 'CV' }, + { calling_code: '+1-345', name: 'Cayman Islands', emoji: '🇰🇾', iso_2: 'KY' }, + { calling_code: '+236', name: 'Central African Republic', emoji: '🇨🇫', iso_2: 'CF' }, + { calling_code: '+235', name: 'Chad', emoji: '🇹🇩', iso_2: 'TD' }, + { calling_code: '+56', name: 'Chile', emoji: '🇨🇱', iso_2: 'CL' }, + { calling_code: '+86', name: 'China', emoji: '🇨🇳', iso_2: 'CN' }, + { calling_code: '+61', name: 'Christmas Island', emoji: '🇨🇽', iso_2: 'CX' }, + { calling_code: '+61', name: 'Cocos (Keeling) Islands', emoji: '🇨🇨', iso_2: 'CC' }, + { calling_code: '+57', name: 'Colombia', emoji: '🇨🇴', iso_2: 'CO' }, + { calling_code: '+269', name: 'Comoros', emoji: '🇰🇲', iso_2: 'KM' }, + { calling_code: '+242', name: 'Congo', emoji: '🇨🇬', iso_2: 'CG' }, + { calling_code: '+243', name: 'Congo, Democratic Republic of the', emoji: '🇨🇩', iso_2: 'CD' }, + { calling_code: '+682', name: 'Cook Islands', emoji: '🇨🇰', iso_2: 'CK' }, + { calling_code: '+506', name: 'Costa Rica', emoji: '🇨🇷', iso_2: 'CR' }, + { calling_code: '+225', name: "Côte d'Ivoire", emoji: '🇨🇮', iso_2: 'CI' }, + { calling_code: '+385', name: 'Croatia', emoji: '🇭🇷', iso_2: 'HR' }, + { calling_code: '+53', name: 'Cuba', emoji: '🇨🇺', iso_2: 'CU' }, + { calling_code: '+599', name: 'Curaçao', emoji: '🇨🇼', iso_2: 'CW' }, + { calling_code: '+357', name: 'Cyprus', emoji: '🇨🇾', iso_2: 'CY' }, + { calling_code: '+420', name: 'Czech Republic', emoji: '🇨🇿', iso_2: 'CZ' }, + { calling_code: '+45', name: 'Denmark', emoji: '🇩🇰', iso_2: 'DK' }, + { calling_code: '+253', name: 'Djibouti', emoji: '🇩🇯', iso_2: 'DJ' }, + { calling_code: '+1-767', name: 'Dominica', emoji: '🇩🇲', iso_2: 'DM' }, + { calling_code: '+1-809', name: 'Dominican Republic', emoji: '🇩🇴', iso_2: 'DO' }, + { calling_code: '+593', name: 'Ecuador', emoji: '🇪🇨', iso_2: 'EC' }, + { calling_code: '+20', name: 'Egypt', emoji: '🇪🇬', iso_2: 'EG' }, + { calling_code: '+503', name: 'El Salvador', emoji: '🇸🇻', iso_2: 'SV' }, + { calling_code: '+240', name: 'Equatorial Guinea', emoji: '🇬🇶', iso_2: 'GQ' }, + { calling_code: '+291', name: 'Eritrea', emoji: '🇪🇷', iso_2: 'ER' }, + { calling_code: '+372', name: 'Estonia', emoji: '🇪🇪', iso_2: 'EE' }, + { calling_code: '+268', name: 'Eswatini', emoji: '🇸🇿', iso_2: 'SZ' }, + { calling_code: '+251', name: 'Ethiopia', emoji: '🇪🇹', iso_2: 'ET' }, + { calling_code: '+500', name: 'Falkland Islands', emoji: '🇫🇰', iso_2: 'FK' }, + { calling_code: '+298', name: 'Faroe Islands', emoji: '🇫🇴', iso_2: 'FO' }, + { calling_code: '+679', name: 'Fiji', emoji: '🇫🇯', iso_2: 'FJ' }, + { calling_code: '+358', name: 'Finland', emoji: '🇫🇮', iso_2: 'FI' }, + { calling_code: '+33', name: 'France', emoji: '🇫🇷', iso_2: 'FR' }, + { calling_code: '+594', name: 'French Guiana', emoji: '🇬🇫', iso_2: 'GF' }, + { calling_code: '+689', name: 'French Polynesia', emoji: '🇵🇫', iso_2: 'PF' }, + { calling_code: '+241', name: 'Gabon', emoji: '🇬🇦', iso_2: 'GA' }, + { calling_code: '+220', name: 'Gambia', emoji: '🇬🇲', iso_2: 'GM' }, + { calling_code: '+995', name: 'Georgia', emoji: '🇬🇪', iso_2: 'GE' }, + { calling_code: '+49', name: 'Germany', emoji: '🇩🇪', iso_2: 'DE' }, + { calling_code: '+233', name: 'Ghana', emoji: '🇬🇭', iso_2: 'GH' }, + { calling_code: '+350', name: 'Gibraltar', emoji: '🇬🇮', iso_2: 'GI' }, + { calling_code: '+30', name: 'Greece', emoji: '🇬🇷', iso_2: 'GR' }, + { calling_code: '+299', name: 'Greenland', emoji: '🇬🇱', iso_2: 'GL' }, + { calling_code: '+1-473', name: 'Grenada', emoji: '🇬🇩', iso_2: 'GD' }, + { calling_code: '+590', name: 'Guadeloupe', emoji: '🇬🇵', iso_2: 'GP' }, + { calling_code: '+1-671', name: 'Guam', emoji: '🇬🇺', iso_2: 'GU' }, + { calling_code: '+502', name: 'Guatemala', emoji: '🇬🇹', iso_2: 'GT' }, + { calling_code: '+44-1481', name: 'Guernsey', emoji: '🇬🇬', iso_2: 'GG' }, + { calling_code: '+224', name: 'Guinea', emoji: '🇬🇳', iso_2: 'GN' }, + { calling_code: '+245', name: 'Guinea-Bissau', emoji: '🇬🇼', iso_2: 'GW' }, + { calling_code: '+592', name: 'Guyana', emoji: '🇬🇾', iso_2: 'GY' }, + { calling_code: '+509', name: 'Haiti', emoji: '🇭🇹', iso_2: 'HT' }, + { calling_code: '+379', name: 'Vatican City', emoji: '🇻🇦', iso_2: 'VA' }, + { calling_code: '+504', name: 'Honduras', emoji: '🇭🇳', iso_2: 'HN' }, + { calling_code: '+852', name: 'Hong Kong', emoji: '🇭🇰', iso_2: 'HK' }, + { calling_code: '+36', name: 'Hungary', emoji: '🇭🇺', iso_2: 'HU' }, + { calling_code: '+354', name: 'Iceland', emoji: '🇮🇸', iso_2: 'IS' }, + { calling_code: '+91', name: 'India', emoji: '🇮🇳', iso_2: 'IN' }, + { calling_code: '+62', name: 'Indonesia', emoji: '🇮🇩', iso_2: 'ID' }, + { calling_code: '+98', name: 'Iran', emoji: '🇮🇷', iso_2: 'IR' }, + { calling_code: '+964', name: 'Iraq', emoji: '🇮🇶', iso_2: 'IQ' }, + { calling_code: '+353', name: 'Ireland', emoji: '🇮🇪', iso_2: 'IE' }, + { calling_code: '+44-1624', name: 'Isle of Man', emoji: '🇮🇲', iso_2: 'IM' }, + { calling_code: '+972', name: 'Israel', emoji: '🇮🇱', iso_2: 'IL' }, + { calling_code: '+39', name: 'Italy', emoji: '🇮🇹', iso_2: 'IT' }, + { calling_code: '+1-876', name: 'Jamaica', emoji: '🇯🇲', iso_2: 'JM' }, + { calling_code: '+81', name: 'Japan', emoji: '🇯🇵', iso_2: 'JP' }, + { calling_code: '+44-1534', name: 'Jersey', emoji: '🇯🇪', iso_2: 'JE' }, + { calling_code: '+962', name: 'Jordan', emoji: '🇯🇴', iso_2: 'JO' }, + { calling_code: '+7', name: 'Kazakhstan', emoji: '🇰🇿', iso_2: 'KZ' }, + { calling_code: '+254', name: 'Kenya', emoji: '🇰🇪', iso_2: 'KE' }, + { calling_code: '+686', name: 'Kiribati', emoji: '🇰🇮', iso_2: 'KI' }, + { calling_code: '+383', name: 'Kosovo', emoji: '🇽🇰', iso_2: 'XK' }, + { calling_code: '+965', name: 'Kuwait', emoji: '🇰🇼', iso_2: 'KW' }, + { calling_code: '+996', name: 'Kyrgyzstan', emoji: '🇰🇬', iso_2: 'KG' }, + { calling_code: '+856', name: 'Laos', emoji: '🇱🇦', iso_2: 'LA' }, + { calling_code: '+371', name: 'Latvia', emoji: '🇱🇻', iso_2: 'LV' }, + { calling_code: '+961', name: 'Lebanon', emoji: '🇱🇧', iso_2: 'LB' }, + { calling_code: '+266', name: 'Lesotho', emoji: '🇱🇸', iso_2: 'LS' }, + { calling_code: '+231', name: 'Liberia', emoji: '🇱🇷', iso_2: 'LR' }, + { calling_code: '+218', name: 'Libya', emoji: '🇱🇾', iso_2: 'LY' }, + { calling_code: '+423', name: 'Liechtenstein', emoji: '🇱🇮', iso_2: 'LI' }, + { calling_code: '+370', name: 'Lithuania', emoji: '🇱🇹', iso_2: 'LT' }, + { calling_code: '+352', name: 'Luxembourg', emoji: '🇱🇺', iso_2: 'LU' }, + { calling_code: '+853', name: 'Macao', emoji: '🇲🇴', iso_2: 'MO' }, + { calling_code: '+389', name: 'North Macedonia', emoji: '🇲🇰', iso_2: 'MK' }, + { calling_code: '+261', name: 'Madagascar', emoji: '🇲🇬', iso_2: 'MG' }, + { calling_code: '+265', name: 'Malawi', emoji: '🇲🇼', iso_2: 'MW' }, + { calling_code: '+60', name: 'Malaysia', emoji: '🇲🇾', iso_2: 'MY' }, + { calling_code: '+960', name: 'Maldives', emoji: '🇲🇻', iso_2: 'MV' }, + { calling_code: '+223', name: 'Mali', emoji: '🇲🇱', iso_2: 'ML' }, + { calling_code: '+356', name: 'Malta', emoji: '🇲🇹', iso_2: 'MT' }, + { calling_code: '+692', name: 'Marshall Islands', emoji: '🇲🇭', iso_2: 'MH' }, + { calling_code: '+596', name: 'Martinique', emoji: '🇲🇶', iso_2: 'MQ' }, + { calling_code: '+222', name: 'Mauritania', emoji: '🇲🇷', iso_2: 'MR' }, + { calling_code: '+230', name: 'Mauritius', emoji: '🇲🇺', iso_2: 'MU' }, + { calling_code: '+262', name: 'Mayotte', emoji: '🇾🇹', iso_2: 'YT' }, + { calling_code: '+52', name: 'Mexico', emoji: '🇲🇽', iso_2: 'MX' }, + { calling_code: '+691', name: 'Micronesia', emoji: '🇫🇲', iso_2: 'FM' }, + { calling_code: '+373', name: 'Moldova', emoji: '🇲🇩', iso_2: 'MD' }, + { calling_code: '+377', name: 'Monaco', emoji: '🇲🇨', iso_2: 'MC' }, + { calling_code: '+976', name: 'Mongolia', emoji: '🇲🇳', iso_2: 'MN' }, + { calling_code: '+382', name: 'Montenegro', emoji: '🇲🇪', iso_2: 'ME' }, + { calling_code: '+1-664', name: 'Montserrat', emoji: '🇲🇸', iso_2: 'MS' }, + { calling_code: '+212', name: 'Morocco', emoji: '🇲🇦', iso_2: 'MA' }, + { calling_code: '+258', name: 'Mozambique', emoji: '🇲🇿', iso_2: 'MZ' }, + { calling_code: '+95', name: 'Myanmar', emoji: '🇲🇲', iso_2: 'MM' }, + { calling_code: '+264', name: 'Namibia', emoji: '🇳🇦', iso_2: 'NA' }, + { calling_code: '+674', name: 'Nauru', emoji: '🇳🇷', iso_2: 'NR' }, + { calling_code: '+977', name: 'Nepal', emoji: '🇳🇵', iso_2: 'NP' }, + { calling_code: '+31', name: 'Netherlands', emoji: '🇳🇱', iso_2: 'NL' }, + { calling_code: '+687', name: 'New Caledonia', emoji: '🇳🇨', iso_2: 'NC' }, + { calling_code: '+64', name: 'New Zealand', emoji: '🇳🇿', iso_2: 'NZ' }, + { calling_code: '+505', name: 'Nicaragua', emoji: '🇳🇮', iso_2: 'NI' }, + { calling_code: '+227', name: 'Niger', emoji: '🇳🇪', iso_2: 'NE' }, + { calling_code: '+234', name: 'Nigeria', emoji: '🇳🇬', iso_2: 'NG' }, + { calling_code: '+683', name: 'Niue', emoji: '🇳🇺', iso_2: 'NU' }, + { calling_code: '+672', name: 'Norfolk Island', emoji: '🇳🇫', iso_2: 'NF' }, + { calling_code: '+850', name: 'North Korea', emoji: '🇰🇵', iso_2: 'KP' }, + { calling_code: '+47', name: 'Norway', emoji: '🇳🇴', iso_2: 'NO' }, + { calling_code: '+968', name: 'Oman', emoji: '🇴🇲', iso_2: 'OM' }, + { calling_code: '+92', name: 'Pakistan', emoji: '🇵🇰', iso_2: 'PK' }, + { calling_code: '+680', name: 'Palau', emoji: '🇵🇼', iso_2: 'PW' }, + { calling_code: '+970', name: 'Palestine', emoji: '🇵🇸', iso_2: 'PS' }, + { calling_code: '+507', name: 'Panama', emoji: '🇵🇦', iso_2: 'PA' }, + { calling_code: '+675', name: 'Papua New Guinea', emoji: '🇵🇬', iso_2: 'PG' }, + { calling_code: '+595', name: 'Paraguay', emoji: '🇵🇾', iso_2: 'PY' }, + { calling_code: '+51', name: 'Peru', emoji: '🇵🇪', iso_2: 'PE' }, + { calling_code: '+63', name: 'Philippines', emoji: '🇵🇭', iso_2: 'PH' }, + { calling_code: '+64', name: 'Pitcairn Islands', emoji: '🇵🇳', iso_2: 'PN' }, + { calling_code: '+48', name: 'Poland', emoji: '🇵🇱', iso_2: 'PL' }, + { calling_code: '+351', name: 'Portugal', emoji: '🇵🇹', iso_2: 'PT' }, + { calling_code: '+1-787', name: 'Puerto Rico', emoji: '🇵🇷', iso_2: 'PR' }, + { calling_code: '+974', name: 'Qatar', emoji: '🇶🇦', iso_2: 'QA' }, + { calling_code: '+40', name: 'Romania', emoji: '🇷🇴', iso_2: 'RO' }, + { calling_code: '+7', name: 'Russia', emoji: '🇷🇺', iso_2: 'RU' }, + { calling_code: '+250', name: 'Rwanda', emoji: '🇷🇼', iso_2: 'RW' }, + { calling_code: '+590', name: 'Saint Barthélemy', emoji: '🇧🇱', iso_2: 'BL' }, + { calling_code: '+290', name: 'Saint Helena, Ascension and Tristan da Cunha', emoji: '🇸🇭', iso_2: 'SH' }, + { calling_code: '+1-869', name: 'Saint Kitts and Nevis', emoji: '🇰🇳', iso_2: 'KN' }, + { calling_code: '+1-758', name: 'Saint Lucia', emoji: '🇱🇨', iso_2: 'LC' }, + { calling_code: '+590', name: 'Saint Martin', emoji: '🇲🇫', iso_2: 'MF' }, + { calling_code: '+508', name: 'Saint Pierre and Miquelon', emoji: '🇵🇲', iso_2: 'PM' }, + { calling_code: '+1-784', name: 'Saint Vincent and the Grenadines', emoji: '🇻🇨', iso_2: 'VC' }, + { calling_code: '+685', name: 'Samoa', emoji: '🇼🇸', iso_2: 'WS' }, + { calling_code: '+378', name: 'San Marino', emoji: '🇸🇲', iso_2: 'SM' }, + { calling_code: '+239', name: 'Sao Tome and Principe', emoji: '🇸🇹', iso_2: 'ST' }, + { calling_code: '+966', name: 'Saudi Arabia', emoji: '🇸🇦', iso_2: 'SA' }, + { calling_code: '+221', name: 'Senegal', emoji: '🇸🇳', iso_2: 'SN' }, + { calling_code: '+381', name: 'Serbia', emoji: '🇷🇸', iso_2: 'RS' }, + { calling_code: '+248', name: 'Seychelles', emoji: '🇸🇨', iso_2: 'SC' }, + { calling_code: '+232', name: 'Sierra Leone', emoji: '🇸🇱', iso_2: 'SL' }, + { calling_code: '+65', name: 'Singapore', emoji: '🇸🇬', iso_2: 'SG' }, + { calling_code: '+1-721', name: 'Sint Maarten', emoji: '🇸🇽', iso_2: 'SX' }, + { calling_code: '+421', name: 'Slovakia', emoji: '🇸🇰', iso_2: 'SK' }, + { calling_code: '+386', name: 'Slovenia', emoji: '🇸🇮', iso_2: 'SI' }, + { calling_code: '+677', name: 'Solomon Islands', emoji: '🇸🇧', iso_2: 'SB' }, + { calling_code: '+252', name: 'Somalia', emoji: '🇸🇴', iso_2: 'SO' }, + { calling_code: '+27', name: 'South Africa', emoji: '🇿🇦', iso_2: 'ZA' }, + { calling_code: '+82', name: 'South Korea', emoji: '🇰🇷', iso_2: 'KR' }, + { calling_code: '+211', name: 'South Sudan', emoji: '🇸🇸', iso_2: 'SS' }, + { calling_code: '+34', name: 'Spain', emoji: '🇪🇸', iso_2: 'ES' }, + { calling_code: '+94', name: 'Sri Lanka', emoji: '🇱🇰', iso_2: 'LK' }, + { calling_code: '+249', name: 'Sudan', emoji: '🇸🇩', iso_2: 'SD' }, + { calling_code: '+597', name: 'Suriname', emoji: '🇸🇷', iso_2: 'SR' }, + { calling_code: '+47', name: 'Svalbard and Jan Mayen', emoji: '🇸🇯', iso_2: 'SJ' }, + { calling_code: '+46', name: 'Sweden', emoji: '🇸🇪', iso_2: 'SE' }, + { calling_code: '+41', name: 'Switzerland', emoji: '🇨🇭', iso_2: 'CH' }, + { calling_code: '+963', name: 'Syria', emoji: '🇸🇾', iso_2: 'SY' }, + { calling_code: '+886', name: 'Taiwan', emoji: '🇹🇼', iso_2: 'TW' }, + { calling_code: '+992', name: 'Tajikistan', emoji: '🇹🇯', iso_2: 'TJ' }, + { calling_code: '+255', name: 'Tanzania', emoji: '🇹🇿', iso_2: 'TZ' }, + { calling_code: '+66', name: 'Thailand', emoji: '🇹🇭', iso_2: 'TH' }, + { calling_code: '+670', name: 'Timor-Leste', emoji: '🇹🇱', iso_2: 'TL' }, + { calling_code: '+228', name: 'Togo', emoji: '🇹🇬', iso_2: 'TG' }, + { calling_code: '+690', name: 'Tokelau', emoji: '🇹🇰', iso_2: 'TK' }, + { calling_code: '+676', name: 'Tonga', emoji: '🇹🇴', iso_2: 'TO' }, + { calling_code: '+1-868', name: 'Trinidad and Tobago', emoji: '🇹🇹', iso_2: 'TT' }, + { calling_code: '+216', name: 'Tunisia', emoji: '🇹🇳', iso_2: 'TN' }, + { calling_code: '+90', name: 'Turkey', emoji: '🇹🇷', iso_2: 'TR' }, + { calling_code: '+993', name: 'Turkmenistan', emoji: '🇹🇲', iso_2: 'TM' }, + { calling_code: '+1-649', name: 'Turks and Caicos Islands', emoji: '🇹🇨', iso_2: 'TC' }, + { calling_code: '+688', name: 'Tuvalu', emoji: '🇹🇻', iso_2: 'TV' }, + { calling_code: '+256', name: 'Uganda', emoji: '🇺🇬', iso_2: 'UG' }, + { calling_code: '+380', name: 'Ukraine', emoji: '🇺🇦', iso_2: 'UA' }, + { calling_code: '+971', name: 'United Arab Emirates', emoji: '🇦🇪', iso_2: 'AE' }, + { calling_code: '+44', name: 'United Kingdom', emoji: '🇬🇧', iso_2: 'GB' }, + { calling_code: '+1', name: 'United States', emoji: '🇺🇸', iso_2: 'US' }, + { calling_code: '+598', name: 'Uruguay', emoji: '🇺🇾', iso_2: 'UY' }, + { calling_code: '+998', name: 'Uzbekistan', emoji: '🇺🇿', iso_2: 'UZ' }, + { calling_code: '+678', name: 'Vanuatu', emoji: '🇻🇺', iso_2: 'VU' }, + { calling_code: '+58', name: 'Venezuela', emoji: '🇻🇪', iso_2: 'VE' }, + { calling_code: '+84', name: 'Vietnam', emoji: '🇻🇳', iso_2: 'VN' }, + { calling_code: '+681', name: 'Wallis and Futuna', emoji: '🇼🇫', iso_2: 'WF' }, + { calling_code: '+212', name: 'Western Sahara', emoji: '🇪🇭', iso_2: 'EH' }, + { calling_code: '+967', name: 'Yemen', emoji: '🇾🇪', iso_2: 'YE' }, + { calling_code: '+260', name: 'Zambia', emoji: '🇿🇲', iso_2: 'ZM' }, + { calling_code: '+263', name: 'Zimbabwe', emoji: '🇿🇼', iso_2: 'ZW' } +] + +export default countries; \ No newline at end of file diff --git a/frontend/src/constants/navigation.js b/frontend/src/constants/navigation.js index 60b24dc..ecab814 100644 --- a/frontend/src/constants/navigation.js +++ b/frontend/src/constants/navigation.js @@ -126,3 +126,10 @@ export const accountNavItems = [ description: 'Update your profile' } ] + +export const contactNavItems = [ + { + titleKey: 'navigation.allContacts', + href: '/contacts', + } +] \ No newline at end of file diff --git a/frontend/src/features/contacts/ContactsList.vue b/frontend/src/features/contacts/ContactsList.vue new file mode 100644 index 0000000..dcf4903 --- /dev/null +++ b/frontend/src/features/contacts/ContactsList.vue @@ -0,0 +1,273 @@ + + + diff --git a/frontend/src/features/conversation/sidebar/ConversationSideBarContact.vue b/frontend/src/features/conversation/sidebar/ConversationSideBarContact.vue index 81f4f62..8cec811 100644 --- a/frontend/src/features/conversation/sidebar/ConversationSideBarContact.vue +++ b/frontend/src/features/conversation/sidebar/ConversationSideBarContact.vue @@ -1,10 +1,8 @@