diff --git a/cmd/web/docs/docs.go b/cmd/web/docs/docs.go index 56538b5..780f29c 100644 --- a/cmd/web/docs/docs.go +++ b/cmd/web/docs/docs.go @@ -904,6 +904,454 @@ const docTemplate = `{ } } }, + "/admin/v1/company-categories": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Search categories with filtering capabilities", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Admin-Company-Categories" + ], + "summary": "Search categories", + "parameters": [ + { + "type": "string", + "description": "Search query", + "name": "q", + "in": "query" + }, + { + "type": "boolean", + "description": "Published status filter", + "name": "published", + "in": "query" + }, + { + "type": "integer", + "default": 20, + "description": "Limit", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "default": 0, + "description": "Offset", + "name": "offset", + "in": "query" + }, + { + "enum": [ + "name", + "created_at", + "updated_at", + "published_at" + ], + "type": "string", + "description": "Sort by field", + "name": "sort_by", + "in": "query" + }, + { + "enum": [ + "asc", + "desc" + ], + "type": "string", + "description": "Sort order", + "name": "sort_order", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Categories search completed", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/company_category.CategoryListResponse" + } + } + } + ] + } + }, + "400": { + "description": "Bad request - Invalid query parameters", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "422": { + "description": "Validation error - Invalid query parameters", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + }, + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Create a new category with name, description, and thumbnail", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Admin-Company-Categories" + ], + "summary": "Create a new category", + "parameters": [ + { + "description": "Category information", + "name": "category", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/company_category.CategoryForm" + } + } + ], + "responses": { + "201": { + "description": "Category created successfully", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/company_category.CategoryResponse" + } + } + } + ] + } + }, + "400": { + "description": "Bad request - Invalid input data", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "409": { + "description": "Conflict - Category with this name already exists", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "422": { + "description": "Validation error - Invalid request data", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/admin/v1/company-categories/{id}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Retrieve detailed category information by category ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Admin-Company-Categories" + ], + "summary": "Get category by ID", + "parameters": [ + { + "type": "string", + "description": "Category ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Category retrieved successfully", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/company_category.CategoryResponse" + } + } + } + ] + } + }, + "400": { + "description": "Bad request - Invalid category ID", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "404": { + "description": "Not found - Category not found", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + }, + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Update category information including name, description, and thumbnail", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Admin-Company-Categories" + ], + "summary": "Update category information", + "parameters": [ + { + "type": "string", + "description": "Category ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Category update information", + "name": "category", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/company_category.CategoryForm" + } + } + ], + "responses": { + "200": { + "description": "Category updated successfully", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/company_category.CategoryResponse" + } + } + } + ] + } + }, + "400": { + "description": "Bad request - Invalid input data", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "404": { + "description": "Not found - Category not found", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "409": { + "description": "Conflict - Category with this name already exists", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "422": { + "description": "Validation error - Invalid request data", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + }, + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Delete a category permanently", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Admin-Company-Categories" + ], + "summary": "Delete category", + "parameters": [ + { + "type": "string", + "description": "Category ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Category deleted successfully", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "400": { + "description": "Bad request - Invalid category ID", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "404": { + "description": "Not found - Category not found", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/admin/v1/company-categories/{id}/publish": { + "patch": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Toggle category publish/unpublish status", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Admin-Company-Categories" + ], + "summary": "Toggle category publish status", + "parameters": [ + { + "type": "string", + "description": "Category ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Category publish status updated successfully", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "400": { + "description": "Bad request - Invalid input data", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "404": { + "description": "Not found - Category not found", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "422": { + "description": "Validation error - Invalid request data", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, "/admin/v1/customers": { "get": { "security": [ @@ -5487,6 +5935,63 @@ const docTemplate = `{ } } }, + "company_category.CategoryForm": { + "type": "object", + "properties": { + "description": { + "type": "string", + "example": "Technology related companies" + }, + "name": { + "type": "string", + "example": "Technology" + }, + "thumbnail": { + "type": "string", + "example": "https://example.com/thumbnail.jpg" + } + } + }, + "company_category.CategoryListResponse": { + "type": "object", + "properties": { + "categories": { + "type": "array", + "items": { + "$ref": "#/definitions/company_category.CategoryResponse" + } + } + } + }, + "company_category.CategoryResponse": { + "type": "object", + "properties": { + "created_at": { + "type": "integer" + }, + "description": { + "type": "string" + }, + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "published": { + "type": "boolean" + }, + "published_at": { + "type": "integer" + }, + "thumbnail": { + "type": "string" + }, + "updated_at": { + "type": "integer" + } + } + }, "customer.AuthResponse": { "type": "object", "properties": { @@ -6552,6 +7057,10 @@ const docTemplate = `{ "description": "Administrative company management operations for web panel including CRUD operations, tag management, verification, status updates, comprehensive search and filtering, and statistical reporting", "name": "Admin-Companies" }, + { + "description": "Administrative company category management operations for web panel including CRUD operations, publish/unpublish status, and comprehensive filtering with pagination", + "name": "Admin-Company-Categories" + }, { "description": "Administrative tender management operations for web panel including CRUD operations, search, statistics, and comprehensive filtering with pagination", "name": "Admin-Tenders" diff --git a/cmd/web/docs/swagger.json b/cmd/web/docs/swagger.json index e4ef130..7dadc1d 100644 --- a/cmd/web/docs/swagger.json +++ b/cmd/web/docs/swagger.json @@ -898,6 +898,454 @@ } } }, + "/admin/v1/company-categories": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Search categories with filtering capabilities", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Admin-Company-Categories" + ], + "summary": "Search categories", + "parameters": [ + { + "type": "string", + "description": "Search query", + "name": "q", + "in": "query" + }, + { + "type": "boolean", + "description": "Published status filter", + "name": "published", + "in": "query" + }, + { + "type": "integer", + "default": 20, + "description": "Limit", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "default": 0, + "description": "Offset", + "name": "offset", + "in": "query" + }, + { + "enum": [ + "name", + "created_at", + "updated_at", + "published_at" + ], + "type": "string", + "description": "Sort by field", + "name": "sort_by", + "in": "query" + }, + { + "enum": [ + "asc", + "desc" + ], + "type": "string", + "description": "Sort order", + "name": "sort_order", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Categories search completed", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/company_category.CategoryListResponse" + } + } + } + ] + } + }, + "400": { + "description": "Bad request - Invalid query parameters", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "422": { + "description": "Validation error - Invalid query parameters", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + }, + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Create a new category with name, description, and thumbnail", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Admin-Company-Categories" + ], + "summary": "Create a new category", + "parameters": [ + { + "description": "Category information", + "name": "category", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/company_category.CategoryForm" + } + } + ], + "responses": { + "201": { + "description": "Category created successfully", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/company_category.CategoryResponse" + } + } + } + ] + } + }, + "400": { + "description": "Bad request - Invalid input data", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "409": { + "description": "Conflict - Category with this name already exists", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "422": { + "description": "Validation error - Invalid request data", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/admin/v1/company-categories/{id}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Retrieve detailed category information by category ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Admin-Company-Categories" + ], + "summary": "Get category by ID", + "parameters": [ + { + "type": "string", + "description": "Category ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Category retrieved successfully", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/company_category.CategoryResponse" + } + } + } + ] + } + }, + "400": { + "description": "Bad request - Invalid category ID", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "404": { + "description": "Not found - Category not found", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + }, + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Update category information including name, description, and thumbnail", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Admin-Company-Categories" + ], + "summary": "Update category information", + "parameters": [ + { + "type": "string", + "description": "Category ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Category update information", + "name": "category", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/company_category.CategoryForm" + } + } + ], + "responses": { + "200": { + "description": "Category updated successfully", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/company_category.CategoryResponse" + } + } + } + ] + } + }, + "400": { + "description": "Bad request - Invalid input data", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "404": { + "description": "Not found - Category not found", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "409": { + "description": "Conflict - Category with this name already exists", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "422": { + "description": "Validation error - Invalid request data", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + }, + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Delete a category permanently", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Admin-Company-Categories" + ], + "summary": "Delete category", + "parameters": [ + { + "type": "string", + "description": "Category ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Category deleted successfully", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "400": { + "description": "Bad request - Invalid category ID", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "404": { + "description": "Not found - Category not found", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/admin/v1/company-categories/{id}/publish": { + "patch": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Toggle category publish/unpublish status", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Admin-Company-Categories" + ], + "summary": "Toggle category publish status", + "parameters": [ + { + "type": "string", + "description": "Category ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Category publish status updated successfully", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "400": { + "description": "Bad request - Invalid input data", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "404": { + "description": "Not found - Category not found", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "422": { + "description": "Validation error - Invalid request data", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, "/admin/v1/customers": { "get": { "security": [ @@ -5481,6 +5929,63 @@ } } }, + "company_category.CategoryForm": { + "type": "object", + "properties": { + "description": { + "type": "string", + "example": "Technology related companies" + }, + "name": { + "type": "string", + "example": "Technology" + }, + "thumbnail": { + "type": "string", + "example": "https://example.com/thumbnail.jpg" + } + } + }, + "company_category.CategoryListResponse": { + "type": "object", + "properties": { + "categories": { + "type": "array", + "items": { + "$ref": "#/definitions/company_category.CategoryResponse" + } + } + } + }, + "company_category.CategoryResponse": { + "type": "object", + "properties": { + "created_at": { + "type": "integer" + }, + "description": { + "type": "string" + }, + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "published": { + "type": "boolean" + }, + "published_at": { + "type": "integer" + }, + "thumbnail": { + "type": "string" + }, + "updated_at": { + "type": "integer" + } + } + }, "customer.AuthResponse": { "type": "object", "properties": { @@ -6546,6 +7051,10 @@ "description": "Administrative company management operations for web panel including CRUD operations, tag management, verification, status updates, comprehensive search and filtering, and statistical reporting", "name": "Admin-Companies" }, + { + "description": "Administrative company category management operations for web panel including CRUD operations, publish/unpublish status, and comprehensive filtering with pagination", + "name": "Admin-Company-Categories" + }, { "description": "Administrative tender management operations for web panel including CRUD operations, search, statistics, and comprehensive filtering with pagination", "name": "Admin-Tenders" diff --git a/cmd/web/docs/swagger.yaml b/cmd/web/docs/swagger.yaml index 80d87c8..fc3a65f 100644 --- a/cmd/web/docs/swagger.yaml +++ b/cmd/web/docs/swagger.yaml @@ -263,6 +263,44 @@ definitions: is_verified: type: boolean type: object + company_category.CategoryForm: + properties: + description: + example: Technology related companies + type: string + name: + example: Technology + type: string + thumbnail: + example: https://example.com/thumbnail.jpg + type: string + type: object + company_category.CategoryListResponse: + properties: + categories: + items: + $ref: '#/definitions/company_category.CategoryResponse' + type: array + type: object + company_category.CategoryResponse: + properties: + created_at: + type: integer + description: + type: string + id: + type: string + name: + type: string + published: + type: boolean + published_at: + type: integer + thumbnail: + type: string + updated_at: + type: integer + type: object customer.AuthResponse: properties: access_token: @@ -1545,6 +1583,285 @@ paths: summary: Update company verification status tags: - Admin-Companies + /admin/v1/company-categories: + get: + consumes: + - application/json + description: Search categories with filtering capabilities + parameters: + - description: Search query + in: query + name: q + type: string + - description: Published status filter + in: query + name: published + type: boolean + - default: 20 + description: Limit + in: query + name: limit + type: integer + - default: 0 + description: Offset + in: query + name: offset + type: integer + - description: Sort by field + enum: + - name + - created_at + - updated_at + - published_at + in: query + name: sort_by + type: string + - description: Sort order + enum: + - asc + - desc + in: query + name: sort_order + type: string + produces: + - application/json + responses: + "200": + description: Categories search completed + schema: + allOf: + - $ref: '#/definitions/response.APIResponse' + - properties: + data: + $ref: '#/definitions/company_category.CategoryListResponse' + type: object + "400": + description: Bad request - Invalid query parameters + schema: + $ref: '#/definitions/response.APIResponse' + "422": + description: Validation error - Invalid query parameters + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal server error + schema: + $ref: '#/definitions/response.APIResponse' + security: + - BearerAuth: [] + summary: Search categories + tags: + - Admin-Company-Categories + post: + consumes: + - application/json + description: Create a new category with name, description, and thumbnail + parameters: + - description: Category information + in: body + name: category + required: true + schema: + $ref: '#/definitions/company_category.CategoryForm' + produces: + - application/json + responses: + "201": + description: Category created successfully + schema: + allOf: + - $ref: '#/definitions/response.APIResponse' + - properties: + data: + $ref: '#/definitions/company_category.CategoryResponse' + type: object + "400": + description: Bad request - Invalid input data + schema: + $ref: '#/definitions/response.APIResponse' + "409": + description: Conflict - Category with this name already exists + schema: + $ref: '#/definitions/response.APIResponse' + "422": + description: Validation error - Invalid request data + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal server error + schema: + $ref: '#/definitions/response.APIResponse' + security: + - BearerAuth: [] + summary: Create a new category + tags: + - Admin-Company-Categories + /admin/v1/company-categories/{id}: + delete: + consumes: + - application/json + description: Delete a category permanently + parameters: + - description: Category ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: Category deleted successfully + schema: + $ref: '#/definitions/response.APIResponse' + "400": + description: Bad request - Invalid category ID + schema: + $ref: '#/definitions/response.APIResponse' + "404": + description: Not found - Category not found + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal server error + schema: + $ref: '#/definitions/response.APIResponse' + security: + - BearerAuth: [] + summary: Delete category + tags: + - Admin-Company-Categories + get: + consumes: + - application/json + description: Retrieve detailed category information by category ID + parameters: + - description: Category ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: Category retrieved successfully + schema: + allOf: + - $ref: '#/definitions/response.APIResponse' + - properties: + data: + $ref: '#/definitions/company_category.CategoryResponse' + type: object + "400": + description: Bad request - Invalid category ID + schema: + $ref: '#/definitions/response.APIResponse' + "404": + description: Not found - Category not found + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal server error + schema: + $ref: '#/definitions/response.APIResponse' + security: + - BearerAuth: [] + summary: Get category by ID + tags: + - Admin-Company-Categories + put: + consumes: + - application/json + description: Update category information including name, description, and thumbnail + parameters: + - description: Category ID + in: path + name: id + required: true + type: string + - description: Category update information + in: body + name: category + required: true + schema: + $ref: '#/definitions/company_category.CategoryForm' + produces: + - application/json + responses: + "200": + description: Category updated successfully + schema: + allOf: + - $ref: '#/definitions/response.APIResponse' + - properties: + data: + $ref: '#/definitions/company_category.CategoryResponse' + type: object + "400": + description: Bad request - Invalid input data + schema: + $ref: '#/definitions/response.APIResponse' + "404": + description: Not found - Category not found + schema: + $ref: '#/definitions/response.APIResponse' + "409": + description: Conflict - Category with this name already exists + schema: + $ref: '#/definitions/response.APIResponse' + "422": + description: Validation error - Invalid request data + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal server error + schema: + $ref: '#/definitions/response.APIResponse' + security: + - BearerAuth: [] + summary: Update category information + tags: + - Admin-Company-Categories + /admin/v1/company-categories/{id}/publish: + patch: + consumes: + - application/json + description: Toggle category publish/unpublish status + parameters: + - description: Category ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: Category publish status updated successfully + schema: + $ref: '#/definitions/response.APIResponse' + "400": + description: Bad request - Invalid input data + schema: + $ref: '#/definitions/response.APIResponse' + "404": + description: Not found - Category not found + schema: + $ref: '#/definitions/response.APIResponse' + "422": + description: Validation error - Invalid request data + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal server error + schema: + $ref: '#/definitions/response.APIResponse' + security: + - BearerAuth: [] + summary: Toggle category publish status + tags: + - Admin-Company-Categories /admin/v1/customers: get: consumes: @@ -4185,6 +4502,10 @@ tags: CRUD operations, tag management, verification, status updates, comprehensive search and filtering, and statistical reporting name: Admin-Companies +- description: Administrative company category management operations for web panel + including CRUD operations, publish/unpublish status, and comprehensive filtering + with pagination + name: Admin-Company-Categories - description: Administrative tender management operations for web panel including CRUD operations, search, statistics, and comprehensive filtering with pagination name: Admin-Tenders diff --git a/cmd/web/main.go b/cmd/web/main.go index d00c52c..aadf427 100644 --- a/cmd/web/main.go +++ b/cmd/web/main.go @@ -35,6 +35,9 @@ package main // @tag.name Admin-Companies // @tag.description Administrative company management operations for web panel including CRUD operations, tag management, verification, status updates, comprehensive search and filtering, and statistical reporting +// @tag.name Admin-Company-Categories +// @tag.description Administrative company category management operations for web panel including CRUD operations, publish/unpublish status, and comprehensive filtering with pagination + // @tag.name Admin-Tenders // @tag.description Administrative tender management operations for web panel including CRUD operations, search, statistics, and comprehensive filtering with pagination @@ -71,6 +74,7 @@ import ( "net/http" "time" "tm/internal/company" + "tm/internal/company_category" "tm/internal/customer" "tm/internal/feedback" "tm/internal/inquiry" @@ -119,12 +123,13 @@ func main() { userRepository := user.NewUserRepository(mongoManager, logger) customerRepository := customer.NewCustomerRepository(mongoManager, logger) companyRepository := company.NewCompanyRepository(mongoManager, logger) + categoryRepository := company_category.NewCategoryRepository(mongoManager, logger) tenderRepository := tender.NewTenderRepository(mongoManager, logger) feedbackRepo := feedback.NewFeedbackRepository(mongoManager, logger) tenderApprovalRepository := tender_approval.NewTenderApprovalRepository(mongoManager, logger) inquiryRepository := inquiry.NewInquiryRepository(mongoManager, logger) logger.Info("Repositories initialized successfully", map[string]interface{}{ - "repositories": []string{"customer", "user", "company", "tender", "feedback", "tender_approval"}, + "repositories": []string{"customer", "user", "company", "category", "tender", "feedback", "tender_approval"}, }) // Initialize validation service @@ -133,33 +138,35 @@ func main() { // Initialize services with repositories userService := user.NewUserService(userRepository, logger, userAuthService, userValidator) companyService := company.NewCompanyService(companyRepository, logger) + categoryService := company_category.NewCategoryService(categoryRepository, logger) customerService := customer.NewCustomerService(customerRepository, logger, customerAuthService, companyService) tenderService := tender.NewTenderService(tenderRepository, companyService, logger) feedbackService := feedback.NewFeedbackService(feedbackRepo, tenderService, logger) tenderApprovalService := tender_approval.NewTenderApprovalService(tenderApprovalRepository, tenderService, logger) inquiryService := inquiry.NewInquiryService(inquiryRepository, logger) logger.Info("Services initialized successfully", map[string]interface{}{ - "services": []string{"customer", "user", "company", "tender", "feedback", "tender_approval"}, + "services": []string{"customer", "user", "company", "category", "tender", "feedback", "tender_approval"}, }) // Initialize handlers with services userHandler := user.NewUserHandler(userService, logger, userValidator, userAuthService) customerHandler := customer.NewHandler(customerService, userHandler, customerAuthService, logger) companyHandler := company.NewHandler(companyService, userHandler, logger) + categoryHandler := company_category.NewHandler(categoryService, logger) tenderHandler := tender.NewTenderHandler(tenderService, logger) feedbackHandler := feedback.NewFeedbackHandler(feedbackService, userHandler, customerHandler, logger) tenderApprovalHandler := tender_approval.NewHandler(tenderApprovalService, logger) inquiryHandler := inquiry.NewHandler(inquiryService, logger) logger.Info("Handlers initialized successfully", map[string]interface{}{ - "handlers": []string{"customer", "user", "company", "tender", "feedback", "tender_approval"}, + "handlers": []string{"customer", "user", "company", "category", "tender", "feedback", "tender_approval"}, }) // Initialize HTTP server e := bootstrap.InitHTTPServer(conf, logger) // Register routes - router.RegisterAdminRoutes(e, userHandler, companyHandler, customerHandler, tenderHandler, feedbackHandler, tenderApprovalHandler, inquiryHandler) - router.RegisterPublicRoutes(e, customerHandler, tenderHandler, feedbackHandler, tenderApprovalHandler, companyHandler, inquiryHandler) + router.RegisterAdminRoutes(e, userHandler, companyHandler, customerHandler, tenderHandler, feedbackHandler, tenderApprovalHandler, inquiryHandler, categoryHandler) + router.RegisterPublicRoutes(e, customerHandler, tenderHandler, feedbackHandler, tenderApprovalHandler, companyHandler, inquiryHandler, categoryHandler) // Start server serverAddr := fmt.Sprintf("%s:%d", conf.Server.Host, conf.Server.Port) diff --git a/cmd/web/router/routes.go b/cmd/web/router/routes.go index 03480fb..5bd7700 100644 --- a/cmd/web/router/routes.go +++ b/cmd/web/router/routes.go @@ -2,6 +2,7 @@ package router import ( "tm/internal/company" + "tm/internal/company_category" "tm/internal/customer" "tm/internal/feedback" "tm/internal/inquiry" @@ -12,7 +13,7 @@ import ( "github.com/labstack/echo/v4" ) -func RegisterAdminRoutes(e *echo.Echo, userHandler *user.Handler, companyHandler *company.Handler, customerHandler *customer.Handler, tenderHandler *tender.TenderHandler, feedbackHandler *feedback.Handler, tenderApprovalHandler *tender_approval.Handler, inquiryHandler *inquiry.Handler) { +func RegisterAdminRoutes(e *echo.Echo, userHandler *user.Handler, companyHandler *company.Handler, customerHandler *customer.Handler, tenderHandler *tender.TenderHandler, feedbackHandler *feedback.Handler, tenderApprovalHandler *tender_approval.Handler, inquiryHandler *inquiry.Handler, categoryHandler *company_category.Handler) { adminV1 := e.Group("/admin/v1") // Admin Users Routes @@ -56,6 +57,18 @@ func RegisterAdminRoutes(e *echo.Echo, userHandler *user.Handler, companyHandler companiesGP.PATCH("/:id/verification", companyHandler.UpdateVerification) } + // Admin Categories Routes + categoriesGP := adminV1.Group("/company-categories") + { + categoriesGP.Use(userHandler.AuthMiddleware()) + categoriesGP.POST("", categoryHandler.Create) + categoriesGP.GET("", categoryHandler.Search) + categoriesGP.GET("/:id", categoryHandler.Get) + categoriesGP.PUT("/:id", categoryHandler.Update) + categoriesGP.DELETE("/:id", categoryHandler.Delete) + categoriesGP.PATCH("/:id/publish", categoryHandler.TogglePublish) + } + // Admin Customers Routes customersGP := adminV1.Group("/customers") { @@ -109,7 +122,7 @@ func RegisterAdminRoutes(e *echo.Echo, userHandler *user.Handler, companyHandler } } -func RegisterPublicRoutes(e *echo.Echo, customerHandler *customer.Handler, tenderHandler *tender.TenderHandler, feedbackHandler *feedback.Handler, tenderApprovalHandler *tender_approval.Handler, companyHandler *company.Handler, inquiryHandler *inquiry.Handler) { +func RegisterPublicRoutes(e *echo.Echo, customerHandler *customer.Handler, tenderHandler *tender.TenderHandler, feedbackHandler *feedback.Handler, tenderApprovalHandler *tender_approval.Handler, companyHandler *company.Handler, inquiryHandler *inquiry.Handler, categoryHandler *company_category.Handler) { v1 := e.Group("/api/v1") customerGP := v1.Group("/profile") diff --git a/internal/company_category/entity.go b/internal/company_category/entity.go new file mode 100644 index 0000000..04ca39d --- /dev/null +++ b/internal/company_category/entity.go @@ -0,0 +1,46 @@ +package company_category + +import ( + "tm/pkg/mongo" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type Category struct { + mongo.Model `bson:",inline"` + Name string `bson:"name" json:"name"` + Description *string `bson:"description" json:"description"` + Thumbnail *string `bson:"thumbnail" json:"thumbnail"` + Published bool `bson:"published" json:"published"` + PublishedAt *int64 `bson:"published_at" json:"published_at"` +} + +// SetID sets the category ID (implements IDSetter interface) +func (c *Category) SetID(id string) { + c.ID, _ = primitive.ObjectIDFromHex(id) +} + +// GetID returns the category ID (implements IDGetter interface) +func (c *Category) GetID() string { + return c.ID.Hex() +} + +// SetCreatedAt sets the created timestamp (implements Timestamp interface) +func (c *Category) SetCreatedAt(timestamp int64) { + c.CreatedAt = timestamp +} + +// SetUpdatedAt sets the updated timestamp (implements Timestamp interface) +func (c *Category) SetUpdatedAt(timestamp int64) { + c.UpdatedAt = timestamp +} + +// GetCreatedAt returns the created timestamp (implements Timestamp interface) +func (c *Category) GetCreatedAt() int64 { + return c.CreatedAt +} + +// GetUpdatedAt returns the updated timestamp (implements Timestamp interface) +func (c *Category) GetUpdatedAt() int64 { + return c.UpdatedAt +} diff --git a/internal/company_category/form.go b/internal/company_category/form.go new file mode 100644 index 0000000..6838ed4 --- /dev/null +++ b/internal/company_category/form.go @@ -0,0 +1,52 @@ +package company_category + +import "tm/pkg/response" + +// CategoryForm represents the form for creating/updating a category +type CategoryForm struct { + Name string `json:"name" valid:"required,length(2|100)" example:"Technology"` + Description *string `json:"description,omitempty" valid:"optional,length(10|500)" example:"Technology related companies"` + Thumbnail *string `json:"thumbnail,omitempty" valid:"optional,url" example:"https://example.com/thumbnail.jpg"` +} + +// SearchForm represents the form for searching categories with filters +type SearchForm struct { + Search *string `query:"q" valid:"optional"` + Published *bool `query:"published" valid:"optional"` + Limit *int `query:"limit" valid:"optional,range(1|100)"` + Offset *int `query:"offset" valid:"optional,min(0)"` + SortBy *string `query:"sort_by" valid:"optional,in(name|created_at|updated_at|published_at)"` + SortOrder *string `query:"sort_order" valid:"optional,in(asc|desc)"` +} + +// CategoryListResponse represents the response for listing categories +type CategoryListResponse struct { + Categories []*CategoryResponse `json:"categories"` + Meta *response.Meta `json:"-"` +} + +// CategoryResponse represents the category data sent in API responses +type CategoryResponse struct { + ID string `json:"id"` + Name string `json:"name"` + Description *string `json:"description,omitempty"` + Thumbnail *string `json:"thumbnail,omitempty"` + Published bool `json:"published"` + PublishedAt *int64 `json:"published_at,omitempty"` + CreatedAt int64 `json:"created_at"` + UpdatedAt int64 `json:"updated_at"` +} + +// ToResponse converts Category to CategoryResponse +func (c *Category) ToResponse() *CategoryResponse { + return &CategoryResponse{ + ID: c.ID.Hex(), + Name: c.Name, + Description: c.Description, + Thumbnail: c.Thumbnail, + Published: c.Published, + PublishedAt: c.PublishedAt, + CreatedAt: c.CreatedAt, + UpdatedAt: c.UpdatedAt, + } +} diff --git a/internal/company_category/handler.go b/internal/company_category/handler.go new file mode 100644 index 0000000..dd16683 --- /dev/null +++ b/internal/company_category/handler.go @@ -0,0 +1,213 @@ +package company_category + +import ( + "tm/pkg/logger" + "tm/pkg/response" + + "github.com/labstack/echo/v4" +) + +// Handler handles HTTP requests for category operations +type Handler struct { + service Service + logger logger.Logger +} + +// NewHandler creates a new category handler +func NewHandler(service Service, logger logger.Logger) *Handler { + return &Handler{ + service: service, + logger: logger, + } +} + +// Create creates a new category (Web Panel) +// @Summary Create a new category +// @Description Create a new category with name, description, and thumbnail +// @Tags Admin-Company-Categories +// @Accept json +// @Produce json +// @Param category body CategoryForm true "Category information" +// @Success 201 {object} response.APIResponse{data=CategoryResponse} "Category created successfully" +// @Failure 400 {object} response.APIResponse "Bad request - Invalid input data" +// @Failure 409 {object} response.APIResponse "Conflict - Category with this name already exists" +// @Failure 422 {object} response.APIResponse "Validation error - Invalid request data" +// @Failure 500 {object} response.APIResponse "Internal server error" +// @Security BearerAuth +// @Router /admin/v1/company-categories [post] +func (h *Handler) Create(c echo.Context) error { + form, err := response.Parse[CategoryForm](c) + if err != nil { + return response.ValidationError(c, "Invalid request data", err.Error()) + } + + category, err := h.service.Create(c.Request().Context(), form) + if err != nil { + if err.Error() == "category with this name already exists" { + return response.Conflict(c, err.Error()) + } + return response.InternalServerError(c, "Failed to create category") + } + + return response.Created(c, category, "Category created successfully") +} + +// GetCategory retrieves a category by ID (Web Panel) +// @Summary Get category by ID +// @Description Retrieve detailed category information by category ID +// @Tags Admin-Company-Categories +// @Accept json +// @Produce json +// @Param id path string true "Category ID" +// @Success 200 {object} response.APIResponse{data=CategoryResponse} "Category retrieved successfully" +// @Failure 400 {object} response.APIResponse "Bad request - Invalid category ID" +// @Failure 404 {object} response.APIResponse "Not found - Category not found" +// @Failure 500 {object} response.APIResponse "Internal server error" +// @Security BearerAuth +// @Router /admin/v1/company-categories/{id} [get] +func (h *Handler) Get(c echo.Context) error { + id := c.Param("id") + + category, err := h.service.GetByID(c.Request().Context(), id) + if err != nil { + if err.Error() == "category not found" { + return response.NotFound(c, "Category not found") + } + return response.InternalServerError(c, "Failed to get category") + } + + return response.Success(c, category, "Category retrieved successfully") +} + +// Update updates a category (Web Panel) +// @Summary Update category information +// @Description Update category information including name, description, and thumbnail +// @Tags Admin-Company-Categories +// @Accept json +// @Produce json +// @Param id path string true "Category ID" +// @Param category body CategoryForm true "Category update information" +// @Success 200 {object} response.APIResponse{data=CategoryResponse} "Category updated successfully" +// @Failure 400 {object} response.APIResponse "Bad request - Invalid input data" +// @Failure 404 {object} response.APIResponse "Not found - Category not found" +// @Failure 409 {object} response.APIResponse "Conflict - Category with this name already exists" +// @Failure 422 {object} response.APIResponse "Validation error - Invalid request data" +// @Failure 500 {object} response.APIResponse "Internal server error" +// @Security BearerAuth +// @Router /admin/v1/company-categories/{id} [put] +func (h *Handler) Update(c echo.Context) error { + id := c.Param("id") + form, err := response.Parse[CategoryForm](c) + if err != nil { + return response.ValidationError(c, "Invalid request data", err.Error()) + } + + category, err := h.service.Update(c.Request().Context(), id, form) + if err != nil { + if err.Error() == "category not found" { + return response.NotFound(c, "Category not found") + } + if err.Error() == "category with this name already exists" { + return response.Conflict(c, err.Error()) + } + return response.InternalServerError(c, "Failed to update category") + } + + return response.Success(c, category, "Category updated successfully") +} + +// Delete deletes a category (Web Panel) +// @Summary Delete category +// @Description Delete a category permanently +// @Tags Admin-Company-Categories +// @Accept json +// @Produce json +// @Param id path string true "Category ID" +// @Success 200 {object} response.APIResponse "Category deleted successfully" +// @Failure 400 {object} response.APIResponse "Bad request - Invalid category ID" +// @Failure 404 {object} response.APIResponse "Not found - Category not found" +// @Failure 500 {object} response.APIResponse "Internal server error" +// @Security BearerAuth +// @Router /admin/v1/company-categories/{id} [delete] +func (h *Handler) Delete(c echo.Context) error { + id := c.Param("id") + + err := h.service.Delete(c.Request().Context(), id) + if err != nil { + if err.Error() == "category not found" { + return response.NotFound(c, "Category not found") + } + return response.InternalServerError(c, "Failed to delete category") + } + + return response.Success(c, map[string]interface{}{ + "message": "Category deleted successfully", + }, "Category deleted successfully") +} + +// Search searches categories with filters (Web Panel) +// @Summary Search categories +// @Description Search categories with filtering capabilities +// @Tags Admin-Company-Categories +// @Accept json +// @Produce json +// @Param q query string false "Search query" +// @Param published query boolean false "Published status filter" +// @Param limit query integer false "Limit" default(20) +// @Param offset query integer false "Offset" default(0) +// @Param sort_by query string false "Sort by field" Enums(name,created_at,updated_at,published_at) +// @Param sort_order query string false "Sort order" Enums(asc,desc) +// @Success 200 {object} response.APIResponse{data=CategoryListResponse} "Categories search completed" +// @Failure 400 {object} response.APIResponse "Bad request - Invalid query parameters" +// @Failure 422 {object} response.APIResponse "Validation error - Invalid query parameters" +// @Failure 500 {object} response.APIResponse "Internal server error" +// @Security BearerAuth +// @Router /admin/v1/company-categories [get] +func (h *Handler) Search(c echo.Context) error { + form, err := response.Parse[SearchForm](c) + if err != nil { + return response.ValidationError(c, "Invalid request data", err.Error()) + } + + result, err := h.service.SearchCategories(c.Request().Context(), form, response.NewPagination(c)) + if err != nil { + return response.InternalServerError(c, "Failed to search categories") + } + + return response.SuccessWithMeta(c, result, result.Meta, "Categories retrieved successfully") +} + +// TogglePublish toggles category publish status (Web Panel) +// @Summary Toggle category publish status +// @Description Toggle category publish/unpublish status +// @Tags Admin-Company-Categories +// @Accept json +// @Produce json +// @Param id path string true "Category ID" +// @Success 200 {object} response.APIResponse "Category publish status updated successfully" +// @Failure 400 {object} response.APIResponse "Bad request - Invalid input data" +// @Failure 404 {object} response.APIResponse "Not found - Category not found" +// @Failure 422 {object} response.APIResponse "Validation error - Invalid request data" +// @Failure 500 {object} response.APIResponse "Internal server error" +// @Security BearerAuth +// @Router /admin/v1/company-categories/{id}/publish [patch] +func (h *Handler) TogglePublish(c echo.Context) error { + id := c.Param("id") + + published, err := h.service.TogglePublish(c.Request().Context(), id) + if err != nil { + if err.Error() == "category not found" { + return response.NotFound(c, "Category not found") + } + return response.InternalServerError(c, "Failed to toggle category publish status") + } + + return response.Success( + c, + map[string]interface{}{ + "message": "Category publish status updated successfully", + "published": published, + }, + "Category publish status updated successfully", + ) +} diff --git a/internal/company_category/repository.go b/internal/company_category/repository.go new file mode 100644 index 0000000..e0b395a --- /dev/null +++ b/internal/company_category/repository.go @@ -0,0 +1,255 @@ +package company_category + +import ( + "context" + "errors" + "time" + "tm/pkg/logger" + orm "tm/pkg/mongo" + "tm/pkg/response" + + "go.mongodb.org/mongo-driver/bson" +) + +// Repository defines the interface for category data operations +type Repository interface { + Create(ctx context.Context, category *Category) error + Update(ctx context.Context, category *Category) error + GetByID(ctx context.Context, id string) (*Category, error) + GetByIDs(ctx context.Context, ids []string) ([]Category, error) + GetByName(ctx context.Context, name string) (*Category, error) + Search(ctx context.Context, form *SearchForm, pagination *response.Pagination) ([]Category, int64, error) + Delete(ctx context.Context, id string) error + TogglePublish(ctx context.Context, id string) (bool, error) +} + +// categoryRepository implements the Repository interface using the MongoDB ORM +type categoryRepository struct { + ormRepo orm.Repository[Category] + logger logger.Logger +} + +// NewCategoryRepository creates a new category repository +func NewCategoryRepository(mongoManager *orm.ConnectionManager, logger logger.Logger) Repository { + // Create indexes using the ORM's index management + indexes := []orm.Index{ + *orm.NewIndex("name_idx", bson.D{{Key: "name", Value: 1}}), + *orm.NewIndex("published_idx", bson.D{{Key: "published", Value: 1}}), + } + + // Create indexes + err := mongoManager.CreateIndexes("company_categories", indexes) + if err != nil { + logger.Warn("Failed to create category indexes", map[string]interface{}{ + "error": err.Error(), + }) + } + + // Create ORM repository + ormRepo := orm.NewRepository[Category](mongoManager.GetCollection("company_categories"), logger) + + return &categoryRepository{ + ormRepo: ormRepo, + logger: logger, + } +} + +// Create creates a new category +func (r *categoryRepository) Create(ctx context.Context, category *Category) error { + // Set created/updated timestamps using Unix timestamps + now := time.Now().Unix() + category.SetCreatedAt(now) + + // Use ORM to create category + err := r.ormRepo.Create(ctx, category) + if err != nil { + r.logger.Error("Failed to create category", map[string]interface{}{ + "error": err.Error(), + "name": category.Name, + }) + return err + } + + r.logger.Info("Category created successfully", map[string]interface{}{ + "category_id": category.ID, + "name": category.Name, + "published": category.Published, + }) + + return nil +} + +// GetByID retrieves a category by ID +func (r *categoryRepository) GetByID(ctx context.Context, id string) (*Category, error) { + category, err := r.ormRepo.FindByID(ctx, id) + if err != nil { + if errors.Is(err, orm.ErrDocumentNotFound) { + return nil, errors.New("category not found") + } + r.logger.Error("Failed to get category by ID", map[string]interface{}{ + "error": err.Error(), + "category_id": id, + }) + return nil, err + } + + return category, nil +} + +// GetByIDs retrieves categories by IDs +func (r *categoryRepository) GetByIDs(ctx context.Context, ids []string) ([]Category, error) { + filter := bson.M{"_id": bson.M{"$in": ids}} + + paginationBuilder := orm.NewPaginationBuilder(). + Build() + + result, err := r.ormRepo.FindAll(ctx, filter, paginationBuilder) + if err != nil { + return nil, err + } + + return result.Items, nil +} + +// GetByName retrieves a category by name +func (r *categoryRepository) GetByName(ctx context.Context, name string) (*Category, error) { + category, err := r.ormRepo.FindOne(ctx, bson.M{"name": name}) + if err != nil { + return nil, err + } + return category, nil +} + +// Update updates a category +func (r *categoryRepository) Update(ctx context.Context, category *Category) error { + // Set updated timestamp using Unix timestamp + category.SetUpdatedAt(time.Now().Unix()) + + // Use ORM to update category + err := r.ormRepo.Update(ctx, category) + if err != nil { + r.logger.Error("Failed to update category", map[string]interface{}{ + "error": err.Error(), + "category_id": category.ID, + }) + return err + } + + r.logger.Info("Category updated successfully", map[string]interface{}{ + "category_id": category.ID, + "name": category.Name, + }) + + return nil +} + +// Delete deletes a category +func (r *categoryRepository) Delete(ctx context.Context, id string) error { + // Get category first + _, err := r.GetByID(ctx, id) + if err != nil { + return err + } + + // Delete from database + err = r.ormRepo.Delete(ctx, id) + if err != nil { + r.logger.Error("Failed to delete category", map[string]interface{}{ + "error": err.Error(), + "category_id": id, + }) + return err + } + + r.logger.Info("Category deleted successfully", map[string]interface{}{ + "category_id": id, + }) + + return nil +} + +// Search retrieves categories with search and filters +func (r *categoryRepository) Search(ctx context.Context, form *SearchForm, pagination *response.Pagination) ([]Category, int64, error) { + // Build filter + filter := bson.M{} + + if form.Search != nil && *form.Search != "" { + // Use regex search for case-insensitive partial matching + searchRegex := bson.M{ + "$regex": *form.Search, + "$options": "i", // case-insensitive + } + filter["$or"] = []bson.M{ + {"name": searchRegex}, + {"description": searchRegex}, + } + } + + if form.Published != nil { + filter["published"] = *form.Published + } + + // Build sort + sort := "created_at" + if pagination.SortBy != "" { + sort = pagination.SortBy + } + + sortOrder := "desc" + if pagination.SortOrder != "" { + sortOrder = pagination.SortOrder + } + + // Build pagination + paginationBuilder := orm.NewPaginationBuilder(). + Limit(pagination.Limit). + Skip(pagination.Offset). + SortBy(sort, sortOrder). + Build() + + // Use ORM to find categories + result, err := r.ormRepo.FindAll(ctx, filter, paginationBuilder) + if err != nil { + r.logger.Error("Failed to search categories", map[string]interface{}{ + "error": err.Error(), + }) + return nil, 0, err + } + + return result.Items, result.TotalCount, nil +} + +// TogglePublish toggles category publish status +func (r *categoryRepository) TogglePublish(ctx context.Context, id string) (bool, error) { + // Get category first + category, err := r.GetByID(ctx, id) + if err != nil { + return false, err + } + + // Update publish status + category.Published = !category.Published + now := time.Now().Unix() + category.SetUpdatedAt(now) + + // Set published_at timestamp if publishing + category.PublishedAt = &now + + // Update in database + err = r.ormRepo.Update(ctx, category) + if err != nil { + r.logger.Error("Failed to toggle category publish status", map[string]interface{}{ + "error": err.Error(), + "category_id": id, + "published": category.Published, + }) + return false, err + } + + r.logger.Info("Category publish status updated successfully", map[string]interface{}{ + "category_id": id, + "published": category.Published, + }) + + return category.Published, nil +} diff --git a/internal/company_category/service.go b/internal/company_category/service.go new file mode 100644 index 0000000..26523ea --- /dev/null +++ b/internal/company_category/service.go @@ -0,0 +1,224 @@ +package company_category + +import ( + "context" + "errors" + + "tm/pkg/logger" + "tm/pkg/response" +) + +// Service defines business logic for category operations +type Service interface { + // Core category operations + Create(ctx context.Context, form *CategoryForm) (*CategoryResponse, error) + Update(ctx context.Context, id string, form *CategoryForm) (*CategoryResponse, error) + GetByID(ctx context.Context, id string) (*CategoryResponse, error) + GetByIDs(ctx context.Context, ids []string) ([]*CategoryResponse, error) + Delete(ctx context.Context, id string) error + + // Category listing and search + SearchCategories(ctx context.Context, form *SearchForm, pagination *response.Pagination) (*CategoryListResponse, error) + + // Category management + TogglePublish(ctx context.Context, id string) (bool, error) +} + +// categoryService implements the Service interface +type categoryService struct { + repository Repository + logger logger.Logger +} + +// NewCategoryService creates a new category service +func NewCategoryService(repository Repository, logger logger.Logger) Service { + return &categoryService{ + repository: repository, + logger: logger, + } +} + +// Create creates a new category +func (s *categoryService) Create(ctx context.Context, form *CategoryForm) (*CategoryResponse, error) { + // Check if category name already exists + existingCategory, _ := s.repository.GetByName(ctx, form.Name) + if existingCategory != nil { + return nil, errors.New("category with this name already exists") + } + + // Create category + category := &Category{ + Name: form.Name, + Description: form.Description, + Thumbnail: form.Thumbnail, + Published: false, // Default to unpublished + } + + // Save to database + err := s.repository.Create(ctx, category) + if err != nil { + s.logger.Error("Failed to create category", map[string]interface{}{ + "error": err.Error(), + "name": form.Name, + }) + return nil, err + } + + s.logger.Info("Category created successfully", map[string]interface{}{ + "category_id": category.ID, + "name": category.Name, + "published": category.Published, + }) + + return category.ToResponse(), nil +} + +// GetByIDs retrieves categories by IDs +func (s *categoryService) GetByIDs(ctx context.Context, ids []string) ([]*CategoryResponse, error) { + categories, err := s.repository.GetByIDs(ctx, ids) + if err != nil { + return nil, err + } + + var categoryResponses []*CategoryResponse + for _, category := range categories { + categoryResponses = append(categoryResponses, category.ToResponse()) + } + + return categoryResponses, nil +} + +// GetByID retrieves a category by ID +func (s *categoryService) GetByID(ctx context.Context, id string) (*CategoryResponse, error) { + category, err := s.repository.GetByID(ctx, id) + if err != nil { + s.logger.Error("Failed to get category by ID", map[string]interface{}{ + "error": err.Error(), + "category_id": id, + }) + return nil, err + } + + s.logger.Info("Category retrieved successfully", map[string]interface{}{ + "category_id": category.ID, + "name": category.Name, + }) + + return category.ToResponse(), nil +} + +// Update updates a category +func (s *categoryService) Update(ctx context.Context, id string, form *CategoryForm) (*CategoryResponse, error) { + // Get existing category + category, err := s.repository.GetByID(ctx, id) + if err != nil { + s.logger.Error("Failed to get category for update", map[string]interface{}{ + "error": err.Error(), + "category_id": id, + }) + return nil, errors.New("category not found") + } + + // Check if name already exists (if changing name) + if form.Name != "" && form.Name != category.Name { + existingCategory, _ := s.repository.GetByName(ctx, form.Name) + if existingCategory != nil { + return nil, errors.New("category with this name already exists") + } + category.Name = form.Name + } + + // Update fields if provided + if form.Description != nil { + category.Description = form.Description + } + + if form.Thumbnail != nil { + category.Thumbnail = form.Thumbnail + } + + // Update in database + err = s.repository.Update(ctx, category) + if err != nil { + s.logger.Error("Failed to update category", map[string]interface{}{ + "error": err.Error(), + "category_id": id, + }) + return nil, errors.New("failed to update category") + } + + s.logger.Info("Category updated successfully", map[string]interface{}{ + "category_id": category.ID, + "name": category.Name, + }) + + return category.ToResponse(), nil +} + +// Delete deletes a category +func (s *categoryService) Delete(ctx context.Context, id string) error { + // Delete category + err := s.repository.Delete(ctx, id) + if err != nil { + s.logger.Error("Failed to delete category", map[string]interface{}{ + "error": err.Error(), + "category_id": id, + }) + return errors.New("failed to delete category") + } + + s.logger.Info("Category deleted successfully", map[string]interface{}{ + "category_id": id, + }) + + return nil +} + +// SearchCategories searches categories with filters +func (s *categoryService) SearchCategories(ctx context.Context, form *SearchForm, pagination *response.Pagination) (*CategoryListResponse, error) { + // Get categories using search + categories, total, err := s.repository.Search(ctx, form, pagination) + if err != nil { + s.logger.Error("Failed to search categories", map[string]interface{}{ + "error": err.Error(), + }) + return nil, errors.New("failed to search categories") + } + + // Convert to responses + var categoryResponses []*CategoryResponse + for _, category := range categories { + categoryResponses = append(categoryResponses, category.ToResponse()) + } + + return &CategoryListResponse{ + Categories: categoryResponses, + Meta: pagination.Response(total), + }, nil +} + +// TogglePublish toggles category publish status +func (s *categoryService) TogglePublish(ctx context.Context, id string) (bool, error) { + // Get category to check if exists + _, err := s.repository.GetByID(ctx, id) + if err != nil { + return false, errors.New("category not found") + } + + // Toggle publish status + published, err := s.repository.TogglePublish(ctx, id) + if err != nil { + s.logger.Error("Failed to toggle category publish status", map[string]interface{}{ + "error": err.Error(), + "category_id": id, + }) + return false, errors.New("failed to toggle category publish status") + } + + s.logger.Info("Category publish status updated successfully", map[string]interface{}{ + "category_id": id, + "published": published, + }) + + return published, nil +}