This commit is contained in:
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "web-petting"
|
name = "web-petting"
|
||||||
version = "1.0.4"
|
version = "1.0.5"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
default-run = "web-petting"
|
default-run = "web-petting"
|
||||||
|
|
||||||
|
|||||||
@@ -1046,6 +1046,7 @@ async fn serve_static(_request: Request, Path(filename): Path<String>) -> cot::R
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
let content_type = match ext {
|
let content_type = match ext {
|
||||||
|
"css" => "text/css; charset=utf-8",
|
||||||
"png" => "image/png",
|
"png" => "image/png",
|
||||||
"jpg" | "jpeg" => "image/jpeg",
|
"jpg" | "jpeg" => "image/jpeg",
|
||||||
"webp" => "image/webp",
|
"webp" => "image/webp",
|
||||||
|
|||||||
@@ -0,0 +1,708 @@
|
|||||||
|
:root {
|
||||||
|
--admin-accent: #7567e8;
|
||||||
|
--admin-accent-dark: #6255cf;
|
||||||
|
--admin-accent-soft: #efedff;
|
||||||
|
--admin-bg: #f6f5f9;
|
||||||
|
--admin-surface: #fff;
|
||||||
|
--admin-surface-soft: #fbfaff;
|
||||||
|
--admin-border: #e7e5ef;
|
||||||
|
--admin-border-soft: #eeecf3;
|
||||||
|
--admin-text: #302c49;
|
||||||
|
--admin-muted: #817c94;
|
||||||
|
--admin-danger: #b54e5b;
|
||||||
|
--admin-success: #177349;
|
||||||
|
color-scheme: light;
|
||||||
|
}
|
||||||
|
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
|
||||||
|
html { background: var(--admin-bg); }
|
||||||
|
|
||||||
|
body {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: var(--admin-bg);
|
||||||
|
color: var(--admin-text);
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
a { color: var(--admin-accent-dark); }
|
||||||
|
|
||||||
|
/* Application shell */
|
||||||
|
.admin-shell { padding-bottom: 4.35rem; }
|
||||||
|
|
||||||
|
.top-header {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 30;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 1rem;
|
||||||
|
min-height: 3.5rem;
|
||||||
|
padding: .55rem 1rem;
|
||||||
|
background: rgba(255,255,255,.94);
|
||||||
|
border-bottom: 1px solid var(--admin-border);
|
||||||
|
box-shadow: 0 1px 8px rgba(42,34,80,.035);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-header .brand {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
color: var(--admin-text);
|
||||||
|
font-size: 1.05rem;
|
||||||
|
font-weight: 800;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-version {
|
||||||
|
color: #aaa5b6;
|
||||||
|
font-size: .66rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-header-right {
|
||||||
|
display: flex;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
align-items: center;
|
||||||
|
gap: .7rem;
|
||||||
|
font-size: .8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-header-right a { color: #777184; text-decoration: none; }
|
||||||
|
.top-header-right a:hover { color: var(--admin-accent-dark); }
|
||||||
|
.top-header-right .admin-name { color: #aaa5b6; }
|
||||||
|
|
||||||
|
.desktop-nav { display: none; }
|
||||||
|
|
||||||
|
.bottom-tabs {
|
||||||
|
position: fixed;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 30;
|
||||||
|
display: flex;
|
||||||
|
height: 3.65rem;
|
||||||
|
overflow-x: auto;
|
||||||
|
background: rgba(255,255,255,.96);
|
||||||
|
border-top: 1px solid var(--admin-border);
|
||||||
|
box-shadow: 0 -4px 18px rgba(42,34,80,.06);
|
||||||
|
scrollbar-width: none;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-tabs::-webkit-scrollbar { display: none; }
|
||||||
|
|
||||||
|
.bottom-tabs a {
|
||||||
|
display: flex;
|
||||||
|
flex: 1 0 3.2rem;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-width: 3.2rem;
|
||||||
|
padding: 0 .42rem;
|
||||||
|
color: #9993a8;
|
||||||
|
font-size: .59rem;
|
||||||
|
font-weight: 700;
|
||||||
|
text-decoration: none;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-tabs a .tab-icon { font-size: 1.2rem; line-height: 1; }
|
||||||
|
.bottom-tabs a .tab-label { display: block; margin-top: .12rem; }
|
||||||
|
.bottom-tabs a.is-active { color: var(--admin-accent); }
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 980px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 1.25rem 1rem 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Typography and page chrome */
|
||||||
|
.page-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: .8rem;
|
||||||
|
margin-bottom: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-head h1 {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--admin-text);
|
||||||
|
font-size: 1.55rem;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: -.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-head p,
|
||||||
|
.page-description {
|
||||||
|
margin: .25rem 0 0;
|
||||||
|
color: var(--admin-muted);
|
||||||
|
font-size: .86rem;
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-head-actions,
|
||||||
|
.toolbar,
|
||||||
|
.action-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: .5rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
margin: 1.55rem 0 .75rem;
|
||||||
|
color: #514b69;
|
||||||
|
font-size: .82rem;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: .04em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
.section-inline-head { display: flex; align-items: center; justify-content: space-between; gap: .75rem; margin-bottom: .75rem; }
|
||||||
|
.section-inline-head .label { margin: 0; }
|
||||||
|
.form-card hr { height: 1px; margin: 1rem 0; background: var(--admin-border-soft); border: 0; }
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
padding: 2rem 1rem;
|
||||||
|
color: var(--admin-muted);
|
||||||
|
text-align: center;
|
||||||
|
background: rgba(255,255,255,.65);
|
||||||
|
border: 1px dashed #dcd8e8;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Buttons */
|
||||||
|
.button {
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: none !important;
|
||||||
|
font-weight: 700;
|
||||||
|
transition: transform .12s, border-color .15s, background .15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button:hover { transform: translateY(-1px); }
|
||||||
|
.button:active { transform: none; }
|
||||||
|
.button.is-primary { background: var(--admin-accent); border-color: var(--admin-accent); }
|
||||||
|
.button.is-primary:not(.is-outlined):not(.is-light),
|
||||||
|
.button.is-link:not(.is-outlined):not(.is-light),
|
||||||
|
.button.is-info:not(.is-outlined):not(.is-light),
|
||||||
|
.button.is-success:not(.is-outlined):not(.is-light),
|
||||||
|
.button.is-danger:not(.is-outlined):not(.is-light) { color: #fff !important; }
|
||||||
|
.button.is-primary:hover { color: #fff !important; background: var(--admin-accent-dark); border-color: var(--admin-accent-dark); }
|
||||||
|
.button.is-primary.is-outlined { color: var(--admin-accent-dark) !important; background: transparent; border-color: #a49bea; }
|
||||||
|
.button.is-primary.is-outlined:hover { color: #fff !important; background: var(--admin-accent); border-color: var(--admin-accent); }
|
||||||
|
.button.is-light { background: #f3f1f8; color: #5f5971; }
|
||||||
|
.button[disabled] { color: #8d8798 !important; background: #eceaf0 !important; border-color: #e0dde7 !important; opacity: 1; transform: none; }
|
||||||
|
.button.is-small,
|
||||||
|
.btn-sm { height: auto !important; padding: .32rem .65rem !important; font-size: .75rem !important; }
|
||||||
|
|
||||||
|
/* Reusable cards and sections */
|
||||||
|
.admin-section,
|
||||||
|
.form-card {
|
||||||
|
overflow: hidden;
|
||||||
|
background: var(--admin-surface);
|
||||||
|
border: 1px solid var(--admin-border);
|
||||||
|
border-radius: 14px;
|
||||||
|
box-shadow: 0 2px 10px rgba(42,34,80,.035);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-card { padding: 1.2rem; }
|
||||||
|
|
||||||
|
.admin-section-head {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 42px minmax(0,1fr);
|
||||||
|
gap: .8rem;
|
||||||
|
padding: 1rem 1.15rem;
|
||||||
|
background: linear-gradient(180deg,#fff,var(--admin-surface-soft));
|
||||||
|
border-bottom: 1px solid #eceaf2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-section-icon {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 42px;
|
||||||
|
height: 42px;
|
||||||
|
background: var(--admin-accent-soft);
|
||||||
|
border-radius: 11px;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-section-head h2 {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--admin-text);
|
||||||
|
font-size: 1.05rem;
|
||||||
|
font-weight: 750;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-section-head p {
|
||||||
|
max-width: 680px;
|
||||||
|
margin: .2rem 0 0;
|
||||||
|
color: var(--admin-muted);
|
||||||
|
font-size: .82rem;
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-section-body { padding: 1.15rem; }
|
||||||
|
|
||||||
|
.admin-title-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: .55rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-pill {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: .12rem .48rem;
|
||||||
|
color: #777184;
|
||||||
|
background: #eeedf2;
|
||||||
|
border-radius: 99px;
|
||||||
|
font-size: .67rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-pill.is-enabled { color: var(--admin-success); background: #dcf7e8; }
|
||||||
|
|
||||||
|
.admin-grid,
|
||||||
|
.form-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2,minmax(0,1fr));
|
||||||
|
gap: 1rem 1.1rem;
|
||||||
|
}
|
||||||
|
.form-grid > .field { margin: 0; }
|
||||||
|
.page-head .button.is-primary { min-height: 2.25rem; padding: .42rem .8rem !important; }
|
||||||
|
|
||||||
|
.admin-field { min-width: 0; margin: 0 !important; }
|
||||||
|
.admin-field-wide { grid-column: 1 / -1; }
|
||||||
|
|
||||||
|
.admin-subsection {
|
||||||
|
margin-top: 1.1rem;
|
||||||
|
padding-top: 1.1rem;
|
||||||
|
border-top: 1px solid var(--admin-border-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-subsection h2,
|
||||||
|
.admin-subsection h3,
|
||||||
|
.form-card > h2 {
|
||||||
|
margin: 0 0 .75rem;
|
||||||
|
color: #514b69;
|
||||||
|
font-size: .82rem;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: .04em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-card {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: .65rem;
|
||||||
|
padding: .9rem 1rem;
|
||||||
|
color: inherit;
|
||||||
|
background: var(--admin-surface);
|
||||||
|
border: 1px solid var(--admin-border);
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 1px 6px rgba(42,34,80,.025);
|
||||||
|
transition: border-color .15s, box-shadow .15s, transform .15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-card:hover {
|
||||||
|
border-color: #dcd8eb;
|
||||||
|
box-shadow: 0 5px 18px rgba(42,34,80,.055);
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-card-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: .75rem;
|
||||||
|
margin-bottom: .35rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-card-header .name {
|
||||||
|
min-width: 0;
|
||||||
|
color: var(--admin-text);
|
||||||
|
font-size: .95rem;
|
||||||
|
font-weight: 750;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-card-meta {
|
||||||
|
display: flex;
|
||||||
|
gap: .25rem 1rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
color: var(--admin-muted);
|
||||||
|
font-size: .79rem;
|
||||||
|
line-height: 1.55;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-card-meta a { color: inherit; text-decoration: none; }
|
||||||
|
|
||||||
|
.item-card-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: .4rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-top: .65rem;
|
||||||
|
padding-top: .6rem;
|
||||||
|
border-top: 1px solid var(--admin-border-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-card-actions form { margin: 0; }
|
||||||
|
.color-dot { display: inline-block; width: 11px; height: 11px; margin-right: 6px; border-radius: 50%; vertical-align: middle; }
|
||||||
|
.card-link { color: inherit; text-decoration: none; }
|
||||||
|
.card-note { margin-top: .35rem; color: var(--admin-muted); font-size: .78rem; line-height: 1.45; }
|
||||||
|
.feedback-card { border-left: 3px solid var(--admin-accent); text-decoration: none; }
|
||||||
|
.feedback-head { display: flex; align-items: center; justify-content: space-between; gap: .75rem; margin-bottom: .3rem; }
|
||||||
|
.feedback-date { color: #9993a8; font-size: .75rem; white-space: nowrap; }
|
||||||
|
.feedback-text { color: #4a4570; font-size: .82rem; line-height: 1.5; }
|
||||||
|
.muted-detail { color: var(--admin-muted); font-weight: 450; }
|
||||||
|
|
||||||
|
/* Statuses */
|
||||||
|
.badge {
|
||||||
|
display: inline-flex;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
align-items: center;
|
||||||
|
padding: .16rem .58rem;
|
||||||
|
border-radius: 99px;
|
||||||
|
font-size: .69rem;
|
||||||
|
font-weight: 750;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-new,
|
||||||
|
.badge-visit-scheduled { color: #1e40af; background: #dbeafe; }
|
||||||
|
.badge-in_progress { color: #92400e; background: #fef3c7; }
|
||||||
|
.badge-converted,
|
||||||
|
.badge-active,
|
||||||
|
.badge-visit-completed { color: #065f46; background: #d1fae5; }
|
||||||
|
.badge-rejected { color: #991b1b; background: #fee2e2; }
|
||||||
|
.badge-archived,
|
||||||
|
.badge-visit-cancelled { color: #554f61; background: #ebe9ef; }
|
||||||
|
|
||||||
|
/* Forms */
|
||||||
|
.field:not(:last-child) { margin-bottom: 1rem; }
|
||||||
|
.field .label,
|
||||||
|
.admin-field .label {
|
||||||
|
margin-bottom: .38rem;
|
||||||
|
color: #514c66 !important;
|
||||||
|
font-size: .76rem;
|
||||||
|
font-weight: 750;
|
||||||
|
}
|
||||||
|
|
||||||
|
input,
|
||||||
|
textarea,
|
||||||
|
select,
|
||||||
|
.input,
|
||||||
|
.textarea,
|
||||||
|
.select select {
|
||||||
|
color: var(--admin-text) !important;
|
||||||
|
background-color: #fff !important;
|
||||||
|
border-color: #dcd9e5 !important;
|
||||||
|
border-radius: 8px !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
font-size: .86rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input,
|
||||||
|
.select select { min-height: 2.45rem; }
|
||||||
|
.textarea { min-height: 76px; resize: vertical; }
|
||||||
|
|
||||||
|
input:focus,
|
||||||
|
textarea:focus,
|
||||||
|
select:focus,
|
||||||
|
.input:focus,
|
||||||
|
.textarea:focus,
|
||||||
|
.select select:focus {
|
||||||
|
border-color: #7c6cff !important;
|
||||||
|
box-shadow: 0 0 0 2px rgba(124,108,255,.12) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[readonly],
|
||||||
|
.input[readonly] { color: #716b81 !important; background: #f8f7fa !important; }
|
||||||
|
|
||||||
|
input[type="file"].input { padding: .36rem .55rem; }
|
||||||
|
input[type="color"] { width: 3rem; height: 2.35rem; padding: 2px; cursor: pointer; }
|
||||||
|
input[type="checkbox"] { accent-color: var(--admin-accent); }
|
||||||
|
|
||||||
|
.field-help {
|
||||||
|
margin-top: .35rem;
|
||||||
|
color: #8b869b;
|
||||||
|
font-size: .72rem;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.secret-control { position: relative; }
|
||||||
|
.secret-control::after { position: absolute; top: 50%; right: .75rem; content: "🔒"; transform: translateY(-50%); opacity: .48; font-size: .78rem; pointer-events: none; }
|
||||||
|
.secret-control .input { padding-right: 2.25rem; letter-spacing: .05em; }
|
||||||
|
.admin-form { display: flex; flex-direction: column; gap: 1rem; }
|
||||||
|
.admin-toggle-list { display: grid; grid-template-columns: repeat(2,minmax(0,1fr)); gap: .7rem; margin-bottom: 1rem; }
|
||||||
|
.admin-toggle { position: relative; display: flex; align-items: flex-start; gap: .7rem; padding: .75rem .85rem; color: inherit !important; background: var(--admin-surface-soft); border: 1px solid #e8e5f2; border-radius: 10px; cursor: pointer; transition: border-color .15s,background .15s; }
|
||||||
|
.admin-toggle:hover { background: #f8f6ff; border-color: #dcd7ef; }
|
||||||
|
.admin-toggle > input { position: absolute; opacity: 0; pointer-events: none; }
|
||||||
|
.admin-toggle-track { flex: 0 0 auto; width: 38px; height: 22px; padding: 2px; background: #ccc8d6; border-radius: 99px; transition: .18s; }
|
||||||
|
.admin-toggle-track span { display: block; width: 18px; height: 18px; background: #fff; border-radius: 50%; box-shadow: 0 1px 4px rgba(0,0,0,.2); transition: .18s; }
|
||||||
|
.admin-toggle > input:checked + .admin-toggle-track { background: var(--admin-accent); }
|
||||||
|
.admin-toggle > input:checked + .admin-toggle-track span { transform: translateX(16px); }
|
||||||
|
.admin-toggle > input:focus-visible + .admin-toggle-track { outline: 2px solid #7c6cff; outline-offset: 2px; }
|
||||||
|
.admin-toggle strong { display: block; color: #3e3956; font-size: .82rem; line-height: 1.35; }
|
||||||
|
.admin-toggle small { display: block; margin-top: .15rem; color: #8a849c; font-size: .7rem; line-height: 1.35; }
|
||||||
|
.admin-toggle-compact { align-items: center; padding: .55rem .65rem; }
|
||||||
|
|
||||||
|
.admin-check { position: relative; display: inline-flex; align-items: center; gap: .45rem; color: #716b81 !important; font-size: .72rem; line-height: 1.35; cursor: pointer; }
|
||||||
|
.admin-check > input { position: absolute; opacity: 0; pointer-events: none; }
|
||||||
|
.admin-check-box { display: inline-flex; flex: 0 0 auto; align-items: center; justify-content: center; width: 18px; height: 18px; color: transparent; background: #fff; border: 1.5px solid #cfcad9; border-radius: 5px; font-size: .68rem; font-weight: 900; transition: .15s; }
|
||||||
|
.admin-check > input:checked + .admin-check-box { color: #fff; background: var(--admin-accent); border-color: var(--admin-accent); }
|
||||||
|
.admin-check > input:focus-visible + .admin-check-box { outline: 2px solid #7c6cff; outline-offset: 2px; }
|
||||||
|
.admin-check-danger { color: #a04e58 !important; }
|
||||||
|
.admin-check-danger > input:checked + .admin-check-box { background: var(--admin-danger); border-color: var(--admin-danger); }
|
||||||
|
|
||||||
|
.admin-note { margin: .9rem 0 1rem; padding: .75rem .9rem; border: 1px solid; border-radius: 9px; font-size: .76rem; line-height: 1.5; }
|
||||||
|
.admin-note p + p { margin-top: .35rem; }
|
||||||
|
.admin-note-info { color: #42627f; background: #f1f7ff; border-color: #d7e8fa; }
|
||||||
|
.admin-note-warning { color: #785f28; background: #fff8e8; border-color: #f1dfb4; }
|
||||||
|
.admin-note code { display: inline-block; margin: .2rem 0 0; padding: .12rem .35rem; color: inherit; background: rgba(255,255,255,.7); border-radius: 4px; user-select: all; }
|
||||||
|
|
||||||
|
.admin-details { margin-top: 1rem; overflow: hidden; background: #fcfbff; border: 1px solid #e8e5f2; border-radius: 9px; }
|
||||||
|
.admin-details summary { display: flex; align-items: center; justify-content: space-between; padding: .7rem .85rem; color: #514b69; font-size: .78rem; font-weight: 700; cursor: pointer; }
|
||||||
|
.admin-details summary span { min-width: 24px; padding: .05rem .4rem; color: #6257bb; text-align: center; background: #ece9ff; border-radius: 99px; }
|
||||||
|
.admin-details-body { padding: 0 .85rem .85rem; }
|
||||||
|
|
||||||
|
.sticky-actions {
|
||||||
|
position: sticky;
|
||||||
|
bottom: 4.3rem;
|
||||||
|
z-index: 10;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding: .65rem;
|
||||||
|
background: rgba(255,255,255,.92);
|
||||||
|
border: 1px solid rgba(222,218,238,.9);
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 8px 28px rgba(46,38,86,.12);
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
}
|
||||||
|
.sticky-actions .button { min-width: 150px; }
|
||||||
|
|
||||||
|
.danger-zone { margin-top: 1rem; padding-top: 1rem; border-top: 1px solid #f1dfe2; }
|
||||||
|
.inline-control { display: flex; align-items: center; gap: .5rem; }
|
||||||
|
.inline-control > .input,
|
||||||
|
.inline-control > .select { flex: 1; min-width: 0; }
|
||||||
|
.color-control { display: flex; align-items: center; gap: .6rem; }
|
||||||
|
.portal-panel { margin-top: 1.1rem; padding-top: 1.1rem; border-top: 1px solid var(--admin-border-soft); }
|
||||||
|
.portal-qr { margin-top: .8rem; text-align: center; }
|
||||||
|
.portal-qr canvas { max-width: 180px; padding: .4rem; background: #fff; border: 1px solid var(--admin-border); border-radius: 10px; }
|
||||||
|
.inline-admin-form { margin-top: .7rem; padding-top: .7rem; border-top: 1px solid var(--admin-border-soft); }
|
||||||
|
.inline-admin-grid { display: grid; grid-template-columns: minmax(180px,1fr) minmax(180px,.8fr) auto; align-items: end; gap: .75rem; }
|
||||||
|
.inline-admin-grid .field { margin: 0; }
|
||||||
|
.testimonial-content { display: flex; align-items: flex-start; gap: .75rem; min-width: 0; }
|
||||||
|
.testimonial-avatar { flex: 0 0 auto; width: 48px; height: 48px; border-radius: 12px; object-fit: cover; }
|
||||||
|
.testimonial-text { color: #49435d; font-size: .88rem; line-height: 1.5; }
|
||||||
|
.testimonial-note { margin-top: .2rem; color: var(--admin-muted); font-size: .76rem; }
|
||||||
|
|
||||||
|
/* Messages and tables */
|
||||||
|
.notification { color: var(--admin-text) !important; border-radius: 10px; }
|
||||||
|
.table-wrap { overflow-x: auto; }
|
||||||
|
.table-wrap .table { margin: 0; background: transparent; font-size: .75rem; }
|
||||||
|
.table th { color: #696278 !important; font-size: .7rem; text-transform: uppercase; letter-spacing: .03em; }
|
||||||
|
.is-nowrap { white-space: nowrap; }
|
||||||
|
.seo-preview { min-height: 2rem; margin-top: .5rem; padding: .5rem .65rem; background: #fafafa; border: 1px solid #ebe9ef; border-radius: 7px; font-size: .78rem; line-height: 2; }
|
||||||
|
|
||||||
|
.pagination-row,
|
||||||
|
.media-pagination { display: flex; justify-content: center; gap: .35rem; flex-wrap: wrap; margin-top: 1.25rem; }
|
||||||
|
|
||||||
|
/* Media */
|
||||||
|
.filter-bar { display: flex; align-items: center; gap: .6rem; margin-bottom: 1rem; padding: .65rem .75rem; background: #fff; border: 1px solid var(--admin-border); border-radius: 10px; }
|
||||||
|
.media-grid { display: grid; grid-template-columns: repeat(auto-fill,minmax(200px,1fr)); gap: .8rem; }
|
||||||
|
.media-card { overflow: hidden; background: #fff; border: 1px solid var(--admin-border); border-radius: 12px; box-shadow: 0 1px 6px rgba(42,34,80,.025); transition: transform .15s, box-shadow .15s; }
|
||||||
|
.media-card:hover { transform: translateY(-2px); box-shadow: 0 7px 20px rgba(42,34,80,.08); }
|
||||||
|
.media-card img { display: block; width: 100%; height: 160px; object-fit: cover; }
|
||||||
|
.media-card .photo-thumb { width: 100%; height: 160px; }
|
||||||
|
.media-card .video-thumb { position: relative; width: 100%; height: 160px; background: #17151e; }
|
||||||
|
.media-card .video-play,
|
||||||
|
.visit-media-item .video-play { position: absolute; top: 50%; left: 50%; color: #fff; line-height: 1; text-shadow: 0 1px 5px #000; transform: translate(-50%,-50%); pointer-events: none; }
|
||||||
|
.media-card .video-play { font-size: 2.5rem; }
|
||||||
|
.media-info { padding: .7rem .8rem; }
|
||||||
|
.media-meta { display: flex; align-items: center; justify-content: space-between; gap: .5rem; color: var(--admin-text); font-size: .82rem; }
|
||||||
|
.media-caption { margin-top: .25rem; color: #746e82; font-size: .78rem; }
|
||||||
|
|
||||||
|
.visit-media-grid { display: grid; grid-template-columns: repeat(auto-fill,minmax(105px,1fr)); gap: .55rem; margin-bottom: 1rem; }
|
||||||
|
.visit-media-item { overflow: hidden; background: #faf9fc; border: 1px solid var(--admin-border); border-radius: 9px; }
|
||||||
|
.visit-media-item img,
|
||||||
|
.visit-media-item .photo-thumb,
|
||||||
|
.visit-media-item .video-thumb-sm { display: block; width: 100%; height: 82px; object-fit: cover; }
|
||||||
|
.visit-media-item .video-thumb-sm { position: relative; background: #17151e; }
|
||||||
|
.visit-media-item .video-play { font-size: 1.6rem; }
|
||||||
|
.visit-media-item .media-cap { padding: .25rem .4rem; overflow: hidden; color: var(--admin-muted); font-size: .69rem; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
|
.visit-media-delete { padding: .25rem .4rem .4rem; }
|
||||||
|
.visit-media-delete .button { width: 100%; min-height: 1.65rem; font-size: .66rem; }
|
||||||
|
|
||||||
|
/* Upload queue */
|
||||||
|
.upload-context { margin-bottom: 1rem; padding: .7rem .8rem; color: #686276; background: var(--admin-surface-soft); border: 1px solid var(--admin-border-soft); border-radius: 9px; font-size: .82rem; }
|
||||||
|
.upload-queue { display: flex; flex-direction: column; gap: .4rem; margin-top: .65rem; }
|
||||||
|
.upload-queue:empty { display: none; }
|
||||||
|
.upload-queue-item { display: grid; grid-template-columns: 34px minmax(0,1fr); align-items: center; gap: .55rem; padding: .45rem .55rem; background: #f7f6ff; border-radius: 8px; }
|
||||||
|
.upload-queue-icon { display: flex; align-items: center; justify-content: center; width: 34px; height: 34px; background: #ebe8ff; border-radius: 7px; font-size: 1.05rem; }
|
||||||
|
.upload-queue-name { overflow: hidden; color: #494467; font-size: .78rem; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
|
.upload-queue-state { color: #8a84a5; font-size: .68rem; }
|
||||||
|
.upload-queue-track { height: 3px; margin-top: .25rem; overflow: hidden; background: #dedbea; border-radius: 99px; }
|
||||||
|
.upload-queue-bar { width: 0; height: 100%; background: var(--admin-accent); transition: width .2s; }
|
||||||
|
.upload-progress { display: none; margin-bottom: 1rem; }
|
||||||
|
.upload-progress-head { display: flex; justify-content: space-between; margin-bottom: .3rem; color: #5f596d; font-size: .78rem; }
|
||||||
|
.upload-progress-track { height: 7px; overflow: hidden; background: #e8e6ed; border-radius: 99px; }
|
||||||
|
.upload-progress-bar { width: 0; height: 100%; background: linear-gradient(90deg,#6c63ff,#b06cff); border-radius: 99px; transition: width .2s; }
|
||||||
|
#uploadBar.is-processing,
|
||||||
|
.upload-queue-bar.is-processing { width: 35% !important; animation: upload-processing 1.15s ease-in-out infinite; }
|
||||||
|
@keyframes upload-processing { from { transform: translateX(-110%); } to { transform: translateX(300%); } }
|
||||||
|
|
||||||
|
/* Schedule */
|
||||||
|
#calendar-wrap { padding: .75rem; overflow: hidden; }
|
||||||
|
.fc { color: var(--admin-text); font-size: .82rem; }
|
||||||
|
.fc .fc-daygrid-day-number,
|
||||||
|
.fc .fc-col-header-cell-cushion,
|
||||||
|
.fc .fc-list-day-text,
|
||||||
|
.fc .fc-list-day-side-text,
|
||||||
|
.fc .fc-toolbar-title,
|
||||||
|
.fc th,
|
||||||
|
.fc td { color: var(--admin-text) !important; }
|
||||||
|
.fc .fc-toolbar { gap: .55rem; flex-wrap: nowrap; }
|
||||||
|
.fc .fc-toolbar-chunk { min-width: 0; }
|
||||||
|
.fc .fc-toolbar-chunk:first-child,
|
||||||
|
.fc .fc-toolbar-chunk:last-child { display: flex; align-items: center; }
|
||||||
|
.fc .fc-button-group { display: flex; flex-wrap: nowrap; gap: .35rem; }
|
||||||
|
.fc .fc-button-group > .fc-button { margin-left: 0 !important; border-radius: 7px !important; }
|
||||||
|
.fc .fc-toolbar-title { font-size: 1.05rem !important; white-space: nowrap; }
|
||||||
|
.fc .fc-button { padding: .28rem .52rem !important; color: #fff !important; background: var(--admin-accent) !important; border-color: var(--admin-accent) !important; border-radius: 7px !important; font-size: .76rem !important; }
|
||||||
|
.fc-event { padding: 2px 5px; border: none !important; border-radius: 5px; cursor: pointer; }
|
||||||
|
.fc-event.ev-completed .fc-event-title,
|
||||||
|
.fc-event.ev-completed .fc-list-event-title,
|
||||||
|
.fc-event.ev-cancelled .fc-event-title,
|
||||||
|
.fc-event.ev-cancelled .fc-list-event-title { text-decoration: line-through; opacity: .75; }
|
||||||
|
.fc .fc-day-today { background: #f0eeff !important; }
|
||||||
|
.fc .fc-day.day-weekend { background: #fdf9f6; }
|
||||||
|
.fc .fc-day-today.day-weekend { background: #f0eeff !important; }
|
||||||
|
|
||||||
|
.time-row { display: flex; align-items: center; gap: .5rem; }
|
||||||
|
.time-block { display: flex; flex: 1; flex-direction: column; }
|
||||||
|
.time-lbl { margin-bottom: .2rem; color: var(--admin-muted); font-size: .72rem; }
|
||||||
|
.time-sep { padding-top: 1.2rem; color: #aaa5b6; font-size: 1.1rem; }
|
||||||
|
.cal-toolbar { display: flex; align-items: center; justify-content: space-between; gap: .5rem; margin-bottom: .55rem; }
|
||||||
|
.sched-cal { overflow: hidden; background: #faf9fc; border: 1px solid var(--admin-border); border-radius: 11px; }
|
||||||
|
.cal-nav { display: flex; align-items: center; justify-content: space-between; padding: .65rem .75rem; background: #fff; border-bottom: 1px solid var(--admin-border-soft); }
|
||||||
|
.cal-nav button { padding: .2rem .5rem; color: var(--admin-accent); background: none; border: 0; border-radius: 6px; font-size: 1.05rem; cursor: pointer; }
|
||||||
|
.cal-nav button:hover { background: #f0eeff; }
|
||||||
|
.cal-nav span { color: var(--admin-text); font-size: .95rem; font-weight: 750; }
|
||||||
|
.cal-grid { display: grid; grid-template-columns: repeat(7,1fr); }
|
||||||
|
.cal-wday { padding: .42rem 0; color: #aaa5b6; background: #faf9fc; font-size: .69rem; font-weight: 750; text-align: center; }
|
||||||
|
.cal-wday.is-weekend { color: #d98a91; }
|
||||||
|
.cal-day { position: relative; padding: .55rem .2rem; color: var(--admin-text); border-radius: 0; font-size: .86rem; text-align: center; cursor: pointer; transition: background .1s; user-select: none; }
|
||||||
|
.cal-day:hover { background: #f0eeff; }
|
||||||
|
.cal-day.is-empty { cursor: default; }
|
||||||
|
.cal-day.is-empty:hover { background: none; }
|
||||||
|
.cal-day.is-today { color: var(--admin-accent); font-weight: 750; }
|
||||||
|
.cal-day.is-selected { color: #fff !important; background: var(--admin-accent) !important; }
|
||||||
|
.cal-day.is-range-start { color: #fff !important; background: #a89cff !important; }
|
||||||
|
.cal-day.is-past { color: #ccc8d3; }
|
||||||
|
.day-row { display: flex; align-items: center; gap: .4rem; padding: .5rem 0; border-bottom: 1px solid #f2f0f5; }
|
||||||
|
.day-row:last-child { border-bottom: 0; }
|
||||||
|
.day-date { flex: 1; min-width: 0; overflow: hidden; font-size: .82rem; font-weight: 650; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
|
.day-times { display: flex; flex-shrink: 0; align-items: center; gap: .25rem; }
|
||||||
|
.day-times .time-sep { padding: 0; font-size: .78rem; }
|
||||||
|
.day-rm { flex-shrink: 0; padding: .15rem .25rem; color: #bbb6c5; background: none; border: 0; font-size: 1rem; line-height: 1; cursor: pointer; }
|
||||||
|
.day-rm:hover { color: var(--admin-danger); }
|
||||||
|
.time-badge-wrap { position: relative; display: inline-flex; align-items: center; justify-content: center; min-width: 3.6rem; padding: .15rem .5rem; color: #5b52d6; background: #ede9ff; border: 1.5px solid #c4beff; border-radius: 20px; font-size: .76rem; font-weight: 750; white-space: nowrap; cursor: pointer; }
|
||||||
|
.time-badge-wrap:focus-within { background: #f0eeff; border-color: var(--admin-accent); }
|
||||||
|
.time-badge-label { z-index: 1; pointer-events: none; }
|
||||||
|
.time-badge-input { position: absolute; inset: 0; width: 100%; height: 100%; padding: 0; margin: 0; opacity: 0; background: transparent; border: 0; font-size: 16px; cursor: pointer; }
|
||||||
|
|
||||||
|
.status-picker { display: flex; gap: .5rem; }
|
||||||
|
.status-btn { display: flex; flex: 1; flex-direction: column; align-items: center; gap: .3rem; padding: .6rem .4rem; color: #777184; background: #f5f4f7; border: 2px solid transparent; border-radius: 10px; font-size: .79rem; font-weight: 650; line-height: 1.2; cursor: pointer; transition: all .15s; }
|
||||||
|
.status-btn-icon { font-size: 1.3rem; line-height: 1; }
|
||||||
|
.status-btn:hover { filter: brightness(.97); }
|
||||||
|
.status-btn-scheduled.is-active { color: #1e40af; background: #dbeafe; border-color: #3b82f6; }
|
||||||
|
.status-btn-completed.is-active { color: #15803d; background: #d1fae5; border-color: #22c55e; }
|
||||||
|
.status-btn-cancelled.is-active { color: #b91c1c; background: #fee2e2; border-color: #ef4444; }
|
||||||
|
|
||||||
|
/* Dialogs */
|
||||||
|
.visit-modal-bg,
|
||||||
|
.upload-modal-bg { position: fixed; inset: 0; z-index: 100; display: none; align-items: center; justify-content: center; padding: 1rem; background: rgba(31,27,48,.38); backdrop-filter: blur(3px); }
|
||||||
|
.visit-modal-bg.is-open,
|
||||||
|
.upload-modal-bg.is-open { display: flex; }
|
||||||
|
.visit-modal,
|
||||||
|
.upload-modal { width: 100%; max-width: 420px; padding: 1.25rem; background: #fff; border: 1px solid var(--admin-border); border-radius: 14px; box-shadow: 0 18px 50px rgba(31,27,48,.18); }
|
||||||
|
.visit-modal { max-width: 380px; }
|
||||||
|
.visit-modal h3 { margin: 0 0 .75rem; color: var(--admin-text); font-size: 1.05rem; font-weight: 750; }
|
||||||
|
.visit-modal .meta { margin-bottom: .75rem; color: var(--admin-muted); font-size: .82rem; line-height: 1.6; }
|
||||||
|
.modal-head { display: flex; align-items: center; justify-content: space-between; gap: .75rem; margin-bottom: 1rem; }
|
||||||
|
.modal-head h3 { margin: 0; color: var(--admin-text); font-size: 1.05rem; font-weight: 750; }
|
||||||
|
.modal-close { padding: .25rem; color: var(--admin-muted); background: none; border: 0; font-size: 1.15rem; cursor: pointer; }
|
||||||
|
|
||||||
|
/* Authentication screens */
|
||||||
|
.auth-shell { display: flex; align-items: center; justify-content: center; padding: 1.5rem 0; }
|
||||||
|
.login-box { width: 100%; max-width: 410px; padding: 0 1rem; }
|
||||||
|
.auth-language { margin-bottom: .7rem; text-align: right; }
|
||||||
|
.auth-language a { color: var(--admin-muted); font-size: .75rem; }
|
||||||
|
.login-card { padding: 1.6rem 1.4rem; background: #fff; border: 1px solid var(--admin-border); border-radius: 16px; box-shadow: 0 12px 38px rgba(42,34,80,.08); }
|
||||||
|
.auth-head { margin-bottom: 1.1rem; text-align: center; }
|
||||||
|
.auth-icon { display: flex; align-items: center; justify-content: center; width: 48px; height: 48px; margin: 0 auto .65rem; background: var(--admin-accent-soft); border-radius: 13px; font-size: 1.55rem; }
|
||||||
|
.auth-head h1 { margin: 0; color: var(--admin-text); font-size: 1.25rem; font-weight: 800; }
|
||||||
|
.auth-head p { margin-top: .2rem; color: var(--admin-muted); font-size: .84rem; }
|
||||||
|
.auth-divider { height: 1px; margin: 1rem 0; background: var(--admin-border-soft); border: 0; }
|
||||||
|
.turnstile-wrap { margin-top: .75rem; }
|
||||||
|
.time-columns { margin-bottom: 0 !important; }
|
||||||
|
|
||||||
|
@media (min-width: 1080px) {
|
||||||
|
.admin-shell { padding-bottom: 0; }
|
||||||
|
.bottom-tabs { display: none; }
|
||||||
|
.desktop-nav { display: flex; align-items: center; justify-content: center; gap: .2rem; }
|
||||||
|
.desktop-nav a { padding: .36rem .62rem; color: #625d70; border-radius: 7px; font-size: .78rem; font-weight: 650; text-decoration: none; transition: background .15s,color .15s; }
|
||||||
|
.desktop-nav a:hover { background: #f2f0f7; }
|
||||||
|
.desktop-nav a.is-active { color: #fff; background: var(--admin-accent); }
|
||||||
|
.sticky-actions { bottom: .75rem; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.top-header .brand { font-size: .95rem; }
|
||||||
|
.brand-version,
|
||||||
|
.top-header-right .admin-name { display: none; }
|
||||||
|
.main-content { padding: 1rem .75rem 1.5rem; }
|
||||||
|
.page-head { align-items: flex-start; }
|
||||||
|
.page-head h1 { font-size: 1.35rem; }
|
||||||
|
.admin-grid,
|
||||||
|
.form-grid,
|
||||||
|
.admin-toggle-list { grid-template-columns: 1fr; }
|
||||||
|
.admin-field-wide { grid-column: auto; }
|
||||||
|
.admin-section-head { grid-template-columns: 36px minmax(0,1fr); padding: .9rem; }
|
||||||
|
.admin-section-icon { width: 36px; height: 36px; }
|
||||||
|
.admin-section-body,
|
||||||
|
.form-card { padding: .9rem; }
|
||||||
|
.sticky-actions .button { width: 100%; }
|
||||||
|
.fc .fc-toolbar { display: grid; grid-template-columns: minmax(0,1fr) auto; align-items: center; gap: .55rem; font-size: .72rem; }
|
||||||
|
.fc .fc-toolbar-chunk:first-child { grid-column: 1; grid-row: 1; justify-self: start; }
|
||||||
|
.fc .fc-toolbar-chunk:nth-child(2) { grid-column: 2; grid-row: 1; justify-self: end; }
|
||||||
|
.fc .fc-toolbar-chunk:last-child { grid-column: 1 / -1; grid-row: 2; width: 100%; overflow-x: auto; }
|
||||||
|
.fc .fc-toolbar-chunk:last-child .fc-button-group { width: 100%; }
|
||||||
|
.fc .fc-toolbar-chunk:last-child .fc-button { flex: 1 0 auto; }
|
||||||
|
.fc .fc-toolbar-title { font-size: .92rem !important; }
|
||||||
|
.fc .fc-button { padding: .22rem .35rem !important; font-size: .68rem !important; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 520px) {
|
||||||
|
.page-head { flex-direction: column; }
|
||||||
|
.page-head-actions { width: 100%; }
|
||||||
|
.page-head-actions .button { flex: 1; }
|
||||||
|
.item-card-header { align-items: flex-start; }
|
||||||
|
.media-grid { grid-template-columns: repeat(2,minmax(0,1fr)); gap: .55rem; }
|
||||||
|
.media-card img,
|
||||||
|
.media-card .photo-thumb,
|
||||||
|
.media-card .video-thumb { height: 122px; }
|
||||||
|
.media-meta { align-items: flex-start; flex-direction: column; gap: .1rem; }
|
||||||
|
.status-picker { flex-direction: column; }
|
||||||
|
.status-btn { flex-direction: row; justify-content: flex-start; }
|
||||||
|
.inline-control { align-items: stretch; flex-direction: column; }
|
||||||
|
.inline-admin-grid { grid-template-columns: 1fr; align-items: stretch; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 400px) {
|
||||||
|
.bottom-tabs a .tab-label { display: none; }
|
||||||
|
.bottom-tabs a { min-width: 2.8rem; padding: 0 .3rem; }
|
||||||
|
}
|
||||||
@@ -5,12 +5,17 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="page-head">
|
<div class="page-head">
|
||||||
<h1>{{ title }}</h1>
|
<h1>{{ t.clients_title }}</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-card">
|
<section class="admin-section">
|
||||||
<form method="post" action="{{ action_url }}">
|
<header class="admin-section-head">
|
||||||
<div class="field">
|
<span class="admin-section-icon" aria-hidden="true">👤</span>
|
||||||
|
<div><h2>{{ title }}</h2></div>
|
||||||
|
</header>
|
||||||
|
<div class="admin-section-body">
|
||||||
|
<form method="post" action="{{ action_url }}" class="form-grid">
|
||||||
|
<div class="field admin-field-wide">
|
||||||
<label class="label">{{ t.clients_name }}</label>
|
<label class="label">{{ t.clients_name }}</label>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<input class="input" type="text" name="name" value="{{ client_name }}" required>
|
<input class="input" type="text" name="name" value="{{ client_name }}" required>
|
||||||
@@ -28,41 +33,42 @@
|
|||||||
<input class="input" type="email" name="email" value="{{ client_email }}">
|
<input class="input" type="email" name="email" value="{{ client_email }}">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field admin-field-wide">
|
||||||
<label class="label">{{ t.clients_address }}</label>
|
<label class="label">{{ t.clients_address }}</label>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<input class="input" type="text" name="address" value="{{ client_address }}">
|
<input class="input" type="text" name="address" value="{{ client_address }}">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field admin-field-wide">
|
||||||
<label class="label">{{ t.clients_notes }}</label>
|
<label class="label">{{ t.clients_notes }}</label>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<textarea class="textarea" name="notes" rows="3">{{ client_notes }}</textarea>
|
<textarea class="textarea" name="notes" rows="3">{{ client_notes }}</textarea>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field admin-field-wide">
|
||||||
<label class="label">{{ t.clients_color }}</label>
|
<label class="label">{{ t.clients_color }}</label>
|
||||||
<div class="control" style="display:flex;align-items:center;gap:0.5rem;">
|
<div class="control color-control">
|
||||||
<input type="color" name="color" value="{{ client_color }}" style="width:3rem;height:2.2rem;padding:0;border:1px solid #ddd;border-radius:6px;cursor:pointer;">
|
<input type="color" name="color" value="{{ client_color }}">
|
||||||
<span class="has-text-grey is-size-7">{{ client_color }}</span>
|
<span class="has-text-grey is-size-7">{{ client_color }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field admin-field-wide">
|
||||||
<button type="submit" class="button is-primary is-fullwidth">{{ submit_label }}</button>
|
<button type="submit" class="button is-primary is-fullwidth">{{ submit_label }}</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
{% if is_edit %}
|
{% if is_edit %}
|
||||||
<hr>
|
<div class="portal-panel">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label">{{ t.portal_link }}</label>
|
<label class="label">{{ t.portal_link }}</label>
|
||||||
<div class="control" style="display:flex;align-items:center;gap:0.5rem;">
|
<div class="control inline-control">
|
||||||
<input class="input is-small" type="text" readonly id="portalUrl" value="" style="flex:1;">
|
<input class="input is-small" type="text" readonly id="portalUrl" value="">
|
||||||
<button type="button" class="button is-small is-info is-outlined" onclick="navigator.clipboard.writeText(document.getElementById('portalUrl').value)">📋</button>
|
<button type="button" class="button is-small is-info is-outlined" onclick="navigator.clipboard.writeText(document.getElementById('portalUrl').value)">📋</button>
|
||||||
</div>
|
</div>
|
||||||
<div style="margin-top:0.75rem;text-align:center;">
|
<div class="portal-qr">
|
||||||
<canvas id="qrCanvas" style="max-width:180px;"></canvas>
|
<canvas id="qrCanvas"></canvas>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/qrious@4.0.2/dist/qrious.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/qrious@4.0.2/dist/qrious.min.js"></script>
|
||||||
<script>
|
<script>
|
||||||
(function() {
|
(function() {
|
||||||
@@ -71,7 +77,7 @@
|
|||||||
new QRious({ element: document.getElementById('qrCanvas'), value: url, size: 180, level: 'M' });
|
new QRious({ element: document.getElementById('qrCanvas'), value: url, size: 180, level: 'M' });
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
<hr>
|
<div class="danger-zone">
|
||||||
{% if client_status == "active" %}
|
{% if client_status == "active" %}
|
||||||
<form method="post" action="/admin/clients/{{ client_id }}/archive">
|
<form method="post" action="/admin/clients/{{ client_id }}/archive">
|
||||||
<button type="submit" class="button is-warning is-outlined is-fullwidth">{{ t.action_archive }}</button>
|
<button type="submit" class="button is-warning is-outlined is-fullwidth">{{ t.action_archive }}</button>
|
||||||
@@ -82,10 +88,12 @@
|
|||||||
</form>
|
</form>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if client_status != "deleted" %}
|
{% if client_status != "deleted" %}
|
||||||
<form method="post" action="/admin/clients/{{ client_id }}/delete" onsubmit="return confirm('{{ t.clients_delete_confirm }}');" style="margin-top:0.75rem;">
|
<form method="post" action="/admin/clients/{{ client_id }}/delete" onsubmit="return confirm('{{ t.clients_delete_confirm }}');" class="mt-3">
|
||||||
<button type="submit" class="button is-danger is-outlined is-fullwidth">{{ t.clients_delete }}</button>
|
<button type="submit" class="button is-danger is-outlined is-fullwidth">{{ t.clients_delete }}</button>
|
||||||
</form>
|
</form>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
</section>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="page-head">
|
<div class="page-head">
|
||||||
<h1>{{ t.clients_title }}</h1>
|
<h1>{{ t.clients_title }}</h1>
|
||||||
<div>
|
<div class="page-head-actions">
|
||||||
{% if show_all %}
|
{% if show_all %}
|
||||||
<a href="/admin/clients?lang={{ lang.code() }}" class="button is-small is-light">{{ t.filter_show_active }}</a>
|
<a href="/admin/clients?lang={{ lang.code() }}" class="button is-small is-light">{{ t.filter_show_active }}</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
@@ -17,19 +17,19 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if clients.is_empty() %}
|
{% if clients.is_empty() %}
|
||||||
<p class="has-text-grey">{{ t.clients_empty }}</p>
|
<p class="empty-state">{{ t.clients_empty }}</p>
|
||||||
{% else %}
|
{% else %}
|
||||||
{% for client in &clients %}
|
{% for client in &clients %}
|
||||||
<div class="item-card">
|
<div class="item-card">
|
||||||
<div class="item-card-header">
|
<div class="item-card-header">
|
||||||
<a href="/admin/clients/{{ client.id }}/edit?lang={{ lang.code() }}" class="name" style="text-decoration:none;color:inherit;">
|
<a href="/admin/clients/{{ client.id }}/edit?lang={{ lang.code() }}" class="name card-link">
|
||||||
<span style="display:inline-block;width:12px;height:12px;border-radius:50%;background:{{ client.color.as_deref().unwrap_or("#7c6ed4") }};margin-right:6px;vertical-align:middle;"></span>{{ client.name }}
|
<span class="color-dot" style="background:{{ client.color.as_deref().unwrap_or("#7c6ed4") }}"></span>{{ client.name }}
|
||||||
</a>
|
</a>
|
||||||
<span class="badge badge-{{ client.status }}">{{ t.client_status(&client.status) }}</span>
|
<span class="badge badge-{{ client.status }}">{{ t.client_status(&client.status) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="item-card-meta">
|
<div class="item-card-meta">
|
||||||
{% if let Some(phone) = client.phone.as_deref() %}
|
{% if let Some(phone) = client.phone.as_deref() %}
|
||||||
<span><a href="tel:{{ phone }}" style="color:inherit;text-decoration:none;">📞 {{ phone }}</a></span>
|
<span><a href="tel:{{ phone }}" class="card-link">📞 {{ phone }}</a></span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if let Some(email) = client.email.as_deref() %}
|
{% if let Some(email) = client.email.as_deref() %}
|
||||||
<span>✉️ {{ email }}</span>
|
<span>✉️ {{ email }}</span>
|
||||||
|
|||||||
@@ -10,27 +10,27 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if today_visits.is_empty() %}
|
{% if today_visits.is_empty() %}
|
||||||
<p class="has-text-grey">{{ t.dashboard_no_visits }}</p>
|
<p class="empty-state">{{ t.dashboard_no_visits }}</p>
|
||||||
{% else %}
|
{% else %}
|
||||||
{% for tv in &today_visits %}
|
{% for tv in &today_visits %}
|
||||||
<div class="item-card">
|
<div class="item-card">
|
||||||
<div class="item-card-header">
|
<div class="item-card-header">
|
||||||
<a href="/admin/schedule/{{ tv.visit.id }}/edit?lang={{ lang.code() }}" class="name" style="text-decoration:none;color:inherit;">
|
<a href="/admin/schedule/{{ tv.visit.id }}/edit?lang={{ lang.code() }}" class="name card-link">
|
||||||
<span style="display:inline-block;width:12px;height:12px;border-radius:50%;background:{{ tv.client_color }};margin-right:6px;vertical-align:middle;"></span>{{ tv.client_name }}
|
<span class="color-dot" style="background:{{ tv.client_color }}"></span>{{ tv.client_name }}
|
||||||
</a>
|
</a>
|
||||||
<span class="badge badge-visit-{{ tv.visit.status }}">{{ t.visit_status(&tv.visit.status) }}</span>
|
<span class="badge badge-visit-{{ tv.visit.status }}">{{ t.visit_status(&tv.visit.status) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="item-card-meta">
|
<div class="item-card-meta">
|
||||||
<span>🕐 {{ tv.visit.time_start }} — {{ tv.visit.time_end }}</span>
|
<span>🕐 {{ tv.visit.time_start }} — {{ tv.visit.time_end }}</span>
|
||||||
{% if !tv.client_phone.is_empty() %}
|
{% if !tv.client_phone.is_empty() %}
|
||||||
<span><a href="tel:{{ tv.client_phone }}" style="color:inherit;text-decoration:none;">📞 {{ tv.client_phone }}</a></span>
|
<span><a href="tel:{{ tv.client_phone }}" class="card-link">📞 {{ tv.client_phone }}</a></span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if !tv.client_address.is_empty() %}
|
{% if !tv.client_address.is_empty() %}
|
||||||
<span>📍 {{ tv.client_address }}</span>
|
<span>📍 {{ tv.client_address }}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% if let Some(notes) = tv.visit.notes.as_deref() %}
|
{% if let Some(notes) = tv.visit.notes.as_deref() %}
|
||||||
<div style="color:#888;font-size:0.82rem;margin-top:0.3rem;">{{ notes }}</div>
|
<div class="card-note">{{ notes }}</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="item-card-actions">
|
<div class="item-card-actions">
|
||||||
{% if tv.visit.status == "scheduled" %}
|
{% if tv.visit.status == "scheduled" %}
|
||||||
@@ -48,22 +48,22 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<!-- Feedbacks -->
|
<!-- Feedbacks -->
|
||||||
<h2 style="font-size:1.15rem;font-weight:700;margin:1.5rem 0 0.75rem;">{{ t.dashboard_recent_feedbacks }}</h2>
|
<h2 class="section-title">{{ t.dashboard_recent_feedbacks }}</h2>
|
||||||
{% if feedbacks.is_empty() %}
|
{% if feedbacks.is_empty() %}
|
||||||
<p class="has-text-grey">{{ t.dashboard_no_feedbacks }}</p>
|
<p class="empty-state">{{ t.dashboard_no_feedbacks }}</p>
|
||||||
{% else %}
|
{% else %}
|
||||||
{% for fb in &feedbacks %}
|
{% for fb in &feedbacks %}
|
||||||
<a href="/admin/schedule/{{ fb.visit_id }}/edit?lang={{ lang.code() }}" class="item-card" style="border-left:3px solid #7c6cff;display:block;text-decoration:none;color:inherit;">
|
<a href="/admin/schedule/{{ fb.visit_id }}/edit?lang={{ lang.code() }}" class="item-card feedback-card">
|
||||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:0.25rem;">
|
<div class="feedback-head">
|
||||||
<strong style="font-size:0.9rem;">{{ fb.client_name }}</strong>
|
<strong>{{ fb.client_name }}</strong>
|
||||||
<span style="color:#999;font-size:0.8rem;">{{ fb.visit_date }}</span>
|
<span class="feedback-date">{{ fb.visit_date }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div style="font-size:0.85rem;color:#4a4570;">{{ fb.feedback }}</div>
|
<div class="feedback-text">{{ fb.feedback }}</div>
|
||||||
</a>
|
</a>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
{% if feedback_total_pages > 1 %}
|
{% if feedback_total_pages > 1 %}
|
||||||
<div style="display:flex;justify-content:center;gap:0.5rem;margin-top:1rem;">
|
<div class="pagination-row">
|
||||||
{% if feedback_page > 1 %}
|
{% if feedback_page > 1 %}
|
||||||
<a href="/admin/?lang={{ lang.code() }}&page={{ feedback_page - 1 }}" class="button is-small is-light">«</a>
|
<a href="/admin/?lang={{ lang.code() }}&page={{ feedback_page - 1 }}" class="button is-small is-light">«</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
+3
-109
@@ -6,119 +6,13 @@
|
|||||||
<title>{{ t.nav_title }} — {% block title %}{% endblock %}</title>
|
<title>{{ t.nav_title }} — {% block title %}{% endblock %}</title>
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@1/css/bulma.min.css">
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@1/css/bulma.min.css">
|
||||||
<style>
|
<link rel="stylesheet" href="/static/admin.css?v={{ t.app_version() }}">
|
||||||
:root { --accent: #6c63ff; color-scheme: light; }
|
|
||||||
body { padding-bottom: 4rem; min-height: 100vh; background: #f5f5f5; color: #333; }
|
|
||||||
|
|
||||||
/* ── Top bar ── */
|
|
||||||
.top-header {
|
|
||||||
background: #fff; border-bottom: 1px solid #e8e8e8;
|
|
||||||
padding: 0.5rem 1rem; display: flex; align-items: center;
|
|
||||||
justify-content: space-between; position: sticky; top: 0; z-index: 30;
|
|
||||||
}
|
|
||||||
.top-header .brand { font-weight: 700; font-size: 1.1rem; color: #333; text-decoration: none; }
|
|
||||||
.top-header-right { display: flex; align-items: center; gap: 0.75rem; font-size: 0.85rem; }
|
|
||||||
.top-header-right a { color: #888; text-decoration: none; }
|
|
||||||
.top-header-right .admin-name { color: #aaa; }
|
|
||||||
|
|
||||||
/* ── Bottom tabs (mobile nav) ── */
|
|
||||||
.bottom-tabs {
|
|
||||||
position: fixed; bottom: 0; left: 0; right: 0; z-index: 30;
|
|
||||||
background: #fff; border-top: 1px solid #e8e8e8;
|
|
||||||
display: flex; height: 3.5rem;
|
|
||||||
overflow-x: auto; -webkit-overflow-scrolling: touch;
|
|
||||||
scrollbar-width: none;
|
|
||||||
}
|
|
||||||
.bottom-tabs::-webkit-scrollbar { display: none; }
|
|
||||||
.bottom-tabs a {
|
|
||||||
flex: 0 0 auto; min-width: 3.2rem; padding: 0 0.45rem;
|
|
||||||
display: flex; flex-direction: column; align-items: center;
|
|
||||||
justify-content: center; text-decoration: none; color: #999;
|
|
||||||
font-size: 0.6rem; font-weight: 600; gap: 0.1rem;
|
|
||||||
transition: color 0.15s; white-space: nowrap;
|
|
||||||
}
|
|
||||||
.bottom-tabs a .tab-icon { font-size: 1.25rem; line-height: 1; }
|
|
||||||
.bottom-tabs a .tab-label { display: block; }
|
|
||||||
.bottom-tabs a.is-active { color: var(--accent); }
|
|
||||||
@media (max-width: 400px) {
|
|
||||||
.bottom-tabs a .tab-label { display: none; }
|
|
||||||
.bottom-tabs a { min-width: 2.8rem; padding: 0 0.3rem; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Desktop: hide bottom tabs, show top nav ── */
|
|
||||||
.desktop-nav { display: none; }
|
|
||||||
@media (min-width: 769px) {
|
|
||||||
body { padding-bottom: 0; }
|
|
||||||
.bottom-tabs { display: none; }
|
|
||||||
.desktop-nav { display: flex; gap: 0.25rem; }
|
|
||||||
.desktop-nav a {
|
|
||||||
padding: 0.3rem 0.75rem; border-radius: 6px; font-size: 0.9rem;
|
|
||||||
color: #555; text-decoration: none; transition: background 0.15s;
|
|
||||||
}
|
|
||||||
.desktop-nav a:hover { background: #f0f0f0; }
|
|
||||||
.desktop-nav a.is-active { background: var(--accent); color: #fff; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Content ── */
|
|
||||||
.main-content { padding: 1rem; max-width: 900px; margin: 0 auto; }
|
|
||||||
|
|
||||||
/* ── Status badges ── */
|
|
||||||
.badge { display: inline-block; padding: 0.15rem 0.6rem; border-radius: 99px; font-size: 0.75rem; font-weight: 600; }
|
|
||||||
.badge-new { background: #dbeafe; color: #1e40af; }
|
|
||||||
.badge-in_progress { background: #fef3c7; color: #92400e; }
|
|
||||||
.badge-converted { background: #d1fae5; color: #065f46; }
|
|
||||||
.badge-rejected { background: #fee2e2; color: #991b1b; }
|
|
||||||
.badge-active { background: #d1fae5; color: #065f46; }
|
|
||||||
.badge-archived { background: #e5e7eb; color: #374151; }
|
|
||||||
.badge-visit-scheduled { background: #dbeafe; color: #1e40af; }
|
|
||||||
.badge-visit-completed { background: #d1fae5; color: #065f46; }
|
|
||||||
.badge-visit-cancelled { background: #e5e7eb; color: #374151; }
|
|
||||||
|
|
||||||
/* ── Item cards ── */
|
|
||||||
.item-card {
|
|
||||||
background: #fff; border-radius: 10px; padding: 0.85rem 1rem;
|
|
||||||
margin-bottom: 0.6rem; border: 1px solid #eee;
|
|
||||||
}
|
|
||||||
.item-card-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.3rem; }
|
|
||||||
.item-card-header .name { font-weight: 700; font-size: 1rem; }
|
|
||||||
.item-card-meta { color: #888; font-size: 0.82rem; line-height: 1.5; }
|
|
||||||
.item-card-meta span { margin-right: 1rem; }
|
|
||||||
.item-card-actions { display: flex; gap: 0.4rem; flex-wrap: wrap; margin-top: 0.5rem; }
|
|
||||||
.item-card-actions form { margin: 0; }
|
|
||||||
|
|
||||||
/* ── Small buttons ── */
|
|
||||||
.btn-sm { font-size: 0.78rem !important; padding: 0.25rem 0.6rem !important; height: auto !important; }
|
|
||||||
|
|
||||||
/* ── Page header ── */
|
|
||||||
.page-head { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 0.5rem; margin-bottom: 1rem; }
|
|
||||||
.page-head h1 { font-size: 1.3rem; font-weight: 700; margin: 0; }
|
|
||||||
|
|
||||||
/* ── Forms ── */
|
|
||||||
.form-card { background: #fff; border-radius: 10px; padding: 1.25rem; border: 1px solid #eee; }
|
|
||||||
input, textarea, select,
|
|
||||||
.input, .textarea, .select select {
|
|
||||||
background-color: #fff !important;
|
|
||||||
color: #333 !important;
|
|
||||||
border-color: #dbdbdb !important;
|
|
||||||
}
|
|
||||||
.label, label {
|
|
||||||
color: #363636 !important;
|
|
||||||
}
|
|
||||||
h1, h2, h3, h4, h5, h6, strong, b, p, span, div, a, td, th, li {
|
|
||||||
color: inherit;
|
|
||||||
}
|
|
||||||
.title, .subtitle, .content, .has-text-dark {
|
|
||||||
color: #363636 !important;
|
|
||||||
}
|
|
||||||
.has-text-grey { color: #7a7a7a !important; }
|
|
||||||
.notification { color: #333 !important; }
|
|
||||||
</style>
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body class="admin-shell">
|
||||||
|
|
||||||
<!-- Top header -->
|
<!-- Top header -->
|
||||||
<div class="top-header">
|
<div class="top-header">
|
||||||
<a class="brand" href="/admin/?lang={{ lang.code() }}">🐾 {{ t.nav_title }} <span style="font-size:0.7rem;font-weight:400;color:#aaa;">v{{ t.app_version() }}</span></a>
|
<a class="brand" href="/admin/?lang={{ lang.code() }}">🐾 {{ t.nav_title }} <span class="brand-version">v{{ t.app_version() }}</span></a>
|
||||||
<nav class="desktop-nav">
|
<nav class="desktop-nav">
|
||||||
<a href="/admin/?lang={{ lang.code() }}" {% if active_page == "dashboard" %}class="is-active"{% endif %}>{{ t.dashboard_title }}</a>
|
<a href="/admin/?lang={{ lang.code() }}" {% if active_page == "dashboard" %}class="is-active"{% endif %}>{{ t.dashboard_title }}</a>
|
||||||
<a href="/admin/leads?lang={{ lang.code() }}" {% if active_page == "leads" %}class="is-active"{% endif %}>{{ t.nav_leads }}</a>
|
<a href="/admin/leads?lang={{ lang.code() }}" {% if active_page == "leads" %}class="is-active"{% endif %}>{{ t.nav_leads }}</a>
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if leads.is_empty() %}
|
{% if leads.is_empty() %}
|
||||||
<p class="has-text-grey">{{ t.leads_empty }}</p>
|
<p class="empty-state">{{ t.leads_empty }}</p>
|
||||||
{% else %}
|
{% else %}
|
||||||
{% for lead in &leads %}
|
{% for lead in &leads %}
|
||||||
<div class="item-card">
|
<div class="item-card">
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="item-card-meta">
|
<div class="item-card-meta">
|
||||||
{% if let Some(phone) = lead.phone.as_deref() %}
|
{% if let Some(phone) = lead.phone.as_deref() %}
|
||||||
<span><a href="tel:{{ phone }}" style="color:inherit;text-decoration:none;">📞 {{ phone }}</a></span>
|
<span><a href="tel:{{ phone }}" class="card-link">📞 {{ phone }}</a></span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if let Some(comment) = lead.comment.as_deref() %}
|
{% if let Some(comment) = lead.comment.as_deref() %}
|
||||||
<span>💬 {{ comment }}</span>
|
<span>💬 {{ comment }}</span>
|
||||||
|
|||||||
+10
-20
@@ -6,31 +6,21 @@
|
|||||||
<title>{{ t.nav_title }} — {{ t.login_title }}</title>
|
<title>{{ t.nav_title }} — {{ t.login_title }}</title>
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@1/css/bulma.min.css">
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@1/css/bulma.min.css">
|
||||||
|
<link rel="stylesheet" href="/static/admin.css?v={{ t.app_version() }}">
|
||||||
{% if !turnstile_site_key.is_empty() %}
|
{% if !turnstile_site_key.is_empty() %}
|
||||||
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
|
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<style>
|
|
||||||
:root { color-scheme: light; }
|
|
||||||
body { background: #f5f5f5; display: flex; align-items: center; justify-content: center; min-height: 100vh; color: #333; }
|
|
||||||
.login-box { width: 100%; max-width: 380px; padding: 0 1rem; }
|
|
||||||
.login-card { background: #fff; border-radius: 12px; padding: 2rem 1.5rem; box-shadow: 0 2px 12px rgba(0,0,0,0.06); }
|
|
||||||
input, textarea, select, .input, .textarea, .select select {
|
|
||||||
background-color: #fff !important; color: #333 !important; border-color: #dbdbdb !important;
|
|
||||||
}
|
|
||||||
.label, label { color: #363636 !important; }
|
|
||||||
.notification { color: #333 !important; }
|
|
||||||
</style>
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body class="auth-shell">
|
||||||
<div class="login-box">
|
<div class="login-box">
|
||||||
<div class="has-text-right mb-3">
|
<div class="auth-language">
|
||||||
<a href="?lang={{ lang.other().code() }}" class="has-text-grey is-size-7">{{ lang.other().label() }}</a>
|
<a href="?lang={{ lang.other().code() }}">{{ lang.other().label() }}</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="login-card">
|
<div class="login-card">
|
||||||
<div class="has-text-centered mb-4">
|
<div class="auth-head">
|
||||||
<p class="is-size-3">🐾</p>
|
<span class="auth-icon">🐾</span>
|
||||||
<h1 class="is-size-4 has-text-weight-bold">{{ t.nav_title }}</h1>
|
<h1>{{ t.nav_title }}</h1>
|
||||||
<p class="has-text-grey">{{ t.login_title }}</p>
|
<p>{{ t.login_title }}</p>
|
||||||
</div>
|
</div>
|
||||||
{% if let Some(err) = error.as_ref() %}
|
{% if let Some(err) = error.as_ref() %}
|
||||||
<div class="notification is-danger is-light">{{ err }}</div>
|
<div class="notification is-danger is-light">{{ err }}</div>
|
||||||
@@ -39,7 +29,7 @@
|
|||||||
<a href="/admin/oidc/start" class="button is-primary is-fullwidth mt-3">{{ t.login_sso_button }}</a>
|
<a href="/admin/oidc/start" class="button is-primary is-fullwidth mt-3">{{ t.login_sso_button }}</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if auth_password_enabled %}
|
{% if auth_password_enabled %}
|
||||||
{% if auth_sso_enabled %}<hr style="margin:1rem 0;">{% endif %}
|
{% if auth_sso_enabled %}<hr class="auth-divider">{% endif %}
|
||||||
<form method="post" action="/admin/login/submit">
|
<form method="post" action="/admin/login/submit">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label">{{ t.users_login }}</label>
|
<label class="label">{{ t.users_login }}</label>
|
||||||
@@ -50,7 +40,7 @@
|
|||||||
<div class="control"><input class="input" type="password" name="password" required></div>
|
<div class="control"><input class="input" type="password" name="password" required></div>
|
||||||
</div>
|
</div>
|
||||||
{% if !turnstile_site_key.is_empty() %}
|
{% if !turnstile_site_key.is_empty() %}
|
||||||
<div class="cf-turnstile" data-sitekey="{{ turnstile_site_key }}" data-theme="light" style="margin-top:0.75rem;"></div>
|
<div class="cf-turnstile turnstile-wrap" data-sitekey="{{ turnstile_site_key }}" data-theme="light"></div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<button type="submit" class="button is-primary is-fullwidth mt-3">{{ t.login_button }}</button>
|
<button type="submit" class="button is-primary is-fullwidth mt-3">{{ t.login_button }}</button>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Client filter -->
|
<!-- Client filter -->
|
||||||
<div style="margin-bottom:1rem;">
|
<div class="filter-bar">
|
||||||
<div class="select is-small">
|
<div class="select is-small">
|
||||||
<select onchange="window.location.href='/admin/media?lang={{ lang.code() }}' + (this.value ? '&client_id=' + this.value : '')">
|
<select onchange="window.location.href='/admin/media?lang={{ lang.code() }}' + (this.value ? '&client_id=' + this.value : '')">
|
||||||
<option value="">{{ t.media_all_clients }}</option>
|
<option value="">{{ t.media_all_clients }}</option>
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if items.is_empty() %}
|
{% if items.is_empty() %}
|
||||||
<p class="has-text-grey">{{ t.media_empty }}</p>
|
<p class="empty-state">{{ t.media_empty }}</p>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="media-grid">
|
<div class="media-grid">
|
||||||
{% for item in &items %}
|
{% for item in &items %}
|
||||||
@@ -46,13 +46,13 @@
|
|||||||
<div class="media-meta">
|
<div class="media-meta">
|
||||||
<strong>{{ item.client_name }}</strong>
|
<strong>{{ item.client_name }}</strong>
|
||||||
{% if let Some(d) = item.visit_date.as_deref() %}
|
{% if let Some(d) = item.visit_date.as_deref() %}
|
||||||
<span style="color:#999;">{{ d }}</span>
|
<span class="feedback-date">{{ d }}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% if let Some(cap) = item.media.caption.as_deref() %}
|
{% if let Some(cap) = item.media.caption.as_deref() %}
|
||||||
<div style="font-size:0.82rem;color:#666;margin-top:0.2rem;">{{ cap }}</div>
|
<div class="media-caption">{{ cap }}</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<form method="post" action="/admin/media/{{ item.media.id.unwrap() }}/delete" onsubmit="return confirm('{{ t.media_delete_confirm }}');" style="margin-top:0.3rem;">
|
<form method="post" action="/admin/media/{{ item.media.id.unwrap() }}/delete" onsubmit="return confirm('{{ t.media_delete_confirm }}');" class="mt-2">
|
||||||
<button class="button is-small is-danger is-outlined btn-sm">{{ t.media_delete }}</button>
|
<button class="button is-small is-danger is-outlined btn-sm">{{ t.media_delete }}</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@@ -74,69 +74,4 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<style>
|
|
||||||
.media-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
|
||||||
gap: 0.75rem;
|
|
||||||
}
|
|
||||||
.media-card {
|
|
||||||
background: #fff;
|
|
||||||
border-radius: 10px;
|
|
||||||
border: 1px solid #eee;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
.media-card img {
|
|
||||||
width: 100%;
|
|
||||||
height: 160px;
|
|
||||||
object-fit: cover;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
.media-card .photo-thumb {
|
|
||||||
width: 100%;
|
|
||||||
height: 160px;
|
|
||||||
}
|
|
||||||
.media-card .video-thumb {
|
|
||||||
position: relative;
|
|
||||||
width: 100%;
|
|
||||||
height: 160px;
|
|
||||||
background: #111;
|
|
||||||
}
|
|
||||||
.media-card .video-play {
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
color: white;
|
|
||||||
font-size: 2.5rem;
|
|
||||||
line-height: 1;
|
|
||||||
text-shadow: 0 1px 5px #000;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
.media-info {
|
|
||||||
padding: 0.6rem 0.75rem;
|
|
||||||
}
|
|
||||||
.media-pagination {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 0.35rem;
|
|
||||||
margin-top: 1.25rem;
|
|
||||||
}
|
|
||||||
.media-meta {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
@media (max-width: 480px) {
|
|
||||||
.media-grid {
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
|
||||||
}
|
|
||||||
.media-card img, .media-card .photo-thumb, .media-card .video-thumb {
|
|
||||||
height: 120px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -5,14 +5,18 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="page-head">
|
<div class="page-head">
|
||||||
<h1>{{ t.media_upload_title }}</h1>
|
<h1>{{ t.media_title }}</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-card">
|
<section class="admin-section">
|
||||||
<div style="margin-bottom:1rem;font-size:0.9rem;color:#666;">
|
<header class="admin-section-head">
|
||||||
<div><strong>{{ t.schedule_client }}:</strong> {{ client_name }}</div>
|
<span class="admin-section-icon" aria-hidden="true">📤</span>
|
||||||
<div><strong>{{ t.schedule_date }}:</strong> {{ visit_label }}</div>
|
<div>
|
||||||
</div>
|
<h2>{{ t.media_upload_title }}</h2>
|
||||||
|
<p>{{ client_name }} · {{ visit_label }}</p>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<div class="admin-section-body">
|
||||||
|
|
||||||
<form id="uploadForm" action="/admin/media/{{ visit_id }}/upload/submit" enctype="multipart/form-data">
|
<form id="uploadForm" action="/admin/media/{{ visit_id }}/upload/submit" enctype="multipart/form-data">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
@@ -20,7 +24,7 @@
|
|||||||
<div class="control">
|
<div class="control">
|
||||||
<input class="input" type="file" id="uploadFiles" name="files" multiple accept="image/*,video/*" required>
|
<input class="input" type="file" id="uploadFiles" name="files" multiple accept="image/*,video/*" required>
|
||||||
</div>
|
</div>
|
||||||
<p id="fileCount" style="font-size:0.8rem;color:#888;margin-top:0.3rem;"></p>
|
<p id="fileCount" class="field-help"></p>
|
||||||
<div id="uploadQueue" class="upload-queue"></div>
|
<div id="uploadQueue" class="upload-queue"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -32,13 +36,13 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Progress -->
|
<!-- Progress -->
|
||||||
<div id="uploadProgress" style="display:none;margin-bottom:1rem;">
|
<div id="uploadProgress" class="upload-progress">
|
||||||
<div style="display:flex;justify-content:space-between;font-size:0.82rem;color:#555;margin-bottom:0.3rem;">
|
<div class="upload-progress-head">
|
||||||
<span id="uploadStatusText">{{ t.media_upload }}...</span>
|
<span id="uploadStatusText">{{ t.media_upload }}...</span>
|
||||||
<span id="uploadPercent">0%</span>
|
<span id="uploadPercent">0%</span>
|
||||||
</div>
|
</div>
|
||||||
<div style="background:#e8e8e8;border-radius:99px;height:8px;overflow:hidden;">
|
<div class="upload-progress-track">
|
||||||
<div id="uploadBar" style="height:100%;width:0%;background:linear-gradient(90deg,#6c63ff,#b06cff);border-radius:99px;transition:width 0.2s;"></div>
|
<div id="uploadBar" class="upload-progress-bar"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -159,18 +163,7 @@
|
|||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
</div>
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<style>
|
|
||||||
.upload-queue { display:flex; flex-direction:column; gap:.4rem; margin-top:.65rem; }
|
|
||||||
.upload-queue:empty { display:none; }
|
|
||||||
.upload-queue-item { display:grid; grid-template-columns:34px minmax(0,1fr); gap:.55rem; align-items:center; padding:.45rem .55rem; background:#f7f6ff; border-radius:8px; }
|
|
||||||
.upload-queue-icon { width:34px; height:34px; display:flex; align-items:center; justify-content:center; border-radius:7px; background:#ebe8ff; font-size:1.05rem; }
|
|
||||||
.upload-queue-name { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; font-size:.78rem; color:#494467; }
|
|
||||||
.upload-queue-state { font-size:.68rem; color:#8a84a5; }
|
|
||||||
.upload-queue-track { height:3px; overflow:hidden; border-radius:99px; background:#dedbea; margin-top:.25rem; }
|
|
||||||
.upload-queue-bar { height:100%; width:0; background:#7567e8; transition:width .2s; }
|
|
||||||
#uploadBar.is-processing, .upload-queue-bar.is-processing { width:35% !important; animation:upload-processing 1.15s ease-in-out infinite; }
|
|
||||||
@keyframes upload-processing { from { transform:translateX(-110%); } to { transform:translateX(300%); } }
|
|
||||||
</style>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
<a href="/admin/schedule/new?lang={{ lang.code() }}" class="button is-primary is-small">+ {{ t.schedule_new }}</a>
|
<a href="/admin/schedule/new?lang={{ lang.code() }}" class="button is-primary is-small">+ {{ t.schedule_new }}</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-card" id="calendar-wrap" style="padding:0.5rem;">
|
<div class="form-card" id="calendar-wrap">
|
||||||
<div id="calendar"></div>
|
<div id="calendar"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -18,40 +18,6 @@
|
|||||||
{% if lang == Lang::Ru %}
|
{% if lang == Lang::Ru %}
|
||||||
<script src="https://cdn.jsdelivr.net/npm/@fullcalendar/core@6.1.17/locales/ru.global.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/@fullcalendar/core@6.1.17/locales/ru.global.min.js"></script>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<style>
|
|
||||||
#calendar-wrap { overflow: hidden; }
|
|
||||||
.fc { font-size: 0.85rem; color: #333; }
|
|
||||||
.fc .fc-daygrid-day-number,
|
|
||||||
.fc .fc-col-header-cell-cushion,
|
|
||||||
.fc .fc-list-day-text,
|
|
||||||
.fc .fc-list-day-side-text { color: #333 !important; }
|
|
||||||
.fc th, .fc td { color: #333; }
|
|
||||||
.fc .fc-toolbar-title { color: #333 !important; }
|
|
||||||
.fc .fc-button { color: #fff !important; }
|
|
||||||
.fc .fc-toolbar { flex-wrap: wrap; gap: 0.3rem; }
|
|
||||||
.fc .fc-toolbar-title { font-size: 1.1rem !important; }
|
|
||||||
.fc .fc-button { padding: 0.25rem 0.5rem !important; font-size: 0.8rem !important; }
|
|
||||||
.fc-event { cursor: pointer; border: none !important; padding: 2px 5px; border-radius: 4px; }
|
|
||||||
.fc-event.ev-completed .fc-event-title,
|
|
||||||
.fc-event.ev-completed .fc-list-event-title { text-decoration: line-through; opacity: 0.75; }
|
|
||||||
.fc-event.ev-cancelled .fc-event-title,
|
|
||||||
.fc-event.ev-cancelled .fc-list-event-title { text-decoration: line-through; }
|
|
||||||
.fc .fc-day-today { background: #eef2ff !important; }
|
|
||||||
.fc .fc-day.day-weekend { background: #faf5f0; }
|
|
||||||
.fc .fc-day-today.day-weekend { background: #eef2ff !important; }
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.fc .fc-toolbar { font-size: 0.75rem; }
|
|
||||||
.fc .fc-toolbar-title { font-size: 0.95rem !important; }
|
|
||||||
.fc .fc-button { padding: 0.2rem 0.35rem !important; font-size: 0.72rem !important; }
|
|
||||||
}
|
|
||||||
.visit-modal-bg { display:none; position:fixed; inset:0; background:rgba(0,0,0,0.35); z-index:100; align-items:center; justify-content:center; }
|
|
||||||
.visit-modal-bg.is-open { display:flex; }
|
|
||||||
.visit-modal { background:#fff; border-radius:12px; padding:1.5rem; width:90%; max-width:380px; box-shadow:0 4px 24px rgba(0,0,0,0.15); }
|
|
||||||
.visit-modal h3 { margin:0 0 0.75rem; font-size:1.1rem; }
|
|
||||||
.visit-modal .meta { color:#888; font-size:0.85rem; margin-bottom:0.75rem; line-height:1.6; }
|
|
||||||
.color-dot { display:inline-block; width:12px; height:12px; border-radius:50%; margin-right:6px; vertical-align:middle; }
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<div class="visit-modal-bg" id="visitModal">
|
<div class="visit-modal-bg" id="visitModal">
|
||||||
<div class="visit-modal">
|
<div class="visit-modal">
|
||||||
<h3><span class="color-dot" id="vmDot"></span><span id="vmTitle"></span></h3>
|
<h3><span class="color-dot" id="vmDot"></span><span id="vmTitle"></span></h3>
|
||||||
@@ -60,10 +26,10 @@
|
|||||||
<div id="vmAddress"></div>
|
<div id="vmAddress"></div>
|
||||||
<div id="vmAdmin"></div>
|
<div id="vmAdmin"></div>
|
||||||
<div id="vmTime"></div>
|
<div id="vmTime"></div>
|
||||||
<div id="vmNotes" style="margin-top:0.3rem;"></div>
|
<div id="vmNotes" class="mt-2"></div>
|
||||||
</div>
|
</div>
|
||||||
<div id="vmStatus" style="margin-bottom:1rem;"></div>
|
<div id="vmStatus" class="mb-4"></div>
|
||||||
<a id="vmEditLink" href="#" class="button is-primary is-fullwidth" style="font-size:1rem;font-weight:700;padding:0.65rem;">📋 {{ t.schedule_edit_title }}</a>
|
<a id="vmEditLink" href="#" class="button is-primary is-fullwidth">📋 {{ t.schedule_edit_title }}</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -82,7 +48,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
timeZone: '{{ timezone }}',
|
timeZone: '{{ timezone }}',
|
||||||
initialView: window.innerWidth < 768 ? 'listWeek' : 'dayGridMonth',
|
initialView: window.innerWidth < 768 ? 'listWeek' : 'dayGridMonth',
|
||||||
headerToolbar: {
|
headerToolbar: {
|
||||||
left: 'prev,next today',
|
left: 'prev,next,today',
|
||||||
center: 'title',
|
center: 'title',
|
||||||
right: 'dayGridMonth,timeGridWeek,timeGridDay,listWeek'
|
right: 'dayGridMonth,timeGridWeek,timeGridDay,listWeek'
|
||||||
},
|
},
|
||||||
@@ -98,7 +64,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
const p = ev.extendedProps;
|
const p = ev.extendedProps;
|
||||||
document.getElementById('vmDot').style.background = p.client_color || '#7c6ed4';
|
document.getElementById('vmDot').style.background = p.client_color || '#7c6ed4';
|
||||||
document.getElementById('vmTitle').textContent = p.client_name;
|
document.getElementById('vmTitle').textContent = p.client_name;
|
||||||
document.getElementById('vmClient').innerHTML = p.client_phone ? ('<a href="tel:' + p.client_phone + '" style="color:inherit;text-decoration:none;">📞 ' + p.client_phone + '</a>') : '';
|
document.getElementById('vmClient').innerHTML = p.client_phone ? ('<a href="tel:' + p.client_phone + '" class="card-link">📞 ' + p.client_phone + '</a>') : '';
|
||||||
document.getElementById('vmAddress').textContent = p.client_address ? ('📍 ' + p.client_address) : '';
|
document.getElementById('vmAddress').textContent = p.client_address ? ('📍 ' + p.client_address) : '';
|
||||||
document.getElementById('vmAdmin').textContent = '👤 ' + p.admin_name;
|
document.getElementById('vmAdmin').textContent = '👤 ' + p.admin_name;
|
||||||
document.getElementById('vmTime').textContent = '🕐 ' + p.time_start + ' — ' + p.time_end;
|
document.getElementById('vmTime').textContent = '🕐 ' + p.time_start + ' — ' + p.time_end;
|
||||||
|
|||||||
@@ -5,10 +5,18 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="page-head">
|
<div class="page-head">
|
||||||
<h1>{{ t.schedule_edit_title }}</h1>
|
<h1>{{ t.schedule_title }}</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-card">
|
<section class="admin-section">
|
||||||
|
<header class="admin-section-head">
|
||||||
|
<span class="admin-section-icon" aria-hidden="true">📋</span>
|
||||||
|
<div>
|
||||||
|
<h2>{{ t.schedule_edit_title }}</h2>
|
||||||
|
<p>{{ client.name }}</p>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<div class="admin-section-body">
|
||||||
<form method="post" action="/admin/schedule/{{ visit.id }}/save">
|
<form method="post" action="/admin/schedule/{{ visit.id }}/save">
|
||||||
<!-- Client -->
|
<!-- Client -->
|
||||||
<div class="field">
|
<div class="field">
|
||||||
@@ -45,7 +53,7 @@
|
|||||||
<!-- Time -->
|
<!-- Time -->
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label">{{ t.schedule_default_time }}</label>
|
<label class="label">{{ t.schedule_default_time }}</label>
|
||||||
<div class="columns is-mobile" style="margin-bottom:0;">
|
<div class="columns is-mobile time-columns">
|
||||||
<div class="column">
|
<div class="column">
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<input class="input" type="time" name="time_start" value="{{ visit.time_start }}" required>
|
<input class="input" type="time" name="time_start" value="{{ visit.time_start }}" required>
|
||||||
@@ -93,21 +101,21 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if let Some(fb) = visit.client_feedback.as_deref() %}
|
{% if let Some(fb) = visit.client_feedback.as_deref() %}
|
||||||
<div style="margin-bottom:1rem;">
|
<div class="mb-4">
|
||||||
<label class="label">{{ t.schedule_client_feedback }}</label>
|
<label class="label">{{ t.schedule_client_feedback }}</label>
|
||||||
<div style="background:#f0f0ff;border-radius:8px;padding:0.6rem 0.85rem;font-size:0.9rem;color:#4a4570;">{{ fb }}</div>
|
<div class="admin-note admin-note-info">{{ fb }}</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<hr style="margin:1rem 0;">
|
<hr>
|
||||||
|
|
||||||
<!-- Media -->
|
<!-- Media -->
|
||||||
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:0.75rem;">
|
<div class="section-inline-head">
|
||||||
<label class="label" style="margin:0;">{{ t.nav_media }}</label>
|
<label class="label">{{ t.nav_media }}</label>
|
||||||
<button type="button" class="button is-info is-small is-outlined" onclick="document.getElementById('uploadModal').classList.add('is-open')">+ {{ t.media_upload }}</button>
|
<button type="button" class="button is-info is-small is-outlined" onclick="document.getElementById('uploadModal').classList.add('is-open')">+ {{ t.media_upload }}</button>
|
||||||
</div>
|
</div>
|
||||||
{% if media.is_empty() %}
|
{% if media.is_empty() %}
|
||||||
<p class="has-text-grey is-size-7" style="margin-bottom:1rem;">{{ t.media_empty }}</p>
|
<p class="empty-state mb-4">{{ t.media_empty }}</p>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="visit-media-grid">
|
<div class="visit-media-grid">
|
||||||
{% for m in &media %}
|
{% for m in &media %}
|
||||||
@@ -139,7 +147,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<hr style="margin:1rem 0;">
|
<hr>
|
||||||
|
|
||||||
<button type="submit" class="button is-primary is-fullwidth">{{ t.schedule_save }}</button>
|
<button type="submit" class="button is-primary is-fullwidth">{{ t.schedule_save }}</button>
|
||||||
</form>
|
</form>
|
||||||
@@ -147,18 +155,19 @@
|
|||||||
<form id="visit-media-delete-{{ m.id.unwrap() }}" method="post" action="/admin/media/{{ m.id.unwrap() }}/delete" onsubmit="return confirm('{{ t.media_delete_confirm }}');"></form>
|
<form id="visit-media-delete-{{ m.id.unwrap() }}" method="post" action="/admin/media/{{ m.id.unwrap() }}/delete" onsubmit="return confirm('{{ t.media_delete_confirm }}');"></form>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
<hr style="margin:1rem 0;">
|
<hr>
|
||||||
<form method="post" action="/admin/schedule/{{ visit.id }}/delete" onsubmit="return confirm('{{ t.schedule_delete_confirm }}');">
|
<form method="post" action="/admin/schedule/{{ visit.id }}/delete" onsubmit="return confirm('{{ t.schedule_delete_confirm }}');">
|
||||||
<button type="submit" class="button is-danger is-outlined is-fullwidth is-small">{{ t.schedule_delete }}</button>
|
<button type="submit" class="button is-danger is-outlined is-fullwidth is-small">{{ t.schedule_delete }}</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<!-- Upload Modal -->
|
<!-- Upload Modal -->
|
||||||
<div class="upload-modal-bg" id="uploadModal">
|
<div class="upload-modal-bg" id="uploadModal">
|
||||||
<div class="upload-modal">
|
<div class="upload-modal">
|
||||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:1rem;">
|
<div class="modal-head">
|
||||||
<h3 style="font-size:1.1rem;font-weight:700;margin:0;">{{ t.media_upload_title }}</h3>
|
<h3>{{ t.media_upload_title }}</h3>
|
||||||
<button type="button" id="uploadModalClose" style="background:none;border:none;font-size:1.2rem;cursor:pointer;color:#888;">✕</button>
|
<button type="button" id="uploadModalClose" class="modal-close">✕</button>
|
||||||
</div>
|
</div>
|
||||||
<form id="uploadForm" action="/admin/media/{{ visit.id }}/upload/submit" enctype="multipart/form-data">
|
<form id="uploadForm" action="/admin/media/{{ visit.id }}/upload/submit" enctype="multipart/form-data">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
@@ -166,7 +175,7 @@
|
|||||||
<div class="control">
|
<div class="control">
|
||||||
<input class="input" type="file" id="uploadFiles" name="files" multiple accept="image/*,video/*" required>
|
<input class="input" type="file" id="uploadFiles" name="files" multiple accept="image/*,video/*" required>
|
||||||
</div>
|
</div>
|
||||||
<p id="fileCount" style="font-size:0.8rem;color:#888;margin-top:0.3rem;"></p>
|
<p id="fileCount" class="field-help"></p>
|
||||||
<div id="uploadQueue" class="upload-queue"></div>
|
<div id="uploadQueue" class="upload-queue"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
@@ -177,13 +186,13 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Progress -->
|
<!-- Progress -->
|
||||||
<div id="uploadProgress" style="display:none;margin-bottom:1rem;">
|
<div id="uploadProgress" class="upload-progress">
|
||||||
<div style="display:flex;justify-content:space-between;font-size:0.82rem;color:#555;margin-bottom:0.3rem;">
|
<div class="upload-progress-head">
|
||||||
<span id="uploadStatusText">{{ t.media_upload }}...</span>
|
<span id="uploadStatusText">{{ t.media_upload }}...</span>
|
||||||
<span id="uploadPercent">0%</span>
|
<span id="uploadPercent">0%</span>
|
||||||
</div>
|
</div>
|
||||||
<div style="background:#e8e8e8;border-radius:99px;height:8px;overflow:hidden;">
|
<div class="upload-progress-track">
|
||||||
<div id="uploadBar" style="height:100%;width:0%;background:linear-gradient(90deg,#6c63ff,#b06cff);border-radius:99px;transition:width 0.2s;"></div>
|
<div id="uploadBar" class="upload-progress-bar"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -192,126 +201,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
|
||||||
.status-picker {
|
|
||||||
display: flex;
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
.status-btn {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.3rem;
|
|
||||||
padding: 0.6rem 0.4rem;
|
|
||||||
border-radius: 10px;
|
|
||||||
border: 2px solid transparent;
|
|
||||||
background: #f5f5f5;
|
|
||||||
font-size: 0.82rem;
|
|
||||||
font-weight: 600;
|
|
||||||
cursor: pointer;
|
|
||||||
color: #777;
|
|
||||||
transition: all 0.15s;
|
|
||||||
line-height: 1.2;
|
|
||||||
}
|
|
||||||
.status-btn-icon { font-size: 1.3rem; line-height: 1; }
|
|
||||||
.status-btn:hover { filter: brightness(0.95); }
|
|
||||||
|
|
||||||
.status-btn-scheduled.is-active { background: #dbeafe; border-color: #3b82f6; color: #1e40af; }
|
|
||||||
.status-btn-completed.is-active { background: #d1fae5; border-color: #22c55e; color: #15803d; }
|
|
||||||
.status-btn-cancelled.is-active { background: #fee2e2; border-color: #ef4444; color: #b91c1c; }
|
|
||||||
|
|
||||||
.status-btn-scheduled:not(.is-active):hover { background: #eff6ff; color: #3b82f6; }
|
|
||||||
.status-btn-completed:not(.is-active):hover { background: #f0fdf4; color: #22c55e; }
|
|
||||||
.status-btn-cancelled:not(.is-active):hover { background: #fff5f5; color: #ef4444; }
|
|
||||||
|
|
||||||
.visit-media-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
|
|
||||||
gap: 0.5rem;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
.visit-media-item {
|
|
||||||
background: #fafafa;
|
|
||||||
border-radius: 8px;
|
|
||||||
border: 1px solid #eee;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
.visit-media-item img {
|
|
||||||
width: 100%;
|
|
||||||
height: 80px;
|
|
||||||
object-fit: cover;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
.visit-media-item .photo-thumb {
|
|
||||||
width: 100%;
|
|
||||||
height: 80px;
|
|
||||||
}
|
|
||||||
.visit-media-item .video-thumb-sm {
|
|
||||||
position: relative;
|
|
||||||
width: 100%;
|
|
||||||
height: 80px;
|
|
||||||
background: #111;
|
|
||||||
}
|
|
||||||
.visit-media-item .video-play {
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
color: white;
|
|
||||||
font-size: 1.6rem;
|
|
||||||
line-height: 1;
|
|
||||||
text-shadow: 0 1px 4px #000;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
.visit-media-item .media-cap {
|
|
||||||
font-size: 0.7rem;
|
|
||||||
color: #888;
|
|
||||||
padding: 0.2rem 0.4rem;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
.visit-media-delete {
|
|
||||||
padding: 0.25rem 0.4rem 0.4rem;
|
|
||||||
}
|
|
||||||
.visit-media-delete .button {
|
|
||||||
width: 100%;
|
|
||||||
font-size: 0.68rem;
|
|
||||||
min-height: 1.65rem;
|
|
||||||
}
|
|
||||||
.upload-modal-bg {
|
|
||||||
display: none;
|
|
||||||
position: fixed;
|
|
||||||
inset: 0;
|
|
||||||
background: rgba(0,0,0,0.35);
|
|
||||||
z-index: 100;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
.upload-modal-bg.is-open {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
.upload-modal {
|
|
||||||
background: #fff;
|
|
||||||
border-radius: 12px;
|
|
||||||
padding: 1.5rem;
|
|
||||||
width: 90%;
|
|
||||||
max-width: 420px;
|
|
||||||
box-shadow: 0 4px 24px rgba(0,0,0,0.15);
|
|
||||||
}
|
|
||||||
.upload-queue { display:flex; flex-direction:column; gap:.4rem; margin-top:.65rem; }
|
|
||||||
.upload-queue:empty { display:none; }
|
|
||||||
.upload-queue-item { display:grid; grid-template-columns:34px minmax(0,1fr); gap:.55rem; align-items:center; padding:.45rem .55rem; background:#f7f6ff; border-radius:8px; }
|
|
||||||
.upload-queue-icon { width:34px; height:34px; display:flex; align-items:center; justify-content:center; border-radius:7px; background:#ebe8ff; font-size:1.05rem; }
|
|
||||||
.upload-queue-name { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; font-size:.78rem; color:#494467; }
|
|
||||||
.upload-queue-state { font-size:.68rem; color:#8a84a5; }
|
|
||||||
.upload-queue-track { height:3px; overflow:hidden; border-radius:99px; background:#dedbea; margin-top:.25rem; }
|
|
||||||
.upload-queue-bar { height:100%; width:0; background:#7567e8; transition:width .2s; }
|
|
||||||
#uploadBar.is-processing, .upload-queue-bar.is-processing { width:35% !important; animation:upload-processing 1.15s ease-in-out infinite; }
|
|
||||||
@keyframes upload-processing { from { transform:translateX(-110%); } to { transform:translateX(300%); } }
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Status picker
|
// Status picker
|
||||||
document.querySelectorAll('.status-btn').forEach(function(btn) {
|
document.querySelectorAll('.status-btn').forEach(function(btn) {
|
||||||
|
|||||||
@@ -5,10 +5,15 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="page-head">
|
<div class="page-head">
|
||||||
<h1>{{ t.schedule_new_title }}</h1>
|
<h1>{{ t.schedule_title }}</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-card">
|
<section class="admin-section">
|
||||||
|
<header class="admin-section-head">
|
||||||
|
<span class="admin-section-icon" aria-hidden="true">📅</span>
|
||||||
|
<div><h2>{{ t.schedule_new_title }}</h2></div>
|
||||||
|
</header>
|
||||||
|
<div class="admin-section-body">
|
||||||
<form method="post" action="/admin/schedule/create" id="visitForm">
|
<form method="post" action="/admin/schedule/create" id="visitForm">
|
||||||
|
|
||||||
<!-- Client -->
|
<!-- Client -->
|
||||||
@@ -94,85 +99,7 @@
|
|||||||
|
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
</section>
|
||||||
<style>
|
|
||||||
/* Time row */
|
|
||||||
.time-row { display:flex; align-items:center; gap:0.5rem; }
|
|
||||||
.time-block { display:flex; flex-direction:column; flex:1; }
|
|
||||||
.time-lbl { font-size:0.75rem; color:#888; margin-bottom:0.2rem; }
|
|
||||||
.time-sep { font-size:1.2rem; color:#aaa; padding-top:1.2rem; }
|
|
||||||
|
|
||||||
/* Calendar toolbar */
|
|
||||||
.cal-toolbar { display:flex; align-items:center; justify-content:space-between; margin-bottom:0.5rem; }
|
|
||||||
|
|
||||||
|
|
||||||
/* Calendar container */
|
|
||||||
.sched-cal { background:#fafafa; border:1px solid #eee; border-radius:10px; overflow:hidden; }
|
|
||||||
|
|
||||||
.cal-nav { display:flex; align-items:center; justify-content:space-between; padding:0.6rem 0.75rem; background:#fff; border-bottom:1px solid #eee; }
|
|
||||||
.cal-nav button { background:none; border:none; font-size:1.1rem; cursor:pointer; color:#6c63ff; padding:0.2rem 0.5rem; border-radius:4px; }
|
|
||||||
.cal-nav button:hover { background:#f0eeff; }
|
|
||||||
.cal-nav span { font-weight:700; font-size:1rem; color:#333; }
|
|
||||||
|
|
||||||
.cal-grid { display:grid; grid-template-columns:repeat(7,1fr); }
|
|
||||||
|
|
||||||
.cal-wday { text-align:center; font-size:0.72rem; font-weight:700; color:#aaa; padding:0.4rem 0; background:#fafafa; }
|
|
||||||
.cal-wday.is-weekend { color:#f0a0a0; }
|
|
||||||
|
|
||||||
.cal-day { text-align:center; padding:0.55rem 0.2rem; font-size:0.9rem; cursor:pointer; color:#333; border-radius:0; transition:background 0.1s; position:relative; user-select:none; -webkit-user-select:none; }
|
|
||||||
.cal-day:hover { background:#f0eeff; }
|
|
||||||
.cal-day.is-empty { cursor:default; }
|
|
||||||
.cal-day.is-empty:hover { background:none; }
|
|
||||||
.cal-day.is-today { font-weight:700; color:#6c63ff; }
|
|
||||||
.cal-day.is-selected { background:#6c63ff !important; color:#fff !important; border-radius:0; }
|
|
||||||
.cal-day.is-range-start { background:#a89cff !important; color:#fff !important; }
|
|
||||||
.cal-day.is-past { color:#ccc; }
|
|
||||||
|
|
||||||
/* Selected days list */
|
|
||||||
.day-row { display:flex; align-items:center; gap:0.4rem; padding:0.45rem 0; border-bottom:1px solid #f5f5f5; }
|
|
||||||
.day-row:last-child { border-bottom:none; }
|
|
||||||
.day-date { font-weight:600; font-size:0.85rem; flex:1; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
||||||
.day-times { display:flex; align-items:center; gap:0.25rem; flex-shrink:0; }
|
|
||||||
.day-times .time-sep { color:#bbb; font-size:0.8rem; }
|
|
||||||
.day-rm { background:none; border:none; color:#ccc; cursor:pointer; font-size:1rem; padding:0.15rem 0.25rem; flex-shrink:0; line-height:1; }
|
|
||||||
.day-rm:hover { color:#e55; }
|
|
||||||
|
|
||||||
/* Time badge */
|
|
||||||
.time-badge-wrap {
|
|
||||||
position: relative;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
background: #ede9ff;
|
|
||||||
color: #5b52d6;
|
|
||||||
border: 1.5px solid #c4beff;
|
|
||||||
border-radius: 20px;
|
|
||||||
padding: 0.15rem 0.5rem;
|
|
||||||
font-size: 0.78rem;
|
|
||||||
font-weight: 700;
|
|
||||||
cursor: pointer;
|
|
||||||
min-width: 3.6rem;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
.time-badge-wrap:focus-within {
|
|
||||||
border-color: #6c63ff;
|
|
||||||
background: #f0eeff;
|
|
||||||
}
|
|
||||||
.time-badge-label { pointer-events: none; z-index: 1; }
|
|
||||||
.time-badge-input {
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
opacity: 0;
|
|
||||||
cursor: pointer;
|
|
||||||
border: none;
|
|
||||||
background: transparent;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
font-size: 16px; /* prevent iOS zoom */
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
(function() {
|
(function() {
|
||||||
|
|||||||
+80
-146
@@ -4,7 +4,7 @@
|
|||||||
{% block title %}{{ t.settings_title }}{% endblock %}
|
{% block title %}{{ t.settings_title }}{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="page-head settings-page-head">
|
<div class="page-head">
|
||||||
<div>
|
<div>
|
||||||
<h1>{{ t.settings_title }}</h1>
|
<h1>{{ t.settings_title }}</h1>
|
||||||
<p>{{ t.settings_intro }}</p>
|
<p>{{ t.settings_intro }}</p>
|
||||||
@@ -12,48 +12,48 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if saved %}
|
{% if saved %}
|
||||||
<div class="notification is-success is-light settings-message">{{ t.settings_saved }}</div>
|
<div class="notification is-success is-light admin-message">{{ t.settings_saved }}</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if let Some(message) = error %}
|
{% if let Some(message) = error %}
|
||||||
<div class="notification is-danger is-light settings-message">{{ message }}</div>
|
<div class="notification is-danger is-light admin-message">{{ message }}</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<form id="settingsForm" class="settings-form" method="post" action="/admin/settings/save">
|
<form id="settingsForm" class="admin-form" method="post" action="/admin/settings/save">
|
||||||
<section class="settings-section">
|
<section class="admin-section">
|
||||||
<header class="settings-section-head">
|
<header class="admin-section-head">
|
||||||
<span class="settings-section-icon" aria-hidden="true">🌐</span>
|
<span class="admin-section-icon" aria-hidden="true">🌐</span>
|
||||||
<div>
|
<div>
|
||||||
<h2>{{ t.settings_section_general }}</h2>
|
<h2>{{ t.settings_section_general }}</h2>
|
||||||
<p>{{ t.settings_section_general_help }}</p>
|
<p>{{ t.settings_section_general_help }}</p>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<div class="settings-section-body">
|
<div class="admin-section-body">
|
||||||
<div class="settings-grid">
|
<div class="admin-grid">
|
||||||
<div class="field settings-field settings-field-wide">
|
<div class="field admin-field admin-field-wide">
|
||||||
<label class="label" for="contactInfo">{{ t.settings_contact_info }}</label>
|
<label class="label" for="contactInfo">{{ t.settings_contact_info }}</label>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<input id="contactInfo" class="input" type="text" name="contact_info" placeholder="+7 999 123-45-67 / info@example.com" value="{% for s in &settings %}{% if s.key == "contact_info" %}{{ s.value }}{% endif %}{% endfor %}">
|
<input id="contactInfo" class="input" type="text" name="contact_info" placeholder="+7 999 123-45-67 / info@example.com" value="{% for s in &settings %}{% if s.key == "contact_info" %}{{ s.value }}{% endif %}{% endfor %}">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="field settings-field">
|
<div class="field admin-field">
|
||||||
<label class="label" for="siteDomain">{{ t.settings_site_domain }}</label>
|
<label class="label" for="siteDomain">{{ t.settings_site_domain }}</label>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<input id="siteDomain" class="input" type="url" name="site_domain" placeholder="https://example.com" value="{% for s in &settings %}{% if s.key == "site_domain" %}{{ s.value }}{% endif %}{% endfor %}">
|
<input id="siteDomain" class="input" type="url" name="site_domain" placeholder="https://example.com" value="{% for s in &settings %}{% if s.key == "site_domain" %}{{ s.value }}{% endif %}{% endfor %}">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="field settings-field">
|
<div class="field admin-field">
|
||||||
<label class="label" for="timezone">{{ t.settings_timezone }}</label>
|
<label class="label" for="timezone">{{ t.settings_timezone }}</label>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<input id="timezone" class="input" type="text" name="timezone" placeholder="Asia/Vladivostok" value="{% for s in &settings %}{% if s.key == "timezone" %}{{ s.value }}{% endif %}{% endfor %}">
|
<input id="timezone" class="input" type="text" name="timezone" placeholder="Asia/Vladivostok" value="{% for s in &settings %}{% if s.key == "timezone" %}{{ s.value }}{% endif %}{% endfor %}">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="field settings-field settings-field-wide">
|
<div class="field admin-field admin-field-wide">
|
||||||
<label class="label" for="pricingInfo">{{ t.settings_pricing_info }}</label>
|
<label class="label" for="pricingInfo">{{ t.settings_pricing_info }}</label>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<textarea id="pricingInfo" class="textarea" name="pricing_info" rows="3" placeholder="от 600 рублей за визит">{% for s in &settings %}{% if s.key == "pricing_info" %}{{ s.value }}{% endif %}{% endfor %}</textarea>
|
<textarea id="pricingInfo" class="textarea" name="pricing_info" rows="3" placeholder="от 600 рублей за визит">{% for s in &settings %}{% if s.key == "pricing_info" %}{{ s.value }}{% endif %}{% endfor %}</textarea>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="field settings-field settings-field-wide">
|
<div class="field admin-field admin-field-wide">
|
||||||
<label class="label" for="seoKeywordsInput">{{ t.settings_seo_keywords }}</label>
|
<label class="label" for="seoKeywordsInput">{{ t.settings_seo_keywords }}</label>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<textarea id="seoKeywordsInput" class="textarea" name="seo_keywords" rows="3" placeholder="зооняня Хабаровск, присмотр за питомцем Хабаровск, догситтер Хабаровск">{% for s in &settings %}{% if s.key == "seo_keywords" %}{{ s.value }}{% endif %}{% endfor %}</textarea>
|
<textarea id="seoKeywordsInput" class="textarea" name="seo_keywords" rows="3" placeholder="зооняня Хабаровск, присмотр за питомцем Хабаровск, догситтер Хабаровск">{% for s in &settings %}{% if s.key == "seo_keywords" %}{{ s.value }}{% endif %}{% endfor %}</textarea>
|
||||||
@@ -64,111 +64,111 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="settings-section">
|
<section class="admin-section">
|
||||||
<header class="settings-section-head">
|
<header class="admin-section-head">
|
||||||
<span class="settings-section-icon" aria-hidden="true">☁️</span>
|
<span class="admin-section-icon" aria-hidden="true">☁️</span>
|
||||||
<div>
|
<div>
|
||||||
<div class="settings-title-row">
|
<div class="admin-title-row">
|
||||||
<h2>{{ t.settings_section_storage }}</h2>
|
<h2>{{ t.settings_section_storage }}</h2>
|
||||||
<span class="settings-status{% if r2_enabled_checked %} is-enabled{% endif %}">
|
<span class="status-pill{% if r2_enabled_checked %} is-enabled{% endif %}">
|
||||||
{% if r2_enabled_checked %}{{ t.settings_status_enabled }}{% else %}{{ t.settings_status_disabled }}{% endif %}
|
{% if r2_enabled_checked %}{{ t.settings_status_enabled }}{% else %}{{ t.settings_status_disabled }}{% endif %}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<p>{{ t.settings_r2_help }}</p>
|
<p>{{ t.settings_r2_help }}</p>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<div class="settings-section-body">
|
<div class="admin-section-body">
|
||||||
<label class="settings-toggle" for="r2Enabled">
|
<label class="admin-toggle" for="r2Enabled">
|
||||||
<input id="r2Enabled" type="checkbox" name="r2_enabled" value="true"{% if r2_enabled_checked %} checked{% endif %}>
|
<input id="r2Enabled" type="checkbox" name="r2_enabled" value="true"{% if r2_enabled_checked %} checked{% endif %}>
|
||||||
<span class="toggle-track" aria-hidden="true"><span></span></span>
|
<span class="admin-toggle-track" aria-hidden="true"><span></span></span>
|
||||||
<span>
|
<span>
|
||||||
<strong>{{ t.settings_r2_enabled }}</strong>
|
<strong>{{ t.settings_r2_enabled }}</strong>
|
||||||
<small>{{ t.settings_r2_help }}</small>
|
<small>{{ t.settings_r2_help }}</small>
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<div class="settings-note settings-note-info">{{ t.settings_r2_migration_help }}</div>
|
<div class="admin-note admin-note-info">{{ t.settings_r2_migration_help }}</div>
|
||||||
|
|
||||||
<div class="settings-grid">
|
<div class="admin-grid">
|
||||||
<div class="field settings-field">
|
<div class="field admin-field">
|
||||||
<label class="label" for="r2AccountId">{{ t.settings_r2_account_id }}</label>
|
<label class="label" for="r2AccountId">{{ t.settings_r2_account_id }}</label>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<input id="r2AccountId" class="input is-family-monospace" type="text" name="r2_account_id" maxlength="32" autocomplete="off" spellcheck="false" placeholder="0123456789abcdef0123456789abcdef" value="{% for s in &settings %}{% if s.key == "r2_account_id" %}{{ s.value }}{% endif %}{% endfor %}">
|
<input id="r2AccountId" class="input is-family-monospace" type="text" name="r2_account_id" maxlength="32" autocomplete="off" spellcheck="false" placeholder="0123456789abcdef0123456789abcdef" value="{% for s in &settings %}{% if s.key == "r2_account_id" %}{{ s.value }}{% endif %}{% endfor %}">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="field settings-field">
|
<div class="field admin-field">
|
||||||
<label class="label" for="r2Bucket">{{ t.settings_r2_bucket }}</label>
|
<label class="label" for="r2Bucket">{{ t.settings_r2_bucket }}</label>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<input id="r2Bucket" class="input is-family-monospace" type="text" name="r2_bucket" autocomplete="off" spellcheck="false" placeholder="pet-media" value="{% for s in &settings %}{% if s.key == "r2_bucket" %}{{ s.value }}{% endif %}{% endfor %}">
|
<input id="r2Bucket" class="input is-family-monospace" type="text" name="r2_bucket" autocomplete="off" spellcheck="false" placeholder="pet-media" value="{% for s in &settings %}{% if s.key == "r2_bucket" %}{{ s.value }}{% endif %}{% endfor %}">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="field settings-field">
|
<div class="field admin-field">
|
||||||
<label class="label" for="r2AccessKey">{{ t.settings_r2_access_key_id }}</label>
|
<label class="label" for="r2AccessKey">{{ t.settings_r2_access_key_id }}</label>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<input id="r2AccessKey" class="input is-family-monospace" type="text" name="r2_access_key_id" autocomplete="off" spellcheck="false" value="{% for s in &settings %}{% if s.key == "r2_access_key_id" %}{{ s.value }}{% endif %}{% endfor %}">
|
<input id="r2AccessKey" class="input is-family-monospace" type="text" name="r2_access_key_id" autocomplete="off" spellcheck="false" value="{% for s in &settings %}{% if s.key == "r2_access_key_id" %}{{ s.value }}{% endif %}{% endfor %}">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="field settings-field">
|
<div class="field admin-field">
|
||||||
<label class="label" for="r2Secret">{{ t.settings_r2_secret_access_key }}</label>
|
<label class="label" for="r2Secret">{{ t.settings_r2_secret_access_key }}</label>
|
||||||
<div class="control secret-control">
|
<div class="control secret-control">
|
||||||
<input id="r2Secret" class="input is-family-monospace" type="password" name="r2_secret_access_key" autocomplete="new-password" placeholder="••••••••••••••••">
|
<input id="r2Secret" class="input is-family-monospace" type="password" name="r2_secret_access_key" autocomplete="new-password" placeholder="••••••••••••••••">
|
||||||
</div>
|
</div>
|
||||||
<p class="settings-help">{% if r2_secret_configured %}{{ t.settings_secret_saved }}{% else %}{{ t.settings_secret_not_set }}{% endif %}</p>
|
<p class="field-help">{% if r2_secret_configured %}{{ t.settings_secret_saved }}{% else %}{{ t.settings_secret_not_set }}{% endif %}</p>
|
||||||
{% if r2_secret_configured %}
|
{% if r2_secret_configured %}
|
||||||
<label class="secret-clear"><input type="checkbox" name="clear_r2_secret_access_key" value="true"> {{ t.settings_secret_clear }}</label>
|
<label class="admin-check admin-check-danger"><input type="checkbox" name="clear_r2_secret_access_key" value="true"><span class="admin-check-box">✓</span><span>{{ t.settings_secret_clear }}</span></label>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="settings-section">
|
<section class="admin-section">
|
||||||
<header class="settings-section-head">
|
<header class="admin-section-head">
|
||||||
<span class="settings-section-icon" aria-hidden="true">🔔</span>
|
<span class="admin-section-icon" aria-hidden="true">🔔</span>
|
||||||
<div>
|
<div>
|
||||||
<div class="settings-title-row">
|
<div class="admin-title-row">
|
||||||
<h2>{{ t.settings_section_notifications }}</h2>
|
<h2>{{ t.settings_section_notifications }}</h2>
|
||||||
<span class="settings-status{% if client_notifications_checked %} is-enabled{% endif %}">
|
<span class="status-pill{% if client_notifications_checked %} is-enabled{% endif %}">
|
||||||
{% if client_notifications_checked %}{{ t.settings_status_enabled }}{% else %}{{ t.settings_status_disabled }}{% endif %}
|
{% if client_notifications_checked %}{{ t.settings_status_enabled }}{% else %}{{ t.settings_status_disabled }}{% endif %}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<p>{{ t.settings_section_notifications_help }}</p>
|
<p>{{ t.settings_section_notifications_help }}</p>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<div class="settings-section-body">
|
<div class="admin-section-body">
|
||||||
<label class="settings-toggle" for="clientNotifications">
|
<label class="admin-toggle" for="clientNotifications">
|
||||||
<input id="clientNotifications" type="checkbox" name="client_notifications_enabled" value="true"{% if client_notifications_checked %} checked{% endif %}>
|
<input id="clientNotifications" type="checkbox" name="client_notifications_enabled" value="true"{% if client_notifications_checked %} checked{% endif %}>
|
||||||
<span class="toggle-track" aria-hidden="true"><span></span></span>
|
<span class="admin-toggle-track" aria-hidden="true"><span></span></span>
|
||||||
<span>
|
<span>
|
||||||
<strong>{{ t.settings_client_notifications_enabled }}</strong>
|
<strong>{{ t.settings_client_notifications_enabled }}</strong>
|
||||||
<small>{{ t.settings_client_notifications_help }}</small>
|
<small>{{ t.settings_client_notifications_help }}</small>
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<div class="settings-subsection">
|
<div class="admin-subsection">
|
||||||
<h3>Web Push / VAPID</h3>
|
<h3>Web Push / VAPID</h3>
|
||||||
<div class="settings-note settings-note-warning">
|
<div class="admin-note admin-note-warning">
|
||||||
<p>{{ t.settings_vapid_warning }}</p>
|
<p>{{ t.settings_vapid_warning }}</p>
|
||||||
<p>{{ t.settings_vapid_generate }} <code>cargo run --bin generate_vapid</code></p>
|
<p>{{ t.settings_vapid_generate }} <code>cargo run --bin generate_vapid</code></p>
|
||||||
</div>
|
</div>
|
||||||
<div class="settings-grid">
|
<div class="admin-grid">
|
||||||
<div class="field settings-field settings-field-wide">
|
<div class="field admin-field admin-field-wide">
|
||||||
<label class="label" for="vapidPublic">{{ t.settings_vapid_public_key }}</label>
|
<label class="label" for="vapidPublic">{{ t.settings_vapid_public_key }}</label>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<input id="vapidPublic" class="input is-family-monospace" type="text" name="vapid_public_key" autocomplete="off" spellcheck="false" value="{% for s in &settings %}{% if s.key == "vapid_public_key" %}{{ s.value }}{% endif %}{% endfor %}">
|
<input id="vapidPublic" class="input is-family-monospace" type="text" name="vapid_public_key" autocomplete="off" spellcheck="false" value="{% for s in &settings %}{% if s.key == "vapid_public_key" %}{{ s.value }}{% endif %}{% endfor %}">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="field settings-field settings-field-wide">
|
<div class="field admin-field admin-field-wide">
|
||||||
<label class="label" for="vapidPrivate">{{ t.settings_vapid_private_key }}</label>
|
<label class="label" for="vapidPrivate">{{ t.settings_vapid_private_key }}</label>
|
||||||
<div class="control secret-control">
|
<div class="control secret-control">
|
||||||
<input id="vapidPrivate" class="input is-family-monospace" type="password" name="vapid_private_key" autocomplete="new-password" placeholder="••••••••••••••••">
|
<input id="vapidPrivate" class="input is-family-monospace" type="password" name="vapid_private_key" autocomplete="new-password" placeholder="••••••••••••••••">
|
||||||
</div>
|
</div>
|
||||||
<p class="settings-help">{% if vapid_private_key_configured %}{{ t.settings_secret_saved }}{% else %}{{ t.settings_secret_not_set }}{% endif %}</p>
|
<p class="field-help">{% if vapid_private_key_configured %}{{ t.settings_secret_saved }}{% else %}{{ t.settings_secret_not_set }}{% endif %}</p>
|
||||||
{% if vapid_private_key_configured %}
|
{% if vapid_private_key_configured %}
|
||||||
<label class="secret-clear"><input type="checkbox" name="clear_vapid_private_key" value="true"> {{ t.settings_secret_clear }}</label>
|
<label class="admin-check admin-check-danger"><input type="checkbox" name="clear_vapid_private_key" value="true"><span class="admin-check-box">✓</span><span>{{ t.settings_secret_clear }}</span></label>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="field settings-field settings-field-wide">
|
<div class="field admin-field admin-field-wide">
|
||||||
<label class="label" for="vapidSubject">{{ t.settings_vapid_subject }}</label>
|
<label class="label" for="vapidSubject">{{ t.settings_vapid_subject }}</label>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<input id="vapidSubject" class="input" type="text" name="vapid_subject" placeholder="mailto:admin@example.com" value="{% for s in &settings %}{% if s.key == "vapid_subject" %}{{ s.value }}{% endif %}{% endfor %}">
|
<input id="vapidSubject" class="input" type="text" name="vapid_subject" placeholder="mailto:admin@example.com" value="{% for s in &settings %}{% if s.key == "vapid_subject" %}{{ s.value }}{% endif %}{% endfor %}">
|
||||||
@@ -176,13 +176,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<details class="settings-details">
|
<details class="admin-details">
|
||||||
<summary>{{ t.settings_push_subscribers }} <span>{{ push_subscribers.len() }}</span></summary>
|
<summary>{{ t.settings_push_subscribers }} <span>{{ push_subscribers.len() }}</span></summary>
|
||||||
<div class="settings-details-body">
|
<div class="admin-details-body">
|
||||||
{% if push_subscribers.is_empty() %}
|
{% if push_subscribers.is_empty() %}
|
||||||
<p class="settings-help">{{ t.settings_push_no_subscribers }}</p>
|
<p class="field-help">{{ t.settings_push_no_subscribers }}</p>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="settings-table-wrap">
|
<div class="table-wrap">
|
||||||
<table class="table is-fullwidth is-striped is-narrow">
|
<table class="table is-fullwidth is-striped is-narrow">
|
||||||
<thead><tr>
|
<thead><tr>
|
||||||
<th>{{ t.settings_push_client }}</th>
|
<th>{{ t.settings_push_client }}</th>
|
||||||
@@ -207,98 +207,98 @@
|
|||||||
</details>
|
</details>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="settings-subsection">
|
<div class="admin-subsection">
|
||||||
<h3>Telegram</h3>
|
<h3>Telegram</h3>
|
||||||
<div class="field settings-field settings-field-wide">
|
<div class="field admin-field admin-field-wide">
|
||||||
<label class="label" for="telegramToken">{{ t.settings_telegram_bot_token }}</label>
|
<label class="label" for="telegramToken">{{ t.settings_telegram_bot_token }}</label>
|
||||||
<div class="control secret-control">
|
<div class="control secret-control">
|
||||||
<input id="telegramToken" class="input is-family-monospace" type="password" name="telegram_bot_token" autocomplete="new-password" placeholder="••••••••••••••••">
|
<input id="telegramToken" class="input is-family-monospace" type="password" name="telegram_bot_token" autocomplete="new-password" placeholder="••••••••••••••••">
|
||||||
</div>
|
</div>
|
||||||
<p class="settings-help">{% if telegram_bot_token_configured %}{{ t.settings_secret_saved }}{% else %}{{ t.settings_secret_not_set }}{% endif %}</p>
|
<p class="field-help">{% if telegram_bot_token_configured %}{{ t.settings_secret_saved }}{% else %}{{ t.settings_secret_not_set }}{% endif %}</p>
|
||||||
{% if telegram_bot_token_configured %}
|
{% if telegram_bot_token_configured %}
|
||||||
<label class="secret-clear"><input type="checkbox" name="clear_telegram_bot_token" value="true"> {{ t.settings_secret_clear }}</label>
|
<label class="admin-check admin-check-danger"><input type="checkbox" name="clear_telegram_bot_token" value="true"><span class="admin-check-box">✓</span><span>{{ t.settings_secret_clear }}</span></label>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="settings-section">
|
<section class="admin-section">
|
||||||
<header class="settings-section-head">
|
<header class="admin-section-head">
|
||||||
<span class="settings-section-icon" aria-hidden="true">🛡️</span>
|
<span class="admin-section-icon" aria-hidden="true">🛡️</span>
|
||||||
<div>
|
<div>
|
||||||
<h2>{{ t.settings_section_captcha }}</h2>
|
<h2>{{ t.settings_section_captcha }}</h2>
|
||||||
<p>{{ t.settings_section_captcha_help }}</p>
|
<p>{{ t.settings_section_captcha_help }}</p>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<div class="settings-section-body">
|
<div class="admin-section-body">
|
||||||
<div class="settings-grid">
|
<div class="admin-grid">
|
||||||
<div class="field settings-field">
|
<div class="field admin-field">
|
||||||
<label class="label" for="turnstileSiteKey">{{ t.settings_turnstile_site_key }}</label>
|
<label class="label" for="turnstileSiteKey">{{ t.settings_turnstile_site_key }}</label>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<input id="turnstileSiteKey" class="input is-family-monospace" type="text" name="turnstile_site_key" autocomplete="off" spellcheck="false" value="{% for s in &settings %}{% if s.key == "turnstile_site_key" %}{{ s.value }}{% endif %}{% endfor %}">
|
<input id="turnstileSiteKey" class="input is-family-monospace" type="text" name="turnstile_site_key" autocomplete="off" spellcheck="false" value="{% for s in &settings %}{% if s.key == "turnstile_site_key" %}{{ s.value }}{% endif %}{% endfor %}">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="field settings-field">
|
<div class="field admin-field">
|
||||||
<label class="label" for="turnstileSecret">{{ t.settings_turnstile_secret_key }}</label>
|
<label class="label" for="turnstileSecret">{{ t.settings_turnstile_secret_key }}</label>
|
||||||
<div class="control secret-control">
|
<div class="control secret-control">
|
||||||
<input id="turnstileSecret" class="input is-family-monospace" type="password" name="turnstile_secret_key" autocomplete="new-password" placeholder="••••••••••••••••">
|
<input id="turnstileSecret" class="input is-family-monospace" type="password" name="turnstile_secret_key" autocomplete="new-password" placeholder="••••••••••••••••">
|
||||||
</div>
|
</div>
|
||||||
<p class="settings-help">{% if turnstile_secret_configured %}{{ t.settings_secret_saved }}{% else %}{{ t.settings_secret_not_set }}{% endif %}</p>
|
<p class="field-help">{% if turnstile_secret_configured %}{{ t.settings_secret_saved }}{% else %}{{ t.settings_secret_not_set }}{% endif %}</p>
|
||||||
{% if turnstile_secret_configured %}
|
{% if turnstile_secret_configured %}
|
||||||
<label class="secret-clear"><input type="checkbox" name="clear_turnstile_secret_key" value="true"> {{ t.settings_secret_clear }}</label>
|
<label class="admin-check admin-check-danger"><input type="checkbox" name="clear_turnstile_secret_key" value="true"><span class="admin-check-box">✓</span><span>{{ t.settings_secret_clear }}</span></label>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="settings-section">
|
<section class="admin-section">
|
||||||
<header class="settings-section-head">
|
<header class="admin-section-head">
|
||||||
<span class="settings-section-icon" aria-hidden="true">🔐</span>
|
<span class="admin-section-icon" aria-hidden="true">🔐</span>
|
||||||
<div>
|
<div>
|
||||||
<h2>{{ t.settings_section_oidc }}</h2>
|
<h2>{{ t.settings_section_oidc }}</h2>
|
||||||
<p>{{ t.settings_section_oidc_help }}</p>
|
<p>{{ t.settings_section_oidc_help }}</p>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<div class="settings-section-body">
|
<div class="admin-section-body">
|
||||||
<div class="settings-toggle-list">
|
<div class="admin-toggle-list">
|
||||||
<label class="settings-toggle" for="passwordAuth">
|
<label class="admin-toggle" for="passwordAuth">
|
||||||
<input id="passwordAuth" type="checkbox" name="auth_password_enabled" value="true"{% if auth_password_checked %} checked{% endif %}>
|
<input id="passwordAuth" type="checkbox" name="auth_password_enabled" value="true"{% if auth_password_checked %} checked{% endif %}>
|
||||||
<span class="toggle-track" aria-hidden="true"><span></span></span>
|
<span class="admin-toggle-track" aria-hidden="true"><span></span></span>
|
||||||
<span><strong>{{ t.settings_auth_password_enabled }}</strong></span>
|
<span><strong>{{ t.settings_auth_password_enabled }}</strong></span>
|
||||||
</label>
|
</label>
|
||||||
<label class="settings-toggle" for="ssoAuth">
|
<label class="admin-toggle" for="ssoAuth">
|
||||||
<input id="ssoAuth" type="checkbox" name="auth_sso_enabled" value="true"{% if auth_sso_checked %} checked{% endif %}>
|
<input id="ssoAuth" type="checkbox" name="auth_sso_enabled" value="true"{% if auth_sso_checked %} checked{% endif %}>
|
||||||
<span class="toggle-track" aria-hidden="true"><span></span></span>
|
<span class="admin-toggle-track" aria-hidden="true"><span></span></span>
|
||||||
<span><strong>{{ t.settings_auth_sso_enabled }}</strong></span>
|
<span><strong>{{ t.settings_auth_sso_enabled }}</strong></span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="settings-grid">
|
<div class="admin-grid">
|
||||||
<div class="field settings-field settings-field-wide">
|
<div class="field admin-field admin-field-wide">
|
||||||
<label class="label" for="oidcIssuer">{{ t.settings_oidc_issuer_url }}</label>
|
<label class="label" for="oidcIssuer">{{ t.settings_oidc_issuer_url }}</label>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<input id="oidcIssuer" class="input" type="url" name="oidc_issuer_url" placeholder="https://keycloak.example.com/realms/myrealm" value="{% for s in &settings %}{% if s.key == "oidc_issuer_url" %}{{ s.value }}{% endif %}{% endfor %}">
|
<input id="oidcIssuer" class="input" type="url" name="oidc_issuer_url" placeholder="https://keycloak.example.com/realms/myrealm" value="{% for s in &settings %}{% if s.key == "oidc_issuer_url" %}{{ s.value }}{% endif %}{% endfor %}">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="field settings-field">
|
<div class="field admin-field">
|
||||||
<label class="label" for="oidcClientId">{{ t.settings_oidc_client_id }}</label>
|
<label class="label" for="oidcClientId">{{ t.settings_oidc_client_id }}</label>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<input id="oidcClientId" class="input is-family-monospace" type="text" name="oidc_client_id" autocomplete="off" spellcheck="false" value="{% for s in &settings %}{% if s.key == "oidc_client_id" %}{{ s.value }}{% endif %}{% endfor %}">
|
<input id="oidcClientId" class="input is-family-monospace" type="text" name="oidc_client_id" autocomplete="off" spellcheck="false" value="{% for s in &settings %}{% if s.key == "oidc_client_id" %}{{ s.value }}{% endif %}{% endfor %}">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="field settings-field">
|
<div class="field admin-field">
|
||||||
<label class="label" for="oidcClientSecret">{{ t.settings_oidc_client_secret }}</label>
|
<label class="label" for="oidcClientSecret">{{ t.settings_oidc_client_secret }}</label>
|
||||||
<div class="control secret-control">
|
<div class="control secret-control">
|
||||||
<input id="oidcClientSecret" class="input is-family-monospace" type="password" name="oidc_client_secret" autocomplete="new-password" placeholder="••••••••••••••••">
|
<input id="oidcClientSecret" class="input is-family-monospace" type="password" name="oidc_client_secret" autocomplete="new-password" placeholder="••••••••••••••••">
|
||||||
</div>
|
</div>
|
||||||
<p class="settings-help">{% if oidc_client_secret_configured %}{{ t.settings_secret_saved }}{% else %}{{ t.settings_secret_not_set }}{% endif %}</p>
|
<p class="field-help">{% if oidc_client_secret_configured %}{{ t.settings_secret_saved }}{% else %}{{ t.settings_secret_not_set }}{% endif %}</p>
|
||||||
{% if oidc_client_secret_configured %}
|
{% if oidc_client_secret_configured %}
|
||||||
<label class="secret-clear"><input type="checkbox" name="clear_oidc_client_secret" value="true"> {{ t.settings_secret_clear }}</label>
|
<label class="admin-check admin-check-danger"><input type="checkbox" name="clear_oidc_client_secret" value="true"><span class="admin-check-box">✓</span><span>{{ t.settings_secret_clear }}</span></label>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="field settings-field settings-field-wide">
|
<div class="field admin-field admin-field-wide">
|
||||||
<label class="label" for="oidcGroups">{{ t.settings_oidc_allowed_groups }}</label>
|
<label class="label" for="oidcGroups">{{ t.settings_oidc_allowed_groups }}</label>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<input id="oidcGroups" class="input" type="text" name="oidc_allowed_groups" placeholder="admins, web-petting" value="{% for s in &settings %}{% if s.key == "oidc_allowed_groups" %}{{ s.value }}{% endif %}{% endfor %}">
|
<input id="oidcGroups" class="input" type="text" name="oidc_allowed_groups" placeholder="admins, web-petting" value="{% for s in &settings %}{% if s.key == "oidc_allowed_groups" %}{{ s.value }}{% endif %}{% endfor %}">
|
||||||
@@ -308,77 +308,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<div class="settings-save-bar">
|
<div class="sticky-actions">
|
||||||
<button type="submit" class="button is-primary">{{ t.settings_save }}</button>
|
<button type="submit" class="button is-primary">{{ t.settings_save }}</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<style>
|
|
||||||
.settings-page-head { align-items:flex-end; margin-bottom:1.25rem; }
|
|
||||||
.settings-page-head h1 { font-size:1.55rem; }
|
|
||||||
.settings-page-head p { margin-top:.25rem; color:#777; font-size:.9rem; }
|
|
||||||
.settings-message { border-radius:10px; margin-bottom:1rem; }
|
|
||||||
.settings-form { display:flex; flex-direction:column; gap:1rem; }
|
|
||||||
.settings-section { background:#fff; border:1px solid #e7e5ef; border-radius:14px; overflow:hidden; box-shadow:0 2px 10px rgba(42,34,80,.035); }
|
|
||||||
.settings-section-head { display:grid; grid-template-columns:42px minmax(0,1fr); gap:.8rem; padding:1rem 1.15rem; background:linear-gradient(180deg,#fff,#fbfaff); border-bottom:1px solid #eceaf2; }
|
|
||||||
.settings-section-icon { display:flex; align-items:center; justify-content:center; width:42px; height:42px; border-radius:11px; background:#efedff; font-size:1.2rem; }
|
|
||||||
.settings-section-head h2 { margin:0; font-size:1.05rem; font-weight:750; color:#302c49; }
|
|
||||||
.settings-section-head p { margin:.2rem 0 0; max-width:680px; color:#817c94; font-size:.82rem; line-height:1.45; }
|
|
||||||
.settings-title-row { display:flex; align-items:center; flex-wrap:wrap; gap:.55rem; }
|
|
||||||
.settings-status { display:inline-flex; align-items:center; padding:.12rem .48rem; border-radius:99px; background:#eeedf2; color:#777184; font-size:.67rem; font-weight:700; }
|
|
||||||
.settings-status.is-enabled { background:#dcf7e8; color:#177349; }
|
|
||||||
.settings-section-body { padding:1.15rem; }
|
|
||||||
.settings-grid { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:1rem 1.1rem; }
|
|
||||||
.settings-field { min-width:0; margin:0 !important; }
|
|
||||||
.settings-field-wide { grid-column:1 / -1; }
|
|
||||||
.settings-field .label { margin-bottom:.38rem; font-size:.78rem; font-weight:700; color:#514c66 !important; }
|
|
||||||
.settings-field .input, .settings-field .textarea { border-radius:8px; box-shadow:none; font-size:.88rem; }
|
|
||||||
.settings-field .input:focus, .settings-field .textarea:focus { border-color:#7c6cff !important; box-shadow:0 0 0 2px rgba(124,108,255,.12); }
|
|
||||||
.settings-field .textarea { min-height:76px; resize:vertical; }
|
|
||||||
.settings-help { margin-top:.35rem; color:#8b869b; font-size:.72rem; line-height:1.4; }
|
|
||||||
.secret-control { position:relative; }
|
|
||||||
.secret-control::after { content:"🔒"; position:absolute; right:.75rem; top:50%; transform:translateY(-50%); font-size:.78rem; opacity:.48; pointer-events:none; }
|
|
||||||
.secret-control .input { padding-right:2.25rem; letter-spacing:.05em; }
|
|
||||||
.secret-clear { display:inline-flex; align-items:center; gap:.35rem; margin-top:.38rem; color:#a04e58 !important; font-size:.7rem; cursor:pointer; }
|
|
||||||
.secret-clear input { accent-color:#b85c66; }
|
|
||||||
.settings-toggle-list { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:.7rem; margin-bottom:1rem; }
|
|
||||||
.settings-toggle { display:flex; align-items:flex-start; gap:.7rem; padding:.75rem .85rem; border:1px solid #e8e5f2; border-radius:10px; background:#fbfaff; cursor:pointer; }
|
|
||||||
.settings-toggle > input { position:absolute; opacity:0; pointer-events:none; }
|
|
||||||
.toggle-track { flex:0 0 auto; width:38px; height:22px; padding:2px; border-radius:99px; background:#ccc8d6; transition:.18s; }
|
|
||||||
.toggle-track span { display:block; width:18px; height:18px; border-radius:50%; background:#fff; box-shadow:0 1px 4px rgba(0,0,0,.2); transition:.18s; }
|
|
||||||
.settings-toggle > input:checked + .toggle-track { background:#7567e8; }
|
|
||||||
.settings-toggle > input:checked + .toggle-track span { transform:translateX(16px); }
|
|
||||||
.settings-toggle > input:focus-visible + .toggle-track { outline:2px solid #7c6cff; outline-offset:2px; }
|
|
||||||
.settings-toggle strong { display:block; color:#3e3956; font-size:.82rem; line-height:1.35; }
|
|
||||||
.settings-toggle small { display:block; margin-top:.15rem; color:#8a849c; font-size:.7rem; line-height:1.35; }
|
|
||||||
.settings-note { margin:.9rem 0 1rem; padding:.75rem .9rem; border:1px solid; border-radius:9px; font-size:.76rem; line-height:1.5; }
|
|
||||||
.settings-note p + p { margin-top:.35rem; }
|
|
||||||
.settings-note-info { background:#f1f7ff; border-color:#d7e8fa; color:#42627f; }
|
|
||||||
.settings-note-warning { background:#fff8e8; border-color:#f1dfb4; color:#785f28; }
|
|
||||||
.settings-note code { display:inline-block; margin:.2rem 0 0; padding:.12rem .35rem; border-radius:4px; background:rgba(255,255,255,.7); color:inherit; user-select:all; }
|
|
||||||
.settings-subsection { margin-top:1.1rem; padding-top:1.1rem; border-top:1px solid #eeecf3; }
|
|
||||||
.settings-subsection h3 { margin:0 0 .7rem; color:#514b69; font-size:.82rem; font-weight:800; text-transform:uppercase; letter-spacing:.04em; }
|
|
||||||
.settings-details { margin-top:1rem; border:1px solid #e8e5f2; border-radius:9px; background:#fcfbff; overflow:hidden; }
|
|
||||||
.settings-details summary { display:flex; justify-content:space-between; align-items:center; padding:.7rem .85rem; cursor:pointer; color:#514b69; font-size:.78rem; font-weight:700; }
|
|
||||||
.settings-details summary span { min-width:24px; padding:.05rem .4rem; border-radius:99px; background:#ece9ff; color:#6257bb; text-align:center; }
|
|
||||||
.settings-details-body { padding:0 .85rem .85rem; }
|
|
||||||
.settings-table-wrap { overflow-x:auto; }
|
|
||||||
.settings-table-wrap .table { margin:0; background:transparent; font-size:.75rem; }
|
|
||||||
.is-nowrap { white-space:nowrap; }
|
|
||||||
.seo-preview { margin-top:.5rem; padding:.5rem .65rem; border:1px solid #ebe9ef; border-radius:7px; background:#fafafa; min-height:2rem; line-height:2; font-size:.78rem; }
|
|
||||||
.settings-save-bar { position:sticky; bottom:.75rem; z-index:10; display:flex; justify-content:flex-end; padding:.65rem; border:1px solid rgba(222,218,238,.9); border-radius:12px; background:rgba(255,255,255,.92); box-shadow:0 8px 28px rgba(46,38,86,.12); backdrop-filter:blur(8px); }
|
|
||||||
.settings-save-bar .button { min-width:150px; border-radius:8px; font-weight:700; }
|
|
||||||
@media (max-width:700px) {
|
|
||||||
.settings-grid, .settings-toggle-list { grid-template-columns:1fr; }
|
|
||||||
.settings-field-wide { grid-column:auto; }
|
|
||||||
.settings-section-head { grid-template-columns:36px minmax(0,1fr); padding:.9rem; }
|
|
||||||
.settings-section-icon { width:36px; height:36px; }
|
|
||||||
.settings-section-body { padding:.9rem; }
|
|
||||||
.settings-save-bar { bottom:4.15rem; }
|
|
||||||
.settings-save-bar .button { width:100%; }
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
(function() {
|
(function() {
|
||||||
var colors = [
|
var colors = [
|
||||||
|
|||||||
@@ -6,28 +6,18 @@
|
|||||||
<title>{{ t.nav_title }} — {{ t.setup_title }}</title>
|
<title>{{ t.nav_title }} — {{ t.setup_title }}</title>
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@1/css/bulma.min.css">
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@1/css/bulma.min.css">
|
||||||
<style>
|
<link rel="stylesheet" href="/static/admin.css?v={{ t.app_version() }}">
|
||||||
:root { color-scheme: light; }
|
|
||||||
body { background: #f5f5f5; display: flex; align-items: center; justify-content: center; min-height: 100vh; color: #333; }
|
|
||||||
.login-box { width: 100%; max-width: 400px; padding: 0 1rem; }
|
|
||||||
.login-card { background: #fff; border-radius: 12px; padding: 2rem 1.5rem; box-shadow: 0 2px 12px rgba(0,0,0,0.06); }
|
|
||||||
input, textarea, select, .input, .textarea, .select select {
|
|
||||||
background-color: #fff !important; color: #333 !important; border-color: #dbdbdb !important;
|
|
||||||
}
|
|
||||||
.label, label { color: #363636 !important; }
|
|
||||||
.notification { color: #333 !important; }
|
|
||||||
</style>
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body class="auth-shell">
|
||||||
<div class="login-box">
|
<div class="login-box">
|
||||||
<div class="has-text-right mb-3">
|
<div class="auth-language">
|
||||||
<a href="?lang={{ lang.other().code() }}" class="has-text-grey is-size-7">{{ lang.other().label() }}</a>
|
<a href="?lang={{ lang.other().code() }}">{{ lang.other().label() }}</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="login-card">
|
<div class="login-card">
|
||||||
<div class="has-text-centered mb-4">
|
<div class="auth-head">
|
||||||
<p class="is-size-3">🐾</p>
|
<span class="auth-icon">🐾</span>
|
||||||
<h1 class="is-size-4 has-text-weight-bold">{{ t.nav_title }}</h1>
|
<h1>{{ t.nav_title }}</h1>
|
||||||
<p class="has-text-grey">{{ t.setup_title }}</p>
|
<p>{{ t.setup_title }}</p>
|
||||||
</div>
|
</div>
|
||||||
<p class="has-text-grey has-text-centered is-size-7 mb-4">{{ t.setup_description }}</p>
|
<p class="has-text-grey has-text-centered is-size-7 mb-4">{{ t.setup_description }}</p>
|
||||||
{% if let Some(err) = error.as_ref() %}
|
{% if let Some(err) = error.as_ref() %}
|
||||||
|
|||||||
@@ -9,13 +9,17 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Add form -->
|
<!-- Add form -->
|
||||||
<div class="form-card" style="margin-bottom:1.5rem;">
|
<section class="admin-section mb-5">
|
||||||
<h2 style="font-size:1.1rem;font-weight:700;margin-bottom:0.75rem;">{{ t.testimonials_add_title }}</h2>
|
<header class="admin-section-head">
|
||||||
<form method="post" action="/admin/testimonials/add" enctype="multipart/form-data">
|
<span class="admin-section-icon" aria-hidden="true">💬</span>
|
||||||
<div class="field">
|
<div><h2>{{ t.testimonials_add_title }}</h2></div>
|
||||||
|
</header>
|
||||||
|
<div class="admin-section-body">
|
||||||
|
<form method="post" action="/admin/testimonials/add" enctype="multipart/form-data" class="form-grid">
|
||||||
|
<div class="field admin-field-wide">
|
||||||
<label class="label">{{ t.testimonials_text }} *</label>
|
<label class="label">{{ t.testimonials_text }} *</label>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<textarea class="input" name="text" rows="3" required style="min-height:80px;resize:vertical;"></textarea>
|
<textarea class="textarea" name="text" rows="3" required></textarea>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
@@ -30,27 +34,28 @@
|
|||||||
<input class="input" type="file" name="image" accept="image/*">
|
<input class="input" type="file" name="image" accept="image/*">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" class="button is-primary">{{ t.testimonials_add_button }}</button>
|
<div class="admin-field-wide"><button type="submit" class="button is-primary">{{ t.testimonials_add_button }}</button></div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<!-- List -->
|
<!-- List -->
|
||||||
{% if testimonials.is_empty() %}
|
{% if testimonials.is_empty() %}
|
||||||
<p style="color:#888;">{{ t.testimonials_empty }}</p>
|
<p class="empty-state">{{ t.testimonials_empty }}</p>
|
||||||
{% else %}
|
{% else %}
|
||||||
{% for item in &testimonials %}
|
{% for item in &testimonials %}
|
||||||
<div class="item-card" id="card-{{ item.id.unwrap() }}">
|
<div class="item-card" id="card-{{ item.id.unwrap() }}">
|
||||||
<!-- View mode -->
|
<!-- View mode -->
|
||||||
<div class="tm-view" id="view-{{ item.id.unwrap() }}">
|
<div class="tm-view" id="view-{{ item.id.unwrap() }}">
|
||||||
<div class="item-card-header">
|
<div class="item-card-header">
|
||||||
<div style="display:flex;align-items:center;gap:0.75rem;">
|
<div class="testimonial-content">
|
||||||
{% if let Some(image_url) = item.image_url.as_deref() %}
|
{% if let Some(image_url) = item.image_url.as_deref() %}
|
||||||
<img src="{{ image_url }}" alt="" style="width:48px;height:48px;border-radius:50%;object-fit:cover;">
|
<img src="{{ image_url }}" alt="" class="testimonial-avatar">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div>
|
<div>
|
||||||
<div style="font-size:0.95rem;line-height:1.5;">{{ item.text }}</div>
|
<div class="testimonial-text">{{ item.text }}</div>
|
||||||
{% if let Some(note) = item.author_note.as_deref() %}
|
{% if let Some(note) = item.author_note.as_deref() %}
|
||||||
<div style="font-size:0.8rem;color:#888;margin-top:0.2rem;">{{ note }}</div>
|
<div class="testimonial-note">{{ note }}</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -79,7 +84,7 @@
|
|||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label">{{ t.testimonials_text }}</label>
|
<label class="label">{{ t.testimonials_text }}</label>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<textarea class="input" name="text" rows="3" style="min-height:70px;resize:vertical;">{{ item.text }}</textarea>
|
<textarea class="textarea" name="text" rows="3">{{ item.text }}</textarea>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
@@ -90,10 +95,12 @@
|
|||||||
</div>
|
</div>
|
||||||
{% if item.image_path.is_some() %}
|
{% if item.image_path.is_some() %}
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<div style="display:flex;align-items:center;gap:0.75rem;margin-bottom:0.5rem;">
|
<div class="testimonial-content mb-2">
|
||||||
{% if let Some(image_url) = item.image_url.as_deref() %}<img src="{{ image_url }}" alt="" style="width:48px;height:48px;border-radius:50%;object-fit:cover;">{% endif %}
|
{% if let Some(image_url) = item.image_url.as_deref() %}<img src="{{ image_url }}" alt="" class="testimonial-avatar">{% endif %}
|
||||||
<label style="font-size:0.85rem;cursor:pointer;color:#888;">
|
<label class="admin-check admin-check-danger">
|
||||||
<input type="checkbox" name="remove_image" value="1"> {{ t.testimonials_remove_image }}
|
<input type="checkbox" name="remove_image" value="1">
|
||||||
|
<span class="admin-check-box">✓</span>
|
||||||
|
<span>{{ t.testimonials_remove_image }}</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -104,7 +111,7 @@
|
|||||||
<input class="input" type="file" name="image" accept="image/*">
|
<input class="input" type="file" name="image" accept="image/*">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="display:flex;gap:0.5rem;">
|
<div class="action-row">
|
||||||
<button type="submit" class="button btn-sm is-primary">{{ t.testimonials_save }}</button>
|
<button type="submit" class="button btn-sm is-primary">{{ t.testimonials_save }}</button>
|
||||||
<button type="button" class="button btn-sm is-light" onclick="toggleEdit({{ item.id.unwrap() }})">✕</button>
|
<button type="button" class="button btn-sm is-light" onclick="toggleEdit({{ item.id.unwrap() }})">✕</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+38
-44
@@ -11,30 +11,29 @@
|
|||||||
{% for user in &users %}
|
{% for user in &users %}
|
||||||
<div class="item-card">
|
<div class="item-card">
|
||||||
<div class="item-card-header">
|
<div class="item-card-header">
|
||||||
<span class="name">{{ user.login }}{% if let Some(dn) = user.display_name.as_deref() %} <span style="font-weight:400;color:#888;">— {{ dn }}</span>{% endif %}</span>
|
<span class="name">{{ user.login }}{% if let Some(dn) = user.display_name.as_deref() %} <span class="muted-detail">— {{ dn }}</span>{% endif %}</span>
|
||||||
<span class="badge badge-{{ user.status }}">{{ t.client_status(&user.status) }}</span>
|
<span class="badge badge-{{ user.status }}">{{ t.client_status(&user.status) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="item-card-meta">
|
<div class="item-card-meta">
|
||||||
<span>🕐 {{ user.created_at.format("%d.%m.%Y %H:%M") }}</span>
|
<span>🕐 {{ user.created_at.format("%d.%m.%Y %H:%M") }}</span>
|
||||||
</div>
|
</div>
|
||||||
{% if user.status == "active" %}
|
{% if user.status == "active" %}
|
||||||
<form method="post" action="/admin/users/{{ user.id }}/telegram" style="margin-top:0.5rem; padding-top:0.5rem; border-top:1px solid #eee;">
|
<form method="post" action="/admin/users/{{ user.id }}/telegram" class="inline-admin-form">
|
||||||
<div class="columns is-mobile is-vcentered" style="margin-bottom:0;">
|
<div class="inline-admin-grid">
|
||||||
<div class="column">
|
<div class="field">
|
||||||
<div class="field" style="margin-bottom:0;">
|
<label class="label is-small">{{ t.users_telegram_chat_id }}</label>
|
||||||
<label class="label is-small">{{ t.users_telegram_chat_id }}</label>
|
<div class="control">
|
||||||
<div class="control">
|
<input class="input is-small" type="text" name="telegram_chat_id" placeholder="123456789" value="{{ user.telegram_chat_id.as_deref().unwrap_or_default() }}">
|
||||||
<input class="input is-small" type="text" name="telegram_chat_id" placeholder="123456789" value="{{ user.telegram_chat_id.as_deref().unwrap_or_default() }}">
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="column is-narrow">
|
<div>
|
||||||
<label class="checkbox is-small">
|
<label class="admin-toggle admin-toggle-compact">
|
||||||
<input type="checkbox" name="telegram_notifications" value="true" {% if user.telegram_notifications == Some(true) %}checked{% endif %}>
|
<input type="checkbox" name="telegram_notifications" value="true" {% if user.telegram_notifications == Some(true) %}checked{% endif %}>
|
||||||
{{ t.users_telegram_enabled }}
|
<span class="admin-toggle-track" aria-hidden="true"><span></span></span>
|
||||||
|
<span><strong>{{ t.users_telegram_enabled }}</strong></span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="column is-narrow">
|
<div>
|
||||||
<button type="submit" class="button is-small is-info is-outlined">💾</button>
|
<button type="submit" class="button is-small is-info is-outlined">💾</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -54,41 +53,36 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
<div class="form-card" style="margin-top:1.5rem;">
|
<section class="admin-section mt-5">
|
||||||
<h2 class="is-size-5 has-text-weight-bold mb-3">{{ t.users_add_title }}</h2>
|
<header class="admin-section-head">
|
||||||
|
<span class="admin-section-icon" aria-hidden="true">➕</span>
|
||||||
|
<div><h2>{{ t.users_add_title }}</h2></div>
|
||||||
|
</header>
|
||||||
|
<div class="admin-section-body">
|
||||||
{% if let Some(err) = error.as_ref() %}
|
{% if let Some(err) = error.as_ref() %}
|
||||||
<div class="notification is-danger is-light">{{ err }}</div>
|
<div class="notification is-danger is-light">{{ err }}</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<form method="post" action="/admin/users/add">
|
<form method="post" action="/admin/users/add" class="form-grid">
|
||||||
<div class="columns is-mobile">
|
<div class="field">
|
||||||
<div class="column">
|
<label class="label is-small">{{ t.users_login }}</label>
|
||||||
<div class="field">
|
<div class="control"><input class="input" type="text" name="login" required></div>
|
||||||
<label class="label is-small">{{ t.users_login }}</label>
|
|
||||||
<div class="control"><input class="input" type="text" name="login" required></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="column">
|
|
||||||
<div class="field">
|
|
||||||
<label class="label is-small">{{ t.users_display_name }}</label>
|
|
||||||
<div class="control"><input class="input" type="text" name="display_name"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="columns is-mobile">
|
<div class="field">
|
||||||
<div class="column">
|
<label class="label is-small">{{ t.users_display_name }}</label>
|
||||||
<div class="field">
|
<div class="control"><input class="input" type="text" name="display_name"></div>
|
||||||
<label class="label is-small">{{ t.users_password }}</label>
|
</div>
|
||||||
<div class="control"><input class="input" type="password" name="password" required minlength="4"></div>
|
<div class="field">
|
||||||
</div>
|
<label class="label is-small">{{ t.users_password }}</label>
|
||||||
</div>
|
<div class="control"><input class="input" type="password" name="password" required minlength="4"></div>
|
||||||
<div class="column">
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label is-small">{{ t.users_password_confirm }}</label>
|
<label class="label is-small">{{ t.users_password_confirm }}</label>
|
||||||
<div class="control"><input class="input" type="password" name="password_confirm" required minlength="4"></div>
|
<div class="control"><input class="input" type="password" name="password_confirm" required minlength="4"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="admin-field-wide">
|
||||||
|
<button type="submit" class="button is-primary">{{ t.users_add_button }}</button>
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" class="button is-primary">{{ t.users_add_button }}</button>
|
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
</section>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
Reference in New Issue
Block a user