UI - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: АДМИНКА ЛАУНЧЕРА
This commit is contained in:
parent
11dd8d1043
commit
795f369947
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue