UI - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: АДМИНКА ЛАУНЧЕРА

This commit is contained in:
DCCONSTRUCTIONS 2026-05-08 17:55:42 +03:00
parent 11dd8d1043
commit 795f369947
2 changed files with 68 additions and 58 deletions

View File

@ -2238,11 +2238,11 @@ code {
} }
.services-admin-table th:nth-child(1) { .services-admin-table th:nth-child(1) {
width: 24%; width: 23%;
} }
.services-admin-table th:nth-child(2) { .services-admin-table th:nth-child(2) {
width: 13%; width: 12%;
} }
.services-admin-table th:nth-child(3) { .services-admin-table th:nth-child(3) {
@ -2250,7 +2250,7 @@ code {
} }
.services-admin-table th:nth-child(4) { .services-admin-table th:nth-child(4) {
width: 25%; width: 27%;
} }
.services-admin-table th:nth-child(5) { .services-admin-table th:nth-child(5) {
@ -2303,6 +2303,9 @@ code {
padding: 0.18rem 0.35rem; padding: 0.18rem 0.35rem;
font-size: 0.78rem; font-size: 0.78rem;
font-weight: 600; font-weight: 600;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
} }
.admin-table-input:hover, .admin-table-input:hover,
@ -2633,13 +2636,44 @@ code {
text-align: right; text-align: right;
} }
.services-admin-table th:nth-child(4),
.services-admin-table td.services-admin-table__launch {
padding-left: 1.35rem;
}
.services-admin-table__edit { .services-admin-table__edit {
width: 2.35rem; width: 1.9rem;
min-width: 2.35rem; min-width: 1.9rem;
height: 2.35rem; height: 1.9rem;
margin-left: auto; margin-left: auto;
} }
.admin-icon-action {
border: 0;
background: transparent !important;
color: rgba(255, 255, 255, 0.62);
box-shadow: none;
}
.admin-icon-action:hover,
.admin-icon-action:focus-visible {
border: 0;
background: transparent !important;
color: var(--text-primary);
outline: none;
}
.admin-icon-action svg {
width: 80%;
height: 80%;
}
.invite-icon-action {
width: 1.9rem;
min-width: 1.9rem;
height: 1.9rem;
}
.services-admin-table__drag-cell { .services-admin-table__drag-cell {
text-align: right; text-align: right;
} }

View File

@ -1230,10 +1230,10 @@ function ServicesSection({
function ServiceTableColGroup() { function ServiceTableColGroup() {
return ( return (
<colgroup> <colgroup>
<col style={{ width: "24%" }} /> <col style={{ width: "23%" }} />
<col style={{ width: "13%" }} />
<col style={{ width: "12%" }} /> <col style={{ width: "12%" }} />
<col style={{ width: "25%" }} /> <col style={{ width: "12%" }} />
<col style={{ width: "27%" }} />
<col style={{ width: "15%" }} /> <col style={{ width: "15%" }} />
<col style={{ width: "3.4rem" }} /> <col style={{ width: "3.4rem" }} />
<col style={{ width: "3.1rem" }} /> <col style={{ width: "3.1rem" }} />
@ -1316,7 +1316,7 @@ function ServiceTableCells({
onChange={(status) => onUpdateService(service.id, { status })} onChange={(status) => onUpdateService(service.id, { status })}
/> />
</td> </td>
<td> <td className="services-admin-table__launch">
<input <input
className="admin-table-input" className="admin-table-input"
value={getServiceLaunchLink(service)} value={getServiceLaunchLink(service)}
@ -1333,8 +1333,13 @@ function ServiceTableCells({
/> />
</td> </td>
<td className="services-admin-table__actions"> <td className="services-admin-table__actions">
<IconButton label={`Контент витрины ${service.title}`} className="admin-circle-action services-admin-table__edit" type="button" onClick={onOpenContent}> <IconButton
<Edit3 size={15} /> label={`Контент витрины ${service.title}`}
className="admin-icon-action services-admin-table__edit"
type="button"
onClick={onOpenContent}
>
<Edit3 size={12} />
</IconButton> </IconButton>
</td> </td>
<td className="services-admin-table__drag-cell"> <td className="services-admin-table__drag-cell">
@ -1865,10 +1870,6 @@ function ClientEditorModal({
<span>Email</span> <span>Email</span>
<input value={draft.contactEmail ?? ""} onChange={(event) => update("contactEmail", event.target.value || null)} /> <input value={draft.contactEmail ?? ""} onChange={(event) => update("contactEmail", event.target.value || null)} />
</label> </label>
<div className="service-content-field">
<span>Демо до</span>
<NodeDcDateField value={draft.demoEndsAt ?? null} label="Демо до" onChange={(value) => update("demoEndsAt", value)} />
</div>
<div className="service-content-field"> <div className="service-content-field">
<span>Договор с</span> <span>Договор с</span>
<NodeDcDateField <NodeDcDateField
@ -1893,6 +1894,10 @@ function ClientEditorModal({
<span>Оплачено до</span> <span>Оплачено до</span>
<NodeDcDateField value={draft.paidUntil ?? null} label="Оплачено до" onChange={(value) => update("paidUntil", value)} /> <NodeDcDateField value={draft.paidUntil ?? null} label="Оплачено до" onChange={(value) => update("paidUntil", value)} />
</div> </div>
<div className="service-content-field">
<span>Демо до</span>
<NodeDcDateField value={draft.demoEndsAt ?? null} label="Демо до" onChange={(value) => update("demoEndsAt", value)} />
</div>
<label className="service-content-field service-content-field--wide"> <label className="service-content-field service-content-field--wide">
<span>Заметки</span> <span>Заметки</span>
<textarea value={draft.notes ?? ""} onChange={(event) => update("notes", event.target.value || null)} rows={4} /> <textarea value={draft.notes ?? ""} onChange={(event) => update("notes", event.target.value || null)} rows={4} />
@ -2198,6 +2203,8 @@ function MediaSourceField({
onFileChange: (file?: File) => void | Promise<void>; onFileChange: (file?: File) => void | Promise<void>;
}) { }) {
const inputId = `${label.replace(/\s+/g, "-").toLowerCase()}-${source}`; const inputId = `${label.replace(/\s+/g, "-").toLowerCase()}-${source}`;
const displayFileName = isUploading ? "Сохраняем в storage..." : truncateText(fileName ?? "Файл не выбран", 15);
const fileTitle = isUploading ? undefined : (fileName ?? "Файл не выбран");
return ( return (
<div className="service-content-field service-media-field"> <div className="service-content-field service-media-field">
@ -2210,7 +2217,9 @@ function MediaSourceField({
<label className="service-media-file-button" htmlFor={inputId}> <label className="service-media-file-button" htmlFor={inputId}>
Выберите файл Выберите файл
</label> </label>
<span className="service-media-file-name">{isUploading ? "Сохраняем в storage..." : fileName ?? "Файл не выбран"}</span> <span className="service-media-file-name" title={fileTitle}>
{displayFileName}
</span>
<input id={inputId} type="file" accept={mediaAccept} onChange={(event) => onFileChange(event.currentTarget.files?.[0])} /> <input id={inputId} type="file" accept={mediaAccept} onChange={(event) => onFileChange(event.currentTarget.files?.[0])} />
</div> </div>
) : ( ) : (
@ -2242,6 +2251,10 @@ function MediaSourceField({
); );
} }
function truncateText(value: string, maxLength: number) {
return value.length > maxLength ? `${value.slice(0, maxLength)}` : value;
}
function MediaPreview({ src, kind }: { src: string; kind?: MediaKind | null }) { function MediaPreview({ src, kind }: { src: string; kind?: MediaKind | null }) {
if (kind === "video" || mediaKindFromUrl(src) === "video") { if (kind === "video" || mediaKindFromUrl(src) === "video") {
return <video src={src} autoPlay loop muted playsInline />; return <video src={src} autoPlay loop muted playsInline />;
@ -2310,21 +2323,10 @@ function AccessSection({
<span>Добавьте участника или инвайт, после этого здесь появятся ячейки доступа.</span> <span>Добавьте участника или инвайт, после этого здесь появятся ячейки доступа.</span>
</div> </div>
</GlassSurface> </GlassSurface>
<GlassSurface className="access-explanation access-explanation--empty">
<p className="eyebrow">Explanation panel</p>
<h3>Ячейка не выбрана</h3>
<div className="explanation-stack">
<InfoLine label="Итог" value="Нет данных" />
<InfoLine label="Причина" value="У выбранного клиента нет участников в текущем наборе данных" />
</div>
</GlassSurface>
</div> </div>
); );
} }
const selectedUser = selectedCell ? getUser(data, selectedCell.userId) : null;
const selectedService = selectedCell ? getService(data, selectedCell.serviceId) : null;
const accessGridTemplateColumns = `15rem repeat(2, 9.7rem) repeat(${matrix.services.length}, 11.25rem)`; const accessGridTemplateColumns = `15rem repeat(2, 9.7rem) repeat(${matrix.services.length}, 11.25rem)`;
return ( return (
@ -2417,32 +2419,6 @@ function AccessSection({
</div> </div>
</GlassSurface> </GlassSurface>
<GlassSurface className="access-explanation">
<p className="eyebrow">Explanation panel</p>
{selectedCell && selectedUser && selectedService ? (
<>
<h3>
{selectedUser.name} / {selectedService.title}
</h3>
<div className="explanation-stack">
<InfoLine label="Итог" value={selectedCell.effectiveAccess.allowed ? "Есть доступ" : "Нет доступа"} />
<InfoLine label="Можно открыть" value={selectedCell.effectiveAccess.openEnabled ? "Да" : "Нет"} />
<InfoLine label="Причина" value={selectedCell.effectiveAccess.reason} />
<InfoLine label="Источник" value={sourceLabel(selectedCell.effectiveAccess.source)} />
<InfoLine label="Роль" value={selectedCell.effectiveAccess.appRole ?? "—"} />
</div>
</>
) : (
<>
<h3>Сервис не выбран</h3>
<div className="explanation-stack">
<InfoLine label="Итог" value="Выберите сервисную ячейку" />
<InfoLine label="MAIN" value="Базовые роли и статусы применяются сразу" />
</div>
</>
)}
</GlassSurface>
{detailsCell ? ( {detailsCell ? (
<OperationalCoreAccessModal <OperationalCoreAccessModal
data={data} data={data}
@ -2938,11 +2914,11 @@ function InvitesSection({
{isCopied ? <span className="invite-link-cell__state">Скопировано</span> : null} {isCopied ? <span className="invite-link-cell__state">Скопировано</span> : null}
<IconButton <IconButton
label={isCopied ? `Инвайт ${invite.email} скопирован` : `Скопировать инвайт ${invite.email}`} label={isCopied ? `Инвайт ${invite.email} скопирован` : `Скопировать инвайт ${invite.email}`}
className="admin-circle-action services-admin-table__edit" className="admin-icon-action invite-icon-action"
type="button" type="button"
onClick={() => void handleCopyInvite(invite)} onClick={() => void handleCopyInvite(invite)}
> >
<Copy size={14} /> <Copy size={11} />
</IconButton> </IconButton>
</div> </div>
</td> </td>
@ -2958,11 +2934,11 @@ function InvitesSection({
<td className="services-admin-table__actions"> <td className="services-admin-table__actions">
<IconButton <IconButton
label={`Удалить инвайт ${invite.email}`} label={`Удалить инвайт ${invite.email}`}
className="admin-circle-action services-admin-table__edit" className="admin-icon-action invite-icon-action"
type="button" type="button"
onClick={() => setDeleteInviteId(invite.id)} onClick={() => setDeleteInviteId(invite.id)}
> >
<Trash2 size={15} /> <Trash2 size={12} />
</IconButton> </IconButton>
</td> </td>
</tr> </tr>