feat: apply new Мои Товары design system across all templates
Replace Pico CSS with custom design: dark sidebar layout, Golos Text + JetBrains Mono fonts, orange accent (#FF5500), new component classes (cards, tables, buttons, tags, toggles, alerts, tabs, login split-panel). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,444 +1,415 @@
|
||||
/* Brand colors */
|
||||
:root {
|
||||
--pico-primary: #F05023;
|
||||
--pico-primary-hover: #d44420;
|
||||
--pico-primary-focus: rgba(240, 80, 35, 0.25);
|
||||
--pico-primary-inverse: #fff;
|
||||
--brand-primary: #F05023;
|
||||
--brand-secondary: #0986E2;
|
||||
--brand-secondary-hover: #0770c0;
|
||||
}
|
||||
/* ─── Reset ──────────────────────────────────────────────────────────── */
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
html, body { height: 100%; }
|
||||
body { font-family: 'Golos Text', sans-serif; background: #F4F5F7; color: #1C1F2E; font-size: 14px; }
|
||||
a { text-decoration: none; color: inherit; }
|
||||
::-webkit-scrollbar { width: 5px; height: 5px; }
|
||||
::-webkit-scrollbar-track { background: transparent; }
|
||||
::-webkit-scrollbar-thumb { background: #E4E6EE; border-radius: 3px; }
|
||||
|
||||
/* Header / nav */
|
||||
.site-header {
|
||||
background: #fff;
|
||||
border-bottom: 2px solid var(--brand-primary);
|
||||
padding: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.site-header nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding-top: 0.75rem;
|
||||
padding-bottom: 0.75rem;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.site-header nav > ul {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.brand-logo {
|
||||
font-size: 1.3rem;
|
||||
font-weight: 700;
|
||||
color: var(--brand-primary) !important;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.nav-links {
|
||||
flex: 1;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.nav-links a {
|
||||
color: var(--pico-color);
|
||||
text-decoration: none;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.nav-links a:hover {
|
||||
color: var(--brand-primary);
|
||||
}
|
||||
|
||||
.nav-links a.secondary {
|
||||
color: var(--pico-muted-color);
|
||||
}
|
||||
|
||||
.mobile-menu {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mobile-menu summary {
|
||||
padding: 0.25rem 0.5rem;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.mobile-menu > ul {
|
||||
position: absolute;
|
||||
right: 1rem;
|
||||
background: var(--pico-background-color);
|
||||
border: 1px solid var(--pico-border-color);
|
||||
border-radius: var(--pico-border-radius);
|
||||
padding: 0.5rem 0;
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
z-index: 100;
|
||||
min-width: 180px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.mobile-menu > ul li a {
|
||||
display: block;
|
||||
padding: 0.5rem 1rem;
|
||||
text-decoration: none;
|
||||
color: var(--pico-color);
|
||||
}
|
||||
|
||||
.mobile-menu > ul li a:hover {
|
||||
background: var(--pico-muted-background-color);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.nav-links { display: none; }
|
||||
.mobile-menu { display: block; }
|
||||
}
|
||||
|
||||
/* Page spacing */
|
||||
.py-4 {
|
||||
padding-top: 1.5rem;
|
||||
padding-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
/* Alerts */
|
||||
.alert {
|
||||
border-radius: var(--pico-border-radius);
|
||||
padding: 0.75rem 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.alert p { margin: 0; }
|
||||
.alert p + p { margin-top: 0.25rem; }
|
||||
|
||||
.alert-danger {
|
||||
background: #fef2f2;
|
||||
border: 1px solid #fecaca;
|
||||
color: #b91c1c;
|
||||
}
|
||||
|
||||
.alert-success {
|
||||
background: #f0fdf4;
|
||||
border: 1px solid #bbf7d0;
|
||||
color: #15803d;
|
||||
}
|
||||
|
||||
.alert-warning {
|
||||
background: #fffbeb;
|
||||
border: 1px solid #fde68a;
|
||||
color: #b45309;
|
||||
}
|
||||
|
||||
/* Cards (using <article>) */
|
||||
article.card {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
article.card > header {
|
||||
padding: 0.75rem 1rem;
|
||||
background: var(--pico-muted-background-color);
|
||||
border-bottom: 1px solid var(--pico-border-color);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
article.card > header h1,
|
||||
article.card > header h2,
|
||||
article.card > header h5 {
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
article.card > .card-body {
|
||||
padding: 1.25rem;
|
||||
}
|
||||
|
||||
article.card > footer {
|
||||
padding: 0.75rem 1rem;
|
||||
background: var(--pico-muted-background-color);
|
||||
border-top: 1px solid var(--pico-border-color);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* List groups */
|
||||
.list-group {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.list-group-item {
|
||||
padding: 0.6rem 1rem;
|
||||
border-bottom: 1px solid var(--pico-border-color);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.list-group-item:last-child { border-bottom: none; }
|
||||
|
||||
/* Badges */
|
||||
.badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
padding: 0.2rem 0.5rem;
|
||||
border-radius: 999px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.badge-success { background: #dcfce7; color: #15803d; }
|
||||
.badge-danger { background: #fee2e2; color: #b91c1c; }
|
||||
.badge-warning { background: #fef3c7; color: #b45309; }
|
||||
.badge-secondary { background: var(--pico-muted-background-color); color: var(--pico-muted-color); }
|
||||
.badge-light { background: var(--pico-muted-background-color); color: var(--pico-muted-color); border: 1px solid var(--pico-border-color); }
|
||||
|
||||
/* Buttons */
|
||||
button.secondary, a[role="button"].secondary {
|
||||
--pico-background-color: var(--brand-secondary);
|
||||
--pico-border-color: var(--brand-secondary);
|
||||
--pico-color: #fff;
|
||||
}
|
||||
|
||||
button.outline.danger, a[role="button"].outline.danger {
|
||||
--pico-color: #dc2626;
|
||||
--pico-border-color: #dc2626;
|
||||
}
|
||||
|
||||
button.danger, a[role="button"].danger {
|
||||
--pico-background-color: #dc2626;
|
||||
--pico-border-color: #dc2626;
|
||||
--pico-color: #fff;
|
||||
}
|
||||
|
||||
button.sm, a[role="button"].sm {
|
||||
padding: 0.25rem 0.6rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
/* Layout helpers */
|
||||
.row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.col { flex: 1 1 0; }
|
||||
.col-auto { flex: 0 0 auto; }
|
||||
|
||||
.col-sm-6 { flex: 0 0 calc(50% - 0.5rem); min-width: 200px; }
|
||||
.col-sm-10 { flex: 0 0 calc(83.33% - 0.5rem); }
|
||||
.col-md-6 { flex: 0 0 calc(50% - 0.5rem); }
|
||||
.col-md-7 { flex: 0 0 calc(58.33% - 0.5rem); }
|
||||
.col-lg-4 { flex: 0 0 calc(33.33% - 0.67rem); }
|
||||
.col-lg-5 { flex: 0 0 calc(41.67% - 0.5rem); }
|
||||
.col-lg-6 { flex: 0 0 calc(50% - 0.5rem); }
|
||||
.col-12 { flex: 0 0 100%; }
|
||||
.col-md-6-auto { flex: 0 0 calc(50% - 0.5rem); }
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.col-sm-6, .col-md-6, .col-md-7, .col-lg-4, .col-lg-5, .col-lg-6, .col-md-6-auto { flex: 0 0 100%; }
|
||||
}
|
||||
|
||||
.justify-center { justify-content: center; }
|
||||
.justify-between { justify-content: space-between; }
|
||||
.justify-end { justify-content: flex-end; }
|
||||
.align-center { align-items: center; }
|
||||
.flex-wrap { flex-wrap: wrap; }
|
||||
.flex-col { flex-direction: column; }
|
||||
.flex-1 { flex: 1; }
|
||||
.flex-fill { flex: 1 1 0; }
|
||||
|
||||
.gap-1 { gap: 0.25rem; }
|
||||
.gap-2 { gap: 0.5rem; }
|
||||
.gap-3 { gap: 0.75rem; }
|
||||
|
||||
.mt-2 { margin-top: 0.5rem; }
|
||||
.mt-3 { margin-top: 0.75rem; }
|
||||
.mt-4 { margin-top: 1.5rem; }
|
||||
.mt-5 { margin-top: 3rem; }
|
||||
.mb-0 { margin-bottom: 0; }
|
||||
.mb-1 { margin-bottom: 0.25rem; }
|
||||
.mb-2 { margin-bottom: 0.5rem; }
|
||||
.mb-3 { margin-bottom: 0.75rem; }
|
||||
.mb-4 { margin-bottom: 1.5rem; }
|
||||
.ms-auto { margin-left: auto; }
|
||||
.me-1 { margin-right: 0.25rem; }
|
||||
.me-2 { margin-right: 0.5rem; }
|
||||
.me-3 { margin-right: 0.75rem; }
|
||||
|
||||
.d-flex { display: flex; }
|
||||
.d-grid { display: grid; }
|
||||
.d-none { display: none; }
|
||||
.d-block { display: block; }
|
||||
|
||||
.text-center { text-align: center; }
|
||||
.text-end { text-align: right; }
|
||||
.text-muted { color: var(--pico-muted-color); }
|
||||
.small { font-size: 0.875rem; }
|
||||
.fs-1 { font-size: 2rem; }
|
||||
.fs-2 { font-size: 1.5rem; }
|
||||
.fs-5 { font-size: 1.15rem; }
|
||||
.fs-6 { font-size: 0.875rem; }
|
||||
|
||||
.text-success { color: #15803d; }
|
||||
.text-danger { color: #dc2626; }
|
||||
.text-warning { color: #b45309; }
|
||||
.text-primary { color: var(--brand-primary); }
|
||||
.text-secondary { color: var(--brand-secondary); }
|
||||
.text-white { color: #fff; }
|
||||
|
||||
.bg-danger-header {
|
||||
background: #dc2626;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.font-monospace { font-family: monospace; }
|
||||
/* ─── Utility ─────────────────────────────────────────────────────────── */
|
||||
.mono { font-family: 'JetBrains Mono', monospace; }
|
||||
.hr { height: 1px; background: #E4E6EE; margin: 18px 0; }
|
||||
.w-100 { width: 100%; }
|
||||
.h-100 { height: 100%; }
|
||||
.text-center { text-align: center; }
|
||||
.d-none { display: none; }
|
||||
|
||||
/* Table */
|
||||
.table-scroll {
|
||||
overflow-x: auto;
|
||||
max-width: 100%;
|
||||
/* ─── Layout ──────────────────────────────────────────────────────────── */
|
||||
.shell { display: flex; min-height: 100vh; }
|
||||
.sidebar {
|
||||
width: 228px; min-height: 100vh; flex-shrink: 0;
|
||||
background: #1C1F2E;
|
||||
display: flex; flex-direction: column;
|
||||
position: sticky; top: 0; height: 100vh; overflow-y: auto;
|
||||
}
|
||||
.main { flex: 1; display: flex; flex-direction: column; min-width: 0; }
|
||||
|
||||
article.card {
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
/* ─── Sidebar ─────────────────────────────────────────────────────────── */
|
||||
.sb-logo {
|
||||
padding: 22px 20px 18px;
|
||||
border-bottom: 1px solid rgba(255,255,255,0.07);
|
||||
display: flex; align-items: center; gap: 11px;
|
||||
}
|
||||
|
||||
table.align-middle {
|
||||
width: 100%;
|
||||
.sb-logo-icon {
|
||||
width: 36px; height: 36px; border-radius: 9px;
|
||||
background: #FF5500;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
table.align-middle td,
|
||||
table.align-middle th {
|
||||
vertical-align: middle;
|
||||
white-space: nowrap;
|
||||
.sb-logo-name { font-size: 15px; font-weight: 800; color: #fff; line-height: 1.1; letter-spacing: -0.02em; }
|
||||
.sb-logo-sub { font-size: 10px; color: #5C6278; font-weight: 500; margin-top: 1px; letter-spacing: 0.03em; }
|
||||
.sb-nav { flex: 1; padding: 10px 12px; display: flex; flex-direction: column; gap: 1px; }
|
||||
.sb-section {
|
||||
font-size: 9.5px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.1em;
|
||||
color: #5C6278; padding: 14px 8px 5px;
|
||||
}
|
||||
|
||||
/* Breadcrumb */
|
||||
.breadcrumb {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0 0 1rem;
|
||||
font-size: 0.9rem;
|
||||
color: var(--pico-muted-color);
|
||||
.sb-item {
|
||||
display: flex; align-items: center; gap: 9px;
|
||||
padding: 9px 10px; border-radius: 8px; cursor: pointer;
|
||||
font-size: 13.5px; font-weight: 500; color: #A8ADC3;
|
||||
transition: all 0.13s; text-decoration: none; position: relative;
|
||||
}
|
||||
|
||||
.breadcrumb-item + .breadcrumb-item::before {
|
||||
content: "/";
|
||||
margin-right: 0.25rem;
|
||||
color: var(--pico-muted-color);
|
||||
.sb-item:hover { background: #252A3D; color: #fff; }
|
||||
.sb-item.active { background: rgba(255,85,0,0.18); color: #fff; }
|
||||
.sb-item.active .sb-icon { color: #FF5500; }
|
||||
.sb-icon { font-size: 16px; width: 20px; text-align: center; flex-shrink: 0; }
|
||||
.sb-badge {
|
||||
margin-left: auto; font-size: 10px; font-weight: 700;
|
||||
padding: 1px 6px; border-radius: 9px;
|
||||
background: rgba(255,85,0,0.25); color: #FF5500;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
}
|
||||
|
||||
.breadcrumb-item.active { color: var(--pico-color); }
|
||||
|
||||
/* Dropdown */
|
||||
.dropdown {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
.sb-badge.err { background: rgba(229,57,53,0.2); color: #E53935; }
|
||||
.sb-user {
|
||||
padding: 14px 16px; border-top: 1px solid rgba(255,255,255,0.07);
|
||||
display: flex; align-items: center; gap: 10px; cursor: pointer;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
display: none;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: calc(100% + 4px);
|
||||
background: var(--pico-background-color);
|
||||
border: 1px solid var(--pico-border-color);
|
||||
border-radius: var(--pico-border-radius);
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.12);
|
||||
z-index: 200;
|
||||
min-width: 220px;
|
||||
padding: 0.25rem 0;
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
.sb-user:hover { background: #252A3D; }
|
||||
.avatar {
|
||||
width: 34px; height: 34px; border-radius: 50%; flex-shrink: 0;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-size: 12px; font-weight: 700; color: #fff;
|
||||
background: linear-gradient(135deg, #FF5500 0%, #FF8C42 100%);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.dropdown.open .dropdown-menu { display: block; }
|
||||
|
||||
.dropdown-item {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 0.45rem 1rem;
|
||||
background: none;
|
||||
border: none;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
color: var(--pico-color);
|
||||
font-size: 0.9rem;
|
||||
text-decoration: none;
|
||||
.avatar.admin { background: linear-gradient(135deg, #6B48FF 0%, #A78BFA 100%); }
|
||||
.sb-user-name { font-size: 12.5px; font-weight: 600; color: #fff; }
|
||||
.sb-user-role { font-size: 10.5px; color: #5C6278; margin-top: 1px; }
|
||||
.role-chip {
|
||||
display: inline-block; font-size: 9px; font-weight: 700;
|
||||
text-transform: uppercase; letter-spacing: 0.05em;
|
||||
padding: 1px 5px; border-radius: 4px;
|
||||
}
|
||||
.role-chip.user { background: rgba(255,85,0,0.2); color: #FF5500; }
|
||||
.role-chip.admin { background: rgba(107,72,255,0.2); color: #A78BFA; }
|
||||
|
||||
.dropdown-item:hover {
|
||||
background: var(--pico-muted-background-color);
|
||||
/* ─── Topbar ──────────────────────────────────────────────────────────── */
|
||||
.topbar {
|
||||
height: 56px; border-bottom: 1px solid #E4E6EE;
|
||||
display: flex; align-items: center; padding: 0 28px; gap: 14px;
|
||||
background: #FFFFFF; position: sticky; top: 0; z-index: 10;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.dropdown-item.muted { color: var(--pico-muted-color); }
|
||||
|
||||
.dropdown-divider {
|
||||
border: none;
|
||||
border-top: 1px solid var(--pico-border-color);
|
||||
margin: 0.25rem 0;
|
||||
.topbar-title { font-size: 15px; font-weight: 700; flex: 1; color: #1C1F2E; }
|
||||
.conn-pill {
|
||||
display: flex; align-items: center; gap: 6px; padding: 5px 12px;
|
||||
border-radius: 20px; border: 1px solid #E4E6EE;
|
||||
font-size: 12px; font-weight: 500; color: #5C6278; background: #F4F5F7;
|
||||
}
|
||||
.dot { width: 7px; height: 7px; border-radius: 50%; flex-shrink: 0; }
|
||||
.dot.g { background: #17A865; box-shadow: 0 0 5px #17A865; }
|
||||
.dot.r { background: #E53935; box-shadow: 0 0 5px #E53935; }
|
||||
.dot.y { background: #F59E0B; box-shadow: 0 0 5px #F59E0B; }
|
||||
.dot.d { background: #CDD0DC; }
|
||||
.dot.pulse { animation: blink 2s ease-in-out infinite; }
|
||||
@keyframes blink { 0%,100%{opacity:1} 50%{opacity:0.4} }
|
||||
|
||||
/* Spinner */
|
||||
/* ─── Content ─────────────────────────────────────────────────────────── */
|
||||
.content { flex: 1; padding: 28px 32px; overflow-y: auto; }
|
||||
.pg-title { font-size: 22px; font-weight: 800; color: #1C1F2E; letter-spacing: -0.02em; }
|
||||
.pg-sub { font-size: 13px; color: #5C6278; margin-top: 3px; margin-bottom: 22px; }
|
||||
|
||||
/* ─── Cards ───────────────────────────────────────────────────────────── */
|
||||
.card {
|
||||
background: #FFFFFF; border: 1px solid #E4E6EE;
|
||||
border-radius: 12px; padding: 22px 24px;
|
||||
}
|
||||
.card-title { font-size: 14px; font-weight: 700; color: #1C1F2E; }
|
||||
.card-sub { font-size: 12px; color: #5C6278; margin-top: 2px; }
|
||||
.card-hd { display: flex; align-items: flex-start; justify-content: space-between; margin-bottom: 16px; }
|
||||
|
||||
/* ─── Stat cards ──────────────────────────────────────────────────────── */
|
||||
.stat-grid { display: grid; grid-template-columns: repeat(3,1fr); gap: 14px; margin-bottom: 20px; }
|
||||
.stat-card {
|
||||
background: #FFFFFF; border: 1px solid #E4E6EE; border-radius: 12px;
|
||||
padding: 18px 20px; position: relative; overflow: hidden;
|
||||
}
|
||||
.stat-card::before {
|
||||
content:''; position: absolute; top: 0; left: 0; right: 0; height: 3px; border-radius: 12px 12px 0 0;
|
||||
}
|
||||
.stat-card.or::before { background: #FF5500; }
|
||||
.stat-card.gr::before { background: #17A865; }
|
||||
.stat-card.yl::before { background: #F59E0B; }
|
||||
.stat-card.rd::before { background: #E53935; }
|
||||
.stat-card.bl::before { background: #3B6FFF; }
|
||||
.stat-lbl { font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.07em; color: #9EA8BE; }
|
||||
.stat-val { font-size: 30px; font-weight: 800; color: #1C1F2E; margin: 5px 0 2px; font-family: 'JetBrains Mono', monospace; letter-spacing: -0.03em; }
|
||||
.stat-delta { font-size: 12px; color: #5C6278; }
|
||||
.stat-icon { position: absolute; right: 16px; top: 50%; transform: translateY(-50%); font-size: 28px; opacity: 0.1; }
|
||||
|
||||
/* ─── Grids ───────────────────────────────────────────────────────────── */
|
||||
.g2 { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
|
||||
.g3 { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 14px; }
|
||||
.g4 { display: grid; grid-template-columns: repeat(4,1fr); gap: 14px; }
|
||||
|
||||
/* ─── Buttons ─────────────────────────────────────────────────────────── */
|
||||
.btn {
|
||||
display: inline-flex; align-items: center; gap: 7px;
|
||||
padding: 8px 18px; border-radius: 8px; font-size: 13px; font-weight: 600;
|
||||
cursor: pointer; border: none; font-family: 'Golos Text', sans-serif;
|
||||
transition: all 0.14s; white-space: nowrap; text-decoration: none;
|
||||
}
|
||||
.btn-primary { background: #FF5500; color: #fff; }
|
||||
.btn-primary:hover { background: #E64D00; box-shadow: 0 3px 12px rgba(255,85,0,0.3); }
|
||||
.btn-outline { background: transparent; border: 1.5px solid #E4E6EE; color: #5C6278; }
|
||||
.btn-outline:hover { border-color: #CDD0DC; color: #1C1F2E; background: #F9FAFB; }
|
||||
.btn-ghost { background: transparent; color: #5C6278; padding: 6px 10px; border: none; }
|
||||
.btn-ghost:hover { color: #1C1F2E; background: #F4F5F7; }
|
||||
.btn-danger { background: #FEF1F1; color: #E53935; border: 1.5px solid #F4AEAE; }
|
||||
.btn-danger:hover { background: #fce4e4; }
|
||||
.btn-sm { padding: 6px 13px; font-size: 12px; border-radius: 7px; }
|
||||
.btn-xs { padding: 4px 9px; font-size: 11.5px; border-radius: 6px; }
|
||||
|
||||
/* ─── Tags ────────────────────────────────────────────────────────────── */
|
||||
.tag {
|
||||
display: inline-flex; align-items: center; gap: 5px;
|
||||
padding: 3px 8px; border-radius: 5px; font-size: 11.5px; font-weight: 600;
|
||||
}
|
||||
.tag-gr { background: #EDFAF4; color: #17A865; border: 1px solid #A7E8CC; }
|
||||
.tag-rd { background: #FEF1F1; color: #E53935; border: 1px solid #F4AEAE; }
|
||||
.tag-yl { background: #FFFBEB; color: #F59E0B; border: 1px solid #FCD678; }
|
||||
.tag-or { background: #FFF2EC; color: #FF5500; border: 1px solid #FFD9C7; }
|
||||
.tag-dim { background: #F4F5F7; color: #5C6278; border: 1px solid #E4E6EE; }
|
||||
.tag-bl { background: #EEF3FF; color: #3B6FFF; border: 1px solid #C7D7FF; }
|
||||
|
||||
/* ─── Table ───────────────────────────────────────────────────────────── */
|
||||
.tbl { width: 100%; border-collapse: collapse; }
|
||||
.tbl th {
|
||||
text-align: left; font-size: 10.5px; font-weight: 700;
|
||||
text-transform: uppercase; letter-spacing: 0.08em; color: #9EA8BE;
|
||||
padding: 10px 16px; border-bottom: 1.5px solid #E4E6EE;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.tbl td { padding: 12px 16px; font-size: 13px; border-bottom: 1px solid #E4E6EE; vertical-align: middle; }
|
||||
.tbl tr:last-child td { border-bottom: none; }
|
||||
.tbl tbody tr:hover td { background: #F9FAFB; }
|
||||
.tbl-name { font-weight: 600; color: #1C1F2E; }
|
||||
.tbl-sub { font-size: 11px; color: #5C6278; margin-top: 1px; }
|
||||
|
||||
/* ─── Toggle ──────────────────────────────────────────────────────────── */
|
||||
.tog {
|
||||
width: 38px; height: 22px; border-radius: 11px;
|
||||
background: #E4E6EE; position: relative; cursor: pointer; transition: background 0.18s; flex-shrink: 0;
|
||||
display: inline-block;
|
||||
}
|
||||
.tog.on { background: #FF5500; }
|
||||
.tog::after {
|
||||
content:''; position: absolute; top: 3px; left: 3px;
|
||||
width: 16px; height: 16px; background: #fff; border-radius: 50%;
|
||||
transition: transform 0.18s; box-shadow: 0 1px 4px rgba(0,0,0,0.15);
|
||||
}
|
||||
.tog.on::after { transform: translateX(16px); }
|
||||
|
||||
/* ─── Progress ────────────────────────────────────────────────────────── */
|
||||
.prog { height: 5px; background: #E4E6EE; border-radius: 3px; overflow: hidden; }
|
||||
.prog-fill { height: 100%; border-radius: 3px; transition: width 0.4s; }
|
||||
.prog-fill.or { background: #FF5500; }
|
||||
.prog-fill.gr { background: #17A865; }
|
||||
|
||||
/* ─── Inputs ──────────────────────────────────────────────────────────── */
|
||||
.inp {
|
||||
background: #FFFFFF; border: 1.5px solid #E4E6EE; border-radius: 8px;
|
||||
padding: 8px 12px; font-size: 13px; color: #1C1F2E;
|
||||
font-family: 'Golos Text', sans-serif; outline: none;
|
||||
transition: border-color 0.14s; width: 100%;
|
||||
display: block;
|
||||
}
|
||||
.inp:focus { border-color: #FF5500; box-shadow: 0 0 0 3px #FFF2EC; }
|
||||
.inp::placeholder { color: #9EA8BE; }
|
||||
select.inp { cursor: pointer; }
|
||||
.form-row { margin-bottom: 14px; }
|
||||
.form-lbl { font-size: 11.5px; font-weight: 600; color: #5C6278; margin-bottom: 5px; text-transform: uppercase; letter-spacing: 0.04em; display: block; }
|
||||
|
||||
/* ─── Alerts ──────────────────────────────────────────────────────────── */
|
||||
.alert { border-radius: 9px; padding: 11px 14px; font-size: 12.5px; display: flex; gap: 9px; margin-bottom: 16px; }
|
||||
.alert-gr { background: #EDFAF4; border: 1px solid #A7E8CC; color: #136B41; }
|
||||
.alert-yl { background: #FFFBEB; border: 1px solid #FCD678; color: #92680A; }
|
||||
.alert-bl { background: #EEF3FF; border: 1px solid #C7D7FF; color: #3B6FFF; }
|
||||
.alert-rd { background: #FEF1F1; border: 1px solid #F4AEAE; color: #E53935; }
|
||||
|
||||
/* ─── Tabs ────────────────────────────────────────────────────────────── */
|
||||
.tabs { display: flex; gap: 0; border-bottom: 1.5px solid #E4E6EE; margin-bottom: 20px; }
|
||||
.tab {
|
||||
padding: 9px 18px; font-size: 13px; font-weight: 600; color: #5C6278;
|
||||
cursor: pointer; border-bottom: 2.5px solid transparent; margin-bottom: -1.5px; transition: all 0.13s;
|
||||
text-decoration: none; display: inline-block;
|
||||
}
|
||||
.tab:hover { color: #1C1F2E; }
|
||||
.tab.active { color: #FF5500; border-bottom-color: #FF5500; }
|
||||
|
||||
/* ─── Conn detail ─────────────────────────────────────────────────────── */
|
||||
.conn-detail {
|
||||
background: #F4F5F7; border: 1px solid #E4E6EE; border-radius: 9px;
|
||||
padding: 12px 16px; display: flex; flex-direction: column; gap: 9px;
|
||||
}
|
||||
.conn-row { display: flex; align-items: center; justify-content: space-between; }
|
||||
.conn-k { font-size: 12px; color: #9EA8BE; }
|
||||
.conn-v { font-size: 12px; font-weight: 600; color: #1C1F2E; font-family: 'JetBrains Mono', monospace; }
|
||||
|
||||
/* ─── Activity ────────────────────────────────────────────────────────── */
|
||||
.act-item { display: flex; gap: 10px; padding: 10px 0; border-bottom: 1px solid #E4E6EE; }
|
||||
.act-item:last-child { border-bottom: none; }
|
||||
.act-icon { width: 32px; height: 32px; border-radius: 8px; display: flex; align-items: center; justify-content: center; font-size: 14px; flex-shrink: 0; }
|
||||
.act-text { font-size: 12.5px; color: #1C1F2E; }
|
||||
.act-time { font-size: 11px; color: #9EA8BE; margin-top: 2px; font-family: 'JetBrains Mono', monospace; }
|
||||
|
||||
/* ─── Section sep ─────────────────────────────────────────────────────── */
|
||||
.section-sep { display: flex; align-items: center; gap: 10px; margin: 20px 0; }
|
||||
.section-sep-line { flex: 1; height: 1px; background: #E4E6EE; }
|
||||
.section-sep-label { font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.06em; color: #9EA8BE; }
|
||||
|
||||
/* ─── Pipeline ────────────────────────────────────────────────────────── */
|
||||
.pipeline { display: flex; align-items: center; padding: 18px 0; }
|
||||
.pipe-step { display: flex; flex-direction: column; align-items: center; gap: 7px; flex: 1; }
|
||||
.pipe-node { width: 46px; height: 46px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 18px; border: 2px solid; }
|
||||
.pipe-node.done { border-color: #17A865; background: #EDFAF4; }
|
||||
.pipe-node.active { border-color: #FF5500; background: #FFF2EC; }
|
||||
.pipe-node.idle { border-color: #E4E6EE; background: #F4F5F7; }
|
||||
.pipe-lbl { font-size: 11px; font-weight: 600; color: #5C6278; text-align: center; }
|
||||
.pipe-line { flex: 1; height: 2px; max-width: 52px; align-self: flex-start; margin-top: 22px; }
|
||||
.pipe-line.done { background: #17A865; }
|
||||
.pipe-line.idle { background: #E4E6EE; }
|
||||
|
||||
/* ─── Sync log ────────────────────────────────────────────────────────── */
|
||||
.log-row { display: flex; align-items: flex-start; gap: 10px; padding: 10px 0; border-bottom: 1px solid #E4E6EE; }
|
||||
.log-row:last-child { border-bottom: none; }
|
||||
.log-bullet { width: 8px; height: 8px; border-radius: 50%; margin-top: 4px; flex-shrink: 0; }
|
||||
.log-msg { font-size: 12.5px; color: #1C1F2E; }
|
||||
.log-meta { font-size: 11px; color: #9EA8BE; font-family: 'JetBrains Mono', monospace; margin-top: 2px; }
|
||||
|
||||
/* ─── User row actions ────────────────────────────────────────────────── */
|
||||
.action-row { display: flex; gap: 6px; }
|
||||
|
||||
/* ─── Inline status ───────────────────────────────────────────────────── */
|
||||
.inline-st { display: flex; align-items: center; gap: 5px; font-size: 12.5px; font-weight: 500; }
|
||||
|
||||
/* ─── List group (legacy compat) ──────────────────────────────────────── */
|
||||
.list-group { list-style: none; padding: 0; margin: 0; }
|
||||
.list-group-item {
|
||||
padding: 10px 0; border-bottom: 1px solid #E4E6EE;
|
||||
display: flex; justify-content: space-between; align-items: center;
|
||||
font-size: 13px;
|
||||
}
|
||||
.list-group-item:last-child { border-bottom: none; }
|
||||
.list-group-item .lbl { font-size: 12px; color: #9EA8BE; }
|
||||
.list-group-item .val { font-size: 12px; font-weight: 600; color: #1C1F2E; font-family: 'JetBrains Mono', monospace; }
|
||||
|
||||
/* ─── Breadcrumb ──────────────────────────────────────────────────────── */
|
||||
.breadcrumb { display: flex; align-items: center; gap: 6px; margin-bottom: 16px; font-size: 12px; color: #9EA8BE; list-style: none; padding: 0; }
|
||||
.breadcrumb a { color: #5C6278; text-decoration: none; }
|
||||
.breadcrumb a:hover { color: #FF5500; }
|
||||
.breadcrumb li + li::before { content: "/"; margin-right: 6px; color: #CDD0DC; }
|
||||
|
||||
/* ─── Empty state ─────────────────────────────────────────────────────── */
|
||||
.empty-state {
|
||||
text-align: center; padding: 48px 24px; color: #9EA8BE;
|
||||
}
|
||||
.empty-state i { font-size: 2.5rem; display: block; margin-bottom: 12px; }
|
||||
.empty-state p { font-size: 13px; }
|
||||
|
||||
/* ─── Table scroll wrapper ────────────────────────────────────────────── */
|
||||
.table-wrap { overflow-x: auto; width: 100%; }
|
||||
|
||||
/* ─── Pagination ──────────────────────────────────────────────────────── */
|
||||
.pagination { display: flex; align-items: center; justify-content: center; gap: 8px; padding: 16px; }
|
||||
|
||||
/* ─── Spinner ─────────────────────────────────────────────────────────── */
|
||||
.spinner {
|
||||
display: inline-block;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
border: 3px solid var(--pico-muted-background-color);
|
||||
border-top-color: var(--brand-primary);
|
||||
border-radius: 50%;
|
||||
animation: spin 0.75s linear infinite;
|
||||
display: inline-block; width: 20px; height: 20px;
|
||||
border: 2px solid #E4E6EE; border-top-color: #FF5500;
|
||||
border-radius: 50%; animation: spin 0.75s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
|
||||
/* Input group */
|
||||
.input-group {
|
||||
display: flex;
|
||||
gap: 0;
|
||||
/* ─── Code ────────────────────────────────────────────────────────────── */
|
||||
pre, code { font-family: 'JetBrains Mono', monospace; font-size: 12px; }
|
||||
pre {
|
||||
background: #F4F5F7; border: 1px solid #E4E6EE; border-radius: 8px;
|
||||
padding: 12px 14px; overflow-x: auto; white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.input-group input {
|
||||
border-radius: var(--pico-border-radius) 0 0 var(--pico-border-radius);
|
||||
margin: 0;
|
||||
flex: 1;
|
||||
/* ─── Dialog ──────────────────────────────────────────────────────────── */
|
||||
dialog {
|
||||
border: none; border-radius: 14px; padding: 0;
|
||||
box-shadow: 0 8px 40px rgba(0,0,0,0.16);
|
||||
max-width: 520px; width: 100%;
|
||||
}
|
||||
dialog::backdrop { background: rgba(0,0,0,0.45); }
|
||||
.dialog-hd {
|
||||
padding: 20px 24px 0; display: flex; align-items: center;
|
||||
justify-content: space-between; margin-bottom: 16px;
|
||||
}
|
||||
.dialog-title { font-size: 16px; font-weight: 800; color: #1C1F2E; }
|
||||
.dialog-body { padding: 0 24px 24px; }
|
||||
.dialog-close {
|
||||
background: none; border: none; cursor: pointer; font-size: 18px;
|
||||
color: #9EA8BE; padding: 4px; border-radius: 6px; transition: color 0.13s;
|
||||
}
|
||||
.dialog-close:hover { color: #1C1F2E; }
|
||||
|
||||
/* ─── Filter bar ──────────────────────────────────────────────────────── */
|
||||
.filter-bar {
|
||||
display: flex; gap: 8px; flex-wrap: wrap; align-items: center;
|
||||
padding: 14px 20px; border-bottom: 1px solid #E4E6EE;
|
||||
}
|
||||
|
||||
.input-group button {
|
||||
border-radius: 0 var(--pico-border-radius) var(--pico-border-radius) 0;
|
||||
margin: 0;
|
||||
white-space: nowrap;
|
||||
/* ─── Viewed-as banner ────────────────────────────────────────────────── */
|
||||
.view-as-bar {
|
||||
background: #E64D00; color: #fff; text-align: center;
|
||||
padding: 6px 16px; font-size: 13px;
|
||||
}
|
||||
.view-as-bar strong { font-weight: 700; }
|
||||
|
||||
/* Empty state */
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 3rem 1rem;
|
||||
color: var(--pico-muted-color);
|
||||
/* ──────────────────────────────────────────────────────────────────────── */
|
||||
/* LOGIN PAGE */
|
||||
/* ──────────────────────────────────────────────────────────────────────── */
|
||||
.login-wrap { min-height: 100vh; display: flex; background: #F4F5F7; }
|
||||
.login-left {
|
||||
flex: 1; background: #1C1F2E; display: flex; flex-direction: column;
|
||||
justify-content: space-between; padding: 48px;
|
||||
position: relative; overflow: hidden;
|
||||
}
|
||||
|
||||
.empty-state .empty-icon {
|
||||
font-size: 3rem;
|
||||
display: block;
|
||||
margin-bottom: 0.75rem;
|
||||
.login-left-bg {
|
||||
position: absolute; inset: 0; pointer-events: none;
|
||||
background:
|
||||
radial-gradient(ellipse 70% 55% at 0% 110%, rgba(255,85,0,0.18) 0%, transparent 65%),
|
||||
radial-gradient(ellipse 50% 40% at 100% -10%, rgba(255,85,0,0.10) 0%, transparent 60%);
|
||||
}
|
||||
.login-left-pattern {
|
||||
position: absolute; inset: 0; pointer-events: none;
|
||||
background-image: radial-gradient(circle, rgba(255,255,255,0.04) 1px, transparent 1px);
|
||||
background-size: 28px 28px;
|
||||
}
|
||||
.login-brand { display: flex; align-items: center; gap: 12px; position: relative; z-index: 1; }
|
||||
.login-brand-icon { width: 44px; height: 44px; border-radius: 11px; background: #FF5500; display: flex; align-items: center; justify-content: center; }
|
||||
.login-brand-name { font-size: 20px; font-weight: 800; color: #fff; letter-spacing: -0.02em; }
|
||||
.login-brand-sub { font-size: 11px; color: #5C6278; }
|
||||
.login-hero { position: relative; z-index: 1; }
|
||||
.login-hero-title { font-size: 32px; font-weight: 800; color: #fff; line-height: 1.25; letter-spacing: -0.03em; margin-bottom: 14px; }
|
||||
.login-hero-title em { color: #FF5500; font-style: normal; }
|
||||
.login-hero-body { font-size: 14px; color: #A8ADC3; line-height: 1.6; max-width: 340px; }
|
||||
.login-chips { display: flex; gap: 10px; margin-top: 22px; flex-wrap: wrap; }
|
||||
.login-chip {
|
||||
display: flex; align-items: center; gap: 7px; padding: 7px 13px;
|
||||
border-radius: 20px; background: rgba(255,255,255,0.07); border: 1px solid rgba(255,255,255,0.1);
|
||||
font-size: 12px; color: rgba(255,255,255,0.65); font-weight: 500;
|
||||
}
|
||||
.login-footer { font-size: 11px; color: #5C6278; position: relative; z-index: 1; }
|
||||
.login-right { width: 460px; flex-shrink: 0; display: flex; align-items: center; justify-content: center; padding: 40px; }
|
||||
.login-box { width: 100%; max-width: 360px; }
|
||||
.login-box-title { font-size: 24px; font-weight: 800; color: #1C1F2E; letter-spacing: -0.02em; margin-bottom: 4px; }
|
||||
.login-box-sub { font-size: 13px; color: #5C6278; margin-bottom: 28px; }
|
||||
.login-btn {
|
||||
width: 100%; padding: 11px; border-radius: 9px; border: none; cursor: pointer;
|
||||
background: #FF5500; color: #fff; font-size: 14px; font-weight: 700;
|
||||
font-family: 'Golos Text', sans-serif; transition: all 0.15s; margin-top: 6px; display: block;
|
||||
}
|
||||
.login-btn:hover { background: #E64D00; box-shadow: 0 6px 20px rgba(255,85,0,0.3); }
|
||||
.login-btn:disabled { opacity: 0.6; cursor: not-allowed; }
|
||||
.login-divider { display: flex; align-items: center; gap: 10px; margin: 18px 0; }
|
||||
.login-divider span { font-size: 11px; color: #9EA8BE; }
|
||||
.login-divider::before, .login-divider::after { content:''; flex: 1; height: 1px; background: #E4E6EE; }
|
||||
.login-hint { font-size: 11.5px; color: #9EA8BE; text-align: center; margin-top: 14px; line-height: 1.5; }
|
||||
.login-hint a { color: #FF5500; text-decoration: none; }
|
||||
.login-hint a:hover { text-decoration: underline; }
|
||||
|
||||
@@ -1,147 +1,147 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}API Логи — ЭВОСИНК{% endblock %}
|
||||
{% block title %}API Логи — Мои Товары{% endblock %}
|
||||
{% block page_title %}API Логи{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="d-flex justify-between align-center mb-3">
|
||||
<h1 style="font-size:1.3rem; margin:0;"><i class="bi bi-journal-text me-2"></i>API Логи</h1>
|
||||
<span class="text-muted small">Найдено: {{ total }}</span>
|
||||
<div class="pg-title">API Логи</div>
|
||||
<div class="pg-sub">Журнал всех исходящих запросов · Найдено: {{ total }}</div>
|
||||
|
||||
<!-- Filters -->
|
||||
<div class="card" style="margin-bottom:14px;padding:14px 20px;">
|
||||
<form method="get" action="/admin/logs" style="display:flex;flex-wrap:wrap;gap:8px;align-items:center;">
|
||||
<select class="inp" name="service" style="width:auto;">
|
||||
<option value="" {% if not filter_service %}selected{% endif %}>Все сервисы</option>
|
||||
<option value="evotor" {% if filter_service == 'evotor' %}selected{% endif %}>Эвотор</option>
|
||||
<option value="vk" {% if filter_service == 'vk' %}selected{% endif %}>ВКонтакте</option>
|
||||
<option value="other" {% if filter_service == 'other' %}selected{% endif %}>Другое</option>
|
||||
</select>
|
||||
<select class="inp" name="method" style="width:auto;">
|
||||
<option value="" {% if not filter_method %}selected{% endif %}>Все методы</option>
|
||||
<option value="GET" {% if filter_method == 'GET' %}selected{% endif %}>GET</option>
|
||||
<option value="POST" {% if filter_method == 'POST' %}selected{% endif %}>POST</option>
|
||||
</select>
|
||||
<select class="inp" name="status" style="width:auto;">
|
||||
<option value="" {% if not filter_status %}selected{% endif %}>Любой статус</option>
|
||||
<option value="ok" {% if filter_status == 'ok' %}selected{% endif %}>2xx / 3xx</option>
|
||||
<option value="error" {% if filter_status == 'error' %}selected{% endif %}>4xx / 5xx</option>
|
||||
<option value="200" {% if filter_status == '200' %}selected{% endif %}>200</option>
|
||||
<option value="401" {% if filter_status == '401' %}selected{% endif %}>401</option>
|
||||
<option value="403" {% if filter_status == '403' %}selected{% endif %}>403</option>
|
||||
<option value="429" {% if filter_status == '429' %}selected{% endif %}>429</option>
|
||||
<option value="500" {% if filter_status == '500' %}selected{% endif %}>500</option>
|
||||
</select>
|
||||
<select class="inp" name="hours" style="width:auto;">
|
||||
<option value="1" {% if filter_hours == 1 %}selected{% endif %}>Последний час</option>
|
||||
<option value="6" {% if filter_hours == 6 %}selected{% endif %}>6 часов</option>
|
||||
<option value="24" {% if filter_hours == 24 %}selected{% endif %}>24 часа</option>
|
||||
<option value="168" {% if filter_hours == 168 or (not filter_hours) %}selected{% endif %}>7 дней</option>
|
||||
<option value="720" {% if filter_hours == 720 %}selected{% endif %}>30 дней</option>
|
||||
</select>
|
||||
<input class="inp" type="search" name="q" value="{{ filter_q }}"
|
||||
placeholder="URL или тело ответа…" style="flex:1;min-width:160px;">
|
||||
<button type="submit" class="btn btn-primary btn-sm">Применить</button>
|
||||
{% if filter_service or filter_method or filter_status or filter_q or filter_hours != 24 %}
|
||||
<a href="/admin/logs" class="btn btn-outline btn-sm">Сбросить</a>
|
||||
{% endif %}
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{# ── filters ── #}
|
||||
<form method="get" action="/admin/logs" class="mb-3" style="display:flex; flex-wrap:wrap; gap:0.5rem; align-items:center;">
|
||||
<select name="service" style="width:auto;">
|
||||
<option value="" {% if not filter_service %}selected{% endif %}>Все сервисы</option>
|
||||
<option value="evotor" {% if filter_service == 'evotor' %}selected{% endif %}>Эвотор</option>
|
||||
<option value="vk" {% if filter_service == 'vk' %}selected{% endif %}>ВКонтакте</option>
|
||||
<option value="other" {% if filter_service == 'other' %}selected{% endif %}>Другое</option>
|
||||
</select>
|
||||
<select name="method" style="width:auto;">
|
||||
<option value="" {% if not filter_method %}selected{% endif %}>Все методы</option>
|
||||
<option value="GET" {% if filter_method == 'GET' %}selected{% endif %}>GET</option>
|
||||
<option value="POST" {% if filter_method == 'POST' %}selected{% endif %}>POST</option>
|
||||
</select>
|
||||
<select name="status" style="width:auto;">
|
||||
<option value="" {% if not filter_status %}selected{% endif %}>Любой статус</option>
|
||||
<option value="ok" {% if filter_status == 'ok' %}selected{% endif %}>2xx / 3xx</option>
|
||||
<option value="error" {% if filter_status == 'error' %}selected{% endif %}>4xx / 5xx</option>
|
||||
<option value="200" {% if filter_status == '200' %}selected{% endif %}>200</option>
|
||||
<option value="401" {% if filter_status == '401' %}selected{% endif %}>401</option>
|
||||
<option value="403" {% if filter_status == '403' %}selected{% endif %}>403</option>
|
||||
<option value="429" {% if filter_status == '429' %}selected{% endif %}>429</option>
|
||||
<option value="500" {% if filter_status == '500' %}selected{% endif %}>500</option>
|
||||
</select>
|
||||
<select name="hours" style="width:auto;">
|
||||
<option value="1" {% if filter_hours == 1 %}selected{% endif %}>Последний час</option>
|
||||
<option value="6" {% if filter_hours == 6 %}selected{% endif %}>6 часов</option>
|
||||
<option value="24" {% if filter_hours == 24 %}selected{% endif %}>24 часа</option>
|
||||
<option value="168" {% if filter_hours == 168 or (not filter_hours) %}selected{% endif %}>7 дней</option>
|
||||
<option value="720" {% if filter_hours == 720 %}selected{% endif %}>30 дней</option>
|
||||
</select>
|
||||
<input type="search" name="q" value="{{ filter_q }}" placeholder="URL или тело ответа…" style="flex:1; min-width:160px;">
|
||||
<button type="submit">Применить</button>
|
||||
{% if filter_service or filter_method or filter_status or filter_q or filter_hours != 24 %}
|
||||
<a href="/admin/logs" role="button" class="outline secondary">Сбросить</a>
|
||||
<div class="card" style="padding:0;">
|
||||
{% if logs %}
|
||||
<div class="table-wrap">
|
||||
<table class="tbl" style="font-size:12px;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:150px;">Время</th>
|
||||
<th style="width:80px;">Сервис</th>
|
||||
<th style="width:50px;">Метод</th>
|
||||
<th style="width:60px;">Статус</th>
|
||||
<th style="width:70px;">Мс</th>
|
||||
<th>URL</th>
|
||||
<th style="width:32px;"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for log in logs %}
|
||||
{% set is_error = log.response_status and log.response_status >= 400 %}
|
||||
<tr style="cursor:pointer;" onclick="toggleDetail({{ log.id }})">
|
||||
<td><span class="mono" style="font-size:11px;color:#9EA8BE;">{{ log.created_at | datefmt }}</span></td>
|
||||
<td>
|
||||
{% if log.service == 'evotor' %}
|
||||
<span class="tag tag-bl" style="font-size:10px;padding:2px 6px;">{{ log.service }}</span>
|
||||
{% elif log.service == 'vk' %}
|
||||
<span class="tag tag-or" style="font-size:10px;padding:2px 6px;">{{ log.service }}</span>
|
||||
{% else %}
|
||||
<span class="tag tag-dim" style="font-size:10px;padding:2px 6px;">{{ log.service }}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td><span class="mono" style="font-size:11px;">{{ log.method }}</span></td>
|
||||
<td>
|
||||
{% if log.response_status %}
|
||||
<span class="mono" style="font-size:11px;color:{% if is_error %}#E53935{% else %}#17A865{% endif %};">{{ log.response_status }}</span>
|
||||
{% else %}
|
||||
<span style="color:#9EA8BE;">—</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td><span class="mono" style="font-size:11px;color:#9EA8BE;">{{ log.duration_ms if log.duration_ms is not none else '—' }}</span></td>
|
||||
<td style="max-width:400px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">
|
||||
<span class="mono" style="font-size:11px;{% if is_error %}color:#E53935;{% endif %}" title="{{ log.url }}">{{ log.url }}</span>
|
||||
</td>
|
||||
<td style="color:#9EA8BE;"><i class="bi bi-chevron-down"></i></td>
|
||||
</tr>
|
||||
<tr id="detail-{{ log.id }}" style="display:none;background:#F9FAFB;">
|
||||
<td colspan="7" style="padding:14px 20px;">
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px;">
|
||||
<div>
|
||||
<div style="font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:0.06em;color:#9EA8BE;margin-bottom:6px;">URL</div>
|
||||
<code style="word-break:break-all;font-size:11px;">{{ log.url }}</code>
|
||||
{% if log.request_body %}
|
||||
<div style="font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:0.06em;color:#9EA8BE;margin:10px 0 6px;">Request body</div>
|
||||
<pre style="font-size:11px;max-height:180px;overflow:auto;">{{ log.request_body }}</pre>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:0.06em;color:#9EA8BE;margin-bottom:6px;">Response ({{ log.response_status }})</div>
|
||||
{% if log.response_body %}
|
||||
<pre style="font-size:11px;max-height:180px;overflow:auto;">{{ log.response_body }}</pre>
|
||||
{% else %}
|
||||
<span style="color:#9EA8BE;font-size:12px;">—</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{% if total_pages > 1 %}
|
||||
<div class="pagination">
|
||||
{% if page > 1 %}
|
||||
<a href="?service={{ filter_service }}&method={{ filter_method }}&status={{ filter_status }}&q={{ filter_q }}&hours={{ filter_hours }}&page={{ page - 1 }}" class="btn btn-outline btn-sm">← Назад</a>
|
||||
{% endif %}
|
||||
</form>
|
||||
|
||||
<article class="card" style="padding:0;">
|
||||
{% if logs %}
|
||||
<div class="table-scroll">
|
||||
<table class="align-middle" style="font-size:0.82rem;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:140px;">Время</th>
|
||||
<th style="width:60px;">Сервис</th>
|
||||
<th style="width:40px;">Метод</th>
|
||||
<th style="width:50px;">Статус</th>
|
||||
<th style="width:60px;">Мс</th>
|
||||
<th>URL</th>
|
||||
<th style="width:30px;"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for log in logs %}
|
||||
{% set is_error = log.response_status and log.response_status >= 400 %}
|
||||
<tr class="{{ 'text-danger' if is_error else '' }}" style="cursor:pointer;" onclick="toggleDetail({{ log.id }})">
|
||||
<td class="text-muted">{{ log.created_at | datefmt }}</td>
|
||||
<td>
|
||||
<span class="badge {{ 'badge-evotor' if log.service == 'evotor' else 'badge-vk' if log.service == 'vk' else '' }}">
|
||||
{{ log.service }}
|
||||
</span>
|
||||
</td>
|
||||
<td><code>{{ log.method }}</code></td>
|
||||
<td>
|
||||
{% if log.response_status %}
|
||||
<span class="{{ 'text-danger' if is_error else 'text-muted' }}">{{ log.response_status }}</span>
|
||||
{% else %}
|
||||
<span class="text-muted">—</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-muted">{{ log.duration_ms if log.duration_ms is not none else '—' }}</td>
|
||||
<td style="max-width:400px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;">
|
||||
<span title="{{ log.url }}">{{ log.url }}</span>
|
||||
</td>
|
||||
<td class="text-muted"><i class="bi bi-chevron-down"></i></td>
|
||||
</tr>
|
||||
<tr id="detail-{{ log.id }}" style="display:none; background:var(--pico-card-background-color);">
|
||||
<td colspan="7" style="padding:0.75rem 1rem;">
|
||||
<div style="display:grid; grid-template-columns:1fr 1fr; gap:1rem;">
|
||||
<div>
|
||||
<div class="text-muted small mb-1"><strong>URL</strong></div>
|
||||
<code style="word-break:break-all; font-size:0.78rem;">{{ log.url }}</code>
|
||||
{% if log.request_body %}
|
||||
<div class="text-muted small mt-2 mb-1"><strong>Request body</strong></div>
|
||||
<pre style="font-size:0.75rem; max-height:200px; overflow:auto; margin:0; background:var(--pico-code-background-color); padding:0.5rem; border-radius:4px;">{{ log.request_body }}</pre>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-muted small mb-1"><strong>Response ({{ log.response_status }})</strong></div>
|
||||
{% if log.response_body %}
|
||||
<pre style="font-size:0.75rem; max-height:200px; overflow:auto; margin:0; background:var(--pico-code-background-color); padding:0.5rem; border-radius:4px;">{{ log.response_body }}</pre>
|
||||
{% else %}
|
||||
<span class="text-muted">—</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{# ── pagination ── #}
|
||||
{% if total_pages > 1 %}
|
||||
<div style="display:flex; justify-content:center; gap:0.5rem; padding:1rem;">
|
||||
{% if page > 1 %}
|
||||
<a href="?service={{ filter_service }}&method={{ filter_method }}&status={{ filter_status }}&q={{ filter_q }}&hours={{ filter_hours }}&page={{ page - 1 }}" role="button" class="outline secondary sm">← Назад</a>
|
||||
{% endif %}
|
||||
<span class="text-muted" style="line-height:2.2rem;">Стр. {{ page }} / {{ total_pages }}</span>
|
||||
{% if page < total_pages %}
|
||||
<a href="?service={{ filter_service }}&method={{ filter_method }}&status={{ filter_status }}&q={{ filter_q }}&hours={{ filter_hours }}&page={{ page + 1 }}" role="button" class="outline secondary sm">Вперёд →</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<span style="font-size:12px;color:#9EA8BE;">Стр. {{ page }} / {{ total_pages }}</span>
|
||||
{% if page < total_pages %}
|
||||
<a href="?service={{ filter_service }}&method={{ filter_method }}&status={{ filter_status }}&q={{ filter_q }}&hours={{ filter_hours }}&page={{ page + 1 }}" class="btn btn-outline btn-sm">Вперёд →</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% else %}
|
||||
<div class="text-center py-5 text-muted">
|
||||
<i class="bi bi-journal-x" style="font-size:2rem;"></i>
|
||||
<p class="mt-2">Записей не найдено за выбранный период.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</article>
|
||||
|
||||
<style>
|
||||
.badge { display:inline-block; padding:0.1rem 0.4rem; border-radius:4px; font-size:0.75rem; font-weight:600; }
|
||||
.badge-evotor { background:#e8f4fd; color:#0986E2; }
|
||||
.badge-vk { background:#e8f0fe; color:#3b5998; }
|
||||
.text-danger { color:#dc3545; }
|
||||
</style>
|
||||
{% else %}
|
||||
<div class="empty-state">
|
||||
<i class="bi bi-journal-x"></i>
|
||||
<p>Записей не найдено за выбранный период.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
function toggleDetail(id) {
|
||||
const row = document.getElementById('detail-' + id);
|
||||
row.style.display = row.style.display === 'none' ? 'table-row' : 'none';
|
||||
const row = document.getElementById('detail-' + id);
|
||||
row.style.display = row.style.display === 'none' ? 'table-row' : 'none';
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,40 +1,44 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Роли и права — ЭВОСИНК{% endblock %}
|
||||
{% block title %}Роли и права — Мои Товары{% endblock %}
|
||||
{% block page_title %}Роли и права{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<nav class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="/admin/users">Пользователи</a></li>
|
||||
<li class="breadcrumb-item active">Роли и права</li>
|
||||
</nav>
|
||||
<ol class="breadcrumb">
|
||||
<li><a href="/admin/users">Пользователи</a></li>
|
||||
<li>Роли и права</li>
|
||||
</ol>
|
||||
|
||||
<h1 style="font-size:1.3rem;" class="mb-3"><i class="bi bi-shield-lock me-2"></i>Роли и права</h1>
|
||||
<div class="pg-title">Роли и права</div>
|
||||
<div class="pg-sub">Управление разрешениями для каждой роли</div>
|
||||
|
||||
{% for role in roles %}
|
||||
<article class="card mb-3">
|
||||
<header>
|
||||
<h2 style="font-size:1rem;">{{ role.name }}
|
||||
<span class="text-muted small fw-normal">— {{ role.description or '' }}</span>
|
||||
</h2>
|
||||
</header>
|
||||
<div class="card-body">
|
||||
<form method="post" action="/admin/roles/{{ role.id }}/permissions">
|
||||
<div class="row gap-2 flex-wrap">
|
||||
{% for perm in permissions %}
|
||||
<div class="col-auto">
|
||||
<label style="display:flex; align-items:center; gap:0.4rem; margin:0;">
|
||||
<input type="checkbox" name="perm_{{ perm.id }}" value="{{ perm.id }}"
|
||||
{% if perm.id in role_perm_ids[role.id] %}checked{% endif %}>
|
||||
{{ perm.name }}
|
||||
{% if perm.description %}
|
||||
<span class="text-muted small">({{ perm.description }})</span>
|
||||
{% endif %}
|
||||
</label>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<button type="submit" class="sm mt-3">Сохранить права для «{{ role.name }}»</button>
|
||||
</form>
|
||||
<div class="card" style="margin-bottom:14px;">
|
||||
<div class="card-hd">
|
||||
<div>
|
||||
<div class="card-title"><i class="bi bi-shield-lock" style="margin-right:6px;"></i>{{ role.name }}</div>
|
||||
{% if role.description %}
|
||||
<div class="card-sub">{{ role.description }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
<form method="post" action="/admin/roles/{{ role.id }}/permissions">
|
||||
<div style="display:flex;flex-wrap:wrap;gap:12px;margin-bottom:16px;">
|
||||
{% for perm in permissions %}
|
||||
<label style="display:flex;align-items:center;gap:7px;cursor:pointer;font-size:13px;padding:6px 10px;border:1px solid #E4E6EE;border-radius:7px;background:#F9FAFB;">
|
||||
<input type="checkbox" name="perm_{{ perm.id }}" value="{{ perm.id }}"
|
||||
{% if perm.id in role_perm_ids[role.id] %}checked{% endif %}
|
||||
style="accent-color:#FF5500;">
|
||||
{{ perm.name }}
|
||||
{% if perm.description %}
|
||||
<span style="font-size:11px;color:#9EA8BE;">({{ perm.description }})</span>
|
||||
{% endif %}
|
||||
</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary btn-sm">
|
||||
<i class="bi bi-save"></i> Сохранить права для «{{ role.name }}»
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,152 +1,176 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}{{ target.first_name }} {{ target.last_name }} — Админ — ЭВОСИНК{% endblock %}
|
||||
{% block title %}{{ target.first_name }} {{ target.last_name }} — Админ — Мои Товары{% endblock %}
|
||||
{% block page_title %}Пользователь{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<nav class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="/admin/users">Пользователи</a></li>
|
||||
<li class="breadcrumb-item active">{{ target.first_name }} {{ target.last_name }}</li>
|
||||
</nav>
|
||||
<ol class="breadcrumb">
|
||||
<li><a href="/admin/users">Пользователи</a></li>
|
||||
<li>{{ target.first_name }} {{ target.last_name }}</li>
|
||||
</ol>
|
||||
|
||||
{% if request.query_params.get('success') == 'reset_sent' %}
|
||||
<div class="alert alert-success mb-3"><p>Ссылка для сброса пароля отправлена.</p></div>
|
||||
<div class="alert alert-gr"><span><i class="bi bi-check-circle"></i></span><div>Ссылка для сброса пароля отправлена.</div></div>
|
||||
{% elif request.query_params.get('success') == 'invite_sent' %}
|
||||
<div class="alert alert-success mb-3"><p>Приглашение отправлено.</p></div>
|
||||
<div class="alert alert-gr"><span><i class="bi bi-check-circle"></i></span><div>Приглашение отправлено.</div></div>
|
||||
{% elif request.query_params.get('success') == 'saved' %}
|
||||
<div class="alert alert-success mb-3"><p>Данные сохранены.</p></div>
|
||||
<div class="alert alert-gr"><span><i class="bi bi-check-circle"></i></span><div>Данные сохранены.</div></div>
|
||||
{% endif %}
|
||||
|
||||
<div class="row gap-3 align-start">
|
||||
<div class="col-lg-6">
|
||||
<article class="card">
|
||||
<header><h2>Профиль</h2></header>
|
||||
<ul class="list-group">
|
||||
<li class="list-group-item"><span class="text-muted small">ID</span><span>{{ target.id }}</span></li>
|
||||
<li class="list-group-item"><span class="text-muted small">Имя</span><span>{{ target.first_name }} {{ target.last_name }}</span></li>
|
||||
<li class="list-group-item"><span class="text-muted small">Email</span>
|
||||
<span>{{ target.email }}
|
||||
{% if target.is_email_confirmed %}
|
||||
<span class="badge badge-success ms-1">подтверждён</span>
|
||||
{% else %}
|
||||
<span class="badge badge-warning ms-1">не подтверждён</span>
|
||||
{% endif %}
|
||||
</span>
|
||||
</li>
|
||||
<li class="list-group-item"><span class="text-muted small">Телефон</span><span>{{ target.phone }}</span></li>
|
||||
<li class="list-group-item"><span class="text-muted small">Роль</span>
|
||||
<span>
|
||||
{% if target.role == 'system' %}<span class="badge badge-danger">Системный</span>
|
||||
{% elif target.role == 'admin' %}<span class="badge badge-warning">Администратор</span>
|
||||
{% else %}<span class="badge badge-secondary">Пользователь</span>
|
||||
{% endif %}
|
||||
</span>
|
||||
</li>
|
||||
<li class="list-group-item"><span class="text-muted small">Статус</span>
|
||||
<span>
|
||||
{% if target.status == 'active' %}<span class="badge badge-success">Активен</span>
|
||||
{% elif target.status == 'pending' %}<span class="badge badge-warning">Ожидает</span>
|
||||
{% else %}<span class="badge badge-danger">Заблокирован</span>
|
||||
{% endif %}
|
||||
</span>
|
||||
</li>
|
||||
<li class="list-group-item"><span class="text-muted small">Регистрация</span><span>{{ target.created_at | datefmt }}</span></li>
|
||||
{% if target.evotor_user_id %}
|
||||
<li class="list-group-item"><span class="text-muted small">Эвотор ID</span><span class="font-monospace small">{{ target.evotor_user_id }}</span></li>
|
||||
{% endif %}
|
||||
{% if target.invite_token %}
|
||||
<li class="list-group-item"><span class="text-muted small">Приглашение до</span><span>{{ target.invite_expires | datefmt }}</span></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</article>
|
||||
<!-- User header -->
|
||||
<div style="display:flex;align-items:center;gap:14px;margin-bottom:24px;">
|
||||
<div class="avatar" style="width:48px;height:48px;font-size:16px;">
|
||||
{{ target.first_name[0] if target.first_name else '?' }}{{ target.last_name[0] if target.last_name else '' }}
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size:18px;font-weight:800;letter-spacing:-0.02em;">{{ target.first_name }} {{ target.last_name }}</div>
|
||||
<div class="mono" style="font-size:12px;color:#9EA8BE;">{{ target.email }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if target.evotor_meta %}
|
||||
<article class="card mt-3">
|
||||
<header><h2>Данные Эвотор</h2></header>
|
||||
<div class="card-body">
|
||||
<pre class="font-monospace small" style="overflow-x:auto; white-space:pre-wrap; margin:0;">{{ target.evotor_meta | tojson(indent=2) }}</pre>
|
||||
</div>
|
||||
</article>
|
||||
<div class="g2" style="align-items:start;">
|
||||
|
||||
<!-- Left column -->
|
||||
<div style="display:flex;flex-direction:column;gap:14px;">
|
||||
|
||||
<!-- Profile -->
|
||||
<div class="card">
|
||||
<div class="card-hd"><div><div class="card-title">Профиль</div></div></div>
|
||||
<div class="conn-detail">
|
||||
<div class="conn-row"><span class="conn-k">ID</span><span class="conn-v">{{ target.id }}</span></div>
|
||||
<div class="conn-row">
|
||||
<span class="conn-k">Email</span>
|
||||
<span class="conn-v" style="display:flex;align-items:center;gap:6px;">
|
||||
{{ target.email }}
|
||||
{% if target.is_email_confirmed %}
|
||||
<span class="tag tag-gr" style="font-size:10px;padding:1px 6px;">подтверждён</span>
|
||||
{% else %}
|
||||
<span class="tag tag-yl" style="font-size:10px;padding:1px 6px;">не подтверждён</span>
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
<div class="conn-row"><span class="conn-k">Телефон</span><span class="conn-v">{{ target.phone or '—' }}</span></div>
|
||||
<div class="conn-row">
|
||||
<span class="conn-k">Роль</span>
|
||||
<span class="conn-v" style="font-family:inherit;">
|
||||
{% if target.role == 'system' %}<span class="tag tag-rd" style="font-size:10.5px;">Системный</span>
|
||||
{% elif target.role == 'admin' %}<span class="tag tag-or" style="font-size:10.5px;">Администратор</span>
|
||||
{% else %}<span class="tag tag-dim" style="font-size:10.5px;">Пользователь</span>
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
<div class="conn-row">
|
||||
<span class="conn-k">Статус</span>
|
||||
<span class="conn-v" style="font-family:inherit;">
|
||||
{% if target.status == 'active' %}<span class="tag tag-gr"><span class="dot g"></span>Активен</span>
|
||||
{% elif target.status == 'pending' %}<span class="tag tag-yl"><span class="dot y pulse"></span>Ожидает</span>
|
||||
{% else %}<span class="tag tag-rd"><span class="dot r"></span>Заблокирован</span>
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
<div class="conn-row"><span class="conn-k">Регистрация</span><span class="conn-v">{{ target.created_at | datefmt }}</span></div>
|
||||
{% if target.evotor_user_id %}
|
||||
<div class="conn-row"><span class="conn-k">Эвотор ID</span><span class="conn-v">{{ target.evotor_user_id }}</span></div>
|
||||
{% endif %}
|
||||
{% if target.invite_token %}
|
||||
<div class="conn-row"><span class="conn-k">Приглашение до</span><span class="conn-v">{{ target.invite_expires | datefmt }}</span></div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-6">
|
||||
<article class="card">
|
||||
<header><h2>Действия</h2></header>
|
||||
<div class="card-body d-grid gap-2">
|
||||
{% if target.status != 'active' %}
|
||||
<form method="post" action="/admin/users/{{ target.id }}/activate">
|
||||
<button type="submit" class="w-100">
|
||||
<i class="bi bi-check-circle me-1"></i>Активировать
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% if target.status != 'suspended' %}
|
||||
<form method="post" action="/admin/users/{{ target.id }}/suspend">
|
||||
<button type="submit" class="w-100 outline danger">
|
||||
<i class="bi bi-slash-circle me-1"></i>Заблокировать
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
<form method="post" action="/admin/users/{{ target.id }}/reset-password">
|
||||
<button type="submit" class="w-100 outline secondary">
|
||||
<i class="bi bi-key me-1"></i>Сбросить пароль
|
||||
</button>
|
||||
</form>
|
||||
<form method="post" action="/admin/users/{{ target.id }}/send-invite">
|
||||
<button type="submit" class="w-100 outline secondary">
|
||||
<i class="bi bi-envelope me-1"></i>Отправить приглашение
|
||||
</button>
|
||||
</form>
|
||||
<form method="post" action="/admin/users/{{ target.id }}/view-as">
|
||||
<button type="submit" class="w-100 outline">
|
||||
<i class="bi bi-eye me-1"></i>Просмотр от имени пользователя
|
||||
</button>
|
||||
</form>
|
||||
{% if user.role == 'system' and target.id != user.id %}
|
||||
<form method="post" action="/admin/users/{{ target.id }}/delete"
|
||||
onsubmit="return confirm('Удалить пользователя {{ target.email }}? Это действие необратимо.')">
|
||||
<button type="submit" class="w-100 danger sm">
|
||||
<i class="bi bi-trash me-1"></i>Удалить
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="card mt-3">
|
||||
<header><h2>Редактировать</h2></header>
|
||||
<div class="card-body">
|
||||
<form method="post" action="/admin/users/{{ target.id }}/edit">
|
||||
<div class="row gap-2 mb-2">
|
||||
<div class="col">
|
||||
<label for="first_name">Имя
|
||||
<input type="text" id="first_name" name="first_name" value="{{ target.first_name }}" required>
|
||||
</label>
|
||||
</div>
|
||||
<div class="col">
|
||||
<label for="last_name">Фамилия
|
||||
<input type="text" id="last_name" name="last_name" value="{{ target.last_name }}" required>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<label for="email">Email
|
||||
<input type="email" id="email" name="email" value="{{ target.email }}">
|
||||
</label>
|
||||
<label for="phone">Телефон
|
||||
<input type="tel" id="phone" name="phone" value="{{ target.phone }}">
|
||||
</label>
|
||||
{% if user.role == 'system' %}
|
||||
<label for="role">Роль
|
||||
<select id="role" name="role">
|
||||
<option value="user" {% if target.role == 'user' %}selected{% endif %}>Пользователь</option>
|
||||
<option value="admin" {% if target.role == 'admin' %}selected{% endif %}>Администратор</option>
|
||||
<option value="system" {% if target.role == 'system' %}selected{% endif %}>Системный</option>
|
||||
</select>
|
||||
</label>
|
||||
{% endif %}
|
||||
<button type="submit">Сохранить</button>
|
||||
</form>
|
||||
</div>
|
||||
</article>
|
||||
{% if target.evotor_meta %}
|
||||
<div class="card">
|
||||
<div class="card-hd"><div><div class="card-title">Данные Эвотор</div></div></div>
|
||||
<pre>{{ target.evotor_meta | tojson(indent=2) }}</pre>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Right column -->
|
||||
<div style="display:flex;flex-direction:column;gap:14px;">
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="card">
|
||||
<div class="card-title" style="margin-bottom:14px;">Действия</div>
|
||||
<div style="display:flex;flex-direction:column;gap:8px;">
|
||||
{% if target.status != 'active' %}
|
||||
<form method="post" action="/admin/users/{{ target.id }}/activate">
|
||||
<button type="submit" class="btn btn-primary w-100">
|
||||
<i class="bi bi-check-circle"></i> Активировать
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% if target.status != 'suspended' %}
|
||||
<form method="post" action="/admin/users/{{ target.id }}/suspend">
|
||||
<button type="submit" class="btn btn-danger w-100">
|
||||
<i class="bi bi-slash-circle"></i> Заблокировать
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
<form method="post" action="/admin/users/{{ target.id }}/reset-password">
|
||||
<button type="submit" class="btn btn-outline w-100">
|
||||
<i class="bi bi-key"></i> Сбросить пароль
|
||||
</button>
|
||||
</form>
|
||||
<form method="post" action="/admin/users/{{ target.id }}/send-invite">
|
||||
<button type="submit" class="btn btn-outline w-100">
|
||||
<i class="bi bi-envelope"></i> Отправить приглашение
|
||||
</button>
|
||||
</form>
|
||||
<form method="post" action="/admin/users/{{ target.id }}/view-as">
|
||||
<button type="submit" class="btn btn-outline w-100">
|
||||
<i class="bi bi-eye"></i> Просмотр от имени пользователя
|
||||
</button>
|
||||
</form>
|
||||
{% if user.role == 'system' and target.id != user.id %}
|
||||
<form method="post" action="/admin/users/{{ target.id }}/delete"
|
||||
onsubmit="return confirm('Удалить пользователя {{ target.email }}? Это действие необратимо.')">
|
||||
<button type="submit" class="btn btn-danger btn-sm w-100">
|
||||
<i class="bi bi-trash"></i> Удалить
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Edit -->
|
||||
<div class="card">
|
||||
<div class="card-title" style="margin-bottom:14px;">Редактировать</div>
|
||||
<form method="post" action="/admin/users/{{ target.id }}/edit">
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;">
|
||||
<div class="form-row">
|
||||
<label class="form-lbl" for="first_name">Имя</label>
|
||||
<input class="inp" type="text" id="first_name" name="first_name" value="{{ target.first_name }}" required>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label class="form-lbl" for="last_name">Фамилия</label>
|
||||
<input class="inp" type="text" id="last_name" name="last_name" value="{{ target.last_name }}" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label class="form-lbl" for="email">Email</label>
|
||||
<input class="inp" type="email" id="email" name="email" value="{{ target.email }}">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label class="form-lbl" for="phone">Телефон</label>
|
||||
<input class="inp" type="tel" id="phone" name="phone" value="{{ target.phone }}">
|
||||
</div>
|
||||
{% if user.role == 'system' %}
|
||||
<div class="form-row">
|
||||
<label class="form-lbl" for="role">Роль</label>
|
||||
<select class="inp" id="role" name="role">
|
||||
<option value="user" {% if target.role == 'user' %}selected{% endif %}>Пользователь</option>
|
||||
<option value="admin" {% if target.role == 'admin' %}selected{% endif %}>Администратор</option>
|
||||
<option value="system" {% if target.role == 'system' %}selected{% endif %}>Системный</option>
|
||||
</select>
|
||||
</div>
|
||||
{% endif %}
|
||||
<button type="submit" class="btn btn-primary btn-sm">
|
||||
<i class="bi bi-save"></i> Сохранить
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,173 +1,201 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Пользователи — Администрирование — ЭВОСИНК{% endblock %}
|
||||
{% block title %}Пользователи — Администрирование — Мои Товары{% endblock %}
|
||||
{% block page_title %}Пользователи{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="d-flex justify-between align-center mb-3">
|
||||
<h1 style="font-size:1.3rem; margin:0;"><i class="bi bi-people me-2"></i>Пользователи</h1>
|
||||
<button onclick="document.getElementById('create-user-dialog').showModal()" class="sm">
|
||||
<i class="bi bi-person-plus me-1"></i>Создать пользователя
|
||||
</button>
|
||||
<div class="pg-title">Пользователи</div>
|
||||
<div class="pg-sub">Управление аккаунтами и подключениями пользователей</div>
|
||||
|
||||
<!-- Topbar action -->
|
||||
<div style="display:flex;justify-content:flex-end;margin-bottom:16px;">
|
||||
<button class="btn btn-primary btn-sm" onclick="document.getElementById('create-user-dialog').showModal()">
|
||||
<i class="bi bi-person-plus"></i> Создать пользователя
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Create user dialog -->
|
||||
<dialog id="create-user-dialog">
|
||||
<article>
|
||||
<header>
|
||||
<button aria-label="Закрыть" rel="prev" onclick="document.getElementById('create-user-dialog').close()"></button>
|
||||
<h3>Создать пользователя</h3>
|
||||
</header>
|
||||
{% if create_errors %}
|
||||
<div role="alert" class="alert alert-danger mb-3">
|
||||
{% for e in create_errors %}<p>{{ e }}</p>{% endfor %}
|
||||
<div class="dialog-hd">
|
||||
<div class="dialog-title">Создать пользователя</div>
|
||||
<button class="dialog-close" onclick="document.getElementById('create-user-dialog').close()">
|
||||
<i class="bi bi-x-lg"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="dialog-body">
|
||||
{% if create_errors %}
|
||||
<div class="alert alert-rd" style="margin-bottom:14px;">
|
||||
<span><i class="bi bi-x-circle"></i></span>
|
||||
<div>{% for e in create_errors %}<div>{{ e }}</div>{% endfor %}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<form method="post" action="/admin/users/create" novalidate>
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;">
|
||||
<div class="form-row">
|
||||
<label class="form-lbl" for="cu_first_name">Имя</label>
|
||||
<input class="inp" type="text" id="cu_first_name" name="first_name"
|
||||
value="{{ create_form.first_name if create_form else '' }}" required>
|
||||
</div>
|
||||
{% endif %}
|
||||
<form method="post" action="/admin/users/create" novalidate>
|
||||
<div class="row gap-2 mb-2">
|
||||
<div class="col">
|
||||
<label for="cu_first_name">Имя
|
||||
<input type="text" id="cu_first_name" name="first_name" value="{{ create_form.first_name if create_form else '' }}" required>
|
||||
</label>
|
||||
</div>
|
||||
<div class="col">
|
||||
<label for="cu_last_name">Фамилия
|
||||
<input type="text" id="cu_last_name" name="last_name" value="{{ create_form.last_name if create_form else '' }}">
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<label for="cu_email">Email
|
||||
<input type="text" id="cu_email" name="email" value="{{ create_form.email if create_form else '' }}" required>
|
||||
</label>
|
||||
<label for="cu_phone">Телефон
|
||||
<input type="tel" id="cu_phone" name="phone" value="{{ create_form.phone if create_form else '' }}" placeholder="+7 (999) 999-99-99">
|
||||
</label>
|
||||
<label for="cu_password">Пароль
|
||||
<input type="password" id="cu_password" name="password" required>
|
||||
</label>
|
||||
{% if user.role == 'system' %}
|
||||
<label for="cu_role">Роль
|
||||
<select id="cu_role" name="role">
|
||||
<option value="user" {% if not create_form or create_form.role == 'user' %}selected{% endif %}>Пользователь</option>
|
||||
<option value="admin" {% if create_form and create_form.role == 'admin' %}selected{% endif %}>Администратор</option>
|
||||
</select>
|
||||
</label>
|
||||
{% endif %}
|
||||
<footer class="d-flex gap-2 justify-end">
|
||||
<button type="button" class="outline secondary" onclick="document.getElementById('create-user-dialog').close()">Отмена</button>
|
||||
<button type="submit">Создать</button>
|
||||
</footer>
|
||||
</form>
|
||||
</article>
|
||||
<div class="form-row">
|
||||
<label class="form-lbl" for="cu_last_name">Фамилия</label>
|
||||
<input class="inp" type="text" id="cu_last_name" name="last_name"
|
||||
value="{{ create_form.last_name if create_form else '' }}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label class="form-lbl" for="cu_email">Email</label>
|
||||
<input class="inp" type="text" id="cu_email" name="email"
|
||||
value="{{ create_form.email if create_form else '' }}" required>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label class="form-lbl" for="cu_phone">Телефон</label>
|
||||
<input class="inp" type="tel" id="cu_phone" name="phone"
|
||||
value="{{ create_form.phone if create_form else '' }}" placeholder="+7 (999) 999-99-99">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label class="form-lbl" for="cu_password">Пароль</label>
|
||||
<input class="inp" type="password" id="cu_password" name="password" required>
|
||||
</div>
|
||||
{% if user.role == 'system' %}
|
||||
<div class="form-row">
|
||||
<label class="form-lbl" for="cu_role">Роль</label>
|
||||
<select class="inp" id="cu_role" name="role">
|
||||
<option value="user" {% if not create_form or create_form.role == 'user' %}selected{% endif %}>Пользователь</option>
|
||||
<option value="admin" {% if create_form and create_form.role == 'admin' %}selected{% endif %}>Администратор</option>
|
||||
</select>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div style="display:flex;gap:8px;justify-content:flex-end;margin-top:16px;">
|
||||
<button type="button" class="btn btn-outline" onclick="document.getElementById('create-user-dialog').close()">Отмена</button>
|
||||
<button type="submit" class="btn btn-primary">Создать</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
{% if create_errors %}
|
||||
<script>document.addEventListener('DOMContentLoaded', () => document.getElementById('create-user-dialog').showModal());</script>
|
||||
{% endif %}
|
||||
|
||||
<article class="card mb-3">
|
||||
<div class="card-body">
|
||||
<form method="get" action="/admin/users" class="d-flex gap-2 flex-wrap align-center">
|
||||
<input type="text" name="search" value="{{ search }}" placeholder="Поиск по имени, email, телефону" style="flex:1; min-width:200px; margin:0;">
|
||||
<select name="status" style="width:auto; margin:0;">
|
||||
<option value="">Все статусы</option>
|
||||
<option value="pending" {% if status_filter == 'pending' %}selected{% endif %}>Ожидает</option>
|
||||
<option value="active" {% if status_filter == 'active' %}selected{% endif %}>Активен</option>
|
||||
<option value="suspended" {% if status_filter == 'suspended' %}selected{% endif %}>Заблокирован</option>
|
||||
</select>
|
||||
<select name="role" style="width:auto; margin:0;">
|
||||
<option value="">Все роли</option>
|
||||
<option value="user" {% if role_filter == 'user' %}selected{% endif %}>Пользователь</option>
|
||||
<option value="admin" {% if role_filter == 'admin' %}selected{% endif %}>Администратор</option>
|
||||
<option value="system" {% if role_filter == 'system' %}selected{% endif %}>Системный</option>
|
||||
</select>
|
||||
<button type="submit" class="sm">Найти</button>
|
||||
{% if search or status_filter or role_filter %}
|
||||
<a href="/admin/users" role="button" class="outline secondary sm">Сбросить</a>
|
||||
{% endif %}
|
||||
</form>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="card">
|
||||
<div class="table-scroll">
|
||||
<table class="align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Имя</th>
|
||||
<th>Email</th>
|
||||
<th>Телефон</th>
|
||||
<th>Роль</th>
|
||||
<th>Статус</th>
|
||||
<th>Эвотор</th>
|
||||
<th>Дата</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for u in users %}
|
||||
<tr>
|
||||
<td class="text-muted small">{{ u.id }}</td>
|
||||
<td>{{ u.first_name }} {{ u.last_name }}</td>
|
||||
<td title="{{ u.email }}">
|
||||
{{ u.email }}
|
||||
{% if not u.is_email_confirmed %}
|
||||
<span class="badge badge-warning ms-1" title="Email не подтверждён"><i class="bi bi-exclamation-circle"></i></span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ u.phone or '—' }}</td>
|
||||
<td>
|
||||
{% if u.role == 'system' %}<span class="badge badge-danger">Системный</span>
|
||||
{% elif u.role == 'admin' %}<span class="badge badge-warning">Админ</span>
|
||||
{% else %}<span class="badge badge-secondary">Польз.</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if u.status == 'active' %}<span class="badge badge-success">Активен</span>
|
||||
{% elif u.status == 'pending' %}<span class="badge badge-warning">Ожидает</span>
|
||||
{% else %}<span class="badge badge-danger">Заблок.</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if u.evotor_user_id %}
|
||||
<i class="bi bi-check-circle text-success" title="{{ u.evotor_user_id }}"></i>
|
||||
{% else %}
|
||||
<span class="text-muted">—</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-muted small">{{ u.created_at | datefmt }}</td>
|
||||
<td>
|
||||
<a href="/admin/users/{{ u.id }}" role="button" class="outline sm">
|
||||
<i class="bi bi-eye"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="9" class="text-center text-muted py-4">Пользователи не найдены</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% if total_pages > 1 %}
|
||||
<footer>
|
||||
<div class="d-flex gap-2 justify-center align-center">
|
||||
{% if page > 1 %}
|
||||
<a href="?page={{ page - 1 }}&search={{ search }}&status={{ status_filter }}&role={{ role_filter }}" role="button" class="outline sm">«</a>
|
||||
{% endif %}
|
||||
<span class="text-muted small">Стр. {{ page }} из {{ total_pages }}</span>
|
||||
{% if page < total_pages %}
|
||||
<a href="?page={{ page + 1 }}&search={{ search }}&status={{ status_filter }}&role={{ role_filter }}" role="button" class="outline sm">»</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</footer>
|
||||
<!-- Search / filter bar -->
|
||||
<div class="card" style="margin-bottom:14px;padding:14px 20px;">
|
||||
<form method="get" action="/admin/users" style="display:flex;gap:8px;flex-wrap:wrap;align-items:center;">
|
||||
<input class="inp" type="text" name="search" value="{{ search }}"
|
||||
placeholder="Поиск по имени, email, телефону" style="flex:1;min-width:200px;">
|
||||
<select class="inp" name="status" style="width:auto;">
|
||||
<option value="">Все статусы</option>
|
||||
<option value="pending" {% if status_filter == 'pending' %}selected{% endif %}>Ожидает</option>
|
||||
<option value="active" {% if status_filter == 'active' %}selected{% endif %}>Активен</option>
|
||||
<option value="suspended" {% if status_filter == 'suspended' %}selected{% endif %}>Заблокирован</option>
|
||||
</select>
|
||||
<select class="inp" name="role" style="width:auto;">
|
||||
<option value="">Все роли</option>
|
||||
<option value="user" {% if role_filter == 'user' %}selected{% endif %}>Пользователь</option>
|
||||
<option value="admin" {% if role_filter == 'admin' %}selected{% endif %}>Администратор</option>
|
||||
<option value="system" {% if role_filter == 'system' %}selected{% endif %}>Системный</option>
|
||||
</select>
|
||||
<button type="submit" class="btn btn-primary btn-sm">Найти</button>
|
||||
{% if search or status_filter or role_filter %}
|
||||
<a href="/admin/users" class="btn btn-outline btn-sm">Сбросить</a>
|
||||
{% endif %}
|
||||
</article>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Users table -->
|
||||
<div class="card" style="padding:0;">
|
||||
<div class="table-wrap">
|
||||
<table class="tbl">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Пользователь</th>
|
||||
<th>Телефон</th>
|
||||
<th>Роль</th>
|
||||
<th>Статус</th>
|
||||
<th>Эвотор</th>
|
||||
<th>Дата</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for u in users %}
|
||||
<tr>
|
||||
<td><span class="mono" style="font-size:11px;color:#9EA8BE;">{{ u.id }}</span></td>
|
||||
<td>
|
||||
<div style="display:flex;align-items:center;gap:10px;">
|
||||
<div class="avatar" style="width:30px;height:30px;font-size:10px;">
|
||||
{{ u.first_name[0] if u.first_name else '?' }}{{ u.last_name[0] if u.last_name else '' }}
|
||||
</div>
|
||||
<div>
|
||||
<div class="tbl-name">{{ u.first_name }} {{ u.last_name }}</div>
|
||||
<div class="tbl-sub">
|
||||
{{ u.email }}
|
||||
{% if not u.is_email_confirmed %}
|
||||
<span class="tag tag-yl" style="font-size:9.5px;padding:0 5px;margin-left:4px;"><i class="bi bi-exclamation-circle"></i></span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td style="font-size:12px;color:#9EA8BE;">{{ u.phone or '—' }}</td>
|
||||
<td>
|
||||
{% if u.role == 'system' %}
|
||||
<span class="tag tag-rd" style="font-size:10.5px;">Системный</span>
|
||||
{% elif u.role == 'admin' %}
|
||||
<span class="tag tag-or" style="font-size:10.5px;">Админ</span>
|
||||
{% else %}
|
||||
<span class="tag tag-dim" style="font-size:10.5px;">Польз.</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if u.status == 'active' %}
|
||||
<span class="tag tag-gr"><span class="dot g"></span>Активен</span>
|
||||
{% elif u.status == 'pending' %}
|
||||
<span class="tag tag-yl"><span class="dot y pulse"></span>Ожидает</span>
|
||||
{% else %}
|
||||
<span class="tag tag-rd"><span class="dot r"></span>Заблок.</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if u.evotor_user_id %}
|
||||
<span class="tag tag-gr" style="font-size:10.5px;"><i class="bi bi-check-circle"></i></span>
|
||||
{% else %}
|
||||
<span style="color:#9EA8BE;font-size:12px;">—</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td><span class="mono" style="font-size:11px;color:#9EA8BE;">{{ u.created_at | datefmt }}</span></td>
|
||||
<td>
|
||||
<a href="/admin/users/{{ u.id }}" class="btn btn-outline btn-xs">
|
||||
<i class="bi bi-eye"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="8" class="text-center" style="padding:32px;color:#9EA8BE;">Пользователи не найдены</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{% if total_pages > 1 %}
|
||||
<div class="pagination">
|
||||
{% if page > 1 %}
|
||||
<a href="?page={{ page - 1 }}&search={{ search }}&status={{ status_filter }}&role={{ role_filter }}" class="btn btn-outline btn-sm">« Назад</a>
|
||||
{% endif %}
|
||||
<span style="font-size:12px;color:#9EA8BE;">Стр. {{ page }} из {{ total_pages }}</span>
|
||||
{% if page < total_pages %}
|
||||
<a href="?page={{ page + 1 }}&search={{ search }}&status={{ status_filter }}&role={{ role_filter }}" class="btn btn-outline btn-sm">Вперёд »</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if user.role == 'system' %}
|
||||
<div class="mt-3 text-end">
|
||||
<a href="/admin/roles" role="button" class="outline secondary sm">
|
||||
<i class="bi bi-shield-lock me-1"></i>Управление ролями
|
||||
</a>
|
||||
<div style="margin-top:14px;text-align:right;">
|
||||
<a href="/admin/roles" class="btn btn-outline btn-sm">
|
||||
<i class="bi bi-shield-lock"></i> Управление ролями
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,121 +1,155 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru" data-theme="light">
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% block title %}ЭВОСИНК{% endblock %}</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
|
||||
<link rel="stylesheet" href="/static/style.css">
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% block title %}Мои Товары{% endblock %}</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Golos+Text:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
|
||||
<link rel="stylesheet" href="/static/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<header class="site-header">
|
||||
<nav class="container">
|
||||
<ul>
|
||||
<li><a href="/" class="brand-logo">ЭВОСИНК</a></li>
|
||||
</ul>
|
||||
<ul class="nav-links">
|
||||
{% if user %}
|
||||
{% if user.role not in ('admin', 'system') or viewed_user %}
|
||||
<li><a href="/connections">Подключения</a></li>
|
||||
<li><a href="/catalog">Каталог Эвотор</a></li>
|
||||
<li><a href="/vk-catalog/albums">Каталог ВК</a></li>
|
||||
<li><a href="/sync">Синхронизация</a></li>
|
||||
{% endif %}
|
||||
{% if user.role in ('admin', 'system') %}
|
||||
<li><a href="/admin/users"><i class="bi bi-shield-lock"></i> Админ</a></li>
|
||||
<li><a href="/admin/logs"><i class="bi bi-journal-text"></i> Логи</a></li>
|
||||
{% endif %}
|
||||
<li><a href="/profile"><i class="bi bi-person-circle"></i> Личный кабинет</a></li>
|
||||
<li><a href="/logout" class="secondary">Выход</a></li>
|
||||
{% else %}
|
||||
<li><a href="/login">Вход</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
{% if user %}
|
||||
<details class="mobile-menu">
|
||||
<summary role="button" class="outline secondary icon-btn"><i class="bi bi-list"></i></summary>
|
||||
<ul>
|
||||
{% if user.role not in ('admin', 'system') or viewed_user %}
|
||||
<li><a href="/connections">Подключения</a></li>
|
||||
<li><a href="/catalog">Каталог Эвотор</a></li>
|
||||
<li><a href="/vk-catalog/albums">Каталог ВК</a></li>
|
||||
<li><a href="/sync">Синхронизация</a></li>
|
||||
{% endif %}
|
||||
{% if user.role in ('admin', 'system') %}
|
||||
<li><a href="/admin/users">Админ</a></li>
|
||||
<li><a href="/admin/logs">Логи</a></li>
|
||||
{% endif %}
|
||||
<li><a href="/profile">Личный кабинет</a></li>
|
||||
<li><a href="/logout">Выход</a></li>
|
||||
</ul>
|
||||
</details>
|
||||
{% else %}
|
||||
<details class="mobile-menu">
|
||||
<summary role="button" class="outline secondary icon-btn"><i class="bi bi-list"></i></summary>
|
||||
<ul>
|
||||
<li><a href="/login">Вход</a></li>
|
||||
</ul>
|
||||
</details>
|
||||
{% endif %}
|
||||
</nav>
|
||||
</header>
|
||||
{% block body %}
|
||||
<div class="shell">
|
||||
|
||||
<!-- ── Sidebar ── -->
|
||||
<aside class="sidebar">
|
||||
<div class="sb-logo">
|
||||
<div class="sb-logo-icon">
|
||||
<svg width="22" height="22" viewBox="0 0 28 28" fill="none">
|
||||
<path d="M9 11h10l-1.5 9H10.5L9 11Z" fill="white" opacity="0.95"/>
|
||||
<path d="M11.5 11V9a2.5 2.5 0 015 0v2" stroke="white" stroke-width="1.6" stroke-linecap="round" fill="none"/>
|
||||
<line x1="12" y1="15" x2="16" y2="15" stroke="#FF5500" stroke-width="1.3" stroke-linecap="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<div class="sb-logo-name">Мои Товары</div>
|
||||
<div class="sb-logo-sub">мои-товары.рф</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav class="sb-nav">
|
||||
{% if user %}
|
||||
{% if user.role in ('admin', 'system') %}
|
||||
<div class="sb-section">Управление</div>
|
||||
<a href="/admin/users" class="sb-item {% if request.url.path.startswith('/admin/users') %}active{% endif %}">
|
||||
<span class="sb-icon"><i class="bi bi-people"></i></span>
|
||||
<span>Пользователи</span>
|
||||
</a>
|
||||
{% if user.role == 'system' %}
|
||||
<a href="/admin/logs" class="sb-item {% if request.url.path.startswith('/admin/logs') %}active{% endif %}">
|
||||
<span class="sb-icon"><i class="bi bi-journal-text"></i></span>
|
||||
<span>Логи</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<div class="sb-section">Главное</div>
|
||||
<a href="/connections" class="sb-item {% if request.url.path == '/connections' %}active{% endif %}">
|
||||
<span class="sb-icon"><i class="bi bi-plug"></i></span>
|
||||
<span>Подключения</span>
|
||||
</a>
|
||||
<a href="/catalog/stores" class="sb-item {% if request.url.path.startswith('/catalog') %}active{% endif %}">
|
||||
<span class="sb-icon"><i class="bi bi-shop"></i></span>
|
||||
<span>Каталог Эвотор</span>
|
||||
</a>
|
||||
<a href="/vk-catalog/albums" class="sb-item {% if request.url.path.startswith('/vk-catalog') %}active{% endif %}">
|
||||
<span class="sb-icon"><i class="bi bi-bag"></i></span>
|
||||
<span>Каталог ВК</span>
|
||||
</a>
|
||||
<a href="/sync" class="sb-item {% if request.url.path == '/sync' %}active{% endif %}">
|
||||
<span class="sb-icon"><i class="bi bi-arrow-repeat"></i></span>
|
||||
<span>Синхронизация</span>
|
||||
</a>
|
||||
<div class="sb-section" style="margin-top:6px;">Аккаунт</div>
|
||||
<a href="/profile" class="sb-item {% if request.url.path.startswith('/profile') %}active{% endif %}">
|
||||
<span class="sb-icon"><i class="bi bi-person"></i></span>
|
||||
<span>Профиль</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</nav>
|
||||
|
||||
{% if user %}
|
||||
<a href="/profile" class="sb-user">
|
||||
<div class="avatar {% if user.role in ('admin','system') %}admin{% endif %}">
|
||||
{{ user.first_name[0] if user.first_name else '?' }}{{ user.last_name[0] if user.last_name else '' }}
|
||||
</div>
|
||||
<div style="flex:1; min-width:0;">
|
||||
<div class="sb-user-name">{{ user.first_name }} {{ user.last_name }}</div>
|
||||
<div class="sb-user-role">
|
||||
<span class="role-chip {% if user.role in ('admin','system') %}admin{% else %}user{% endif %}">
|
||||
{% if user.role == 'system' %}SYSTEM{% elif user.role == 'admin' %}ADMIN{% else %}USER{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
{% endif %}
|
||||
</aside>
|
||||
|
||||
<!-- ── Main ── -->
|
||||
<div class="main">
|
||||
|
||||
<!-- Topbar -->
|
||||
<div class="topbar">
|
||||
<div class="topbar-title">{% block page_title %}{% endblock %}</div>
|
||||
{% block topbar_extras %}{% endblock %}
|
||||
{% if user %}
|
||||
<a href="/logout" class="btn btn-ghost btn-sm" style="margin-left:auto;" title="Выйти">
|
||||
<i class="bi bi-box-arrow-right"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if viewed_user %}
|
||||
<div style="background:#e65c00;color:#fff;text-align:center;padding:0.4rem 1rem;font-size:0.9rem;">
|
||||
<i class="bi bi-eye me-1"></i>Просмотр от имени: <strong>{{ viewed_user.first_name }} {{ viewed_user.last_name }}</strong> ({{ viewed_user.email }})
|
||||
<form method="post" action="/admin/view-as/stop" style="display:inline;margin-left:1rem;">
|
||||
<button type="submit" style="background:none;border:1px solid #fff;color:#fff;padding:0.1rem 0.6rem;font-size:0.85rem;cursor:pointer;border-radius:4px;">Выйти</button>
|
||||
</form>
|
||||
<div class="view-as-bar">
|
||||
<i class="bi bi-eye"></i> Просмотр от имени: <strong>{{ viewed_user.first_name }} {{ viewed_user.last_name }}</strong> ({{ viewed_user.email }})
|
||||
<form method="post" action="/admin/view-as/stop" style="display:inline;margin-left:1rem;">
|
||||
<button type="submit" style="background:none;border:1px solid rgba(255,255,255,0.6);color:#fff;padding:2px 10px;font-size:12px;cursor:pointer;border-radius:4px;font-family:inherit;">Выйти</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<main class="container py-4">
|
||||
{% if errors %}
|
||||
<div role="alert" class="alert alert-danger">
|
||||
{% for error in errors %}
|
||||
<p>{{ error }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="content">
|
||||
{% if errors %}
|
||||
<div class="alert alert-rd" style="margin-bottom:16px;">
|
||||
<span><i class="bi bi-x-circle"></i></span>
|
||||
<div>{% for e in errors %}<div>{{ e }}</div>{% endfor %}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if success %}
|
||||
<div class="alert alert-gr" style="margin-bottom:16px;">
|
||||
<span><i class="bi bi-check-circle"></i></span>
|
||||
<div>{{ success }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
|
||||
{% if success %}
|
||||
<div role="alert" class="alert alert-success">
|
||||
<p>{{ success }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
|
||||
{% if jivosite_widget_id %}
|
||||
<script src="//code.jivosite.com/widget/{{ jivosite_widget_id }}" async></script>
|
||||
{% endif %}
|
||||
<script src="https://cdn.jsdelivr.net/npm/inputmask@5.0.9/dist/inputmask.min.js"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
var phoneInputs = document.querySelectorAll('input[name="phone"]');
|
||||
if (phoneInputs.length) {
|
||||
Inputmask('+7 (999) 999-99-99', {
|
||||
placeholder: '_',
|
||||
showMaskOnHover: false,
|
||||
clearMaskOnLostFocus: false
|
||||
}).mask(phoneInputs);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<script>
|
||||
document.addEventListener('invalid', function(e) {
|
||||
if (e.target.validity.valueMissing) {
|
||||
e.target.setCustomValidity('Пожалуйста, заполните это поле');
|
||||
} else if (e.target.validity.typeMismatch) {
|
||||
e.target.setCustomValidity('Пожалуйста, введите корректное значение');
|
||||
}
|
||||
}, true);
|
||||
document.addEventListener('input', function(e) {
|
||||
if (e.target.required) e.target.setCustomValidity('');
|
||||
}, true);
|
||||
</script>
|
||||
{% block scripts %}{% endblock %}
|
||||
{% if jivosite_widget_id %}
|
||||
<script src="//code.jivosite.com/widget/{{ jivosite_widget_id }}" async></script>
|
||||
{% endif %}
|
||||
<script src="https://cdn.jsdelivr.net/npm/inputmask@5.0.9/dist/inputmask.min.js"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
var phoneInputs = document.querySelectorAll('input[name="phone"]');
|
||||
if (phoneInputs.length) {
|
||||
Inputmask('+7 (999) 999-99-99', { placeholder: '_', showMaskOnHover: false, clearMaskOnLostFocus: false }).mask(phoneInputs);
|
||||
}
|
||||
});
|
||||
document.addEventListener('invalid', function(e) {
|
||||
if (e.target.validity.valueMissing) e.target.setCustomValidity('Пожалуйста, заполните это поле');
|
||||
else if (e.target.validity.typeMismatch) e.target.setCustomValidity('Пожалуйста, введите корректное значение');
|
||||
}, true);
|
||||
document.addEventListener('input', function(e) {
|
||||
if (e.target.required) e.target.setCustomValidity('');
|
||||
}, true);
|
||||
</script>
|
||||
{% block scripts %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,71 +1,63 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Группы — {{ store.name }} — ЭВОСИНК{% endblock %}
|
||||
{% block title %}Группы — {{ store.name }} — Мои Товары{% endblock %}
|
||||
{% block page_title %}Каталог Эвотор{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<nav aria-label="breadcrumb" class="mb-3">
|
||||
<ol class="breadcrumb">
|
||||
<li><a href="/catalog/stores">Магазины</a></li>
|
||||
<li>{{ store.name }}</li>
|
||||
<li>Группы</li>
|
||||
</ol>
|
||||
</nav>
|
||||
<ol class="breadcrumb">
|
||||
<li><a href="/catalog/stores">Магазины</a></li>
|
||||
<li>{{ store.name }}</li>
|
||||
<li>Группы</li>
|
||||
</ol>
|
||||
|
||||
<div class="d-flex justify-between align-center mb-3">
|
||||
<h1 style="font-size:1.3rem; margin:0;"><i class="bi bi-folder me-2"></i>Группы товаров — {{ store.name }}</h1>
|
||||
<span class="text-muted small">Всего: {{ groups | length }}</span>
|
||||
<div class="pg-title">Группы товаров — {{ store.name }}</div>
|
||||
<div class="pg-sub">Управление категориями · Всего: {{ groups | length }}</div>
|
||||
|
||||
<div class="card" style="padding:0;">
|
||||
{% if groups %}
|
||||
<div class="table-wrap">
|
||||
<table class="tbl">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Синхронизация</th>
|
||||
<th>Группа</th>
|
||||
<th>Товаров</th>
|
||||
<th>ID</th>
|
||||
<th>Обновлено</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for g in groups %}
|
||||
{% set is_enabled = (enabled_ids is none) or (g.evotor_id in enabled_ids) %}
|
||||
<tr>
|
||||
<td>
|
||||
<form method="post" action="/catalog/stores/{{ store.evotor_id }}/groups/{{ g.evotor_id }}/toggle" style="margin:0;">
|
||||
<button type="submit" class="tog {% if is_enabled %}on{% endif %}"
|
||||
title="{% if is_enabled %}Отключить синхронизацию{% else %}Включить синхронизацию{% endif %}"
|
||||
style="border:none;background:none;padding:0;cursor:pointer;"></button>
|
||||
</form>
|
||||
</td>
|
||||
<td>
|
||||
<div class="tbl-name"><i class="bi bi-folder2" style="color:#9EA8BE;margin-right:6px;"></i>{{ g.name }}</div>
|
||||
</td>
|
||||
<td><span class="mono" style="font-size:12px;">{{ product_counts.get(g.evotor_id, 0) }}</span></td>
|
||||
<td><span class="mono" style="font-size:11px;color:#9EA8BE;">{{ g.evotor_id }}</span></td>
|
||||
<td><span class="mono" style="font-size:11px;color:#9EA8BE;">{{ g.fetched_at | datefmt }}</span></td>
|
||||
<td>
|
||||
<a href="/catalog/stores/{{ store.evotor_id }}/products?group={{ g.evotor_id }}" class="btn btn-outline btn-xs">
|
||||
<i class="bi bi-box-seam"></i> Товары
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="empty-state">
|
||||
<i class="bi bi-folder"></i>
|
||||
<p>Группы для этого магазина ещё не загружены.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<article class="card">
|
||||
{% if groups %}
|
||||
<div class="table-scroll">
|
||||
<table class="align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Синхронизация</th>
|
||||
<th>Название</th>
|
||||
<th>Количество товаров</th>
|
||||
<th>ID</th>
|
||||
<th>Обновлено</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for g in groups %}
|
||||
{% set is_enabled = (enabled_ids is none) or (g.evotor_id in enabled_ids) %}
|
||||
<tr class="{% if not is_enabled %}text-muted{% endif %}">
|
||||
<td>
|
||||
<form method="post" action="/catalog/stores/{{ store.evotor_id }}/groups/{{ g.evotor_id }}/toggle" style="margin:0;">
|
||||
<button type="submit"
|
||||
class="outline sm {% if is_enabled %}success{% else %}secondary{% endif %}"
|
||||
title="{% if is_enabled %}Отключить синхронизацию{% else %}Включить синхронизацию{% endif %}"
|
||||
style="padding:0.2rem 0.6rem;">
|
||||
{% if is_enabled %}
|
||||
<i class="bi bi-toggle-on"></i>
|
||||
{% else %}
|
||||
<i class="bi bi-toggle-off"></i>
|
||||
{% endif %}
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
<td><i class="bi bi-folder2 me-1 text-muted"></i> <strong>{{ g.name }}</strong></td>
|
||||
<td class="text-muted small">{{ product_counts.get(g.evotor_id, 0) }}</td>
|
||||
<td class="text-muted small">{{ g.evotor_id }}</td>
|
||||
<td class="text-muted small">{{ g.fetched_at | datefmt }}</td>
|
||||
<td>
|
||||
<a href="/catalog/stores/{{ store.evotor_id }}/products?group={{ g.evotor_id }}" role="button" class="outline sm">
|
||||
<i class="bi bi-box-seam"></i> Товары
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-center py-5 text-muted">
|
||||
<i class="bi bi-folder" style="font-size:2rem;"></i>
|
||||
<p class="mt-2">Группы для этого магазина ещё не загружены.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</article>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,83 +1,78 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Товары — {{ store.name }} — ЭВОСИНК{% endblock %}
|
||||
{% block title %}Товары — {{ store.name }} — Мои Товары{% endblock %}
|
||||
{% block page_title %}Каталог Эвотор{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<nav aria-label="breadcrumb" class="mb-3">
|
||||
<ol class="breadcrumb">
|
||||
<li><a href="/catalog/stores">Магазины</a></li>
|
||||
<li>{{ store.name }}</li>
|
||||
<li>Товары</li>
|
||||
</ol>
|
||||
</nav>
|
||||
<ol class="breadcrumb">
|
||||
<li><a href="/catalog/stores">Магазины</a></li>
|
||||
<li>{{ store.name }}</li>
|
||||
<li>Товары</li>
|
||||
</ol>
|
||||
|
||||
<div class="d-flex justify-between align-center mb-3">
|
||||
<h1 style="font-size:1.3rem; margin:0;"><i class="bi bi-box-seam me-2"></i>Товары — {{ store.name }}</h1>
|
||||
<span class="text-muted small">Всего: {{ products | length }}</span>
|
||||
</div>
|
||||
<div class="pg-title">Товары — {{ store.name }}</div>
|
||||
<div class="pg-sub">Всего: {{ products | length }}</div>
|
||||
|
||||
{% if groups %}
|
||||
<article class="card mb-3">
|
||||
<div class="card-body">
|
||||
<form method="get" class="d-flex gap-2 align-center flex-wrap">
|
||||
<select name="group" style="width:auto; margin:0;" onchange="this.form.submit()">
|
||||
<option value="">Все группы</option>
|
||||
{% for g in groups %}
|
||||
<option value="{{ g.evotor_id }}" {% if group_id == g.evotor_id %}selected{% endif %}>{{ g.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
{% if group_id %}
|
||||
<a href="/catalog/stores/{{ store.evotor_id }}/products" role="button" class="outline secondary sm">Сбросить</a>
|
||||
{% endif %}
|
||||
</form>
|
||||
</div>
|
||||
</article>
|
||||
<div class="card" style="margin-bottom:16px;padding:14px 20px;">
|
||||
<form method="get" style="display:flex;gap:8px;align-items:center;flex-wrap:wrap;">
|
||||
<select class="inp" name="group" style="width:auto;" onchange="this.form.submit()">
|
||||
<option value="">Все группы</option>
|
||||
{% for g in groups %}
|
||||
<option value="{{ g.evotor_id }}" {% if group_id == g.evotor_id %}selected{% endif %}>{{ g.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
{% if group_id %}
|
||||
<a href="/catalog/stores/{{ store.evotor_id }}/products" class="btn btn-outline btn-sm">Сбросить</a>
|
||||
{% endif %}
|
||||
</form>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<article class="card">
|
||||
{% if products %}
|
||||
<div class="table-scroll">
|
||||
<table class="align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Название</th>
|
||||
<th>Группа</th>
|
||||
<th>Артикул</th>
|
||||
<th>Цена</th>
|
||||
<th>Остаток</th>
|
||||
<th>Ед.</th>
|
||||
<th>Продаётся</th>
|
||||
<th>Обновлено</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for p in products %}
|
||||
<tr>
|
||||
<td>{{ p.name }}</td>
|
||||
<td class="text-muted small">{{ group_map.get(p.group_evotor_id) or '—' }}</td>
|
||||
<td class="text-muted small">{{ p.article_number or '—' }}</td>
|
||||
<td>{% if p.price is not none %}{{ p.price | price }}{% else %}—{% endif %}</td>
|
||||
<td>{% if p.quantity is not none %}{{ p.quantity }}{% else %}—{% endif %}</td>
|
||||
<td class="text-muted small">{{ p.measure_name or '—' }}</td>
|
||||
<td>
|
||||
{% if p.allow_to_sell %}
|
||||
<i class="bi bi-check-circle text-success"></i>
|
||||
{% elif p.allow_to_sell == false %}
|
||||
<i class="bi bi-x-circle text-danger"></i>
|
||||
{% else %}
|
||||
<span class="text-muted">—</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-muted small">{{ p.fetched_at | datefmt }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-center py-5 text-muted">
|
||||
<i class="bi bi-box-seam" style="font-size:2rem;"></i>
|
||||
<p class="mt-2">Товары не найдены.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</article>
|
||||
<div class="card" style="padding:0;">
|
||||
{% if products %}
|
||||
<div class="table-wrap">
|
||||
<table class="tbl">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Название</th>
|
||||
<th>Группа</th>
|
||||
<th>Артикул</th>
|
||||
<th>Цена</th>
|
||||
<th>Остаток</th>
|
||||
<th>Ед.</th>
|
||||
<th>Продаётся</th>
|
||||
<th>Обновлено</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for p in products %}
|
||||
<tr>
|
||||
<td><div class="tbl-name">{{ p.name }}</div></td>
|
||||
<td style="font-size:12px;color:#9EA8BE;">{{ group_map.get(p.group_evotor_id) or '—' }}</td>
|
||||
<td><span class="mono" style="font-size:11px;color:#9EA8BE;">{{ p.article_number or '—' }}</span></td>
|
||||
<td><span class="mono">{% if p.price is not none %}{{ p.price | price }}{% else %}—{% endif %}</span></td>
|
||||
<td><span class="mono" style="color:{% if p.quantity is not none and p.quantity == 0 %}#E53935{% else %}#1C1F2E{% endif %};">{% if p.quantity is not none %}{{ p.quantity }}{% else %}—{% endif %}</span></td>
|
||||
<td style="font-size:12px;color:#9EA8BE;">{{ p.measure_name or '—' }}</td>
|
||||
<td>
|
||||
{% if p.allow_to_sell %}
|
||||
<span class="tag tag-gr" style="font-size:10.5px;"><i class="bi bi-check-circle"></i></span>
|
||||
{% elif p.allow_to_sell == false %}
|
||||
<span class="tag tag-rd" style="font-size:10.5px;"><i class="bi bi-x-circle"></i></span>
|
||||
{% else %}
|
||||
<span style="color:#9EA8BE;">—</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td><span class="mono" style="font-size:11px;color:#9EA8BE;">{{ p.fetched_at | datefmt }}</span></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="empty-state">
|
||||
<i class="bi bi-box-seam"></i>
|
||||
<p>Товары не найдены.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,66 +1,62 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Магазины — ЭВОСИНК{% endblock %}
|
||||
{% block title %}Магазины — Мои Товары{% endblock %}
|
||||
{% block page_title %}Каталог Эвотор{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="d-flex justify-between align-center mb-3">
|
||||
<h1 style="font-size:1.3rem; margin:0;"><i class="bi bi-shop me-2"></i>Магазины Эвотор</h1>
|
||||
<span class="text-muted small">Всего: {{ stores | length }}</span>
|
||||
</div>
|
||||
<div class="pg-title">Магазины Эвотор</div>
|
||||
<div class="pg-sub">Выберите магазины для синхронизации · Всего: {{ stores | length }}</div>
|
||||
|
||||
<article class="card">
|
||||
{% if stores %}
|
||||
<div class="table-scroll">
|
||||
<table class="align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Синхронизация</th>
|
||||
<th>Название</th>
|
||||
<th>Адрес</th>
|
||||
<th>ID</th>
|
||||
<th>Обновлено</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for s in stores %}
|
||||
{% set is_enabled = (enabled_ids is none) or (s.evotor_id in enabled_ids) %}
|
||||
<tr class="{% if not is_enabled %}text-muted{% endif %}">
|
||||
<td>
|
||||
<form method="post" action="/catalog/stores/{{ s.evotor_id }}/toggle" style="margin:0;">
|
||||
<button type="submit"
|
||||
class="outline sm {% if is_enabled %}success{% else %}secondary{% endif %}"
|
||||
title="{% if is_enabled %}Отключить синхронизацию{% else %}Включить синхронизацию{% endif %}"
|
||||
style="padding:0.2rem 0.6rem;">
|
||||
{% if is_enabled %}
|
||||
<i class="bi bi-toggle-on"></i>
|
||||
{% else %}
|
||||
<i class="bi bi-toggle-off"></i>
|
||||
{% endif %}
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
<td><strong>{{ s.name }}</strong></td>
|
||||
<td class="text-muted">{{ s.address or '—' }}</td>
|
||||
<td class="text-muted small">{{ s.evotor_id }}</td>
|
||||
<td class="text-muted small">{{ s.fetched_at | datefmt }}</td>
|
||||
<td>
|
||||
<a href="/catalog/stores/{{ s.evotor_id }}/products" role="button" class="outline sm" title="Товары">
|
||||
<i class="bi bi-box-seam"></i> Товары
|
||||
</a>
|
||||
<a href="/catalog/stores/{{ s.evotor_id }}/groups" role="button" class="outline secondary sm" title="Группы">
|
||||
<i class="bi bi-folder"></i> Группы
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-center py-5 text-muted">
|
||||
<i class="bi bi-shop" style="font-size:2rem;"></i>
|
||||
<p class="mt-2">Магазины ещё не загружены.<br>Синхронизация выполняется каждые {{ refresh_interval }} сек. автоматически.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</article>
|
||||
<div class="card" style="padding:0;">
|
||||
{% if stores %}
|
||||
<div class="table-wrap">
|
||||
<table class="tbl">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Синхронизация</th>
|
||||
<th>Название</th>
|
||||
<th>Адрес</th>
|
||||
<th>ID</th>
|
||||
<th>Обновлено</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for s in stores %}
|
||||
{% set is_enabled = (enabled_ids is none) or (s.evotor_id in enabled_ids) %}
|
||||
<tr>
|
||||
<td>
|
||||
<form method="post" action="/catalog/stores/{{ s.evotor_id }}/toggle" style="margin:0;">
|
||||
<button type="submit" class="tog {% if is_enabled %}on{% endif %}"
|
||||
title="{% if is_enabled %}Отключить синхронизацию{% else %}Включить синхронизацию{% endif %}"
|
||||
style="border:none;background:none;padding:0;cursor:pointer;"></button>
|
||||
</form>
|
||||
</td>
|
||||
<td>
|
||||
<div class="tbl-name">{{ s.name }}</div>
|
||||
</td>
|
||||
<td style="color:#9EA8BE;font-size:12px;">{{ s.address or '—' }}</td>
|
||||
<td><span class="mono" style="font-size:11px;color:#9EA8BE;">{{ s.evotor_id }}</span></td>
|
||||
<td><span class="mono" style="font-size:11px;color:#9EA8BE;">{{ s.fetched_at | datefmt }}</span></td>
|
||||
<td>
|
||||
<div style="display:flex;gap:6px;">
|
||||
<a href="/catalog/stores/{{ s.evotor_id }}/products" class="btn btn-outline btn-xs">
|
||||
<i class="bi bi-box-seam"></i> Товары
|
||||
</a>
|
||||
<a href="/catalog/stores/{{ s.evotor_id }}/groups" class="btn btn-outline btn-xs">
|
||||
<i class="bi bi-folder"></i> Группы
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="empty-state">
|
||||
<i class="bi bi-shop"></i>
|
||||
<p>Магазины ещё не загружены.<br>Синхронизация выполняется каждые {{ refresh_interval }} сек. автоматически.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,16 +1,22 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Подтверждение email — ЭВОСИНК{% endblock %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Подтверждение email — Мои Товары</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Golos+Text:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
|
||||
<link rel="stylesheet" href="/static/style.css">
|
||||
</head>
|
||||
<body style="display:flex;align-items:center;justify-content:center;min-height:100vh;background:#F4F5F7;">
|
||||
|
||||
{% block content %}
|
||||
<div class="row justify-center">
|
||||
<div class="col-sm-10 col-md-6 col-lg-5">
|
||||
<article class="card mt-5 text-center">
|
||||
<div class="card-body" style="padding: 2.5rem;">
|
||||
<i class="bi bi-envelope-check fs-1 text-primary mb-3 d-block"></i>
|
||||
<h1 style="font-size:1.3rem;" class="mb-3">Подтвердите ваш email</h1>
|
||||
<p class="text-muted">Проверьте почту и нажмите на ссылку для подтверждения.</p>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
<div class="card" style="max-width:400px;width:100%;padding:40px 32px;text-align:center;">
|
||||
<i class="bi bi-envelope-check" style="font-size:48px;color:#FF5500;display:block;margin-bottom:16px;"></i>
|
||||
<div class="pg-title" style="margin-bottom:8px;">Подтвердите ваш email</div>
|
||||
<div style="font-size:13px;color:#5C6278;">Проверьте почту и нажмите на ссылку для подтверждения.</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,210 +1,212 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Подключения — ЭВОСИНК{% endblock %}
|
||||
{% block title %}Подключения — Мои Товары{% endblock %}
|
||||
{% block page_title %}Подключения{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row justify-center">
|
||||
<div class="col-sm-10 col-md-8 col-lg-6">
|
||||
<div class="pg-title">Подключения</div>
|
||||
<div class="pg-sub">Управление интеграциями с Эвотор и VK Market</div>
|
||||
|
||||
<h1 style="font-size:1.3rem; margin-bottom:1.5rem;">
|
||||
<i class="bi bi-plug me-2"></i>Подключения
|
||||
</h1>
|
||||
{% if request.query_params.get('success') %}
|
||||
<div class="alert alert-gr">
|
||||
<span><i class="bi bi-check-circle"></i></span>
|
||||
<div>Подключение сохранено.</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if request.query_params.get('success') %}
|
||||
<div role="alert" class="alert alert-success mb-3">
|
||||
<p>Подключение сохранено.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# ── Evotor ── #}
|
||||
<article class="card mb-4">
|
||||
<header class="d-flex align-center justify-between">
|
||||
<span><i class="bi bi-cpu me-2"></i><strong>Эвотор</strong></span>
|
||||
{% if evotor %}
|
||||
<span class="badge badge-success"><i class="bi bi-check-circle me-1"></i>Подключено</span>
|
||||
{% else %}
|
||||
<span class="badge badge-secondary">Не подключено</span>
|
||||
{% endif %}
|
||||
</header>
|
||||
|
||||
{% if evotor %}
|
||||
<ul class="list-group mb-3">
|
||||
<li class="list-group-item">
|
||||
<span class="text-muted small">Токен</span>
|
||||
<span class="font-monospace small">{{ evotor.access_token[:8] }}••••••••</span>
|
||||
</li>
|
||||
{% if evotor.evotor_user_id %}
|
||||
<li class="list-group-item">
|
||||
<span class="text-muted small">Evotor User ID</span>
|
||||
<span class="font-monospace small">{{ evotor.evotor_user_id }}</span>
|
||||
</li>
|
||||
{% endif %}
|
||||
<li class="list-group-item">
|
||||
<span class="text-muted small">Подключено</span>
|
||||
<span>{{ evotor.connected_at | datefmt }}</span>
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<span class="text-muted small">Обновлено</span>
|
||||
<span>{{ evotor.updated_at | datefmt }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
||||
<div class="card-body">
|
||||
<details {% if not evotor %}open{% endif %}>
|
||||
<summary>
|
||||
{% if evotor %}Обновить токен{% else %}Ввести API-токен{% endif %}
|
||||
</summary>
|
||||
<form method="post" action="/connections/evotor" class="mt-3">
|
||||
<label>
|
||||
API-токен Эвотор
|
||||
<input type="text" name="access_token"
|
||||
placeholder="Вставьте токен из личного кабинета Эвотор"
|
||||
value="{{ evotor.access_token if evotor else '' }}"
|
||||
required autocomplete="off">
|
||||
</label>
|
||||
<label>
|
||||
Evotor User ID <span class="text-muted small">(необязательно)</span>
|
||||
<input type="text" name="evotor_user_id"
|
||||
placeholder="Например: 01234567-89ab-cdef-0123-456789abcdef"
|
||||
value="{{ evotor.evotor_user_id if evotor and evotor.evotor_user_id else '' }}"
|
||||
autocomplete="off">
|
||||
</label>
|
||||
<button type="submit">
|
||||
<i class="bi bi-save me-1"></i>Сохранить
|
||||
</button>
|
||||
</form>
|
||||
</details>
|
||||
|
||||
{% if evotor %}
|
||||
<div class="d-flex gap-2 mt-3" style="flex-wrap:wrap; align-items:center;">
|
||||
<button type="button" class="outline sm" onclick="testConnection('evotor', this)">
|
||||
<i class="bi bi-wifi me-1"></i>Проверить соединение
|
||||
</button>
|
||||
<span id="evotor-test-result" class="small"></span>
|
||||
</div>
|
||||
<form method="post" action="/connections/evotor/disconnect"
|
||||
class="mt-2"
|
||||
onsubmit="return confirm('Отключить Эвотор? Кешированные данные каталога останутся.')">
|
||||
<button type="submit" class="outline danger sm">
|
||||
<i class="bi bi-plug me-1"></i>Отключить
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
</article>
|
||||
|
||||
{# ── VK ── #}
|
||||
<article class="card mb-4">
|
||||
<header class="d-flex align-center justify-between">
|
||||
<span><i class="bi bi-badge-vr me-2"></i><strong>ВКонтакте (Маркет)</strong></span>
|
||||
{% if vk %}
|
||||
<span class="badge badge-success"><i class="bi bi-check-circle me-1"></i>Подключено</span>
|
||||
{% else %}
|
||||
<span class="badge badge-secondary">Не подключено</span>
|
||||
{% endif %}
|
||||
</header>
|
||||
|
||||
{% if vk %}
|
||||
<ul class="list-group mb-3">
|
||||
<li class="list-group-item">
|
||||
<span class="text-muted small">Токен</span>
|
||||
<span class="font-monospace small">{{ vk.access_token[:8] }}••••••••</span>
|
||||
</li>
|
||||
{% if vk.vk_user_id %}
|
||||
<li class="list-group-item">
|
||||
<span class="text-muted small">ID сообщества</span>
|
||||
<span class="font-monospace small">{{ vk.vk_user_id }}</span>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if vk.first_name or vk.last_name %}
|
||||
<li class="list-group-item">
|
||||
<span class="text-muted small">Аккаунт</span>
|
||||
<span>{{ vk.first_name }} {{ vk.last_name }}</span>
|
||||
</li>
|
||||
{% endif %}
|
||||
<li class="list-group-item">
|
||||
<span class="text-muted small">Подключено</span>
|
||||
<span>{{ vk.connected_at | datefmt }}</span>
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<span class="text-muted small">Обновлено</span>
|
||||
<span>{{ vk.updated_at | datefmt }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
||||
<div class="card-body">
|
||||
<div class="mt-3">
|
||||
<a href="/vk-auth" role="button">
|
||||
<i class="bi bi-box-arrow-in-right me-1"></i>
|
||||
{% if vk %}Переподключить ВКонтакте{% else %}Войти через ВКонтакте{% endif %}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<details class="mt-3">
|
||||
<summary class="text-muted small">Ввести токен вручную</summary>
|
||||
<form method="post" action="/connections/vk" class="mt-2">
|
||||
<label>
|
||||
Токен доступа VK
|
||||
<input type="text" name="access_token"
|
||||
placeholder="vk1.a.xxxxxxxxxxxxxxxx…"
|
||||
value="{{ vk.access_token if vk else '' }}"
|
||||
required autocomplete="off">
|
||||
</label>
|
||||
<label>
|
||||
ID сообщества ВКонтакте
|
||||
<input type="text" name="vk_group_id"
|
||||
placeholder="Например: 229744980"
|
||||
value="{{ vk.vk_user_id if vk and vk.vk_user_id else '' }}"
|
||||
autocomplete="off">
|
||||
<small class="text-muted">Числовой ID группы/паблика с включённым Маркетом (без минуса)</small>
|
||||
</label>
|
||||
<button type="submit">
|
||||
<i class="bi bi-save me-1"></i>Сохранить
|
||||
</button>
|
||||
</form>
|
||||
</details>
|
||||
|
||||
{% if vk %}
|
||||
<div class="d-flex gap-2 mt-3" style="flex-wrap:wrap; align-items:center;">
|
||||
<button type="button" class="outline sm" onclick="testConnection('vk', this)">
|
||||
<i class="bi bi-wifi me-1"></i>Проверить соединение
|
||||
</button>
|
||||
<span id="vk-test-result" class="small"></span>
|
||||
</div>
|
||||
<form method="post" action="/connections/vk/disconnect"
|
||||
class="mt-2"
|
||||
onsubmit="return confirm('Отключить ВКонтакте?')">
|
||||
<button type="submit" class="outline danger sm">
|
||||
<i class="bi bi-plug me-1"></i>Отключить
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
</article>
|
||||
<div class="g2" style="align-items:start;">
|
||||
|
||||
{# ── Evotor ── #}
|
||||
<div class="card">
|
||||
<div class="card-hd">
|
||||
<div>
|
||||
<div class="card-title"><i class="bi bi-cpu" style="margin-right:6px;"></i>Эвотор</div>
|
||||
<div class="card-sub">Платформа кассовых решений и товарного учёта</div>
|
||||
</div>
|
||||
{% if evotor %}
|
||||
<span class="tag tag-gr"><span class="dot g"></span>Подключено</span>
|
||||
{% else %}
|
||||
<span class="tag tag-dim"><span class="dot d"></span>Не подключено</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if evotor %}
|
||||
<div class="conn-detail" style="margin-bottom:16px;">
|
||||
<div class="conn-row">
|
||||
<span class="conn-k">Токен</span>
|
||||
<span class="conn-v">{{ evotor.access_token[:8] }}••••••••</span>
|
||||
</div>
|
||||
{% if evotor.evotor_user_id %}
|
||||
<div class="conn-row">
|
||||
<span class="conn-k">Evotor User ID</span>
|
||||
<span class="conn-v">{{ evotor.evotor_user_id }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="conn-row">
|
||||
<span class="conn-k">Подключено</span>
|
||||
<span class="conn-v">{{ evotor.connected_at | datefmt }}</span>
|
||||
</div>
|
||||
<div class="conn-row">
|
||||
<span class="conn-k">Обновлено</span>
|
||||
<span class="conn-v">{{ evotor.updated_at | datefmt }}</span>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<details {% if not evotor %}open{% endif %} style="margin-bottom:14px;">
|
||||
<summary class="btn btn-outline btn-sm" style="cursor:pointer;list-style:none;display:inline-flex;align-items:center;gap:6px;">
|
||||
<i class="bi bi-pencil"></i>
|
||||
{% if evotor %}Обновить токен{% else %}Ввести API-токен{% endif %}
|
||||
</summary>
|
||||
<form method="post" action="/connections/evotor" style="margin-top:12px;">
|
||||
<div class="form-row">
|
||||
<label class="form-lbl">API-токен Эвотор</label>
|
||||
<input class="inp" type="text" name="access_token"
|
||||
placeholder="Вставьте токен из личного кабинета Эвотор"
|
||||
value="{{ evotor.access_token if evotor else '' }}"
|
||||
required autocomplete="off">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label class="form-lbl">Evotor User ID <span style="font-weight:400;text-transform:none;letter-spacing:0;">(необязательно)</span></label>
|
||||
<input class="inp" type="text" name="evotor_user_id"
|
||||
placeholder="Например: 01234567-89ab-cdef-0123-456789abcdef"
|
||||
value="{{ evotor.evotor_user_id if evotor and evotor.evotor_user_id else '' }}"
|
||||
autocomplete="off">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary btn-sm">
|
||||
<i class="bi bi-save"></i> Сохранить
|
||||
</button>
|
||||
</form>
|
||||
</details>
|
||||
|
||||
{% if evotor %}
|
||||
<div style="display:flex;gap:8px;flex-wrap:wrap;align-items:center;">
|
||||
<button type="button" class="btn btn-outline btn-sm" onclick="testConnection('evotor', this)">
|
||||
<i class="bi bi-wifi"></i> Проверить соединение
|
||||
</button>
|
||||
<span id="evotor-test-result" style="font-size:12px;"></span>
|
||||
</div>
|
||||
<form method="post" action="/connections/evotor/disconnect" style="margin-top:10px;"
|
||||
onsubmit="return confirm('Отключить Эвотор? Кешированные данные каталога останутся.')">
|
||||
<button type="submit" class="btn btn-danger btn-sm">
|
||||
<i class="bi bi-plug"></i> Отключить
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{# ── VK ── #}
|
||||
<div class="card">
|
||||
<div class="card-hd">
|
||||
<div>
|
||||
<div class="card-title"><i class="bi bi-badge-vr" style="margin-right:6px;"></i>ВКонтакте (Маркет)</div>
|
||||
<div class="card-sub">market.* API, версия 5.199</div>
|
||||
</div>
|
||||
{% if vk %}
|
||||
<span class="tag tag-gr"><span class="dot g"></span>Подключено</span>
|
||||
{% else %}
|
||||
<span class="tag tag-dim"><span class="dot d"></span>Не подключено</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if vk %}
|
||||
<div class="conn-detail" style="margin-bottom:16px;">
|
||||
<div class="conn-row">
|
||||
<span class="conn-k">Токен</span>
|
||||
<span class="conn-v">{{ vk.access_token[:8] }}••••••••</span>
|
||||
</div>
|
||||
{% if vk.vk_user_id %}
|
||||
<div class="conn-row">
|
||||
<span class="conn-k">ID сообщества</span>
|
||||
<span class="conn-v">{{ vk.vk_user_id }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if vk.first_name or vk.last_name %}
|
||||
<div class="conn-row">
|
||||
<span class="conn-k">Аккаунт</span>
|
||||
<span class="conn-v">{{ vk.first_name }} {{ vk.last_name }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="conn-row">
|
||||
<span class="conn-k">Подключено</span>
|
||||
<span class="conn-v">{{ vk.connected_at | datefmt }}</span>
|
||||
</div>
|
||||
<div class="conn-row">
|
||||
<span class="conn-k">Обновлено</span>
|
||||
<span class="conn-v">{{ vk.updated_at | datefmt }}</span>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div style="margin-bottom:14px;">
|
||||
<a href="/vk-auth" class="btn btn-primary btn-sm">
|
||||
<i class="bi bi-box-arrow-in-right"></i>
|
||||
{% if vk %}Переподключить ВКонтакте{% else %}Войти через ВКонтакте{% endif %}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<details style="margin-bottom:14px;">
|
||||
<summary class="btn btn-outline btn-sm" style="cursor:pointer;list-style:none;display:inline-flex;align-items:center;gap:6px;">
|
||||
<i class="bi bi-key"></i> Ввести токен вручную
|
||||
</summary>
|
||||
<form method="post" action="/connections/vk" style="margin-top:12px;">
|
||||
<div class="form-row">
|
||||
<label class="form-lbl">Токен доступа VK</label>
|
||||
<input class="inp" type="text" name="access_token"
|
||||
placeholder="vk1.a.xxxxxxxxxxxxxxxx…"
|
||||
value="{{ vk.access_token if vk else '' }}"
|
||||
required autocomplete="off">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label class="form-lbl">ID сообщества ВКонтакте</label>
|
||||
<input class="inp" type="text" name="vk_group_id"
|
||||
placeholder="Например: 229744980"
|
||||
value="{{ vk.vk_user_id if vk and vk.vk_user_id else '' }}"
|
||||
autocomplete="off">
|
||||
<div style="font-size:11px;color:#9EA8BE;margin-top:4px;">Числовой ID группы/паблика с включённым Маркетом (без минуса)</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary btn-sm">
|
||||
<i class="bi bi-save"></i> Сохранить
|
||||
</button>
|
||||
</form>
|
||||
</details>
|
||||
|
||||
{% if vk %}
|
||||
<div style="display:flex;gap:8px;flex-wrap:wrap;align-items:center;">
|
||||
<button type="button" class="btn btn-outline btn-sm" onclick="testConnection('vk', this)">
|
||||
<i class="bi bi-wifi"></i> Проверить соединение
|
||||
</button>
|
||||
<span id="vk-test-result" style="font-size:12px;"></span>
|
||||
</div>
|
||||
<form method="post" action="/connections/vk/disconnect" style="margin-top:10px;"
|
||||
onsubmit="return confirm('Отключить ВКонтакте?')">
|
||||
<button type="submit" class="btn btn-danger btn-sm">
|
||||
<i class="bi bi-plug"></i> Отключить
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
async function testConnection(provider, btn) {
|
||||
const resultEl = document.getElementById(provider + '-test-result');
|
||||
btn.disabled = true;
|
||||
resultEl.textContent = 'Проверяем…';
|
||||
resultEl.style.color = '';
|
||||
try {
|
||||
const resp = await fetch('/connections/' + provider + '/test', {method: 'POST'});
|
||||
const data = await resp.json();
|
||||
resultEl.textContent = data.message;
|
||||
resultEl.style.color = data.ok ? 'var(--pico-color-green-500, #2d8a4e)' : 'var(--pico-color-red-500, #c0392b)';
|
||||
} catch (e) {
|
||||
resultEl.textContent = 'Ошибка сети';
|
||||
resultEl.style.color = 'var(--pico-color-red-500, #c0392b)';
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
}
|
||||
const resultEl = document.getElementById(provider + '-test-result');
|
||||
btn.disabled = true;
|
||||
resultEl.textContent = 'Проверяем…';
|
||||
resultEl.style.color = '';
|
||||
try {
|
||||
const resp = await fetch('/connections/' + provider + '/test', {method: 'POST'});
|
||||
const data = await resp.json();
|
||||
resultEl.textContent = data.message;
|
||||
resultEl.style.color = data.ok ? '#17A865' : '#E53935';
|
||||
} catch (e) {
|
||||
resultEl.textContent = 'Ошибка сети';
|
||||
resultEl.style.color = '#E53935';
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,17 +1,23 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Email подтвержден — ЭВОСИНК{% endblock %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Email подтверждён — Мои Товары</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Golos+Text:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
|
||||
<link rel="stylesheet" href="/static/style.css">
|
||||
</head>
|
||||
<body style="display:flex;align-items:center;justify-content:center;min-height:100vh;background:#F4F5F7;">
|
||||
|
||||
{% block content %}
|
||||
<div class="row justify-center">
|
||||
<div class="col-sm-10 col-md-6 col-lg-5">
|
||||
<article class="card mt-5 text-center">
|
||||
<div class="card-body" style="padding: 2.5rem;">
|
||||
<i class="bi bi-check-circle fs-1 text-success mb-3 d-block"></i>
|
||||
<h1 style="font-size:1.3rem;" class="mb-3">Email подтвержден!</h1>
|
||||
<p class="text-muted">Ваш email успешно подтвержден. Теперь вы можете войти в систему.</p>
|
||||
<a href="/login" role="button" class="mt-2">Войти</a>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
<div class="card" style="max-width:400px;width:100%;padding:40px 32px;text-align:center;">
|
||||
<i class="bi bi-check-circle" style="font-size:48px;color:#17A865;display:block;margin-bottom:16px;"></i>
|
||||
<div class="pg-title" style="margin-bottom:8px;">Email подтверждён!</div>
|
||||
<div style="font-size:13px;color:#5C6278;margin-bottom:20px;">Ваш email успешно подтверждён. Теперь вы можете войти в систему.</div>
|
||||
<a href="/login" class="btn btn-primary">Войти</a>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,24 +1,48 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Забыли пароль — ЭВОСИНК{% endblock %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Забыли пароль — Мои Товары</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Golos+Text:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
|
||||
<link rel="stylesheet" href="/static/style.css">
|
||||
</head>
|
||||
<body style="display:flex;align-items:center;justify-content:center;min-height:100vh;background:#F4F5F7;">
|
||||
|
||||
{% block content %}
|
||||
<div class="row justify-center">
|
||||
<div class="col-sm-10 col-md-6 col-lg-5">
|
||||
<article class="card mt-4">
|
||||
<div class="card-body">
|
||||
<h1 style="font-size:1.3rem;" class="mb-2">Забыли пароль?</h1>
|
||||
<p class="text-muted small mb-4">Введите email, указанный при регистрации.</p>
|
||||
<form method="post" action="/forgot-password">
|
||||
<label for="email">Email
|
||||
<input type="email" id="email" name="email" required>
|
||||
</label>
|
||||
<button type="submit" class="w-100">Отправить ссылку для сброса</button>
|
||||
</form>
|
||||
<div class="text-center small mt-3">
|
||||
<a href="/login">Вернуться ко входу</a>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
<div class="card" style="max-width:400px;width:100%;padding:32px;">
|
||||
<div style="text-align:center;margin-bottom:24px;">
|
||||
<div style="width:44px;height:44px;border-radius:11px;background:#FF5500;display:flex;align-items:center;justify-content:center;margin:0 auto 16px;">
|
||||
<svg width="24" height="24" viewBox="0 0 28 28" fill="none">
|
||||
<path d="M9 11h10l-1.5 9H10.5L9 11Z" fill="white" opacity="0.95"/>
|
||||
<path d="M11.5 11V9a2.5 2.5 0 015 0v2" stroke="white" stroke-width="1.6" stroke-linecap="round" fill="none"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="login-box-title">Забыли пароль?</div>
|
||||
<div style="font-size:13px;color:#5C6278;margin-top:4px;">Введите email, указанный при регистрации</div>
|
||||
</div>
|
||||
|
||||
{% if errors %}
|
||||
<div class="alert alert-rd" style="margin-bottom:14px;">
|
||||
<span><i class="bi bi-x-circle"></i></span>
|
||||
<div>{% for e in errors %}<div>{{ e }}</div>{% endfor %}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<form method="post" action="/forgot-password">
|
||||
<div class="form-row">
|
||||
<label class="form-lbl" for="email">Email</label>
|
||||
<input class="inp" type="email" id="email" name="email" placeholder="you@store.ru" required>
|
||||
</div>
|
||||
<button type="submit" class="login-btn">Отправить ссылку для сброса</button>
|
||||
</form>
|
||||
|
||||
<div class="login-hint" style="margin-top:16px;">
|
||||
<a href="/login">← Вернуться ко входу</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,44 +1,79 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Завершение регистрации — ЭВОСИНК{% endblock %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Завершение регистрации — Мои Товары</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Golos+Text:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
|
||||
<link rel="stylesheet" href="/static/style.css">
|
||||
</head>
|
||||
<body style="display:flex;align-items:center;justify-content:center;min-height:100vh;background:#F4F5F7;padding:24px;">
|
||||
|
||||
{% block content %}
|
||||
<div class="row justify-center">
|
||||
<div class="col-sm-10 col-md-7 col-lg-6">
|
||||
<article class="card mt-4">
|
||||
<header>
|
||||
<h1><i class="bi bi-person-plus me-2"></i>Добро пожаловать в ЭВОСИНК!</h1>
|
||||
</header>
|
||||
<div class="card-body">
|
||||
<p class="text-muted mb-4">Ваш аккаунт был создан через Эвотор. Заполните данные профиля и задайте пароль для входа.</p>
|
||||
<form method="post" action="/invite?token={{ token }}">
|
||||
<div class="row gap-2 mb-2">
|
||||
<div class="col">
|
||||
<label for="first_name">Имя <span class="text-danger">*</span>
|
||||
<input type="text" id="first_name" name="first_name" value="{{ form.first_name if form else (invite_user.first_name or '') }}" required>
|
||||
</label>
|
||||
</div>
|
||||
<div class="col">
|
||||
<label for="last_name">Фамилия <span class="text-danger">*</span>
|
||||
<input type="text" id="last_name" name="last_name" value="{{ form.last_name if form else (invite_user.last_name or '') }}" required>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<label for="email">Email <span class="text-danger">*</span>
|
||||
<input type="email" id="email" name="email" value="{{ form.email if form else (invite_user.email or '') }}" required>
|
||||
</label>
|
||||
<label for="phone">Телефон <span class="text-danger">*</span>
|
||||
<input type="tel" id="phone" name="phone" value="{{ form.phone if form else (invite_user.phone or '') }}" required>
|
||||
</label>
|
||||
<label for="password">Пароль <span class="text-danger">*</span>
|
||||
<input type="password" id="password" name="password" required minlength="8">
|
||||
</label>
|
||||
<label for="password_confirm">Подтверждение пароля <span class="text-danger">*</span>
|
||||
<input type="password" id="password_confirm" name="password_confirm" required>
|
||||
</label>
|
||||
<button type="submit" class="w-100">Завершить регистрацию</button>
|
||||
</form>
|
||||
</div>
|
||||
</article>
|
||||
<div class="card" style="max-width:480px;width:100%;padding:32px;">
|
||||
<div style="text-align:center;margin-bottom:24px;">
|
||||
<div style="width:44px;height:44px;border-radius:11px;background:#FF5500;display:flex;align-items:center;justify-content:center;margin:0 auto 16px;">
|
||||
<svg width="24" height="24" viewBox="0 0 28 28" fill="none">
|
||||
<path d="M9 11h10l-1.5 9H10.5L9 11Z" fill="white" opacity="0.95"/>
|
||||
<path d="M11.5 11V9a2.5 2.5 0 015 0v2" stroke="white" stroke-width="1.6" stroke-linecap="round" fill="none"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="login-box-title">Добро пожаловать!</div>
|
||||
<div style="font-size:13px;color:#5C6278;margin-top:4px;">Ваш аккаунт создан через Эвотор. Заполните профиль и задайте пароль для входа.</div>
|
||||
</div>
|
||||
|
||||
{% if errors %}
|
||||
<div class="alert alert-rd" style="margin-bottom:14px;">
|
||||
<span><i class="bi bi-x-circle"></i></span>
|
||||
<div>{% for e in errors %}<div>{{ e }}</div>{% endfor %}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<form method="post" action="/invite?token={{ token }}">
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;">
|
||||
<div class="form-row">
|
||||
<label class="form-lbl" for="first_name">Имя <span style="color:#E53935;">*</span></label>
|
||||
<input class="inp" type="text" id="first_name" name="first_name"
|
||||
value="{{ form.first_name if form else (invite_user.first_name or '') }}" required>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label class="form-lbl" for="last_name">Фамилия <span style="color:#E53935;">*</span></label>
|
||||
<input class="inp" type="text" id="last_name" name="last_name"
|
||||
value="{{ form.last_name if form else (invite_user.last_name or '') }}" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label class="form-lbl" for="email">Email <span style="color:#E53935;">*</span></label>
|
||||
<input class="inp" type="email" id="email" name="email"
|
||||
value="{{ form.email if form else (invite_user.email or '') }}" required>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label class="form-lbl" for="phone">Телефон <span style="color:#E53935;">*</span></label>
|
||||
<input class="inp" type="tel" id="phone" name="phone"
|
||||
value="{{ form.phone if form else (invite_user.phone or '') }}" required>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label class="form-lbl" for="password">Пароль <span style="color:#E53935;">*</span></label>
|
||||
<input class="inp" type="password" id="password" name="password" required minlength="8">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label class="form-lbl" for="password_confirm">Подтверждение пароля <span style="color:#E53935;">*</span></label>
|
||||
<input class="inp" type="password" id="password_confirm" name="password_confirm" required>
|
||||
</div>
|
||||
<button type="submit" class="login-btn">Завершить регистрацию</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/inputmask@5.0.9/dist/inputmask.min.js"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
var phoneInputs = document.querySelectorAll('input[name="phone"]');
|
||||
if (phoneInputs.length) {
|
||||
Inputmask('+7 (999) 999-99-99', { placeholder: '_', showMaskOnHover: false, clearMaskOnLostFocus: false }).mask(phoneInputs);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,26 +1,105 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Вход — ЭВОСИНК{% endblock %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Вход — Мои Товары</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Golos+Text:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
|
||||
<link rel="stylesheet" href="/static/style.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
{% block content %}
|
||||
<div class="row justify-center">
|
||||
<div class="col-sm-10 col-md-6 col-lg-5">
|
||||
<article class="card mt-4">
|
||||
<div class="card-body">
|
||||
<h1 class="mb-4" style="font-size:1.3rem;">Вход</h1>
|
||||
<form method="post" action="/login">
|
||||
<label for="email">Email
|
||||
<input type="email" id="email" name="email" value="{{ form.email if form else '' }}" required>
|
||||
</label>
|
||||
<label for="password">Пароль
|
||||
<input type="password" id="password" name="password" required>
|
||||
</label>
|
||||
<button type="submit" class="w-100">Войти</button>
|
||||
</form>
|
||||
<div class="text-center small mt-3">
|
||||
<a href="/forgot-password">Забыли пароль?</a>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
<div class="login-wrap">
|
||||
|
||||
<!-- Left panel -->
|
||||
<div class="login-left">
|
||||
<div class="login-left-bg"></div>
|
||||
<div class="login-left-pattern"></div>
|
||||
|
||||
<div class="login-brand">
|
||||
<div class="login-brand-icon">
|
||||
<svg width="28" height="28" viewBox="0 0 28 28" fill="none">
|
||||
<path d="M9 11h10l-1.5 9H10.5L9 11Z" fill="white" opacity="0.95"/>
|
||||
<path d="M11.5 11V9a2.5 2.5 0 015 0v2" stroke="white" stroke-width="1.6" stroke-linecap="round" fill="none"/>
|
||||
<line x1="12" y1="15" x2="16" y2="15" stroke="#FF5500" stroke-width="1.3" stroke-linecap="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<div class="login-brand-name">Мои Товары</div>
|
||||
<div class="login-brand-sub">мои-товары.рф</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="login-hero">
|
||||
<div class="login-hero-title">
|
||||
Управляйте товарами<br>и продавайте<br><em>везде сразу</em>
|
||||
</div>
|
||||
<div class="login-hero-body">
|
||||
Синхронизируйте ассортимент вашего магазина Эвотор с онлайн-площадками — быстро, без ручной работы.
|
||||
</div>
|
||||
<div class="login-chips">
|
||||
<div class="login-chip"><i class="bi bi-shop"></i> Эвотор</div>
|
||||
<div class="login-chip"><i class="bi bi-badge-vr"></i> VK Market</div>
|
||||
<div class="login-chip"><i class="bi bi-arrow-repeat"></i> Авто-синхронизация</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="login-footer">© 2025 Мои Товары · мои-товары.рф</div>
|
||||
</div>
|
||||
|
||||
<!-- Right panel -->
|
||||
<div class="login-right">
|
||||
<div class="login-box">
|
||||
<div style="margin-bottom:28px;">
|
||||
<div style="margin-bottom:18px;">
|
||||
<div style="width:36px;height:36px;border-radius:9px;background:#FF5500;display:flex;align-items:center;justify-content:center;">
|
||||
<svg width="20" height="20" viewBox="0 0 28 28" fill="none">
|
||||
<path d="M9 11h10l-1.5 9H10.5L9 11Z" fill="white" opacity="0.95"/>
|
||||
<path d="M11.5 11V9a2.5 2.5 0 015 0v2" stroke="white" stroke-width="1.6" stroke-linecap="round" fill="none"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="login-box-title">Вход в аккаунт</div>
|
||||
<div class="login-box-sub">Введите данные, полученные после регистрации</div>
|
||||
</div>
|
||||
|
||||
{% if errors %}
|
||||
<div class="alert alert-rd" style="margin-bottom:14px;">
|
||||
<span><i class="bi bi-x-circle"></i></span>
|
||||
<div>{% for e in errors %}<div>{{ e }}</div>{% endfor %}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<form method="post" action="/login">
|
||||
<div class="form-row">
|
||||
<label class="form-lbl" for="email">Email</label>
|
||||
<input class="inp" type="email" id="email" name="email"
|
||||
placeholder="you@store.ru"
|
||||
value="{{ form.email if form else '' }}" required>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label class="form-lbl" for="password">Пароль</label>
|
||||
<input class="inp" type="password" id="password" name="password"
|
||||
placeholder="••••••••" required>
|
||||
</div>
|
||||
<button type="submit" class="login-btn">Войти →</button>
|
||||
</form>
|
||||
|
||||
<div class="login-hint" style="margin-top:16px;">
|
||||
<a href="/forgot-password">Забыли пароль?</a>
|
||||
</div>
|
||||
<div class="login-hint">
|
||||
Нет аккаунта? Купите приложение на
|
||||
<a href="https://market.evotor.ru" target="_blank" rel="noreferrer">Эвотор.Маркет</a>
|
||||
и получите доступ автоматически.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,18 +1,24 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}{{ title }} — ЭВОСИНК{% endblock %}
|
||||
{% block title %}{{ title }} — Мои Товары{% endblock %}
|
||||
{% block page_title %}{{ title }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row justify-center">
|
||||
<div class="col-sm-10 col-md-6 col-lg-5">
|
||||
<article class="card mt-5 text-center">
|
||||
<div class="card-body" style="padding: 2.5rem;">
|
||||
<h1 style="font-size:1.3rem;" class="mb-3">{{ title }}</h1>
|
||||
<p class="text-muted">{{ message }}</p>
|
||||
{% if link %}
|
||||
<a href="{{ link }}" role="button" class="mt-2">{{ link_text }}</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</article>
|
||||
<div style="display:flex;align-items:center;justify-content:center;min-height:60vh;">
|
||||
<div class="card" style="max-width:440px;width:100%;text-align:center;padding:40px 32px;">
|
||||
<div style="font-size:48px;margin-bottom:16px;">
|
||||
{% if 'ошибка' in title|lower or 'error' in title|lower %}
|
||||
<i class="bi bi-x-circle" style="color:#E53935;"></i>
|
||||
{% elif 'успешно' in title|lower or 'готово' in title|lower or 'подтвержден' in title|lower %}
|
||||
<i class="bi bi-check-circle" style="color:#17A865;"></i>
|
||||
{% else %}
|
||||
<i class="bi bi-info-circle" style="color:#3B6FFF;"></i>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="pg-title" style="margin-bottom:8px;">{{ title }}</div>
|
||||
<div style="font-size:13px;color:#5C6278;margin-bottom:20px;">{{ message }}</div>
|
||||
{% if link %}
|
||||
<a href="{{ link }}" class="btn btn-primary">{{ link_text }}</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,31 +1,31 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Изменить пароль — ЭВОСИНК{% endblock %}
|
||||
{% block title %}Изменить пароль — Мои Товары{% endblock %}
|
||||
{% block page_title %}Изменить пароль{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row justify-center">
|
||||
<div class="col-sm-10 col-md-6 col-lg-5">
|
||||
<article class="card mt-4">
|
||||
<header>
|
||||
<h1><i class="bi bi-key me-2"></i>Изменить пароль</h1>
|
||||
</header>
|
||||
<div class="card-body">
|
||||
<form method="post" action="/profile/change-password">
|
||||
<label for="current_password">Текущий пароль
|
||||
<input type="password" id="current_password" name="current_password" required>
|
||||
</label>
|
||||
<label for="password">Новый пароль
|
||||
<input type="password" id="password" name="password" required>
|
||||
</label>
|
||||
<label for="password_confirm">Подтвердить пароль
|
||||
<input type="password" id="password_confirm" name="password_confirm" required>
|
||||
</label>
|
||||
<div class="d-flex gap-2">
|
||||
<button type="submit">Изменить пароль</button>
|
||||
<a href="/profile" role="button" class="outline secondary">Отмена</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</article>
|
||||
<div class="pg-title">Изменить пароль</div>
|
||||
<div class="pg-sub">Обновите пароль для входа в систему</div>
|
||||
|
||||
<div class="card" style="max-width:440px;">
|
||||
<form method="post" action="/profile/change-password">
|
||||
<div class="form-row">
|
||||
<label class="form-lbl" for="current_password">Текущий пароль</label>
|
||||
<input class="inp" type="password" id="current_password" name="current_password" required>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label class="form-lbl" for="password">Новый пароль</label>
|
||||
<input class="inp" type="password" id="password" name="password" required>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label class="form-lbl" for="password_confirm">Подтвердить пароль</label>
|
||||
<input class="inp" type="password" id="password_confirm" name="password_confirm" required>
|
||||
</div>
|
||||
<div style="display:flex;gap:8px;">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="bi bi-key"></i> Изменить пароль
|
||||
</button>
|
||||
<a href="/profile" class="btn btn-outline">Отмена</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,31 +1,27 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Удалить аккаунт — ЭВОСИНК{% endblock %}
|
||||
{% block title %}Удалить аккаунт — Мои Товары{% endblock %}
|
||||
{% block page_title %}Удалить аккаунт{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row justify-center">
|
||||
<div class="col-sm-10 col-md-6 col-lg-5">
|
||||
<article class="card mt-4" style="border-color: #dc2626;">
|
||||
<header class="bg-danger-header">
|
||||
<h1><i class="bi bi-trash me-2"></i>Удалить аккаунт</h1>
|
||||
</header>
|
||||
<div class="card-body">
|
||||
<div role="alert" class="alert alert-warning mb-3">
|
||||
<i class="bi bi-exclamation-triangle me-1"></i>
|
||||
<strong>Внимание!</strong> Это действие необратимо. Все ваши данные будут удалены.
|
||||
</div>
|
||||
<form method="post" action="/profile/delete">
|
||||
<label for="password">Введите пароль для подтверждения
|
||||
<input type="password" id="password" name="password" required>
|
||||
</label>
|
||||
<div class="d-flex gap-2">
|
||||
<button type="submit" class="danger">
|
||||
<i class="bi bi-trash me-1"></i>Удалить мой аккаунт
|
||||
</button>
|
||||
<a href="/profile" role="button" class="outline secondary">Отмена</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</article>
|
||||
<div class="pg-title">Удалить аккаунт</div>
|
||||
<div class="pg-sub">Это действие необратимо</div>
|
||||
|
||||
<div class="card" style="max-width:440px;border-color:#F4AEAE;">
|
||||
<div class="alert alert-yl" style="margin-bottom:18px;">
|
||||
<span><i class="bi bi-exclamation-triangle"></i></span>
|
||||
<div><strong>Внимание!</strong> Все ваши данные, подключения и история синхронизации будут удалены без возможности восстановления.</div>
|
||||
</div>
|
||||
<form method="post" action="/profile/delete">
|
||||
<div class="form-row">
|
||||
<label class="form-lbl" for="password">Введите пароль для подтверждения</label>
|
||||
<input class="inp" type="password" id="password" name="password" required>
|
||||
</div>
|
||||
<div style="display:flex;gap:8px;">
|
||||
<button type="submit" class="btn btn-danger">
|
||||
<i class="bi bi-trash"></i> Удалить мой аккаунт
|
||||
</button>
|
||||
<a href="/profile" class="btn btn-outline">Отмена</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,43 +1,40 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Редактировать профиль — ЭВОСИНК{% endblock %}
|
||||
{% block title %}Редактировать профиль — Мои Товары{% endblock %}
|
||||
{% block page_title %}Редактировать профиль{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row justify-center">
|
||||
<div class="col-sm-10 col-md-7 col-lg-6">
|
||||
<article class="card mt-4">
|
||||
<header>
|
||||
<h1><i class="bi bi-pencil me-2"></i>Редактировать профиль</h1>
|
||||
</header>
|
||||
<div class="card-body">
|
||||
<form method="post" action="/profile/edit">
|
||||
<div class="row gap-2 mb-2">
|
||||
<div class="col">
|
||||
<label for="first_name">Имя
|
||||
<input type="text" id="first_name" name="first_name"
|
||||
value="{{ form.first_name if form else user.first_name }}" required>
|
||||
</label>
|
||||
</div>
|
||||
<div class="col">
|
||||
<label for="last_name">Фамилия
|
||||
<input type="text" id="last_name" name="last_name"
|
||||
value="{{ form.last_name if form else user.last_name }}" required>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<label>Email
|
||||
<input type="email" value="{{ user.email }}" disabled>
|
||||
</label>
|
||||
<label for="phone">Телефон
|
||||
<input type="tel" id="phone" name="phone"
|
||||
value="{{ form.phone if form else user.phone }}" required>
|
||||
</label>
|
||||
<div class="d-flex gap-2">
|
||||
<button type="submit">Сохранить</button>
|
||||
<a href="/profile" role="button" class="outline secondary">Отмена</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</article>
|
||||
<div class="pg-title">Редактировать профиль</div>
|
||||
<div class="pg-sub">Обновите ваши личные данные</div>
|
||||
|
||||
<div class="card" style="max-width:540px;">
|
||||
<form method="post" action="/profile/edit">
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;">
|
||||
<div class="form-row">
|
||||
<label class="form-lbl" for="first_name">Имя</label>
|
||||
<input class="inp" type="text" id="first_name" name="first_name"
|
||||
value="{{ form.first_name if form else user.first_name }}" required>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label class="form-lbl" for="last_name">Фамилия</label>
|
||||
<input class="inp" type="text" id="last_name" name="last_name"
|
||||
value="{{ form.last_name if form else user.last_name }}" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label class="form-lbl">Email</label>
|
||||
<input class="inp" type="email" value="{{ user.email }}" disabled style="opacity:0.6;cursor:not-allowed;">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label class="form-lbl" for="phone">Телефон</label>
|
||||
<input class="inp" type="tel" id="phone" name="phone"
|
||||
value="{{ form.phone if form else user.phone }}" required>
|
||||
</div>
|
||||
<div style="display:flex;gap:8px;">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="bi bi-save"></i> Сохранить
|
||||
</button>
|
||||
<a href="/profile" class="btn btn-outline">Отмена</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,86 +1,113 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Личный кабинет — ЭВОСИНК{% endblock %}
|
||||
{% block title %}Профиль — Мои Товары{% endblock %}
|
||||
{% block page_title %}Профиль{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row justify-center">
|
||||
<div class="col-sm-10 col-md-7 col-lg-6">
|
||||
<article class="card mt-4">
|
||||
<header>
|
||||
<h1><i class="bi bi-person-circle me-2"></i>Личный кабинет</h1>
|
||||
</header>
|
||||
<ul class="list-group">
|
||||
<li class="list-group-item">
|
||||
<span class="text-muted small">Имя</span>
|
||||
<span>{{ user.first_name }}</span>
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<span class="text-muted small">Фамилия</span>
|
||||
<span>{{ user.last_name }}</span>
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<span class="text-muted small">Email</span>
|
||||
<span>
|
||||
{{ user.email }}
|
||||
{% if user.is_email_confirmed %}
|
||||
<span class="badge badge-success ms-1"><i class="bi bi-check-circle"></i> подтверждён</span>
|
||||
{% else %}
|
||||
<span class="badge badge-warning ms-1"><i class="bi bi-exclamation-circle"></i> не подтверждён</span>
|
||||
{% endif %}
|
||||
</span>
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<span class="text-muted small">Телефон</span>
|
||||
<span>{{ user.phone }}</span>
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<span class="text-muted small">Роль</span>
|
||||
<span>
|
||||
{% if user.role == 'system' %}<span class="badge badge-danger">Системный</span>
|
||||
{% elif user.role == 'admin' %}<span class="badge badge-warning">Администратор</span>
|
||||
{% else %}<span class="badge badge-secondary">Пользователь</span>
|
||||
{% endif %}
|
||||
</span>
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<span class="text-muted small">Статус</span>
|
||||
<span>
|
||||
{% if user.status == 'active' %}<span class="badge badge-success">Активен</span>
|
||||
{% elif user.status == 'pending' %}<span class="badge badge-warning">Ожидает подтверждения</span>
|
||||
{% else %}<span class="badge badge-danger">Заблокирован</span>
|
||||
{% endif %}
|
||||
</span>
|
||||
</li>
|
||||
{% if user.evotor_user_id %}
|
||||
<li class="list-group-item">
|
||||
<span class="text-muted small">Эвотор ID</span>
|
||||
<span class="font-monospace small">{{ user.evotor_user_id }}</span>
|
||||
</li>
|
||||
{% endif %}
|
||||
<li class="list-group-item">
|
||||
<span class="text-muted small">Регистрация</span>
|
||||
<span>{{ user.created_at | datefmt }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="card-body d-grid gap-2">
|
||||
<a href="/profile/edit" role="button">
|
||||
<i class="bi bi-pencil me-1"></i>Редактировать профиль
|
||||
</a>
|
||||
<a href="/profile/change-password" role="button" class="secondary">
|
||||
<i class="bi bi-key me-1"></i>Изменить пароль
|
||||
</a>
|
||||
{% if not user.is_email_confirmed %}
|
||||
<a href="/resend-confirm" role="button" class="outline secondary">
|
||||
<i class="bi bi-envelope me-1"></i>Отправить письмо с подтверждением
|
||||
</a>
|
||||
{% endif %}
|
||||
<a href="/logout" role="button" class="outline secondary">
|
||||
<i class="bi bi-box-arrow-right me-1"></i>Выход
|
||||
</a>
|
||||
<a href="/profile/delete" role="button" class="outline danger sm mt-2">
|
||||
<i class="bi bi-trash me-1"></i>Удалить аккаунт
|
||||
</a>
|
||||
</div>
|
||||
</article>
|
||||
<div class="pg-title">Личный кабинет</div>
|
||||
<div class="pg-sub">Ваши данные и настройки аккаунта</div>
|
||||
|
||||
<div class="g2" style="align-items:start;">
|
||||
|
||||
<div class="card">
|
||||
<div class="card-hd">
|
||||
<div>
|
||||
<div class="card-title">Данные профиля</div>
|
||||
<div class="card-sub">Основная информация об аккаунте</div>
|
||||
</div>
|
||||
<div class="avatar {% if user.role in ('admin','system') %}admin{% endif %}" style="width:44px;height:44px;font-size:15px;">
|
||||
{{ user.first_name[0] if user.first_name else '?' }}{{ user.last_name[0] if user.last_name else '' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="conn-detail">
|
||||
<div class="conn-row">
|
||||
<span class="conn-k">Имя</span>
|
||||
<span class="conn-v" style="font-family:'Golos Text',sans-serif;">{{ user.first_name }}</span>
|
||||
</div>
|
||||
<div class="conn-row">
|
||||
<span class="conn-k">Фамилия</span>
|
||||
<span class="conn-v" style="font-family:'Golos Text',sans-serif;">{{ user.last_name }}</span>
|
||||
</div>
|
||||
<div class="conn-row">
|
||||
<span class="conn-k">Email</span>
|
||||
<span class="conn-v" style="display:flex;align-items:center;gap:6px;">
|
||||
{{ user.email }}
|
||||
{% if user.is_email_confirmed %}
|
||||
<span class="tag tag-gr" style="font-size:10px;padding:1px 6px;"><i class="bi bi-check-circle"></i> подтверждён</span>
|
||||
{% else %}
|
||||
<span class="tag tag-yl" style="font-size:10px;padding:1px 6px;"><i class="bi bi-exclamation-circle"></i> не подтверждён</span>
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
<div class="conn-row">
|
||||
<span class="conn-k">Телефон</span>
|
||||
<span class="conn-v">{{ user.phone or '—' }}</span>
|
||||
</div>
|
||||
<div class="conn-row">
|
||||
<span class="conn-k">Роль</span>
|
||||
<span class="conn-v" style="font-family:'Golos Text',sans-serif;">
|
||||
{% if user.role == 'system' %}
|
||||
<span class="tag tag-rd">Системный</span>
|
||||
{% elif user.role == 'admin' %}
|
||||
<span class="tag tag-or">Администратор</span>
|
||||
{% else %}
|
||||
<span class="tag tag-dim">Пользователь</span>
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
<div class="conn-row">
|
||||
<span class="conn-k">Статус</span>
|
||||
<span class="conn-v" style="font-family:'Golos Text',sans-serif;">
|
||||
{% if user.status == 'active' %}
|
||||
<span class="tag tag-gr"><span class="dot g"></span>Активен</span>
|
||||
{% elif user.status == 'pending' %}
|
||||
<span class="tag tag-yl"><span class="dot y pulse"></span>Ожидает</span>
|
||||
{% else %}
|
||||
<span class="tag tag-rd"><span class="dot r"></span>Заблокирован</span>
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
{% if user.evotor_user_id %}
|
||||
<div class="conn-row">
|
||||
<span class="conn-k">Эвотор ID</span>
|
||||
<span class="conn-v">{{ user.evotor_user_id }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="conn-row">
|
||||
<span class="conn-k">Регистрация</span>
|
||||
<span class="conn-v">{{ user.created_at | datefmt }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display:flex;flex-direction:column;gap:12px;">
|
||||
<div class="card">
|
||||
<div class="card-title" style="margin-bottom:14px;">Действия</div>
|
||||
<div style="display:flex;flex-direction:column;gap:8px;">
|
||||
<a href="/profile/edit" class="btn btn-primary">
|
||||
<i class="bi bi-pencil"></i> Редактировать профиль
|
||||
</a>
|
||||
<a href="/profile/change-password" class="btn btn-outline">
|
||||
<i class="bi bi-key"></i> Изменить пароль
|
||||
</a>
|
||||
{% if not user.is_email_confirmed %}
|
||||
<a href="/resend-confirm" class="btn btn-outline">
|
||||
<i class="bi bi-envelope"></i> Отправить письмо с подтверждением
|
||||
</a>
|
||||
{% endif %}
|
||||
<a href="/logout" class="btn btn-outline">
|
||||
<i class="bi bi-box-arrow-right"></i> Выйти
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card" style="border-color:#F4AEAE;">
|
||||
<div class="card-title" style="color:#E53935;margin-bottom:10px;"><i class="bi bi-exclamation-triangle" style="margin-right:6px;"></i>Опасная зона</div>
|
||||
<div class="card-sub" style="margin-bottom:12px;">Необратимые действия с аккаунтом</div>
|
||||
<a href="/profile/delete" class="btn btn-danger btn-sm">
|
||||
<i class="bi bi-trash"></i> Удалить аккаунт
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,44 +1,83 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Регистрация — ЭВОСИНК{% endblock %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Регистрация — Мои Товары</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Golos+Text:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
|
||||
<link rel="stylesheet" href="/static/style.css">
|
||||
</head>
|
||||
<body style="display:flex;align-items:center;justify-content:center;min-height:100vh;background:#F4F5F7;padding:24px;">
|
||||
|
||||
{% block content %}
|
||||
<div class="row justify-center">
|
||||
<div class="col-sm-10 col-md-7 col-lg-6">
|
||||
<article class="card mt-4">
|
||||
<div class="card-body">
|
||||
<h1 class="mb-4" style="font-size:1.3rem;">Регистрация</h1>
|
||||
<form method="post" action="/register">
|
||||
<div class="row gap-2 mb-2">
|
||||
<div class="col">
|
||||
<label for="first_name">Имя
|
||||
<input type="text" id="first_name" name="first_name" value="{{ form.first_name if form else '' }}">
|
||||
</label>
|
||||
</div>
|
||||
<div class="col">
|
||||
<label for="last_name">Фамилия
|
||||
<input type="text" id="last_name" name="last_name" value="{{ form.last_name if form else '' }}">
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<label for="email">Email <span class="text-danger">*</span>
|
||||
<input type="email" id="email" name="email" value="{{ form.email if form else '' }}" required>
|
||||
</label>
|
||||
<label for="phone">Телефон <span class="text-danger">*</span>
|
||||
<input type="tel" id="phone" name="phone" value="{{ form.phone if form else '' }}" required>
|
||||
</label>
|
||||
<label for="password">Пароль <span class="text-danger">*</span>
|
||||
<input type="password" id="password" name="password" required>
|
||||
</label>
|
||||
<label for="password_confirm">Подтверждение пароля <span class="text-danger">*</span>
|
||||
<input type="password" id="password_confirm" name="password_confirm" required>
|
||||
</label>
|
||||
<button type="submit" class="w-100">Зарегистрироваться</button>
|
||||
</form>
|
||||
<div class="text-center small mt-3">
|
||||
<a href="/login">Уже есть аккаунт? Войти</a>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
<div class="card" style="max-width:480px;width:100%;padding:32px;">
|
||||
<div style="text-align:center;margin-bottom:24px;">
|
||||
<div style="width:44px;height:44px;border-radius:11px;background:#FF5500;display:flex;align-items:center;justify-content:center;margin:0 auto 16px;">
|
||||
<svg width="24" height="24" viewBox="0 0 28 28" fill="none">
|
||||
<path d="M9 11h10l-1.5 9H10.5L9 11Z" fill="white" opacity="0.95"/>
|
||||
<path d="M11.5 11V9a2.5 2.5 0 015 0v2" stroke="white" stroke-width="1.6" stroke-linecap="round" fill="none"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="login-box-title">Регистрация</div>
|
||||
<div style="font-size:13px;color:#5C6278;margin-top:4px;">Создайте аккаунт для работы с Мои Товары</div>
|
||||
</div>
|
||||
|
||||
{% if errors %}
|
||||
<div class="alert alert-rd" style="margin-bottom:14px;">
|
||||
<span><i class="bi bi-x-circle"></i></span>
|
||||
<div>{% for e in errors %}<div>{{ e }}</div>{% endfor %}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<form method="post" action="/register">
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;">
|
||||
<div class="form-row">
|
||||
<label class="form-lbl" for="first_name">Имя</label>
|
||||
<input class="inp" type="text" id="first_name" name="first_name"
|
||||
value="{{ form.first_name if form else '' }}">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label class="form-lbl" for="last_name">Фамилия</label>
|
||||
<input class="inp" type="text" id="last_name" name="last_name"
|
||||
value="{{ form.last_name if form else '' }}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label class="form-lbl" for="email">Email <span style="color:#E53935;">*</span></label>
|
||||
<input class="inp" type="email" id="email" name="email"
|
||||
value="{{ form.email if form else '' }}" required>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label class="form-lbl" for="phone">Телефон <span style="color:#E53935;">*</span></label>
|
||||
<input class="inp" type="tel" id="phone" name="phone"
|
||||
value="{{ form.phone if form else '' }}" required>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label class="form-lbl" for="password">Пароль <span style="color:#E53935;">*</span></label>
|
||||
<input class="inp" type="password" id="password" name="password" required>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label class="form-lbl" for="password_confirm">Подтверждение пароля <span style="color:#E53935;">*</span></label>
|
||||
<input class="inp" type="password" id="password_confirm" name="password_confirm" required>
|
||||
</div>
|
||||
<button type="submit" class="login-btn">Зарегистрироваться</button>
|
||||
</form>
|
||||
|
||||
<div class="login-hint" style="margin-top:14px;">
|
||||
<a href="/login">Уже есть аккаунт? Войти</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/inputmask@5.0.9/dist/inputmask.min.js"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
var phoneInputs = document.querySelectorAll('input[name="phone"]');
|
||||
if (phoneInputs.length) {
|
||||
Inputmask('+7 (999) 999-99-99', { placeholder: '_', showMaskOnHover: false, clearMaskOnLostFocus: false }).mask(phoneInputs);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,23 +1,45 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Новый пароль — ЭВОСИНК{% endblock %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Новый пароль — Мои Товары</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Golos+Text:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
|
||||
<link rel="stylesheet" href="/static/style.css">
|
||||
</head>
|
||||
<body style="display:flex;align-items:center;justify-content:center;min-height:100vh;background:#F4F5F7;">
|
||||
|
||||
{% block content %}
|
||||
<div class="row justify-center">
|
||||
<div class="col-sm-10 col-md-6 col-lg-5">
|
||||
<article class="card mt-4">
|
||||
<div class="card-body">
|
||||
<h1 style="font-size:1.3rem;" class="mb-4">Новый пароль</h1>
|
||||
<form method="post" action="/reset-password?token={{ token }}">
|
||||
<label for="password">Новый пароль
|
||||
<input type="password" id="password" name="password" required>
|
||||
</label>
|
||||
<label for="password_confirm">Подтверждение пароля
|
||||
<input type="password" id="password_confirm" name="password_confirm" required>
|
||||
</label>
|
||||
<button type="submit" class="w-100">Сменить пароль</button>
|
||||
</form>
|
||||
</div>
|
||||
</article>
|
||||
<div class="card" style="max-width:400px;width:100%;padding:32px;">
|
||||
<div style="text-align:center;margin-bottom:24px;">
|
||||
<div style="width:44px;height:44px;border-radius:11px;background:#FF5500;display:flex;align-items:center;justify-content:center;margin:0 auto 16px;">
|
||||
<i class="bi bi-key" style="color:#fff;font-size:20px;"></i>
|
||||
</div>
|
||||
<div class="login-box-title">Новый пароль</div>
|
||||
<div style="font-size:13px;color:#5C6278;margin-top:4px;">Введите и подтвердите новый пароль</div>
|
||||
</div>
|
||||
|
||||
{% if errors %}
|
||||
<div class="alert alert-rd" style="margin-bottom:14px;">
|
||||
<span><i class="bi bi-x-circle"></i></span>
|
||||
<div>{% for e in errors %}<div>{{ e }}</div>{% endfor %}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<form method="post" action="/reset-password?token={{ token }}">
|
||||
<div class="form-row">
|
||||
<label class="form-lbl" for="password">Новый пароль</label>
|
||||
<input class="inp" type="password" id="password" name="password" required>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label class="form-lbl" for="password_confirm">Подтверждение пароля</label>
|
||||
<input class="inp" type="password" id="password_confirm" name="password_confirm" required>
|
||||
</div>
|
||||
<button type="submit" class="login-btn">Сменить пароль</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,71 +1,102 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Синхронизация — ЭВОСИНК{% endblock %}
|
||||
{% block title %}Синхронизация — Мои Товары{% endblock %}
|
||||
{% block page_title %}Синхронизация{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="d-flex justify-between align-center mb-3">
|
||||
<h1 style="font-size:1.3rem; margin:0;"><i class="bi bi-arrow-repeat me-2"></i>Синхронизация</h1>
|
||||
</div>
|
||||
<div class="pg-title">Синхронизация</div>
|
||||
<div class="pg-sub">Настройка и управление синхронизацией товаров Эвотор → VK Market</div>
|
||||
|
||||
{% if saved %}
|
||||
<div role="alert" class="alert alert-success"><p>Настройки сохранены.</p></div>
|
||||
<div class="alert alert-gr">
|
||||
<span><i class="bi bi-check-circle"></i></span>
|
||||
<div>Настройки сохранены.</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<form method="post" action="/sync/settings">
|
||||
|
||||
<article class="card mb-3">
|
||||
<h2 style="font-size:1.1rem; margin-bottom:1.25rem;">Фоновые задачи</h2>
|
||||
<p class="text-muted small" style="margin-bottom:1.25rem;">Включайте поочерёдно: сначала проверьте зеркало Эвотор, затем ВК, затем синхронизацию.</p>
|
||||
<div class="g2" style="align-items:start; margin-bottom:16px;">
|
||||
|
||||
<div style="display:flex; flex-direction:column; gap:1rem;">
|
||||
<div class="card">
|
||||
<div class="card-hd">
|
||||
<div>
|
||||
<div class="card-title">Фоновые задачи</div>
|
||||
<div class="card-sub">Включайте поочерёдно: сначала Эвотор, затем ВК, затем синхронизацию</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label style="display:flex; align-items:flex-start; gap:0.75rem; cursor:pointer;">
|
||||
<input type="hidden" name="evo_mirror_enabled" value="0">
|
||||
<input type="checkbox" name="evo_mirror_enabled" value="1" role="switch"
|
||||
{% if config and config.evo_mirror_enabled %}checked{% endif %}
|
||||
style="margin-top:0.2rem; flex-shrink:0;">
|
||||
<span>
|
||||
<strong>Зеркало Эвотор</strong><br>
|
||||
<span class="text-muted small">Периодически импортирует товары, группы и магазины из Эвотор в локальную базу.</span>
|
||||
</span>
|
||||
<div style="display:flex;flex-direction:column;gap:0;">
|
||||
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;padding:12px 0;border-bottom:1px solid #E4E6EE;">
|
||||
<div>
|
||||
<div style="font-size:13px;font-weight:600;color:#1C1F2E;">Зеркало Эвотор</div>
|
||||
<div style="font-size:12px;color:#9EA8BE;margin-top:2px;">Импортирует товары, группы и магазины из Эвотор в локальную базу</div>
|
||||
</div>
|
||||
<label style="display:flex;align-items:center;gap:8px;cursor:pointer;margin:0;">
|
||||
<input type="hidden" name="evo_mirror_enabled" value="0">
|
||||
<input type="checkbox" name="evo_mirror_enabled" value="1"
|
||||
{% if config and config.evo_mirror_enabled %}checked{% endif %}
|
||||
id="evo_mirror_cb" style="display:none;">
|
||||
<div class="tog {% if config and config.evo_mirror_enabled %}on{% endif %}"
|
||||
onclick="this.previousElementSibling.click(); this.classList.toggle('on')"
|
||||
id="evo_mirror_tog"></div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<label style="display:flex; align-items:flex-start; gap:0.75rem; cursor:pointer;">
|
||||
<input type="hidden" name="vk_mirror_enabled" value="0">
|
||||
<input type="checkbox" name="vk_mirror_enabled" value="1" role="switch"
|
||||
{% if config and config.vk_mirror_enabled %}checked{% endif %}
|
||||
style="margin-top:0.2rem; flex-shrink:0;">
|
||||
<span>
|
||||
<strong>Зеркало ВК</strong><br>
|
||||
<span class="text-muted small">Периодически импортирует альбомы и товары из VK Market в локальный кэш.</span>
|
||||
</span>
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;padding:12px 0;border-bottom:1px solid #E4E6EE;">
|
||||
<div>
|
||||
<div style="font-size:13px;font-weight:600;color:#1C1F2E;">Зеркало ВК</div>
|
||||
<div style="font-size:12px;color:#9EA8BE;margin-top:2px;">Импортирует альбомы и товары из VK Market в локальный кэш</div>
|
||||
</div>
|
||||
<label style="display:flex;align-items:center;gap:8px;cursor:pointer;margin:0;">
|
||||
<input type="hidden" name="vk_mirror_enabled" value="0">
|
||||
<input type="checkbox" name="vk_mirror_enabled" value="1"
|
||||
{% if config and config.vk_mirror_enabled %}checked{% endif %}
|
||||
id="vk_mirror_cb" style="display:none;">
|
||||
<div class="tog {% if config and config.vk_mirror_enabled %}on{% endif %}"
|
||||
onclick="this.previousElementSibling.click(); this.classList.toggle('on')"></div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<label style="display:flex; align-items:flex-start; gap:0.75rem; cursor:pointer;">
|
||||
<input type="hidden" name="is_enabled" value="0">
|
||||
<input type="checkbox" name="is_enabled" value="1" role="switch"
|
||||
{% if config and config.is_enabled %}checked{% endif %}
|
||||
style="margin-top:0.2rem; flex-shrink:0;">
|
||||
<span>
|
||||
<strong>Синхронизация</strong><br>
|
||||
<span class="text-muted small">Зеркалит каталог Эвотор в VK Market: создаёт, обновляет и удаляет товары.</span>
|
||||
</span>
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;padding:12px 0;">
|
||||
<div>
|
||||
<div style="font-size:13px;font-weight:600;color:#1C1F2E;">Синхронизация</div>
|
||||
<div style="font-size:12px;color:#9EA8BE;margin-top:2px;">Зеркалит каталог Эвотор в VK Market: создаёт, обновляет и удаляет товары</div>
|
||||
</div>
|
||||
<label style="display:flex;align-items:center;gap:8px;cursor:pointer;margin:0;">
|
||||
<input type="hidden" name="is_enabled" value="0">
|
||||
<input type="checkbox" name="is_enabled" value="1"
|
||||
{% if config and config.is_enabled %}checked{% endif %}
|
||||
id="sync_cb" style="display:none;">
|
||||
<div class="tog {% if config and config.is_enabled %}on{% endif %}"
|
||||
onclick="this.previousElementSibling.click(); this.classList.toggle('on')"></div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<article class="card mb-3">
|
||||
<h2 style="font-size:1.1rem; margin-bottom:1.25rem;">Настройки цены</h2>
|
||||
<label style="max-width:320px;">
|
||||
Множитель цены
|
||||
<input type="number" name="price_multiplier" step="0.0001" min="0.0001"
|
||||
value="{{ config.price_multiplier if config else '1' }}"
|
||||
placeholder="1">
|
||||
<small class="text-muted">Цена из Эвотор умножается на это значение перед отправкой в ВК. По умолчанию: 1.</small>
|
||||
</label>
|
||||
</article>
|
||||
<div class="card">
|
||||
<div class="card-hd">
|
||||
<div>
|
||||
<div class="card-title">Настройки цены</div>
|
||||
<div class="card-sub">Трансформация цен при передаче в VK Market</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label class="form-lbl">Множитель цены</label>
|
||||
<input class="inp" type="number" name="price_multiplier" step="0.0001" min="0.0001"
|
||||
value="{{ config.price_multiplier if config else '1' }}"
|
||||
placeholder="1" style="max-width:160px;">
|
||||
<div style="font-size:11px;color:#9EA8BE;margin-top:5px;">Цена из Эвотор умножается на это значение перед отправкой в ВК. По умолчанию: 1.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit">Сохранить</button>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="bi bi-save"></i> Сохранить настройки
|
||||
</button>
|
||||
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,52 +1,53 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Альбомы ВК — ЭВОСИНК{% endblock %}
|
||||
{% block title %}Альбомы ВК — Мои Товары{% endblock %}
|
||||
{% block page_title %}Каталог ВКонтакте{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="d-flex justify-between align-center mb-3">
|
||||
<h1 style="font-size:1.3rem; margin:0;"><i class="bi bi-badge-vr me-2"></i>Каталог ВКонтакте — Альбомы</h1>
|
||||
<span class="text-muted small">Всего: {{ albums | length }}</span>
|
||||
</div>
|
||||
<div class="pg-title">Каталог ВКонтакте — Альбомы</div>
|
||||
<div class="pg-sub">Альбомы из VK Market · Всего: {{ albums | length }}</div>
|
||||
|
||||
<article class="card">
|
||||
{% if not vk_conn %}
|
||||
<div class="text-center py-5 text-muted">
|
||||
<i class="bi bi-plug" style="font-size:2rem;"></i>
|
||||
<p class="mt-2">ВКонтакте не подключён.<br><a href="/connections">Перейти к подключениям</a></p>
|
||||
</div>
|
||||
{% elif albums %}
|
||||
<div class="table-scroll">
|
||||
<table class="align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Название</th>
|
||||
<th>Товаров</th>
|
||||
<th>ID</th>
|
||||
<th>Обновлено</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for a in albums %}
|
||||
<tr>
|
||||
<td><i class="bi bi-collection me-1 text-muted"></i> <strong>{{ a.title }}</strong></td>
|
||||
<td class="text-muted">{{ a.count if a.count is not none else '—' }}</td>
|
||||
<td class="text-muted small">{{ a.album_id }}</td>
|
||||
<td class="text-muted small">{{ a.fetched_at | datefmt }}</td>
|
||||
<td>
|
||||
<a href="/vk-catalog/albums/{{ a.album_id }}/products" role="button" class="outline sm">
|
||||
<i class="bi bi-box-seam"></i> Товары
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-center py-5 text-muted">
|
||||
<i class="bi bi-collection" style="font-size:2rem;"></i>
|
||||
<p class="mt-2">Альбомы ещё не загружены.<br>Синхронизация выполняется каждые {{ refresh_interval }} сек. автоматически.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</article>
|
||||
<div class="card" style="padding:0;">
|
||||
{% if not vk_conn %}
|
||||
<div class="empty-state">
|
||||
<i class="bi bi-plug"></i>
|
||||
<p>ВКонтакте не подключён.<br><a href="/connections" style="color:#FF5500;">Перейти к подключениям</a></p>
|
||||
</div>
|
||||
{% elif albums %}
|
||||
<div class="table-wrap">
|
||||
<table class="tbl">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Название</th>
|
||||
<th>Товаров</th>
|
||||
<th>ID</th>
|
||||
<th>Обновлено</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for a in albums %}
|
||||
<tr>
|
||||
<td>
|
||||
<div class="tbl-name"><i class="bi bi-collection" style="color:#9EA8BE;margin-right:6px;"></i>{{ a.title }}</div>
|
||||
</td>
|
||||
<td><span class="mono" style="font-size:12px;">{{ a.count if a.count is not none else '—' }}</span></td>
|
||||
<td><span class="mono" style="font-size:11px;color:#9EA8BE;">{{ a.album_id }}</span></td>
|
||||
<td><span class="mono" style="font-size:11px;color:#9EA8BE;">{{ a.fetched_at | datefmt }}</span></td>
|
||||
<td>
|
||||
<a href="/vk-catalog/albums/{{ a.album_id }}/products" class="btn btn-outline btn-xs">
|
||||
<i class="bi bi-box-seam"></i> Товары
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="empty-state">
|
||||
<i class="bi bi-collection"></i>
|
||||
<p>Альбомы ещё не загружены.<br>Синхронизация выполняется каждые {{ refresh_interval }} сек. автоматически.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,75 +1,72 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Товары ВК — {{ album.title }} — ЭВОСИНК{% endblock %}
|
||||
{% block title %}Товары ВК — {{ album.title }} — Мои Товары{% endblock %}
|
||||
{% block page_title %}Каталог ВКонтакте{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<nav aria-label="breadcrumb" class="mb-3">
|
||||
<ol class="breadcrumb">
|
||||
<li><a href="/vk-catalog/albums">Альбомы ВК</a></li>
|
||||
<li>{{ album.title }}</li>
|
||||
</ol>
|
||||
</nav>
|
||||
<ol class="breadcrumb">
|
||||
<li><a href="/vk-catalog/albums">Альбомы ВК</a></li>
|
||||
<li>{{ album.title }}</li>
|
||||
</ol>
|
||||
|
||||
<div class="d-flex justify-between align-center mb-3">
|
||||
<h1 style="font-size:1.3rem; margin:0;"><i class="bi bi-box-seam me-2"></i>{{ album.title }}</h1>
|
||||
<span class="text-muted small">Всего: {{ products | length }}</span>
|
||||
<div class="pg-title">{{ album.title }}</div>
|
||||
<div class="pg-sub">Товары из VK Market · Всего: {{ products | length }}</div>
|
||||
|
||||
<div class="card" style="padding:0;">
|
||||
{% if products %}
|
||||
<div class="table-wrap">
|
||||
<table class="tbl">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:56px;"></th>
|
||||
<th>Название</th>
|
||||
<th>Цена</th>
|
||||
<th>Статус</th>
|
||||
<th>ID</th>
|
||||
<th>Обновлено</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for p in products %}
|
||||
<tr>
|
||||
<td>
|
||||
{% if p.thumb_url %}
|
||||
<img src="{{ p.thumb_url }}" alt="" style="width:40px;height:40px;object-fit:cover;border-radius:6px;">
|
||||
{% else %}
|
||||
<div style="width:40px;height:40px;border-radius:6px;background:#F4F5F7;display:flex;align-items:center;justify-content:center;color:#9EA8BE;">
|
||||
<i class="bi bi-image"></i>
|
||||
</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<div class="tbl-name">{{ p.name }}</div>
|
||||
{% if p.description %}
|
||||
<div class="tbl-sub">{{ p.description[:80] }}{% if p.description|length > 80 %}…{% endif %}</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td><span class="mono">{% if p.price is not none %}{{ p.price | price }}{% else %}—{% endif %}</span></td>
|
||||
<td>
|
||||
{% if p.availability == 0 %}
|
||||
<span class="tag tag-gr"><span class="dot g"></span>В наличии</span>
|
||||
{% elif p.availability == 1 %}
|
||||
<span class="tag tag-dim"><span class="dot d"></span>Удалён</span>
|
||||
{% elif p.availability == 2 %}
|
||||
<span class="tag tag-yl"><span class="dot y"></span>Недоступен</span>
|
||||
{% else %}
|
||||
<span style="color:#9EA8BE;font-size:12px;">—</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td><span class="mono" style="font-size:11px;color:#9EA8BE;">{{ p.vk_product_id }}</span></td>
|
||||
<td><span class="mono" style="font-size:11px;color:#9EA8BE;">{{ p.fetched_at | datefmt }}</span></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="empty-state">
|
||||
<i class="bi bi-box-seam"></i>
|
||||
<p>Товары в этом альбоме не найдены.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<article class="card">
|
||||
{% if products %}
|
||||
<div class="table-scroll">
|
||||
<table class="align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>Название</th>
|
||||
<th>Цена</th>
|
||||
<th>Статус</th>
|
||||
<th>ID</th>
|
||||
<th>Обновлено</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for p in products %}
|
||||
<tr>
|
||||
<td style="width:48px;">
|
||||
{% if p.thumb_url %}
|
||||
<img src="{{ p.thumb_url }}" alt="" style="width:40px;height:40px;object-fit:cover;border-radius:4px;">
|
||||
{% else %}
|
||||
<span class="text-muted"><i class="bi bi-image" style="font-size:1.5rem;"></i></span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<strong>{{ p.name }}</strong>
|
||||
{% if p.description %}
|
||||
<br><span class="text-muted small">{{ p.description[:80] }}{% if p.description|length > 80 %}…{% endif %}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-muted">
|
||||
{% if p.price is not none %}{{ p.price | price }}{% else %}—{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if p.availability == 0 %}
|
||||
<span class="badge badge-success">В наличии</span>
|
||||
{% elif p.availability == 1 %}
|
||||
<span class="badge badge-secondary">Удалён</span>
|
||||
{% elif p.availability == 2 %}
|
||||
<span class="badge badge-warning">Недоступен</span>
|
||||
{% else %}
|
||||
<span class="text-muted small">—</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-muted small">{{ p.vk_product_id }}</td>
|
||||
<td class="text-muted small">{{ p.fetched_at | datefmt }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-center py-5 text-muted">
|
||||
<i class="bi bi-box-seam" style="font-size:2rem;"></i>
|
||||
<p class="mt-2">Товары в этом альбоме не найдены.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</article>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user