From 4a519d1439fde2843c8b12b6ca21a8b808425581 Mon Sep 17 00:00:00 2001 From: DCCONSTRUCTIONS Date: Fri, 8 May 2026 18:16:22 +0300 Subject: [PATCH] =?UTF-8?q?UI=20-=20=D0=9C=D0=95=D0=96=D0=9F=D0=A0=D0=9E?= =?UTF-8?q?=D0=95=D0=9A=D0=A2=D0=9D=D0=90=D0=AF=20=D0=9A=D0=9E=D0=9C=D0=9C?= =?UTF-8?q?=D0=A3=D0=9D=D0=98=D0=9A=D0=90=D0=A6=D0=98=D0=AF:=20=D0=9A?= =?UTF-8?q?=D0=9B=D0=98=D0=95=D0=9D=D0=A2=D0=A1=D0=9A=D0=98=D0=99=20=D0=91?= =?UTF-8?q?=D0=A0=D0=95=D0=9D=D0=94=D0=98=D0=9D=D0=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/control-plane-store.mjs | 2 + src/entities/client/types.ts | 1 + src/shared/api/mockData.ts | 1 + src/styles/globals.css | 91 +++++++++++++++++++++- src/widgets/admin-overlay/AdminOverlay.tsx | 64 +++++++++++++-- src/widgets/top-bar/TopBar.tsx | 29 +------ 6 files changed, 152 insertions(+), 36 deletions(-) diff --git a/server/control-plane-store.mjs b/server/control-plane-store.mjs index f800c5b..480ac13 100644 --- a/server/control-plane-store.mjs +++ b/server/control-plane-store.mjs @@ -105,6 +105,7 @@ export function createControlPlaneStore({ projectRoot }) { demoEndsAt: nullableString(payload?.demoEndsAt), contactName: nullableString(payload?.contactName), contactEmail: nullableString(payload?.contactEmail), + avatarUrl: nullableString(payload?.avatarUrl), integrations: normalizeClientIntegrations(payload?.integrations), notes: nullableString(payload?.notes), createdAt: now, @@ -141,6 +142,7 @@ export function createControlPlaneStore({ projectRoot }) { client.demoEndsAt = nullableStringWithFallback(payload?.demoEndsAt, client.demoEndsAt ?? null); client.contactName = nullableStringWithFallback(payload?.contactName, client.contactName ?? null); client.contactEmail = nullableStringWithFallback(payload?.contactEmail, client.contactEmail ?? null); + client.avatarUrl = nullableStringWithFallback(payload?.avatarUrl, client.avatarUrl ?? null); if ("integrations" in (payload ?? {})) { client.integrations = normalizeClientIntegrations(payload.integrations, client.integrations); } diff --git a/src/entities/client/types.ts b/src/entities/client/types.ts index 405619b..4137181 100644 --- a/src/entities/client/types.ts +++ b/src/entities/client/types.ts @@ -20,6 +20,7 @@ export interface Client { demoEndsAt?: string | null; contactName?: string | null; contactEmail?: string | null; + avatarUrl?: string | null; integrations?: { taskManager?: { workspaceSlug?: string | null; diff --git a/src/shared/api/mockData.ts b/src/shared/api/mockData.ts index a9acc14..5598e6b 100644 --- a/src/shared/api/mockData.ts +++ b/src/shared/api/mockData.ts @@ -21,6 +21,7 @@ export const mockClients: Client[] = [ demoEndsAt: null, contactName: "DC Touch", contactEmail: "dcctouch@gmail.com", + avatarUrl: null, notes: "Live-клиент NODE.DC для первичной проверки control-plane, SSO и доступа к сервисам.", createdAt: "2026-05-04T00:00:00.000Z", updatedAt: now, diff --git a/src/styles/globals.css b/src/styles/globals.css index c0d0593..43de421 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -449,6 +449,13 @@ code { object-fit: contain; } +.nodedc-expanded-workspace-avatar { + width: 72%; + height: 72%; + border-radius: inherit; + object-fit: contain; +} + .nodedc-expanded-user-group { display: inline-flex; height: var(--nodedc-shell-pill-height); @@ -1429,6 +1436,7 @@ code { } .admin-panel-content { + position: relative; display: grid; grid-template-rows: auto minmax(0, 1fr); gap: 1rem; @@ -2664,8 +2672,8 @@ code { } .admin-icon-action svg { - width: 80%; - height: 80%; + width: 0.78rem; + height: 0.78rem; } .invite-icon-action { @@ -2857,6 +2865,11 @@ code { -webkit-backdrop-filter: blur(34px) saturate(1.12); } +.client-editor-modal { + width: 100%; + max-height: 100%; +} + .service-content-modal__head, .service-content-modal__foot { display: flex; @@ -3021,6 +3034,80 @@ code { color: rgba(8, 8, 10, 0.96); } +.client-avatar-field { + align-self: stretch; +} + +.client-avatar-control { + display: grid; + min-height: 4rem; + grid-template-columns: 3.25rem minmax(0, 1fr) auto auto; + align-items: center; + gap: 0.75rem; + overflow: hidden; + border-radius: var(--launcher-radius-control); + background: rgba(255, 255, 255, 0.06); + padding: 0.38rem; +} + +.client-avatar-preview { + display: grid; + width: 3.1rem; + height: 3.1rem; + place-items: center; + overflow: hidden; + border-radius: var(--launcher-radius-circle); + background: rgba(255, 255, 255, 0.055); +} + +.client-avatar-preview img { + width: 72%; + height: 72%; + object-fit: contain; +} + +.client-avatar-control__copy { + display: grid; + min-width: 0; + gap: 0.18rem; +} + +.client-avatar-control__copy strong { + overflow: hidden; + color: var(--text-primary); + font-size: 0.84rem; + font-weight: 850; + text-overflow: ellipsis; + white-space: nowrap; +} + +.client-avatar-control__copy small, +.client-avatar-error { + color: var(--text-muted); + font-size: 0.74rem; + font-weight: 650; +} + +.client-avatar-upload-button { + position: relative; + min-height: 2.65rem; +} + +.client-avatar-upload-button input { + position: absolute; + width: 1px; + height: 1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; +} + +.client-avatar-clear-action { + width: 2.35rem; + min-width: 2.35rem; + height: 2.35rem; +} + .service-content-field textarea { resize: vertical; line-height: 1.45; diff --git a/src/widgets/admin-overlay/AdminOverlay.tsx b/src/widgets/admin-overlay/AdminOverlay.tsx index 1d62205..f53f950 100644 --- a/src/widgets/admin-overlay/AdminOverlay.tsx +++ b/src/widgets/admin-overlay/AdminOverlay.tsx @@ -572,11 +572,11 @@ function ClientsSection({ setEditingClientId(client.id)} > - + @@ -845,11 +845,11 @@ function GroupsSection({ setEditingGroupId(group.id)} > - + @@ -1709,11 +1709,17 @@ function ClientEditorModal({ canDelete: boolean; }) { const [draft, setDraft] = useState(client); + const [uploadingAvatar, setUploadingAvatar] = useState(false); + const [storageError, setStorageError] = useState(null); const taskManagerWorkspaceBindings = getClientTaskManagerWorkspaces(draft); const selectedTaskManagerWorkspaceSlugs = new Set(taskManagerWorkspaceBindings.map((workspace) => workspace.slug)); const primaryTaskManagerWorkspace = getPrimaryTaskManagerWorkspace(draft); - useEffect(() => setDraft(client), [client]); + useEffect(() => { + setDraft(client); + setUploadingAvatar(false); + setStorageError(null); + }, [client]); function update(key: K, value: Client[K]) { setDraft((current) => ({ ...current, [key]: value })); @@ -1760,9 +1766,25 @@ function ClientEditorModal({ updateTaskManagerWorkspaces(normalizeClientTaskManagerWorkspaceDraft(nextWorkspaces)); } + async function handleAvatarUpload(file?: File) { + if (!file) return; + + setUploadingAvatar(true); + setStorageError(null); + + try { + const storedFile = await uploadStorageFile(file); + update("avatarUrl", storedFile.url); + } catch (error) { + setStorageError(error instanceof Error ? error.message : "Не удалось сохранить аватар компании"); + } finally { + setUploadingAvatar(false); + } + } + return (
-
+
Статус update("status", status)} />
+
+ Аватар компании +
+ +
+ {draft.avatarUrl ? "Аватар подключён" : "Аватар не задан"} + Показывается в верхнем переключателе компании. +
+ + {draft.avatarUrl ? ( + + ) : null} +
+ {storageError ? {storageError} : null} +
Operational Core workspaces
diff --git a/src/widgets/top-bar/TopBar.tsx b/src/widgets/top-bar/TopBar.tsx index 8956713..c0ad4a5 100644 --- a/src/widgets/top-bar/TopBar.tsx +++ b/src/widgets/top-bar/TopBar.tsx @@ -36,17 +36,11 @@ export function TopBar({ const availableClientIds = new Set(me.memberships.map((membership) => membership.clientId)); const availableClients = clients.filter((client) => availableClientIds.has(client.id)); const activeClient = availableClients.find((client) => client.id === activeClientId); - const activeProfile = profileOptions.find((profile) => profile.userId === activeProfileId); const clientOptions = availableClients.map((client) => ({ value: client.id, label: client.name, description: client.legalName ?? undefined, })); - const profileSelectOptions = profileOptions.map((profile) => ({ - value: profile.userId, - label: profile.label, - description: profile.description, - })); return (
@@ -76,7 +70,7 @@ export function TopBar({ aria-expanded={open} onClick={toggle} > - + {activeClient?.avatarUrl ? : null} )} /> @@ -86,27 +80,6 @@ export function TopBar({ Витрина - onProfileChange(userId)} - trigger={({ open, selectedOption, toggle, setTriggerRef }) => ( - - )} - /> - {me.permissions.canOpenAdmin ? (