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/workspace | src/components/hr_manager/Index.js |
| Recruiter | /recruiter/operators_applicants/workspace | src/components/recruiter/Index.tsx |
Layout
Робочий простір складається з трьох колонок:
- Tasks + History — активні/завершені задачі, пошук і фільтрація діалогів.
- Live chat — повідомлення, media attachments, voice messages і scheduled reminder.
- Applicant information — загальні дані, hiring, cooperation та коментарі кандидата.
Ролі та scope
| Поведінка | HR | Recruiter |
|---|---|---|
| Працювати від свого імені | так | так |
| Вибрати іншого 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— відкритий кандидат;viewerRole—hr,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 кандидата зі статусом
New—Assign 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_linkentities; - 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
- Доступ до мікрофона запитується тільки після натискання mic.
- Максимальна тривалість одного запису —
15 хвилин. - Якщо браузер записав OGG/Opus, blob відправляється без конвертації.
- Якщо браузер записав WebM/Opus, модуль FFmpeg/WASM ліниво завантажується тільки під час фактичної відправки.
- Максимальний розмір WebM для клієнтської конвертації —
50 MB. - Тимчасові файли 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, відкриття діалогу та mutations | Global loader. |
| Пагінація History або messages | Локальний spinner у відповідній колонці. |
| Порожні Tasks або History | Окремого текстового empty state немає. |
| Картку заборонено відкривати | Замість форми показується повідомлення про недоступність. |
| Socket connection error | Overlay зі станом reconnecting; після reconnect дані завантажуються повторно. |
Backend повернув success: false | Global notification із backend message або frontend fallback. |
| Один із початкових transport-запитів завершився rejected promise | Loading завершується, але окремої 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.tsx | orchestration трьох колонок, viewer room, URL initialization і unsaved guard |
config.ts | відмінності доступу HR/recruiter |
hooks/useHiringWorkspaceNavigation.ts | query parameters і URL actions |
hooks/useHiringWorkspaceData.ts | Tasks, History, search і початкове завантаження |
hooks/useHiringWorkspaceChatData.ts | user permissions і messages |
hooks/useHiringWorkspaceSocket.ts | симетричні socket on/off |
hooks/useHiringWorkspaceSocketState.ts | state 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.