This commit is contained in:
@@ -0,0 +1,209 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="{{ lang.code() }}">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{ t.portal_title }} — {{ client.name }}</title>
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
color: #2d2b55; line-height: 1.6; background: #f8f7ff;
|
||||
padding: 0 0 2rem;
|
||||
}
|
||||
.portal-header {
|
||||
background: linear-gradient(135deg, #7c6cff, #b06cff);
|
||||
color: #fff; padding: 2rem 1.5rem 1.5rem; text-align: center;
|
||||
}
|
||||
.portal-header h1 { font-size: 1.5rem; font-weight: 700; }
|
||||
.portal-header .sub { opacity: 0.85; font-size: 0.9rem; margin-top: 0.25rem; }
|
||||
.container { max-width: 700px; margin: 0 auto; padding: 0 1rem; }
|
||||
.section-title {
|
||||
font-size: 1.15rem; font-weight: 700; margin: 1.5rem 0 0.75rem;
|
||||
padding-bottom: 0.4rem; border-bottom: 2px solid #ede7f6;
|
||||
}
|
||||
.visit-card {
|
||||
background: #fff; border-radius: 12px; padding: 1rem;
|
||||
margin-bottom: 0.75rem; border: 1px solid #eee;
|
||||
}
|
||||
.visit-card-head {
|
||||
display: flex; justify-content: space-between; align-items: center;
|
||||
margin-bottom: 0.3rem;
|
||||
}
|
||||
.visit-card-head .date { font-weight: 700; font-size: 0.95rem; }
|
||||
.visit-card-head .time { color: #7a7599; font-size: 0.85rem; }
|
||||
.visit-card .admin { color: #999; font-size: 0.82rem; }
|
||||
.badge-sm {
|
||||
display: inline-block; padding: 0.1rem 0.5rem; border-radius: 99px;
|
||||
font-size: 0.7rem; font-weight: 600;
|
||||
}
|
||||
.badge-completed { background: #d1fae5; color: #065f46; }
|
||||
.badge-scheduled { background: #dbeafe; color: #1e40af; }
|
||||
.media-row {
|
||||
display: flex; gap: 0.5rem; flex-wrap: wrap; margin-top: 0.5rem;
|
||||
}
|
||||
.media-row a { display: block; cursor: pointer; }
|
||||
.media-row img {
|
||||
width: 80px; height: 60px; object-fit: cover; border-radius: 6px;
|
||||
}
|
||||
.media-row .vid-thumb {
|
||||
width: 80px; height: 60px; border-radius: 6px; background: #f0f0f0;
|
||||
display: flex; align-items: center; justify-content: center; font-size: 1.5rem;
|
||||
}
|
||||
.feedback-form { margin-top: 0.6rem; }
|
||||
.feedback-form textarea {
|
||||
width: 100%; padding: 0.5rem 0.75rem; border: 1px solid #ddd; border-radius: 8px;
|
||||
font-size: 0.85rem; font-family: inherit; resize: vertical; min-height: 50px;
|
||||
}
|
||||
.feedback-form textarea:focus { outline: none; border-color: #7c6cff; }
|
||||
.feedback-form button {
|
||||
margin-top: 0.3rem; padding: 0.35rem 1rem; border: none; border-radius: 8px;
|
||||
background: #7c6cff; color: #fff; font-size: 0.82rem; font-weight: 600;
|
||||
cursor: pointer;
|
||||
}
|
||||
.feedback-text {
|
||||
margin-top: 0.4rem; padding: 0.5rem 0.75rem; background: #f0f0ff;
|
||||
border-radius: 8px; font-size: 0.85rem; color: #4a4570;
|
||||
display: flex; justify-content: space-between; align-items: flex-start; gap: 0.5rem;
|
||||
}
|
||||
.fb-edit-btn {
|
||||
background: none; border: none; cursor: pointer; font-size: 0.85rem;
|
||||
opacity: 0.35; padding: 0; line-height: 1; flex-shrink: 0;
|
||||
}
|
||||
.fb-edit-btn:hover { opacity: 0.7; }
|
||||
.fb-cancel-btn {
|
||||
padding: 0.35rem 0.75rem; border: 1px solid #ddd; border-radius: 8px;
|
||||
background: #fff; font-size: 0.82rem; cursor: pointer; color: #999;
|
||||
}
|
||||
.success-msg {
|
||||
background: #d1fae5; color: #065f46; padding: 0.6rem 1rem;
|
||||
border-radius: 8px; margin: 1rem 0; font-size: 0.9rem; text-align: center;
|
||||
}
|
||||
.empty-msg { color: #999; font-size: 0.9rem; padding: 0.5rem 0; }
|
||||
.lang-switch { text-align: right; padding: 0.5rem 1rem 0; }
|
||||
.lang-switch a { color: #7c6cff; font-size: 0.85rem; text-decoration: none; }
|
||||
|
||||
/* Compact upcoming schedule */
|
||||
.upcoming-list {
|
||||
display: flex; flex-direction: column; gap: 0.35rem;
|
||||
}
|
||||
.upcoming-row {
|
||||
display: flex; align-items: center; gap: 0.75rem;
|
||||
background: #fff; border-radius: 8px; padding: 0.5rem 0.75rem;
|
||||
border: 1px solid #eee; font-size: 0.85rem;
|
||||
}
|
||||
.upcoming-row .up-date {
|
||||
font-weight: 700; min-width: 5.5rem;
|
||||
}
|
||||
.upcoming-row .up-time { color: #7a7599; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="lang-switch">
|
||||
<a href="?lang={{ lang.other().code() }}">{{ lang.other().label() }}</a>
|
||||
</div>
|
||||
|
||||
<div class="portal-header">
|
||||
<h1>{{ t.portal_title }}</h1>
|
||||
<div class="sub">{{ client.name }}</div>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
|
||||
{% if feedback_sent %}
|
||||
<div class="success-msg">{{ t.portal_feedback_thanks }}</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Past visits with media first -->
|
||||
<h2 class="section-title">{{ t.portal_past }}</h2>
|
||||
{% if past.is_empty() %}
|
||||
<p class="empty-msg">{{ t.portal_no_past }}</p>
|
||||
{% else %}
|
||||
{% for pv in &past %}
|
||||
<div class="visit-card">
|
||||
<div class="visit-card-head">
|
||||
<span class="date">{{ pv.visit.visit_date }}</span>
|
||||
<span class="badge-sm badge-{{ pv.visit.status }}">
|
||||
{{ t.visit_status(&pv.visit.status) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="visit-card-head" style="margin-bottom:0;">
|
||||
<span class="time">{{ pv.visit.time_start }} — {{ pv.visit.time_end }}</span>
|
||||
<span class="admin">{{ pv.admin_name }}</span>
|
||||
</div>
|
||||
|
||||
{% if let Some(pn) = pv.visit.public_notes.as_deref() %}
|
||||
<div style="margin-top:0.4rem;padding:0.5rem 0.75rem;background:#f5f0ff;border-radius:8px;font-size:0.85rem;color:#4a4570;">{{ pn }}</div>
|
||||
{% endif %}
|
||||
|
||||
{% if !pv.media.is_empty() %}
|
||||
<div class="media-row">
|
||||
{% for m in &pv.media %}
|
||||
{% if m.file_type == "photo" %}
|
||||
<a href="/client/{{ client.media_token }}/media/{{ m.id }}" data-lightbox="photo">
|
||||
<img src="/client/{{ client.media_token }}/media/{{ m.id }}" alt="" loading="lazy">
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="/client/{{ client.media_token }}/media/{{ m.id }}" data-lightbox="video">
|
||||
<div class="vid-thumb">🎬</div>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if pv.visit.status == "completed" %}
|
||||
{% if let Some(fb) = pv.visit.client_feedback.as_deref() %}
|
||||
<div class="feedback-view" id="fb-view-{{ pv.visit.id }}">
|
||||
<div class="feedback-text">
|
||||
<span>{{ fb }}</span>
|
||||
<button class="fb-edit-btn" onclick="showFbEdit({{ pv.visit.id }})" title="Edit">✏️</button>
|
||||
</div>
|
||||
</div>
|
||||
<form class="feedback-form" id="fb-form-{{ pv.visit.id }}" style="display:none;" method="post" action="/client/{{ client.media_token }}/{{ pv.visit.id }}/feedback">
|
||||
<textarea name="feedback" required>{{ fb }}</textarea>
|
||||
<div style="display:flex;gap:0.4rem;">
|
||||
<button type="submit">{{ t.portal_feedback_submit }}</button>
|
||||
<button type="button" class="fb-cancel-btn" onclick="hideFbEdit({{ pv.visit.id }})">✕</button>
|
||||
</div>
|
||||
</form>
|
||||
{% else %}
|
||||
<form class="feedback-form" method="post" action="/client/{{ client.media_token }}/{{ pv.visit.id }}/feedback">
|
||||
<textarea name="feedback" placeholder="{{ t.portal_feedback_placeholder }}" required></textarea>
|
||||
<button type="submit">{{ t.portal_feedback_submit }}</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
<!-- Compact upcoming schedule -->
|
||||
{% if !upcoming.is_empty() %}
|
||||
<h2 class="section-title">{{ t.portal_upcoming }}</h2>
|
||||
<div class="upcoming-list">
|
||||
{% for pv in &upcoming %}
|
||||
<div class="upcoming-row">
|
||||
<span class="up-date">{{ pv.visit.visit_date }}</span>
|
||||
<span class="up-time">{{ pv.visit.time_start }} — {{ pv.visit.time_end }}</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function showFbEdit(id) {
|
||||
document.getElementById('fb-view-' + id).style.display = 'none';
|
||||
document.getElementById('fb-form-' + id).style.display = '';
|
||||
}
|
||||
function hideFbEdit(id) {
|
||||
document.getElementById('fb-form-' + id).style.display = 'none';
|
||||
document.getElementById('fb-view-' + id).style.display = '';
|
||||
}
|
||||
</script>
|
||||
{% include "partials/lightbox.html" %}
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user