diff --git a/app/database/entity/clients.entity.go b/app/database/entity/clients.entity.go index 20d76da..528126b 100644 --- a/app/database/entity/clients.entity.go +++ b/app/database/entity/clients.entity.go @@ -9,6 +9,7 @@ import ( type Clients struct { ID uuid.UUID `json:"id" gorm:"primaryKey;type:UUID"` Name string `json:"name" gorm:"type:varchar"` + Slug string `json:"slug" gorm:"type:varchar;uniqueIndex"` Description *string `json:"description" gorm:"type:text"` ClientType string `json:"client_type" gorm:"type:varchar;default:'sub_client'"` // 'parent_client', 'sub_client', 'standalone' ParentClientId *uuid.UUID `json:"parent_client_id" gorm:"type:UUID;index"` diff --git a/app/module/clients/mapper/clients.mapper.go b/app/module/clients/mapper/clients.mapper.go index b5ae671..9f8bb4b 100644 --- a/app/module/clients/mapper/clients.mapper.go +++ b/app/module/clients/mapper/clients.mapper.go @@ -10,6 +10,7 @@ func ClientsResponseMapper(clientsReq *entity.Clients) (clientsRes *res.ClientsR clientsRes = &res.ClientsResponse{ ID: clientsReq.ID, Name: clientsReq.Name, + Slug: clientsReq.Slug, Description: clientsReq.Description, ClientType: clientsReq.ClientType, ParentClientId: clientsReq.ParentClientId, diff --git a/app/module/clients/response/clients.response.go b/app/module/clients/response/clients.response.go index 01564bd..d989ccd 100644 --- a/app/module/clients/response/clients.response.go +++ b/app/module/clients/response/clients.response.go @@ -14,6 +14,7 @@ import ( type ClientsResponse struct { ID uuid.UUID `json:"id"` Name string `json:"name"` + Slug string `json:"slug"` Description *string `json:"description"` ClientType string `json:"clientType"` ParentClientId *uuid.UUID `json:"parentClientId"` @@ -108,6 +109,7 @@ type UserInfo struct { type ClientHierarchyResponse struct { ID uuid.UUID `json:"id"` Name string `json:"name"` + Slug string `json:"slug"` Description *string `json:"description"` ClientType string `json:"clientType"` Level int `json:"level"` // Depth in tree (0 = root) diff --git a/app/module/clients/service/client_logo_upload.service.go b/app/module/clients/service/client_logo_upload.service.go index 0995bfa..132cbf9 100644 --- a/app/module/clients/service/client_logo_upload.service.go +++ b/app/module/clients/service/client_logo_upload.service.go @@ -30,7 +30,7 @@ func NewClientLogoUploadService(minioStorage *config.MinioStorage, log zerolog.L } // UploadLogo uploads client logo to MinIO and returns the image path -func (s *ClientLogoUploadService) UploadLogo(c *fiber.Ctx, clientId, clientName string) (string, string, error) { +func (s *ClientLogoUploadService) UploadLogo(c *fiber.Ctx, clientId, clientSlug string) (string, string, error) { s.Log.Info(). Str("UploadLogo", clientId). @@ -71,7 +71,7 @@ func (s *ClientLogoUploadService) UploadLogo(c *fiber.Ctx, clientId, clientName Str("Upload Logo bucketName", clientId).Interface("bucketName", bucketName).Msg("Client upload logo files") // Generate unique filename using client name - filename := s.generateUniqueFilename(file.Filename, clientName) + filename := s.generateUniqueFilename(file.Filename, clientSlug) objectName := fmt.Sprintf("clients/logos/%s/%s", clientId, filename) // Open file @@ -211,12 +211,9 @@ func (s *ClientLogoUploadService) isValidImageType(filename string) bool { } // generateUniqueFilename generates a unique filename using client name slug + Unix timestamp -func (s *ClientLogoUploadService) generateUniqueFilename(originalFilename, clientName string) string { +func (s *ClientLogoUploadService) generateUniqueFilename(originalFilename, clientSlug string) string { ext := filepath.Ext(originalFilename) - // Create slug from client name - clientSlug := s.createSlug(clientName) - // Get Unix timestamp now := time.Now() timestamp := now.Unix() diff --git a/app/module/clients/service/clients.service.go b/app/module/clients/service/clients.service.go index 550f8de..07c8cd2 100644 --- a/app/module/clients/service/clients.service.go +++ b/app/module/clients/service/clients.service.go @@ -157,9 +157,13 @@ func (_i *clientsService) ShowWithAuth(authToken string) (clients *response.Clie func (_i *clientsService) Save(req request.ClientsCreateRequest, authToken string) (clients *entity.Clients, err error) { _i.Log.Info().Interface("data", req).Msg("") + // Generate slug from client name + slug := utilSvc.MakeSlug(req.Name) + // Convert request to entity newReq := &entity.Clients{ Name: req.Name, + Slug: slug, Description: req.Description, ClientType: req.ClientType, ParentClientId: req.ParentClientId, @@ -269,9 +273,13 @@ func (_i *clientsService) CreateSubClient(parentId uuid.UUID, req request.Client req.ClientType = "sub_client" req.ParentClientId = &parentId + // Generate slug from client name + slug := utilSvc.MakeSlug(req.Name) + // Convert to entity newReq := &entity.Clients{ Name: req.Name, + Slug: slug, Description: req.Description, ClientType: req.ClientType, ParentClientId: req.ParentClientId, @@ -326,6 +334,7 @@ func (_i *clientsService) buildHierarchyResponse(client *entity.Clients, level i resp := &response.ClientHierarchyResponse{ ID: client.ID, Name: client.Name, + Slug: client.Slug, Description: client.Description, ClientType: client.ClientType, Level: level, @@ -428,8 +437,13 @@ func (_i *clientsService) CreateClientWithUser(req request.ClientWithUserCreateR // Step 1: Create the client clientReq := req.Client + + // Generate slug from client name + slug := utilSvc.MakeSlug(clientReq.Name) + newClient := &entity.Clients{ Name: clientReq.Name, + Slug: slug, Description: clientReq.Description, ClientType: clientReq.ClientType, ParentClientId: clientReq.ParentClientId, @@ -553,7 +567,7 @@ func (_i *clientsService) UploadLogo(authToken string, c *fiber.Ctx) (string, er } // Upload logo using the upload service - imagePath, newFilename, err := _i.ClientLogoUploadSvc.UploadLogo(c, clientId.String(), client.Name) + imagePath, newFilename, err := _i.ClientLogoUploadSvc.UploadLogo(c, clientId.String(), client.Slug) if err != nil { _i.Log.Error().Err(err).Str("clientId", clientId.String()).Msg("Failed to upload client logo") return "", err diff --git a/docs/migrations/003_add_client_slug_field.sql b/docs/migrations/003_add_client_slug_field.sql new file mode 100644 index 0000000..938df29 --- /dev/null +++ b/docs/migrations/003_add_client_slug_field.sql @@ -0,0 +1,42 @@ +-- Migration: Add slug field to clients table +-- Description: Adds a unique slug field to the clients table for URL-friendly client identification + +ALTER TABLE clients +ADD COLUMN slug VARCHAR(255); + +-- Add unique index on slug field +CREATE UNIQUE INDEX IF NOT EXISTS idx_clients_slug ON clients(slug); + +-- Add comment +COMMENT ON COLUMN clients.slug IS 'URL-friendly slug generated from client name'; + +-- Update existing records with slugs based on their names +-- Note: This will generate slugs for existing clients +UPDATE clients +SET slug = LOWER( + REGEXP_REPLACE( + REGEXP_REPLACE(name, '[^a-zA-Z0-9\s-]', '', 'g'), + '\s+', '-', 'g' + ) +) +WHERE slug IS NULL OR slug = ''; + +-- Handle potential duplicates by appending numbers +WITH numbered_slugs AS ( + SELECT + id, + slug, + ROW_NUMBER() OVER (PARTITION BY slug ORDER BY created_at) as rn + FROM clients + WHERE slug IS NOT NULL +) +UPDATE clients +SET slug = CASE + WHEN ns.rn > 1 THEN ns.slug || '-' || ns.rn + ELSE ns.slug +END +FROM numbered_slugs ns +WHERE clients.id = ns.id AND ns.rn > 1; + +-- Make slug field NOT NULL after populating existing data +ALTER TABLE clients ALTER COLUMN slug SET NOT NULL;