Merge pull request 'filters' (#26) from filters into main
Reviewed-on: #26
This commit was merged in pull request #26.
This commit is contained in:
1
.env
1
.env
@@ -57,3 +57,4 @@ FILE_MAAL_PL_USER=git_operator
|
||||
FILE_MAAL_PL_PASSWORD=1FnwqcEgIUjQHjt1
|
||||
|
||||
IMAGE_PREFIX=https://www.naluconcept.com # remove prefix to serv them from same host as presta
|
||||
CORS_ORGIN=https://www.naluconcept.com
|
||||
59
.env_example
Normal file
59
.env_example
Normal file
@@ -0,0 +1,59 @@
|
||||
SERVER_PORT=3000
|
||||
SERVER_HOST=0.0.0.0
|
||||
|
||||
# Database Configuration
|
||||
DB_HOST=localhost
|
||||
DB_PORT=3306
|
||||
DB_USER=root
|
||||
DB_PASSWORD=Maal12345678
|
||||
DB_NAME=nalu
|
||||
PROJECT_NAME=nalu_b2b
|
||||
DB_SERVICE_NAME=nalu_b2b
|
||||
DB_SSLMODE=disable
|
||||
|
||||
# App COnfig
|
||||
APP_NAME="B2b Management System"
|
||||
APP_VERSION=2.1.0
|
||||
APP_ENVIRONMENT=development
|
||||
|
||||
# JWT Configuration
|
||||
AUTH_JWT_SECRET=5c020e6ed3d8d6e67e5804d67c83c4bd5ae474df749af6d63d8f20e7e2ba29b3
|
||||
AUTH_JWT_EXPIRATION=86400
|
||||
AUTH_REFRESH_EXPIRATION=604800
|
||||
|
||||
# Meili search
|
||||
MEILISEARCH_URL=http://localhost:7700
|
||||
MEILISEARCH_API_KEY=e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
|
||||
|
||||
# OpenAI
|
||||
OPENAI_KEY=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
|
||||
|
||||
# Google Translate Client
|
||||
GOOGLE_APPLICATION_CREDENTIALS=./google-cred.json
|
||||
GOOGLE_CLOUD_PROJECT_ID=translation-343517
|
||||
|
||||
# Google OAuth2
|
||||
OAUTH_GOOGLE_CLIENT_ID=331979954218-9vrpe08oqhhcgj6bvu6d4lds0dt630m9.apps.googleusercontent.com
|
||||
OAUTH_GOOGLE_CLIENT_SECRET=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
|
||||
OAUTH_GOOGLE_REDIRECT_URL=http://localhost:3000/api/v1/public/auth/google/callback
|
||||
|
||||
# Email Configuration (SMTP)
|
||||
# Set EMAIL_ENABLED=true to require email verification
|
||||
EMAIL_ENABLED=true
|
||||
EMAIL_SMTP_HOST=mail.ma-al.com
|
||||
EMAIL_SMTP_PORT=587
|
||||
EMAIL_SMTP_USER=test@ma-al.com
|
||||
EMAIL_SMTP_PASSWORD=maal12345678
|
||||
EMAIL_FROM=test@ma-al.com
|
||||
EMAIL_FROM_NAME=Gitea Manager
|
||||
EMAIL_ADMIN=goc_marek@ma-al.pl
|
||||
|
||||
I18N_LANGS=en,pl,cs
|
||||
|
||||
PDF_SERVER_URL=http://localhost:8000
|
||||
|
||||
|
||||
FILE_MAAL_PL_USER=git_operator
|
||||
FILE_MAAL_PL_PASSWORD=1FnwqcEgIUjQHjt1
|
||||
|
||||
IMAGE_PREFIX=https://www.naluconcept.com # remove prefix to serv them from same host as presta
|
||||
@@ -2,7 +2,7 @@
|
||||
"openapi": "3.0.3",
|
||||
"info": {
|
||||
"title": "b2b API",
|
||||
"description": "Authentication, user management, and repository time tracking API",
|
||||
"description": "Authentication, user management, and repository time tracking API\n\n## Filter Operators\nAll filter parameters support the following operators by adding a suffix to the parameter name:\n\n| Suffix | Operator | Example |\n|--------|----------|---------|\n| `_eq` | equals | `product_id_eq=12` |\n| `_neq` | not equals | `active_neq=0` |\n| `_gt` | greater than | `price_gt=100` |\n| `_gte` | greater than or equal | `price_gte=10` |\n| `_lt` | less than | `price_lt=500` |\n| `_lte` | less than or equal | `price_lte=500` |\n| `_in` | IN (comma-separated) | `product_id_in=1,2,3,4` |\n\n## Special Filters\n\n| Parameter | Example | Description |\n|-----------|---------|-------------|\n| `name=~text` | `name=~gold` | LIKE search (case-insensitive partial match) |\n| `price=[min,max]` | `price=[100,500]` | BETWEEN range (inclusive) |\n| `sort=field,direction` | `sort=price,desc` | ORDER BY clause (direction: asc/desc, default: desc) |\n| `p` | `p=1` | Page number (1-based, default: 1) |\n| `elems` | `elems=30` | Elements per page (max: 100, default: 30) |",
|
||||
"version": "1.0.0",
|
||||
"contact": {
|
||||
"name": "API Support",
|
||||
@@ -11,8 +11,8 @@
|
||||
},
|
||||
"servers": [
|
||||
{
|
||||
"url": "http://localhost:3000",
|
||||
"description": "Development server"
|
||||
"url": "/",
|
||||
"description": "Development server on same host"
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
@@ -48,17 +48,13 @@
|
||||
"name": "Locale",
|
||||
"description": "Locale selection endpoints (under /api/v1/restricted/langs-and-countries, requires authentication)"
|
||||
},
|
||||
{
|
||||
"name": "Repo",
|
||||
"description": "Repository time tracking data endpoints (under /api/v1/restricted/repo, requires authentication)"
|
||||
},
|
||||
{
|
||||
"name": "Admin",
|
||||
"description": "Admin-only endpoints"
|
||||
},
|
||||
{
|
||||
"name": "Settings",
|
||||
"description": "Application settings and configuration endpoints"
|
||||
},
|
||||
{
|
||||
"name": "Carts",
|
||||
"description": "Shopping cart management endpoints (under /api/v1/restricted/carts, requires authentication)"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
@@ -629,6 +625,41 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/public/auth/update-choice": {
|
||||
"post": {
|
||||
"tags": ["Auth"],
|
||||
"summary": "Update JWT token choice",
|
||||
"description": "Updates the user's JWT token preference or refreshes the token. Requires authentication.",
|
||||
"operationId": "updateChoice",
|
||||
"security": [
|
||||
{
|
||||
"CookieAuth": []
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Token choice updated successfully",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ApiResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Not authenticated",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/public/auth/google": {
|
||||
"get": {
|
||||
"tags": ["Auth"],
|
||||
@@ -733,292 +764,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/restricted/repo/get-repos": {
|
||||
"get": {
|
||||
"tags": ["Repo"],
|
||||
"summary": "Get accessible repositories",
|
||||
"description": "Returns a list of repository IDs that the authenticated user has access to.",
|
||||
"operationId": "getRepos",
|
||||
"security": [
|
||||
{
|
||||
"CookieAuth": []
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "List of repository IDs",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "integer",
|
||||
"format": "uint"
|
||||
},
|
||||
"example": [1, 2, 5]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Invalid user session",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Not authenticated",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/restricted/repo/get-years": {
|
||||
"get": {
|
||||
"tags": ["Repo"],
|
||||
"summary": "Get available years for a repository",
|
||||
"description": "Returns a list of years for which tracked time data exists in the given repository. User must have access to the repository.",
|
||||
"operationId": "getYears",
|
||||
"security": [
|
||||
{
|
||||
"CookieAuth": []
|
||||
}
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "repoID",
|
||||
"in": "query",
|
||||
"description": "Repository ID",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"format": "uint"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "List of years with tracked time data",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "integer",
|
||||
"format": "uint"
|
||||
},
|
||||
"example": [2023, 2024, 2025]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Invalid repoID parameter or user does not have access to the repository",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Not authenticated",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/restricted/repo/get-quarters": {
|
||||
"get": {
|
||||
"tags": ["Repo"],
|
||||
"summary": "Get quarterly time data for a repository",
|
||||
"description": "Returns time tracked per quarter for the given repository and year. All 4 quarters are returned; quarters with no data have time=0. User must have access to the repository.",
|
||||
"operationId": "getQuarters",
|
||||
"security": [
|
||||
{
|
||||
"CookieAuth": []
|
||||
}
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "repoID",
|
||||
"in": "query",
|
||||
"description": "Repository ID",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"format": "uint"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "year",
|
||||
"in": "query",
|
||||
"description": "Year to retrieve quarterly data for",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"format": "uint",
|
||||
"example": 2024
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Quarterly time data",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/QuarterData"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Invalid repoID or year parameter, or user does not have access to the repository",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Not authenticated",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/restricted/repo/get-issues": {
|
||||
"get": {
|
||||
"tags": ["Repo"],
|
||||
"summary": "Get issues with time summaries",
|
||||
"description": "Returns a paginated list of issues with time tracking summaries for the given repository, year, and quarter. User must have access to the repository.",
|
||||
"operationId": "getIssues",
|
||||
"security": [
|
||||
{
|
||||
"CookieAuth": []
|
||||
}
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "repoID",
|
||||
"in": "query",
|
||||
"description": "Repository ID",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"format": "uint"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "year",
|
||||
"in": "query",
|
||||
"description": "Year to filter issues by",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"format": "uint",
|
||||
"example": 2024
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "quarter",
|
||||
"in": "query",
|
||||
"description": "Quarter number (1-4) to filter issues by",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"format": "uint",
|
||||
"minimum": 1,
|
||||
"maximum": 4,
|
||||
"example": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "page_number",
|
||||
"in": "query",
|
||||
"description": "Page number for pagination (1-based)",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"format": "uint",
|
||||
"example": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "elements_per_page",
|
||||
"in": "query",
|
||||
"description": "Number of items per page",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"format": "uint",
|
||||
"example": 30
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Paginated list of issues with time summaries",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/PaginatedIssues"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Invalid parameters or user does not have access to the repository",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Not authenticated",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/settings": {
|
||||
"get": {
|
||||
"tags": ["Settings"],
|
||||
@@ -1043,7 +788,7 @@
|
||||
"get": {
|
||||
"tags": ["Products"],
|
||||
"summary": "Get product listing",
|
||||
"description": "Returns a paginated list of products with their basic information. Requires authentication.",
|
||||
"description": "Returns a paginated list of products with their basic information. Supports filtering via query parameters with operators (e.g., product_id_eq=12, name=~wałek). Use sort parameter for ordering. Requires authentication.",
|
||||
"operationId": "getProductListing",
|
||||
"security": [
|
||||
{
|
||||
@@ -1064,12 +809,76 @@
|
||||
{
|
||||
"name": "elems",
|
||||
"in": "query",
|
||||
"description": "Number of items per page",
|
||||
"description": "Number of items per page (max: 100, default: 30)",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"default": 30
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "sort",
|
||||
"in": "query",
|
||||
"description": "Sort field and direction. Format: field,direction (e.g., 'product_id,desc' or 'name,asc')",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"example": "product_id,desc"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "product_id",
|
||||
"in": "query",
|
||||
"description": "Filter by product ID. Use suffix _eq, _gt, _gte, _lt, _lte, _neq, _in for operators.",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"in": "query",
|
||||
"description": "Filter by product name using LIKE (case-insensitive). Use ~ prefix for partial match (e.g., '~wałek')",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "reference",
|
||||
"in": "query",
|
||||
"description": "Filter by product reference using LIKE (case-insensitive)",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "id_category",
|
||||
"in": "query",
|
||||
"description": "Filter by category ID. Use suffix _eq, _gt, _gte, _lt, _lte, _neq, _in for operators.",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "category_name",
|
||||
"in": "query",
|
||||
"description": "Filter by category name using LIKE (case-insensitive)",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "quantity",
|
||||
"in": "query",
|
||||
"description": "Filter by quantity. Use suffix _eq, _gt, _gte, _lt, _lte for operators.",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
@@ -1702,6 +1511,280 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/restricted/carts/add-new-cart": {
|
||||
"get": {
|
||||
"tags": ["Carts"],
|
||||
"summary": "Create a new cart",
|
||||
"description": "Creates a new shopping cart for the authenticated user. Requires authentication.",
|
||||
"operationId": "addNewCart",
|
||||
"security": [
|
||||
{
|
||||
"CookieAuth": []
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Cart created successfully",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ApiResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Not authenticated",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/restricted/carts/change-cart-name": {
|
||||
"get": {
|
||||
"tags": ["Carts"],
|
||||
"summary": "Change cart name",
|
||||
"description": "Updates the name of an existing cart. Requires authentication.",
|
||||
"operationId": "changeCartName",
|
||||
"security": [
|
||||
{
|
||||
"CookieAuth": []
|
||||
}
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "cart_id",
|
||||
"in": "query",
|
||||
"description": "ID of the cart to rename",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "new_name",
|
||||
"in": "query",
|
||||
"description": "New name for the cart",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Cart name updated successfully",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ApiResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Invalid request parameters",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Not authenticated",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/restricted/carts/retrieve-carts-info": {
|
||||
"get": {
|
||||
"tags": ["Carts"],
|
||||
"summary": "Retrieve all carts info",
|
||||
"description": "Returns information about all carts belonging to the authenticated user. Requires authentication.",
|
||||
"operationId": "retrieveCartsInfo",
|
||||
"security": [
|
||||
{
|
||||
"CookieAuth": []
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Carts info retrieved successfully",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ApiResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Not authenticated",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/restricted/carts/retrieve-cart": {
|
||||
"get": {
|
||||
"tags": ["Carts"],
|
||||
"summary": "Retrieve cart details",
|
||||
"description": "Returns detailed contents of a specific cart. Requires authentication.",
|
||||
"operationId": "retrieveCart",
|
||||
"security": [
|
||||
{
|
||||
"CookieAuth": []
|
||||
}
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "cart_id",
|
||||
"in": "query",
|
||||
"description": "ID of the cart to retrieve",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Cart retrieved successfully",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ApiResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Invalid request parameters",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Not authenticated",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/restricted/carts/add-product-to-cart": {
|
||||
"get": {
|
||||
"tags": ["Carts"],
|
||||
"summary": "Add product to cart",
|
||||
"description": "Adds a product to the specified cart. Requires authentication.",
|
||||
"operationId": "addProductToCart",
|
||||
"security": [
|
||||
{
|
||||
"CookieAuth": []
|
||||
}
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "cart_id",
|
||||
"in": "query",
|
||||
"description": "ID of the cart to add product to",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "product_id",
|
||||
"in": "query",
|
||||
"description": "ID of the product to add",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "product_attribute_id",
|
||||
"in": "query",
|
||||
"description": "ID of the product attribute (optional, for product variants)",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "amount",
|
||||
"in": "query",
|
||||
"description": "Quantity to add",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Product added to cart successfully",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ApiResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Invalid request parameters",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Not authenticated",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
|
||||
@@ -23,6 +23,7 @@ type Config struct {
|
||||
Pdf PdfPrinter
|
||||
GoogleTranslate GoogleTranslateConfig
|
||||
Image ImageConfig
|
||||
Cors CorsConfig
|
||||
}
|
||||
|
||||
type I18n struct {
|
||||
@@ -33,6 +34,10 @@ type ServerConfig struct {
|
||||
Host string `env:"SERVER_HOST,0.0.0.0"`
|
||||
}
|
||||
|
||||
type CorsConfig struct {
|
||||
Origins []string `env:"CORS_ORGIN"`
|
||||
}
|
||||
|
||||
type ImageConfig struct {
|
||||
ImagePrefix string `env:"IMAGE_PREFIX"`
|
||||
}
|
||||
@@ -176,6 +181,11 @@ func load() *Config {
|
||||
if err != nil {
|
||||
slog.Error("not possible to load env variables for google translate : ", err.Error(), "")
|
||||
}
|
||||
|
||||
err = loadEnv(&cfg.Cors)
|
||||
if err != nil {
|
||||
slog.Error("not possible to load env variables for google translate : ", err.Error(), "")
|
||||
}
|
||||
return cfg
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,23 @@
|
||||
package middleware
|
||||
|
||||
import "github.com/gofiber/fiber/v3"
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"git.ma-al.com/goc_daniel/b2b/app/config"
|
||||
"github.com/gofiber/fiber/v3"
|
||||
)
|
||||
|
||||
// CORSMiddleware creates CORS middleware
|
||||
func CORSMiddleware() fiber.Handler {
|
||||
return func(c fiber.Ctx) error {
|
||||
c.Set("Access-Control-Allow-Origin", "*")
|
||||
|
||||
if strings.Contains(c.Get("Host"), "localhost") {
|
||||
c.Set("Access-Control-Allow-Origin", c.Get("Host"))
|
||||
} else {
|
||||
origins := strings.Join(config.Get().Cors.Origins, ",")
|
||||
c.Set("Access-Control-Allow-Origin", origins)
|
||||
}
|
||||
|
||||
c.Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
|
||||
c.Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
|
||||
|
||||
|
||||
@@ -2,11 +2,10 @@ package restricted
|
||||
|
||||
import (
|
||||
"git.ma-al.com/goc_daniel/b2b/app/config"
|
||||
"git.ma-al.com/goc_daniel/b2b/app/model"
|
||||
"git.ma-al.com/goc_daniel/b2b/app/service/listProductsService"
|
||||
"git.ma-al.com/goc_daniel/b2b/app/utils/i18n"
|
||||
"git.ma-al.com/goc_daniel/b2b/app/utils/nullable"
|
||||
"git.ma-al.com/goc_daniel/b2b/app/utils/query/filters"
|
||||
"git.ma-al.com/goc_daniel/b2b/app/utils/query/find"
|
||||
"git.ma-al.com/goc_daniel/b2b/app/utils/query/query_params"
|
||||
"git.ma-al.com/goc_daniel/b2b/app/utils/response"
|
||||
"git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors"
|
||||
@@ -37,7 +36,7 @@ func ListProductsHandlerRoutes(r fiber.Router) fiber.Router {
|
||||
}
|
||||
|
||||
func (h *ListProductsHandler) GetListing(c fiber.Ctx) error {
|
||||
paging, filters, err := ParseProductFilters(c)
|
||||
paging, filters, err := query_params.ParseFilters[model.Product](c, columnMappingGetListing)
|
||||
if err != nil {
|
||||
return c.Status(responseErrors.GetErrorStatus(err)).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
||||
@@ -58,14 +57,11 @@ func (h *ListProductsHandler) GetListing(c fiber.Ctx) error {
|
||||
return c.JSON(response.Make(&listing.Items, int(listing.Count), i18n.T_(c, response.Message_OK)))
|
||||
}
|
||||
|
||||
var columnMapping map[string]string = map[string]string{}
|
||||
|
||||
func ParseProductFilters(c fiber.Ctx) (find.Paging, *filters.FiltersList, error) {
|
||||
var p find.Paging
|
||||
fl := filters.NewFiltersList()
|
||||
|
||||
pageNum, pageElems := query_params.ParsePagination(c)
|
||||
p = find.Paging{Page: pageNum, Elements: pageElems}
|
||||
|
||||
return p, &fl, nil
|
||||
var columnMappingGetListing map[string]string = map[string]string{
|
||||
"product_id": "ps.id_product",
|
||||
"name": "pl.name",
|
||||
"reference": "ps.reference",
|
||||
"category_name": "cl.name",
|
||||
"id_category": "cp.id_category",
|
||||
"quantity": "sa.quantity",
|
||||
}
|
||||
|
||||
@@ -47,6 +47,11 @@ var swaggerHTML = `
|
||||
url: "/openapi.json",
|
||||
dom_id: '#swagger-ui',
|
||||
deepLinking: true,
|
||||
withCredentials: true,
|
||||
"servers": [
|
||||
{ "url": "http://localhost:3000" },
|
||||
{ "url": "http://localhost:5173" }
|
||||
],
|
||||
presets: [
|
||||
SwaggerUIBundle.presets.apis,
|
||||
SwaggerUIStandalonePreset
|
||||
|
||||
@@ -9,3 +9,16 @@ type Country struct {
|
||||
CurrencyISOCode string `gorm:"column:iso_code" json:"currency_iso_code"`
|
||||
CurrencyName string `gorm:"column:name" json:"currency_name"`
|
||||
}
|
||||
|
||||
func (Country) TableName() string {
|
||||
return "b2b_countries"
|
||||
}
|
||||
|
||||
type PSCountry struct {
|
||||
ID uint `gorm:"primaryKey;column:id_country" json:"id"`
|
||||
CurrencyID uint `gorm:"column:id_currency" json:"currency_id"`
|
||||
}
|
||||
|
||||
func (PSCountry) TableName() string {
|
||||
return "ps_country"
|
||||
}
|
||||
|
||||
@@ -62,11 +62,14 @@ type Product struct {
|
||||
DeliveryDays uint `gorm:"column:delivery_days" json:"delivery_days" form:"delivery_days"`
|
||||
}
|
||||
type ProductInList struct {
|
||||
ProductID uint `gorm:"column:product_id;primaryKey" json:"product_id" form:"product_id"`
|
||||
ProductID uint `gorm:"column:product_id" json:"product_id" form:"product_id"`
|
||||
Name string `gorm:"column:name" json:"name" form:"name"`
|
||||
ImageID string `gorm:"column:id_image"`
|
||||
LinkRewrite string `gorm:"column:link_rewrite"`
|
||||
Active uint `gorm:"column:active" json:"active" form:"active"`
|
||||
LinkRewrite string `gorm:"column:link_rewrite" json:"link_rewrite"`
|
||||
ImageLink string `gorm:"column:image_link" json:"image_link"`
|
||||
CategoryName string `gorm:"column:category_name" json:"category_name" form:"category_name"`
|
||||
Reference string `gorm:"column:reference" json:"reference"`
|
||||
VariantsNumber uint `gorm:"column:variants_number" json:"variants_number"`
|
||||
Quantity int64 `gorm:"column:quantity" json:"quantity"`
|
||||
}
|
||||
|
||||
type ProductFilters struct {
|
||||
|
||||
@@ -30,12 +30,22 @@ func (repo *CategoriesRepo) GetAllCategories(id_lang uint) ([]model.ScannedCateg
|
||||
ps_category.is_root_category AS is_root_category,
|
||||
ps_category_lang.link_rewrite AS link_rewrite,
|
||||
ps_lang.iso_code AS iso_code
|
||||
FROM ps_category
|
||||
LEFT JOIN ps_category_lang ON ps_category_lang.id_category = ps_category.id_category AND ps_category_lang.id_shop = ? AND ps_category_lang.id_lang = ?
|
||||
LEFT JOIN ps_category_shop ON ps_category_shop.id_category = ps_category.id_category AND ps_category_shop.id_shop = ?
|
||||
JOIN ps_lang ON ps_lang.id_lang = ps_category_lang.id_lang
|
||||
`,
|
||||
constdata.SHOP_ID, id_lang, constdata.SHOP_ID).
|
||||
`).
|
||||
Joins(`
|
||||
LEFT JOIN ps_category_lang
|
||||
ON ps_category_lang.id_category = ps_category.id_category
|
||||
AND ps_category_lang.id_shop = ?
|
||||
AND ps_category_lang.id_lang = ?
|
||||
`, constdata.SHOP_ID, id_lang).
|
||||
Joins(`
|
||||
LEFT JOIN ps_category_shop
|
||||
ON ps_category_shop.id_category = ps_category.id_category
|
||||
AND ps_category_shop.id_shop = ?
|
||||
`, constdata.SHOP_ID).
|
||||
Joins(`
|
||||
JOIN ps_lang
|
||||
ON ps_lang.id_lang = ps_category_lang.id_lang
|
||||
`).
|
||||
Scan(&allCategories).Error
|
||||
|
||||
return allCategories, err
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
package listProductsRepo
|
||||
|
||||
import (
|
||||
"git.ma-al.com/goc_daniel/b2b/app/config"
|
||||
"git.ma-al.com/goc_daniel/b2b/app/db"
|
||||
"git.ma-al.com/goc_daniel/b2b/app/model"
|
||||
constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data"
|
||||
"git.ma-al.com/goc_daniel/b2b/app/utils/query/filters"
|
||||
"git.ma-al.com/goc_daniel/b2b/app/utils/query/find"
|
||||
)
|
||||
@@ -23,43 +21,44 @@ func (repo *ListProductsRepo) GetListing(id_lang uint, p find.Paging, filt *filt
|
||||
var listing []model.ProductInList
|
||||
var total int64
|
||||
|
||||
subQuery := db.DB.
|
||||
Table("ps_image").
|
||||
Select("id_product, MIN(id_image) AS id_image").
|
||||
Group("id_product")
|
||||
|
||||
err := db.DB.
|
||||
Table("ps_product").
|
||||
query := db.Get().
|
||||
Table("ps_product_shop AS ps").
|
||||
Select(`
|
||||
ps_product.id_product AS product_id,
|
||||
ps_product_lang.name AS name,
|
||||
ps_product.active AS active,
|
||||
ps_product_lang.link_rewrite AS link_rewrite,
|
||||
COALESCE(CONCAT( ?, '/', ps_image_shop.id_image, '-small_default/', ps_product_lang.link_rewrite, '.webp'), CONCAT( ?, '/', any_image.id_image, '-small_default/', ps_product_lang.link_rewrite, '.webp')) AS id_image
|
||||
`, config.Get().Image.ImagePrefix, config.Get().Image.ImagePrefix).
|
||||
Joins(`
|
||||
LEFT JOIN ps_product_lang
|
||||
ON ps_product_lang.id_product = ps_product.id_product
|
||||
AND ps_product_lang.id_shop = ?
|
||||
AND ps_product_lang.id_lang = ?
|
||||
`, constdata.SHOP_ID, id_lang).
|
||||
Joins(`
|
||||
LEFT JOIN ps_image_shop
|
||||
ON ps_image_shop.id_product = ps_product.id_product
|
||||
AND ps_image_shop.id_shop = ?
|
||||
AND ps_image_shop.cover = 1
|
||||
`, constdata.SHOP_ID).
|
||||
Joins("LEFT JOIN (?) AS any_image ON ps_product.id_product = any_image.id_product", subQuery).
|
||||
Limit(p.Limit()).
|
||||
Offset(p.Offset()).
|
||||
Scan(&listing).Error
|
||||
ps.id_product AS product_id,
|
||||
pl.name AS name,
|
||||
pl.link_rewrite AS link_rewrite,
|
||||
CONCAT('https://www.naluconcept.com', '/', ims.id_image, '-small_default/', pl.link_rewrite, '.webp') AS image_link,
|
||||
cl.name AS category_name,
|
||||
p.reference AS reference,
|
||||
COUNT(DISTINCT pas.id_product_attribute) AS variants_number,
|
||||
sa.quantity AS quantity
|
||||
`).
|
||||
Joins("JOIN ps_product p ON p.id_product = ps.id_product").
|
||||
Joins("JOIN ps_category_product cp ON ps.id_product = cp.id_product").
|
||||
Joins("JOIN ps_product_lang pl ON pl.id_product = ps.id_product AND pl.id_lang = ?", id_lang).
|
||||
Joins("JOIN ps_image_shop ims ON ims.id_product = ps.id_product AND ims.cover = 1").
|
||||
Joins("JOIN ps_category_lang cl ON cl.id_category = ps.id_category_default AND cl.id_lang = ?", id_lang).
|
||||
Joins("LEFT JOIN ps_product_attribute_shop pas ON pas.id_product = cp.id_product").
|
||||
Joins("LEFT JOIN ps_stock_available sa ON sa.id_product = ps.id_product").
|
||||
Where("ps.active = ?", 1).
|
||||
Group("cp.id_product")
|
||||
|
||||
// Apply all filters
|
||||
if filt != nil {
|
||||
filt.ApplyAll(query)
|
||||
}
|
||||
|
||||
// run counter first as query is without limit and offset
|
||||
err := query.Count(&total).Error
|
||||
if err != nil {
|
||||
return find.Found[model.ProductInList]{}, err
|
||||
}
|
||||
|
||||
err = db.DB.
|
||||
Table("ps_product").
|
||||
Count(&total).Error
|
||||
err = query.
|
||||
Order("ps.id_product DESC").
|
||||
Limit(p.Limit()).
|
||||
Offset(p.Offset()).
|
||||
Scan(&listing).Error
|
||||
if err != nil {
|
||||
return find.Found[model.ProductInList]{}, err
|
||||
}
|
||||
|
||||
@@ -27,9 +27,9 @@ func (repo *LocaleSelectorRepo) GetLanguages() ([]model.Language, error) {
|
||||
func (repo *LocaleSelectorRepo) GetCountriesAndCurrencies() ([]model.Country, error) {
|
||||
var countries []model.Country
|
||||
|
||||
err := db.DB.Table("b2b_countries").
|
||||
Select("b2b_countries.id, b2b_countries.name, b2b_countries.flag, ps_currency.id as id_currency, ps_currency.name as currency_name, ps_currency.iso_code as currency_iso_code").
|
||||
Joins("JOIN ps_currency ON ps_currency.id = b2b_countries.currency").
|
||||
err := db.DB.
|
||||
Select("b2b_countries.id, b2b_countries.name, b2b_countries.flag, ps_currency.id_currency as id_currency, ps_currency.name as currency_name, ps_currency.iso_code as currency_iso_code").
|
||||
Joins("JOIN ps_currency ON ps_currency.id_currency = b2b_countries.currency").
|
||||
Scan(&countries).Error
|
||||
|
||||
return countries, err
|
||||
|
||||
@@ -55,7 +55,9 @@ func WhereFromStrings(column, conditionOperator, value string) Filter {
|
||||
value = strings.ReplaceAll(value, "~", "")
|
||||
|
||||
filt = func(d *gorm.DB) *gorm.DB {
|
||||
return d.Where("lower("+column+`) LIKE lower(?)`, "%"+value+"%")
|
||||
// return d.Where("lower("+column+`) LIKE lower(?)`, "%"+value+"%")
|
||||
// (jeśli masz collation case-insensitive, np. utf8mb4_general_ci)
|
||||
return d.Where(column+` LIKE ?`, "%"+value+"%")
|
||||
|
||||
}
|
||||
|
||||
@@ -65,6 +67,33 @@ func WhereFromStrings(column, conditionOperator, value string) Filter {
|
||||
}
|
||||
}
|
||||
|
||||
// Handle IN operator for comma-separated values (e.g., product_id_in=1,2,3,4)
|
||||
if conditionOperator == "IN" {
|
||||
parts := strings.Split(value, ",")
|
||||
var values []interface{}
|
||||
for _, p := range parts {
|
||||
p = strings.TrimSpace(p)
|
||||
if p == "" {
|
||||
continue
|
||||
}
|
||||
// Try to parse as int first
|
||||
if i, err := strconv.ParseInt(p, 10, 64); err == nil {
|
||||
values = append(values, i)
|
||||
} else if f, err := strconv.ParseFloat(p, 64); err == nil {
|
||||
values = append(values, f)
|
||||
} else {
|
||||
values = append(values, p)
|
||||
}
|
||||
}
|
||||
filt = func(d *gorm.DB) *gorm.DB {
|
||||
return d.Where(column+" IN ?", values)
|
||||
}
|
||||
return Filter{
|
||||
category: WHERE_FILTER,
|
||||
filter: filt,
|
||||
}
|
||||
}
|
||||
|
||||
if strings.Contains(value, "]") && strings.Contains(value, "[") {
|
||||
period := strings.ReplaceAll(value, "[", "")
|
||||
period = strings.ReplaceAll(period, "]", "")
|
||||
|
||||
@@ -46,7 +46,7 @@ func ParseWhereScopes[T any](c fiber.Ctx, ignoredKeys []string, formColumnMappin
|
||||
}
|
||||
|
||||
func extractOperator(key string) (base string, operatorSuffix string) {
|
||||
suffixes := []string{"_gt", "_gte", "_lt", "_lte", "_eq", "_neq"}
|
||||
suffixes := []string{"_gt", "_gte", "_lt", "_lte", "_eq", "_neq", "_in"}
|
||||
for _, suf := range suffixes {
|
||||
if strings.HasSuffix(key, suf) {
|
||||
return strings.TrimSuffix(key, suf), suf[1:]
|
||||
@@ -69,6 +69,8 @@ func resolveOperator(suffix string) string {
|
||||
return "!="
|
||||
case "eq":
|
||||
return "="
|
||||
case "in":
|
||||
return "IN"
|
||||
default:
|
||||
return "LIKE"
|
||||
}
|
||||
|
||||
@@ -7,7 +7,8 @@
|
||||
<div v-if="!customerStore.hasAccount" class="flex flex-col items-center justify-center py-12">
|
||||
<div class="text-center flex flex-col items-center justify-center mb-6">
|
||||
<UIcon name="mdi:domain" class="text-[60px] text-gray-400 dark:text-gray-500" />
|
||||
<p class="mt-4 text-lg text-gray-600 dark:text-gray-400">{{ t('No customer account found') }}</p>
|
||||
<p class="mt-4 text-lg text-gray-600 dark:text-gray-400">{{ t('No customer account found') }}
|
||||
</p>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-500">{{ t('Create an account to manage your company data') }}</p>
|
||||
</div>
|
||||
<UButton color="primary" @click="goToCreateAccount"
|
||||
@@ -26,6 +27,7 @@
|
||||
class="text-[24px] text-(--accent-blue-light) dark:text-(--accent-blue-dark)" />
|
||||
{{ t('Company Information') }}
|
||||
</h2>
|
||||
<div class="grid grid-cols-1 gap-10">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-2">
|
||||
<div>
|
||||
<label
|
||||
@@ -34,38 +36,47 @@
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-500 dark:text-gray-400 mb-1">{{t('Company Email') }}</label>
|
||||
<label
|
||||
class="block text-sm font-medium text-gray-500 dark:text-gray-400 mb-1">{{
|
||||
t('Company Email') }}</label>
|
||||
<p class="text-black dark:text-white">{{ customerStore.customer?.companyEmail || '-'}}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-500 dark:text-gray-400 mb-1">{{t('REGON') }}</label>
|
||||
<p class="text-black dark:text-white">{{ customerStore.customer?.regon || '-' }}</p>
|
||||
<label
|
||||
class="block text-sm font-medium text-gray-500 dark:text-gray-400 mb-1">{{ t('REGON')}}</label>
|
||||
<p class="text-black dark:text-white">{{ customerStore.customer?.regon || '-' }}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-500 dark:text-gray-400 mb-1">{{t('NIP') }}</label>
|
||||
<p class="text-black dark:text-white">{{ customerStore.customer?.nip || '-' }}</p>
|
||||
<label
|
||||
class="block text-sm font-medium text-gray-500 dark:text-gray-400 mb-1">{{t('NIP')}}</label>
|
||||
<p class="text-black dark:text-white">{{ customerStore.customer?.nip || '-' }}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-500 dark:text-gray-400 mb-1">{{t('VAT') }}</label>
|
||||
<p class="text-black dark:text-white">{{ customerStore.customer?.vat || '-' }}</p>
|
||||
<label
|
||||
class="block text-sm font-medium text-gray-500 dark:text-gray-400 mb-1">{{t('VAT')}}</label>
|
||||
<p class="text-black dark:text-white">{{ customerStore.customer?.vat || '-' }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-(--second-light) dark:bg-(--main-dark) rounded-lg border border-(--border-light) dark:border-(--border-dark) p-4">
|
||||
<h2 class="text-xl font-semibold text-black dark:text-white mb-4 flex items-center gap-2">
|
||||
<div>
|
||||
<h2
|
||||
class="text-xl font-semibold text-black dark:text-white mb-2 flex items-center gap-2">
|
||||
<UIcon name="mdi:map-marker"
|
||||
class="text-[24px] text-(--accent-blue-light) dark:text-(--accent-blue-dark)" />
|
||||
{{ t('Addresses') }}
|
||||
</h2>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-500 dark:text-gray-400 mb-2">{{ t('Company Address') }}</label>
|
||||
<label
|
||||
class="block text-sm font-medium text-gray-500 dark:text-gray-400 mb-2">{{ t('Company Address') }}</label>
|
||||
<div v-if="companyAddress"
|
||||
class="p-4 bg-white dark:bg-(--black) rounded-md border border-(--border-light) dark:border-(--border-dark)">
|
||||
<p class="text-black dark:text-white">{{ companyAddress.street }}</p>
|
||||
<p class="text-black dark:text-white">{{ companyAddress.zipCode }}, {{
|
||||
companyAddress.city }}</p>
|
||||
<p class="text-black dark:text-white">{{ companyAddress.zipCode }},
|
||||
{{ companyAddress.city }}</p>
|
||||
<p class="text-black dark:text-white">{{ companyAddress.country }}</p>
|
||||
</div>
|
||||
<p v-else class="text-gray-400 dark:text-gray-500">-</p>
|
||||
@@ -73,6 +84,8 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-end">
|
||||
<UButton color="primary" variant="outline"
|
||||
class="text-(--accent-blue-light) dark:text-(--accent-blue-dark) border-(--accent-blue-light) dark:border-(--accent-blue-dark)"
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
<tr v-for="product in productsList" :key="product.product_id"
|
||||
class="hover:bg-gray-50 dark:hover:bg-gray-800">
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<img :src="getImageUrl(product.ImageID, product.LinkRewrite,)" alt="product image"
|
||||
<img :src="product.ImageID" alt="product image"
|
||||
class="w-16 h-16 object-cover rounded" />
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white">{{
|
||||
@@ -49,6 +49,9 @@
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="flex justify-center items-center py-8">
|
||||
<UPagination v-model:page="page" :total="total" :page-size="perPage" />
|
||||
</div>
|
||||
<div v-if="productsList.length === 0" class="text-center py-8 text-gray-500 dark:text-gray-400">
|
||||
No products found
|
||||
</div>
|
||||
@@ -59,17 +62,21 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, Suspense } from 'vue'
|
||||
import { ref, onMounted, watch } from 'vue'
|
||||
import { useFetchJson } from '@/composable/useFetchJson'
|
||||
import Default from '@/layouts/default.vue'
|
||||
// import CategoryMenu from '@/components/inner/categoryMenu.vue'
|
||||
interface Product {
|
||||
product_id: number
|
||||
name: string
|
||||
ImageID: number
|
||||
ImageID: string
|
||||
LinkRewrite: string
|
||||
}
|
||||
|
||||
|
||||
const page = ref(1)
|
||||
const perPage = ref(15)
|
||||
const total = ref(0)
|
||||
|
||||
interface ApiResponse {
|
||||
message: string
|
||||
items: Product[]
|
||||
@@ -80,21 +87,26 @@ const productsList = ref<Product[]>([])
|
||||
const loading = ref(true)
|
||||
const error = ref<string | null>(null)
|
||||
|
||||
function getImageUrl(imageID: number, linkRewrite: string, size: string = 'small_default') {
|
||||
return `https://www.naluconcept.com/${imageID}-${size}/${linkRewrite}.webp`
|
||||
}
|
||||
async function fetchProductList() {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
|
||||
try {
|
||||
const response = await useFetchJson('/api/v1/restricted/list-products/get-listing?p&elems&shopID=1') as ApiResponse
|
||||
const response = await useFetchJson(
|
||||
`api/v1/restricted/list-products/get-listing?p=${page.value}&elems=${perPage.value}`
|
||||
) as ApiResponse
|
||||
|
||||
productsList.value = response.items || []
|
||||
total.value = response.count || 0
|
||||
} catch (e: unknown) {
|
||||
error.value = e instanceof Error ? e.message : 'Failed to load products'
|
||||
console.error(e)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
watch(page, () => {
|
||||
fetchProductList()
|
||||
})
|
||||
onMounted(fetchProductList)
|
||||
</script>
|
||||
@@ -238,7 +238,7 @@ const columns = computed<TableColumn<IssueTimeSummary>[]>(() => [
|
||||
</h2>
|
||||
<UTable :data="issues" :columns="columns" class="flex-1 dark:text-white! text-dark" />
|
||||
<div class="pt-4 flex justify-center items-center dark:text-white! text-dark">
|
||||
<UPagination v-model:page="page" :total="totalItems" />
|
||||
<UPagination v-model:page="page" :total="totalItems" :page-size="10" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user