operators-applicants-workspace

Operators applicants — Work space

src/features/hiring_workspace/ · (UI-екран) — єдине frontend-джерело вкладки Work space для HR і recruiter.

Суть

Вкладка об’єднує задачі, історію діалогів, live chat і картку кандидата. HR може змінювати робочий контекст, recruiter працює тільки у власному контексті. Компоненти, state transitions і socket handlers спільні; рольові відмінності задаються конфігурацією.

Use cases

  • HR працює від свого імені, вибирає recruiter або переглядає задачі й діалоги всієї команди.
  • Recruiter обробляє власні задачі, діалоги та картки кандидатів.
  • Користувач веде live chat, додає вкладення або voice message та планує reminder.
  • Користувач переглядає й редагує доступні поля картки кандидата, зберігаючи незавершені зміни перед переходом до іншого діалогу.

Доступ і маршрути

РольМаршрутТочка підключення
HR/hr/operators_applicants/workspacesrc/components/hr_manager/Index.js
Recruiter/recruiter/operators_applicants/workspacesrc/components/recruiter/Index.tsx

Layout

Робочий простір складається з трьох колонок:

  1. Tasks + History — активні/завершені задачі, пошук і фільтрація діалогів.
  2. Live chat — повідомлення, media attachments, voice messages і scheduled reminder.
  3. Applicant information — загальні дані, hiring, cooperation та коментарі кандидата.

Ролі та scope

ПоведінкаHRRecruiter
Працювати від свого іменітактак
Вибрати іншого recruiterтакні
Вибрати всю командутакні
Відкрити кандидата іншого recruiterзалежить від вибраного viewerні
Перевірка ownership кандидатане застосовуєтьсяобов’язкова

Рольові відмінності задаються в src/features/hiring_workspace/config.ts. Окремих HR/recruiter копій workspace немає.

URL recruiter перевіряється проти авторизованого користувача. Якщо recruiter намагається відкрити чужого кандидата, workspace очищає активний чат і повертає URL до дозволеного viewer.

Tasks

Активна задача прив’язана до кандидата й може мати тип:

  • new_card — нова картка;
  • unanswered — повідомлення без відповіді;
  • question — окрема question-сесія;
  • reminder — заплановане нагадування.

Перемикач Tasks має вкладки Active і Completed. Таймер активної задачі оновлюється раз на секунду, після завершення показує 0 sec. Активну задачу можна скасувати вручну через trash action. Question task залишається в Tasks до завершення й одночасно не додається в History.

Completed tasks групуються за кандидатом. Відкриття completed entry переводить чат у режим перегляду повідомлень закритої задачі.

History і пошук

Стартовий статус History — In progress. Status menu не дублює поточний статус і не пропонує New, але додає All та Scheduled. History фільтрується за статусом або date range. Текстовий пошук використовує окремий search flow і тимчасово робить status selector read-only.

Pagination:

  • розмір сторінки — 30;
  • наступна сторінка завантажується, коли scroll наближається до кінця контейнера на 10 px;
  • lockCursorLogic зупиняє подальші запити, коли відповідь містить менше ніж 30 елементів або запит завершився помилкою;
  • нові сторінки об’єднуються з поточним списком і повторно сортуються за lastMessageAt від новіших до старіших.

Відкриття діалогу та URL

Workspace зберігає контекст у query parameters:

  • hireId — відкритий кандидат;
  • viewerRolehr, recruiter або all;
  • viewerId — користувач, від імені якого відкрито workspace; для all відсутній.

Зміни параметрів виконуються через history replace, щоб браузерна кнопка Back не проходила через кожне внутрішнє перемикання кандидата.

При старті URL обробляється після готовності socket connection: спочатку перевіряється viewer, потім за потреби змінюється socket room, після цього відкривається кандидат.

Live chat

Перед завантаженням повідомлень workspace отримує user info та backend-прапор flagCanOpenMessages. Без цього дозволу messages endpoint не викликається.

Messages завантажуються по 30. Старіша сторінка запитується при наближенні scroll до початку контейнера; коротка відповідь або помилка блокує подальшу пагінацію.

Header live chat показує доступну дію відповідно до стану кандидата:

  • для unassigned кандидата зі статусом NewAssign to me;
  • для question candidate без активного hiring — призначення question task;
  • для активної question task — Close task session.

HR не може виконати assignment, коли workspace відкритий у scope іншого recruiter. Footer прихований під час перегляду closed task або коли flagCanSendMessages дорівнює false; без відкритого writable діалогу він працює в read-only режимі.

Повідомлення підтримують:

  • звичайний текст;
  • URL та text_link entities;
  • photo, video, document і audio attachments;
  • voice message;
  • scheduled reminder.

Telegram entities рахують offsets по LF-тексту. Renderer коригує offsets для CRLF, щоб links не зміщувалися у веб-клієнті.

Відображення, fullscreen, playback та відкриття/завантаження message attachments делеговано Shared media renderer. Workspace визначає склад галереї поточного повідомлення і початковий вибраний файл.

У composer локальні вкладення мають preview і можуть бути видалені до відправлення:

  • за один вибір можна додати не більше 5 файлів;
  • image-файл не може перевищувати 10 MB;
  • інший файл не може перевищувати 50 MB;
  • один файл понад ліміт відхиляє весь поточний вибір;
  • новий вибір local files, gallery media або voice замінює попередній attachment draft, а не накопичується поверх нього.

Media gallery у footer завантажує глобальну бібліотеку approved hiring media, а не media вибраного TU. Вона має незалежні фільтри Photo, Video, PDF, Docs, cursor-пагінацію по 20, віртуалізований список і fullscreen preview. Порожній набір фільтрів нормалізується до всіх типів. До draft можна додати від 1 до 5 наявних media IDs без повторного upload.

Scheduled reminder доступний лише для відкритої картки з writable footer. Минула дата або час не приймаються; існуючий reminder можна змінити або видалити.

Voice messages

  1. Доступ до мікрофона запитується тільки після натискання mic.
  2. Максимальна тривалість одного запису — 15 хвилин.
  3. Якщо браузер записав OGG/Opus, blob відправляється без конвертації.
  4. Якщо браузер записав WebM/Opus, модуль FFmpeg/WASM ліниво завантажується тільки під час фактичної відправки.
  5. Максимальний розмір WebM для клієнтської конвертації — 50 MB.
  6. Тимчасові файли FFmpeg видаляються після успіху або помилки.

FFmpeg навмисно не preload-иться при mount footer: користувач, який не надсилає WebM voice message, не витрачає мережу, CPU та пам’ять на конвертер.

Applicant information

Картка складається з блоків:

  • General info — ім’я, прізвище, дата народження, країна, контакти, паспорт і документи;
  • Hiring — source, recruiter і статус;
  • Cooperation — Family, team lead і стан cooperation;
  • Comments — текстові коментарі та attachments.

Frontend перевіряє валідність email і мінімальний вік 18 років.

Доступ до картки визначають backend-прапори flagCanOpenCard і flagCanEditCard. Якщо картку не можна відкрити, замість форми показується повідомлення про недоступність; у read-only режимі кнопка UPDATE та edit actions приховані.

Passport scans, documents і comment attachments використовують Shared media renderer як для вже збережених, так і для щойно вибраних локальних файлів. Кожне поле passport/documents може містити максимум 3 файли разом зі збереженими й pending. Один файл у General info або comments не може перевищувати 20 MB.

Дозволені формати:

  • passport — JPEG/JPG/PNG;
  • documents — JPEG/JPG/PNG, TXT, PDF, MP4.

Один comment draft може містити максимум 5 attachments. Файли понад ліміт розміру пропускаються, а прийнятні файли залишаються у draft; загальна кількість attachments у draft не може перевищити п’ять.

Comment потребує непорожнього тексту: самі attachments не активують додавання. Нові pending attachments можна видаляти окремо. Збережені attachments можна відкрити у fullscreen, але окремої delete action для них немає; за наявності edit-доступу можна видалити весь comment.

Backend повертає доступний список hiring statuses. Для Company declined, Applicant declined, No response і Duplicate frontend додатково вимагає reason. Зміна Family скидає team lead і cooperation status, щоб попередні значення не залишилися в іншому Family-контексті.

Картка зберігає окремо editable state і server snapshot. При переході до іншого кандидата з незбереженими змінами показується confirmation. Після перепризначення recruiter HR-ом відкритий діалог закривається, якщо поточний scope більше не відповідає картці.

Збереження двоетапне: спочатку оновлюються поля картки, потім завантажуються нові файли. Помилка file upload не відкочує вже збережені metadata; користувач отримує окрему notification, після чого картка оновлюється з backend.

Realtime-оновлення

Workspace реагує на:

  • reconnect / connection error;
  • новий діалог;
  • нову задачу;
  • видалення задач;
  • видалення картки;
  • нове повідомлення;
  • зовнішнє редагування відкритої картки.

Після reconnect state очищається зі збереженням актуального viewer та повторно завантажує Tasks і History. Нове повідомлення додається в активний чат тільки для відкритого кандидата. History оновлюється лише коли діалог відповідає поточному status, немає активної question task і не виконується text search.

Loading, empty та error states

СценарійПоведінка
Початкове завантаження, зміна viewer, відкриття діалогу та mutationsGlobal loader.
Пагінація History або messagesЛокальний spinner у відповідній колонці.
Порожні Tasks або HistoryОкремого текстового empty state немає.
Картку заборонено відкриватиЗамість форми показується повідомлення про недоступність.
Socket connection errorOverlay зі станом reconnecting; після reconnect дані завантажуються повторно.
Backend повернув success: falseGlobal notification із backend message або frontend fallback.
Один із початкових transport-запитів завершився rejected promiseLoading завершується, але окремої notification для цієї гілки поточна реалізація не показує.

API та sockets

Основні endpoint constants розташовані у src/utils/hiringData.js:

  • HIRING_GET_TASKS/hiring/getTasks;
  • HIRING_GET_RECRUITERS_FOR_SELECT/recruiter/getActiveRecruitersForHr;
  • HIRING_GET_HISTORY_DIALOGS/hiring/getDialogs;
  • HIRING_GET_USER_INFO/hiring/getUserInfo;
  • HIRING_GET_MESSAGES/hiring/getMessages;
  • HIRING_GET_MESSAGES_BY_CLOSED_TASK/hiring/getMessagesByClosedTask;
  • HIRING_SEND_MESSAGE/hiring/sendMessage;
  • HIRING_SET_TO_USER/recruit/operatorCard/setToHiring;
  • HIRING_SET_START_QUESTION_TASK_SESSION/hiring/assignHiringToQuestionTaskByHireId;
  • HIRING_SET_CLOSE_QUESTION_TASK_SESSION/hiring/cancelQuestionTaskByHiring;
  • HIRING_SET_SCHEDULE_REMINDER_TASK/hiring/scheduleReminderTask;
  • HIRING_DELETE_SCHEDULE_REMINDER_TASK/hiring/deleteScheduledReminderTask;
  • HIRING_CANCEL_TASK_MANUALLY/hiring/cancelTaskManually;
  • HIRING_SEARCH_DIALOGS/hiring/searchDialogs.

Картка кандидата використовує constants із src/utils/hrManagerDate.ts:

  • HR_GET_ALL_USERS/hr/getHrWithUsers;
  • HR_GET_PERSONAL_CARD_OF_OPERATOR_APPLICANT/recruit/operatorCard/getCard;
  • HR_EDIT_PERSONAL_CARD_OF_OPERATOR_APPLICANT/recruit/operatorCard/editOperator;
  • HR_GET_AVAILABLE_STATUSES_FOR_OPERATOR_APPLICANT/recruit/operatorCard/getAllowStatusesForOperator;
  • HR_UPLOAD_NEW_MEDIA/recruit/operatorCard/uploadFiles.

Family/recruiter selection використовує constants із src/utils/recruiterData.js:

  • RECRUITER_GET_SUPERVISORS_BY_FAMILY_FOR_APPLICANT/recruiter/getSupervisorsForApplicant;
  • RECRUITER_GET_ALL_ACTIVE_FOR_USER_CARD/recruiter/getRecruitersList.

Footer gallery використовує MEDIA_GALLERY_GET_MEDIA (/media-gallery/getMedia) із src/utils/mediaGallery.js.

Socket room підключається подіями connectToRoomHiring і connectToAllTeam у src/features/hiring_workspace/Index.tsx. src/features/hiring_workspace/hooks/useHiringWorkspaceSocket.ts слухає connect, connect_error, hiring:dialog:add, hiring:task:add, hiring:tasks:remove, hiring:cards:remove та hiring:message; card component окремо слухає hiring:cards:edit. Для handlers визначені симетричні off.

Frontend-архітектура

ФайлВідповідальність
Index.tsxorchestration трьох колонок, viewer room, URL initialization і unsaved guard
config.tsвідмінності доступу HR/recruiter
hooks/useHiringWorkspaceNavigation.tsquery parameters і URL actions
hooks/useHiringWorkspaceData.tsTasks, History, search і початкове завантаження
hooks/useHiringWorkspaceChatData.tsuser permissions і messages
hooks/useHiringWorkspaceSocket.tsсиметричні socket on/off
hooks/useHiringWorkspaceSocketState.tsstate transitions від socket events
tasks_block/активні та completed tasks
history_block/history, status/date filters і pagination
live_chat/header, messages, composer, media та reminders
personal_card/редагування й збереження картки кандидата

Правила безпечного розширення

  • Не створювати окремі HR/recruiter workspace-компоненти. Рольову відмінність додавати в config.ts або передавати як access-aware параметр у доменному компоненті.
  • Не викликати messages API до отримання flagCanOpenMessages.
  • При додаванні socket event завжди додавати симетричний off у useHiringWorkspaceSocket.
  • Не змінювати viewerRole/viewerId поза useHiringWorkspaceNavigation.
  • Не preload-ити FFmpeg у footer.
  • Зберігати API field names без перейменування; frontend helpers можуть мати власні зрозумілі назви.
  • Після зміни pagination перевіряти обидва режими: status/date history та text search.
  • Після зміни access logic перевіряти окремо HR, recruiter і HR all team.

Зв’язки

  • Батьківський розділ і вкладки: operators-applicants
  • Recruiters: recruiters
  • Hiring media library у footer: media-gallery
  • Спільна поведінка вкладень: media-renderer
  • Реєстр Swagger UI: swagger
  • Frontend endpoint constants: src/utils/hiringData.js, src/utils/hrManagerDate.ts, src/utils/recruiterData.js, src/utils/mediaGallery.js
  • Role routes: src/components/hr_manager/Index.js, src/components/recruiter/Index.tsx
  • TODO: додати перевірені operation deeplink-и для workspace, applicant card і hiring media picker.