소스 검색

feat:紧急事件复用

徐福静0235668 1 주 전
부모
커밋
231127ecad

+ 78 - 0
src/app/pages/admin/services/project-auto-case.service.ts

@@ -107,6 +107,12 @@ export class ProjectAutoCaseService {
     let created = 0;
     for (const p of projects) {
       try {
+        // 仅当满足售后归档同步条件时才创建案例
+        const eligible = await this.isProjectEligibleForCase(p);
+        if (!eligible) {
+          continue;
+        }
+
         const ProductQuery = new Parse.Query('Product');
         ProductQuery.equalTo('project', p.toPointer());
         ProductQuery.notEqualTo('isDeleted', true);
@@ -138,6 +144,12 @@ export class ProjectAutoCaseService {
         return { success: true, caseId: data.caseId };
       }
 
+      // 条件校验:售后归档同步条件(评价+支付凭证+复盘),尾款部分支付也允许
+      const eligible = await this.isProjectEligibleForCase(project);
+      if (!eligible) {
+        return { success: false, error: '项目未满足售后归档同步条件(需评价、支付凭证、复盘)' };
+      }
+
       // 加载项目的空间(Product)
       const ProductQuery = new Parse.Query('Product');
       ProductQuery.equalTo('project', project.toPointer());
@@ -593,5 +605,71 @@ export class ProjectAutoCaseService {
       };
     }
   }
+
+  /**
+   * 判断项目是否满足同步到案例库的条件:
+   * - 当前阶段为售后归档/aftercare/尾款结算 或 状态已归档
+   * - 有客户评价(ProjectFeedback)
+   * - 有支付凭证(ProjectPayment[type=final,status=paid] 或 ProjectFile(stage=aftercare,fileType=payment_voucher))
+   * - 有项目复盘(project.data.retrospective)
+   * - 尾款允许部分支付(只要存在任意尾款支付记录或凭证即可)
+   */
+  private async isProjectEligibleForCase(project: any): Promise<boolean> {
+    try {
+      const stage: string = project.get('currentStage') || '';
+      const status: string = project.get('status') || '';
+      const data = project.get('data') || {};
+
+      const inAftercare = ['售后归档', 'aftercare', '尾款结算'].some(s => stage.includes(s)) || status === '已归档';
+      if (!inAftercare) return false;
+
+      // 评价:至少有一条 ProjectFeedback
+      const fbQuery = new Parse.Query('ProjectFeedback');
+      fbQuery.equalTo('project', project.toPointer());
+      fbQuery.notEqualTo('isDeleted', true);
+      const feedbackCount = await fbQuery.count();
+      if (feedbackCount <= 0) return false;
+
+      // 复盘:存在 data.retrospective 对象或标记
+      const hasRetrospective = !!(data.retrospective && (data.retrospective.generated !== false));
+      if (!hasRetrospective) return false;
+
+      // 支付凭证:优先查 ProjectPayment(允许部分支付),再降级查 ProjectFile(payment_voucher)
+      let hasVoucher = false;
+      try {
+        // 允许部分支付:存在任意记录即可;优先匹配已付款
+        const paidQuery = new Parse.Query('ProjectPayment');
+        paidQuery.equalTo('project', project.toPointer());
+        paidQuery.equalTo('type', 'final');
+        paidQuery.notEqualTo('isDeleted', true);
+        paidQuery.equalTo('status', 'paid');
+        const paidCount = await paidQuery.count();
+        if (paidCount > 0) hasVoucher = true;
+        if (!hasVoucher) {
+          const anyQuery = new Parse.Query('ProjectPayment');
+          anyQuery.equalTo('project', project.toPointer());
+          anyQuery.equalTo('type', 'final');
+          anyQuery.notEqualTo('isDeleted', true);
+          const anyCount = await anyQuery.count();
+          hasVoucher = anyCount > 0;
+        }
+      } catch (e) {
+        // 忽略类不存在错误
+      }
+      if (!hasVoucher) {
+        const fileQuery = new Parse.Query('ProjectFile');
+        fileQuery.equalTo('project', project.toPointer());
+        fileQuery.equalTo('stage', 'aftercare');
+        fileQuery.equalTo('fileType', 'payment_voucher');
+        const fileCount = await fileQuery.count();
+        hasVoucher = fileCount > 0;
+      }
+
+      return hasVoucher;
+    } catch (e) {
+      console.warn('⚠️ 案例同步条件检测失败,默认不创建:', e);
+      return false;
+    }
+  }
 }
 

+ 76 - 69
src/app/pages/customer-service/dashboard/dashboard.html

@@ -267,81 +267,88 @@
       </div>
     </div>
     
-    <!-- ⭐ 使用 UrgentEventsPanelComponent,保持原有样式结构 -->
     <div class="tasks-list">
-      @if (urgentEventsList().length === 0 && !loadingUrgentEvents()) {
-      <div class="empty-state">
-        <svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor">
-          <circle cx="12" cy="12" r="10"></circle>
-          <polyline points="12 6 12 12 16 14"></polyline>
-        </svg>
-        <p>暂无紧急事件</p>
-      </div>
+      @if (loadingUrgentEvents()) {
+        <div class="loading-state">
+          <svg class="spinner" viewBox="0 0 50 50">
+            <circle cx="25" cy="25" r="20" fill="none" stroke-width="4"></circle>
+          </svg>
+          <p>计算紧急事件中...</p>
+        </div>
       }
-      
-      <!-- ⭐ 使用紧急事件组件显示紧急任务 -->
-      @for (event of urgentEventsList(); track event.id) {
-      <div class="task-item-enhanced" [class.overdue]="event.overdueDays && event.overdueDays > 0">
-        <div class="task-priority-indicator" 
-             [class.high]="event.urgencyLevel === 'critical' || event.urgencyLevel === 'high'" 
-             [class.medium]="event.urgencyLevel === 'medium'"></div>
-        
-        <div class="task-main-content">
-          <div class="task-header-row">
-            <div class="task-checkbox">
-              <input type="checkbox" (change)="onUrgentEventMarkAsHandled(event)">
-            </div>
-            <div class="task-info">
-              <h4 class="task-title">{{ event.title }}</h4>
-              <div class="task-tags">
-                <span class="tag tag-project">📋 {{ event.projectName }}</span>
-                @if (event.phaseName) {
-                  <span class="tag tag-stage">🔄 {{ event.phaseName }}</span>
-                }
-                @if (event.designerName) {
-                  <span class="tag tag-assignee">👤 {{ event.designerName }}</span>
-                }
+      @if (!loadingUrgentEvents() && urgentEventsList().length === 0) {
+        <div class="empty-state">
+          <svg viewBox="0 0 24 24" width="64" height="64" fill="#d1d5db">
+            <path d="M9 16.2L4.8 12l-1.4 1.4L9 19 21 7l-1.4-1.4L9 16.2z"/>
+          </svg>
+          <p>暂无紧急事件</p>
+          <p class="hint">所有项目时间节点正常 ✅</p>
+        </div>
+      }
+      @if (!loadingUrgentEvents() && urgentEventsList().length > 0) {
+        <div class="todo-list-compact urgent-list">
+          @for (event of urgentEventsList(); track event.id) {
+            <div class="todo-item-compact urgent-item" [attr.data-urgency]="event.urgencyLevel">
+              <div class="urgency-indicator" [attr.data-urgency]="event.urgencyLevel"></div>
+              <div class="task-content">
+                <div class="task-header">
+                  <span class="task-title">{{ event.title }}</span>
+                  <div class="task-badges">
+                    <span class="badge badge-urgency" [attr.data-urgency]="event.urgencyLevel">
+                      @if (event.urgencyLevel === 'critical') { 🔴 紧急 }
+                      @else if (event.urgencyLevel === 'high') { 🟠 重要 }
+                      @else { 🟡 注意 }
+                    </span>
+                    <span class="badge badge-event-type">
+                      @if (event.eventType === 'review') { 对图 }
+                      @else if (event.eventType === 'delivery') { 交付 }
+                      @else if (event.eventType === 'phase_deadline') { {{ event.phaseName }} }
+                    </span>
+                  </div>
+                </div>
+                <div class="task-description">{{ event.description }}</div>
+                <div class="task-meta">
+                  <span class="project-info">
+                    <svg viewBox="0 0 24 24" width="12" height="12" fill="currentColor">
+                      <path d="M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z"/>
+                    </svg>
+                    项目: {{ event.projectName }}
+                  </span>
+                  @if (event.designerName) {
+                    <span class="designer-info">
+                      <svg viewBox="0 0 24 24" width="12" height="12" fill="currentColor">
+                        <path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/>
+                      </svg>
+                      设计师: {{ event.designerName }}
+                    </span>
+                  }
+                </div>
+                <div class="task-footer">
+                  <span class="deadline-info" [class.overdue]="event.overdueDays && event.overdueDays > 0">
+                    <svg viewBox="0 0 24 24" width="12" height="12" fill="currentColor">
+                      <path d="M11.99 2C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zM12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8zm.5-13H11v6l5.25 3.15.75-1.23-4.5-2.67z"/>
+                    </svg>
+                    截止: {{ event.deadline | date:'MM-dd HH:mm' }}
+                    @if (event.overdueDays && event.overdueDays > 0) { <span class="overdue-label">(逾期{{ event.overdueDays }}天)</span> }
+                    @else if (event.overdueDays && event.overdueDays < 0) { <span class="upcoming-label">(还剩{{ -event.overdueDays }}天)</span> }
+                    @else { <span class="today-label">(今天)</span> }
+                  </span>
+                  @if (event.completionRate !== undefined) {
+                    <span class="completion-info">
+                      <svg viewBox="0 0 24 24" width="12" height="12" fill="currentColor">
+                        <path d="M9 16.2L4.8 12l-1.4 1.4L9 19 21 7l-1.4-1.4L9 16.2z"/>
+                      </svg>
+                      完成率: {{ event.completionRate }}%
+                    </span>
+                  }
+                </div>
+              </div>
+              <div class="task-actions">
+                <button class="btn-action btn-view" (click)="onUrgentEventViewProject(event.projectId)">查看项目</button>
               </div>
-            </div>
-          </div>
-          
-          @if (event.description) {
-            <div class="task-description">
-              <p>{{ event.description }}</p>
             </div>
           }
-          
-          <div class="task-meta-row">
-            <div class="task-meta-info">
-              <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor">
-                <circle cx="12" cy="12" r="10"></circle>
-                <polyline points="12 6 12 12 16 14"></polyline>
-              </svg>
-              <span class="task-time" [class.overdue]="event.overdueDays && event.overdueDays > 0">
-                {{ formatDate(event.deadline) }}
-                @if (event.overdueDays && event.overdueDays > 0) {
-                  <span class="overdue-badge">已逾期</span>
-                }
-              </span>
-            </div>
-            <div class="task-priority-badge" 
-                 [class.high]="event.urgencyLevel === 'critical' || event.urgencyLevel === 'high'" 
-                 [class.medium]="event.urgencyLevel === 'medium'">
-              {{ event.urgencyLevel === 'critical' ? '紧急' : event.urgencyLevel === 'high' ? '高优先级' : '中优先级' }}
-            </div>
-          </div>
-        </div>
-        
-        <div class="task-actions-row">
-          <button class="btn-action btn-process" (click)="onUrgentEventViewProject(event.projectId)">
-            <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor">
-              <polyline points="9 11 12 14 22 4"></polyline>
-              <path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"></path>
-            </svg>
-            查看项目
-          </button>
         </div>
-      </div>
       }
     </div>
   </section>

+ 87 - 10
src/app/pages/customer-service/dashboard/dashboard.ts

@@ -1625,6 +1625,8 @@ onSearchInput(event: Event): void {
       const projectQuery = new Parse.Query('Project');
       projectQuery.equalTo('company', cid);
       projectQuery.notEqualTo('isDeleted', true);
+      // 关键:包含 assignee 指针,避免出现 assignee.get 不是函数
+      projectQuery.include('assignee');
       projectQuery.limit(100);
       
       const projects = await projectQuery.find();
@@ -1637,7 +1639,7 @@ onSearchInput(event: Event): void {
         const phaseDeadlines = data.phaseDeadlines || {};
         
         // 获取小图对图时间
-        const reviewDate = project.get('demoday') || project.get('reviewDate');
+        let reviewDate = project.get('demoday') || project.get('reviewDate');
         
         // 获取交付时间
         const deliveryDate = project.get('deadline') || 
@@ -1652,15 +1654,90 @@ onSearchInput(event: Event): void {
         
         // 获取设计师名称
         const assignee = project.get('assignee');
-        const designerName = assignee?.get('name') || assignee?.get('realname') || '未分配';
+        let designerName: string = '未分配';
+        let designerId: string | undefined = undefined;
+        if (assignee) {
+          // 兼容 Parse.Object / 普通对象 / 字符串ID
+          // eslint-disable-next-line @typescript-eslint/no-explicit-any
+          const anyAssignee: any = assignee;
+          designerId = anyAssignee?.id || (typeof anyAssignee === 'string' ? anyAssignee : undefined);
+          if (typeof anyAssignee?.get === 'function') {
+            designerName = anyAssignee.get('name') || anyAssignee.get('realname') || anyAssignee.get('realName') || '未分配';
+          } else if (typeof anyAssignee === 'object') {
+            designerName = anyAssignee.name || anyAssignee.realname || anyAssignee.realName || '未分配';
+          } else if (typeof anyAssignee === 'string') {
+            // 如果后端直接存ID字符串,这里先显示ID占位,避免崩溃
+            designerName = anyAssignee;
+          }
+        }
         
         // 获取空间交付物汇总
         const spaceDeliverableSummary = data.spaceDeliverableSummary;
+
+        // ===== 复用组长端的回退计算逻辑,补齐缺失的 reviewDate 与 phaseDeadlines =====
+        // 计算today,以便生成合理的回退时间
+        const now = new Date();
+        const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
+
+        // 若 reviewDate 缺失且存在交付日期,则将其设置在项目周期60%位置(下午2点)
+        if (!reviewDate && deliveryDate && startDate) {
+          const end = new Date(deliveryDate).getTime();
+          const start = new Date(startDate).getTime();
+          const mid = start + Math.max(0, Math.floor((end - start) * 0.6));
+          const midDate = new Date(mid);
+          midDate.setHours(14, 0, 0, 0);
+          reviewDate = midDate;
+        }
+
+        // 若缺少阶段截止时间,按交付日期向前推算(后期=交付日,渲染=交付-1天,软装=交付-2天,建模=交付-3天)
+        let phaseDeadlinesFallback = phaseDeadlines;
+        if ((!phaseDeadlinesFallback || Object.keys(phaseDeadlinesFallback).length === 0) && deliveryDate && startDate) {
+          const deliveryTime = new Date(deliveryDate).getTime();
+          const postProcessingDeadline = new Date(deliveryTime);
+          const renderingDeadline = new Date(deliveryTime - 1 * 24 * 60 * 60 * 1000);
+          const softDecorDeadline = new Date(deliveryTime - 2 * 24 * 60 * 60 * 1000);
+          const modelingDeadline = new Date(deliveryTime - 3 * 24 * 60 * 60 * 1000);
+
+          phaseDeadlinesFallback = {
+            modeling: {
+              startDate: new Date(startDate),
+              deadline: modelingDeadline,
+              estimatedDays: 1,
+              status: now.getTime() >= modelingDeadline.getTime() && now.getTime() < softDecorDeadline.getTime() ? 'in_progress' :
+                      now.getTime() >= softDecorDeadline.getTime() ? 'completed' : 'not_started',
+              priority: 'high'
+            },
+            softDecor: {
+              startDate: modelingDeadline,
+              deadline: softDecorDeadline,
+              estimatedDays: 1,
+              status: now.getTime() >= softDecorDeadline.getTime() && now.getTime() < renderingDeadline.getTime() ? 'in_progress' :
+                      now.getTime() >= renderingDeadline.getTime() ? 'completed' : 'not_started',
+              priority: 'medium'
+            },
+            rendering: {
+              startDate: softDecorDeadline,
+              deadline: renderingDeadline,
+              estimatedDays: 1,
+              status: now.getTime() >= renderingDeadline.getTime() && now.getTime() < postProcessingDeadline.getTime() ? 'in_progress' :
+                      now.getTime() >= postProcessingDeadline.getTime() ? 'completed' : 'not_started',
+              priority: 'high'
+            },
+            postProcessing: {
+              startDate: renderingDeadline,
+              deadline: postProcessingDeadline,
+              estimatedDays: 1,
+              status: now.getTime() >= postProcessingDeadline.getTime() ? 'completed' :
+                      now.getTime() >= renderingDeadline.getTime() ? 'in_progress' : 'not_started',
+              priority: 'medium'
+            }
+          } as any;
+        }
         
         return {
           projectId: project.id,
           projectName: project.get('name') || project.get('title') || '未命名项目',
-          designerId: assignee?.id,
+          designerId,
           designerName,
           startDate: startDate ? new Date(startDate) : new Date(),
           endDate: deliveryDate ? new Date(deliveryDate) : new Date(),
@@ -1676,7 +1753,7 @@ onSearchInput(event: Event): void {
           priority: 'medium' as const,
           spaceName: '',
           customerName: project.get('customerName') || '',
-          phaseDeadlines,
+          phaseDeadlines: phaseDeadlinesFallback,
           spaceDeliverableSummary
         };
       });
@@ -1701,14 +1778,14 @@ onSearchInput(event: Event): void {
     try {
       // 从 projectTimelineData 中提取数据
       this.projectTimelineData.forEach(project => {
-        // 1. 检查小图对图事件
+        // 1. 检查小图对图事件(与组长端一致)
         if (project.reviewDate) {
           const reviewTime = project.reviewDate.getTime();
           const timeDiff = reviewTime - now.getTime();
           const daysDiff = Math.ceil(timeDiff / oneDayMs);
           
-          // 如果小图对图已经到期或即将到期(1天内),且不在已完成状态
-          if (daysDiff <= 1 && project.currentStage !== 'delivery' && project.currentStage !== '已完成' && project.currentStage !== '交付完成') {
+          // 如果小图对图已经到期或即将到期(1天内),且不在交付完成阶段
+          if (daysDiff <= 1 && project.currentStage !== 'delivery') {
             events.push({
               id: `${project.projectId}-review`,
               title: `小图对图截止`,
@@ -1724,14 +1801,14 @@ onSearchInput(event: Event): void {
           }
         }
         
-        // 2. 检查交付事件
+        // 2. 检查交付事件(与组长端一致)
         if (project.deliveryDate) {
           const deliveryTime = project.deliveryDate.getTime();
           const timeDiff = deliveryTime - now.getTime();
           const daysDiff = Math.ceil(timeDiff / oneDayMs);
           
-          // 如果交付已经到期或即将到期(1天内),且不在已完成状态
-          if (daysDiff <= 1 && project.currentStage !== 'delivery' && project.currentStage !== '已完成' && project.currentStage !== '交付完成') {
+          // 如果交付已经到期或即将到期(1天内),且不在交付完成阶段
+          if (daysDiff <= 1 && project.currentStage !== 'delivery') {
             const summary = project.spaceDeliverableSummary;
             const completionRate = summary?.overallCompletionRate || 0;