Commit 4cc0e852 by Augusto

Lists and players update

parent 83be1096
...@@ -1178,6 +1178,12 @@ const docTemplate = `{ ...@@ -1178,6 +1178,12 @@ const docTemplate = `{
"in": "query" "in": "query"
}, },
{ {
"type": "string",
"description": "Filter by internal player ID",
"name": "playerId",
"in": "query"
},
{
"type": "integer", "type": "integer",
"description": "Filter by player WyID", "description": "Filter by player WyID",
"name": "playerWyId", "name": "playerWyId",
...@@ -1888,20 +1894,20 @@ const docTemplate = `{ ...@@ -1888,20 +1894,20 @@ const docTemplate = `{
}, },
{ {
"type": "string", "type": "string",
"description": "Entity type (player, coach, etc.)", "description": "Entity type (player, coach, report, team)",
"name": "entityType", "name": "entityType",
"in": "formData", "in": "formData",
"required": true "required": true
}, },
{ {
"type": "integer", "type": "integer",
"description": "Entity ID", "description": "Entity ID (required if entityWyId not provided)",
"name": "entityId", "name": "entityId",
"in": "formData" "in": "formData"
}, },
{ {
"type": "integer", "type": "integer",
"description": "Entity WyID", "description": "Entity WyID (required if entityId not provided)",
"name": "entityWyId", "name": "entityWyId",
"in": "formData" "in": "formData"
}, },
...@@ -2258,6 +2264,102 @@ const docTemplate = `{ ...@@ -2258,6 +2264,102 @@ const docTemplate = `{
} }
} }
}, },
"/lists/by-player/{playerId}": {
"get": {
"security": [
{
"BearerAuth": []
}
],
"description": "Get paginated lists (owned or shared) that contain the given player ID",
"produces": [
"application/json"
],
"tags": [
"Lists"
],
"summary": "Get lists by player ID",
"parameters": [
{
"type": "string",
"description": "Player ID",
"name": "playerId",
"in": "path",
"required": true
},
{
"type": "string",
"description": "Filter by list type",
"name": "type",
"in": "query"
},
{
"type": "string",
"description": "Filter by name",
"name": "name",
"in": "query"
},
{
"type": "string",
"description": "Filter by season",
"name": "season",
"in": "query"
},
{
"type": "boolean",
"description": "Filter by active status",
"name": "isActive",
"in": "query"
},
{
"type": "integer",
"description": "Limit results",
"name": "limit",
"in": "query"
},
{
"type": "integer",
"description": "Offset for pagination",
"name": "offset",
"in": "query"
},
{
"type": "string",
"description": "Sort field",
"name": "sortBy",
"in": "query"
},
{
"type": "string",
"description": "Sort order (asc/desc)",
"name": "sortOrder",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/services.PaginatedListsResponse"
}
},
"400": {
"description": "Invalid query parameters",
"schema": {
"type": "object",
"additionalProperties": true
}
},
"401": {
"description": "Unauthorized",
"schema": {
"type": "object",
"additionalProperties": true
}
}
}
}
},
"/lists/shared-with-me": { "/lists/shared-with-me": {
"get": { "get": {
"security": [ "security": [
...@@ -2474,6 +2576,137 @@ const docTemplate = `{ ...@@ -2474,6 +2576,137 @@ const docTemplate = `{
} }
} }
}, },
"/lists/{id}/shares": {
"post": {
"security": [
{
"BearerAuth": []
}
],
"description": "Share a list with another user",
"consumes": [
"application/json"
],
"tags": [
"Lists"
],
"summary": "Share a list",
"parameters": [
{
"type": "string",
"description": "List ID",
"name": "id",
"in": "path",
"required": true
},
{
"description": "Share payload",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/handlers.createListShareRequest"
}
}
],
"responses": {
"204": {
"description": "No Content"
},
"400": {
"description": "Invalid request body",
"schema": {
"type": "object",
"additionalProperties": true
}
},
"401": {
"description": "Unauthorized",
"schema": {
"type": "object",
"additionalProperties": true
}
},
"403": {
"description": "Forbidden",
"schema": {
"type": "object",
"additionalProperties": true
}
},
"404": {
"description": "List not found",
"schema": {
"type": "object",
"additionalProperties": true
}
}
}
}
},
"/lists/{id}/shares/{userId}": {
"delete": {
"security": [
{
"BearerAuth": []
}
],
"description": "Unshare a list from a user",
"tags": [
"Lists"
],
"summary": "Unshare a list",
"parameters": [
{
"type": "string",
"description": "List ID",
"name": "id",
"in": "path",
"required": true
},
{
"type": "integer",
"description": "User ID",
"name": "userId",
"in": "path",
"required": true
}
],
"responses": {
"204": {
"description": "No Content"
},
"400": {
"description": "Invalid user ID",
"schema": {
"type": "object",
"additionalProperties": true
}
},
"401": {
"description": "Unauthorized",
"schema": {
"type": "object",
"additionalProperties": true
}
},
"403": {
"description": "Forbidden",
"schema": {
"type": "object",
"additionalProperties": true
}
},
"404": {
"description": "List not found",
"schema": {
"type": "object",
"additionalProperties": true
}
}
}
}
},
"/player-features/categories": { "/player-features/categories": {
"get": { "get": {
"security": [ "security": [
...@@ -3531,6 +3764,18 @@ const docTemplate = `{ ...@@ -3531,6 +3764,18 @@ const docTemplate = `{
}, },
{ {
"type": "integer", "type": "integer",
"description": "Filter by createdByUserId",
"name": "createdByUserId",
"in": "query"
},
{
"type": "integer",
"description": "Filter by updatedByUserId",
"name": "updatedByUserId",
"in": "query"
},
{
"type": "integer",
"description": "Minimum age", "description": "Minimum age",
"name": "minAge", "name": "minAge",
"in": "query" "in": "query"
...@@ -6311,24 +6556,21 @@ const docTemplate = `{ ...@@ -6311,24 +6556,21 @@ const docTemplate = `{
} }
} }
}, },
"/users": { "/teams": {
"get": { "get": {
"security": [ "security": [
{ {
"BearerAuth": [] "BearerAuth": []
} }
], ],
"description": "Get paginated list of users with optional filters", "description": "Get paginated list of teams with optional filters",
"consumes": [
"application/json"
],
"produces": [ "produces": [
"application/json" "application/json"
], ],
"tags": [ "tags": [
"Users" "Teams"
], ],
"summary": "Get all users", "summary": "Get all teams",
"parameters": [ "parameters": [
{ {
"type": "string", "type": "string",
...@@ -6338,14 +6580,26 @@ const docTemplate = `{ ...@@ -6338,14 +6580,26 @@ const docTemplate = `{
}, },
{ {
"type": "string", "type": "string",
"description": "Filter by email", "description": "Filter by type",
"name": "email", "name": "type",
"in": "query" "in": "query"
}, },
{ {
"type": "string", "type": "string",
"description": "Filter by role", "description": "Filter by category",
"name": "role", "name": "category",
"in": "query"
},
{
"type": "string",
"description": "Filter by gender",
"name": "gender",
"in": "query"
},
{
"type": "integer",
"description": "Filter by area WyID",
"name": "areaWyId",
"in": "query" "in": "query"
}, },
{ {
...@@ -6385,10 +6639,273 @@ const docTemplate = `{ ...@@ -6385,10 +6639,273 @@ const docTemplate = `{
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "schema": {
"type": "array", "$ref": "#/definitions/services.PaginatedTeamsResponse"
"items": { }
"$ref": "#/definitions/services.UserResponse" },
} "400": {
"description": "Invalid query parameters",
"schema": {
"type": "object",
"additionalProperties": true
}
}
}
},
"post": {
"security": [
{
"BearerAuth": []
}
],
"description": "Create a new team",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Teams"
],
"summary": "Create a new team",
"parameters": [
{
"description": "Team data",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/services.CreateTeamRequest"
}
}
],
"responses": {
"201": {
"description": "Created",
"schema": {
"$ref": "#/definitions/services.TeamResponse"
}
},
"400": {
"description": "Invalid request",
"schema": {
"type": "object",
"additionalProperties": true
}
}
}
}
},
"/teams/{id}": {
"get": {
"security": [
{
"BearerAuth": []
}
],
"description": "Get a single team by its ID",
"produces": [
"application/json"
],
"tags": [
"Teams"
],
"summary": "Get team by ID",
"parameters": [
{
"type": "string",
"description": "Team ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/services.TeamResponse"
}
},
"404": {
"description": "Team not found",
"schema": {
"type": "object",
"additionalProperties": true
}
}
}
},
"delete": {
"security": [
{
"BearerAuth": []
}
],
"description": "Soft delete a team",
"tags": [
"Teams"
],
"summary": "Delete a team",
"parameters": [
{
"type": "string",
"description": "Team ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"204": {
"description": "No Content"
},
"404": {
"description": "Team not found",
"schema": {
"type": "object",
"additionalProperties": true
}
}
}
},
"patch": {
"security": [
{
"BearerAuth": []
}
],
"description": "Update an existing team's information",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Teams"
],
"summary": "Update a team",
"parameters": [
{
"type": "string",
"description": "Team ID",
"name": "id",
"in": "path",
"required": true
},
{
"description": "Team data",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/services.UpdateTeamRequest"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/services.TeamResponse"
}
},
"400": {
"description": "Invalid request",
"schema": {
"type": "object",
"additionalProperties": true
}
},
"404": {
"description": "Team not found",
"schema": {
"type": "object",
"additionalProperties": true
}
}
}
}
},
"/users": {
"get": {
"security": [
{
"BearerAuth": []
}
],
"description": "Get paginated list of users with optional filters",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Users"
],
"summary": "Get all users",
"parameters": [
{
"type": "string",
"description": "Filter by name",
"name": "name",
"in": "query"
},
{
"type": "string",
"description": "Filter by email",
"name": "email",
"in": "query"
},
{
"type": "string",
"description": "Filter by role",
"name": "role",
"in": "query"
},
{
"type": "boolean",
"description": "Filter by active status",
"name": "isActive",
"in": "query"
},
{
"type": "integer",
"default": 100,
"description": "Limit results",
"name": "limit",
"in": "query"
},
{
"type": "integer",
"default": 0,
"description": "Offset for pagination",
"name": "offset",
"in": "query"
},
{
"type": "string",
"description": "Sort field",
"name": "sortBy",
"in": "query"
},
{
"type": "string",
"description": "Sort order (asc/desc)",
"name": "sortOrder",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/services.UserResponse"
}
} }
}, },
"400": { "400": {
...@@ -6675,6 +7192,17 @@ const docTemplate = `{ ...@@ -6675,6 +7192,17 @@ const docTemplate = `{
} }
}, },
"definitions": { "definitions": {
"handlers.createListShareRequest": {
"type": "object",
"required": [
"userId"
],
"properties": {
"userId": {
"type": "integer"
}
}
},
"models.ClientModule": { "models.ClientModule": {
"type": "object", "type": "object",
"properties": { "properties": {
...@@ -6923,6 +7451,9 @@ const docTemplate = `{ ...@@ -6923,6 +7451,9 @@ const docTemplate = `{
"type": "integer" "type": "integer"
}, },
"metadata": {}, "metadata": {},
"playerId": {
"type": "string"
},
"playerWyId": { "playerWyId": {
"type": "integer" "type": "integer"
}, },
...@@ -7131,7 +7662,7 @@ const docTemplate = `{ ...@@ -7131,7 +7662,7 @@ const docTemplate = `{
"type": "string" "type": "string"
}, },
"endDate": { "endDate": {
"type": "string" "$ref": "#/definitions/services.FlexibleTime"
}, },
"eventType": { "eventType": {
"type": "string" "type": "string"
...@@ -7140,11 +7671,14 @@ const docTemplate = `{ ...@@ -7140,11 +7671,14 @@ const docTemplate = `{
"type": "integer" "type": "integer"
}, },
"metadata": {}, "metadata": {},
"playerId": {
"type": "string"
},
"playerWyId": { "playerWyId": {
"type": "integer" "type": "integer"
}, },
"startDate": { "startDate": {
"type": "string" "$ref": "#/definitions/services.FlexibleTime"
}, },
"title": { "title": {
"type": "string" "type": "string"
...@@ -7391,9 +7925,12 @@ const docTemplate = `{ ...@@ -7391,9 +7925,12 @@ const docTemplate = `{
"contractUntil": { "contractUntil": {
"type": "string" "type": "string"
}, },
"countryTsId": { "countryTsID": {
"type": "string" "type": "string"
}, },
"createdByUserId": {
"type": "integer"
},
"currentNationalTeamId": { "currentNationalTeamId": {
"type": "integer" "type": "integer"
}, },
...@@ -7527,6 +8064,9 @@ const docTemplate = `{ ...@@ -7527,6 +8064,9 @@ const docTemplate = `{
"tsId": { "tsId": {
"type": "string" "type": "string"
}, },
"updatedByUserId": {
"type": "integer"
},
"valueRange": { "valueRange": {
"type": "string" "type": "string"
}, },
...@@ -7688,6 +8228,47 @@ const docTemplate = `{ ...@@ -7688,6 +8228,47 @@ const docTemplate = `{
} }
} }
}, },
"services.CreateTeamRequest": {
"type": "object",
"required": [
"name"
],
"properties": {
"areaWyId": {
"type": "integer"
},
"category": {
"type": "string"
},
"city": {
"type": "string"
},
"description": {
"type": "string"
},
"gender": {
"type": "string"
},
"imageDataURL": {
"type": "string"
},
"imageDataUrl": {
"type": "string"
},
"name": {
"type": "string"
},
"officialName": {
"type": "string"
},
"shortName": {
"type": "string"
},
"type": {
"type": "string"
}
}
},
"services.CreateUserRequest": { "services.CreateUserRequest": {
"type": "object", "type": "object",
"required": [ "required": [
...@@ -7779,6 +8360,14 @@ const docTemplate = `{ ...@@ -7779,6 +8360,14 @@ const docTemplate = `{
} }
} }
}, },
"services.FlexibleTime": {
"type": "object",
"properties": {
"time": {
"type": "string"
}
}
},
"services.GlobalSettingResponse": { "services.GlobalSettingResponse": {
"type": "object", "type": "object",
"properties": { "properties": {
...@@ -8102,6 +8691,29 @@ const docTemplate = `{ ...@@ -8102,6 +8691,29 @@ const docTemplate = `{
} }
} }
}, },
"services.PaginatedTeamsResponse": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/definitions/services.TeamResponse"
}
},
"hasMore": {
"type": "boolean"
},
"limit": {
"type": "integer"
},
"offset": {
"type": "integer"
},
"total": {
"type": "integer"
}
}
},
"services.PlayerAreaResponse": { "services.PlayerAreaResponse": {
"type": "object", "type": "object",
"properties": { "properties": {
...@@ -8415,6 +9027,9 @@ const docTemplate = `{ ...@@ -8415,6 +9027,9 @@ const docTemplate = `{
"createdAt": { "createdAt": {
"type": "string" "type": "string"
}, },
"createdByUserId": {
"type": "integer"
},
"currentNationalTeam": { "currentNationalTeam": {
"$ref": "#/definitions/services.PlayerTeamResponse" "$ref": "#/definitions/services.PlayerTeamResponse"
}, },
...@@ -8544,6 +9159,9 @@ const docTemplate = `{ ...@@ -8544,6 +9159,9 @@ const docTemplate = `{
"updatedAt": { "updatedAt": {
"type": "string" "type": "string"
}, },
"updatedByUserId": {
"type": "integer"
},
"valueRange": { "valueRange": {
"type": "string" "type": "string"
}, },
...@@ -8721,6 +9339,56 @@ const docTemplate = `{ ...@@ -8721,6 +9339,56 @@ const docTemplate = `{
} }
} }
}, },
"services.TeamResponse": {
"type": "object",
"properties": {
"areaWyId": {
"type": "integer"
},
"category": {
"type": "string"
},
"city": {
"type": "string"
},
"createdAt": {
"type": "string"
},
"deletedAt": {
"type": "string"
},
"description": {
"type": "string"
},
"gender": {
"type": "string"
},
"id": {
"type": "string"
},
"imageDataUrl": {
"type": "string"
},
"isActive": {
"type": "boolean"
},
"name": {
"type": "string"
},
"officialName": {
"type": "string"
},
"shortName": {
"type": "string"
},
"type": {
"type": "string"
},
"updatedAt": {
"type": "string"
}
}
},
"services.UpdateAgentRequest": { "services.UpdateAgentRequest": {
"type": "object", "type": "object",
"properties": { "properties": {
...@@ -8774,7 +9442,7 @@ const docTemplate = `{ ...@@ -8774,7 +9442,7 @@ const docTemplate = `{
"type": "string" "type": "string"
}, },
"endDate": { "endDate": {
"type": "string" "$ref": "#/definitions/services.FlexibleTime"
}, },
"eventType": { "eventType": {
"type": "string" "type": "string"
...@@ -8786,11 +9454,14 @@ const docTemplate = `{ ...@@ -8786,11 +9454,14 @@ const docTemplate = `{
"type": "integer" "type": "integer"
}, },
"metadata": {}, "metadata": {},
"playerId": {
"type": "string"
},
"playerWyId": { "playerWyId": {
"type": "integer" "type": "integer"
}, },
"startDate": { "startDate": {
"type": "string" "$ref": "#/definitions/services.FlexibleTime"
}, },
"title": { "title": {
"type": "string" "type": "string"
...@@ -9006,9 +9677,12 @@ const docTemplate = `{ ...@@ -9006,9 +9677,12 @@ const docTemplate = `{
"contractUntil": { "contractUntil": {
"type": "string" "type": "string"
}, },
"countryTsId": { "countryTsID": {
"type": "string" "type": "string"
}, },
"createdByUserId": {
"type": "integer"
},
"currentNationalTeamId": { "currentNationalTeamId": {
"type": "integer" "type": "integer"
}, },
...@@ -9142,6 +9816,9 @@ const docTemplate = `{ ...@@ -9142,6 +9816,9 @@ const docTemplate = `{
"tsId": { "tsId": {
"type": "string" "type": "string"
}, },
"updatedByUserId": {
"type": "integer"
},
"valueRange": { "valueRange": {
"type": "string" "type": "string"
}, },
...@@ -9276,6 +9953,47 @@ const docTemplate = `{ ...@@ -9276,6 +9953,47 @@ const docTemplate = `{
} }
} }
}, },
"services.UpdateTeamRequest": {
"type": "object",
"properties": {
"areaWyId": {
"type": "integer"
},
"category": {
"type": "string"
},
"city": {
"type": "string"
},
"description": {
"type": "string"
},
"gender": {
"type": "string"
},
"imageDataURL": {
"type": "string"
},
"imageDataUrl": {
"type": "string"
},
"isActive": {
"type": "boolean"
},
"name": {
"type": "string"
},
"officialName": {
"type": "string"
},
"shortName": {
"type": "string"
},
"type": {
"type": "string"
}
}
},
"services.UpdateUserRequest": { "services.UpdateUserRequest": {
"type": "object", "type": "object",
"properties": { "properties": {
......
...@@ -1172,6 +1172,12 @@ ...@@ -1172,6 +1172,12 @@
"in": "query" "in": "query"
}, },
{ {
"type": "string",
"description": "Filter by internal player ID",
"name": "playerId",
"in": "query"
},
{
"type": "integer", "type": "integer",
"description": "Filter by player WyID", "description": "Filter by player WyID",
"name": "playerWyId", "name": "playerWyId",
...@@ -1882,20 +1888,20 @@ ...@@ -1882,20 +1888,20 @@
}, },
{ {
"type": "string", "type": "string",
"description": "Entity type (player, coach, etc.)", "description": "Entity type (player, coach, report, team)",
"name": "entityType", "name": "entityType",
"in": "formData", "in": "formData",
"required": true "required": true
}, },
{ {
"type": "integer", "type": "integer",
"description": "Entity ID", "description": "Entity ID (required if entityWyId not provided)",
"name": "entityId", "name": "entityId",
"in": "formData" "in": "formData"
}, },
{ {
"type": "integer", "type": "integer",
"description": "Entity WyID", "description": "Entity WyID (required if entityId not provided)",
"name": "entityWyId", "name": "entityWyId",
"in": "formData" "in": "formData"
}, },
...@@ -2252,6 +2258,102 @@ ...@@ -2252,6 +2258,102 @@
} }
} }
}, },
"/lists/by-player/{playerId}": {
"get": {
"security": [
{
"BearerAuth": []
}
],
"description": "Get paginated lists (owned or shared) that contain the given player ID",
"produces": [
"application/json"
],
"tags": [
"Lists"
],
"summary": "Get lists by player ID",
"parameters": [
{
"type": "string",
"description": "Player ID",
"name": "playerId",
"in": "path",
"required": true
},
{
"type": "string",
"description": "Filter by list type",
"name": "type",
"in": "query"
},
{
"type": "string",
"description": "Filter by name",
"name": "name",
"in": "query"
},
{
"type": "string",
"description": "Filter by season",
"name": "season",
"in": "query"
},
{
"type": "boolean",
"description": "Filter by active status",
"name": "isActive",
"in": "query"
},
{
"type": "integer",
"description": "Limit results",
"name": "limit",
"in": "query"
},
{
"type": "integer",
"description": "Offset for pagination",
"name": "offset",
"in": "query"
},
{
"type": "string",
"description": "Sort field",
"name": "sortBy",
"in": "query"
},
{
"type": "string",
"description": "Sort order (asc/desc)",
"name": "sortOrder",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/services.PaginatedListsResponse"
}
},
"400": {
"description": "Invalid query parameters",
"schema": {
"type": "object",
"additionalProperties": true
}
},
"401": {
"description": "Unauthorized",
"schema": {
"type": "object",
"additionalProperties": true
}
}
}
}
},
"/lists/shared-with-me": { "/lists/shared-with-me": {
"get": { "get": {
"security": [ "security": [
...@@ -2468,6 +2570,137 @@ ...@@ -2468,6 +2570,137 @@
} }
} }
}, },
"/lists/{id}/shares": {
"post": {
"security": [
{
"BearerAuth": []
}
],
"description": "Share a list with another user",
"consumes": [
"application/json"
],
"tags": [
"Lists"
],
"summary": "Share a list",
"parameters": [
{
"type": "string",
"description": "List ID",
"name": "id",
"in": "path",
"required": true
},
{
"description": "Share payload",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/handlers.createListShareRequest"
}
}
],
"responses": {
"204": {
"description": "No Content"
},
"400": {
"description": "Invalid request body",
"schema": {
"type": "object",
"additionalProperties": true
}
},
"401": {
"description": "Unauthorized",
"schema": {
"type": "object",
"additionalProperties": true
}
},
"403": {
"description": "Forbidden",
"schema": {
"type": "object",
"additionalProperties": true
}
},
"404": {
"description": "List not found",
"schema": {
"type": "object",
"additionalProperties": true
}
}
}
}
},
"/lists/{id}/shares/{userId}": {
"delete": {
"security": [
{
"BearerAuth": []
}
],
"description": "Unshare a list from a user",
"tags": [
"Lists"
],
"summary": "Unshare a list",
"parameters": [
{
"type": "string",
"description": "List ID",
"name": "id",
"in": "path",
"required": true
},
{
"type": "integer",
"description": "User ID",
"name": "userId",
"in": "path",
"required": true
}
],
"responses": {
"204": {
"description": "No Content"
},
"400": {
"description": "Invalid user ID",
"schema": {
"type": "object",
"additionalProperties": true
}
},
"401": {
"description": "Unauthorized",
"schema": {
"type": "object",
"additionalProperties": true
}
},
"403": {
"description": "Forbidden",
"schema": {
"type": "object",
"additionalProperties": true
}
},
"404": {
"description": "List not found",
"schema": {
"type": "object",
"additionalProperties": true
}
}
}
}
},
"/player-features/categories": { "/player-features/categories": {
"get": { "get": {
"security": [ "security": [
...@@ -3525,6 +3758,18 @@ ...@@ -3525,6 +3758,18 @@
}, },
{ {
"type": "integer", "type": "integer",
"description": "Filter by createdByUserId",
"name": "createdByUserId",
"in": "query"
},
{
"type": "integer",
"description": "Filter by updatedByUserId",
"name": "updatedByUserId",
"in": "query"
},
{
"type": "integer",
"description": "Minimum age", "description": "Minimum age",
"name": "minAge", "name": "minAge",
"in": "query" "in": "query"
...@@ -6305,24 +6550,21 @@ ...@@ -6305,24 +6550,21 @@
} }
} }
}, },
"/users": { "/teams": {
"get": { "get": {
"security": [ "security": [
{ {
"BearerAuth": [] "BearerAuth": []
} }
], ],
"description": "Get paginated list of users with optional filters", "description": "Get paginated list of teams with optional filters",
"consumes": [
"application/json"
],
"produces": [ "produces": [
"application/json" "application/json"
], ],
"tags": [ "tags": [
"Users" "Teams"
], ],
"summary": "Get all users", "summary": "Get all teams",
"parameters": [ "parameters": [
{ {
"type": "string", "type": "string",
...@@ -6332,14 +6574,26 @@ ...@@ -6332,14 +6574,26 @@
}, },
{ {
"type": "string", "type": "string",
"description": "Filter by email", "description": "Filter by type",
"name": "email", "name": "type",
"in": "query" "in": "query"
}, },
{ {
"type": "string", "type": "string",
"description": "Filter by role", "description": "Filter by category",
"name": "role", "name": "category",
"in": "query"
},
{
"type": "string",
"description": "Filter by gender",
"name": "gender",
"in": "query"
},
{
"type": "integer",
"description": "Filter by area WyID",
"name": "areaWyId",
"in": "query" "in": "query"
}, },
{ {
...@@ -6379,10 +6633,273 @@ ...@@ -6379,10 +6633,273 @@
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "schema": {
"type": "array", "$ref": "#/definitions/services.PaginatedTeamsResponse"
"items": { }
"$ref": "#/definitions/services.UserResponse" },
} "400": {
"description": "Invalid query parameters",
"schema": {
"type": "object",
"additionalProperties": true
}
}
}
},
"post": {
"security": [
{
"BearerAuth": []
}
],
"description": "Create a new team",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Teams"
],
"summary": "Create a new team",
"parameters": [
{
"description": "Team data",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/services.CreateTeamRequest"
}
}
],
"responses": {
"201": {
"description": "Created",
"schema": {
"$ref": "#/definitions/services.TeamResponse"
}
},
"400": {
"description": "Invalid request",
"schema": {
"type": "object",
"additionalProperties": true
}
}
}
}
},
"/teams/{id}": {
"get": {
"security": [
{
"BearerAuth": []
}
],
"description": "Get a single team by its ID",
"produces": [
"application/json"
],
"tags": [
"Teams"
],
"summary": "Get team by ID",
"parameters": [
{
"type": "string",
"description": "Team ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/services.TeamResponse"
}
},
"404": {
"description": "Team not found",
"schema": {
"type": "object",
"additionalProperties": true
}
}
}
},
"delete": {
"security": [
{
"BearerAuth": []
}
],
"description": "Soft delete a team",
"tags": [
"Teams"
],
"summary": "Delete a team",
"parameters": [
{
"type": "string",
"description": "Team ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"204": {
"description": "No Content"
},
"404": {
"description": "Team not found",
"schema": {
"type": "object",
"additionalProperties": true
}
}
}
},
"patch": {
"security": [
{
"BearerAuth": []
}
],
"description": "Update an existing team's information",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Teams"
],
"summary": "Update a team",
"parameters": [
{
"type": "string",
"description": "Team ID",
"name": "id",
"in": "path",
"required": true
},
{
"description": "Team data",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/services.UpdateTeamRequest"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/services.TeamResponse"
}
},
"400": {
"description": "Invalid request",
"schema": {
"type": "object",
"additionalProperties": true
}
},
"404": {
"description": "Team not found",
"schema": {
"type": "object",
"additionalProperties": true
}
}
}
}
},
"/users": {
"get": {
"security": [
{
"BearerAuth": []
}
],
"description": "Get paginated list of users with optional filters",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Users"
],
"summary": "Get all users",
"parameters": [
{
"type": "string",
"description": "Filter by name",
"name": "name",
"in": "query"
},
{
"type": "string",
"description": "Filter by email",
"name": "email",
"in": "query"
},
{
"type": "string",
"description": "Filter by role",
"name": "role",
"in": "query"
},
{
"type": "boolean",
"description": "Filter by active status",
"name": "isActive",
"in": "query"
},
{
"type": "integer",
"default": 100,
"description": "Limit results",
"name": "limit",
"in": "query"
},
{
"type": "integer",
"default": 0,
"description": "Offset for pagination",
"name": "offset",
"in": "query"
},
{
"type": "string",
"description": "Sort field",
"name": "sortBy",
"in": "query"
},
{
"type": "string",
"description": "Sort order (asc/desc)",
"name": "sortOrder",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/services.UserResponse"
}
} }
}, },
"400": { "400": {
...@@ -6669,6 +7186,17 @@ ...@@ -6669,6 +7186,17 @@
} }
}, },
"definitions": { "definitions": {
"handlers.createListShareRequest": {
"type": "object",
"required": [
"userId"
],
"properties": {
"userId": {
"type": "integer"
}
}
},
"models.ClientModule": { "models.ClientModule": {
"type": "object", "type": "object",
"properties": { "properties": {
...@@ -6917,6 +7445,9 @@ ...@@ -6917,6 +7445,9 @@
"type": "integer" "type": "integer"
}, },
"metadata": {}, "metadata": {},
"playerId": {
"type": "string"
},
"playerWyId": { "playerWyId": {
"type": "integer" "type": "integer"
}, },
...@@ -7125,7 +7656,7 @@ ...@@ -7125,7 +7656,7 @@
"type": "string" "type": "string"
}, },
"endDate": { "endDate": {
"type": "string" "$ref": "#/definitions/services.FlexibleTime"
}, },
"eventType": { "eventType": {
"type": "string" "type": "string"
...@@ -7134,11 +7665,14 @@ ...@@ -7134,11 +7665,14 @@
"type": "integer" "type": "integer"
}, },
"metadata": {}, "metadata": {},
"playerId": {
"type": "string"
},
"playerWyId": { "playerWyId": {
"type": "integer" "type": "integer"
}, },
"startDate": { "startDate": {
"type": "string" "$ref": "#/definitions/services.FlexibleTime"
}, },
"title": { "title": {
"type": "string" "type": "string"
...@@ -7385,9 +7919,12 @@ ...@@ -7385,9 +7919,12 @@
"contractUntil": { "contractUntil": {
"type": "string" "type": "string"
}, },
"countryTsId": { "countryTsID": {
"type": "string" "type": "string"
}, },
"createdByUserId": {
"type": "integer"
},
"currentNationalTeamId": { "currentNationalTeamId": {
"type": "integer" "type": "integer"
}, },
...@@ -7521,6 +8058,9 @@ ...@@ -7521,6 +8058,9 @@
"tsId": { "tsId": {
"type": "string" "type": "string"
}, },
"updatedByUserId": {
"type": "integer"
},
"valueRange": { "valueRange": {
"type": "string" "type": "string"
}, },
...@@ -7682,6 +8222,47 @@ ...@@ -7682,6 +8222,47 @@
} }
} }
}, },
"services.CreateTeamRequest": {
"type": "object",
"required": [
"name"
],
"properties": {
"areaWyId": {
"type": "integer"
},
"category": {
"type": "string"
},
"city": {
"type": "string"
},
"description": {
"type": "string"
},
"gender": {
"type": "string"
},
"imageDataURL": {
"type": "string"
},
"imageDataUrl": {
"type": "string"
},
"name": {
"type": "string"
},
"officialName": {
"type": "string"
},
"shortName": {
"type": "string"
},
"type": {
"type": "string"
}
}
},
"services.CreateUserRequest": { "services.CreateUserRequest": {
"type": "object", "type": "object",
"required": [ "required": [
...@@ -7773,6 +8354,14 @@ ...@@ -7773,6 +8354,14 @@
} }
} }
}, },
"services.FlexibleTime": {
"type": "object",
"properties": {
"time": {
"type": "string"
}
}
},
"services.GlobalSettingResponse": { "services.GlobalSettingResponse": {
"type": "object", "type": "object",
"properties": { "properties": {
...@@ -8096,6 +8685,29 @@ ...@@ -8096,6 +8685,29 @@
} }
} }
}, },
"services.PaginatedTeamsResponse": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/definitions/services.TeamResponse"
}
},
"hasMore": {
"type": "boolean"
},
"limit": {
"type": "integer"
},
"offset": {
"type": "integer"
},
"total": {
"type": "integer"
}
}
},
"services.PlayerAreaResponse": { "services.PlayerAreaResponse": {
"type": "object", "type": "object",
"properties": { "properties": {
...@@ -8409,6 +9021,9 @@ ...@@ -8409,6 +9021,9 @@
"createdAt": { "createdAt": {
"type": "string" "type": "string"
}, },
"createdByUserId": {
"type": "integer"
},
"currentNationalTeam": { "currentNationalTeam": {
"$ref": "#/definitions/services.PlayerTeamResponse" "$ref": "#/definitions/services.PlayerTeamResponse"
}, },
...@@ -8538,6 +9153,9 @@ ...@@ -8538,6 +9153,9 @@
"updatedAt": { "updatedAt": {
"type": "string" "type": "string"
}, },
"updatedByUserId": {
"type": "integer"
},
"valueRange": { "valueRange": {
"type": "string" "type": "string"
}, },
...@@ -8715,6 +9333,56 @@ ...@@ -8715,6 +9333,56 @@
} }
} }
}, },
"services.TeamResponse": {
"type": "object",
"properties": {
"areaWyId": {
"type": "integer"
},
"category": {
"type": "string"
},
"city": {
"type": "string"
},
"createdAt": {
"type": "string"
},
"deletedAt": {
"type": "string"
},
"description": {
"type": "string"
},
"gender": {
"type": "string"
},
"id": {
"type": "string"
},
"imageDataUrl": {
"type": "string"
},
"isActive": {
"type": "boolean"
},
"name": {
"type": "string"
},
"officialName": {
"type": "string"
},
"shortName": {
"type": "string"
},
"type": {
"type": "string"
},
"updatedAt": {
"type": "string"
}
}
},
"services.UpdateAgentRequest": { "services.UpdateAgentRequest": {
"type": "object", "type": "object",
"properties": { "properties": {
...@@ -8768,7 +9436,7 @@ ...@@ -8768,7 +9436,7 @@
"type": "string" "type": "string"
}, },
"endDate": { "endDate": {
"type": "string" "$ref": "#/definitions/services.FlexibleTime"
}, },
"eventType": { "eventType": {
"type": "string" "type": "string"
...@@ -8780,11 +9448,14 @@ ...@@ -8780,11 +9448,14 @@
"type": "integer" "type": "integer"
}, },
"metadata": {}, "metadata": {},
"playerId": {
"type": "string"
},
"playerWyId": { "playerWyId": {
"type": "integer" "type": "integer"
}, },
"startDate": { "startDate": {
"type": "string" "$ref": "#/definitions/services.FlexibleTime"
}, },
"title": { "title": {
"type": "string" "type": "string"
...@@ -9000,9 +9671,12 @@ ...@@ -9000,9 +9671,12 @@
"contractUntil": { "contractUntil": {
"type": "string" "type": "string"
}, },
"countryTsId": { "countryTsID": {
"type": "string" "type": "string"
}, },
"createdByUserId": {
"type": "integer"
},
"currentNationalTeamId": { "currentNationalTeamId": {
"type": "integer" "type": "integer"
}, },
...@@ -9136,6 +9810,9 @@ ...@@ -9136,6 +9810,9 @@
"tsId": { "tsId": {
"type": "string" "type": "string"
}, },
"updatedByUserId": {
"type": "integer"
},
"valueRange": { "valueRange": {
"type": "string" "type": "string"
}, },
...@@ -9270,6 +9947,47 @@ ...@@ -9270,6 +9947,47 @@
} }
} }
}, },
"services.UpdateTeamRequest": {
"type": "object",
"properties": {
"areaWyId": {
"type": "integer"
},
"category": {
"type": "string"
},
"city": {
"type": "string"
},
"description": {
"type": "string"
},
"gender": {
"type": "string"
},
"imageDataURL": {
"type": "string"
},
"imageDataUrl": {
"type": "string"
},
"isActive": {
"type": "boolean"
},
"name": {
"type": "string"
},
"officialName": {
"type": "string"
},
"shortName": {
"type": "string"
},
"type": {
"type": "string"
}
}
},
"services.UpdateUserRequest": { "services.UpdateUserRequest": {
"type": "object", "type": "object",
"properties": { "properties": {
......
basePath: /api basePath: /api
definitions: definitions:
handlers.createListShareRequest:
properties:
userId:
type: integer
required:
- userId
type: object
models.ClientModule: models.ClientModule:
properties: properties:
clientId: clientId:
...@@ -163,6 +170,8 @@ definitions: ...@@ -163,6 +170,8 @@ definitions:
matchWyId: matchWyId:
type: integer type: integer
metadata: {} metadata: {}
playerId:
type: string
playerWyId: playerWyId:
type: integer type: integer
startDate: startDate:
...@@ -299,16 +308,18 @@ definitions: ...@@ -299,16 +308,18 @@ definitions:
description: description:
type: string type: string
endDate: endDate:
type: string $ref: '#/definitions/services.FlexibleTime'
eventType: eventType:
type: string type: string
matchWyId: matchWyId:
type: integer type: integer
metadata: {} metadata: {}
playerId:
type: string
playerWyId: playerWyId:
type: integer type: integer
startDate: startDate:
type: string $ref: '#/definitions/services.FlexibleTime'
title: title:
type: string type: string
required: required:
...@@ -475,8 +486,10 @@ definitions: ...@@ -475,8 +486,10 @@ definitions:
type: string type: string
contractUntil: contractUntil:
type: string type: string
countryTsId: countryTsID:
type: string type: string
createdByUserId:
type: integer
currentNationalTeamId: currentNationalTeamId:
type: integer type: integer
currentNationalTeamName: currentNationalTeamName:
...@@ -567,6 +580,8 @@ definitions: ...@@ -567,6 +580,8 @@ definitions:
type: number type: number
tsId: tsId:
type: string type: string
updatedByUserId:
type: integer
valueRange: valueRange:
type: string type: string
weight: weight:
...@@ -676,6 +691,33 @@ definitions: ...@@ -676,6 +691,33 @@ definitions:
- name - name
- type - type
type: object type: object
services.CreateTeamRequest:
properties:
areaWyId:
type: integer
category:
type: string
city:
type: string
description:
type: string
gender:
type: string
imageDataURL:
type: string
imageDataUrl:
type: string
name:
type: string
officialName:
type: string
shortName:
type: string
type:
type: string
required:
- name
type: object
services.CreateUserRequest: services.CreateUserRequest:
properties: properties:
email: email:
...@@ -738,6 +780,11 @@ definitions: ...@@ -738,6 +780,11 @@ definitions:
uploadedBy: uploadedBy:
type: integer type: integer
type: object type: object
services.FlexibleTime:
properties:
time:
type: string
type: object
services.GlobalSettingResponse: services.GlobalSettingResponse:
properties: properties:
category: category:
...@@ -950,6 +997,21 @@ definitions: ...@@ -950,6 +997,21 @@ definitions:
total: total:
type: integer type: integer
type: object type: object
services.PaginatedTeamsResponse:
properties:
data:
items:
$ref: '#/definitions/services.TeamResponse'
type: array
hasMore:
type: boolean
limit:
type: integer
offset:
type: integer
total:
type: integer
type: object
services.PlayerAreaResponse: services.PlayerAreaResponse:
properties: properties:
alpha2code: alpha2code:
...@@ -1155,6 +1217,8 @@ definitions: ...@@ -1155,6 +1217,8 @@ definitions:
type: string type: string
createdAt: createdAt:
type: string type: string
createdByUserId:
type: integer
currentNationalTeam: currentNationalTeam:
$ref: '#/definitions/services.PlayerTeamResponse' $ref: '#/definitions/services.PlayerTeamResponse'
currentTeam: currentTeam:
...@@ -1241,6 +1305,8 @@ definitions: ...@@ -1241,6 +1305,8 @@ definitions:
type: string type: string
updatedAt: updatedAt:
type: string type: string
updatedByUserId:
type: integer
valueRange: valueRange:
type: string type: string
weight: weight:
...@@ -1357,6 +1423,39 @@ definitions: ...@@ -1357,6 +1423,39 @@ definitions:
secret: secret:
type: string type: string
type: object type: object
services.TeamResponse:
properties:
areaWyId:
type: integer
category:
type: string
city:
type: string
createdAt:
type: string
deletedAt:
type: string
description:
type: string
gender:
type: string
id:
type: string
imageDataUrl:
type: string
isActive:
type: boolean
name:
type: string
officialName:
type: string
shortName:
type: string
type:
type: string
updatedAt:
type: string
type: object
services.UpdateAgentRequest: services.UpdateAgentRequest:
properties: properties:
address: address:
...@@ -1392,7 +1491,7 @@ definitions: ...@@ -1392,7 +1491,7 @@ definitions:
description: description:
type: string type: string
endDate: endDate:
type: string $ref: '#/definitions/services.FlexibleTime'
eventType: eventType:
type: string type: string
isActive: isActive:
...@@ -1400,10 +1499,12 @@ definitions: ...@@ -1400,10 +1499,12 @@ definitions:
matchWyId: matchWyId:
type: integer type: integer
metadata: {} metadata: {}
playerId:
type: string
playerWyId: playerWyId:
type: integer type: integer
startDate: startDate:
type: string $ref: '#/definitions/services.FlexibleTime'
title: title:
type: string type: string
type: object type: object
...@@ -1545,8 +1646,10 @@ definitions: ...@@ -1545,8 +1646,10 @@ definitions:
type: string type: string
contractUntil: contractUntil:
type: string type: string
countryTsId: countryTsID:
type: string type: string
createdByUserId:
type: integer
currentNationalTeamId: currentNationalTeamId:
type: integer type: integer
currentNationalTeamName: currentNationalTeamName:
...@@ -1637,6 +1740,8 @@ definitions: ...@@ -1637,6 +1740,8 @@ definitions:
type: number type: number
tsId: tsId:
type: string type: string
updatedByUserId:
type: integer
valueRange: valueRange:
type: string type: string
weight: weight:
...@@ -1725,6 +1830,33 @@ definitions: ...@@ -1725,6 +1830,33 @@ definitions:
type: type:
type: string type: string
type: object type: object
services.UpdateTeamRequest:
properties:
areaWyId:
type: integer
category:
type: string
city:
type: string
description:
type: string
gender:
type: string
imageDataURL:
type: string
imageDataUrl:
type: string
isActive:
type: boolean
name:
type: string
officialName:
type: string
shortName:
type: string
type:
type: string
type: object
services.UpdateUserRequest: services.UpdateUserRequest:
properties: properties:
email: email:
...@@ -2544,6 +2676,10 @@ paths: ...@@ -2544,6 +2676,10 @@ paths:
in: query in: query
name: matchWyId name: matchWyId
type: integer type: integer
- description: Filter by internal player ID
in: query
name: playerId
type: string
- description: Filter by player WyID - description: Filter by player WyID
in: query in: query
name: playerWyId name: playerWyId
...@@ -3003,16 +3139,16 @@ paths: ...@@ -3003,16 +3139,16 @@ paths:
name: file name: file
required: true required: true
type: file type: file
- description: Entity type (player, coach, etc.) - description: Entity type (player, coach, report, team)
in: formData in: formData
name: entityType name: entityType
required: true required: true
type: string type: string
- description: Entity ID - description: Entity ID (required if entityWyId not provided)
in: formData in: formData
name: entityId name: entityId
type: integer type: integer
- description: Entity WyID - description: Entity WyID (required if entityId not provided)
in: formData in: formData
name: entityWyId name: entityWyId
type: integer type: integer
...@@ -3335,6 +3471,157 @@ paths: ...@@ -3335,6 +3471,157 @@ paths:
summary: Update a list summary: Update a list
tags: tags:
- Lists - Lists
/lists/{id}/shares:
post:
consumes:
- application/json
description: Share a list with another user
parameters:
- description: List ID
in: path
name: id
required: true
type: string
- description: Share payload
in: body
name: request
required: true
schema:
$ref: '#/definitions/handlers.createListShareRequest'
responses:
"204":
description: No Content
"400":
description: Invalid request body
schema:
additionalProperties: true
type: object
"401":
description: Unauthorized
schema:
additionalProperties: true
type: object
"403":
description: Forbidden
schema:
additionalProperties: true
type: object
"404":
description: List not found
schema:
additionalProperties: true
type: object
security:
- BearerAuth: []
summary: Share a list
tags:
- Lists
/lists/{id}/shares/{userId}:
delete:
description: Unshare a list from a user
parameters:
- description: List ID
in: path
name: id
required: true
type: string
- description: User ID
in: path
name: userId
required: true
type: integer
responses:
"204":
description: No Content
"400":
description: Invalid user ID
schema:
additionalProperties: true
type: object
"401":
description: Unauthorized
schema:
additionalProperties: true
type: object
"403":
description: Forbidden
schema:
additionalProperties: true
type: object
"404":
description: List not found
schema:
additionalProperties: true
type: object
security:
- BearerAuth: []
summary: Unshare a list
tags:
- Lists
/lists/by-player/{playerId}:
get:
description: Get paginated lists (owned or shared) that contain the given player
ID
parameters:
- description: Player ID
in: path
name: playerId
required: true
type: string
- description: Filter by list type
in: query
name: type
type: string
- description: Filter by name
in: query
name: name
type: string
- description: Filter by season
in: query
name: season
type: string
- description: Filter by active status
in: query
name: isActive
type: boolean
- description: Limit results
in: query
name: limit
type: integer
- description: Offset for pagination
in: query
name: offset
type: integer
- description: Sort field
in: query
name: sortBy
type: string
- description: Sort order (asc/desc)
in: query
name: sortOrder
type: string
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/services.PaginatedListsResponse'
"400":
description: Invalid query parameters
schema:
additionalProperties: true
type: object
"401":
description: Unauthorized
schema:
additionalProperties: true
type: object
security:
- BearerAuth: []
summary: Get lists by player ID
tags:
- Lists
/lists/shared-with-me: /lists/shared-with-me:
get: get:
description: Get paginated list of player lists that have been shared with the description: Get paginated list of player lists that have been shared with the
...@@ -4042,6 +4329,14 @@ paths: ...@@ -4042,6 +4329,14 @@ paths:
in: query in: query
name: positions name: positions
type: string type: string
- description: Filter by createdByUserId
in: query
name: createdByUserId
type: integer
- description: Filter by updatedByUserId
in: query
name: updatedByUserId
type: integer
- description: Minimum age - description: Minimum age
in: query in: query
name: minAge name: minAge
...@@ -5810,6 +6105,182 @@ paths: ...@@ -5810,6 +6105,182 @@ paths:
summary: Unlock user account summary: Unlock user account
tags: tags:
- SuperAdmin - SuperAdmin
/teams:
get:
description: Get paginated list of teams with optional filters
parameters:
- description: Filter by name
in: query
name: name
type: string
- description: Filter by type
in: query
name: type
type: string
- description: Filter by category
in: query
name: category
type: string
- description: Filter by gender
in: query
name: gender
type: string
- description: Filter by area WyID
in: query
name: areaWyId
type: integer
- description: Filter by active status
in: query
name: isActive
type: boolean
- default: 100
description: Limit results
in: query
name: limit
type: integer
- default: 0
description: Offset for pagination
in: query
name: offset
type: integer
- description: Sort field
in: query
name: sortBy
type: string
- description: Sort order (asc/desc)
in: query
name: sortOrder
type: string
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/services.PaginatedTeamsResponse'
"400":
description: Invalid query parameters
schema:
additionalProperties: true
type: object
security:
- BearerAuth: []
summary: Get all teams
tags:
- Teams
post:
consumes:
- application/json
description: Create a new team
parameters:
- description: Team data
in: body
name: request
required: true
schema:
$ref: '#/definitions/services.CreateTeamRequest'
produces:
- application/json
responses:
"201":
description: Created
schema:
$ref: '#/definitions/services.TeamResponse'
"400":
description: Invalid request
schema:
additionalProperties: true
type: object
security:
- BearerAuth: []
summary: Create a new team
tags:
- Teams
/teams/{id}:
delete:
description: Soft delete a team
parameters:
- description: Team ID
in: path
name: id
required: true
type: string
responses:
"204":
description: No Content
"404":
description: Team not found
schema:
additionalProperties: true
type: object
security:
- BearerAuth: []
summary: Delete a team
tags:
- Teams
get:
description: Get a single team by its ID
parameters:
- description: Team ID
in: path
name: id
required: true
type: string
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/services.TeamResponse'
"404":
description: Team not found
schema:
additionalProperties: true
type: object
security:
- BearerAuth: []
summary: Get team by ID
tags:
- Teams
patch:
consumes:
- application/json
description: Update an existing team's information
parameters:
- description: Team ID
in: path
name: id
required: true
type: string
- description: Team data
in: body
name: request
required: true
schema:
$ref: '#/definitions/services.UpdateTeamRequest'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/services.TeamResponse'
"400":
description: Invalid request
schema:
additionalProperties: true
type: object
"404":
description: Team not found
schema:
additionalProperties: true
type: object
security:
- BearerAuth: []
summary: Update a team
tags:
- Teams
/users: /users:
get: get:
consumes: consumes:
......
package config package config
import "os" import (
"os"
"strings"
)
type Config struct { type Config struct {
Port string Port string
...@@ -16,6 +19,7 @@ type Config struct { ...@@ -16,6 +19,7 @@ type Config struct {
ProviderUser string ProviderUser string
ProviderSecret string ProviderSecret string
UploadDir string UploadDir string
DisableAutoMigrate bool
} }
func Load() Config { func Load() Config {
...@@ -33,6 +37,7 @@ func Load() Config { ...@@ -33,6 +37,7 @@ func Load() Config {
ProviderUser: os.Getenv("ProviderUser"), ProviderUser: os.Getenv("ProviderUser"),
ProviderSecret: os.Getenv("ProviderSecret"), ProviderSecret: os.Getenv("ProviderSecret"),
UploadDir: envOrDefault("UPLOAD_DIR", "./uploads"), UploadDir: envOrDefault("UPLOAD_DIR", "./uploads"),
DisableAutoMigrate: envBool("DISABLE_AUTOMIGRATE"),
} }
} }
...@@ -42,3 +47,12 @@ func envOrDefault(key, def string) string { ...@@ -42,3 +47,12 @@ func envOrDefault(key, def string) string {
} }
return def return def
} }
func envBool(key string) bool {
v := os.Getenv(key)
if v == "" {
return false
}
v = strings.ToLower(v)
return v == "1" || v == "true" || v == "yes" || v == "y" || v == "on"
}
...@@ -106,6 +106,9 @@ func Connect(cfg config.Config) (*gorm.DB, error) { ...@@ -106,6 +106,9 @@ func Connect(cfg config.Config) (*gorm.DB, error) {
} }
registerNanoIDHook(db) registerNanoIDHook(db)
if cfg.DisableAutoMigrate {
return db, nil
}
// Ensure enum types exist before AutoMigrate creates columns that reference them. // Ensure enum types exist before AutoMigrate creates columns that reference them.
if err := ensureEnumType(db, "foot", []string{"left", "right", "both"}); err != nil { if err := ensureEnumType(db, "foot", []string{"left", "right", "both"}); err != nil {
...@@ -147,6 +150,7 @@ func Connect(cfg config.Config) (*gorm.DB, error) { ...@@ -147,6 +150,7 @@ func Connect(cfg config.Config) (*gorm.DB, error) {
&models.UserSession{}, &models.UserSession{},
&models.Area{}, &models.Area{},
&models.Position{}, &models.Position{},
&models.Team{},
&models.Player{}, &models.Player{},
&models.Coach{}, &models.Coach{},
&models.Agent{}, &models.Agent{},
......
...@@ -2,10 +2,12 @@ package handlers ...@@ -2,10 +2,12 @@ package handlers
import ( import (
"net/http" "net/http"
"strconv"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"gorm.io/gorm" "gorm.io/gorm"
"ScoutSystemElite/internal/models"
"ScoutSystemElite/internal/services" "ScoutSystemElite/internal/services"
) )
...@@ -13,6 +15,10 @@ type ListHandler struct { ...@@ -13,6 +15,10 @@ type ListHandler struct {
service *services.ListService service *services.ListService
} }
type createListShareRequest struct {
UserID uint `json:"userId" binding:"required"`
}
func NewListHandler(db *gorm.DB) *ListHandler { func NewListHandler(db *gorm.DB) *ListHandler {
return &ListHandler{ return &ListHandler{
service: services.NewListService(db), service: services.NewListService(db),
...@@ -151,6 +157,69 @@ func (h *ListHandler) FindAll(c *gin.Context) { ...@@ -151,6 +157,69 @@ func (h *ListHandler) FindAll(c *gin.Context) {
c.JSON(http.StatusOK, result) c.JSON(http.StatusOK, result)
} }
// FindByPlayerID godoc
// @Summary Get lists by player ID
// @Description Get paginated lists (owned or shared) that contain the given player ID
// @Tags Lists
// @Security BearerAuth
// @Produce json
// @Param playerId path string true "Player ID"
// @Param type query string false "Filter by list type"
// @Param name query string false "Filter by name"
// @Param season query string false "Filter by season"
// @Param isActive query boolean false "Filter by active status"
// @Param limit query int false "Limit results"
// @Param offset query int false "Offset for pagination"
// @Param sortBy query string false "Sort field"
// @Param sortOrder query string false "Sort order (asc/desc)"
// @Success 200 {object} services.PaginatedListsResponse
// @Failure 400 {object} map[string]interface{} "Invalid query parameters"
// @Failure 401 {object} map[string]interface{} "Unauthorized"
// @Router /lists/by-player/{playerId} [get]
func (h *ListHandler) FindByPlayerID(c *gin.Context) {
userID, exists := c.Get("userId")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{
"statusCode": http.StatusUnauthorized,
"message": "Unauthorized",
"errorCode": "UNAUTHORIZED",
})
return
}
playerID := c.Param("playerId")
if playerID == "" {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid playerId",
"errorCode": "VALIDATION_ERROR",
})
return
}
var query services.QueryListsRequest
if err := c.ShouldBindQuery(&query); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid query parameters",
"errorCode": "VALIDATION_ERROR",
})
return
}
result, err := h.service.FindByPlayerID(userID.(uint), playerID, query)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusOK, result)
}
// FindSharedWithMe godoc // FindSharedWithMe godoc
// @Summary Get lists shared with the authenticated user // @Summary Get lists shared with the authenticated user
// @Description Get paginated list of player lists that have been shared with the authenticated user // @Description Get paginated list of player lists that have been shared with the authenticated user
...@@ -299,3 +368,132 @@ func (h *ListHandler) Delete(c *gin.Context) { ...@@ -299,3 +368,132 @@ func (h *ListHandler) Delete(c *gin.Context) {
c.Status(http.StatusNoContent) c.Status(http.StatusNoContent)
} }
// Share godoc
// @Summary Share a list
// @Description Share a list with another user
// @Tags Lists
// @Security BearerAuth
// @Accept json
// @Param id path string true "List ID"
// @Param request body createListShareRequest true "Share payload"
// @Success 204
// @Failure 400 {object} map[string]interface{} "Invalid request body"
// @Failure 401 {object} map[string]interface{} "Unauthorized"
// @Failure 403 {object} map[string]interface{} "Forbidden"
// @Failure 404 {object} map[string]interface{} "List not found"
// @Router /lists/{id}/shares [post]
func (h *ListHandler) Share(c *gin.Context) {
userVal, exists := c.Get("user")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{
"statusCode": http.StatusUnauthorized,
"message": "Unauthorized",
"errorCode": "UNAUTHORIZED",
})
return
}
actor, ok := userVal.(models.User)
if !ok {
c.JSON(http.StatusUnauthorized, gin.H{
"statusCode": http.StatusUnauthorized,
"message": "Unauthorized",
"errorCode": "UNAUTHORIZED",
})
return
}
listID := c.Param("id")
var req createListShareRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid request body",
"errorCode": "VALIDATION_ERROR",
})
return
}
if err := h.service.Share(actor, listID, req.UserID); err != nil {
code := http.StatusInternalServerError
errCode := "INTERNAL_ERROR"
if err.Error() == "list not found" {
code = http.StatusNotFound
errCode = "NOT_FOUND"
} else if err.Error() == "forbidden" {
code = http.StatusForbidden
errCode = "FORBIDDEN"
}
c.JSON(code, gin.H{
"statusCode": code,
"message": err.Error(),
"errorCode": errCode,
})
return
}
c.Status(http.StatusNoContent)
}
// Unshare godoc
// @Summary Unshare a list
// @Description Unshare a list from a user
// @Tags Lists
// @Security BearerAuth
// @Param id path string true "List ID"
// @Param userId path int true "User ID"
// @Success 204
// @Failure 400 {object} map[string]interface{} "Invalid user ID"
// @Failure 401 {object} map[string]interface{} "Unauthorized"
// @Failure 403 {object} map[string]interface{} "Forbidden"
// @Failure 404 {object} map[string]interface{} "List not found"
// @Router /lists/{id}/shares/{userId} [delete]
func (h *ListHandler) Unshare(c *gin.Context) {
userVal, exists := c.Get("user")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{
"statusCode": http.StatusUnauthorized,
"message": "Unauthorized",
"errorCode": "UNAUTHORIZED",
})
return
}
actor, ok := userVal.(models.User)
if !ok {
c.JSON(http.StatusUnauthorized, gin.H{
"statusCode": http.StatusUnauthorized,
"message": "Unauthorized",
"errorCode": "UNAUTHORIZED",
})
return
}
listID := c.Param("id")
uid, err := strconv.ParseUint(c.Param("userId"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid user ID",
"errorCode": "VALIDATION_ERROR",
})
return
}
if err := h.service.Unshare(actor, listID, uint(uid)); err != nil {
code := http.StatusInternalServerError
errCode := "INTERNAL_ERROR"
if err.Error() == "list not found" {
code = http.StatusNotFound
errCode = "NOT_FOUND"
} else if err.Error() == "forbidden" {
code = http.StatusForbidden
errCode = "FORBIDDEN"
}
c.JSON(code, gin.H{
"statusCode": code,
"message": err.Error(),
"errorCode": errCode,
})
return
}
c.Status(http.StatusNoContent)
}
...@@ -8,6 +8,7 @@ import ( ...@@ -8,6 +8,7 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"gorm.io/gorm" "gorm.io/gorm"
"ScoutSystemElite/internal/models"
"ScoutSystemElite/internal/services" "ScoutSystemElite/internal/services"
) )
...@@ -33,6 +34,44 @@ func NewPlayerHandler(db *gorm.DB) *PlayerHandler { ...@@ -33,6 +34,44 @@ func NewPlayerHandler(db *gorm.DB) *PlayerHandler {
// @Failure 400 {object} map[string]interface{} "Invalid request" // @Failure 400 {object} map[string]interface{} "Invalid request"
// @Router /players [post] // @Router /players [post]
func (h *PlayerHandler) Save(c *gin.Context) { func (h *PlayerHandler) Save(c *gin.Context) {
userVal, exists := c.Get("user")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{
"statusCode": http.StatusUnauthorized,
"message": "Unauthorized",
"errorCode": "UNAUTHORIZED",
})
return
}
actor, ok := userVal.(models.User)
if !ok {
c.JSON(http.StatusUnauthorized, gin.H{
"statusCode": http.StatusUnauthorized,
"message": "Unauthorized",
"errorCode": "UNAUTHORIZED",
})
return
}
userIDVal, exists := c.Get("userId")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{
"statusCode": http.StatusUnauthorized,
"message": "Unauthorized",
"errorCode": "UNAUTHORIZED",
})
return
}
userID, ok := userIDVal.(uint)
if !ok {
c.JSON(http.StatusUnauthorized, gin.H{
"statusCode": http.StatusUnauthorized,
"message": "Unauthorized",
"errorCode": "UNAUTHORIZED",
})
return
}
var req services.CreatePlayerRequest var req services.CreatePlayerRequest
if err := c.ShouldBindJSON(&req); err != nil { if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{ c.JSON(http.StatusBadRequest, gin.H{
...@@ -43,7 +82,7 @@ func (h *PlayerHandler) Save(c *gin.Context) { ...@@ -43,7 +82,7 @@ func (h *PlayerHandler) Save(c *gin.Context) {
return return
} }
player, err := h.service.UpsertByWyID(req) player, err := h.service.UpsertByWyID(userID, actor.Role, req)
if err != nil { if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{ c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError, "statusCode": http.StatusInternalServerError,
...@@ -221,6 +260,8 @@ func (h *PlayerHandler) GetByProviderID(c *gin.Context) { ...@@ -221,6 +260,8 @@ func (h *PlayerHandler) GetByProviderID(c *gin.Context) {
// @Param teamWyId query int false "Filter by team WyID" // @Param teamWyId query int false "Filter by team WyID"
// @Param nationalityWyId query int false "Filter by nationality WyID" // @Param nationalityWyId query int false "Filter by nationality WyID"
// @Param positions query string false "Filter by positions (comma-separated)" // @Param positions query string false "Filter by positions (comma-separated)"
// @Param createdByUserId query int false "Filter by createdByUserId"
// @Param updatedByUserId query int false "Filter by updatedByUserId"
// @Param minAge query int false "Minimum age" // @Param minAge query int false "Minimum age"
// @Param maxAge query int false "Maximum age" // @Param maxAge query int false "Maximum age"
// @Param minHeight query int false "Minimum height in cm" // @Param minHeight query int false "Minimum height in cm"
...@@ -272,6 +313,25 @@ func (h *PlayerHandler) FindAll(c *gin.Context) { ...@@ -272,6 +313,25 @@ func (h *PlayerHandler) FindAll(c *gin.Context) {
// @Failure 404 {object} map[string]interface{} "Player not found" // @Failure 404 {object} map[string]interface{} "Player not found"
// @Router /players/{id} [patch] // @Router /players/{id} [patch]
func (h *PlayerHandler) UpdateByID(c *gin.Context) { func (h *PlayerHandler) UpdateByID(c *gin.Context) {
userVal, exists := c.Get("user")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{
"statusCode": http.StatusUnauthorized,
"message": "Unauthorized",
"errorCode": "UNAUTHORIZED",
})
return
}
actor, ok := userVal.(models.User)
if !ok {
c.JSON(http.StatusUnauthorized, gin.H{
"statusCode": http.StatusUnauthorized,
"message": "Unauthorized",
"errorCode": "UNAUTHORIZED",
})
return
}
id := c.Param("id") id := c.Param("id")
var req services.UpdatePlayerRequest var req services.UpdatePlayerRequest
...@@ -284,7 +344,7 @@ func (h *PlayerHandler) UpdateByID(c *gin.Context) { ...@@ -284,7 +344,7 @@ func (h *PlayerHandler) UpdateByID(c *gin.Context) {
return return
} }
player, err := h.service.UpdateByID(id, req) player, err := h.service.UpdateByID(actor.ID, actor.Role, id, req)
if err != nil { if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{ c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError, "statusCode": http.StatusInternalServerError,
......
...@@ -7,9 +7,12 @@ import ( ...@@ -7,9 +7,12 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"gorm.io/gorm" "gorm.io/gorm"
"ScoutSystemElite/internal/models"
"ScoutSystemElite/internal/services" "ScoutSystemElite/internal/services"
) )
var _ = models.ClientModule{}
type SuperAdminHandler struct { type SuperAdminHandler struct {
service *services.SuperAdminService service *services.SuperAdminService
} }
......
package handlers
import (
"log"
"net/http"
"github.com/gin-gonic/gin"
"github.com/go-playground/validator/v10"
"gorm.io/gorm"
"ScoutSystemElite/internal/services"
)
type TeamHandler struct {
service *services.TeamService
}
func NewTeamHandler(db *gorm.DB) *TeamHandler {
return &TeamHandler{
service: services.NewTeamService(db),
}
}
// Create godoc
// @Summary Create a new team
// @Description Create a new team
// @Tags Teams
// @Security BearerAuth
// @Accept json
// @Produce json
// @Param request body services.CreateTeamRequest true "Team data"
// @Success 201 {object} services.TeamResponse
// @Failure 400 {object} map[string]interface{} "Invalid request"
// @Router /teams [post]
func (h *TeamHandler) Create(c *gin.Context) {
var req services.CreateTeamRequest
if err := c.ShouldBindBodyWithJSON(&req); err != nil {
log.Printf("teams.Create: invalid request body: %v", err)
if ve, ok := err.(validator.ValidationErrors); ok {
errs := make([]gin.H, 0, len(ve))
for _, fe := range ve {
errs = append(errs, gin.H{
"field": fe.Field(),
"tag": fe.Tag(),
})
}
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid request body",
"errorCode": "VALIDATION_ERROR",
"details": errs,
})
return
}
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid request body",
"errorCode": "VALIDATION_ERROR",
"details": err.Error(),
})
return
}
team, err := h.service.Create(req)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusCreated, team)
}
// FindAll godoc
// @Summary Get all teams
// @Description Get paginated list of teams with optional filters
// @Tags Teams
// @Security BearerAuth
// @Produce json
// @Param name query string false "Filter by name"
// @Param type query string false "Filter by type"
// @Param category query string false "Filter by category"
// @Param gender query string false "Filter by gender"
// @Param areaWyId query int false "Filter by area WyID"
// @Param isActive query boolean false "Filter by active status"
// @Param limit query int false "Limit results" default(100)
// @Param offset query int false "Offset for pagination" default(0)
// @Param sortBy query string false "Sort field"
// @Param sortOrder query string false "Sort order (asc/desc)"
// @Success 200 {object} services.PaginatedTeamsResponse
// @Failure 400 {object} map[string]interface{} "Invalid query parameters"
// @Router /teams [get]
func (h *TeamHandler) FindAll(c *gin.Context) {
var query services.QueryTeamsRequest
if err := c.ShouldBindQuery(&query); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid query parameters",
"errorCode": "VALIDATION_ERROR",
})
return
}
result, err := h.service.FindAll(query)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
c.JSON(http.StatusOK, result)
}
// GetByID godoc
// @Summary Get team by ID
// @Description Get a single team by its ID
// @Tags Teams
// @Security BearerAuth
// @Produce json
// @Param id path string true "Team ID"
// @Success 200 {object} services.TeamResponse
// @Failure 404 {object} map[string]interface{} "Team not found"
// @Router /teams/{id} [get]
func (h *TeamHandler) GetByID(c *gin.Context) {
id := c.Param("id")
team, err := h.service.FindByID(id)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
if team == nil {
c.JSON(http.StatusNotFound, gin.H{
"statusCode": http.StatusNotFound,
"message": "Team not found",
"errorCode": "NOT_FOUND",
})
return
}
c.JSON(http.StatusOK, team)
}
// UpdateByID godoc
// @Summary Update a team
// @Description Update an existing team's information
// @Tags Teams
// @Security BearerAuth
// @Accept json
// @Produce json
// @Param id path string true "Team ID"
// @Param request body services.UpdateTeamRequest true "Team data"
// @Success 200 {object} services.TeamResponse
// @Failure 400 {object} map[string]interface{} "Invalid request"
// @Failure 404 {object} map[string]interface{} "Team not found"
// @Router /teams/{id} [patch]
func (h *TeamHandler) UpdateByID(c *gin.Context) {
id := c.Param("id")
var req services.UpdateTeamRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"statusCode": http.StatusBadRequest,
"message": "Invalid request body",
"errorCode": "VALIDATION_ERROR",
})
return
}
team, err := h.service.UpdateByID(id, req)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"statusCode": http.StatusInternalServerError,
"message": err.Error(),
"errorCode": "INTERNAL_ERROR",
})
return
}
if team == nil {
c.JSON(http.StatusNotFound, gin.H{
"statusCode": http.StatusNotFound,
"message": "Team not found",
"errorCode": "NOT_FOUND",
})
return
}
c.JSON(http.StatusOK, team)
}
// DeleteByID godoc
// @Summary Delete a team
// @Description Soft delete a team
// @Tags Teams
// @Security BearerAuth
// @Param id path string true "Team ID"
// @Success 204
// @Failure 404 {object} map[string]interface{} "Team not found"
// @Router /teams/{id} [delete]
func (h *TeamHandler) DeleteByID(c *gin.Context) {
id := c.Param("id")
if err := h.service.DeleteByID(id); err != nil {
code := http.StatusInternalServerError
errCode := "INTERNAL_ERROR"
if err.Error() == "team not found" {
code = http.StatusNotFound
errCode = "NOT_FOUND"
}
c.JSON(code, gin.H{
"statusCode": code,
"message": err.Error(),
"errorCode": errCode,
})
return
}
c.Status(http.StatusNoContent)
}
...@@ -100,6 +100,26 @@ type Position struct { ...@@ -100,6 +100,26 @@ type Position struct {
func (Position) TableName() string { return "positions" } func (Position) TableName() string { return "positions" }
type Team struct {
ID string `gorm:"primaryKey;size:16;column:id" json:"id"`
Name string `gorm:"column:name;not null;index:idx_teams_name" json:"name"`
OfficialName *string `gorm:"column:official_name" json:"officialName"`
ShortName *string `gorm:"column:short_name" json:"shortName"`
Description *string `gorm:"column:description" json:"description"`
Type string `gorm:"column:type;default:club" json:"type"`
Category string `gorm:"column:category;default:default" json:"category"`
Gender *string `gorm:"column:gender" json:"gender"`
AreaWyID *int `gorm:"column:area_wy_id;index:idx_teams_area_wy_id" json:"areaWyId"`
City *string `gorm:"column:city" json:"city"`
ImageDataURL *string `gorm:"column:image_data_url" json:"imageDataUrl"`
IsActive bool `gorm:"column:is_active;default:true;index:idx_teams_is_active" json:"isActive"`
CreatedAt time.Time `gorm:"column:created_at" json:"createdAt"`
UpdatedAt time.Time `gorm:"column:updated_at" json:"updatedAt"`
DeletedAt *time.Time `gorm:"column:deleted_at;index:idx_teams_deleted_at" json:"deletedAt"`
}
func (Team) TableName() string { return "teams" }
type Player struct { type Player struct {
ID string `gorm:"primaryKey;size:16;column:id" json:"id"` ID string `gorm:"primaryKey;size:16;column:id" json:"id"`
WyID *int `gorm:"column:wy_id;uniqueIndex" json:"wyId"` WyID *int `gorm:"column:wy_id;uniqueIndex" json:"wyId"`
...@@ -107,6 +127,8 @@ type Player struct { ...@@ -107,6 +127,8 @@ type Player struct {
TeamWyID *int `gorm:"column:team_wy_id" json:"teamWyId"` TeamWyID *int `gorm:"column:team_wy_id" json:"teamWyId"`
TeamTsID *string `gorm:"column:team_ts_id;size:64" json:"teamTsId"` TeamTsID *string `gorm:"column:team_ts_id;size:64" json:"teamTsId"`
CountryTsID *string `gorm:"column:country_ts_id;size:64" json:"countryTsId"` CountryTsID *string `gorm:"column:country_ts_id;size:64" json:"countryTsId"`
CreatedByUserID *uint `gorm:"column:created_by_user_id;index:idx_players_created_by_user_id" json:"createdByUserId"`
UpdatedByUserID *uint `gorm:"column:updated_by_user_id;index:idx_players_updated_by_user_id" json:"updatedByUserId"`
FirstName string `gorm:"column:first_name" json:"firstName"` FirstName string `gorm:"column:first_name" json:"firstName"`
LastName string `gorm:"column:last_name" json:"lastName"` LastName string `gorm:"column:last_name" json:"lastName"`
MiddleName *string `gorm:"column:middle_name" json:"middleName"` MiddleName *string `gorm:"column:middle_name" json:"middleName"`
......
...@@ -320,10 +320,25 @@ func New(db *gorm.DB, cfg config.Config) *gin.Engine { ...@@ -320,10 +320,25 @@ func New(db *gorm.DB, cfg config.Config) *gin.Engine {
{ {
listsGroup.POST("", listHandler.Create) listsGroup.POST("", listHandler.Create)
listsGroup.GET("", listHandler.FindAll) listsGroup.GET("", listHandler.FindAll)
listsGroup.GET("/by-player/:playerId", listHandler.FindByPlayerID)
listsGroup.GET("/shared-with-me", listHandler.FindSharedWithMe) listsGroup.GET("/shared-with-me", listHandler.FindSharedWithMe)
listsGroup.GET("/:id", listHandler.GetByID) listsGroup.GET("/:id", listHandler.GetByID)
listsGroup.PATCH("/:id", listHandler.Update) listsGroup.PATCH("/:id", listHandler.Update)
listsGroup.DELETE("/:id", listHandler.Delete) listsGroup.DELETE("/:id", listHandler.Delete)
listsGroup.POST("/:id/shares", listHandler.Share)
listsGroup.DELETE("/:id/shares/:userId", listHandler.Unshare)
}
// Teams routes
teamHandler := handlers.NewTeamHandler(db)
teamsGroup := api.Group("/teams")
teamsGroup.Use(jwtMiddleware)
{
teamsGroup.POST("", teamHandler.Create)
teamsGroup.GET("", teamHandler.FindAll)
teamsGroup.GET("/:id", teamHandler.GetByID)
teamsGroup.PATCH("/:id", teamHandler.UpdateByID)
teamsGroup.DELETE("/:id", teamHandler.DeleteByID)
} }
// Positions routes // Positions routes
......
...@@ -94,12 +94,20 @@ func (s *ListService) Create(userID uint, req CreateListRequest) (*ListResponse, ...@@ -94,12 +94,20 @@ func (s *ListService) Create(userID uint, req CreateListRequest) (*ListResponse,
func (s *ListService) FindByID(userID uint, id string) (*ListResponse, error) { func (s *ListService) FindByID(userID uint, id string) (*ListResponse, error) {
var list models.List var list models.List
if err := s.db.Where("id = ? AND user_id = ? AND deleted_at IS NULL", id, userID).First(&list).Error; err != nil { if err := s.db.Where("id = ? AND deleted_at IS NULL", id).First(&list).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fmt.Errorf("list not found") return nil, fmt.Errorf("list not found")
} }
return nil, err return nil, err
} }
ok, err := s.canAccessList(userID, id)
if err != nil {
return nil, err
}
if !ok {
return nil, fmt.Errorf("list not found")
}
return toListResponse(list), nil return toListResponse(list), nil
} }
...@@ -167,6 +175,79 @@ func (s *ListService) FindAll(userID uint, query QueryListsRequest) (*PaginatedL ...@@ -167,6 +175,79 @@ func (s *ListService) FindAll(userID uint, query QueryListsRequest) (*PaginatedL
}, nil }, nil
} }
func (s *ListService) FindByPlayerID(userID uint, playerID string, query QueryListsRequest) (*PaginatedListsResponse, error) {
q := s.db.
Model(&models.List{}).
Joins("LEFT JOIN list_shares ON list_shares.list_id = lists.id AND list_shares.user_id = ?", userID).
Where("lists.deleted_at IS NULL").
Where("lists.user_id = ? OR list_shares.user_id = ?", userID, userID).
Where(
"jsonb_path_exists(lists.players_by_position, ?::jsonpath, jsonb_build_object('pid', to_jsonb(?::text)))",
"$.*.playersIds[*].playerId ? (@ == $pid)",
playerID,
)
if query.Type != "" {
q = q.Where("lists.type = ?", query.Type)
}
if query.Name != "" {
q = q.Where("LOWER(lists.name) LIKE ?", "%"+strings.ToLower(query.Name)+"%")
}
if query.Season != "" {
q = q.Where("lists.season = ?", query.Season)
}
if query.IsActive != nil {
q = q.Where("lists.is_active = ?", *query.IsActive)
}
var total int64
q.Count(&total)
sortBy := "name"
allowedSorts := map[string]string{
"name": "lists.name",
"type": "lists.type",
"season": "lists.season",
"createdAt": "lists.created_at",
"updatedAt": "lists.updated_at",
}
if col, ok := allowedSorts[query.SortBy]; ok {
sortBy = col
}
sortOrder := "ASC"
if strings.ToLower(query.SortOrder) == "desc" {
sortOrder = "DESC"
}
limit := query.Limit
if limit <= 0 || limit > 1000 {
limit = 100
}
offset := query.Offset
if offset < 0 {
offset = 0
}
var lists []models.List
if err := q.Order(sortBy + " " + sortOrder).Limit(limit).Offset(offset).Find(&lists).Error; err != nil {
return nil, err
}
data := make([]ListResponse, len(lists))
for i, l := range lists {
data[i] = *toListResponse(l)
}
return &PaginatedListsResponse{
Data: data,
Total: total,
Limit: limit,
Offset: offset,
HasMore: int64(offset+len(lists)) < total,
}, nil
}
func (s *ListService) FindSharedWithUser(userID uint, query QueryListsRequest) (*PaginatedListsResponse, error) { func (s *ListService) FindSharedWithUser(userID uint, query QueryListsRequest) (*PaginatedListsResponse, error) {
q := s.db. q := s.db.
Model(&models.List{}). Model(&models.List{}).
...@@ -287,6 +368,58 @@ func (s *ListService) Delete(userID uint, id string) error { ...@@ -287,6 +368,58 @@ func (s *ListService) Delete(userID uint, id string) error {
return s.db.Model(&list).Update("deleted_at", now).Error return s.db.Model(&list).Update("deleted_at", now).Error
} }
func (s *ListService) Share(actor models.User, listID string, shareWithUserID uint) error {
var list models.List
if err := s.db.Where("id = ? AND deleted_at IS NULL", listID).First(&list).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fmt.Errorf("list not found")
}
return err
}
if list.UserID != actor.ID {
return fmt.Errorf("forbidden")
}
share := models.ListShare{ListID: listID, UserID: shareWithUserID}
if err := s.db.Create(&share).Error; err != nil {
return err
}
return nil
}
func (s *ListService) Unshare(actor models.User, listID string, shareWithUserID uint) error {
var list models.List
if err := s.db.Where("id = ? AND deleted_at IS NULL", listID).First(&list).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fmt.Errorf("list not found")
}
return err
}
if list.UserID != actor.ID {
return fmt.Errorf("forbidden")
}
res := s.db.Where("list_id = ? AND user_id = ?", listID, shareWithUserID).Delete(&models.ListShare{})
if res.Error != nil {
return res.Error
}
return nil
}
func (s *ListService) canAccessList(userID uint, listID string) (bool, error) {
var count int64
err := s.db.
Model(&models.List{}).
Joins("LEFT JOIN list_shares ON list_shares.list_id = lists.id AND list_shares.user_id = ?", userID).
Where("lists.id = ? AND lists.deleted_at IS NULL", listID).
Where("lists.user_id = ? OR list_shares.user_id = ?", userID, userID).
Count(&count).Error
if err != nil {
return false, err
}
return count > 0, nil
}
func toListResponse(l models.List) *ListResponse { func toListResponse(l models.List) *ListResponse {
isActive := l.IsActive != nil && *l.IsActive isActive := l.IsActive != nil && *l.IsActive
var players interface{} var players interface{}
......
...@@ -6,6 +6,7 @@ import ( ...@@ -6,6 +6,7 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"strconv"
"strings" "strings"
"time" "time"
...@@ -106,6 +107,8 @@ type PlayerResponse struct { ...@@ -106,6 +107,8 @@ type PlayerResponse struct {
TeamWyID *int `json:"teamWyId"` TeamWyID *int `json:"teamWyId"`
TeamTsID *string `json:"teamTsId"` TeamTsID *string `json:"teamTsId"`
CountryTsID *string `json:"countryTsId"` CountryTsID *string `json:"countryTsId"`
CreatedByUserID *uint `json:"createdByUserId"`
UpdatedByUserID *uint `json:"updatedByUserId"`
ShortName string `json:"shortName"` ShortName string `json:"shortName"`
FirstName string `json:"firstName"` FirstName string `json:"firstName"`
MiddleName string `json:"middleName"` MiddleName string `json:"middleName"`
...@@ -160,14 +163,20 @@ type QueryPlayersRequest struct { ...@@ -160,14 +163,20 @@ type QueryPlayersRequest struct {
Name string `form:"name"` Name string `form:"name"`
Foot string `form:"foot"` Foot string `form:"foot"`
TeamWyID *int `form:"teamWyId"` TeamWyID *int `form:"teamWyId"`
CurrentTeamWyID *int `form:"currentTeamWyId"`
NationalityWyID *int `form:"nationalityWyId"` NationalityWyID *int `form:"nationalityWyId"`
NationalTeamID *int `form:"nationalTeamId"` NationalTeamID *int `form:"nationalTeamId"`
NationalTeamWyID *int `form:"nationalTeamWyId"`
PassportAreaWyID *int `form:"passportAreaWyId"` PassportAreaWyID *int `form:"passportAreaWyId"`
EUPassport *bool `form:"euPassport"` EUPassport *bool `form:"euPassport"`
Archived *bool `form:"archived"` Archived *bool `form:"archived"`
Positions string `form:"positions"` Positions string `form:"positions"`
ScoutID *int `form:"scoutId"` ScoutID *int `form:"scoutId"`
CreatedByUserID *uint `form:"createdByUserId"`
UpdatedByUserID *uint `form:"updatedByUserId"`
BornOn string `form:"bornOn"` BornOn string `form:"bornOn"`
MinDate string `form:"minDate"`
MaxDate string `form:"maxDate"`
MinAge *int `form:"minAge"` MinAge *int `form:"minAge"`
MaxAge *int `form:"maxAge"` MaxAge *int `form:"maxAge"`
MinHeight *int `form:"minHeight"` MinHeight *int `form:"minHeight"`
...@@ -176,6 +185,7 @@ type QueryPlayersRequest struct { ...@@ -176,6 +185,7 @@ type QueryPlayersRequest struct {
MaxWeight *int `form:"maxWeight"` MaxWeight *int `form:"maxWeight"`
Gender string `form:"gender"` Gender string `form:"gender"`
Status string `form:"status"` Status string `form:"status"`
IsActive *bool `form:"isActive"`
Limit int `form:"limit"` Limit int `form:"limit"`
Offset int `form:"offset"` Offset int `form:"offset"`
SortBy string `form:"sortBy"` SortBy string `form:"sortBy"`
...@@ -186,6 +196,8 @@ type CreatePlayerRequest struct { ...@@ -186,6 +196,8 @@ type CreatePlayerRequest struct {
WyID *int `json:"wyId"` WyID *int `json:"wyId"`
TeamWyID *int `json:"teamWyId"` TeamWyID *int `json:"teamWyId"`
GsmID *int `json:"gsmId"` GsmID *int `json:"gsmId"`
CreatedByUserID *uint `json:"createdByUserId"`
UpdatedByUserID *uint `json:"updatedByUserId"`
FirstName string `json:"firstName" binding:"required"` FirstName string `json:"firstName" binding:"required"`
LastName string `json:"lastName"` LastName string `json:"lastName"`
...@@ -251,7 +263,7 @@ type CreatePlayerRequest struct { ...@@ -251,7 +263,7 @@ type CreatePlayerRequest struct {
TsID *string `json:"tsId"` TsID *string `json:"tsId"`
TeamTsID *string `json:"teamTsId"` TeamTsID *string `json:"teamTsId"`
CountryTsID *string `json:"countryTsId"` CountryTsID *string `json:"countryTsID"`
MarketValueCurrency *string `json:"marketValueCurrency"` MarketValueCurrency *string `json:"marketValueCurrency"`
ContractUntil *string `json:"contractUntil"` ContractUntil *string `json:"contractUntil"`
} }
...@@ -260,6 +272,8 @@ type UpdatePlayerRequest struct { ...@@ -260,6 +272,8 @@ type UpdatePlayerRequest struct {
WyID *int `json:"wyId"` WyID *int `json:"wyId"`
TeamWyID *int `json:"teamWyId"` TeamWyID *int `json:"teamWyId"`
GsmID *int `json:"gsmId"` GsmID *int `json:"gsmId"`
CreatedByUserID *uint `json:"createdByUserId"`
UpdatedByUserID *uint `json:"updatedByUserId"`
FirstName *string `json:"firstName"` FirstName *string `json:"firstName"`
LastName *string `json:"lastName"` LastName *string `json:"lastName"`
...@@ -323,7 +337,7 @@ type UpdatePlayerRequest struct { ...@@ -323,7 +337,7 @@ type UpdatePlayerRequest struct {
TsID *string `json:"tsId"` TsID *string `json:"tsId"`
TeamTsID *string `json:"teamTsId"` TeamTsID *string `json:"teamTsId"`
CountryTsID *string `json:"countryTsId"` CountryTsID *string `json:"countryTsID"`
MarketValueCurrency *string `json:"marketValueCurrency"` MarketValueCurrency *string `json:"marketValueCurrency"`
ContractUntil *string `json:"contractUntil"` ContractUntil *string `json:"contractUntil"`
} }
...@@ -381,11 +395,11 @@ type PaginatedPlayersResponse struct { ...@@ -381,11 +395,11 @@ type PaginatedPlayersResponse struct {
HasMore bool `json:"hasMore"` HasMore bool `json:"hasMore"`
} }
func (s *PlayerService) UpsertByWyID(req CreatePlayerRequest) (*PlayerResponse, error) { func (s *PlayerService) UpsertByWyID(actorID uint, actorRole string, req CreatePlayerRequest) (*PlayerResponse, error) {
if req.WyID != nil { if req.WyID != nil {
var existing models.Player var existing models.Player
if err := s.db.Where("wy_id = ?", *req.WyID).First(&existing).Error; err == nil { if err := s.db.Where("wy_id = ?", *req.WyID).First(&existing).Error; err == nil {
return s.updatePlayer(&existing, req) return s.updatePlayer(actorID, actorRole, &existing, req)
} }
} }
...@@ -460,6 +474,17 @@ func (s *PlayerService) UpsertByWyID(req CreatePlayerRequest) (*PlayerResponse, ...@@ -460,6 +474,17 @@ func (s *PlayerService) UpsertByWyID(req CreatePlayerRequest) (*PlayerResponse,
otherPositionIDs = datatypes.JSON(b) otherPositionIDs = datatypes.JSON(b)
} }
createdBy := actorID
updatedBy := actorID
if isPrivilegedRole(actorRole) {
if req.CreatedByUserID != nil {
createdBy = *req.CreatedByUserID
}
if req.UpdatedByUserID != nil {
updatedBy = *req.UpdatedByUserID
}
}
player := models.Player{ player := models.Player{
ID: id, ID: id,
WyID: req.WyID, WyID: req.WyID,
...@@ -467,6 +492,8 @@ func (s *PlayerService) UpsertByWyID(req CreatePlayerRequest) (*PlayerResponse, ...@@ -467,6 +492,8 @@ func (s *PlayerService) UpsertByWyID(req CreatePlayerRequest) (*PlayerResponse,
TeamWyID: req.TeamWyID, TeamWyID: req.TeamWyID,
TeamTsID: req.TeamTsID, TeamTsID: req.TeamTsID,
CountryTsID: req.CountryTsID, CountryTsID: req.CountryTsID,
CreatedByUserID: &createdBy,
UpdatedByUserID: &updatedBy,
FirstName: req.FirstName, FirstName: req.FirstName,
LastName: req.LastName, LastName: req.LastName,
MiddleName: req.MiddleName, MiddleName: req.MiddleName,
...@@ -515,7 +542,12 @@ func (s *PlayerService) UpsertByWyID(req CreatePlayerRequest) (*PlayerResponse, ...@@ -515,7 +542,12 @@ func (s *PlayerService) UpsertByWyID(req CreatePlayerRequest) (*PlayerResponse,
return s.toPlayerResponse(player) return s.toPlayerResponse(player)
} }
func (s *PlayerService) updatePlayer(player *models.Player, req CreatePlayerRequest) (*PlayerResponse, error) { func isPrivilegedRole(role string) bool {
r := strings.ToLower(strings.TrimSpace(role))
return r == "admin" || r == "superadmin"
}
func (s *PlayerService) updatePlayer(actorID uint, actorRole string, player *models.Player, req CreatePlayerRequest) (*PlayerResponse, error) {
dateStr := coalesceString(req.DateOfBirth, req.BirthDate) dateStr := coalesceString(req.DateOfBirth, req.BirthDate)
dateOfBirth, err := parseTimeFlexible(dateStr) dateOfBirth, err := parseTimeFlexible(dateStr)
if err != nil { if err != nil {
...@@ -563,11 +595,17 @@ func (s *PlayerService) updatePlayer(player *models.Player, req CreatePlayerRequ ...@@ -563,11 +595,17 @@ func (s *PlayerService) updatePlayer(player *models.Player, req CreatePlayerRequ
apiSyncStatus = *req.APISyncStatus apiSyncStatus = *req.APISyncStatus
} }
updatedBy := actorID
if isPrivilegedRole(actorRole) && req.UpdatedByUserID != nil {
updatedBy = *req.UpdatedByUserID
}
updates := map[string]interface{}{ updates := map[string]interface{}{
"ts_id": req.TsID, "ts_id": req.TsID,
"team_wy_id": req.TeamWyID, "team_wy_id": req.TeamWyID,
"team_ts_id": req.TeamTsID, "team_ts_id": req.TeamTsID,
"country_ts_id": req.CountryTsID, "country_ts_id": req.CountryTsID,
"updated_by_user_id": updatedBy,
"first_name": req.FirstName, "first_name": req.FirstName,
"last_name": req.LastName, "last_name": req.LastName,
"middle_name": req.MiddleName, "middle_name": req.MiddleName,
...@@ -684,16 +722,24 @@ func (s *PlayerService) FindAll(query QueryPlayersRequest) (*PaginatedPlayersRes ...@@ -684,16 +722,24 @@ func (s *PlayerService) FindAll(query QueryPlayersRequest) (*PaginatedPlayersRes
q = q.Where("foot = ?", query.Foot) q = q.Where("foot = ?", query.Foot)
} }
if query.TeamWyID != nil { teamWyID := query.TeamWyID
q = q.Where("team_wy_id = ?", *query.TeamWyID) if teamWyID == nil {
teamWyID = query.CurrentTeamWyID
}
if teamWyID != nil {
q = q.Where("(current_team_id = ? OR team_wy_id = ?)", *teamWyID, *teamWyID)
} }
if query.NationalityWyID != nil { if query.NationalityWyID != nil {
q = q.Where("birth_area_wy_id = ?", *query.NationalityWyID) q = q.Where("birth_area_wy_id = ?", *query.NationalityWyID)
} }
if query.NationalTeamID != nil { nationalTeamID := query.NationalTeamID
q = q.Where("current_national_team_id = ?", *query.NationalTeamID) if nationalTeamID == nil {
nationalTeamID = query.NationalTeamWyID
}
if nationalTeamID != nil {
q = q.Where("current_national_team_id = ?", *nationalTeamID)
} }
if query.PassportAreaWyID != nil { if query.PassportAreaWyID != nil {
...@@ -709,8 +755,44 @@ func (s *PlayerService) FindAll(query QueryPlayersRequest) (*PaginatedPlayersRes ...@@ -709,8 +755,44 @@ func (s *PlayerService) FindAll(query QueryPlayersRequest) (*PaginatedPlayersRes
} }
if query.Positions != "" { if query.Positions != "" {
positions := strings.Split(query.Positions, ",") parts := strings.Split(query.Positions, ",")
q = q.Where("role_name IN ?", positions) ids := make([]uint, 0, len(parts))
labels := make([]string, 0, len(parts))
for _, p := range parts {
p = strings.TrimSpace(p)
if p == "" {
continue
}
if n, err := strconv.Atoi(p); err == nil && n > 0 {
ids = append(ids, uint(n))
continue
}
labels = append(labels, strings.ToLower(p))
}
if len(labels) > 0 {
var resolved []uint
if err := s.db.Table("positions").
Select("id").
Where("deleted_at IS NULL").
Where("LOWER(name) IN ? OR LOWER(code2) IN ? OR LOWER(code3) IN ?", labels, labels, labels).
Scan(&resolved).Error; err != nil {
return nil, err
}
ids = append(ids, resolved...)
}
if len(ids) > 0 {
orParts := make([]string, 0, len(ids)+1)
args := make([]interface{}, 0, len(ids)*2)
orParts = append(orParts, "position_id IN ?")
args = append(args, ids)
for _, id := range ids {
orParts = append(orParts, "other_position_ids @> ?")
args = append(args, datatypes.JSON([]byte(fmt.Sprintf("[%d]", id))))
}
q = q.Where("("+strings.Join(orParts, " OR ")+")", args...)
}
} }
if query.BornOn != "" { if query.BornOn != "" {
...@@ -719,18 +801,48 @@ func (s *PlayerService) FindAll(query QueryPlayersRequest) (*PaginatedPlayersRes ...@@ -719,18 +801,48 @@ func (s *PlayerService) FindAll(query QueryPlayersRequest) (*PaginatedPlayersRes
} }
} }
if query.MinDate != "" {
if t, err := time.Parse("2006-01-02", query.MinDate); err == nil {
q = q.Where("DATE(date_of_birth) >= ?", t.Format("2006-01-02"))
}
}
if query.MaxDate != "" {
if t, err := time.Parse("2006-01-02", query.MaxDate); err == nil {
q = q.Where("DATE(date_of_birth) <= ?", t.Format("2006-01-02"))
}
}
if query.MinAge != nil || query.MaxAge != nil { if query.MinAge != nil || query.MaxAge != nil {
now := time.Now() today := time.Now().Truncate(24 * time.Hour)
if query.MinAge != nil { if query.MinAge != nil {
maxBirthDate := now.AddDate(-*query.MinAge, 0, 0) maxBirthDate := today.AddDate(-*query.MinAge, 0, 0)
q = q.Where("date_of_birth <= ?", maxBirthDate) q = q.Where("date_of_birth <= ?", maxBirthDate)
} }
if query.MaxAge != nil { if query.MaxAge != nil {
minBirthDate := now.AddDate(-*query.MaxAge-1, 0, 0) minBirthDate := today.AddDate(-(*query.MaxAge+1), 0, 0).AddDate(0, 0, 1)
q = q.Where("date_of_birth > ?", minBirthDate) q = q.Where("date_of_birth >= ?", minBirthDate)
} }
} }
if query.ScoutID != nil {
scout := uint(*query.ScoutID)
q = q.Where(
"EXISTS (SELECT 1 FROM reports r WHERE r.player_wy_id = players.wy_id AND r.user_id = ?)",
scout,
)
}
if query.CreatedByUserID != nil {
q = q.Where("created_by_user_id = ?", *query.CreatedByUserID)
}
if query.UpdatedByUserID != nil {
q = q.Where("updated_by_user_id = ?", *query.UpdatedByUserID)
}
if query.IsActive != nil {
q = q.Where("is_active = ?", *query.IsActive)
}
if query.MinHeight != nil { if query.MinHeight != nil {
q = q.Where("height_cm >= ?", *query.MinHeight) q = q.Where("height_cm >= ?", *query.MinHeight)
} }
...@@ -764,7 +876,9 @@ func (s *PlayerService) FindAll(query QueryPlayersRequest) (*PaginatedPlayersRes ...@@ -764,7 +876,9 @@ func (s *PlayerService) FindAll(query QueryPlayersRequest) (*PaginatedPlayersRes
"height": "height_cm", "height": "height_cm",
"weight": "weight_kg", "weight": "weight_kg",
"birthDate": "date_of_birth", "birthDate": "date_of_birth",
"position": "position", "position": "__position__",
"team": "__team__",
"nationality": "__nationality__",
"foot": "foot", "foot": "foot",
"gender": "gender", "gender": "gender",
"status": "status", "status": "status",
...@@ -789,8 +903,24 @@ func (s *PlayerService) FindAll(query QueryPlayersRequest) (*PaginatedPlayersRes ...@@ -789,8 +903,24 @@ func (s *PlayerService) FindAll(query QueryPlayersRequest) (*PaginatedPlayersRes
offset = 0 offset = 0
} }
if sortBy == "__position__" {
q = q.Joins("LEFT JOIN positions ppos ON ppos.id = players.position_id AND ppos.deleted_at IS NULL")
q = q.Order("ppos.\"order\" " + sortOrder).Order("ppos.name " + sortOrder)
q = q.Order("last_name " + sortOrder)
} else if sortBy == "__team__" {
q = q.Joins("LEFT JOIN teams t_ct ON t_ct.wy_id = players.current_team_id")
q = q.Order("COALESCE(t_ct.official_name, t_ct.name) " + sortOrder)
q = q.Order("last_name " + sortOrder)
} else if sortBy == "__nationality__" {
q = q.Joins("LEFT JOIN areas a_nat ON a_nat.wy_id = players.birth_area_wy_id AND a_nat.deleted_at IS NULL")
q = q.Order("a_nat.name " + sortOrder)
q = q.Order("last_name " + sortOrder)
} else {
q = q.Order(sortBy + " " + sortOrder)
}
var players []models.Player var players []models.Player
if err := q.Order(sortBy + " " + sortOrder).Limit(limit).Offset(offset).Find(&players).Error; err != nil { if err := q.Limit(limit).Offset(offset).Find(&players).Error; err != nil {
return nil, err return nil, err
} }
...@@ -812,7 +942,7 @@ func (s *PlayerService) FindAll(query QueryPlayersRequest) (*PaginatedPlayersRes ...@@ -812,7 +942,7 @@ func (s *PlayerService) FindAll(query QueryPlayersRequest) (*PaginatedPlayersRes
}, nil }, nil
} }
func (s *PlayerService) UpdateByID(id string, req UpdatePlayerRequest) (*PlayerResponse, error) { func (s *PlayerService) UpdateByID(actorID uint, actorRole string, id string, req UpdatePlayerRequest) (*PlayerResponse, error) {
var player models.Player var player models.Player
if err := s.db.Where("id = ? AND deleted_at IS NULL", id).First(&player).Error; err != nil { if err := s.db.Where("id = ? AND deleted_at IS NULL", id).First(&player).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
...@@ -822,6 +952,14 @@ func (s *PlayerService) UpdateByID(id string, req UpdatePlayerRequest) (*PlayerR ...@@ -822,6 +952,14 @@ func (s *PlayerService) UpdateByID(id string, req UpdatePlayerRequest) (*PlayerR
} }
updates := make(map[string]interface{}) updates := make(map[string]interface{})
updatedBy := actorID
if isPrivilegedRole(actorRole) && req.UpdatedByUserID != nil {
updatedBy = *req.UpdatedByUserID
}
updates["updated_by_user_id"] = updatedBy
if isPrivilegedRole(actorRole) && req.CreatedByUserID != nil {
updates["created_by_user_id"] = *req.CreatedByUserID
}
if req.WyID != nil { if req.WyID != nil {
updates["wy_id"] = *req.WyID updates["wy_id"] = *req.WyID
...@@ -1050,6 +1188,8 @@ func toPlayerResponse(p models.Player) *PlayerResponse { ...@@ -1050,6 +1188,8 @@ func toPlayerResponse(p models.Player) *PlayerResponse {
TeamWyID: p.TeamWyID, TeamWyID: p.TeamWyID,
TeamTsID: p.TeamTsID, TeamTsID: p.TeamTsID,
CountryTsID: p.CountryTsID, CountryTsID: p.CountryTsID,
CreatedByUserID: p.CreatedByUserID,
UpdatedByUserID: p.UpdatedByUserID,
ShortName: derefStr(p.ShortName), ShortName: derefStr(p.ShortName),
FirstName: p.FirstName, FirstName: p.FirstName,
MiddleName: derefStr(p.MiddleName), MiddleName: derefStr(p.MiddleName),
...@@ -1255,6 +1395,8 @@ func basePlayerResponse(p models.Player) *PlayerResponse { ...@@ -1255,6 +1395,8 @@ func basePlayerResponse(p models.Player) *PlayerResponse {
TeamWyID: p.TeamWyID, TeamWyID: p.TeamWyID,
TeamTsID: p.TeamTsID, TeamTsID: p.TeamTsID,
CountryTsID: p.CountryTsID, CountryTsID: p.CountryTsID,
CreatedByUserID: p.CreatedByUserID,
UpdatedByUserID: p.UpdatedByUserID,
ShortName: derefStr(p.ShortName), ShortName: derefStr(p.ShortName),
FirstName: p.FirstName, FirstName: p.FirstName,
MiddleName: derefStr(p.MiddleName), MiddleName: derefStr(p.MiddleName),
......
package services
import (
"crypto/rand"
"encoding/hex"
"errors"
"fmt"
"strings"
"time"
"gorm.io/gorm"
"ScoutSystemElite/internal/models"
)
type TeamService struct {
db *gorm.DB
}
func NewTeamService(db *gorm.DB) *TeamService {
return &TeamService{db: db}
}
type TeamResponse struct {
ID string `json:"id"`
Name string `json:"name"`
OfficialName *string `json:"officialName"`
ShortName *string `json:"shortName"`
Description *string `json:"description"`
Type string `json:"type"`
Category string `json:"category"`
Gender *string `json:"gender"`
AreaWyID *int `json:"areaWyId"`
City *string `json:"city"`
ImageDataURL *string `json:"imageDataUrl"`
IsActive bool `json:"isActive"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
DeletedAt *time.Time `json:"deletedAt"`
}
type QueryTeamsRequest struct {
Name string `form:"name"`
Type string `form:"type"`
Category string `form:"category"`
Gender string `form:"gender"`
AreaWyID *int `form:"areaWyId"`
IsActive *bool `form:"isActive"`
Limit int `form:"limit"`
Offset int `form:"offset"`
SortBy string `form:"sortBy"`
SortOrder string `form:"sortOrder"`
}
type CreateTeamRequest struct {
Name string `json:"name" binding:"required"`
OfficialName *string `json:"officialName"`
ShortName *string `json:"shortName"`
Description *string `json:"description"`
Type *string `json:"type"`
Category *string `json:"category"`
Gender *string `json:"gender"`
AreaWyID *int `json:"areaWyId"`
City *string `json:"city"`
ImageDataUrl *string `json:"imageDataUrl"`
ImageDataURL *string `json:"imageDataURL"`
}
type UpdateTeamRequest struct {
Name *string `json:"name"`
OfficialName *string `json:"officialName"`
ShortName *string `json:"shortName"`
Description *string `json:"description"`
Type *string `json:"type"`
Category *string `json:"category"`
Gender *string `json:"gender"`
AreaWyID *int `json:"areaWyId"`
City *string `json:"city"`
ImageDataUrl *string `json:"imageDataUrl"`
ImageDataURL *string `json:"imageDataURL"`
IsActive *bool `json:"isActive"`
}
type PaginatedTeamsResponse struct {
Data []TeamResponse `json:"data"`
Total int64 `json:"total"`
Limit int `json:"limit"`
Offset int `json:"offset"`
HasMore bool `json:"hasMore"`
}
func (s *TeamService) Create(req CreateTeamRequest) (*TeamResponse, error) {
team := models.Team{
ID: generateTeamID(),
Name: req.Name,
OfficialName: req.OfficialName,
ShortName: req.ShortName,
Description: req.Description,
Type: derefStringTeam(req.Type, "club"),
Category: derefStringTeam(req.Category, "default"),
Gender: req.Gender,
AreaWyID: req.AreaWyID,
City: req.City,
ImageDataURL: coalesceStringTeam(req.ImageDataURL, req.ImageDataUrl),
IsActive: true,
}
if err := s.db.Create(&team).Error; err != nil {
return nil, err
}
return toTeamResponse(team), nil
}
func (s *TeamService) FindAll(query QueryTeamsRequest) (*PaginatedTeamsResponse, error) {
q := s.db.Model(&models.Team{}).Where("deleted_at IS NULL")
if query.Name != "" {
nameLower := "%" + strings.ToLower(query.Name) + "%"
q = q.Where(
strings.Join([]string{
"LOWER(name) LIKE ?",
"LOWER(official_name) LIKE ?",
"LOWER(short_name) LIKE ?",
}, " OR "),
nameLower, nameLower, nameLower,
)
}
if query.Type != "" {
q = q.Where("type = ?", query.Type)
}
if query.Category != "" {
q = q.Where("category = ?", query.Category)
}
if query.Gender != "" {
q = q.Where("gender = ?", query.Gender)
}
if query.AreaWyID != nil {
q = q.Where("area_wy_id = ?", *query.AreaWyID)
}
if query.IsActive != nil {
q = q.Where("is_active = ?", *query.IsActive)
}
var total int64
q.Count(&total)
sortBy := "name"
allowedSorts := map[string]string{
"name": "name",
"type": "type",
"category": "category",
"areaWyId": "area_wy_id",
"createdAt": "created_at",
"updatedAt": "updated_at",
}
if col, ok := allowedSorts[query.SortBy]; ok {
sortBy = col
}
sortOrder := "ASC"
if strings.ToLower(query.SortOrder) == "desc" {
sortOrder = "DESC"
}
limit := query.Limit
if limit <= 0 || limit > 1000 {
limit = 100
}
offset := query.Offset
if offset < 0 {
offset = 0
}
var teams []models.Team
if err := q.Order(sortBy + " " + sortOrder).Limit(limit).Offset(offset).Find(&teams).Error; err != nil {
return nil, err
}
data := make([]TeamResponse, len(teams))
for i, t := range teams {
data[i] = *toTeamResponse(t)
}
return &PaginatedTeamsResponse{
Data: data,
Total: total,
Limit: limit,
Offset: offset,
HasMore: int64(offset+len(teams)) < total,
}, nil
}
func (s *TeamService) FindByID(id string) (*TeamResponse, error) {
var team models.Team
if err := s.db.Where("id = ? AND deleted_at IS NULL", id).First(&team).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
return nil, err
}
return toTeamResponse(team), nil
}
func (s *TeamService) UpdateByID(id string, req UpdateTeamRequest) (*TeamResponse, error) {
var team models.Team
if err := s.db.Where("id = ? AND deleted_at IS NULL", id).First(&team).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
return nil, err
}
updates := make(map[string]interface{})
if req.Name != nil {
updates["name"] = *req.Name
}
if req.OfficialName != nil {
updates["official_name"] = *req.OfficialName
}
if req.ShortName != nil {
updates["short_name"] = *req.ShortName
}
if req.Description != nil {
updates["description"] = *req.Description
}
if req.Type != nil {
updates["type"] = *req.Type
}
if req.Category != nil {
updates["category"] = *req.Category
}
if req.Gender != nil {
updates["gender"] = *req.Gender
}
if req.AreaWyID != nil {
updates["area_wy_id"] = *req.AreaWyID
}
if req.City != nil {
updates["city"] = *req.City
}
if img := coalesceStringTeam(req.ImageDataURL, req.ImageDataUrl); img != nil {
updates["image_data_url"] = *img
}
if req.IsActive != nil {
updates["is_active"] = *req.IsActive
}
if len(updates) > 0 {
updates["updated_at"] = time.Now()
if err := s.db.Model(&team).Updates(updates).Error; err != nil {
return nil, err
}
s.db.First(&team, "id = ?", id)
}
return toTeamResponse(team), nil
}
func (s *TeamService) DeleteByID(id string) error {
var team models.Team
if err := s.db.Where("id = ? AND deleted_at IS NULL", id).First(&team).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fmt.Errorf("team not found")
}
return err
}
now := time.Now()
return s.db.Model(&team).Update("deleted_at", now).Error
}
func toTeamResponse(t models.Team) *TeamResponse {
return &TeamResponse{
ID: t.ID,
Name: t.Name,
OfficialName: t.OfficialName,
ShortName: t.ShortName,
Description: t.Description,
Type: t.Type,
Category: t.Category,
Gender: t.Gender,
AreaWyID: t.AreaWyID,
City: t.City,
ImageDataURL: t.ImageDataURL,
IsActive: t.IsActive,
CreatedAt: t.CreatedAt,
UpdatedAt: t.UpdatedAt,
DeletedAt: t.DeletedAt,
}
}
func generateTeamID() string {
b := make([]byte, 8)
rand.Read(b)
return hex.EncodeToString(b)
}
func derefStringTeam(s *string, def string) string {
if s == nil {
return def
}
return *s
}
func coalesceStringTeam(a *string, b *string) *string {
if a != nil && *a != "" {
return a
}
if b != nil && *b != "" {
return b
}
return nil
}
ALTER TABLE players
ADD COLUMN IF NOT EXISTS created_by_user_id bigint,
ADD COLUMN IF NOT EXISTS updated_by_user_id bigint;
CREATE INDEX IF NOT EXISTS idx_players_created_by_user_id ON players(created_by_user_id);
CREATE INDEX IF NOT EXISTS idx_players_updated_by_user_id ON players(updated_by_user_id);
-- Optional foreign keys:
ALTER TABLE players
ADD CONSTRAINT fk_players_created_by_user FOREIGN KEY (created_by_user_id) REFERENCES users(id),
ADD CONSTRAINT fk_players_updated_by_user FOREIGN KEY (updated_by_user_id) REFERENCES users(id);
CREATE TABLE IF NOT EXISTS teams (
id varchar(16) PRIMARY KEY,
name text NOT NULL,
official_name text,
short_name text,
description text,
type text NOT NULL DEFAULT 'club',
category text NOT NULL DEFAULT 'default',
gender text,
area_wy_id integer,
city text,
image_data_url text,
is_active boolean NOT NULL DEFAULT true,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
deleted_at timestamptz
);
CREATE INDEX IF NOT EXISTS idx_teams_name ON teams(name);
CREATE INDEX IF NOT EXISTS idx_teams_area_wy_id ON teams(area_wy_id);
CREATE INDEX IF NOT EXISTS idx_teams_is_active ON teams(is_active);
CREATE INDEX IF NOT EXISTS idx_teams_deleted_at ON teams(deleted_at);
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment