簡化郵件發送流程,新增發送記錄功能
Browse files- app/api/post_routes.py +24 -12
- frontend/css/posts.css +88 -0
- frontend/js/pages/posts.js +136 -44
- schemas/supabase-posts-schema.sql +11 -3
app/api/post_routes.py
CHANGED
|
@@ -212,7 +212,7 @@ async def update_post(
|
|
| 212 |
):
|
| 213 |
"""
|
| 214 |
更新貼文內容
|
| 215 |
-
|
| 216 |
"""
|
| 217 |
try:
|
| 218 |
client = supabase_clients.get_license_client()
|
|
@@ -226,11 +226,7 @@ async def update_post(
|
|
| 226 |
|
| 227 |
post = existing.data[0]
|
| 228 |
|
| 229 |
-
#
|
| 230 |
-
if post['status'] != 'draft':
|
| 231 |
-
raise HTTPException(status_code=400, detail="Can only edit draft posts")
|
| 232 |
-
|
| 233 |
-
# 準備更新資料
|
| 234 |
update_data = {}
|
| 235 |
if post_data.subject is not None:
|
| 236 |
update_data['subject'] = post_data.subject
|
|
@@ -599,8 +595,9 @@ async def send_post_email_selective(
|
|
| 599 |
current_user = Depends(verify_admin)
|
| 600 |
):
|
| 601 |
"""
|
| 602 |
-
發送郵件給指定用戶
|
| 603 |
- user_emails: 指定收件人列表,為空則發送給所有啟用用戶
|
|
|
|
| 604 |
"""
|
| 605 |
try:
|
| 606 |
client = supabase_clients.get_license_client()
|
|
@@ -640,26 +637,41 @@ async def send_post_email_selective(
|
|
| 640 |
sent_count = await email_service.send_to_specific_users(
|
| 641 |
subject=post['subject'],
|
| 642 |
content=post['content'],
|
| 643 |
-
recipients=recipients
|
|
|
|
| 644 |
)
|
| 645 |
|
| 646 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 647 |
update_data = {
|
| 648 |
'status': 'sent',
|
| 649 |
'sent_at': datetime.utcnow().isoformat(),
|
| 650 |
'sent_by': current_user.get('email', 'Unknown'),
|
| 651 |
-
'recipient_count': sent_count
|
|
|
|
| 652 |
}
|
| 653 |
|
| 654 |
result = client.table('posts').update(update_data).eq('id', post_id).execute()
|
| 655 |
|
| 656 |
-
logger.info(f"Email sent for post {post_id} to {sent_count} recipients")
|
| 657 |
|
| 658 |
return {
|
| 659 |
"success": True,
|
| 660 |
"message": f"Email sent to {sent_count} recipients",
|
| 661 |
"recipient_count": sent_count,
|
| 662 |
-
"
|
|
|
|
| 663 |
"post": result.data[0] if result.data else None
|
| 664 |
}
|
| 665 |
|
|
|
|
| 212 |
):
|
| 213 |
"""
|
| 214 |
更新貼文內容
|
| 215 |
+
允許任何狀態下編輯(簡化流程)
|
| 216 |
"""
|
| 217 |
try:
|
| 218 |
client = supabase_clients.get_license_client()
|
|
|
|
| 226 |
|
| 227 |
post = existing.data[0]
|
| 228 |
|
| 229 |
+
# 準備更新資料(允許任何狀態編輯)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 230 |
update_data = {}
|
| 231 |
if post_data.subject is not None:
|
| 232 |
update_data['subject'] = post_data.subject
|
|
|
|
| 595 |
current_user = Depends(verify_admin)
|
| 596 |
):
|
| 597 |
"""
|
| 598 |
+
發送郵件給指定用戶(簡化流程:允許任何狀態發送,可重複發送)
|
| 599 |
- user_emails: 指定收件人列表,為空則發送給所有啟用用戶
|
| 600 |
+
- 每次發送會記錄到 email_logs 陣列中
|
| 601 |
"""
|
| 602 |
try:
|
| 603 |
client = supabase_clients.get_license_client()
|
|
|
|
| 637 |
sent_count = await email_service.send_to_specific_users(
|
| 638 |
subject=post['subject'],
|
| 639 |
content=post['content'],
|
| 640 |
+
recipients=recipients,
|
| 641 |
+
product_type=post.get('product_type', 'announcement')
|
| 642 |
)
|
| 643 |
|
| 644 |
+
# 建立新的發送記錄
|
| 645 |
+
send_log = {
|
| 646 |
+
'sent_at': datetime.utcnow().isoformat(),
|
| 647 |
+
'sent_by': current_user.get('email', 'Unknown'),
|
| 648 |
+
'recipient_count': sent_count,
|
| 649 |
+
'recipients_preview': recipients[:5] if len(recipients) > 5 else recipients
|
| 650 |
+
}
|
| 651 |
+
|
| 652 |
+
# 取得現有的 email_logs,如果沒有則初始化為空陣列
|
| 653 |
+
existing_logs = post.get('email_logs') or []
|
| 654 |
+
existing_logs.append(send_log)
|
| 655 |
+
|
| 656 |
+
# 更新貼文狀態(保留 sent 狀態,但允許重複發送)
|
| 657 |
update_data = {
|
| 658 |
'status': 'sent',
|
| 659 |
'sent_at': datetime.utcnow().isoformat(),
|
| 660 |
'sent_by': current_user.get('email', 'Unknown'),
|
| 661 |
+
'recipient_count': sent_count,
|
| 662 |
+
'email_logs': existing_logs
|
| 663 |
}
|
| 664 |
|
| 665 |
result = client.table('posts').update(update_data).eq('id', post_id).execute()
|
| 666 |
|
| 667 |
+
logger.info(f"Email sent for post {post_id} to {sent_count} recipients (total sends: {len(existing_logs)})")
|
| 668 |
|
| 669 |
return {
|
| 670 |
"success": True,
|
| 671 |
"message": f"Email sent to {sent_count} recipients",
|
| 672 |
"recipient_count": sent_count,
|
| 673 |
+
"total_sends": len(existing_logs),
|
| 674 |
+
"recipients": recipients[:10] if len(recipients) > 10 else recipients,
|
| 675 |
"post": result.data[0] if result.data else None
|
| 676 |
}
|
| 677 |
|
frontend/css/posts.css
CHANGED
|
@@ -755,3 +755,91 @@
|
|
| 755 |
font-size: 12px;
|
| 756 |
}
|
| 757 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 755 |
font-size: 12px;
|
| 756 |
}
|
| 757 |
}
|
| 758 |
+
|
| 759 |
+
/* ==================== 郵件發送記錄 ==================== */
|
| 760 |
+
|
| 761 |
+
.email-logs-container {
|
| 762 |
+
max-height: 400px;
|
| 763 |
+
overflow-y: auto;
|
| 764 |
+
}
|
| 765 |
+
|
| 766 |
+
.email-logs-container .post-info {
|
| 767 |
+
padding: 12px;
|
| 768 |
+
background: var(--bg-secondary);
|
| 769 |
+
border-radius: 8px;
|
| 770 |
+
font-size: 14px;
|
| 771 |
+
color: var(--text-primary);
|
| 772 |
+
}
|
| 773 |
+
|
| 774 |
+
.logs-list {
|
| 775 |
+
display: flex;
|
| 776 |
+
flex-direction: column;
|
| 777 |
+
gap: 12px;
|
| 778 |
+
}
|
| 779 |
+
|
| 780 |
+
.email-log-item {
|
| 781 |
+
padding: 12px 16px;
|
| 782 |
+
background: var(--bg-secondary);
|
| 783 |
+
border: 1px solid var(--border-color);
|
| 784 |
+
border-radius: 8px;
|
| 785 |
+
transition: all 0.2s ease;
|
| 786 |
+
}
|
| 787 |
+
|
| 788 |
+
.email-log-item:hover {
|
| 789 |
+
border-color: var(--accent-blue);
|
| 790 |
+
}
|
| 791 |
+
|
| 792 |
+
.email-log-item .log-header {
|
| 793 |
+
display: flex;
|
| 794 |
+
justify-content: space-between;
|
| 795 |
+
align-items: center;
|
| 796 |
+
margin-bottom: 8px;
|
| 797 |
+
}
|
| 798 |
+
|
| 799 |
+
.email-log-item .log-number {
|
| 800 |
+
font-size: 14px;
|
| 801 |
+
font-weight: 600;
|
| 802 |
+
color: var(--accent-blue);
|
| 803 |
+
}
|
| 804 |
+
|
| 805 |
+
.email-log-item .log-time {
|
| 806 |
+
font-size: 12px;
|
| 807 |
+
color: var(--text-secondary);
|
| 808 |
+
}
|
| 809 |
+
|
| 810 |
+
.email-log-item .log-time i {
|
| 811 |
+
margin-right: 4px;
|
| 812 |
+
}
|
| 813 |
+
|
| 814 |
+
.email-log-item .log-details {
|
| 815 |
+
display: flex;
|
| 816 |
+
flex-direction: column;
|
| 817 |
+
gap: 4px;
|
| 818 |
+
font-size: 13px;
|
| 819 |
+
color: var(--text-secondary);
|
| 820 |
+
}
|
| 821 |
+
|
| 822 |
+
.email-log-item .log-details i {
|
| 823 |
+
margin-right: 6px;
|
| 824 |
+
width: 14px;
|
| 825 |
+
text-align: center;
|
| 826 |
+
}
|
| 827 |
+
|
| 828 |
+
/* 發送記錄點擊提示 */
|
| 829 |
+
.sent-info {
|
| 830 |
+
font-size: 11px;
|
| 831 |
+
color: var(--accent-blue);
|
| 832 |
+
margin-right: 8px;
|
| 833 |
+
padding: 2px 6px;
|
| 834 |
+
background: rgba(88, 166, 255, 0.1);
|
| 835 |
+
border-radius: 4px;
|
| 836 |
+
transition: all 0.2s ease;
|
| 837 |
+
}
|
| 838 |
+
|
| 839 |
+
.sent-info:hover {
|
| 840 |
+
background: rgba(88, 166, 255, 0.2);
|
| 841 |
+
}
|
| 842 |
+
|
| 843 |
+
.sent-info i {
|
| 844 |
+
margin-right: 4px;
|
| 845 |
+
}
|
frontend/js/pages/posts.js
CHANGED
|
@@ -333,41 +333,27 @@ class PostsPage {
|
|
| 333 |
? post.selected_emails.length
|
| 334 |
: '全部';
|
| 335 |
|
| 336 |
-
//
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
|
| 341 |
-
<
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
|
| 349 |
-
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
<i class="fas fa-trash"></i>
|
| 358 |
-
</button>
|
| 359 |
-
`;
|
| 360 |
-
} else if (post.status === 'sent') {
|
| 361 |
-
const sentInfo = post.sent_at
|
| 362 |
-
? `<small class="sent-info">已發送 ${post.recipient_count || 0} 封郵件</small>`
|
| 363 |
-
: '';
|
| 364 |
-
actionButtons = `
|
| 365 |
-
${sentInfo}
|
| 366 |
-
<button class="btn btn-sm btn-secondary" onclick="postsPage.viewPost('${post.id}')" title="查看">
|
| 367 |
-
<i class="fas fa-eye"></i>
|
| 368 |
-
</button>
|
| 369 |
-
`;
|
| 370 |
-
}
|
| 371 |
|
| 372 |
// 版本顯示
|
| 373 |
const versionDisplay = post.product_type === 'announcement'
|
|
@@ -460,7 +446,7 @@ class PostsPage {
|
|
| 460 |
}
|
| 461 |
}
|
| 462 |
|
| 463 |
-
// 發送郵件
|
| 464 |
async sendEmail(postId) {
|
| 465 |
const post = this.posts.find(p => p.id === postId);
|
| 466 |
if (!post) return;
|
|
@@ -480,11 +466,17 @@ class PostsPage {
|
|
| 480 |
: post.product_type === 'autocad' ? 'AutoCAD'
|
| 481 |
: '公告';
|
| 482 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 483 |
const confirmed = confirm(
|
| 484 |
`確定要發送郵件通知嗎?\n\n` +
|
| 485 |
`主旨:${post.subject}\n` +
|
| 486 |
`類型:${productName}\n` +
|
| 487 |
-
`收件人:${recipientInfo}
|
|
|
|
| 488 |
`發送後無法撤回。`
|
| 489 |
);
|
| 490 |
if (!confirmed) return;
|
|
@@ -492,7 +484,8 @@ class PostsPage {
|
|
| 492 |
try {
|
| 493 |
// 使用貼文中儲存的收件人列表
|
| 494 |
const result = await api.sendPostEmailSelective(postId, post.selected_emails);
|
| 495 |
-
|
|
|
|
| 496 |
await this.loadPosts();
|
| 497 |
} catch (error) {
|
| 498 |
Utils.handleError(error, '發送郵件時');
|
|
@@ -546,6 +539,110 @@ class PostsPage {
|
|
| 546 |
`;
|
| 547 |
}
|
| 548 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 549 |
// 刪除貼文
|
| 550 |
async deletePost(postId) {
|
| 551 |
const post = this.posts.find(p => p.id === postId);
|
|
@@ -724,16 +821,11 @@ class PostsPage {
|
|
| 724 |
|
| 725 |
// ==================== 更新編輯功能 ====================
|
| 726 |
|
| 727 |
-
// 編輯貼文(
|
| 728 |
async editPost(postId) {
|
| 729 |
const post = this.posts.find(p => p.id === postId);
|
| 730 |
if (!post) return;
|
| 731 |
|
| 732 |
-
if (post.status !== 'draft') {
|
| 733 |
-
Utils.showError('無法編輯', '只能編輯草稿狀態的貼文');
|
| 734 |
-
return;
|
| 735 |
-
}
|
| 736 |
-
|
| 737 |
this.currentEditPost = post;
|
| 738 |
document.getElementById('editSubject').value = post.subject || '';
|
| 739 |
document.getElementById('editContent').value = post.content || '';
|
|
|
|
| 333 |
? post.selected_emails.length
|
| 334 |
: '全部';
|
| 335 |
|
| 336 |
+
// 統一的操作按鈕(簡化流程:所有狀態都可以編輯和發送)
|
| 337 |
+
const emailLogs = post.email_logs || [];
|
| 338 |
+
const sendCount = emailLogs.length;
|
| 339 |
+
const sentInfo = sendCount > 0
|
| 340 |
+
? `<small class="sent-info" onclick="postsPage.viewEmailLogs('${post.id}')" style="cursor:pointer;" title="點擊查看發送記錄">
|
| 341 |
+
<i class="fas fa-history"></i> 已發送 ${sendCount} 次
|
| 342 |
+
</small>`
|
| 343 |
+
: '';
|
| 344 |
+
|
| 345 |
+
const actionButtons = `
|
| 346 |
+
${sentInfo}
|
| 347 |
+
<button class="btn btn-sm btn-secondary" onclick="postsPage.editPost('${post.id}')" title="編輯">
|
| 348 |
+
<i class="fas fa-edit"></i>
|
| 349 |
+
</button>
|
| 350 |
+
<button class="btn btn-sm btn-primary" onclick="postsPage.sendEmail('${post.id}')" title="發送郵件">
|
| 351 |
+
<i class="fas fa-envelope"></i>
|
| 352 |
+
</button>
|
| 353 |
+
<button class="btn btn-sm btn-danger" onclick="postsPage.deletePost('${post.id}')" title="刪除">
|
| 354 |
+
<i class="fas fa-trash"></i>
|
| 355 |
+
</button>
|
| 356 |
+
`;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 357 |
|
| 358 |
// 版本顯示
|
| 359 |
const versionDisplay = post.product_type === 'announcement'
|
|
|
|
| 446 |
}
|
| 447 |
}
|
| 448 |
|
| 449 |
+
// 發送郵件(簡化流程:可重複發送)
|
| 450 |
async sendEmail(postId) {
|
| 451 |
const post = this.posts.find(p => p.id === postId);
|
| 452 |
if (!post) return;
|
|
|
|
| 466 |
: post.product_type === 'autocad' ? 'AutoCAD'
|
| 467 |
: '公告';
|
| 468 |
|
| 469 |
+
const emailLogs = post.email_logs || [];
|
| 470 |
+
const resendWarning = emailLogs.length > 0
|
| 471 |
+
? `\n⚠️ 此貼文已發送過 ${emailLogs.length} 次`
|
| 472 |
+
: '';
|
| 473 |
+
|
| 474 |
const confirmed = confirm(
|
| 475 |
`確定要發送郵件通知嗎?\n\n` +
|
| 476 |
`主旨:${post.subject}\n` +
|
| 477 |
`類型:${productName}\n` +
|
| 478 |
+
`收件人:${recipientInfo}` +
|
| 479 |
+
`${resendWarning}\n\n` +
|
| 480 |
`發送後無法撤回。`
|
| 481 |
);
|
| 482 |
if (!confirmed) return;
|
|
|
|
| 484 |
try {
|
| 485 |
// 使用貼文中儲存的收件人列表
|
| 486 |
const result = await api.sendPostEmailSelective(postId, post.selected_emails);
|
| 487 |
+
const totalSends = result.total_sends || (emailLogs.length + 1);
|
| 488 |
+
Utils.showSuccess(`郵件已發送給 ${result.recipient_count || 0} 位用戶(第 ${totalSends} 次發送)`);
|
| 489 |
await this.loadPosts();
|
| 490 |
} catch (error) {
|
| 491 |
Utils.handleError(error, '發送郵件時');
|
|
|
|
| 539 |
`;
|
| 540 |
}
|
| 541 |
|
| 542 |
+
// 查看郵件發送記錄
|
| 543 |
+
viewEmailLogs(postId) {
|
| 544 |
+
const post = this.posts.find(p => p.id === postId);
|
| 545 |
+
if (!post) return;
|
| 546 |
+
|
| 547 |
+
const emailLogs = post.email_logs || [];
|
| 548 |
+
if (emailLogs.length === 0) {
|
| 549 |
+
Utils.showInfo('發送記錄', '此貼文尚未發送過郵件');
|
| 550 |
+
return;
|
| 551 |
+
}
|
| 552 |
+
|
| 553 |
+
// 建立發���記錄 HTML
|
| 554 |
+
const logsHtml = emailLogs.map((log, index) => {
|
| 555 |
+
const sentAt = new Date(log.sent_at).toLocaleString('zh-TW');
|
| 556 |
+
const recipients = log.recipients_preview || [];
|
| 557 |
+
const recipientText = recipients.length > 0
|
| 558 |
+
? recipients.join(', ') + (log.recipient_count > recipients.length ? ` 等 ${log.recipient_count} 人` : '')
|
| 559 |
+
: `${log.recipient_count} 位用戶`;
|
| 560 |
+
|
| 561 |
+
return `
|
| 562 |
+
<div class="email-log-item">
|
| 563 |
+
<div class="log-header">
|
| 564 |
+
<span class="log-number">#${emailLogs.length - index}</span>
|
| 565 |
+
<span class="log-time"><i class="fas fa-clock"></i> ${sentAt}</span>
|
| 566 |
+
</div>
|
| 567 |
+
<div class="log-details">
|
| 568 |
+
<div><i class="fas fa-user"></i> 發送者: ${log.sent_by || '未知'}</div>
|
| 569 |
+
<div><i class="fas fa-users"></i> 收件人: ${recipientText}</div>
|
| 570 |
+
</div>
|
| 571 |
+
</div>
|
| 572 |
+
`;
|
| 573 |
+
}).reverse().join('');
|
| 574 |
+
|
| 575 |
+
// 使用編輯 Modal 顯示發送記錄
|
| 576 |
+
const modal = document.getElementById('editPostModal');
|
| 577 |
+
modal.querySelector('.modal-title').textContent = '郵件發送記錄';
|
| 578 |
+
modal.querySelector('.modal-body').innerHTML = `
|
| 579 |
+
<div class="email-logs-container">
|
| 580 |
+
<div class="post-info mb-3">
|
| 581 |
+
<strong>主旨:</strong> ${this.escapeHtml(post.subject || '(未設定)')}
|
| 582 |
+
</div>
|
| 583 |
+
<div class="logs-list">
|
| 584 |
+
${logsHtml}
|
| 585 |
+
</div>
|
| 586 |
+
</div>
|
| 587 |
+
`;
|
| 588 |
+
modal.querySelector('.modal-footer').innerHTML = `
|
| 589 |
+
<button class="btn btn-secondary" onclick="postsPage.closeEmailLogsModal()">關閉</button>
|
| 590 |
+
`;
|
| 591 |
+
|
| 592 |
+
modal.classList.add('active');
|
| 593 |
+
modal.style.display = 'flex';
|
| 594 |
+
modal.style.opacity = '1';
|
| 595 |
+
modal.style.visibility = 'visible';
|
| 596 |
+
}
|
| 597 |
+
|
| 598 |
+
closeEmailLogsModal() {
|
| 599 |
+
const modal = document.getElementById('editPostModal');
|
| 600 |
+
modal.classList.remove('active');
|
| 601 |
+
modal.style.display = 'none';
|
| 602 |
+
modal.style.opacity = '';
|
| 603 |
+
modal.style.visibility = '';
|
| 604 |
+
|
| 605 |
+
// 恢復原本的 modal 內容
|
| 606 |
+
modal.querySelector('.modal-title').textContent = '編輯貼文';
|
| 607 |
+
modal.querySelector('.modal-body').innerHTML = `
|
| 608 |
+
<form id="editPostForm">
|
| 609 |
+
<div class="form-group">
|
| 610 |
+
<label class="form-label">主旨</label>
|
| 611 |
+
<input type="text" id="editSubject" class="form-input" placeholder="輸入郵件主旨..." required>
|
| 612 |
+
</div>
|
| 613 |
+
<div class="form-group">
|
| 614 |
+
<label class="form-label">內容</label>
|
| 615 |
+
<textarea id="editContent" class="form-textarea" rows="8" placeholder="輸入貼文內容..." required></textarea>
|
| 616 |
+
<small class="form-hint">可使用 {download_link} 變數,系統會自動替換為下載連結</small>
|
| 617 |
+
</div>
|
| 618 |
+
<div class="form-group">
|
| 619 |
+
<label class="form-label">收件人</label>
|
| 620 |
+
<div class="recipient-controls mb-2">
|
| 621 |
+
<button type="button" class="btn btn-sm btn-secondary" onclick="postsPage.selectAllUsers()">
|
| 622 |
+
<i class="fas fa-check-double"></i> 全選
|
| 623 |
+
</button>
|
| 624 |
+
<button type="button" class="btn btn-sm btn-secondary" onclick="postsPage.deselectAllUsers()">
|
| 625 |
+
<i class="fas fa-times"></i> 取消全選
|
| 626 |
+
</button>
|
| 627 |
+
<span class="selected-count" id="selectedCount">已選擇: 0 位用戶</span>
|
| 628 |
+
</div>
|
| 629 |
+
<div class="recipient-list" id="recipientList">
|
| 630 |
+
<div class="text-center p-3">
|
| 631 |
+
<div class="spinner-sm"></div>
|
| 632 |
+
<small>載入用戶列表...</small>
|
| 633 |
+
</div>
|
| 634 |
+
</div>
|
| 635 |
+
</div>
|
| 636 |
+
</form>
|
| 637 |
+
`;
|
| 638 |
+
modal.querySelector('.modal-footer').innerHTML = `
|
| 639 |
+
<button class="btn btn-secondary" onclick="postsPage.closeEditModal()">取消</button>
|
| 640 |
+
<button class="btn btn-primary" onclick="postsPage.savePost()">
|
| 641 |
+
<i class="fas fa-save"></i> 儲存
|
| 642 |
+
</button>
|
| 643 |
+
`;
|
| 644 |
+
}
|
| 645 |
+
|
| 646 |
// 刪除貼文
|
| 647 |
async deletePost(postId) {
|
| 648 |
const post = this.posts.find(p => p.id === postId);
|
|
|
|
| 821 |
|
| 822 |
// ==================== 更新編輯功能 ====================
|
| 823 |
|
| 824 |
+
// 編輯貼文(允許任何狀態編輯)
|
| 825 |
async editPost(postId) {
|
| 826 |
const post = this.posts.find(p => p.id === postId);
|
| 827 |
if (!post) return;
|
| 828 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 829 |
this.currentEditPost = post;
|
| 830 |
document.getElementById('editSubject').value = post.subject || '';
|
| 831 |
document.getElementById('editContent').value = post.content || '';
|
schemas/supabase-posts-schema.sql
CHANGED
|
@@ -24,6 +24,7 @@ CREATE TABLE IF NOT EXISTS posts (
|
|
| 24 |
sent_at TIMESTAMP WITH TIME ZONE,
|
| 25 |
sent_by VARCHAR(255),
|
| 26 |
recipient_count INTEGER DEFAULT 0,
|
|
|
|
| 27 |
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
| 28 |
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
| 29 |
);
|
|
@@ -78,6 +79,13 @@ COMMENT ON COLUMN posts.content IS '郵件內容';
|
|
| 78 |
COMMENT ON COLUMN posts.status IS '狀態: draft(草稿) -> published(已發布) -> sent(已發送郵件)';
|
| 79 |
COMMENT ON COLUMN posts.is_deleted IS '軟刪除標記';
|
| 80 |
COMMENT ON COLUMN posts.selected_emails IS '選定的收件人郵箱陣列,NULL 表示發送給所有用戶';
|
| 81 |
-
COMMENT ON COLUMN posts.sent_at IS '郵件發送時間';
|
| 82 |
-
COMMENT ON COLUMN posts.sent_by IS '發送者';
|
| 83 |
-
COMMENT ON COLUMN posts.recipient_count IS '收件人數量';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
sent_at TIMESTAMP WITH TIME ZONE,
|
| 25 |
sent_by VARCHAR(255),
|
| 26 |
recipient_count INTEGER DEFAULT 0,
|
| 27 |
+
email_logs JSONB DEFAULT '[]'::jsonb,
|
| 28 |
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
| 29 |
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
| 30 |
);
|
|
|
|
| 79 |
COMMENT ON COLUMN posts.status IS '狀態: draft(草稿) -> published(已發布) -> sent(已發送郵件)';
|
| 80 |
COMMENT ON COLUMN posts.is_deleted IS '軟刪除標記';
|
| 81 |
COMMENT ON COLUMN posts.selected_emails IS '選定的收件人郵箱陣列,NULL 表示發送給所有用戶';
|
| 82 |
+
COMMENT ON COLUMN posts.sent_at IS '最後一次郵件發送時間';
|
| 83 |
+
COMMENT ON COLUMN posts.sent_by IS '最後一次發送者';
|
| 84 |
+
COMMENT ON COLUMN posts.recipient_count IS '最後一次收件人數量';
|
| 85 |
+
COMMENT ON COLUMN posts.email_logs IS '郵件發送記錄陣列,每次發送都會新增一筆記錄';
|
| 86 |
+
|
| 87 |
+
-- ============================================
|
| 88 |
+
-- Migration: Add email_logs column (for existing installations)
|
| 89 |
+
-- ============================================
|
| 90 |
+
-- Run this ALTER TABLE if the posts table already exists:
|
| 91 |
+
-- ALTER TABLE posts ADD COLUMN IF NOT EXISTS email_logs JSONB DEFAULT '[]'::jsonb;
|