diff --git a/api.json b/api.json new file mode 100644 index 0000000..c95adf1 --- /dev/null +++ b/api.json @@ -0,0 +1,2609 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "FastAPI", + "version": "unknown" + }, + "paths": { + "/api/v1/auth/complete": { + "get": { + "tags": ["auth"], + "summary": "Complete Auth", + "operationId": "complete_auth_api_v1_auth_complete_get", + "parameters": [ + { + "name": "token", + "in": "query", + "required": true, + "schema": { + "type": "string", + "title": "Token" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ValidateLoginTokenOutput" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/auth/me": { + "get": { + "tags": ["auth"], + "summary": "Get Me", + "operationId": "get_me_api_v1_auth_me_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserOutput" + } + } + } + } + }, + "security": [ + { + "HTTPBearer": [] + } + ] + } + }, + "/api/v1/target_channels": { + "get": { + "tags": ["target_channels"], + "summary": "Get User Target Chans", + "operationId": "get_user_target_chans_api_v1_target_channels_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetUserTargetChansOutput" + } + } + } + } + }, + "security": [ + { + "HTTPBearer": [] + } + ] + } + }, + "/api/v1/target_channels/{channel_id}": { + "delete": { + "tags": ["target_channels"], + "summary": "Disconnect Target Chan", + "operationId": "disconnect_target_chan_api_v1_target_channels__channel_id__delete", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "channel_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "title": "Channel Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/external_channels/target/{target_channel_id}": { + "get": { + "tags": ["external_channels"], + "summary": "Get External Channels", + "description": "Get all external channels for a specific target channel.", + "operationId": "get_external_channels_api_v1_external_channels_target__target_channel_id__get", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "target_channel_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "title": "Target Channel Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetExternalChannelsOutput" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/external_channels": { + "post": { + "tags": ["external_channels"], + "summary": "Create External Channel", + "description": "Create a new external channel and link it to target channels.", + "operationId": "create_external_channel_api_v1_external_channels_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateExternalChannelInput" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ExternalChannelOutput" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "HTTPBearer": [] + } + ] + } + }, + "/api/v1/external_channels/{channel_id}": { + "patch": { + "tags": ["external_channels"], + "summary": "Update External Channel", + "description": "Update external channel fields (title, username, description, subscribers_count).", + "operationId": "update_external_channel_api_v1_external_channels__channel_id__patch", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "channel_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "title": "Channel Id" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateExternalChannelInput" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ExternalChannelOutput" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "delete": { + "tags": ["external_channels"], + "summary": "Delete External Channel", + "description": "Delete an external channel.", + "operationId": "delete_external_channel_api_v1_external_channels__channel_id__delete", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "channel_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "title": "Channel Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/external_channels/{channel_id}/links": { + "patch": { + "tags": ["external_channels"], + "summary": "Update External Channel Links", + "description": "Update target channel links for an existing external channel.\n\nYou can add new target channels, remove existing ones, or both in a single request.", + "operationId": "update_external_channel_links_api_v1_external_channels__channel_id__links_patch", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "channel_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "title": "Channel Id" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateExternalChannelLinksInput" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ExternalChannelOutput" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/creatives": { + "get": { + "tags": ["creatives"], + "summary": "List Creatives", + "operationId": "list_creatives_api_v1_creatives_get", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "target_channel_id", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string", + "format": "uuid" + }, + { + "type": "null" + } + ], + "title": "Target Channel Id" + } + }, + { + "name": "include_archived", + "in": "query", + "required": false, + "schema": { + "type": "boolean", + "default": false, + "title": "Include Archived" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetCreativesOutput" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "post": { + "tags": ["creatives"], + "summary": "Create Creative", + "operationId": "create_creative_api_v1_creatives_post", + "security": [ + { + "HTTPBearer": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateCreativeInput" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreativeOutput" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/creatives/{creative_id}": { + "get": { + "tags": ["creatives"], + "summary": "Get Creative", + "operationId": "get_creative_api_v1_creatives__creative_id__get", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "creative_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "title": "Creative Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreativeOutput" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "patch": { + "tags": ["creatives"], + "summary": "Update Creative", + "operationId": "update_creative_api_v1_creatives__creative_id__patch", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "creative_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "title": "Creative Id" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateCreativeInput" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreativeOutput" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "delete": { + "tags": ["creatives"], + "summary": "Delete Creative", + "operationId": "delete_creative_api_v1_creatives__creative_id__delete", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "creative_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "title": "Creative Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/placements": { + "get": { + "tags": ["placements"], + "summary": "List Placements", + "operationId": "list_placements_api_v1_placements_get", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "target_channel_id", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string", + "format": "uuid" + }, + { + "type": "null" + } + ], + "title": "Target Channel Id" + } + }, + { + "name": "external_channel_id", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string", + "format": "uuid" + }, + { + "type": "null" + } + ], + "title": "External Channel Id" + } + }, + { + "name": "creative_id", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string", + "format": "uuid" + }, + { + "type": "null" + } + ], + "title": "Creative Id" + } + }, + { + "name": "include_archived", + "in": "query", + "required": false, + "schema": { + "type": "boolean", + "default": false, + "title": "Include Archived" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetPlacementsOutput" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "post": { + "tags": ["placements"], + "summary": "Create Placement", + "operationId": "create_placement_api_v1_placements_post", + "security": [ + { + "HTTPBearer": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreatePlacementInput" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlacementOutput" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/placements/{placement_id}": { + "get": { + "tags": ["placements"], + "summary": "Get Placement", + "operationId": "get_placement_api_v1_placements__placement_id__get", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "placement_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "title": "Placement Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlacementOutput" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "patch": { + "tags": ["placements"], + "summary": "Update Placement", + "operationId": "update_placement_api_v1_placements__placement_id__patch", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "placement_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "title": "Placement Id" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdatePlacementInput" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlacementOutput" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "delete": { + "tags": ["placements"], + "summary": "Delete Placement", + "operationId": "delete_placement_api_v1_placements__placement_id__delete", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "placement_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "title": "Placement Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/placements/{placement_id}/views/history": { + "get": { + "tags": ["views"], + "summary": "Get Views History", + "description": "Получить историю просмотров для закупа.", + "operationId": "get_views_history_api_v1_placements__placement_id__views_history_get", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "placement_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "title": "Placement Id" + } + }, + { + "name": "from_date", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "From Date" + } + }, + { + "name": "to_date", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "To Date" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetViewsHistoryOutput" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/placements/{placement_id}/views/fetch": { + "post": { + "tags": ["views"], + "summary": "Fetch Views", + "description": "Получить актуальные просмотры через Telegram API.", + "operationId": "fetch_views_api_v1_placements__placement_id__views_fetch_post", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "placement_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "title": "Placement Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FetchViewsManuallyOutput" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/placements/{placement_id}/views/manual": { + "post": { + "tags": ["views"], + "summary": "Update Views Manually", + "description": "Обновить просмотры вручную.", + "operationId": "update_views_manually_api_v1_placements__placement_id__views_manual_post", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "placement_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "title": "Placement Id" + } + }, + { + "name": "views_count", + "in": "query", + "required": true, + "schema": { + "type": "integer", + "title": "Views Count" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlacementOutput" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/analytics/placements": { + "get": { + "tags": ["analytics"], + "summary": "Get Placements Analytics", + "operationId": "get_placements_analytics_api_v1_analytics_placements_get", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "target_channel_id", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string", + "format": "uuid" + }, + { + "type": "null" + } + ], + "title": "Target Channel Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetPlacementsAnalyticsOutput" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/analytics/creatives": { + "get": { + "tags": ["analytics"], + "summary": "Get Creatives Analytics", + "operationId": "get_creatives_analytics_api_v1_analytics_creatives_get", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "target_channel_id", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string", + "format": "uuid" + }, + { + "type": "null" + } + ], + "title": "Target Channel Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetCreativesAnalyticsOutput" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/analytics/external-channels": { + "get": { + "tags": ["analytics"], + "summary": "Get External Channels Analytics", + "operationId": "get_external_channels_analytics_api_v1_analytics_external_channels_get", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "target_channel_id", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string", + "format": "uuid" + }, + { + "type": "null" + } + ], + "title": "Target Channel Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetExternalChannelsAnalyticsOutput" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/analytics/spending": { + "get": { + "tags": ["analytics"], + "summary": "Get Spending Analytics", + "operationId": "get_spending_analytics_api_v1_analytics_spending_get", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "target_channel_id", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string", + "format": "uuid" + }, + { + "type": "null" + } + ], + "title": "Target Channel Id" + } + }, + { + "name": "date_from", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "Date From" + } + }, + { + "name": "date_to", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "Date To" + } + }, + { + "name": "grouping", + "in": "query", + "required": false, + "schema": { + "$ref": "#/components/schemas/DateGrouping", + "default": "day" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetSpendingAnalyticsOutput" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "ConnectTargetChanOutput": { + "properties": { + "id": { + "type": "string", + "format": "uuid", + "title": "Id" + }, + "telegram_id": { + "type": "integer", + "title": "Telegram Id" + }, + "title": { + "type": "string", + "title": "Title" + }, + "username": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Username" + }, + "is_active": { + "type": "boolean", + "title": "Is Active" + } + }, + "type": "object", + "required": ["id", "telegram_id", "title", "username", "is_active"], + "title": "ConnectTargetChanOutput" + }, + "CreateCreativeInput": { + "properties": { + "name": { + "type": "string", + "title": "Name" + }, + "text": { + "type": "string", + "title": "Text" + }, + "target_channel_id": { + "type": "string", + "format": "uuid", + "title": "Target Channel Id" + } + }, + "type": "object", + "required": ["name", "text", "target_channel_id"], + "title": "CreateCreativeInput" + }, + "CreateExternalChannelInput": { + "properties": { + "telegram_id": { + "type": "integer", + "title": "Telegram Id" + }, + "title": { + "type": "string", + "title": "Title" + }, + "username": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Username" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description" + }, + "subscribers_count": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Subscribers Count" + }, + "target_channel_ids": { + "items": { + "type": "string", + "format": "uuid" + }, + "type": "array", + "title": "Target Channel Ids" + } + }, + "type": "object", + "required": ["telegram_id", "title", "target_channel_ids"], + "title": "CreateExternalChannelInput" + }, + "CreatePlacementInput": { + "properties": { + "target_channel_id": { + "type": "string", + "format": "uuid", + "title": "Target Channel Id" + }, + "external_channel_id": { + "type": "string", + "format": "uuid", + "title": "External Channel Id" + }, + "creative_id": { + "type": "string", + "format": "uuid", + "title": "Creative Id" + }, + "placement_date": { + "type": "string", + "format": "date-time", + "title": "Placement Date" + }, + "cost": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "title": "Cost" + }, + "comment": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Comment" + }, + "ad_post_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Ad Post Url" + }, + "invite_link_type": { + "$ref": "#/components/schemas/InviteLinkType", + "default": "public" + } + }, + "type": "object", + "required": [ + "target_channel_id", + "external_channel_id", + "creative_id", + "placement_date" + ], + "title": "CreatePlacementInput" + }, + "CreativeAnalyticsOutput": { + "properties": { + "id": { + "type": "string", + "format": "uuid", + "title": "Id" + }, + "name": { + "type": "string", + "title": "Name" + }, + "placements_count": { + "type": "integer", + "title": "Placements Count" + }, + "total_cost": { + "type": "number", + "title": "Total Cost" + }, + "total_subscriptions": { + "type": "integer", + "title": "Total Subscriptions" + }, + "total_views": { + "type": "integer", + "title": "Total Views" + }, + "avg_cpf": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "title": "Avg Cpf" + }, + "avg_cpm": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "title": "Avg Cpm" + } + }, + "type": "object", + "required": [ + "id", + "name", + "placements_count", + "total_cost", + "total_subscriptions", + "total_views", + "avg_cpf", + "avg_cpm" + ], + "title": "CreativeAnalyticsOutput" + }, + "CreativeOutput": { + "properties": { + "id": { + "type": "string", + "format": "uuid", + "title": "Id" + }, + "name": { + "type": "string", + "title": "Name" + }, + "text": { + "type": "string", + "title": "Text" + }, + "target_channel_id": { + "type": "string", + "format": "uuid", + "title": "Target Channel Id" + }, + "target_channel_title": { + "type": "string", + "title": "Target Channel Title" + }, + "created_at": { + "type": "string", + "format": "date-time", + "title": "Created At" + }, + "status": { + "$ref": "#/components/schemas/CreativeStatus" + }, + "placements_count": { + "type": "integer", + "title": "Placements Count" + } + }, + "type": "object", + "required": [ + "id", + "name", + "text", + "target_channel_id", + "target_channel_title", + "created_at", + "status", + "placements_count" + ], + "title": "CreativeOutput" + }, + "CreativeStatus": { + "type": "string", + "enum": ["active", "archived"], + "title": "CreativeStatus" + }, + "DateGrouping": { + "type": "string", + "enum": ["day", "week", "month", "quarter", "year"], + "title": "DateGrouping" + }, + "ExternalChannelAnalyticsOutput": { + "properties": { + "id": { + "type": "string", + "format": "uuid", + "title": "Id" + }, + "title": { + "type": "string", + "title": "Title" + }, + "username": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Username" + }, + "placements_count": { + "type": "integer", + "title": "Placements Count" + }, + "total_cost": { + "type": "number", + "title": "Total Cost" + }, + "total_subscriptions": { + "type": "integer", + "title": "Total Subscriptions" + }, + "total_views": { + "type": "integer", + "title": "Total Views" + }, + "avg_cpf": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "title": "Avg Cpf" + }, + "avg_cpm": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "title": "Avg Cpm" + } + }, + "type": "object", + "required": [ + "id", + "title", + "username", + "placements_count", + "total_cost", + "total_subscriptions", + "total_views", + "avg_cpf", + "avg_cpm" + ], + "title": "ExternalChannelAnalyticsOutput" + }, + "ExternalChannelOutput": { + "properties": { + "id": { + "type": "string", + "format": "uuid", + "title": "Id" + }, + "telegram_id": { + "type": "integer", + "title": "Telegram Id" + }, + "title": { + "type": "string", + "title": "Title" + }, + "username": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Username" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description" + }, + "subscribers_count": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Subscribers Count" + } + }, + "type": "object", + "required": [ + "id", + "telegram_id", + "title", + "username", + "description", + "subscribers_count" + ], + "title": "ExternalChannelOutput" + }, + "FetchViewsManuallyOutput": { + "properties": { + "placement_id": { + "type": "string", + "format": "uuid", + "title": "Placement Id" + }, + "views_count": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Views Count" + }, + "views_availability": { + "$ref": "#/components/schemas/PostViewsAvailability" + }, + "fetched_at": { + "type": "string", + "format": "date-time", + "title": "Fetched At" + }, + "error_message": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Error Message" + } + }, + "type": "object", + "required": [ + "placement_id", + "views_count", + "views_availability", + "fetched_at" + ], + "title": "FetchViewsManuallyOutput" + }, + "GetCreativesAnalyticsOutput": { + "properties": { + "creatives": { + "items": { + "$ref": "#/components/schemas/CreativeAnalyticsOutput" + }, + "type": "array", + "title": "Creatives" + } + }, + "type": "object", + "required": ["creatives"], + "title": "GetCreativesAnalyticsOutput" + }, + "GetCreativesOutput": { + "properties": { + "creatives": { + "items": { + "$ref": "#/components/schemas/CreativeOutput" + }, + "type": "array", + "title": "Creatives" + } + }, + "type": "object", + "required": ["creatives"], + "title": "GetCreativesOutput" + }, + "GetExternalChannelsAnalyticsOutput": { + "properties": { + "external_channels": { + "items": { + "$ref": "#/components/schemas/ExternalChannelAnalyticsOutput" + }, + "type": "array", + "title": "External Channels" + } + }, + "type": "object", + "required": ["external_channels"], + "title": "GetExternalChannelsAnalyticsOutput" + }, + "GetExternalChannelsOutput": { + "properties": { + "external_channels": { + "items": { + "$ref": "#/components/schemas/ExternalChannelOutput" + }, + "type": "array", + "title": "External Channels" + } + }, + "type": "object", + "required": ["external_channels"], + "title": "GetExternalChannelsOutput" + }, + "GetPlacementsAnalyticsOutput": { + "properties": { + "placements": { + "items": { + "$ref": "#/components/schemas/PlacementAnalyticsOutput" + }, + "type": "array", + "title": "Placements" + } + }, + "type": "object", + "required": ["placements"], + "title": "GetPlacementsAnalyticsOutput" + }, + "GetPlacementsOutput": { + "properties": { + "placements": { + "items": { + "$ref": "#/components/schemas/PlacementOutput" + }, + "type": "array", + "title": "Placements" + } + }, + "type": "object", + "required": ["placements"], + "title": "GetPlacementsOutput" + }, + "GetSpendingAnalyticsOutput": { + "properties": { + "total_cost": { + "type": "number", + "title": "Total Cost" + }, + "total_subscriptions": { + "type": "integer", + "title": "Total Subscriptions" + }, + "total_views": { + "type": "integer", + "title": "Total Views" + }, + "avg_cpf": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "title": "Avg Cpf" + }, + "avg_cpm": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "title": "Avg Cpm" + }, + "chart_data": { + "items": { + "$ref": "#/components/schemas/SpendingDataPoint" + }, + "type": "array", + "title": "Chart Data" + } + }, + "type": "object", + "required": [ + "total_cost", + "total_subscriptions", + "total_views", + "avg_cpf", + "avg_cpm", + "chart_data" + ], + "title": "GetSpendingAnalyticsOutput" + }, + "GetUserTargetChansOutput": { + "properties": { + "target_channels": { + "items": { + "$ref": "#/components/schemas/ConnectTargetChanOutput" + }, + "type": "array", + "title": "Target Channels" + } + }, + "type": "object", + "required": ["target_channels"], + "title": "GetUserTargetChansOutput" + }, + "GetViewsHistoryOutput": { + "properties": { + "histories": { + "items": { + "$ref": "#/components/schemas/PlacementViewsHistoryOutput" + }, + "type": "array", + "title": "Histories" + } + }, + "type": "object", + "required": ["histories"], + "title": "GetViewsHistoryOutput", + "description": "История просмотров." + }, + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + "type": "array", + "title": "Detail" + } + }, + "type": "object", + "title": "HTTPValidationError" + }, + "InviteLinkType": { + "type": "string", + "enum": ["public", "approval"], + "title": "InviteLinkType" + }, + "PlacementAnalyticsOutput": { + "properties": { + "id": { + "type": "string", + "format": "uuid", + "title": "Id" + }, + "target_channel_id": { + "type": "string", + "format": "uuid", + "title": "Target Channel Id" + }, + "target_channel_title": { + "type": "string", + "title": "Target Channel Title" + }, + "external_channel_id": { + "type": "string", + "format": "uuid", + "title": "External Channel Id" + }, + "external_channel_title": { + "type": "string", + "title": "External Channel Title" + }, + "creative_id": { + "type": "string", + "format": "uuid", + "title": "Creative Id" + }, + "creative_name": { + "type": "string", + "title": "Creative Name" + }, + "placement_date": { + "type": "string", + "format": "date-time", + "title": "Placement Date" + }, + "cost": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "title": "Cost" + }, + "subscriptions_count": { + "type": "integer", + "title": "Subscriptions Count" + }, + "views_count": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Views Count" + }, + "cpf": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "title": "Cpf" + }, + "cpm": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "title": "Cpm" + } + }, + "type": "object", + "required": [ + "id", + "target_channel_id", + "target_channel_title", + "external_channel_id", + "external_channel_title", + "creative_id", + "creative_name", + "placement_date", + "cost", + "subscriptions_count", + "views_count", + "cpf", + "cpm" + ], + "title": "PlacementAnalyticsOutput" + }, + "PlacementOutput": { + "properties": { + "id": { + "type": "string", + "format": "uuid", + "title": "Id" + }, + "target_channel_id": { + "type": "string", + "format": "uuid", + "title": "Target Channel Id" + }, + "target_channel_title": { + "type": "string", + "title": "Target Channel Title" + }, + "external_channel_id": { + "type": "string", + "format": "uuid", + "title": "External Channel Id" + }, + "external_channel_title": { + "type": "string", + "title": "External Channel Title" + }, + "creative_id": { + "type": "string", + "format": "uuid", + "title": "Creative Id" + }, + "creative_name": { + "type": "string", + "title": "Creative Name" + }, + "placement_date": { + "type": "string", + "format": "date-time", + "title": "Placement Date" + }, + "cost": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "title": "Cost" + }, + "comment": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Comment" + }, + "ad_post_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Ad Post Url" + }, + "invite_link_type": { + "$ref": "#/components/schemas/InviteLinkType" + }, + "invite_link": { + "type": "string", + "title": "Invite Link" + }, + "status": { + "$ref": "#/components/schemas/PlacementStatus" + }, + "subscriptions_count": { + "type": "integer", + "title": "Subscriptions Count" + }, + "views_count": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Views Count" + }, + "views_availability": { + "$ref": "#/components/schemas/PostViewsAvailability" + }, + "last_views_fetch_at": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "Last Views Fetch At" + }, + "created_at": { + "type": "string", + "format": "date-time", + "title": "Created At" + } + }, + "type": "object", + "required": [ + "id", + "target_channel_id", + "target_channel_title", + "external_channel_id", + "external_channel_title", + "creative_id", + "creative_name", + "placement_date", + "cost", + "comment", + "ad_post_url", + "invite_link_type", + "invite_link", + "status", + "subscriptions_count", + "views_count", + "views_availability", + "last_views_fetch_at", + "created_at" + ], + "title": "PlacementOutput" + }, + "PlacementStatus": { + "type": "string", + "enum": ["active", "archived"], + "title": "PlacementStatus" + }, + "PlacementViewsHistoryOutput": { + "properties": { + "id": { + "type": "string", + "format": "uuid", + "title": "Id" + }, + "placement_id": { + "type": "string", + "format": "uuid", + "title": "Placement Id" + }, + "views_count": { + "type": "integer", + "title": "Views Count" + }, + "fetched_at": { + "type": "string", + "format": "date-time", + "title": "Fetched At" + }, + "created_at": { + "type": "string", + "format": "date-time", + "title": "Created At" + } + }, + "type": "object", + "required": [ + "id", + "placement_id", + "views_count", + "fetched_at", + "created_at" + ], + "title": "PlacementViewsHistoryOutput", + "description": "История просмотров placement." + }, + "PostViewsAvailability": { + "type": "string", + "enum": ["unknown", "available", "unavailable", "manual"], + "title": "PostViewsAvailability", + "description": "Статус доступности получения просмотров поста." + }, + "SpendingDataPoint": { + "properties": { + "period": { + "type": "string", + "title": "Period" + }, + "cost": { + "type": "number", + "title": "Cost" + }, + "subscriptions": { + "type": "integer", + "title": "Subscriptions" + }, + "views": { + "type": "integer", + "title": "Views" + }, + "cpf": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "title": "Cpf" + }, + "cpm": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "title": "Cpm" + } + }, + "type": "object", + "required": ["period", "cost", "subscriptions", "views", "cpf", "cpm"], + "title": "SpendingDataPoint" + }, + "UpdateCreativeInput": { + "properties": { + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Name" + }, + "text": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Text" + }, + "status": { + "anyOf": [ + { + "$ref": "#/components/schemas/CreativeStatus" + }, + { + "type": "null" + } + ] + } + }, + "type": "object", + "title": "UpdateCreativeInput" + }, + "UpdateExternalChannelInput": { + "properties": { + "title": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Title" + }, + "username": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Username" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description" + }, + "subscribers_count": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Subscribers Count" + } + }, + "type": "object", + "title": "UpdateExternalChannelInput" + }, + "UpdateExternalChannelLinksInput": { + "properties": { + "add_target_channel_ids": { + "items": { + "type": "string", + "format": "uuid" + }, + "type": "array", + "title": "Add Target Channel Ids", + "default": [] + }, + "remove_target_channel_ids": { + "items": { + "type": "string", + "format": "uuid" + }, + "type": "array", + "title": "Remove Target Channel Ids", + "default": [] + } + }, + "type": "object", + "title": "UpdateExternalChannelLinksInput" + }, + "UpdatePlacementInput": { + "properties": { + "placement_date": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "Placement Date" + }, + "cost": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "title": "Cost" + }, + "comment": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Comment" + }, + "status": { + "anyOf": [ + { + "$ref": "#/components/schemas/PlacementStatus" + }, + { + "type": "null" + } + ] + } + }, + "type": "object", + "title": "UpdatePlacementInput" + }, + "UserOutput": { + "properties": { + "id": { + "type": "string", + "format": "uuid", + "title": "Id" + }, + "telegram_id": { + "type": "integer", + "title": "Telegram Id" + }, + "username": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Username" + } + }, + "type": "object", + "required": ["id", "telegram_id", "username"], + "title": "UserOutput" + }, + "ValidateLoginTokenOutput": { + "properties": { + "access_token": { + "type": "string", + "title": "Access Token" + } + }, + "type": "object", + "required": ["access_token"], + "title": "ValidateLoginTokenOutput" + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "integer" + } + ] + }, + "type": "array", + "title": "Location" + }, + "msg": { + "type": "string", + "title": "Message" + }, + "type": { + "type": "string", + "title": "Error Type" + } + }, + "type": "object", + "required": ["loc", "msg", "type"], + "title": "ValidationError" + } + }, + "securitySchemes": { + "HTTPBearer": { + "type": "http", + "scheme": "bearer" + } + } + } +} diff --git a/app/(dashboard)/analytics/costs/page.tsx b/app/(dashboard)/analytics/costs/page.tsx index 6a26a5b..91bd61c 100644 --- a/app/(dashboard)/analytics/costs/page.tsx +++ b/app/(dashboard)/analytics/costs/page.tsx @@ -21,11 +21,11 @@ import { SelectValue, } from "@/components/ui/select"; import { Alert, AlertDescription } from "@/components/ui/alert"; -import { analyticsApi, channelsApi } from "@/lib/api"; +import { analyticsApi } from "@/lib/api"; import { formatCurrency, formatNumber } from "@/lib/utils/format"; import { Loader2, AlertCircle, DollarSign } from "lucide-react"; -import type { CostsReport } from "@/lib/types/api"; -import type { TargetChannel } from "@/lib/types/api"; +import type { SpendingAnalytics, DateGrouping } from "@/lib/types/api"; +import { useTargetChannel } from "@/components/providers/target-channel-provider"; import { LineChart, Line, @@ -40,32 +40,30 @@ import { } from "recharts"; export default function AnalyticsCostsPage() { - const [report, setReport] = useState(null); - const [channels, setChannels] = useState([]); + const { selectedChannel } = useTargetChannel(); + const [report, setReport] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); - const [period, setPeriod] = useState<"day" | "week" | "month">("week"); - const [targetChannelId, setTargetChannelId] = useState( - undefined - ); + const [grouping, setGrouping] = useState("week"); useEffect(() => { - loadData(); - }, [period, targetChannelId]); + if (selectedChannel) { + loadData(); + } else { + setLoading(false); + } + }, [grouping, selectedChannel]); const loadData = async () => { + if (!selectedChannel) return; + try { setLoading(true); - const [reportData, channelsData] = await Promise.all([ - analyticsApi.costs({ period, target_channel_id: targetChannelId }), - channels.length > 0 - ? Promise.resolve({ target_channels: channels }) - : channelsApi.list(), - ]); + const reportData = await analyticsApi.spending({ + grouping, + target_channel_id: selectedChannel.id, + }); setReport(reportData); - if (channels.length === 0) { - setChannels(channelsData.target_channels); - } } catch (err: any) { setError(err?.error?.message || "Ошибка загрузки аналитики"); } finally { @@ -106,47 +104,25 @@ export default function AnalyticsCostsPage() { Фильтры -
-
- - -
- -
- - -
+
+ +
@@ -159,7 +135,7 @@ export default function AnalyticsCostsPage() { ) : report ? ( <> -
+
@@ -169,7 +145,7 @@ export default function AnalyticsCostsPage() {
- {formatCurrency(report.totalCost)} + {formatCurrency(report.total_cost)}

За выбранный период @@ -180,15 +156,15 @@ export default function AnalyticsCostsPage() { - Средние затраты + Подписчики

- {formatCurrency(report.averageCost)} + {formatNumber(report.total_subscriptions)}

- В среднем за период + Всего привлечено

@@ -196,15 +172,31 @@ export default function AnalyticsCostsPage() { - Периодов + Просмотры
- {formatNumber(report.periods.length)} + {formatNumber(report.total_views)}

- С активностью + Всего просмотров +

+
+
+ + + + + Средний CPF + + + +
+ {report.avg_cpf ? formatCurrency(report.avg_cpf) : "—"} +
+

+ В среднем за период

@@ -217,13 +209,13 @@ export default function AnalyticsCostsPage() { - + @@ -245,7 +237,7 @@ export default function AnalyticsCostsPage() { />
- {report.periods.map((period) => ( + {report.chart_data.map((period) => (
-

{period.date}

+

{period.period}

- {formatNumber(period.purchases_count)} закупов + {formatNumber(period.subscriptions)} подписок • {formatNumber(period.views)} просмотров

- {formatCurrency(period.total_cost)} + {formatCurrency(period.cost)}

- ср. ₽{formatNumber(period.avg_cost)} + {period.cpf ? `CPF: ${formatCurrency(period.cpf)}` : "—"}

diff --git a/app/(dashboard)/analytics/page.tsx b/app/(dashboard)/analytics/page.tsx index 133a58a..1fd1d5a 100644 --- a/app/(dashboard)/analytics/page.tsx +++ b/app/(dashboard)/analytics/page.tsx @@ -32,7 +32,8 @@ import { ShoppingCart, DollarSign, } from "lucide-react"; -import type { AnalyticsOverview } from "@/lib/types/api"; +import type { SpendingAnalytics } from "@/lib/types/api"; +import { useTargetChannel } from "@/components/providers/target-channel-provider"; import { LineChart, Line, @@ -47,19 +48,29 @@ import { } from "recharts"; export default function AnalyticsPage() { - const [overview, setOverview] = useState(null); + const { selectedChannel } = useTargetChannel(); + const [spending, setSpending] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { - loadOverview(); - }, []); + if (selectedChannel) { + loadData(); + } else { + setLoading(false); + } + }, [selectedChannel]); + + const loadData = async () => { + if (!selectedChannel) return; - const loadOverview = async () => { try { setLoading(true); - const data = await analyticsApi.overview(); - setOverview(data); + const data = await analyticsApi.spending({ + grouping: "week", + target_channel_id: selectedChannel.id, + }); + setSpending(data); } catch (err: any) { setError(err?.error?.message || "Ошибка загрузки аналитики"); } finally { @@ -83,7 +94,7 @@ export default function AnalyticsPage() { ); } - if (error || !overview) { + if (error || !spending) { return ( <> - Всего закупов + Всего затрат - +
- {formatNumber(overview.totalPurchases)} -
-
- {overview.purchasesChange >= 0 ? ( - - ) : ( - - )} - = 0 - ? "text-green-600" - : "text-red-600" - } - > - {overview.purchasesChange >= 0 ? "+" : ""} - {formatNumber(overview.purchasesChange)} - - - за прошлый месяц - + {formatCurrency(spending.total_cost)}
+

+ За выбранный период +

@@ -165,28 +159,11 @@ export default function AnalyticsPage() {
- {formatNumber(overview.totalFollowers)} -
-
- {overview.followersChange >= 0 ? ( - - ) : ( - - )} - = 0 - ? "text-green-600" - : "text-red-600" - } - > - {overview.followersChange >= 0 ? "+" : ""} - {formatNumber(overview.followersChange)} - - - за прошлый месяц - + {formatNumber(spending.total_subscriptions)}
+

+ Всего привлечено +

@@ -197,151 +174,68 @@ export default function AnalyticsPage() {
- ₽{formatMetric(overview.averageCpf)} -
-
- {overview.cpfChange <= 0 ? ( - - ) : ( - - )} - - {formatPercent(overview.cpfChange, true)} - - - от прошлого месяца - + {spending.avg_cpf ? `₽${formatMetric(spending.avg_cpf)}` : "—"}
+

+ Стоимость подписки +

Средний CPM - +
- ₽{formatMetric(overview.averageCpm)} -
-
- {overview.cpmChange <= 0 ? ( - - ) : ( - - )} - - {formatPercent(overview.cpmChange, true)} - - - от прошлого месяца - + {spending.avg_cpm ? `₽${formatMetric(spending.avg_cpm)}` : "—"}
+

+ За 1000 просмотров +

-
- - - Топ каналов по CPF - - Самые эффективные внешние каналы - - - -
- {overview.topChannelsByCpf?.length > 0 ? ( - overview.topChannelsByCpf.slice(0, 5).map((item, index) => ( -
- - {index + 1} - -
-

- {item.channel_name} -

-

- {formatNumber(item.total_purchases)} закупов -

-
-
-

- ₽{formatMetric(item.avg_cpf)} -

-

CPF

-
-
- )) - ) : ( -

- Нет данных -

- )} -
-
-
+ + + Динамика затрат + Расходы по периодам + + + + + + + `₽${formatNumber(value)}`} + /> + + + + + + - - - Топ креативов - - Самые эффективные рекламные креативы - - - -
- {overview.topCreativesByCpf?.length > 0 ? ( - overview.topCreativesByCpf.slice(0, 5).map((item, index) => ( -
- - {index + 1} - -
-

- {item.creative_name} -

-

- {formatNumber(item.total_subscriptions)} подписчиков -

-
-
-

- ₽{formatMetric(item.avg_cpf)} -

-

CPF

-
-
- )) - ) : ( -

- Нет данных -

- )} -
-
-
-
); diff --git a/app/(dashboard)/creatives/new/page.tsx b/app/(dashboard)/creatives/new/page.tsx index c6a6207..3cc6ee0 100644 --- a/app/(dashboard)/creatives/new/page.tsx +++ b/app/(dashboard)/creatives/new/page.tsx @@ -29,32 +29,19 @@ import { Alert, AlertDescription } from "@/components/ui/alert"; import { creativesApi, channelsApi } from "@/lib/api"; import { AlertCircle, Loader2, ArrowLeft, Info } from "lucide-react"; import type { TargetChannel } from "@/lib/types/api"; +import { useTargetChannel } from "@/components/providers/target-channel-provider"; export default function CreateCreativePage() { const router = useRouter(); - const [channels, setChannels] = useState([]); + const { selectedChannel } = useTargetChannel(); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const [formData, setFormData] = useState({ name: "", text: "", - target_channel_id: "", }); - useEffect(() => { - loadChannels(); - }, []); - - const loadChannels = async () => { - try { - const response = await channelsApi.list(); - setChannels(response.target_channels.filter((c) => c.is_active)); - } catch (err) { - console.error("Error loading channels:", err); - } - }; - const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setError(null); @@ -75,14 +62,17 @@ export default function CreateCreativePage() { return; } - if (!formData.target_channel_id) { - setError("Выберите целевой канал"); + if (!selectedChannel) { + setError("Выберите целевой канал в верхнем меню"); return; } try { setIsLoading(true); - await creativesApi.create(formData); + await creativesApi.create({ + ...formData, + target_channel_id: selectedChannel.id, + }); router.push("/creatives"); } catch (err: any) { setError(err?.error?.message || "Ошибка создания креатива"); @@ -151,26 +141,14 @@ export default function CreateCreativePage() { />
-
- - -
+ {selectedChannel && ( + + + + Креатив будет создан для канала: {selectedChannel.title} + + + )} diff --git a/app/(dashboard)/creatives/page.tsx b/app/(dashboard)/creatives/page.tsx index 8975de6..17b591a 100644 --- a/app/(dashboard)/creatives/page.tsx +++ b/app/(dashboard)/creatives/page.tsx @@ -36,8 +36,10 @@ import { } from "lucide-react"; import type { Creative } from "@/lib/types/api"; import { Alert, AlertDescription } from "@/components/ui/alert"; +import { useTargetChannel } from "@/components/providers/target-channel-provider"; export default function CreativesPage() { + const { selectedChannel } = useTargetChannel(); const [creatives, setCreatives] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); @@ -46,13 +48,21 @@ export default function CreativesPage() { ); useEffect(() => { - loadCreatives(); - }, []); + if (selectedChannel) { + loadCreatives(); + } else { + setLoading(false); + } + }, [selectedChannel]); const loadCreatives = async () => { + if (!selectedChannel) return; + try { setLoading(true); - const response = await creativesApi.list(); + const response = await creativesApi.list({ + target_channel_id: selectedChannel.id, + }); setCreatives(response.creatives); } catch (err: any) { setError(err?.error?.message || "Ошибка загрузки креативов"); diff --git a/app/(dashboard)/external-channels/page.tsx b/app/(dashboard)/external-channels/page.tsx index 0fbdd14..b47509d 100644 --- a/app/(dashboard)/external-channels/page.tsx +++ b/app/(dashboard)/external-channels/page.tsx @@ -49,8 +49,10 @@ import { } from "@/components/ui/dialog"; import { Label } from "@/components/ui/label"; import { Textarea } from "@/components/ui/textarea"; +import { useTargetChannel } from "@/components/providers/target-channel-provider"; export default function ExternalChannelsPage() { + const { selectedChannel } = useTargetChannel(); const [channels, setChannels] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); @@ -68,23 +70,20 @@ export default function ExternalChannelsPage() { }); useEffect(() => { - loadChannels(); - }, []); + if (selectedChannel) { + loadChannels(); + } else { + setLoading(false); + } + }, [selectedChannel]); const loadChannels = async () => { + if (!selectedChannel) return; + try { setLoading(true); - // Сначала загружаем target channels - const targetChannelsRes = await channelsApi.list(); - if (targetChannelsRes.target_channels.length === 0) { - setError("Добавьте сначала целевой канал"); - setLoading(false); - return; - } - - // Загружаем external channels для первого target channel - const firstTargetChannel = targetChannelsRes.target_channels[0]; - const response = await externalChannelsApi.list(firstTargetChannel.id); + setError(null); + const response = await externalChannelsApi.list(selectedChannel.id); setChannels(response.external_channels); } catch (err: any) { const errorMsg = diff --git a/app/(dashboard)/page.tsx b/app/(dashboard)/page.tsx index 1cd78ed..6ec05cf 100644 --- a/app/(dashboard)/page.tsx +++ b/app/(dashboard)/page.tsx @@ -27,26 +27,53 @@ import { ShoppingCart, Loader2, } from "lucide-react"; -import type { AnalyticsOverview } from "@/lib/types/api"; +import type { SpendingAnalytics } from "@/lib/types/api"; +import { useTargetChannel } from "@/components/providers/target-channel-provider"; export default function DashboardPage() { - const [overview, setOverview] = useState(null); + const { selectedChannel } = useTargetChannel(); + const [spending, setSpending] = useState(null); const [loading, setLoading] = useState(true); useEffect(() => { - const loadOverview = async () => { + if (!selectedChannel) { + setLoading(false); + return; + } + + const loadData = async () => { try { - const data = await analyticsApi.overview(); - setOverview(data); + setLoading(true); + const data = await analyticsApi.spending({ + grouping: "week", + target_channel_id: selectedChannel.id, + }); + setSpending(data); } catch (error) { - console.error("Error loading overview:", error); + console.error("Error loading spending data:", error); } finally { setLoading(false); } }; - loadOverview(); - }, []); + loadData(); + }, [selectedChannel]); + + if (!selectedChannel) { + return ( + <> + +
+
+

Выберите целевой канал

+

+ Для начала работы выберите целевой канал в верхнем меню +

+
+
+ + ); + } return ( <> @@ -66,10 +93,10 @@ export default function DashboardPage() { ) : ( <>
- {formatNumber(overview?.totalPurchases)} + {formatNumber(spending?.total_cost)}

- {formatNumber(overview?.purchasesChange || 0, true)} за + {formatNumber(0 || 0, true)} за прошлый месяц

@@ -90,10 +117,10 @@ export default function DashboardPage() { ) : ( <>
- {formatNumber(overview?.totalFollowers)} + {formatNumber(spending?.total_subscriptions)}

- {formatNumber(overview?.followersChange || 0, true)} за + {formatNumber(0 || 0, true)} за прошлый месяц

@@ -112,10 +139,10 @@ export default function DashboardPage() { ) : ( <>
- ₽{formatMetric(overview?.averageCpf)} + ₽{formatMetric(spending?.avg_cpf)}

- {formatPercent(overview?.cpfChange || 0, true)} от прошлого + {formatPercent(0 || 0, true)} от прошлого месяца

@@ -134,10 +161,10 @@ export default function DashboardPage() { ) : ( <>
- ₽{formatMetric(overview?.averageCpm)} + ₽{formatMetric(spending?.avg_cpm)}

- {formatPercent(overview?.cpmChange || 0, true)} от прошлого + {formatPercent(0 || 0, true)} от прошлого месяца

diff --git a/app/(dashboard)/purchases/new/page.tsx b/app/(dashboard)/purchases/new/page.tsx index b6d8844..402107c 100644 --- a/app/(dashboard)/purchases/new/page.tsx +++ b/app/(dashboard)/purchases/new/page.tsx @@ -42,11 +42,12 @@ import { Copy, Check, } from "lucide-react"; -import type { TargetChannel, ExternalChannel, Creative } from "@/lib/types/api"; +import type { ExternalChannel, Creative } from "@/lib/types/api"; +import { useTargetChannel } from "@/components/providers/target-channel-provider"; export default function CreatePurchasePage() { const router = useRouter(); - const [channels, setChannels] = useState([]); + const { selectedChannel } = useTargetChannel(); const [externalChannels, setExternalChannels] = useState( [] ); @@ -57,7 +58,6 @@ export default function CreatePurchasePage() { const [copied, setCopied] = useState(false); const [formData, setFormData] = useState({ - target_channel_id: "", external_channel_id: "", creative_id: "", placement_date: "", @@ -68,41 +68,28 @@ export default function CreatePurchasePage() { }); useEffect(() => { - loadData(); - }, []); + if (selectedChannel) { + loadData(); + } + }, [selectedChannel]); const loadData = async () => { - try { - const channelsRes = await channelsApi.list(); - setChannels(channelsRes.target_channels.filter((c) => c.is_active)); + if (!selectedChannel) return; - // Загружаем внешние каналы для первого доступного target channel - if (channelsRes.target_channels.length > 0) { - const firstTargetChannel = channelsRes.target_channels[0]; - const [externalChannelsRes, creativesRes] = await Promise.all([ - externalChannelsApi.list(firstTargetChannel.id), - creativesApi.list(), - ]); - setExternalChannels(externalChannelsRes.external_channels); - setCreatives( - creativesRes.creatives.filter((c) => c.status === "active") - ); - } else { - setCreatives([]); - setExternalChannels([]); - } + try { + const [externalChannelsRes, creativesRes] = await Promise.all([ + externalChannelsApi.list(selectedChannel.id), + creativesApi.list({ target_channel_id: selectedChannel.id }), + ]); + setExternalChannels(externalChannelsRes.external_channels); + setCreatives( + creativesRes.creatives.filter((c) => c.status === "active") + ); } catch (err) { console.error("Error loading data:", err); } }; - // Фильтр креативов по выбранному целевому каналу - const filteredCreatives = formData.target_channel_id - ? creatives.filter( - (c) => c.target_channel_id === formData.target_channel_id - ) - : creatives; - const selectedCreative = creatives.find((c) => c.id === formData.creative_id); const handleSubmit = async (e: React.FormEvent) => { @@ -110,8 +97,8 @@ export default function CreatePurchasePage() { setError(null); // Validation - if (!formData.target_channel_id) { - setError("Выберите целевой канал"); + if (!selectedChannel) { + setError("Выберите целевой канал в верхнем меню"); return; } @@ -138,7 +125,7 @@ export default function CreatePurchasePage() { try { setIsLoading(true); const result = await purchasesApi.create({ - target_channel_id: formData.target_channel_id, + target_channel_id: selectedChannel.id, external_channel_id: formData.external_channel_id, creative_id: formData.creative_id, placement_date: formData.placement_date, @@ -253,7 +240,6 @@ export default function CreatePurchasePage() { onClick={() => { setInviteLink(""); setFormData({ - target_channel_id: "", external_channel_id: "", creative_id: "", placement_date: "", @@ -315,42 +301,29 @@ export default function CreatePurchasePage() { -
-
- - -
+ {selectedChannel && ( + + + + Размещение будет создано для канала: {selectedChannel.title} + + + )} +
-
-
- - + setFormData({ ...formData, creative_id: value }) + } > - - - - - {filteredCreatives.map((creative) => ( - - {creative.name} - - ))} - - + + + + + {creatives.map((creative) => ( + + {creative.name} + + ))} + + +
diff --git a/app/(dashboard)/purchases/page.tsx b/app/(dashboard)/purchases/page.tsx index 6d9eb86..99612c4 100644 --- a/app/(dashboard)/purchases/page.tsx +++ b/app/(dashboard)/purchases/page.tsx @@ -52,31 +52,35 @@ import { Clock, Archive, } from "lucide-react"; -import type { Purchase, TargetChannel } from "@/lib/types/api"; +import type { Purchase } from "@/lib/types/api"; import { Alert, AlertDescription } from "@/components/ui/alert"; +import { useTargetChannel } from "@/components/providers/target-channel-provider"; export default function PurchasesPage() { + const { selectedChannel } = useTargetChannel(); const [purchases, setPurchases] = useState([]); - const [channels, setChannels] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [searchQuery, setSearchQuery] = useState(""); - const [filterChannel, setFilterChannel] = useState("all"); const [filterStatus, setFilterStatus] = useState("all"); useEffect(() => { - loadData(); - }, []); + if (selectedChannel) { + loadData(); + } else { + setLoading(false); + } + }, [selectedChannel]); const loadData = async () => { + if (!selectedChannel) return; + try { setLoading(true); - const [purchasesRes, channelsRes] = await Promise.all([ - purchasesApi.list(), - channelsApi.list(), - ]); + const purchasesRes = await purchasesApi.list({ + target_channel_id: selectedChannel.id, + }); setPurchases(purchasesRes.placements); - setChannels(channelsRes.target_channels); } catch (err: any) { setError(err?.error?.message || "Ошибка загрузки данных"); } finally { @@ -90,15 +94,8 @@ export default function PurchasesPage() { purchase.external_channel_title .toLowerCase() .includes(searchQuery.toLowerCase()) || - purchase.target_channel_title - .toLowerCase() - .includes(searchQuery.toLowerCase()) || purchase.creative_name.toLowerCase().includes(searchQuery.toLowerCase()); - // Фильтр по каналу - const matchesChannel = - filterChannel === "all" || purchase.target_channel_id === filterChannel; - // Фильтр по статусу let matchesStatus = true; if (filterStatus === "archived") { @@ -107,7 +104,7 @@ export default function PurchasesPage() { matchesStatus = purchase.status === "active"; } - return matchesSearch && matchesChannel && matchesStatus; + return matchesSearch && matchesStatus; }); // Статистика @@ -215,20 +212,6 @@ export default function PurchasesPage() { />
- - { + if (value === "add-new") { + handleAddChannel(); + } else { + const channel = channels.find((c) => c.id === value); + if (channel) { + setSelectedChannel(channel); + } + } + }} + > + + + {selectedChannel && ( +
+ + {selectedChannel.title} + + {selectedChannel.username && ( + + @{selectedChannel.username} + + )} +
+ )} +
+
+ + {channels.map((channel) => ( + +
+
+ + {channel.title} + + {channel.username && ( + + @{channel.username} + + )} +
+
+
+ ))} + +
+ + Добавить канал +
+
+
+ + + + !isSuccess && e.preventDefault()} + > + + Добавить целевой канал + + Следуйте инструкциям ниже, чтобы подключить новый канал + + + + {isSuccess ? ( +
+
+ +
+
+

+ Канал успешно добавлен! +

+

+ Страница обновится автоматически... +

+
+
+ ) : ( +
+ + +

Шаги для добавления канала:

+
    +
  1. Откройте свой Telegram канал
  2. +
  3. Перейдите в настройки канала → Администраторы
  4. +
  5. + Добавьте бота{" "} + + @{BOT_USERNAME} + + {" "} + как администратора +
  6. +
  7. + Предоставьте боту права:{" "} + + Публикация сообщений, Удаление сообщений + +
  8. +
+
+
+ +
+ +

+ Ждем добавления бота... +
+ + Как только вы добавите бота, канал автоматически появится + +

+
+
+ )} +
+
+ + ); +}; diff --git a/lib/api/analytics.ts b/lib/api/analytics.ts index b14e3ee..3cc4c80 100644 --- a/lib/api/analytics.ts +++ b/lib/api/analytics.ts @@ -4,40 +4,41 @@ import { api } from "./client"; import type { - AnalyticsOverview, - CostsReport, - AnalyticsChannelData, - AnalyticsCreativeData, + SpendingAnalytics, + PlacementsAnalytics, + CreativesAnalytics, + ExternalChannelsAnalytics, + DateGrouping, } from "@/lib/types/api"; export const analyticsApi = { /** - * Get overview analytics + * Get spending analytics */ - overview: (params?: { target_channel_id?: string }) => - api.get("/analytics/overview", { params }), - - /** - * Get costs report - */ - costs: (params: { - period?: "day" | "week" | "month"; + spending: (params?: { target_channel_id?: string; - }) => api.get("/analytics/costs", { params }), + date_from?: string; + date_to?: string; + grouping?: DateGrouping; + }) => api.get("/analytics/spending", { params }), /** - * Get channels analytics + * Get placements analytics */ - channels: (params?: { target_channel_id?: string }) => - api.get<{ data: AnalyticsChannelData[] }>("/analytics/channels", { - params, - }), + placements: (params?: { target_channel_id?: string }) => + api.get("/analytics/placements", { params }), /** * Get creatives analytics */ creatives: (params?: { target_channel_id?: string }) => - api.get<{ data: AnalyticsCreativeData[] }>("/analytics/creatives", { + api.get("/analytics/creatives", { params }), + + /** + * Get external channels analytics + */ + externalChannels: (params?: { target_channel_id?: string }) => + api.get("/analytics/external-channels", { params, }), }; diff --git a/lib/api/client.ts b/lib/api/client.ts index 580e1de..f5c7e83 100644 --- a/lib/api/client.ts +++ b/lib/api/client.ts @@ -205,17 +205,17 @@ const routeMockRequest = async ( } // Analytics - if (endpoint === "/analytics/overview") { - return mockApiHandlers.getAnalyticsOverview(params); + if (endpoint === "/analytics/spending") { + return mockApiHandlers.getSpendingAnalytics(params); } - if (endpoint === "/analytics/costs") { - return mockApiHandlers.getAnalyticsCosts(params); - } - if (endpoint === "/analytics/channels") { - return mockApiHandlers.getAnalyticsChannels(params); + if (endpoint === "/analytics/placements") { + return mockApiHandlers.getPlacementsAnalytics(params); } if (endpoint === "/analytics/creatives") { - return mockApiHandlers.getAnalyticsCreatives(params); + return mockApiHandlers.getCreativesAnalytics(params); + } + if (endpoint === "/analytics/external-channels") { + return mockApiHandlers.getExternalChannelsAnalytics(params); } throw { diff --git a/lib/mocks/data/analytics.ts b/lib/mocks/data/analytics.ts deleted file mode 100644 index 677ea74..0000000 --- a/lib/mocks/data/analytics.ts +++ /dev/null @@ -1,277 +0,0 @@ -import type { - AnalyticsOverview, - CostsReport, - AnalyticsChannelData, - AnalyticsCreativeData, -} from "@/lib/types/api"; - -// ============================================================================ -// Mock Analytics Data -// ============================================================================ - -export const mockAnalyticsOverview: AnalyticsOverview = { - totalPurchases: 25, - purchasesChange: 5, - totalFollowers: 2500, - followersChange: 320, - averageCpf: 50.0, - cpfChange: -5.2, - averageCpm: 450.0, - cpmChange: -3.8, - topChannelsByCpf: [ - { - channel_id: "ec3", - channel_name: "IT и Технологии", - total_purchases: 3, - avg_cpf: 38.2, - }, - { - channel_id: "ec1", - channel_name: "Канал о Маркетинге", - total_purchases: 5, - avg_cpf: 40.0, - }, - { - channel_id: "ec5", - channel_name: "Продажи и Переговоры", - total_purchases: 4, - avg_cpf: 45.0, - }, - { - channel_id: "ec2", - channel_name: "Бизнес и Стартапы", - total_purchases: 8, - avg_cpf: 55.8, - }, - { - channel_id: "ec4", - channel_name: "Крипто Инвестиции", - total_purchases: 2, - avg_cpf: 68.5, - }, - ], - topCreativesByCpf: [ - { - creative_id: "cr1", - creative_name: 'Креатив "Присоединяйся"', - total_subscriptions: 1180, - avg_cpf: 42.4, - }, - { - creative_id: "cr2", - creative_name: 'Креатив "Эксклюзив"', - total_subscriptions: 577, - avg_cpf: 48.5, - }, - { - creative_id: "cr3", - creative_name: 'Креатив "Обучение"', - total_subscriptions: 803, - avg_cpf: 52.3, - }, - ], -}; - -// Mock data for costs report -export const mockCostsReportByDay: CostsReport = { - totalCost: 76500, - averageCost: 7650, - periods: [ - { - date: "2024-02-20", - total_cost: 5000, - purchases_count: 1, - avg_cost: 5000, - }, - { - date: "2024-02-22", - total_cost: 3500, - purchases_count: 1, - avg_cost: 3500, - }, - { - date: "2024-02-25", - total_cost: 7000, - purchases_count: 2, - avg_cost: 3500, - }, - { - date: "2024-02-28", - total_cost: 4500, - purchases_count: 1, - avg_cost: 4500, - }, - { - date: "2024-03-02", - total_cost: 8500, - purchases_count: 2, - avg_cost: 4250, - }, - { - date: "2024-03-05", - total_cost: 6000, - purchases_count: 1, - avg_cost: 6000, - }, - { - date: "2024-03-08", - total_cost: 12000, - purchases_count: 3, - avg_cost: 4000, - }, - { - date: "2024-03-12", - total_cost: 5500, - purchases_count: 1, - avg_cost: 5500, - }, - { - date: "2024-03-15", - total_cost: 15000, - purchases_count: 3, - avg_cost: 5000, - }, - { - date: "2024-03-18", - total_cost: 9500, - purchases_count: 2, - avg_cost: 4750, - }, - ], -}; - -export const mockCostsReportByWeek: CostsReport = { - totalCost: 125000, - averageCost: 15625, - periods: [ - { - date: "2024-02-19", - total_cost: 15500, - purchases_count: 4, - avg_cost: 3875, - }, - { - date: "2024-02-26", - total_cost: 13000, - purchases_count: 3, - avg_cost: 4333, - }, - { - date: "2024-03-04", - total_cost: 26500, - purchases_count: 6, - avg_cost: 4417, - }, - { - date: "2024-03-11", - total_cost: 33000, - purchases_count: 7, - avg_cost: 4714, - }, - { - date: "2024-03-18", - total_cost: 24500, - purchases_count: 5, - avg_cost: 4900, - }, - ], -}; - -export const mockCostsReportByMonth: CostsReport = { - totalCost: 125000, - averageCost: 62500, - periods: [ - { - date: "2024-02-01", - total_cost: 20000, - purchases_count: 5, - avg_cost: 4000, - }, - { - date: "2024-03-01", - total_cost: 105000, - purchases_count: 20, - avg_cost: 5250, - }, - ], -}; - -// Mock data for channels analytics -export const mockAnalyticsChannelsData: AnalyticsChannelData[] = [ - { - channel_id: "ec1", - channel_name: "Канал о Маркетинге", - purchases_count: 5, - total_cost: 25000, - total_subscriptions: 625, - avg_cpf: 40.0, - avg_cpm: 380.0, - }, - { - channel_id: "ec2", - channel_name: "Бизнес и Стартапы", - purchases_count: 8, - total_cost: 48000, - total_subscriptions: 860, - avg_cpf: 55.8, - avg_cpm: 450.0, - }, - { - channel_id: "ec3", - channel_name: "IT и Технологии", - purchases_count: 3, - total_cost: 12000, - total_subscriptions: 314, - avg_cpf: 38.2, - avg_cpm: 360.0, - }, - { - channel_id: "ec4", - channel_name: "Крипто Инвестиции", - purchases_count: 2, - total_cost: 18000, - total_subscriptions: 263, - avg_cpf: 68.5, - avg_cpm: 520.0, - }, - { - channel_id: "ec5", - channel_name: "Продажи и Переговоры", - purchases_count: 4, - total_cost: 16000, - total_subscriptions: 356, - avg_cpf: 45.0, - avg_cpm: 410.0, - }, -]; - -// Mock data for creatives analytics -export const mockAnalyticsCreativesData: AnalyticsCreativeData[] = [ - { - creative_id: "cr1", - creative_name: 'Креатив "Присоединяйся"', - purchases_count: 10, - total_cost: 50000, - total_subscriptions: 1180, - avg_cpf: 42.4, - conversion_rate: 0.95, - }, - { - creative_id: "cr2", - creative_name: 'Креатив "Эксклюзив"', - purchases_count: 5, - total_cost: 28000, - total_subscriptions: 577, - avg_cpf: 48.5, - conversion_rate: 0.88, - }, - { - creative_id: "cr3", - creative_name: 'Креатив "Обучение"', - purchases_count: 8, - total_cost: 42000, - total_subscriptions: 803, - avg_cpf: 52.3, - conversion_rate: 0.92, - }, -]; diff --git a/lib/mocks/handlers.ts b/lib/mocks/handlers.ts index 2277b7b..04e2e07 100644 --- a/lib/mocks/handlers.ts +++ b/lib/mocks/handlers.ts @@ -11,12 +11,6 @@ import { mockPlacements, mockSubscriptions, mockViewsHistory, - mockAnalyticsOverview, - mockCostsReportByDay, - mockCostsReportByWeek, - mockCostsReportByMonth, - mockAnalyticsChannelsData, - mockAnalyticsCreativesData, } from "./index"; import type { @@ -481,50 +475,99 @@ export const mockApiHandlers = { // -------------------------------------------------------------------------- // Analytics // -------------------------------------------------------------------------- - async getAnalyticsOverview(params?: { + async getSpendingAnalytics(params?: { target_channel_id?: string; date_from?: string; date_to?: string; - }) { - await delay(); - return mockAnalyticsOverview; - }, - - async getAnalyticsCosts(params?: { - period?: "day" | "week" | "month"; - target_channel_id?: string; - }) { - await delay(); - const period = params?.period || "week"; - - if (period === "day") { - return mockCostsReportByDay; - } else if (period === "month") { - return mockCostsReportByMonth; - } - return mockCostsReportByWeek; - }, - - async getAnalyticsChannels(params?: { - target_channel_id?: string; - date_from?: string; - date_to?: string; - type?: "external" | "target"; + grouping?: "day" | "week" | "month" | "quarter" | "year"; }) { await delay(); return { - data: mockAnalyticsChannelsData, + total_cost: 45000, + total_subscriptions: 850, + total_views: 125000, + avg_cpf: 52.94, + avg_cpm: 360, + chart_data: [ + { + period: "2024-03-01", + cost: 15000, + subscriptions: 280, + views: 42000, + cpf: 53.57, + cpm: 357.14, + }, + { + period: "2024-03-08", + cost: 18000, + subscriptions: 340, + views: 51000, + cpf: 52.94, + cpm: 352.94, + }, + { + period: "2024-03-15", + cost: 12000, + subscriptions: 230, + views: 32000, + cpf: 52.17, + cpm: 375, + }, + ], }; }, - async getAnalyticsCreatives(params?: { - target_channel_id?: string; - date_from?: string; - date_to?: string; - }) { + async getPlacementsAnalytics(params?: { target_channel_id?: string }) { await delay(); return { - data: mockAnalyticsCreativesData, + placements: mockPlacements.map((p) => ({ + id: p.id, + target_channel_id: p.target_channel_id, + target_channel_title: p.target_channel_title, + external_channel_id: p.external_channel_id, + external_channel_title: p.external_channel_title, + creative_id: p.creative_id, + creative_name: p.creative_name, + placement_date: p.placement_date, + cost: p.cost, + subscriptions_count: p.subscriptions_count, + views_count: p.views_count, + cpf: p.cost && p.subscriptions_count > 0 ? p.cost / p.subscriptions_count : null, + cpm: p.cost && p.views_count ? (p.cost / p.views_count) * 1000 : null, + })), + }; + }, + + async getCreativesAnalytics(params?: { target_channel_id?: string }) { + await delay(); + return { + creatives: mockCreatives.map((c) => ({ + id: c.id, + name: c.name, + placements_count: c.placements_count, + total_cost: 15000, + total_subscriptions: 280, + total_views: 42000, + avg_cpf: 53.57, + avg_cpm: 357.14, + })), + }; + }, + + async getExternalChannelsAnalytics(params?: { target_channel_id?: string }) { + await delay(); + return { + external_channels: mockExternalChannels.map((ec) => ({ + id: ec.id, + title: ec.title, + username: ec.username, + placements_count: 3, + total_cost: 12000, + total_subscriptions: 210, + total_views: 28000, + avg_cpf: 57.14, + avg_cpm: 428.57, + })), }; }, }; diff --git a/lib/mocks/index.ts b/lib/mocks/index.ts index e4c5366..872cf97 100644 --- a/lib/mocks/index.ts +++ b/lib/mocks/index.ts @@ -7,4 +7,3 @@ export * from "./data/channels"; export * from "./data/external-channels"; export * from "./data/creatives"; export * from "./data/placements"; -export * from "./data/analytics"; diff --git a/lib/types/api.ts b/lib/types/api.ts index 1c874ad..b7e34c7 100644 --- a/lib/types/api.ts +++ b/lib/types/api.ts @@ -238,79 +238,79 @@ export interface ViewsSnapshot { // Analytics // ---------------------------------------------------------------------------- -export interface AnalyticsOverview { - totalPurchases: number; - purchasesChange: number; - totalFollowers: number; - followersChange: number; - averageCpf: number; - cpfChange: number; - averageCpm: number; - cpmChange: number; - topChannelsByCpf: { - channel_id: string; - channel_name: string; - total_purchases: number; - avg_cpf: number; - }[]; - topCreativesByCpf: { - creative_id: string; - creative_name: string; - total_subscriptions: number; - avg_cpf: number; - }[]; -} +export type DateGrouping = "day" | "week" | "month" | "quarter" | "year"; -export type GroupByPeriod = "day" | "week" | "month" | "quarter" | "year"; - -export interface AnalyticsCostsDataPoint { - period: string; // ISO 8601 date (start of period) +// Spending Analytics +export interface SpendingDataPoint { + period: string; cost: number; - purchases_count: number; subscriptions: number; + views: number; + cpf: number | null; + cpm: number | null; } -export interface AnalyticsCostsResponse { - data: AnalyticsCostsDataPoint[]; -} - -export interface CostsReport { - totalCost: number; - averageCost: number; - periods: { - date: string; - total_cost: number; - purchases_count: number; - avg_cost: number; - }[]; -} - -export interface AnalyticsChannelData { - channel_id: string; - channel_name: string; - purchases_count: number; +export interface SpendingAnalytics { total_cost: number; total_subscriptions: number; - avg_cpf: number; - avg_cpm: number; + total_views: number; + avg_cpf: number | null; + avg_cpm: number | null; + chart_data: SpendingDataPoint[]; } -export interface AnalyticsChannelsResponse { - data: AnalyticsChannelData[]; -} - -export interface AnalyticsCreativeData { +// Placements Analytics +export interface PlacementAnalytics { + id: string; + target_channel_id: string; + target_channel_title: string; + external_channel_id: string; + external_channel_title: string; creative_id: string; creative_name: string; - purchases_count: number; - total_cost: number; - total_subscriptions: number; - avg_cpf: number; - conversion_rate: number; + placement_date: string; + cost: number | null; + subscriptions_count: number; + views_count: number | null; + cpf: number | null; + cpm: number | null; } -export interface AnalyticsCreativesResponse { - data: AnalyticsCreativeData[]; +export interface PlacementsAnalytics { + placements: PlacementAnalytics[]; +} + +// Creatives Analytics +export interface CreativeAnalytics { + id: string; + name: string; + placements_count: number; + total_cost: number; + total_subscriptions: number; + total_views: number; + avg_cpf: number | null; + avg_cpm: number | null; +} + +export interface CreativesAnalytics { + creatives: CreativeAnalytics[]; +} + +// External Channels Analytics +export interface ExternalChannelAnalytics { + id: string; + title: string; + username: string | null; + placements_count: number; + total_cost: number; + total_subscriptions: number; + total_views: number; + avg_cpf: number | null; + avg_cpm: number | null; +} + +export interface ExternalChannelsAnalytics { + external_channels: ExternalChannelAnalytics[]; } // ---------------------------------------------------------------------------- @@ -369,22 +369,3 @@ export interface AnalyticsOverviewQueryParams { date_to?: string; } -export interface AnalyticsCostsQueryParams { - target_channel_id?: string; - date_from: string; // required - date_to: string; // required - group_by: GroupByPeriod; -} - -export interface AnalyticsChannelsQueryParams { - target_channel_id?: string; - date_from?: string; - date_to?: string; - type: "external" | "target"; -} - -export interface AnalyticsCreativesQueryParams { - target_channel_id?: string; - date_from?: string; - date_to?: string; -} diff --git a/lib/utils/constants.ts b/lib/utils/constants.ts index 9c96d3c..09898fe 100644 --- a/lib/utils/constants.ts +++ b/lib/utils/constants.ts @@ -14,6 +14,7 @@ export const STORAGE_KEYS = { ACCESS_TOKEN: "tgex_access_token", REFRESH_TOKEN: "tgex_refresh_token", USER: "tgex_user", + SELECTED_TARGET_CHANNEL: "tgex_selected_target_channel", } as const; // API Endpoints