Compare commits
8
Commits
7da337cc4d
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0955c36f6b | ||
|
|
d2a864c7ca | ||
|
|
41dedff238 | ||
|
|
b309144349 | ||
|
|
373db831c8 | ||
|
|
12de30d91f | ||
|
|
64adbba302 | ||
|
|
996770020f |
No files matched your search
+16
-12
@@ -593,6 +593,7 @@ def dashboard(request: Request, user: User = Depends(get_current_user_web), db:
|
||||
all_cards.extend(cards)
|
||||
|
||||
total = len(all_cards)
|
||||
pending = sum(1 for c in all_cards if c.status == "pending")
|
||||
sent = sum(1 for c in all_cards if c.status == "sent")
|
||||
delivered = sum(1 for c in all_cards if c.status == "delivered")
|
||||
received = sum(1 for c in all_cards if c.status == "received")
|
||||
@@ -604,31 +605,34 @@ def dashboard(request: Request, user: User = Depends(get_current_user_web), db:
|
||||
|
||||
# status distribution for progress bar
|
||||
if total > 0:
|
||||
pending_pct = round(pending / total * 100)
|
||||
sent_pct = round(sent / total * 100)
|
||||
delivered_pct = round(delivered / total * 100)
|
||||
received_pct = round(received / total * 100)
|
||||
else:
|
||||
sent_pct = delivered_pct = received_pct = 0
|
||||
pending_pct = sent_pct = delivered_pct = received_pct = 0
|
||||
|
||||
# country stats split by sent/received
|
||||
sent_country_counts: dict[str, int] = {}
|
||||
received_country_counts: dict[str, int] = {}
|
||||
for c in all_cards:
|
||||
if not c.country_to:
|
||||
continue
|
||||
if c.status in ("pending", "sent", "delivered"):
|
||||
if not c.country_to:
|
||||
continue
|
||||
sent_country_counts[c.country_to] = sent_country_counts.get(c.country_to, 0) + 1
|
||||
elif c.status == "received":
|
||||
received_country_counts[c.country_to] = received_country_counts.get(c.country_to, 0) + 1
|
||||
if not c.country_from:
|
||||
continue
|
||||
received_country_counts[c.country_from] = received_country_counts.get(c.country_from, 0) + 1
|
||||
sent_country_stats = sorted(sent_country_counts.items(), key=lambda x: -x[1])
|
||||
received_country_stats = sorted(received_country_counts.items(), key=lambda x: -x[1])
|
||||
|
||||
from datetime import datetime, timezone as _tz
|
||||
|
||||
return _render(request, "dashboard.html", {"user": user, "profiles": profiles,
|
||||
"total": total, "sent": sent, "delivered": delivered, "received": received,
|
||||
"total": total, "pending": pending, "sent": sent, "delivered": delivered, "received": received,
|
||||
"countries": countries, "recent": recent,
|
||||
"sent_pct": sent_pct, "delivered_pct": delivered_pct, "received_pct": received_pct,
|
||||
"pending_pct": pending_pct, "sent_pct": sent_pct, "delivered_pct": delivered_pct, "received_pct": received_pct,
|
||||
"sent_country_stats": sent_country_stats, "received_country_stats": received_country_stats,
|
||||
"now": datetime.now(_tz.utc) + timedelta(hours=8),
|
||||
})
|
||||
@@ -896,7 +900,7 @@ def postcard_delete(postcard_id: int, user: User = Depends(get_current_user_web)
|
||||
|
||||
|
||||
@router.post("/postcards/{postcard_id}/mark-delivered")
|
||||
def mark_delivered(
|
||||
async def mark_delivered(
|
||||
postcard_id: int,
|
||||
user: User = Depends(get_current_user_web),
|
||||
db: Session = Depends(get_db),
|
||||
@@ -908,16 +912,16 @@ def mark_delivered(
|
||||
.first()
|
||||
)
|
||||
if not pc:
|
||||
return _redirect("/profiles")
|
||||
return JSONResponse({"ok": False}, status_code=404)
|
||||
pc.status = "delivered"
|
||||
if not pc.arrival_time:
|
||||
pc.arrival_time = datetime.now()
|
||||
db.commit()
|
||||
return _redirect(f"/postcards/{postcard_id}")
|
||||
return JSONResponse({"ok": True})
|
||||
|
||||
|
||||
@router.post("/postcards/{postcard_id}/mark-sent")
|
||||
def mark_sent(
|
||||
async def mark_sent(
|
||||
postcard_id: int,
|
||||
user: User = Depends(get_current_user_web),
|
||||
db: Session = Depends(get_db),
|
||||
@@ -930,12 +934,12 @@ def mark_sent(
|
||||
.first()
|
||||
)
|
||||
if not pc:
|
||||
return _redirect("/profiles")
|
||||
return JSONResponse({"ok": False}, status_code=404)
|
||||
pc.status = "sent"
|
||||
if not pc.send_time:
|
||||
pc.send_time = datetime.now()
|
||||
db.commit()
|
||||
return _redirect(f"/postcards/{postcard_id}")
|
||||
return JSONResponse({"ok": True})
|
||||
|
||||
|
||||
# ---------- Image Upload ----------
|
||||
|
||||
@@ -186,17 +186,20 @@ code { background: var(--bg); padding: .15rem .4rem; border-radius: 4px; font-si
|
||||
.dash-stat-body { display: flex; flex-direction: column; }
|
||||
.dash-stat-value { font-size: 1.15rem; font-weight: 700; line-height: 1.2; }
|
||||
.dash-stat-label { font-size: .68rem; color: var(--text-muted); }
|
||||
.dash-pending { color: var(--primary); }
|
||||
.dash-sent { color: var(--sent); }
|
||||
.dash-delivered { color: var(--delivered); }
|
||||
.dash-received { color: var(--received); }
|
||||
.dash-progress { margin-bottom: 2rem; }
|
||||
.progress-bar { display: flex; height: 8px; border-radius: 4px; overflow: hidden; background: var(--border); }
|
||||
.progress-seg { height: 100%; }
|
||||
.progress-pending { background: var(--primary); }
|
||||
.progress-sent { background: var(--sent); }
|
||||
.progress-delivered { background: var(--delivered); }
|
||||
.progress-received { background: var(--received); }
|
||||
.progress-legend { display: flex; gap: 1.25rem; margin-top: .5rem; font-size: .8rem; color: var(--text-muted); }
|
||||
.dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: .25rem; vertical-align: middle; }
|
||||
.dot-pending { background: var(--primary); }
|
||||
.dot-sent { background: var(--sent); }
|
||||
.dot-delivered { background: var(--delivered); }
|
||||
.dot-received { background: var(--received); }
|
||||
|
||||
@@ -14,6 +14,13 @@
|
||||
<span class="dash-stat-label">{{ 'dash.total_cards'|t(user.language) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dash-stat-card">
|
||||
<div class="dash-stat-icon">📋</div>
|
||||
<div class="dash-stat-body">
|
||||
<span class="dash-stat-value dash-pending">{{ pending }}</span>
|
||||
<span class="dash-stat-label">{{ 'status.pending'|t(user.language) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dash-stat-card">
|
||||
<div class="dash-stat-icon">📤</div>
|
||||
<div class="dash-stat-body">
|
||||
@@ -47,11 +54,13 @@
|
||||
{% if total > 0 %}
|
||||
<div class="dash-progress">
|
||||
<div class="progress-bar">
|
||||
<div class="progress-seg progress-pending" style="width:{{ pending_pct }}%"></div>
|
||||
<div class="progress-seg progress-sent" style="width:{{ sent_pct }}%"></div>
|
||||
<div class="progress-seg progress-delivered" style="width:{{ delivered_pct }}%"></div>
|
||||
<div class="progress-seg progress-received" style="width:{{ received_pct }}%"></div>
|
||||
</div>
|
||||
<div class="progress-legend">
|
||||
<span><i class="dot dot-pending"></i>{{ 'status.pending'|t(user.language) }} {{ pending_pct }}%</span>
|
||||
<span><i class="dot dot-sent"></i>{{ 'dash.sent'|t(user.language) }} {{ sent_pct }}%</span>
|
||||
<span><i class="dot dot-delivered"></i>{{ 'dash.delivered'|t(user.language) }} {{ delivered_pct }}%</span>
|
||||
<span><i class="dot dot-received"></i>{{ 'dash.received'|t(user.language) }} {{ received_pct }}%</span>
|
||||
@@ -78,22 +87,22 @@
|
||||
<script>
|
||||
{% if sent_country_stats %}
|
||||
new Chart(document.getElementById('chart-sent'), {
|
||||
type: 'bar',
|
||||
type: 'pie',
|
||||
data: {
|
||||
labels: [{% for code, _ in sent_country_stats %}'{{ code|flag }} {{ code }}',{% endfor %}],
|
||||
datasets: [{ data: [{% for _, count in sent_country_stats %}{{ count }},{% endfor %}], backgroundColor: '#3b82f6', borderRadius: 4, maxBarThickness: 40 }]
|
||||
datasets: [{ data: [{% for _, count in sent_country_stats %}{{ count }},{% endfor %}], backgroundColor: ['#3b82f6','#f59e0b','#10b981','#8b5cf6','#ef4444','#06b6d4','#f97316','#84cc16','#ec4899','#6366f1'] }]
|
||||
},
|
||||
options: { responsive: true, plugins: { legend: { display: false } }, scales: { y: { beginAtZero: true, ticks: { stepSize: 1 } }, x: { grid: { display: false } } } }
|
||||
options: { responsive: true, plugins: { legend: { position: 'bottom', labels: { padding: 12, usePointStyle: true } } } }
|
||||
});
|
||||
{% endif %}
|
||||
{% if received_country_stats %}
|
||||
new Chart(document.getElementById('chart-received'), {
|
||||
type: 'bar',
|
||||
type: 'pie',
|
||||
data: {
|
||||
labels: [{% for code, _ in received_country_stats %}'{{ code|flag }} {{ code }}',{% endfor %}],
|
||||
datasets: [{ data: [{% for _, count in received_country_stats %}{{ count }},{% endfor %}], backgroundColor: '#8b5cf6', borderRadius: 4, maxBarThickness: 40 }]
|
||||
datasets: [{ data: [{% for _, count in received_country_stats %}{{ count }},{% endfor %}], backgroundColor: ['#8b5cf6','#3b82f6','#f59e0b','#10b981','#ef4444','#06b6d4','#f97316','#84cc16','#ec4899','#6366f1'] }]
|
||||
},
|
||||
options: { responsive: true, plugins: { legend: { display: false } }, scales: { y: { beginAtZero: true, ticks: { stepSize: 1 } }, x: { grid: { display: false } } } }
|
||||
options: { responsive: true, plugins: { legend: { position: 'bottom', labels: { padding: 12, usePointStyle: true } } } }
|
||||
});
|
||||
{% endif %}
|
||||
</script>
|
||||
|
||||
@@ -10,13 +10,9 @@
|
||||
<div class="detail-actions">
|
||||
<a href="/postcards/{{ postcard.id }}/edit" class="btn">{{ 'pc_detail.edit'|t(user.language) }}</a>
|
||||
{% if postcard.status == 'pending' %}
|
||||
<form method="post" action="/postcards/{{ postcard.id }}/mark-sent" class="inline">
|
||||
<button type="submit" class="btn btn-primary" onclick="return confirm('{{ 'pc_detail.confirm_sent'|t(user.language) }}')">{{ 'pc_detail.mark_sent'|t(user.language) }}</button>
|
||||
</form>
|
||||
<button type="button" class="btn btn-primary" onclick="if(confirm('{{ 'pc_detail.confirm_sent'|t(user.language) }}'))fetch('/postcards/{{ postcard.id }}/mark-sent',{method:'POST'}).then(function(){location.reload();})">{{ 'pc_detail.mark_sent'|t(user.language) }}</button>
|
||||
{% elif postcard.status == 'sent' %}
|
||||
<form method="post" action="/postcards/{{ postcard.id }}/mark-delivered" class="inline">
|
||||
<button type="submit" class="btn btn-primary" onclick="return confirm('{{ 'pc_detail.confirm_delivered'|t(user.language) }}')">{{ 'pc_detail.mark_delivered'|t(user.language) }}</button>
|
||||
</form>
|
||||
<button type="button" class="btn btn-primary" onclick="if(confirm('{{ 'pc_detail.confirm_delivered'|t(user.language) }}'))fetch('/postcards/{{ postcard.id }}/mark-delivered',{method:'POST'}).then(function(){location.reload();})">{{ 'pc_detail.mark_delivered'|t(user.language) }}</button>
|
||||
{% endif %}
|
||||
<form method="post" action="/postcards/{{ postcard.id }}/delete" class="inline">
|
||||
<button type="submit" class="btn btn-danger" onclick="return confirm('{{ 'pc_detail.confirm_delete'|t(user.language) }}')">{{ 'pc_detail.delete'|t(user.language) }}</button>
|
||||
|
||||
@@ -23,6 +23,9 @@
|
||||
<label>{{ 'pc_form.sender'|t(user.language) }} *</label>
|
||||
<input type="text" name="sender_name" value="{{ postcard.sender_name if is_edit else '' }}">
|
||||
|
||||
<label>{{ 'pc_form.send_time'|t(user.language) }}</label>
|
||||
<input type="date" name="send_time" value="{{ postcard.send_time.strftime('%Y-%m-%d') if is_edit and postcard.send_time else '' }}">
|
||||
|
||||
<label>{{ 'pc_form.receive_time'|t(user.language) }} *</label>
|
||||
<input type="date" name="receive_time" value="{{ postcard.receive_time.strftime('%Y-%m-%d') if is_edit and postcard.receive_time else today }}">
|
||||
|
||||
@@ -55,7 +58,7 @@
|
||||
<div id="fields-sent-delivered">
|
||||
<label>{{ 'pc_form.recipient'|t(user.language) }} *</label>
|
||||
<input type="text" name="recipient_name" value="{{ postcard.recipient_name if is_edit else '' }}">
|
||||
<label>{{ 'pc_form.send_time'|t(user.language) }} *</label>
|
||||
<label id="label-send-time">{{ 'pc_form.send_time'|t(user.language) }} *</label>
|
||||
<input type="date" name="send_time" id="input-send-time" value="{{ postcard.send_time.strftime('%Y-%m-%d') if is_edit and postcard.send_time else (today if is_new else '') }}">
|
||||
<label id="label-arrival">{{ 'pc_form.arrival_time'|t(user.language) }}</label>
|
||||
<input type="date" name="arrival_time" id="input-arrival" value="{{ postcard.arrival_time.strftime('%Y-%m-%d') if is_edit and postcard.arrival_time else '' }}" {% if is_new or (is_edit and edit_status=='sent') %}disabled{% endif %}>
|
||||
@@ -79,21 +82,25 @@
|
||||
</form>
|
||||
<script>
|
||||
{% if is_new and not is_received_form %}
|
||||
// ── 新建寄出:根据日期自动判定状态,可手动覆盖 ──
|
||||
// ── 新建寄出:待寄出时禁用寄出时间,寄出时自动填入 ──
|
||||
(function() {
|
||||
var sendTime = document.getElementById('input-send-time');
|
||||
var select = document.getElementById('status-select');
|
||||
var arrival = document.getElementById('input-arrival');
|
||||
var labelArrival = document.getElementById('label-arrival');
|
||||
var labelSend = document.getElementById('label-send-time');
|
||||
var today = new Date().toISOString().slice(0, 10);
|
||||
if (!sendTime.value) sendTime.value = today;
|
||||
|
||||
var userOverrode = false;
|
||||
|
||||
function autoFromSendTime() {
|
||||
var v = sendTime.value;
|
||||
if (!v) return 'sent';
|
||||
return v > today ? 'pending' : 'sent';
|
||||
function updateSendTime(s) {
|
||||
if (s === 'pending') {
|
||||
sendTime.disabled = true;
|
||||
sendTime.value = '';
|
||||
if (labelSend) labelSend.textContent = '{{ "pc_form.send_time"|t(user.language) }}';
|
||||
} else {
|
||||
sendTime.disabled = false;
|
||||
if (!sendTime.value) sendTime.value = today;
|
||||
if (labelSend) labelSend.textContent = '{{ "pc_form.send_time"|t(user.language) }} *';
|
||||
}
|
||||
}
|
||||
|
||||
function updateArrival(s) {
|
||||
@@ -109,22 +116,29 @@
|
||||
}
|
||||
|
||||
function sync() {
|
||||
var auto = autoFromSendTime();
|
||||
if (!userOverrode) select.value = auto;
|
||||
updateArrival(select.value);
|
||||
updateSendTime(select.value);
|
||||
}
|
||||
|
||||
select.addEventListener('change', function() { userOverrode = true; updateArrival(select.value); });
|
||||
sendTime.addEventListener('change', function() { if (!userOverrode) sync(); });
|
||||
sendTime.addEventListener('input', function() { if (!userOverrode) sync(); });
|
||||
select.addEventListener('change', sync);
|
||||
sync();
|
||||
})();
|
||||
{% elif is_edit and not is_received_form %}
|
||||
// ── 编辑寄出:根据状态切换 arrival 可用性 ──
|
||||
// ── 编辑寄出:根据状态切换 send_time / arrival 可用性 ──
|
||||
function toggleFields() {
|
||||
var s = document.getElementById('status-select').value;
|
||||
var sendTime = document.getElementById('input-send-time');
|
||||
var labelSend = document.getElementById('label-send-time');
|
||||
var arrival = document.getElementById('input-arrival');
|
||||
var labelArrival = document.getElementById('label-arrival');
|
||||
var today = new Date().toISOString().slice(0, 10);
|
||||
if (s === 'pending') {
|
||||
sendTime.disabled = true;
|
||||
if (labelSend) labelSend.textContent = '{{ "pc_form.send_time"|t(user.language) }}';
|
||||
} else {
|
||||
sendTime.disabled = false;
|
||||
if (labelSend) labelSend.textContent = '{{ "pc_form.send_time"|t(user.language) }} *';
|
||||
}
|
||||
if (s === 'delivered') {
|
||||
arrival.disabled = false;
|
||||
labelArrival.textContent = '{{ "pc_form.arrival_time"|t(user.language) }} *';
|
||||
|
||||
@@ -56,13 +56,9 @@
|
||||
{% if pc.notes %}<span class="postcard-row-notes">{{ pc.notes[:50] }}{% if pc.notes|length > 50 %}…{% endif %}</span>{% endif %}
|
||||
<div class="postcard-row-action">
|
||||
{% if pc.status == 'pending' %}
|
||||
<form method="post" action="/postcards/{{ pc.id }}/mark-sent" onclick="event.preventDefault();event.stopPropagation();if(confirm('{{ 'pc_list.confirm_sent'|t(user.language) }}'))this.submit();">
|
||||
<button type="submit" class="btn btn-sm btn-primary">{{ 'pc_list.mark_sent'|t(user.language) }}</button>
|
||||
</form>
|
||||
<button type="button" class="btn btn-sm btn-primary" onclick="event.preventDefault();event.stopPropagation();if(confirm('{{ 'pc_list.confirm_sent'|t(user.language) }}'))fetch('/postcards/{{ pc.id }}/mark-sent',{method:'POST'}).then(function(){location.reload();})">{{ 'pc_list.mark_sent'|t(user.language) }}</button>
|
||||
{% elif pc.status == 'sent' %}
|
||||
<form method="post" action="/postcards/{{ pc.id }}/mark-delivered" onclick="event.preventDefault();event.stopPropagation();if(confirm('{{ 'pc_list.confirm_delivered'|t(user.language) }}'))this.submit();">
|
||||
<button type="submit" class="btn btn-sm btn-primary">{{ 'pc_list.mark_delivered'|t(user.language) }}</button>
|
||||
</form>
|
||||
<button type="button" class="btn btn-sm btn-primary" onclick="event.preventDefault();event.stopPropagation();if(confirm('{{ 'pc_list.confirm_delivered'|t(user.language) }}'))fetch('/postcards/{{ pc.id }}/mark-delivered',{method:'POST'}).then(function(){location.reload();})">{{ 'pc_list.mark_delivered'|t(user.language) }}</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</a>
|
||||
|
||||
Reference in New Issue
Block a user