refactor(frontend): refine profile timeline balance
This commit is contained in:
parent
d437f63d02
commit
caf6eebc0f
@ -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
|
||||
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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 ? (
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user