diff --git a/api-docs.html b/api-docs.html new file mode 100644 index 00000000..d8eff4a6 --- /dev/null +++ b/api-docs.html @@ -0,0 +1,577 @@ + + + + + + QUIZ Service API Documentation + + + +
+
+

QUIZ Service API Documentation

+

Version 1.0.0

+
+
+ + + +
+
+

Components

+ +
+

Quiz Model

+
+
{
+  "id": integer,          // Id of created quiz
+  "qid": string,         // string id for customers
+  "deleted": boolean,    // true if quiz deleted
+  "archived": boolean,   // true if quiz archived
+  "fingerprinting": boolean,  // set true for save deviceId
+  "repeatable": boolean,     // set true for allow user to repeat quiz
+  "note_prevented": boolean, // set true for save statistic of incomplete quiz passing
+  "mail_notifications": boolean, // set true for mail notification for each quiz passing
+  "unique_answers": boolean,    // set true for save statistics only for unique quiz passing
+  "name": string,        // name of quiz. max 280 length
+  "description": string, // description of quiz
+  "config": string,      // config of quiz. serialized json for rules of quiz flow
+  "status": string,      // status of quiz. allow only '', 'draft', 'template', 'stop', 'start'
+  "limit": integer,      // limit is count of max quiz passing
+  "due_to": integer,     // last time when quiz is valid. timestamp in seconds
+  "time_of_passing": integer, // seconds to pass quiz
+  "pausable": boolean,   // true if it is allowed for pause quiz
+  "version": integer,    // version of quiz
+  "version_comment": string, // version comment to version of quiz
+  "parent_ids": integer[], // array of previous versions of quiz
+  "created_at": string,  // time of creating
+  "updated_at": string,  // time of last updating
+  "question_cnt": integer, // count of questions
+  "passed_count": integer, // count passings
+  "average_time": integer, // average time of passing
+  "super": boolean,      // set true if squiz realize group functionality
+  "group_id": integer    // group of new quiz
+}
+
+
+ +
+

Question Model

+
+
{
+  "id": integer,         // Id of created question
+  "quiz_id": integer,    // relation to quiz
+  "title": string,       // title of question. max 512 length
+  "description": string, // description of question
+  "type": string,        // status of question. allow only text, select, file, variant, images, varimg, emoji, date, number, page, rating
+  "required": boolean,   // user must pass this question
+  "deleted": boolean,    // true if question is deleted
+  "page": integer,       // page if question
+  "content": string,     // serialized json of created question
+  "version": integer,    // version of quiz
+  "parent_ids": integer[], // array of previous versions of quiz
+  "created_at": string,  // time of creating
+  "updated_at": string   // time of last updating
+}
+
+
+ +
+

Answer Model

+
+
{
+  "Id": integer,         // id ответа
+  "Content": string,     // контент ответа
+  "QuestionId": integer, // id вопроса к которому ответ
+  "QuizId": integer,     // id опроса к которому ответ
+  "Fingerprint": string, // fingerprint
+  "Session": string,     // сессия
+  "Result": boolean,     // true or false?
+  "CreatedAt": string,   // таймшап когда ответ создан
+  "New": boolean,        // новый ответ?
+  "Deleted": boolean     // удален?
+}
+
+
+ +
+

LeadTarget Model

+
+
{
+  "ID": integer,         // primary key
+  "AccountID": string,   // account identifier
+  "Type": string,        // type of target (mail, telegram, whatsapp)
+  "QuizID": integer,     // ID of the quiz
+  "Target": string,      // target address
+  "InviteLink": string,  // invitation link
+  "Deleted": boolean,    // is deleted
+  "CreatedAt": string    // creation timestamp
+}
+
+
+ +
+

TgAccount Model

+
+
{
+  "ID": integer,         // primary key
+  "ApiID": integer,      // Telegram API ID
+  "ApiHash": string,     // Telegram API Hash
+  "PhoneNumber": string, // phone number
+  "Password": string,    // account password
+  "Status": string,      // account status (active, inactive, ban)
+  "Deleted": boolean,    // is deleted
+  "CreatedAt": string    // creation timestamp
+}
+
+
+
+ +
+

Quiz Endpoints

+ +
+

Create Quiz

+ POST + /quiz/create +

Create a new quiz with specified parameters.

+ +
+

Security

+

This endpoint requires authentication.

+
+ +

Request Body:

+
+
{
+  "fingerprinting": boolean,  // set true for save deviceId
+  "repeatable": boolean,     // set true for allow user to repeat quiz
+  "note_prevented": boolean, // set true for save statistic of incomplete quiz passing
+  "mail_notifications": boolean, // set true for mail notification for each quiz passing
+  "unique_answers": boolean,    // set true for save statistics only for unique quiz passing
+  "name": string,        // name of quiz. max 280 length
+  "description": string, // description of quiz
+  "config": string,      // config of quiz. serialized json for rules of quiz flow
+  "status": string,      // status of quiz. allow only '', 'draft', 'template', 'stop', 'start'
+  "limit": integer,      // limit is count of max quiz passing
+  "due_to": integer,     // last time when quiz is valid. timestamp in seconds
+  "time_of_passing": integer, // seconds to pass quiz
+  "pausable": boolean,   // true if it is allowed for pause quiz
+  "question_cnt": integer, // count of questions
+  "super": boolean,      // set true if squiz realize group functionality
+  "group_id": integer    // group of new quiz
+}
+
+ +

Responses:

+
+
201 Created
+

Quiz successfully created. Returns the created quiz object.

+
+
{
+  "id": integer,
+  "qid": string,
+  "name": string,
+  "description": string,
+  // ... other quiz properties
+}
+
+
+ +
+
422 Unprocessable Entity
+

Name field should have less than 280 characters.

+
+ +
+
406 Not Acceptable
+

Status on creating must be only draft, template, stop, start or due to time must be lesser than now.

+
+ Allowed status values: '', 'draft', 'template', 'stop', 'start' +
+
+ +
+
409 Conflict
+

You can pause quiz only if it has deadline for passing.

+
+ +
+
500 Internal Server Error
+

If you get any content string send it to developer.

+
+
+ +
+

Get Quiz List

+ POST + /quiz/getList +

Get paginated list of quizzes with filtering options.

+ +

Request Body:

+
+
{
+  "limit": integer,
+  "offset": integer,
+  "from": integer,
+  "to": integer,
+  "search": string,
+  "status": string,
+  "deleted": boolean,
+  "archived": boolean,
+  "super": boolean,
+  "group_id": integer
+}
+
+ +

Responses:

+
+
200 OK
+

Returns list of quizzes with total count.

+
+
{
+  "count": integer,
+  "items": [
+    {
+      "id": integer,
+      "qid": string,
+      // ... other quiz properties
+    }
+  ]
+}
+
+
+ +
+
406 Not Acceptable
+

Inappropriate status, allowed only '', 'stop', 'start', 'draft', 'template', 'timeout', 'offlimit'.

+
+ +
+
500 Internal Server Error
+

If you get any content string send it to developer.

+
+
+ + +
+ +
+

Question Endpoints

+ +
+

Create Question

+ POST + /question/create +

Create a new question for a quiz.

+
+ + +
+ +
+

Results Endpoints

+ +
+

Get Quiz Results

+ POST + /results/getResults/{quizId} +

Get list of quiz results with pagination.

+ +

Path Parameters:

+
+ quizId - ID of the quiz to get results for +
+ +

Request Body:

+
+
{
+  "to": integer,         // таймштамп времени, до которого выбирать статистику. если 0 или не передано - этого ограничения нет
+  "from": integer,       // таймштамп времени, после которого выбирать статистику. если 0 или не передано - этого ограничения нет
+  "new": boolean,        // флаг, по которому вернутся только новые результаты, ещё не просмотренные пользователем
+  "page": integer,       // номер страницы для пагинации
+  "limit": integer       // размер страницы для пагинации
+}
+
+ +

Responses:

+
+
200 OK
+

Returns paginated list of results.

+
+
{
+  "total_count": integer,  // общее количество элементов удволетворяющее фильтру
+  "results": [
+    {
+      "content": string,   // содержимое ответа
+      "id": integer,       // айдишник ответа
+      "new": boolean,      // статус, был ли просмотрен ответ
+      "created_at": string // время создания этого результата
+    }
+  ]
+}
+
+
+
+ +
+

Export Results

+ POST + /results/{quizID}/export +

Export quiz results to CSV format.

+ +

Path Parameters:

+
+ quizID - ID of the quiz to export results from +
+ +

Request Body:

+
+
{
+  "to": string,          // Дата окончания диапазона времени для экспорта результатов
+  "from": string,        // Дата начала временного диапазона для экспорта результатов
+  "new": boolean         // Экспортировать ли только новые результаты?
+}
+
+ +

Responses:

+
+
200 OK
+

Returns CSV file with quiz results.

+
+
Content-Type: text/csv
+
+
+
+
+ +
+

Telegram Endpoints

+ +
+

Create Telegram Account

+ POST + /telegram/create +

Authorize server in Telegram account.

+ +

Request Body:

+
+
{
+  "ApiID": integer,      // Telegram API ID
+  "ApiHash": string,     // Telegram API Hash
+  "PhoneNumber": string, // Phone number
+  "Password": string     // Account password
+}
+
+ +

Responses:

+
+
200 OK
+

Returns signature for code verification.

+
+
{
+  "signature": string    // Session identifier for code verification
+}
+
+
+ +
+
409 Conflict
+

Account already exists and is active.

+
+
+
+ +
+

Audience Endpoints

+ +
+

Create Quiz Audience

+ POST + /quiz/{quizID}/auditory +

Create audience for a quiz.

+ +

Path Parameters:

+
+ quizID - ID of the quiz +
+ +

Responses:

+
+
200 OK
+

Returns ID of created audience.

+
+
{
+  "id": integer    // ID of created auditory
+}
+
+
+
+
+ +
+

Statistics Endpoints

+ +
+

Get Question Statistics

+ POST + /statistic/{quizID}/questions +

Get statistics for specific questions in a quiz.

+
+ + +
+ +
+

Account Endpoints

+ +
+

Add Lead Target

+ POST + /account/leadtarget +

Add a target destination for lead notifications.

+
+ + +
+
+ + + + \ No newline at end of file diff --git a/src/api/auditory.ts b/src/api/auditory.ts new file mode 100644 index 00000000..77a2a705 --- /dev/null +++ b/src/api/auditory.ts @@ -0,0 +1,106 @@ +import { makeRequest } from "@frontend/kitui"; +import { parseAxiosError } from "@utils/parse-error"; + +const API_URL = `${process.env.REACT_APP_DOMAIN}/squiz`; + +// Types +export interface AuditoryItem { + id: number; + quiz_id: number; + sex: boolean; + age: string; + deleted: boolean; + created_at: number; +} + +export interface AuditoryResponse { + data: AuditoryItem[]; +} + +// Request Types +export interface AuditoryGetRequest { + quizId: number; +} + +export interface AuditoryDeleteRequest { + id: number; +} + +export interface AuditoryAddRequest { + sex: boolean; + age: string; +} + +// Parameters +export interface AuditoryGetParams { + quizId: number; +} + +export interface AuditoryDeleteParams { + quizId: number; + auditoryId: number; +} + +export interface AuditoryAddParams { + quizId: number; + body: AuditoryAddRequest; +} + +// API calls +export const auditoryGet = async ({ quizId }: AuditoryGetParams): Promise<[AuditoryResponse | null, string?]> => { + if (!quizId) { + return [null, "Quiz ID is required"]; + } + + try { + const response = await makeRequest({ + url: `${API_URL}/quiz/${quizId}/auditory`, + method: "GET", + }); + + return [response]; + } catch (nativeError) { + const [error] = parseAxiosError(nativeError); + return [null, `Не удалось получить аудиторию. ${error}`]; + } +}; + +export const auditoryDelete = async ({ quizId, auditoryId }: AuditoryDeleteParams): Promise<[AuditoryResponse | null, string?]> => { + if (!quizId || !auditoryId) { + return [null, "Quiz ID and Auditory ID are required"]; + } + + try { + const response = await makeRequest({ + url: `${API_URL}/quiz/${quizId}/auditory`, + body: { + id: auditoryId + }, + method: "DELETE", + }); + + return [response]; + } catch (nativeError) { + const [error] = parseAxiosError(nativeError); + return [null, `Не удалось удалить аудиторию. ${error}`]; + } +}; + +export const auditoryAdd = async ({ quizId, body }: AuditoryAddParams): Promise<[AuditoryResponse | null, string?]> => { + if (!quizId) { + return [null, "Quiz ID is required"]; + } + + try { + const response = await makeRequest({ + url: `${API_URL}/quiz/${quizId}/auditory`, + body, + method: "POST", + }); + + return [response]; + } catch (nativeError) { + const [error] = parseAxiosError(nativeError); + return [null, `Не удалось добавить аудиторию. ${error}`]; + } +}; \ No newline at end of file diff --git a/src/api/quiz.ts b/src/api/quiz.ts index f43c3f06..1bcea534 100644 --- a/src/api/quiz.ts +++ b/src/api/quiz.ts @@ -148,8 +148,8 @@ export const addQuizImages = async ( const name = image?.name ? transliterate(image?.name.replace(/\s/g, '_')) : "blob" //Замена всех побелов на _ - const renamedImage = new File([image], name) - + const renamedImage = new File([image], name) + formData.append("quiz", quizId.toString()); formData.append("image", renamedImage); diff --git a/src/pages/PersonalizationAI/AuditoryList.tsx b/src/pages/PersonalizationAI/AuditoryList.tsx new file mode 100644 index 00000000..b9e26d49 --- /dev/null +++ b/src/pages/PersonalizationAI/AuditoryList.tsx @@ -0,0 +1,114 @@ +import { auditoryGet, AuditoryResponse, AuditoryItem } from "@/api/auditory"; +import ArrowDownIcon from "@/assets/icons/ArrowDownIcon"; +import CopyIcon from "@/assets/icons/CopyIcon"; +import { useCurrentQuiz } from "@/stores/quizes/hooks"; +import { InfoPopover } from "@/ui_kit/InfoPopover"; +import { useDomainDefine } from "@/utils/hooks/useDomainDefine"; +import { Box, Collapse, IconButton, List, ListItem, Typography, useTheme } from "@mui/material"; +import { useEffect, useState } from "react"; + +const PURPLE = "#7E2AEA"; + +export const AuditoryList = () => { + const theme = useTheme(); + const quiz = useCurrentQuiz(); + const { isTestServer } = useDomainDefine(); + const [linksOpen, setLinksOpen] = useState(true); + const [auditory, setAuditory] = useState([]); + + const handleCopy = (text: string) => { + navigator.clipboard.writeText(text); + }; + + useEffect(() => { + (async () => { + if (quiz?.backendId) { + const [result, error] = await auditoryGet({ quizId: quiz.backendId }); + console.log("result-___---_------__---__-__---_------__---__-__---_------__---__-__---_------__---__-____--__") + console.log(result) + if (result) { + setAuditory(result); + } + } + })(); + }, [quiz]); + + console.log("auditory-___---_auditory__---__-__auditory_------__---__-__---_------__---__-__---_------__---__-____--__") + console.log(auditory) + + return ( + <> + + + + Ваши сохраненные ссылки + + setLinksOpen((prev) => !prev)} + size="large" + > + + + + + + {auditory.map((item, idx) => { + const linkText = `${isTestServer ? "https://s.hbpn.link/" : "https://hbpn.link/"}?_paud=${item.id}`; + console.log(item) + return ( + + + + + handleCopy(linkText)} + > + + + + } + > + + {linkText} + + + ); + })} + + + + + ); +}; \ No newline at end of file diff --git a/src/pages/PersonalizationAI/PersonalizationAI.tsx b/src/pages/PersonalizationAI/PersonalizationAI.tsx index 80331c4a..bd7fc388 100644 --- a/src/pages/PersonalizationAI/PersonalizationAI.tsx +++ b/src/pages/PersonalizationAI/PersonalizationAI.tsx @@ -2,24 +2,20 @@ import { Box, Container, Typography, TextField, Button, List, ListItem, IconButt import { InfoPopover } from '@ui_kit/InfoPopover'; import CopyIcon from "@/assets/icons/CopyIcon"; import GenderAndAgeSelector from "./GenderAndAgeSelector"; -import { useState } from "react"; +import { useEffect, useState } from "react"; import CustomTextField from "@ui_kit/CustomTextField"; import Collapse from '@mui/material/Collapse'; import { ArrowDownIcon } from "../../assets/icons/questionsPage/ArrowDownIcon"; import { useTheme } from "@mui/material"; +import { auditoryAdd, auditoryGet } from "@/api/auditory"; +import { useCurrentQuiz } from "@/stores/quizes/hooks"; +import { AuditoryList } from "./AuditoryList"; const PURPLE = "#7E2AEA"; -const GREY_TEXT = "#A0A0A0"; -const GREY_BORDER = "#E0E0E0"; -const GREY_ICON = "#B0B0B0"; -const BLOCK_RADIUS = "16px"; -const BLOCK_PX = "32px"; -const BLOCK_PY = "24px"; export default function PersonalizationAI() { - const [gender, setGender] = useState(''); - const [linksOpen, setLinksOpen] = useState(true); const theme = useTheme(); + const [gender, setGender] = useState(''); return ( @@ -47,9 +43,6 @@ export default function PersonalizationAI() { }}> - - - {/* Ссылка */} @@ -95,73 +88,8 @@ export default function PersonalizationAI() { - {/* Второй белый блок */} - - - - Ваши сохраненные ссылки - - setLinksOpen((prev) => !prev)} - size="large" - > - - - - - - {[1, 2, 3, 4, 5].map((_, idx) => ( - - - - - + - - - - } - > - - linkexample.ru - - - ))} - - - ); }