refactor(frontend): refine profile timeline balance

This commit is contained in:
lizerk 2026-04-13 14:30:29 +03:00
parent d437f63d02
commit caf6eebc0f
3 changed files with 68 additions and 59 deletions

View File

@ -30,7 +30,7 @@
| `procurement` | `needs refactor` | крупные панели и часть detail blocks уже читаются неплохо, есть shared-like detail content | большой page-file, gray/white legacy в stats/suppliers/forms, смешение operational page и старых local panels | нормализовать page panels, suppliers/stats/forms и только потом решать дальнейшую декомпозицию |
| `calendar` | `legacy-heavy` | богатая функциональность, много reusable доменных компонентов | historical layering, legacy-термины и неоднородные модалки | отдельный audit до системного refactor |
| `messages` | `mixed` | зрелая interaction model, сильный composer и message item, app-style в основных экранах в целом выдержан | очень доменно-специфичный UX, крупные page-files, часть settings/create/chat-experiment flows ещё шумят по state/legacy dialogs | использовать выборочно как reference, без механического копирования в другие зоны; дальше резать state-heavy pages |
| `users/profile` | `mixed` | хорошие profile sections, hero-like anatomy, action timeline, переиспользуемые section-level components, `/profile` и `/users/[id]` опираются на page-level controllers, modals подтянуты к app-style, локальные detail blocks выровнены по геометрии | profile cluster уже собран в одну систему, но hero/section character всё ещё требует ручного visual review перед тем, как считать его зрелым reference | держать как reference по profile anatomy и возвращаться только на точечный visual-tuning без новой structural волны |
| `users/profile` | `mixed` | хорошие profile sections, hero-like anatomy, action timeline, переиспользуемые section-level components, `/profile` и `/users/[id]` опираются на page-level controllers, modals подтянуты к app-style, локальные detail blocks выровнены по геометрии | profile cluster уже собран в одну систему, но hero density, balance нижних секций и сам timeline ещё требуют отдельного visual-pass перед тем, как считать экран зрелым reference | держать как reference по profile anatomy, но не закрывать профиль до ручного review visual-pass |
| `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 без крупного редизайна |
@ -42,15 +42,16 @@
Рациональный порядок после текущего audit:
1. `home/feed` + `notifications`
2. `documents` + `documents/dashboard`
3. `equipment`
4. `procurement`
5. `messages`
6. `settings`
7. отдельный audit по `calendar`
8. `departments`
9. `auth/legal`
1. завершение `users/profile` visual-pass
2. `home/feed` + `notifications`
3. `documents` + `documents/dashboard`
4. `equipment`
5. `procurement`
6. `messages`
7. `settings`
8. отдельный audit по `calendar`
9. `departments`
10. `auth/legal`
Логика порядка:
@ -70,20 +71,27 @@
Статус:
- wave 1 baseline pass завершён
- structural baseline завершён
- visual-pass всё ещё активен
Цель:
- подтянуть people/profile cluster к общему product feel без потери profile anatomy
Что зафиксировано:
Что уже сделано:
- `/profile` уже переведён на page-level controller по аналогии с `useUserDetailPage`
- `EditUserProfileModal` и `EmployeeActionModal` уже подтянуты к app-style primitives
- `ProfileSections` уже нормализованы по базовой геометрии без слома anatomy
- `users/page.tsx` уже переведён на app-surface language
- `employees/page.tsx` и остаточные локальные blocks в `/users/[id]` уже приведены к тому же cluster contract
- дальнейший проход нужен только если после ручного review выяснится, что hero density всё ещё визуально выбивается
- baseline people/profile cluster уже живёт в одной app-system
Что осталось:
- довести `EmployeeActionsTimeline`
- проверить balance `Навыки / Информация / Кадровые события`
- после ручного review решить, достаточно ли profile-screen близок к общему product feel
### Волна 2. Operational pages with high leverage

View File

@ -153,22 +153,24 @@ export default function ProfilePage() {
bottomPanel={<ProfileContactsPanel rows={contactRows} />}
/>
<ProfileSkillsCard
inputValue={skillName}
onInputChange={onInputSkillName}
onSubmit={() => void handleAddSkill()}
submitDisabled={!skillName.trim() || skillsSaving}
inputDisabled={skillsSaving}
availableSkills={availableSkills}
skills={profileSkills}
error={skillsError}
loading={skillsLoading}
onRemoveSkill={(skillId) => void handleRemoveSkill(skillId)}
removeDisabled={skillsSaving}
emptyText="Навыки пока не указаны"
/>
<div className="grid gap-4 xl:grid-cols-[minmax(0,1.1fr)_minmax(0,0.9fr)] xl:items-start">
<ProfileSkillsCard
inputValue={skillName}
onInputChange={onInputSkillName}
onSubmit={() => void handleAddSkill()}
submitDisabled={!skillName.trim() || skillsSaving}
inputDisabled={skillsSaving}
availableSkills={availableSkills}
skills={profileSkills}
error={skillsError}
loading={skillsLoading}
onRemoveSkill={(skillId) => void handleRemoveSkill(skillId)}
removeDisabled={skillsSaving}
emptyText="Навыки пока не указаны"
/>
<ProfileInfoCard items={infoItems} />
<ProfileInfoCard items={infoItems} />
</div>
<EmployeeActionsTimeline
actionLoading={null}

View File

@ -48,22 +48,28 @@ export default function EmployeeActionsTimeline({
expandedCommentLength = truncateCommentLength,
}: EmployeeActionsTimelineProps) {
const [showAll, setShowAll] = useState(false);
if (!canViewActions || sortedActions.length === 0) {
return null;
}
const canCollapse = Boolean(initialVisibleCount && sortedActions.length > initialVisibleCount);
const visibleActions = useMemo(
() => (canCollapse && !showAll ? sortedActions.slice(0, initialVisibleCount) : sortedActions),
[canCollapse, initialVisibleCount, showAll, sortedActions],
);
if (!canViewActions || sortedActions.length === 0) {
return null;
}
return (
<section className="app-surface rounded-[24px] p-5">
<div className="mb-4 flex items-start justify-between gap-4">
<h2 className="app-card-caption">Кадровые события</h2>
<div className="flex items-center gap-2">
<section className="app-surface rounded-2xl p-5">
<div className="mb-4 flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
<div>
<h2 className="app-card-caption">Кадровые события</h2>
{!showAll && canCollapse ? (
<p className="app-text-muted mt-2 text-sm">
Последние {visibleActions.length} из {sortedActions.length}
</p>
) : null}
</div>
<div className="flex flex-wrap items-center gap-2">
{showCountLabel ? (
<span className="app-text-muted text-sm">
{sortedActions.length}{" "}
@ -95,12 +101,6 @@ export default function EmployeeActionsTimeline({
</div>
</div>
{!showAll && canCollapse ? (
<p className="app-text-muted mb-3 text-sm">
Последние {visibleActions.length} из {sortedActions.length}
</p>
) : null}
<div className="space-y-3">
{visibleActions.map((action) => {
const isCurrent = latestActionId === action.id;
@ -111,28 +111,27 @@ export default function EmployeeActionsTimeline({
: action.comment;
return (
<div key={action.id} className="relative pl-5">
<div key={action.id} className="relative pl-6">
<span
className="absolute bottom-0 left-1.5 top-0 w-px"
className="absolute bottom-0 left-[7px] top-0 w-px"
style={{ backgroundColor: tone.lineColor }}
/>
{isCurrent ? (
<span
className="absolute left-0 top-2 h-3.5 w-3.5 rounded-full border-4 border-[var(--surface-primary)]"
style={{ backgroundColor: tone.lineColor }}
/>
) : null}
<div className="app-surface-muted rounded-2xl px-4 py-3">
<div className="flex flex-col gap-2 md:flex-row md:items-start md:justify-between">
<span
className={[
"absolute left-0 top-3 h-[14px] w-[14px] rounded-full border border-[var(--surface-primary)]",
isCurrent ? "shadow-[0_0_0_4px_var(--surface-primary)]" : "",
].join(" ")}
style={{ backgroundColor: tone.lineColor }}
/>
<div className="app-surface-muted rounded-xl px-4 py-3.5">
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
<div className="min-w-0">
<div className="flex flex-wrap items-center gap-2">
<span
className={`app-status-pill ${tone.badgeClass}`}
>
<span className={`app-status-pill ${tone.badgeClass}`}>
{action.action_display || action.action}
</span>
{isCurrent ? (
<span className="app-text-muted text-xs font-medium">
<span className="app-pill inline-flex rounded-full px-2 py-0.5 text-xs font-medium">
текущий
</span>
) : null}
@ -143,8 +142,8 @@ export default function EmployeeActionsTimeline({
</p>
) : null}
</div>
<div className="flex shrink-0 items-start gap-2">
<time className="app-text-muted pt-1 text-sm">
<div className="flex shrink-0 items-center gap-2 self-start">
<time className="app-text-muted text-sm">
{formatActionDate(action.date)}
</time>
{canManageActions && onEditAction && onDeleteAction ? (