Fixed media upload. added comment section

This commit is contained in:
Ultradesu
2026-05-12 14:47:24 +01:00
parent 87fb8c744d
commit 6395e36c62
6 changed files with 159 additions and 62 deletions
+1
View File
@@ -2,3 +2,4 @@
/data
db.sqlite3
/uploads
.DS_Store
+42 -10
View File
@@ -182,7 +182,9 @@ struct DashboardTemplate<'a> {
lang: Lang,
admin_name: &'a str,
today_visits: Vec<TodayVisit>,
recent_feedbacks: Vec<RecentFeedback>,
feedbacks: Vec<RecentFeedback>,
feedback_page: usize,
feedback_total_pages: usize,
}
#[derive(Debug, Template)]
@@ -494,13 +496,10 @@ async fn admin_index(request: Request, session: Session, db: Database) -> cot::R
.collect();
today_visits.sort_by(|a, b| a.visit.time_start.cmp(&b.visit.time_start));
let week_ago = today - chrono::Duration::days(7);
let mut recent_feedbacks: Vec<RecentFeedback> = all_visits
let mut all_feedbacks: Vec<RecentFeedback> = all_visits
.iter()
.filter(|v| {
v.user_id.primary_key().unwrap() == user_id
&& v.client_feedback.is_some()
&& v.updated_at.date() >= week_ago
v.user_id.primary_key().unwrap() == user_id && v.client_feedback.is_some()
})
.map(|v| {
let cid: i64 = v.client_id.primary_key().unwrap();
@@ -517,14 +516,34 @@ async fn admin_index(request: Request, session: Session, db: Database) -> cot::R
}
})
.collect();
recent_feedbacks.sort_by(|a, b| b.visit_date.cmp(&a.visit_date));
all_feedbacks.sort_by(|a, b| b.visit_date.cmp(&a.visit_date));
const PER_PAGE: usize = 10;
let feedback_page: usize = request
.uri()
.query()
.and_then(|q| {
q.split('&')
.find_map(|p| p.strip_prefix("page="))
.and_then(|v| v.parse().ok())
})
.unwrap_or(1)
.max(1);
let feedback_total_pages = (all_feedbacks.len() + PER_PAGE - 1).max(1) / PER_PAGE.max(1);
let feedbacks: Vec<RecentFeedback> = all_feedbacks
.into_iter()
.skip((feedback_page - 1) * PER_PAGE)
.take(PER_PAGE)
.collect();
let body = DashboardTemplate {
t: lang.t(),
lang,
admin_name: &admin_name,
today_visits,
recent_feedbacks,
feedbacks,
feedback_page,
feedback_total_pages,
}
.render()?;
html_response(body, lang)
@@ -1569,7 +1588,12 @@ async fn media_upload_submit(
media.save(&db).await?;
}
Redirect::new(format!("/admin/?lang={}", lang.code())).into_response()
Redirect::new(format!(
"/admin/schedule/{}/edit?lang={}",
visit_id,
lang.code()
))
.into_response()
}
async fn media_delete(
@@ -1582,11 +1606,19 @@ async fn media_delete(
if let Err(resp) = require_auth(&session, lang).await {
return Ok(resp);
}
let referer = request
.headers()
.get("referer")
.and_then(|v| v.to_str().ok())
.map(|s| s.to_string());
if let Some(mut m) = query!(Media, $id == media_id).get(&db).await? {
m.status = "archived".to_string();
m.save(&db).await?;
}
Redirect::new(format!("/admin/media?lang={}", lang.code())).into_response()
let redirect_url = referer
.filter(|r| r.contains("/schedule/") && r.contains("/edit"))
.unwrap_or_else(|| format!("/admin/media?lang={}", lang.code()));
Redirect::new(redirect_url).into_response()
}
/// Serve uploaded files by media ID.
+2 -2
View File
@@ -360,7 +360,7 @@ static RU: Translations = Translations {
media_delete_confirm: "Удалить этот файл?",
media_all_clients: "Все клиенты",
portal_title: "Мои визиты",
portal_title: "Визиты",
portal_upcoming: "Предстоящие визиты",
portal_past: "Прошлые визиты",
portal_no_upcoming: "Нет предстоящих визитов.",
@@ -559,7 +559,7 @@ static EN: Translations = Translations {
media_delete_confirm: "Delete this file?",
media_all_clients: "All clients",
portal_title: "My Visits",
portal_title: "Visits",
portal_upcoming: "Upcoming visits",
portal_past: "Past visits",
portal_no_upcoming: "No upcoming visits.",
+1 -2
View File
@@ -1,6 +1,5 @@
use chrono::TimeZone;
use chrono_tz::Tz;
use cot::db::{Database, Model, query};
use cot::db::{Database, query};
use crate::models::Setting;
+24 -6
View File
@@ -47,19 +47,37 @@
{% endfor %}
{% endif %}
<!-- Recent feedbacks -->
<!-- Feedbacks -->
<h2 style="font-size:1.15rem;font-weight:700;margin:1.5rem 0 0.75rem;">{{ t.dashboard_recent_feedbacks }}</h2>
{% if recent_feedbacks.is_empty() %}
{% if feedbacks.is_empty() %}
<p class="has-text-grey">{{ t.dashboard_no_feedbacks }}</p>
{% else %}
{% for fb in &recent_feedbacks %}
<div class="item-card" style="border-left:3px solid #7c6cff;">
{% 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;">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:0.25rem;">
<strong style="font-size:0.9rem;">{{ fb.client_name }}</strong>
<a href="/admin/schedule/{{ fb.visit_id }}/edit?lang={{ lang.code() }}" style="color:#999;font-size:0.8rem;text-decoration:none;">{{ fb.visit_date }}</a>
<span style="color:#999;font-size:0.8rem;">{{ fb.visit_date }}</span>
</div>
<div style="font-size:0.85rem;color:#4a4570;">{{ fb.feedback }}</div>
</div>
</a>
{% endfor %}
{% if feedback_total_pages > 1 %}
<div style="display:flex;justify-content:center;gap:0.5rem;margin-top:1rem;">
{% if feedback_page > 1 %}
<a href="/admin/?lang={{ lang.code() }}&page={{ feedback_page - 1 }}" class="button is-small is-light">&laquo;</a>
{% endif %}
{% for p in 1..=feedback_total_pages %}
{% if p == feedback_page %}
<span class="button is-small is-primary">{{ p }}</span>
{% else %}
<a href="/admin/?lang={{ lang.code() }}&page={{ p }}" class="button is-small is-light">{{ p }}</a>
{% endif %}
{% endfor %}
{% if feedback_page < feedback_total_pages %}
<a href="/admin/?lang={{ lang.code() }}&page={{ feedback_page + 1 }}" class="button is-small is-light">&raquo;</a>
{% endif %}
</div>
{% endif %}
{% endif %}
{% endblock %}
+89 -42
View File
@@ -97,55 +97,79 @@
</div>
</div>
{% if let Some(fb) = visit.client_feedback.as_deref() %}
<div style="margin-bottom:1rem;">
<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>
{% endif %}
<hr style="margin:1rem 0;">
<!-- Media -->
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:0.75rem;">
<label class="label" style="margin:0;">{{ 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>
</div>
{% if media.is_empty() %}
<p class="has-text-grey is-size-7" style="margin-bottom:1rem;">{{ t.media_empty }}</p>
{% else %}
<div class="visit-media-grid">
{% for m in &media %}
<div class="visit-media-item">
{% if m.file_type == "photo" %}
<a href="/admin/uploads/{{ m.id }}" data-lightbox="photo">
<img src="/admin/uploads/{{ m.id }}" alt="" loading="lazy">
</a>
{% else %}
<a href="/admin/uploads/{{ m.id }}" data-lightbox="video">
<div class="video-thumb-sm">🎬</div>
</a>
{% endif %}
{% if let Some(cap) = m.caption.as_deref() %}
<div class="media-cap">{{ cap }}</div>
{% endif %}
</div>
{% endfor %}
</div>
{% endif %}
<hr style="margin:1rem 0;">
<button type="submit" class="button is-primary is-fullwidth">{{ t.schedule_save }}</button>
</form>
{% if let Some(fb) = visit.client_feedback.as_deref() %}
<div style="margin-top:1rem;">
<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>
{% endif %}
<hr style="margin:1rem 0;">
<!-- Media -->
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:0.75rem;">
<label class="label" style="margin:0;">{{ t.nav_media }}</label>
<a href="/admin/media/{{ visit.id }}/upload?lang={{ lang.code() }}" class="button is-info is-small is-outlined">📷 {{ t.media_upload }}</a>
</div>
{% if media.is_empty() %}
<p class="has-text-grey is-size-7" style="margin-bottom:1rem;">{{ t.media_empty }}</p>
{% else %}
<div class="visit-media-grid">
{% for m in &media %}
<div class="visit-media-item">
{% if m.file_type == "photo" %}
<a href="/admin/uploads/{{ m.id }}" data-lightbox="photo">
<img src="/admin/uploads/{{ m.id }}" alt="" loading="lazy">
</a>
{% else %}
<a href="/admin/uploads/{{ m.id }}" data-lightbox="video">
<div class="video-thumb-sm">🎬</div>
</a>
{% endif %}
{% if let Some(cap) = m.caption.as_deref() %}
<div class="media-cap">{{ cap }}</div>
{% endif %}
<form method="post" action="/admin/media/{{ m.id }}/delete" onsubmit="return confirm('{{ t.media_delete_confirm }}');" style="text-align:center;">
<button class="button is-danger is-outlined btn-sm" style="font-size:0.7rem;padding:0.15rem 0.4rem;">{{ t.media_delete }}</button>
</form>
</div>
{% endfor %}
</div>
{% endif %}
<hr style="margin:1rem 0;">
<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>
</form>
</div>
<!-- Upload Modal -->
<div class="upload-modal-bg" id="uploadModal">
<div class="upload-modal">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:1rem;">
<h3 style="font-size:1.1rem;font-weight:700;margin:0;">{{ t.media_upload_title }}</h3>
<button type="button" style="background:none;border:none;font-size:1.2rem;cursor:pointer;color:#888;" onclick="document.getElementById('uploadModal').classList.remove('is-open')"></button>
</div>
<form method="post" action="/admin/media/{{ visit.id }}/upload/submit" enctype="multipart/form-data">
<div class="field">
<label class="label">{{ t.media_choose_files }}</label>
<div class="control">
<input class="input" type="file" name="files" multiple accept="image/*,video/*" required>
</div>
</div>
<div class="field">
<label class="label">{{ t.media_caption }}</label>
<div class="control">
<input class="input" type="text" name="caption" placeholder="{{ t.media_caption }}">
</div>
</div>
<button type="submit" class="button is-primary is-fullwidth">{{ t.media_upload }}</button>
</form>
</div>
</div>
<style>
.visit-media-grid {
display: grid;
@@ -182,8 +206,31 @@
overflow: hidden;
text-overflow: ellipsis;
}
.visit-media-item form {
padding: 0.2rem;
.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);
}
</style>
<script>
document.getElementById('uploadModal').addEventListener('click', function(e) {
if (e.target === this) this.classList.remove('is-open');
});
</script>
{% endblock %}