KyrosDev commited on
Commit
0bbbd93
·
1 Parent(s): 1469c18

簡化郵件發送流程,新增發送記錄功能

Browse files
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
- "recipients": recipients[:10] if len(recipients) > 10 else recipients, # 只回傳前 10 個
 
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
- let actionButtons = '';
338
-
339
- if (post.status === 'draft') {
340
- actionButtons = `
341
- <button class="btn btn-sm btn-secondary" onclick="postsPage.editPost('${post.id}')" title="編輯">
342
- <i class="fas fa-edit"></i>
343
- </button>
344
- <button class="btn btn-sm btn-primary" onclick="postsPage.publishPost('${post.id}')" title="發布">
345
- <i class="fas fa-paper-plane"></i>
346
- </button>
347
- <button class="btn btn-sm btn-danger" onclick="postsPage.deletePost('${post.id}')" title="刪除">
348
- <i class="fas fa-trash"></i>
349
- </button>
350
- `;
351
- } else if (post.status === 'published') {
352
- actionButtons = `
353
- <button class="btn btn-sm btn-primary" onclick="postsPage.sendEmail('${post.id}')" title="發送郵件">
354
- <i class="fas fa-envelope"></i> 發送郵件
355
- </button>
356
- <button class="btn btn-sm btn-danger" onclick="postsPage.deletePost('${post.id}')" title="刪除">
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}\n\n` +
 
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
- Utils.showSuccess(`郵件已發送給 ${result.recipient_count || 0} 位用戶`);
 
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;