diff --git a/docs/guides/frontend/04-PATTERNS.md b/docs/guides/frontend/04-PATTERNS.md index 79cc27e9..178add9c 100644 --- a/docs/guides/frontend/04-PATTERNS.md +++ b/docs/guides/frontend/04-PATTERNS.md @@ -74,6 +74,19 @@ - `frontend/src/components/requests/RequestUserBadge.tsx` +### Object detail screens + +Если один detail-route обслуживает и обычный просмотр, и управление объектом: + +- базовый режим должен быть `showcase-first`, а не выглядеть как обрубленная админка +- management controls встраиваются в ту же anatomy, а не создают второй отдельный layout +- главный акцент у сильного header и одного основного рабочего блока +- secondary-настройки должны быть заметно спокойнее главного контента + +Референс по духу: + +- `frontend/src/app/departments/[id]/page.tsx` + ### Compose forms Compose-паттерн должен восприниматься как рабочее пространство, а не как список независимых инпутов. diff --git a/docs/guides/frontend/06-COMPONENT-INVENTORY.md b/docs/guides/frontend/06-COMPONENT-INVENTORY.md index b3922e9c..580f5b79 100644 --- a/docs/guides/frontend/06-COMPONENT-INVENTORY.md +++ b/docs/guides/frontend/06-COMPONENT-INVENTORY.md @@ -231,10 +231,11 @@ Использовать как референс для: -- detail screen, который должен быть полноценным management workspace, а не бедной карточкой-описанием +- detail screen, который должен уметь переключаться между `showcase` и `management` без смены route и без ощущения двух разных страниц - page-level controller для domain actions вокруг одного объекта: overview, members, head management, roles -- richer detail layout, где есть сильный header, один основной roster-блок и один secondary-блок настроек, без раздувания служебных summary-panels +- richer detail layout, где есть сильный header, один основной roster-блок и один secondary-блок ролей, без раздувания служебных summary-panels - member roster с role assignment и person-chip подачей вместо сырых строк участников +- role management на уровне названий ролей и назначений участникам, без фронтового permission-editor С оговоркой: diff --git a/docs/guides/frontend/09-FRONTEND-AUDIT-MAP.md b/docs/guides/frontend/09-FRONTEND-AUDIT-MAP.md index 1cd22501..076acf6e 100644 --- a/docs/guides/frontend/09-FRONTEND-AUDIT-MAP.md +++ b/docs/guides/frontend/09-FRONTEND-AUDIT-MAP.md @@ -34,7 +34,7 @@ | `users/directory` | `mixed` | `users/page.tsx` и `employees/page.tsx` уже живут в app-surface language, сценарии user/employee navigation выровнены и читаются как один cluster | список людей всё ещё проще и беднее по interaction density, чем основные рабочие модули, поэтому это не полноценный stylistic reference | считать baseline-pass завершённым и трогать дальше только при появлении richer directory requirements | | `home/feed` | `mixed` | сильный app-shell fit, хорошая operational density, уже много app primitives | page очень большой, комментарии и post-compose logic перегружены, встречаются legacy `rounded-md` fragments | использовать как reference по feed density, но разрезать modal/comments/post flows и дочистить geometry | | `notifications` | `mixed` | близок к operational app-style, фильтры и list hierarchy уже читаются системно | page-level logic пока монолитная, нет выделенного feature controller, есть место для уплотнения и симметрии с messages/requests | после people cluster сделать короткий structural pass без крупного редизайна | -| `departments` | `mixed` | detail screen уже поднят до полноценного management workspace: сильный header, команда как основной блок, роли как secondary-блок, page-level controller вынесен в hook | список отделов всё ещё проще detail screen, а сам модуль ещё не проверен длинным visual-pass и живым backend-сценарием | считать `departments/[id]` baseline reference для object-management pages и затем при желании подтянуть list screen до той же richness | +| `departments` | `mixed` | detail screen уже работает как dual-mode page: showcase-first для обычного просмотра и встроенный management-layer для управленцев, page-level controller вынесен в hook | список отделов всё ещё проще detail screen, а сам модуль ещё не проверен длинным visual-pass и живым backend-сценарием | считать `departments/[id]` baseline reference для object-detail pages с mixed audience и затем при желании подтянуть list screen до той же richness | | `auth/legal` | `mixed` | изолированные flows уже отделены от app-shell, общая подача аккуратная | `rounded-[2rem]` и локальная hero-card геометрия образуют мини-систему отдельно от app shell | не смешивать с operational refactor; сделать отдельный compact pass по auth/legal geometry | | `documents/dashboard` | `legacy-heavy` | есть готовые data blocks | заметны старые rounded patterns и мелкие utility-решения | включить в documents refactor pass | @@ -177,8 +177,8 @@ Что делать: - у `settings` после controller extraction резать section-level UI и только затем geometry -- у `departments` поднять богатство patterns до общего уровня app -- detail screen `departments/[id]` уже не считать пустой зоной; дальнейшие работы здесь должны быть про polishing, а не про изобретение management UI с нуля +- у `departments` держать detail screen в логике `showcase + management layer`, а не возвращать его в mini-admin panel +- detail screen `departments/[id]` уже не считать пустой зоной; дальнейшие работы здесь должны быть про polishing, а не про возврат permission-editor и служебных панелей - у `auth/legal` выровнять isolated geometry отдельно от app-shell жанра ### Волна 7. Calendar special audit diff --git a/frontend/src/app/departments/[id]/page.tsx b/frontend/src/app/departments/[id]/page.tsx index 8eeb658e..788f2c54 100644 --- a/frontend/src/app/departments/[id]/page.tsx +++ b/frontend/src/app/departments/[id]/page.tsx @@ -15,31 +15,30 @@ import { Users, } from "lucide-react"; +import { AppShell } from "@/components/AppShell"; import { DepartmentPersonChip } from "@/components/departments/DepartmentPersonChip"; import { SearchableSelectSingle } from "@/components/shared/SearchableSelect"; -import { AppShell } from "@/components/AppShell"; import { Modal } from "@/components/ui/Modal"; import { useDepartmentPage } from "@/hooks/useDepartmentPage"; -import type { - DepartmentMemberLink, - DepartmentPermissionChoice, - DepartmentRole, -} from "@/types/api"; +import type { DepartmentMemberLink, DepartmentRole } from "@/types/api"; -function PermissionBadge({ - active, +function MetaChip({ children, + tone = "neutral", }: { - active: boolean; children: ReactNode; + tone?: "neutral" | "accent" | "warning"; }) { + const className = + tone === "accent" + ? "app-selected app-accent-text" + : tone === "warning" + ? "app-feedback-warning" + : "app-badge"; + return ( {children} @@ -72,98 +71,104 @@ function DepartmentMemberRow({ onSetHead: (employeeId: number) => Promise; }) { const isHead = currentHeadId === member.employee.id; - const isBusy = pendingKey === `member-remove-${member.employee.id}`; + const isRemoving = pendingKey === `member-remove-${member.employee.id}`; const isRoleBusy = pendingKey === `member-role-${member.employee.id}`; - const subtitle = member.employee.position?.name || member.employee.email || null; - const roleItems = roleOptions.map((role) => ({ id: role.id, name: role.name })); + const subtitle = + member.employee.position?.name || member.employee.email || null; + const managementMode = canAssignRoles || canChangeHead || canManage; return (
-
- - +
+ {isHead ? ( - + Руководитель - + ) : null} {!member.is_active ? ( Неактивное участие ) : null} - {member.role && !canAssignRoles ? ( +
+ +
+ {member.role ? ( {member.role.name} - ) : null} - {!member.role && !canAssignRoles ? ( + ) : ( Без роли - ) : null} + )}
-
- {canAssignRoles ? ( -
- void onAssignRole(member.employee.id, roleId)} - placeholder="Без роли" - disabled={isRoleBusy} - /> + {managementMode ? ( +
+ {canAssignRoles ? ( +
+ + void onAssignRole(member.employee.id, roleId) + } + placeholder="Без роли" + disabled={isRoleBusy} + /> +
+ ) : null} + +
+ {canChangeHead && !isHead ? ( + + ) : null} + {canManage && !isHead ? ( + + ) : null}
- ) : null} - -
- {canChangeHead && !isHead ? ( - - ) : null} - {canManage && !isHead ? ( - - ) : null}
-
+ ) : null}
); } function DepartmentRoleCard({ - canAssignRoles, + canManageRoles, pendingKey, role, usageCount, onDelete, onEdit, }: { - canAssignRoles: boolean; + canManageRoles: boolean; pendingKey: string | null; role: DepartmentRole; usageCount: number; @@ -174,32 +179,32 @@ function DepartmentRoleCard({ return (
-
+

{role.name}

- + {usageCount} участников
-

- {role.permissions_verbose.length - ? "Набор прав для участников отдела" - : "У роли пока нет назначенных прав"} +

+ {usageCount + ? "Используется в составе отдела." + : "Пока не назначена участникам отдела."}

- {canAssignRoles ? ( -
+ {canManageRoles ? ( +
- -
- {role.permissions_verbose.length ? ( - role.permissions_verbose.map((permission) => ( - - {permission.name} - - )) - ) : ( - Права ещё не выбраны - )} -
); } @@ -282,7 +272,7 @@ function DepartmentEditorModal({ value={draft.name} onChange={(event) => onChange({ name: event.target.value })} className="app-input w-full rounded-lg px-3 py-2 text-sm" - placeholder="Например, Финансовый отдел" + placeholder="Например, Отдел закупок" /> @@ -293,9 +283,9 @@ function DepartmentEditorModal({