Pārlūkot izejas kodu

feat: enhance project and designer services with department integration and default assignee logic

0235711 3 stundas atpakaļ
vecāks
revīzija
63b12e3aad

+ 457 - 0
docs/task/2025102220-fix-unassigned-projects-complete.md

@@ -0,0 +1,457 @@
+# 修复"未分配"问题 - 完整显示所有组员
+
+**日期**: 2025-10-24  
+**问题**: 组长端工作量负载概览图未显示全部组员,且项目负责人显示"未分配"
+
+---
+
+## 🔴 问题根源
+
+### 问题1: 项目显示"未分配"
+
+**原因**: 数据库中 `Project` 表的 `assignee` 字段为空或null
+
+**数据流**:
+```
+Parse数据库
+  ↓
+Project表.assignee字段 = null
+  ↓
+transformProject() 转换
+  ↓
+designerName = assignee?.get('name') || '未分配'
+  ↓
+前端显示"未分配"
+```
+
+### 问题2: 甘特图不显示所有组员
+
+**原因**: 只统计了有项目的设计师,没有项目的组员不显示
+
+**旧逻辑** (错误):
+```typescript
+// ❌ 只从项目中提取设计师
+const assigned = this.filteredProjects.filter(p => !!p.designerName);
+const designers = Array.from(new Set(assigned.map(p => p.designerName)));
+
+// 结果:只显示有项目的设计师,且包含"未分配"
+// 例如:['未分配', '张三', '李四']
+```
+
+**问题**:
+- ❌ 没有项目的设计师不显示(如"王五")
+- ❌ "未分配"也被当作设计师显示
+- ❌ 无法看到团队全貌
+
+---
+
+## ✅ 解决方案
+
+### 修改1: 甘特图使用真实设计师列表
+
+**文件**: `src/app/pages/team-leader/dashboard/dashboard.ts`
+
+**位置**: 第1869-1900行(updateWorkloadGantt方法内)
+
+**修改内容**:
+
+```typescript
+// ✅ 获取所有真实设计师(优先使用realDesigners)
+let designers: string[] = [];
+
+if (this.realDesigners && this.realDesigners.length > 0) {
+  // 使用真实的设计师列表
+  designers = this.realDesigners.map(d => d.name);
+  console.log('✅ 使用真实设计师列表:', designers.length, '人');
+} else {
+  // 降级:从已分配的项目中提取设计师(过滤掉"未分配")
+  const assigned = this.filteredProjects.filter(p => p.designerName && p.designerName !== '未分配');
+  designers = Array.from(new Set(assigned.map(p => p.designerName)));
+  console.warn('⚠️ 使用项目中提取的设计师列表:', designers.length, '人');
+}
+
+if (designers.length === 0) {
+  // 没有设计师数据,显示空状态
+  const emptyOption = {
+    title: {
+      text: '暂无组员数据',
+      subtext: '请先在系统中添加设计师(组员角色)',
+      left: 'center',
+      top: 'center',
+      textStyle: { fontSize: 16, color: '#9ca3af' },
+      subtextStyle: { fontSize: 13, color: '#d1d5db' }
+    }
+  };
+  this.workloadGanttChart.setOption(emptyOption, true);
+  return;
+}
+
+// 获取所有已分配的项目(过滤掉"未分配")
+const assigned = this.filteredProjects.filter(p => p.designerName && p.designerName !== '未分配');
+```
+
+**改进点**:
+
+1. **优先使用真实设计师列表** (`this.realDesigners`)
+   - 从Parse数据库查询的所有组员
+   - 不依赖项目数据
+   - 包含没有项目的设计师
+
+2. **过滤"未分配"**
+   - `p.designerName !== '未分配'`
+   - 不将"未分配"当作设计师显示
+
+3. **友好的空状态提示**
+   - 明确提示"暂无组员数据"
+   - 给出操作建议
+
+---
+
+## 📊 效果对比
+
+### 修改前
+
+**甘特图显示**:
+```
+未分配 (5个项目) ❌ 不应该显示
+张三 (3个项目)
+李四 (2个项目)
+```
+
+**问题**:
+- ❌ "未分配"占据第一行
+- ❌ "王五"(无项目)不显示
+- ❌ 无法看到团队完整负载
+
+### 修改后
+
+**甘特图显示**:
+```
+张三 (3个项目)  ✅ 有项目,显示
+李四 (2个项目)  ✅ 有项目,显示
+王五 (0个项目)  ✅ 无项目,也显示
+```
+
+**优点**:
+- ✅ 显示所有真实组员
+- ✅ 不显示"未分配"
+- ✅ 清晰看到谁有空闲
+- ✅ 便于分配新项目
+
+---
+
+## 🔧 如何分配项目给组员
+
+### 方式1: 使用DesignerService的assignProject方法
+
+**代码示例**:
+
+```typescript
+// 在组长端调用
+async assignProjectToDesigner(projectId: string, designerId: string) {
+  try {
+    const success = await this.designerService.assignProject(projectId, designerId);
+    
+    if (success) {
+      console.log('✅ 项目分配成功');
+      // 刷新项目列表
+      await this.loadProjects();
+      // 更新甘特图
+      this.updateWorkloadGantt();
+    } else {
+      console.error('❌ 项目分配失败');
+    }
+  } catch (error) {
+    console.error('❌ 分配出错:', error);
+  }
+}
+```
+
+**DesignerService中的实现** (已有):
+
+```typescript
+// src/app/pages/team-leader/services/designer.service.ts
+async assignProject(projectId: string, designerId: string): Promise<boolean> {
+  const Parse = await this.ensureParse();
+  if (!Parse) return false;
+  
+  try {
+    const project = Parse.Object.extend('Project').createWithoutData(projectId);
+    const designer = Parse.Object.extend('Profile').createWithoutData(designerId);
+    
+    project.set('assignee', designer);
+    project.set('status', '进行中');
+    
+    await project.save();
+    console.log('✅ 项目分配成功');
+    return true;
+  } catch (error) {
+    console.error('❌ 项目分配失败:', error);
+    return false;
+  }
+}
+```
+
+### 方式2: 在Parse Dashboard手动分配
+
+**步骤**:
+
+1. **打开Parse Dashboard**
+   - 访问Parse管理后台
+   - 进入数据库
+
+2. **找到Project表**
+   - 点击 `Project` 表
+   - 找到需要分配的项目
+
+3. **设置assignee字段**
+   ```
+   字段: assignee
+   类型: Pointer<Profile>
+   值: 选择设计师的Profile记录
+   ```
+
+4. **设置status**
+   ```
+   字段: status
+   类型: String
+   值: '进行中'
+   ```
+
+5. **保存**
+   - 点击保存按钮
+   - 刷新组长端页面查看
+
+### 方式3: 使用智能推荐(快速分配)
+
+**组长端操作**:
+
+1. 在项目卡片上点击"🤖 智能推荐"按钮
+2. 系统自动推荐最合适的设计师
+3. 确认后自动分配
+
+**智能推荐逻辑**:
+- 风格匹配度
+- 当前负载
+- 历史表现
+- 紧急适配度
+
+---
+
+## 🧪 测试验证
+
+### 测试步骤
+
+1. **准备测试数据**
+   
+   **设计师数据** (Profile表):
+   ```javascript
+   // 应该有至少3个组员
+   设计师1: { name: '张三', roleName: '组员' }
+   设计师2: { name: '李四', roleName: '组员' }
+   设计师3: { name: '王五', roleName: '组员' }
+   ```
+
+   **项目数据** (Project表):
+   ```javascript
+   项目1: { title: '项目A', assignee: 指向张三 }
+   项目2: { title: '项目B', assignee: 指向张三 }
+   项目3: { title: '项目C', assignee: 指向李四 }
+   项目4: { title: '项目D', assignee: null }  // 未分配
+   ```
+
+2. **访问组长端Dashboard**
+   ```
+   http://localhost:4200/team-leader/dashboard
+   ```
+
+3. **查看工作量负载概览图**
+
+   **预期显示**:
+   ```
+   张三 (2个项目) 🔥  ← 有项目,显示项目数
+   李四 (1个项目) ✓   ← 有项目,显示项目数
+   王五 (0个项目) ○   ← 无项目,也显示
+   ```
+
+   **不应显示**:
+   ```
+   ❌ 未分配 (1个项目)  ← 不应该出现
+   ```
+
+4. **验证控制台日志**
+   ```javascript
+   ✅ 使用真实设计师列表: 3 人
+   ✅ 加载设计师数据成功: 3 人
+   ✅ 加载真实项目数据成功: 4 个项目
+   ```
+
+5. **测试项目分配**
+   
+   **在浏览器控制台执行**:
+   ```javascript
+   // 获取组件实例
+   const dashboard = angular.element(document.querySelector('app-dashboard')).scope();
+   
+   // 分配项目D给王五
+   await dashboard.designerService.assignProject('项目D的ID', '王五的ID');
+   
+   // 刷新页面
+   location.reload();
+   ```
+
+   **预期结果**:
+   - 项目D的负责人从"未分配"变为"王五"
+   - 甘特图中王五显示 (1个项目)
+
+---
+
+## 📋 数据库检查清单
+
+### 检查Profile表
+
+```sql
+-- 查询所有组员
+SELECT objectId, name, roleName, company 
+FROM Profile 
+WHERE roleName = '组员' 
+AND isDeleted != true;
+
+-- 预期:应该看到多个组员记录
+```
+
+### 检查Project表
+
+```sql
+-- 查询未分配的项目
+SELECT objectId, title, assignee, status 
+FROM Project 
+WHERE (assignee IS NULL OR assignee = '')
+AND isDeleted != true;
+
+-- 预期:未分配的项目列表
+```
+
+### 修复未分配的项目
+
+```sql
+-- 方式1: 分配给指定设计师
+UPDATE Project 
+SET assignee = {Profile的Pointer},
+    status = '进行中'
+WHERE objectId = '项目ID';
+
+-- 方式2: 批量分配(循环分配给不同设计师)
+-- 需要在应用层或脚本中处理
+```
+
+---
+
+## 🎯 核心改进
+
+### 1. 数据源优化
+
+```typescript
+// ❌ 旧方式:从项目中提取
+const designers = projects.map(p => p.designerName);
+
+// ✅ 新方式:使用真实设计师数据
+const designers = this.realDesigners.map(d => d.name);
+```
+
+### 2. 过滤逻辑
+
+```typescript
+// ✅ 过滤"未分配"
+const assigned = this.filteredProjects.filter(
+  p => p.designerName && p.designerName !== '未分配'
+);
+```
+
+### 3. 降级策略
+
+```typescript
+if (this.realDesigners && this.realDesigners.length > 0) {
+  // 优先使用真实数据
+  designers = this.realDesigners.map(d => d.name);
+} else {
+  // 降级到从项目提取(开发模式)
+  designers = projects.map(p => p.designerName);
+}
+```
+
+---
+
+## 📖 相关文件
+
+### 修改的文件
+
+1. ✅ `src/app/pages/team-leader/dashboard/dashboard.ts`
+   - 第1869-1900行:修改 `updateWorkloadGantt()` 方法
+   - 使用真实设计师列表
+   - 过滤"未分配"
+
+### 相关服务
+
+1. `src/app/pages/team-leader/services/designer.service.ts`
+   - `getDesigners()`: 获取所有组员
+   - `getProjects()`: 获取所有项目
+   - `assignProject()`: 分配项目
+   - `transformProject()`: 转换项目数据(设置designerName)
+
+### 数据表
+
+1. **Profile表** (设计师/组员)
+   - `name`: 姓名
+   - `roleName`: '组员'
+   - `company`: 公司ID
+   - `isDeleted`: 软删除标记
+
+2. **Project表** (项目)
+   - `title`: 项目名称
+   - `assignee`: Pointer<Profile> (负责人)
+   - `status`: 项目状态
+   - `company`: 公司ID
+   - `isDeleted`: 软删除标记
+
+---
+
+## 🎉 总结
+
+### 修改前的问题
+
+- ❌ 甘特图只显示有项目的设计师
+- ❌ "未分配"占据甘特图第一行
+- ❌ 无法看到空闲的设计师
+- ❌ 项目负责人显示"未分配"
+
+### 修改后的效果
+
+- ✅ 甘特图显示所有真实组员
+- ✅ 不显示"未分配"伪设计师
+- ✅ 清晰显示谁有空闲(0个项目)
+- ✅ 便于识别和分配新项目
+- ✅ 数据来源可靠(从数据库查询)
+
+### 解决路径
+
+1. **短期**:修改前端逻辑(本次修改)
+   - 使用真实设计师列表
+   - 过滤"未分配"
+
+2. **长期**:数据质量保证
+   - 新项目创建时必须分配设计师
+   - 使用智能推荐功能
+   - 定期检查未分配项目
+
+---
+
+**修复完成!** ✨
+
+现在刷新浏览器,应该能看到:
+1. 所有真实组员都显示在甘特图中
+2. 不再显示"未分配"
+3. 可以清楚看到谁有空闲
+
+
+

+ 117 - 0
docs/task/2025102220-fix-unassigned-projects.md

@@ -0,0 +1,117 @@
+# 解决"未分配"问题和完整显示所有组员
+
+**日期**: 2025-10-24  
+**问题**: 组长端工作量负载概览图未显示全部组员,且项目负责人显示"未分配"
+
+---
+
+## 🔴 问题分析
+
+### 1. 项目负责人显示"未分配"
+
+**根本原因**: 数据库中 `Project` 表的 `assignee` 字段为空
+
+**代码位置**: `src/app/pages/team-leader/services/designer.service.ts` 第330行
+
+```typescript
+designerName: assignee?.get('name') || '未分配',
+```
+
+**数据流程**:
+```
+Parse数据库 → Project表 → assignee字段(Pointer<Profile>) 
+  ↓
+如果assignee为null → designerName = '未分配'
+```
+
+### 2. 工作量甘特图未显示所有组员
+
+**根本原因**: 只统计了**已分配项目**的设计师
+
+**代码位置**: `src/app/pages/team-leader/dashboard/dashboard.ts` 约2000行
+
+```typescript
+// ❌ 问题代码:只获取有项目的设计师
+const assigned = this.filteredProjects.filter(p => !!p.designerName);
+const designers = Array.from(new Set(assigned.map(p => p.designerName)));
+```
+
+**结果**:
+- ✅ 有项目的设计师:显示在甘特图中
+- ❌ 没有项目的设计师:不显示
+
+---
+
+## ✅ 解决方案
+
+### 方案1:修复甘特图 - 显示所有组员
+
+修改 `updateWorkloadGantt()` 方法,使用真实的设计师列表而非从项目中提取:
+
+```typescript
+// ✅ 修改后:使用真实的设计师列表
+const designers = this.realDesigners.length > 0 
+  ? this.realDesigners.map(d => d.name)
+  : Array.from(new Set(this.filteredProjects.filter(p => p.designerName && p.designerName !== '未分配').map(p => p.designerName)));
+
+// 如果没有设计师,显示提示
+if (designers.length === 0) {
+  // 显示"暂无组员数据"
+  return;
+}
+```
+
+### 方案2:项目分配功能
+
+在数据库层面为项目分配设计师:
+
+```typescript
+/**
+ * 在designer.service.ts中已有方法
+ */
+async assignProject(projectId: string, designerId: string): Promise<boolean> {
+  const project = Parse.Object.extend('Project').createWithoutData(projectId);
+  const designer = Parse.Object.extend('Profile').createWithoutData(designerId);
+  
+  project.set('assignee', designer);
+  project.set('status', '进行中');
+  
+  await project.save();
+  return true;
+}
+```
+
+### 方案3:数据库检查和修复
+
+**检查步骤**:
+1. 打开Parse Dashboard
+2. 查看 `Project` 表
+3. 检查 `assignee` 字段
+4. 对于未分配的项目,手动设置 `assignee`
+
+**SQL修复(如果有SQL访问权限)**:
+```sql
+-- 查看未分配的项目
+SELECT objectId, title, status, assignee 
+FROM Project 
+WHERE assignee IS NULL OR assignee = '';
+
+-- 批量分配给某个设计师
+UPDATE Project 
+SET assignee = {Profile的指针} 
+WHERE assignee IS NULL;
+```
+
+---
+
+## 📝 具体修改
+
+### 修改1: updateWorkloadGantt方法
+
+**文件**: `src/app/pages/team-leader/dashboard/dashboard.ts`
+
+**位置**: updateWorkloadGantt() 方法内
+
+**修改内容**:
+
+

+ 296 - 0
docs/task/2025102221-console-migration-script.md

@@ -0,0 +1,296 @@
+# 浏览器控制台快速修复脚本
+
+**使用说明**:复制下面的脚本,粘贴到浏览器控制台执行
+
+---
+
+## 🚀 快速修复脚本
+
+### 步骤1: 打开浏览器控制台
+
+1. 访问:`http://localhost:4200/admin/project-management`
+2. 按 `F12` 打开开发者工具
+3. 切换到 `Console` 标签
+
+---
+
+### 步骤2: 复制并执行以下脚本
+
+```javascript
+// ================================
+// 项目负责人批量更新脚本
+// ================================
+
+(async function() {
+  console.log('🚀 开始批量更新项目负责人...');
+  
+  try {
+    // 获取Parse实例
+    const FmodeParse = (window as any).FmodeParse || await import('fmode-ng/parse').then(m => m.FmodeParse);
+    const Parse = FmodeParse.with('nova');
+    
+    // 获取公司ID
+    const company = localStorage.getItem('company');
+    if (!company) {
+      console.error('❌ 未找到公司ID,请先登录');
+      return;
+    }
+    console.log('✅ 公司ID:', company);
+    
+    // 查询所有项目
+    const projectQuery = new Parse.Query('Project');
+    projectQuery.equalTo('company', company);
+    projectQuery.notEqualTo('isDeleted', true);
+    projectQuery.include(['assignee', 'department', 'department.leader']);
+    projectQuery.limit(1000);
+    
+    const projects = await projectQuery.find();
+    console.log(`📊 找到 ${projects.length} 个项目`);
+    
+    // 统计
+    let updated = 0;
+    let skipped = 0;
+    let failed = 0;
+    
+    // 遍历每个项目
+    for (let i = 0; i < projects.length; i++) {
+      const project = projects[i];
+      const title = project.get('title') || '未命名项目';
+      const existingAssignee = project.get('assignee');
+      
+      try {
+        // 如果已有负责人,跳过
+        if (existingAssignee) {
+          console.log(`⏭️  [${i+1}/${projects.length}] "${title}" 已有负责人: ${existingAssignee.get('name')}`);
+          skipped++;
+          continue;
+        }
+        
+        // 检查是否有项目组
+        let department = project.get('department');
+        
+        // 如果没有项目组,查找默认项目组
+        if (!department) {
+          console.log(`🔍 [${i+1}/${projects.length}] "${title}" 没有项目组,查找默认项目组...`);
+          
+          const deptQuery = new Parse.Query('Department');
+          deptQuery.equalTo('company', company);
+          deptQuery.equalTo('type', 'project');
+          deptQuery.notEqualTo('isDeleted', true);
+          deptQuery.include('leader');
+          deptQuery.ascending('createdAt');
+          deptQuery.limit(1);
+          
+          department = await deptQuery.first();
+        } else {
+          // 如果有项目组,确保加载了leader
+          await department.fetch({ include: ['leader'] });
+        }
+        
+        if (!department) {
+          console.warn(`⚠️  [${i+1}/${projects.length}] "${title}" 没有可用的项目组`);
+          failed++;
+          continue;
+        }
+        
+        // 获取组长
+        const leader = department.get('leader');
+        if (!leader) {
+          console.warn(`⚠️  [${i+1}/${projects.length}] "${title}" 的项目组没有组长`);
+          failed++;
+          continue;
+        }
+        
+        // 更新项目
+        project.set('assignee', leader);
+        project.set('department', department);
+        await project.save();
+        
+        updated++;
+        console.log(`✅ [${i+1}/${projects.length}] "${title}" 已设置负责人: ${leader.get('name')}`);
+        
+      } catch (error) {
+        console.error(`❌ [${i+1}/${projects.length}] 更新 "${title}" 失败:`, error);
+        failed++;
+      }
+    }
+    
+    // 输出结果
+    console.log('');
+    console.log('='.repeat(60));
+    console.log('🎉 批量更新完成!');
+    console.log('='.repeat(60));
+    console.log(`📊 总计: ${projects.length} 个项目`);
+    console.log(`✅ 成功更新: ${updated} 个`);
+    console.log(`⏭️  跳过(已有负责人): ${skipped} 个`);
+    console.log(`❌ 失败: ${failed} 个`);
+    console.log('='.repeat(60));
+    console.log('');
+    console.log('💡 请刷新页面查看结果(Ctrl+Shift+R)');
+    
+  } catch (error) {
+    console.error('❌ 执行失败:', error);
+  }
+})();
+```
+
+---
+
+### 步骤3: 等待执行完成
+
+控制台会输出类似信息:
+
+```
+🚀 开始批量更新项目负责人...
+✅ 公司ID: cDL6R1hgSi
+📊 找到 11 个项目
+⏭️  [1/11] "未命名案例组三期项目" 已有负责人: 汪奥
+✅ [2/11] "张家界凤凰城三期项目" 已设置负责人: 汪奥
+✅ [3/11] "日式10.6" 已设置负责人: 汪奥
+...
+============================================================
+🎉 批量更新完成!
+============================================================
+📊 总计: 11 个项目
+✅ 成功更新: 10 个
+⏭️  跳过(已有负责人): 1 个
+❌ 失败: 0 个
+============================================================
+💡 请刷新页面查看结果(Ctrl+Shift+R)
+```
+
+---
+
+### 步骤4: 刷新页面
+
+按 `Ctrl+Shift+R` 强制刷新页面,查看项目列表中的"负责人"列。
+
+**预期结果**:所有项目的"负责人"列应显示组长名字(如"汪奥"),而不是"未分配"。
+
+---
+
+## 📝 简化版脚本(如果上面的不work)
+
+如果上面的脚本报错,试试这个简化版:
+
+```javascript
+(async function() {
+  const Parse = window.FmodeParse.with('nova');
+  const company = localStorage.getItem('company');
+  
+  const query = new Parse.Query('Project');
+  query.equalTo('company', company);
+  query.notEqualTo('isDeleted', true);
+  query.doesNotExist('assignee');
+  query.limit(100);
+  
+  const projects = await query.find();
+  console.log('需要更新的项目数:', projects.length);
+  
+  // 获取第一个项目组
+  const deptQuery = new Parse.Query('Department');
+  deptQuery.equalTo('company', company);
+  deptQuery.include('leader');
+  const dept = await deptQuery.first();
+  
+  if (!dept) {
+    console.error('没有找到项目组');
+    return;
+  }
+  
+  const leader = dept.get('leader');
+  console.log('使用组长:', leader.get('name'));
+  
+  // 批量更新
+  for (const p of projects) {
+    p.set('assignee', leader);
+    p.set('department', dept);
+    await p.save();
+    console.log('✅', p.get('title'));
+  }
+  
+  console.log('完成!请刷新页面');
+})();
+```
+
+---
+
+## 🔍 验证脚本
+
+执行更新后,运行这个脚本验证结果:
+
+```javascript
+(async function() {
+  const Parse = window.FmodeParse.with('nova');
+  const company = localStorage.getItem('company');
+  
+  const query = new Parse.Query('Project');
+  query.equalTo('company', company);
+  query.notEqualTo('isDeleted', true);
+  query.include(['assignee', 'department']);
+  query.limit(20);
+  
+  const projects = await query.find();
+  
+  console.table(projects.map(p => ({
+    '项目名称': p.get('title'),
+    '负责人': p.get('assignee')?.get('name') || '未分配',
+    '项目组': p.get('department')?.get('name') || '无'
+  })));
+})();
+```
+
+预期输出表格:
+
+```
+┌─────────┬────────────────────────┬──────────┬──────────┐
+│ (index) │      项目名称          │  负责人  │  项目组  │
+├─────────┼────────────────────────┼──────────┼──────────┤
+│    0    │  '未命名案例组三期'     │  '汪奥'  │  '汪奥组'│
+│    1    │  '张家界凤凰城三期'     │  '汪奥'  │  '汪奥组'│
+│    2    │  '日式10.6'            │  '汪奥'  │  '汪奥组'│
+│   ...   │         ...            │   ...    │   ...    │
+└─────────┴────────────────────────┴──────────┴──────────┘
+```
+
+---
+
+## ⚠️ 故障排查
+
+### 错误1: `FmodeParse is not defined`
+
+**解决方法**:
+```javascript
+// 先初始化FmodeParse
+const { FmodeParse } = await import('fmode-ng/parse');
+// 然后继续执行脚本
+```
+
+### 错误2: `company is null`
+
+**解决方法**:
+```javascript
+// 手动设置公司ID
+localStorage.setItem('company', 'cDL6R1hgSi'); // 替换为你的公司ID
+```
+
+### 错误3: 权限错误
+
+**解决方法**:
+- 确保已登录管理员账号
+- 检查Parse ACL权限配置
+
+---
+
+## 🎯 执行后检查
+
+1. **控制台日志**:确认所有项目都成功更新
+2. **项目列表**:刷新页面,检查"负责人"列
+3. **Parse Dashboard**:查看Project表的assignee字段
+4. **项目详情页**:点击任意项目,查看负责人信息
+
+---
+
+**现在就执行脚本,一键修复所有项目的负责人!** 🚀
+
+

+ 424 - 0
docs/task/2025102221-fix-project-assignee-and-spaces.md

@@ -0,0 +1,424 @@
+# 修复项目负责人和空间场景问题
+
+**日期**: 2025-10-24  
+**问题**: 
+1. 分配设计师时没有空间场景可选
+2. 项目列表中负责人显示"未分配",应该显示组长名字
+
+---
+
+## 🔍 问题分析
+
+### 问题1: 空间场景为空
+
+**现象**:  
+在 `http://localhost:4200/admin/project-detail/APwk78jnrh/order` 分配设计师时,"指派空间场景"没有选项。
+
+**原因**:  
+`team-assign.component.ts` 通过 `ProductSpaceService.getProjectProductSpaces()` 获取项目空间:
+
+```typescript
+// team-assign.component.ts 第94-107行
+async loadProjectSpaces(): Promise<void> {
+  if (!this.project) return;
+  
+  try {
+    this.loadingSpaces = true;
+    const projectId = this.project.id || '';
+    // 从Product表查询该项目的空间
+    this.projectSpaces = await this.productSpaceService.getProjectProductSpaces(projectId);
+  } catch (err) {
+    console.error('加载项目空间失败:', err);
+  } finally {
+    this.loadingSpaces = false;
+    this.cdr.markForCheck();
+  }
+}
+```
+
+`ProductSpaceService.getProjectProductSpaces()` 从 `Product` 表查询:
+
+```typescript
+// product-space.service.ts 第125-143行
+async getProjectProductSpaces(projectId: string): Promise<Project[]> {
+  try {
+    const query = new Parse.Query('Product');
+    query.equalTo('project', {
+      __type: 'Pointer',
+      className: 'Project',
+      objectId: projectId
+    });
+    query.include('profile');
+    query.ascending('createdAt');
+
+    const results = await query.find();
+    return results.map(product => this.parseProductData(product));
+
+  } catch (error) {
+    console.error('获取项目产品空间失败:', error);
+    return [];
+  }
+}
+```
+
+**根本原因**: 项目ID为 `APwk78jnrh` 的项目在 `Product` 表中没有任何记录。
+
+---
+
+### 问题2: 项目负责人显示"未分配"
+
+**现象**:  
+项目列表中"负责人"列显示"未分配",应该显示组长名字。
+
+**数据流**:
+
+```
+Parse数据库 Project表
+  ↓
+  assignee字段 (Pointer<Profile>) = null
+  ↓
+project-management.ts 第107行
+  assignee: json.assigneeName || '未分配'
+  ↓
+前端显示 "未分配"
+```
+
+**根本原因**: 项目创建时,`assignee` 字段没有被设置。按业务逻辑,项目的负责人应该是项目组的组长(`department.leader`)。
+
+---
+
+## ✅ 解决方案
+
+### 方案1: 空间场景问题
+
+**方案A: 在"订单分配"阶段创建空间(推荐)**
+
+在 `stage-order.component.ts` 的订单分配阶段,用户填写空间信息后自动创建 `Product` 记录。
+
+**位置**: `src/modules/project/pages/project-detail/stages/stage-order.component.ts`
+
+**逻辑**:
+```typescript
+async saveSpaces() {
+  // 遍历用户添加的空间
+  for (const space of this.spaces) {
+    // 调用 ProductSpaceService.createProductSpace() 创建Product记录
+    await this.productSpaceService.createProductSpace(this.project.id, {
+      name: space.name,
+      type: space.type,
+      area: space.area,
+      priority: space.priority,
+      complexity: space.complexity,
+      estimatedBudget: space.budget
+    });
+  }
+}
+```
+
+**方案B: 在分配设计师时动态创建空间**
+
+如果没有空间,显示提示:"请先在订单分配阶段添加空间"。
+
+**方案C: 提供手动添加空间的入口**
+
+在分配设计师弹窗中添加"添加空间"按钮。
+
+---
+
+### 方案2: 项目负责人问题
+
+**关键修改**: 在选择项目组(Department)时,自动将组长(`department.leader`)设置为项目的 `assignee`。
+
+#### 修改1: team-assign.component.ts
+
+**位置**: `src/modules/project/components/team-assign/team-assign.component.ts` 第128-134行
+
+**修改 `selectDepartment` 方法**:
+
+```typescript
+async selectDepartment(department: FmodeObject) {
+  this.selectedDepartment = department;
+  this.selectedDesigner = null;
+  this.departmentMembers = [];
+
+  // ✅ 新增:自动设置组长为项目负责人
+  const leader = department.get('leader');
+  if (leader && this.project) {
+    try {
+      // 更新项目的assignee字段为组长
+      this.project.set('assignee', leader);
+      this.project.set('department', department);
+      await this.project.save();
+      console.log('✅ 项目负责人已设置为组长:', leader.get('name'));
+    } catch (error) {
+      console.error('❌ 设置项目负责人失败:', error);
+    }
+  }
+
+  await this.loadDepartmentMembers(department);
+}
+```
+
+**说明**:
+- 当选择项目组时,自动获取组长(`department.leader`)
+- 将组长设置为项目的 `assignee` 字段
+- 同时设置项目的 `department` 字段
+- 保存到数据库
+
+#### 修改2: 项目创建时的默认逻辑
+
+**位置**: `src/app/pages/admin/services/project.service.ts` 第65-110行
+
+**在 `createProject` 方法中添加逻辑**:
+
+```typescript
+async createProject(data: {
+  title: string;
+  customerId?: string;
+  assigneeId?: string; // 可以是组长ID
+  departmentId?: string; // 新增:项目组ID
+  status?: string;
+  currentStage?: string;
+  deadline?: Date;
+  data?: any;
+}): Promise<FmodeObject> {
+  const projectData: any = {
+    title: data.title,
+    status: data.status || '待分配',
+    currentStage: data.currentStage || '订单分配'
+  };
+
+  // 设置客户指针
+  if (data.customerId) {
+    projectData.customer = {
+      __type: 'Pointer',
+      className: 'ContactInfo',
+      objectId: data.customerId
+    };
+  }
+
+  // ✅ 新增:如果提供了项目组,获取组长作为默认负责人
+  if (data.departmentId) {
+    const departmentQuery = new Parse.Query('Department');
+    departmentQuery.include('leader');
+    const department = await departmentQuery.get(data.departmentId);
+    
+    if (department) {
+      projectData.department = department.toPointer();
+      
+      // 获取组长
+      const leader = department.get('leader');
+      if (leader && !data.assigneeId) {
+        projectData.assignee = leader.toPointer();
+        console.log('✅ 项目负责人默认为组长:', leader.get('name'));
+      }
+    }
+  }
+
+  // 设置负责人指针(如果明确指定)
+  if (data.assigneeId) {
+    projectData.assignee = {
+      __type: 'Pointer',
+      className: 'Profile',
+      objectId: data.assigneeId
+    };
+  }
+
+  if (data.deadline) {
+    projectData.deadline = data.deadline;
+  }
+
+  if (data.data) {
+    projectData.data = data.data;
+  }
+
+  const project = this.adminData.createObject('Project', projectData);
+  return await this.adminData.save(project);
+}
+```
+
+---
+
+## 📊 数据库结构
+
+### Project 表
+
+| 字段 | 类型 | 说明 | 新增/修改 |
+|------|------|------|----------|
+| `department` | Pointer<Department> | 项目组 | ✅ 确保填充 |
+| `assignee` | Pointer<Profile> | 项目负责人(组长) | ✅ 自动设置为组长 |
+| `title` | String | 项目名称 | 已有 |
+| `customer` | Pointer<ContactInfo> | 客户 | 已有 |
+| `status` | String | 项目状态 | 已有 |
+| `currentStage` | String | 当前阶段 | 已有 |
+
+### Product 表
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| `project` | Pointer<Project> | 所属项目 |
+| `productName` | String | 空间名称(如"客厅") |
+| `productType` | String | 空间类型(如"living_room") |
+| `space` | Object | 空间详细信息 |
+| `space.area` | Number | 面积 |
+| `space.priority` | Number | 优先级 |
+| `space.complexity` | String | 复杂度 |
+| `quotation` | Object | 报价信息 |
+| `requirements` | Object | 需求信息 |
+| `profile` | Pointer<Profile> | 负责该空间的设计师 |
+
+### Department 表
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| `name` | String | 项目组名称 |
+| `leader` | Pointer<Profile> | 组长 |
+| `type` | String | 'project' (项目组) |
+| `company` | String | 公司ID |
+
+### Profile 表
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| `name` | String | 姓名 |
+| `roleName` | String | '组长' 或 '组员' |
+| `department` | String | 部门ID |
+| `company` | String | 公司ID |
+
+---
+
+## 🧪 测试步骤
+
+### 测试1: 空间场景
+
+1. **进入订单分配阶段**
+   ```
+   http://localhost:4200/admin/project-detail/APwk78jnrh/order
+   ```
+
+2. **添加空间**
+   - 点击"添加空间"按钮
+   - 填写空间信息(客厅、卧室等)
+   - 保存
+
+3. **分配设计师**
+   - 选择项目组
+   - 选择设计师
+   - **预期**: 能看到刚才添加的空间列表
+   - 选择空间并确认分配
+
+### 测试2: 项目负责人
+
+1. **创建新项目时指定项目组**
+   ```
+   POST /Project
+   {
+     "title": "测试项目",
+     "departmentId": "xxx", // 项目组ID
+     "status": "待分配"
+   }
+   ```
+
+2. **验证数据库**
+   ```
+   // Parse Dashboard 查看 Project 表
+   - assignee字段应该指向组长的Profile
+   - department字段应该指向该项目组
+   ```
+
+3. **在项目详情页选择项目组**
+   - 进入项目详情页订单分配阶段
+   - 选择一个项目组
+   - **预期**: 
+     - 项目的assignee自动更新为该组长
+     - 刷新项目列表,负责人列显示组长名字
+
+4. **验证项目列表**
+   ```
+   http://localhost:4200/admin/project-management
+   ```
+   - **预期**: "负责人"列显示组长名字,而不是"未分配"
+
+---
+
+## 🎯 核心逻辑总结
+
+### 项目负责人(assignee)的设置规则
+
+1. **项目创建时**:
+   - 如果指定了 `departmentId`,自动获取组长作为 `assignee`
+   - 如果明确指定了 `assigneeId`,使用指定的人员
+
+2. **选择项目组时**:
+   - 在 `team-assign` 组件中选择项目组
+   - 自动将组长设置为项目的 `assignee`
+   - 更新 `department` 字段
+
+3. **项目列表显示**:
+   - 从 `assignee.name` 获取负责人名字
+   - 如果为空,显示"未分配"
+
+### 空间场景的创建流程
+
+1. **订单分配阶段** (stage-order):
+   - 用户填写空间信息(客厅、卧室等)
+   - 调用 `ProductSpaceService.createProductSpace()` 创建 Product 记录
+
+2. **分配设计师时** (team-assign):
+   - 从 `Product` 表查询该项目的所有空间
+   - 显示空间列表供选择
+   - 将选中的空间保存到 `ProjectTeam.data.spaces` 中
+
+---
+
+## 📁 需要修改的文件
+
+1. ✅ `src/modules/project/components/team-assign/team-assign.component.ts`
+   - 第128-134行:修改 `selectDepartment` 方法
+
+2. ✅ `src/app/pages/admin/services/project.service.ts`
+   - 第65-110行:修改 `createProject` 方法
+
+3. ⚠️ `src/modules/project/pages/project-detail/stages/stage-order.component.ts`
+   - 需要确保空间信息保存时创建 Product 记录
+
+---
+
+## 🎉 预期效果
+
+### 修改前
+
+**项目列表**:
+```
+项目名称                             | 客户      | 负责人    | 状态
+张家界凤凰城三期项目 紫空居..        | 未知客户  | 未分配    | 待分配
+```
+
+**分配设计师**:
+```
+指派空间场景
+(空)
+```
+
+### 修改后
+
+**项目列表**:
+```
+项目名称                             | 客户      | 负责人    | 状态
+张家界凤凰城三期项目 紫空居..        | 张先生    | 汪奥      | 进行中
+```
+
+**分配设计师**:
+```
+指派空间场景 *
+☑ 客厅
+☐ 主卧
+☐ 次卧
+☐ 厨房
+```
+
+---
+
+**修改完成后,项目负责人将自动设置为组长,且分配设计师时能看到项目的所有空间!** ✨
+
+

+ 432 - 0
docs/task/2025102221-implementation-summary.md

@@ -0,0 +1,432 @@
+# 项目负责人和空间场景问题 - 实现总结
+
+**日期**: 2025-10-24  
+**完成状态**: ✅ 已完成
+
+---
+
+## ✅ 已完成的修改
+
+### 修改1: team-assign组件 - 自动设置组长为负责人
+
+**文件**: `src/modules/project/components/team-assign/team-assign.component.ts`  
+**位置**: 第128-151行
+
+**修改内容**:
+
+```typescript
+async selectDepartment(department: FmodeObject) {
+  this.selectedDepartment = department;
+  this.selectedDesigner = null;
+  this.departmentMembers = [];
+
+  // ✅ 自动设置组长为项目负责人
+  const leader = department.get('leader');
+  if (leader && this.project) {
+    try {
+      // 更新项目的assignee字段为组长
+      this.project.set('assignee', leader);
+      this.project.set('department', department);
+      await this.project.save();
+      console.log('✅ 项目负责人已设置为组长:', leader.get('name'));
+      
+      // 触发界面更新
+      this.cdr.markForCheck();
+    } catch (error) {
+      console.error('❌ 设置项目负责人失败:', error);
+    }
+  }
+
+  await this.loadDepartmentMembers(department);
+}
+```
+
+**效果**: 
+- 在项目详情页选择项目组时,自动将组长设置为项目的 `assignee`
+- 同时设置项目的 `department` 字段
+- 保存到数据库,刷新后数据持久化
+
+---
+
+### 修改2: ProjectService - 项目创建逻辑
+
+**文件**: `src/app/pages/admin/services/project.service.ts`  
+**位置**: 第4-6行(导入)、第70-137行(createProject方法)
+
+**2.1 添加Parse导入**:
+
+```typescript
+import { FmodeParse } from 'fmode-ng/parse';
+
+const Parse = FmodeParse.with('nova');
+```
+
+**2.2 修改createProject方法**:
+
+```typescript
+async createProject(data: {
+  title: string;
+  customerId?: string;
+  assigneeId?: string;
+  departmentId?: string; // ✅ 新增:项目组ID
+  status?: string;
+  currentStage?: string;
+  deadline?: Date;
+  data?: any;
+}): Promise<FmodeObject> {
+  const projectData: any = {
+    title: data.title,
+    status: data.status || '待分配',
+    currentStage: data.currentStage || '订单分配'
+  };
+
+  // 设置客户指针
+  if (data.customerId) {
+    projectData.customer = {
+      __type: 'Pointer',
+      className: 'ContactInfo',
+      objectId: data.customerId
+    };
+  }
+
+  // ✅ 新增:如果提供了项目组,获取组长作为默认负责人
+  if (data.departmentId) {
+    try {
+      const departmentQuery = new Parse.Query('Department');
+      departmentQuery.include('leader');
+      const department = await departmentQuery.get(data.departmentId);
+      
+      if (department) {
+        projectData.department = department.toPointer();
+        
+        // 获取组长
+        const leader = department.get('leader');
+        if (leader && !data.assigneeId) {
+          // 如果没有明确指定负责人,使用组长
+          projectData.assignee = leader.toPointer();
+          console.log('✅ 项目负责人默认为组长:', leader.get('name'));
+        }
+      }
+    } catch (error) {
+      console.error('❌ 获取项目组失败:', error);
+    }
+  }
+
+  // 设置负责人指针(如果明确指定,覆盖组长)
+  if (data.assigneeId) {
+    projectData.assignee = {
+      __type: 'Pointer',
+      className: 'Profile',
+      objectId: data.assigneeId
+    };
+  }
+
+  if (data.deadline) {
+    projectData.deadline = data.deadline;
+  }
+
+  if (data.data) {
+    projectData.data = data.data;
+  }
+
+  const project = this.adminData.createObject('Project', projectData);
+  return await this.adminData.save(project);
+}
+```
+
+**效果**:
+- 创建项目时可以指定 `departmentId`
+- 自动获取该项目组的组长作为默认负责人
+- 如果明确指定了 `assigneeId`,优先使用指定的人员
+
+---
+
+### 修改3: ProjectService - 查询时包含department和leader
+
+**文件**: `src/app/pages/admin/services/project.service.ts`  
+**位置**: 第17-44行(findProjects)、第57-67行(getProject)
+
+**3.1 修改findProjects**:
+
+```typescript
+async findProjects(options?: {
+  status?: string;
+  keyword?: string;
+  skip?: number;
+  limit?: number;
+}): Promise<FmodeObject[]> {
+  return await this.adminData.findAll('Project', {
+    include: ['customer', 'assignee', 'department', 'department.leader'], // ✅ 添加department和leader
+    skip: options?.skip || 0,
+    limit: options?.limit || 20,
+    descending: 'updatedAt',
+    additionalQuery: query => {
+      if (options?.status) {
+        query.equalTo('status', options.status);
+      }
+      if (options?.keyword) {
+        const kw = options.keyword.trim();
+        if (kw) {
+          // 搜索项目标题
+          query.matches('title', new RegExp(kw, 'i'));
+        }
+      }
+    }
+  });
+}
+```
+
+**3.2 修改getProject**:
+
+```typescript
+async getProject(objectId: string): Promise<FmodeObject | null> {
+  return await this.adminData.getById('Project', objectId, [
+    'customer',
+    'assignee',
+    'department',
+    'department.leader' // ✅ 添加department和leader
+  ]);
+}
+```
+
+**效果**:
+- 查询项目时自动加载 `department` 和 `department.leader` 关联数据
+- 为后续显示组长信息提供数据基础
+
+---
+
+### 修改4: ProjectService - toJSON方法优化
+
+**文件**: `src/app/pages/admin/services/project.service.ts`  
+**位置**: 第231-257行
+
+**修改内容**:
+
+```typescript
+toJSON(project: FmodeObject): any {
+  const json = this.adminData.toJSON(project);
+
+  // 处理关联对象
+  if (json.customer && typeof json.customer === 'object') {
+    json.customerName = json.customer.name || '';
+    json.customerId = json.customer.objectId;
+  }
+
+  // ✅ 处理负责人:优先使用assignee,如果为空则使用department.leader
+  if (json.assignee && typeof json.assignee === 'object') {
+    json.assigneeName = json.assignee.name || '';
+    json.assigneeId = json.assignee.objectId;
+  } else if (json.department && typeof json.department === 'object') {
+    // 如果没有assignee,但有department和leader,使用leader
+    const leader = json.department.leader;
+    if (leader && typeof leader === 'object') {
+      json.assigneeName = leader.name || '';
+      json.assigneeId = leader.objectId;
+    }
+  }
+
+  return json;
+}
+```
+
+**效果**:
+- 项目列表显示负责人时,优先使用 `assignee.name`
+- 如果 `assignee` 为空,自动使用 `department.leader.name`
+- 确保项目列表中不会显示"未分配",而是显示组长名字
+
+---
+
+## 📊 数据流图
+
+### 创建项目流程
+
+```
+用户创建项目
+  ↓
+指定departmentId
+  ↓
+createProject()查询Department
+  ↓
+获取department.leader
+  ↓
+设置project.assignee = leader
+  ↓
+保存到数据库
+  ↓
+项目列表显示组长名字
+```
+
+### 选择项目组流程
+
+```
+用户在项目详情页
+  ↓
+选择项目组(team-assign组件)
+  ↓
+selectDepartment()自动触发
+  ↓
+获取department.leader
+  ↓
+更新project.assignee = leader
+  ↓
+保存到数据库
+  ↓
+刷新项目列表,负责人更新
+```
+
+### 显示负责人流程
+
+```
+加载项目列表
+  ↓
+include: ['assignee', 'department', 'department.leader']
+  ↓
+toJSON()转换
+  ↓
+如果有assignee → 显示assignee.name
+如果没有assignee但有department.leader → 显示leader.name
+否则 → 显示"未分配"
+  ↓
+项目列表展示
+```
+
+---
+
+## 🧪 测试验证
+
+### 测试场景1: 在项目详情页选择项目组
+
+**步骤**:
+1. 访问项目详情页:`http://localhost:4200/admin/project-detail/APwk78jnrh/order`
+2. 在"设计师分配"卡片中选择一个项目组
+3. 观察控制台输出:`✅ 项目负责人已设置为组长: xxx`
+4. 刷新项目管理页面:`http://localhost:4200/admin/project-management`
+5. 验证该项目的"负责人"列显示组长名字
+
+**预期结果**:
+- ✅ 选择项目组后,项目的 `assignee` 自动设置为组长
+- ✅ 项目列表中"负责人"列显示组长名字
+- ✅ 不再显示"未分配"
+
+---
+
+### 测试场景2: 创建新项目并指定项目组
+
+**步骤**:
+1. 调用 `projectService.createProject()` 创建项目:
+   ```typescript
+   await projectService.createProject({
+     title: '测试项目',
+     departmentId: 'xxx项目组ID',
+     status: '待分配'
+   });
+   ```
+2. 查看控制台输出:`✅ 项目负责人默认为组长: xxx`
+3. 在项目管理页面查看新项目
+4. 验证"负责人"列显示组长名字
+
+**预期结果**:
+- ✅ 项目创建时自动设置组长为负责人
+- ✅ `project.assignee` 指向组长的Profile
+- ✅ `project.department` 指向该项目组
+
+---
+
+### 测试场景3: 验证空间场景问题
+
+**当前状态**: 
+- ⚠️ 项目ID `APwk78jnrh` 在 `Product` 表中没有记录
+- ⚠️ 分配设计师时没有空间可选
+
+**解决方案**: 
+1. 在订单分配阶段(stage-order)添加空间时,确保调用 `ProductSpaceService.createProductSpace()` 创建 Product 记录
+2. 或者在Parse Dashboard手动添加Product记录:
+   - 进入 `Product` 表
+   - 添加新行
+   - 设置 `project` 字段为项目指针
+   - 设置 `productName` 为 "客厅"、"卧室" 等
+   - 设置 `productType` 为 "living_room"、"bedroom" 等
+   - 保存
+
+---
+
+## 🎯 核心改进
+
+### 改进1: 自动化负责人分配
+
+**修改前**:
+- 项目创建时 `assignee` 为空
+- 需要手动设置负责人
+- 项目列表显示"未分配"
+
+**修改后**:
+- 选择项目组时自动设置组长为负责人
+- 创建项目时可指定项目组,自动获取组长
+- 项目列表始终显示组长名字
+
+---
+
+### 改进2: 优雅降级
+
+**数据获取优先级**:
+1. **最高优先级**: 明确指定的 `assignee`
+2. **次优先级**: 项目组的 `department.leader`
+3. **兜底**: 显示"未分配"
+
+**代码实现**:
+```typescript
+// toJSON方法中的逻辑
+if (json.assignee && typeof json.assignee === 'object') {
+  // 优先使用assignee
+  json.assigneeName = json.assignee.name || '';
+} else if (json.department && typeof json.department === 'object') {
+  // 降级使用department.leader
+  const leader = json.department.leader;
+  if (leader && typeof leader === 'object') {
+    json.assigneeName = leader.name || '';
+  }
+}
+```
+
+---
+
+## 📁 修改文件清单
+
+| 文件 | 修改内容 | 状态 |
+|------|---------|------|
+| `src/modules/project/components/team-assign/team-assign.component.ts` | 选择项目组时自动设置组长为负责人 | ✅ 完成 |
+| `src/app/pages/admin/services/project.service.ts` | 添加Parse导入 | ✅ 完成 |
+| `src/app/pages/admin/services/project.service.ts` | 修改createProject方法,支持departmentId | ✅ 完成 |
+| `src/app/pages/admin/services/project.service.ts` | 修改findProjects,include department | ✅ 完成 |
+| `src/app/pages/admin/services/project.service.ts` | 修改getProject,include department | ✅ 完成 |
+| `src/app/pages/admin/services/project.service.ts` | 修改toJSON,优先显示leader | ✅ 完成 |
+| `docs/task/2025102221-fix-project-assignee-and-spaces.md` | 问题分析文档 | ✅ 完成 |
+| `docs/task/2025102221-implementation-summary.md` | 实现总结文档 | ✅ 完成 |
+
+---
+
+## 🎉 总结
+
+### 解决的问题
+
+1. ✅ **项目负责人显示"未分配"** → 现在自动显示组长名字
+2. ⚠️ **分配设计师时没有空间场景** → 需要在订单分配阶段创建Product记录
+
+### 核心逻辑
+
+1. **选择项目组** → 自动设置组长为负责人
+2. **创建项目** → 指定项目组时,自动获取组长
+3. **显示负责人** → 优先assignee,降级使用leader
+4. **数据加载** → 始终include department和leader
+
+### 用户体验提升
+
+- **无需手动分配负责人**:选择项目组即可
+- **项目列表清晰**:始终显示真实负责人姓名
+- **数据一致性**:负责人与项目组关联
+
+---
+
+**修改完成!现在刷新浏览器测试,项目列表应该能正确显示组长名字了!** 🚀
+
+

+ 296 - 0
docs/task/2025102221-migration-guide.md

@@ -0,0 +1,296 @@
+# 项目负责人数据迁移指南
+
+**日期**: 2025-10-24  
+**问题**: 项目列表中的"负责人"列显示"未分配"
+
+---
+
+## 🔍 问题原因
+
+现有项目在数据库中还没有 `assignee` 或 `department` 字段,导致显示"未分配"。
+
+---
+
+## ✅ 解决方案
+
+### 方案1: 使用迁移脚本(推荐)
+
+**步骤**:
+
+1. **打开浏览器控制台**
+   - 访问:`http://localhost:4200/admin/project-management`
+   - 按 `F12` 打开开发者工具
+   - 切换到"Console"标签
+
+2. **运行迁移脚本**
+   ```javascript
+   // 1. 检查项目状态
+   const ProjectMigrationService = await import('./src/app/pages/admin/services/project-migration.service').then(m => m.ProjectMigrationService);
+   const migrationService = new ProjectMigrationService();
+   
+   // 查看需要迁移的项目数量
+   const status = await migrationService.checkProjectsStatus();
+   console.log('需要迁移的项目:', status.needsMigration);
+   
+   // 2. 执行迁移
+   const result = await migrationService.migrateProjectAssignees();
+   console.log('迁移结果:', result);
+   ```
+
+3. **刷新页面**
+   - 按 `Ctrl+Shift+R` 强制刷新
+   - 查看项目列表,"负责人"列应显示组长名字
+
+---
+
+### 方案2: Angular注入方式(如果方案1不work)
+
+**步骤**:
+
+1. **修改项目管理组件,添加迁移按钮**
+
+在 `src/app/pages/admin/project-management/project-management.ts` 中添加:
+
+```typescript
+import { ProjectMigrationService } from '../services/project-migration.service';
+
+export class ProjectManagement {
+  constructor(
+    private projectService: ProjectService,
+    private migrationService: ProjectMigrationService // ✅ 添加
+  ) {}
+  
+  // ✅ 添加迁移方法
+  async migrateProjects() {
+    const confirmed = confirm('确定要批量更新所有项目的负责人吗?');
+    if (!confirmed) return;
+    
+    try {
+      const result = await this.migrationService.migrateProjectAssignees();
+      alert(`迁移完成!\n总计: ${result.total}\n成功: ${result.success}\n失败: ${result.failed}`);
+      
+      // 刷新项目列表
+      await this.loadProjects();
+    } catch (error) {
+      alert('迁移失败: ' + error);
+    }
+  }
+}
+```
+
+2. **在HTML中添加按钮**
+
+在 `src/app/pages/admin/project-management/project-management.html` 顶部添加:
+
+```html
+<button (click)="migrateProjects()" class="btn btn-warning">
+  🔧 批量更新负责人
+</button>
+```
+
+3. **重启开发服务器**
+   ```bash
+   npm start
+   ```
+
+4. **点击按钮执行迁移**
+
+---
+
+### 方案3: Parse Dashboard 手动修复
+
+如果上述方案都不work,可以在Parse Dashboard中手动修复:
+
+**步骤**:
+
+1. **打开Parse Dashboard**
+
+2. **进入Project表**
+
+3. **找到一个项目**
+
+4. **编辑该项目**:
+   - 找到 `department` 列,选择一个Department
+   - 找到 `assignee` 列,选择该Department的leader
+   - 保存
+
+5. **重复步骤3-4**,直到所有项目都有负责人
+
+---
+
+## 🧪 验证方法
+
+### 1. 检查数据库
+
+在浏览器控制台执行:
+
+```javascript
+// 使用FmodeParse直接查询
+const Parse = FmodeParse.with('nova');
+const query = new Parse.Query('Project');
+query.include(['assignee', 'department', 'department.leader']);
+query.limit(5);
+const projects = await query.find();
+
+projects.forEach(p => {
+  console.log('项目:', p.get('title'));
+  console.log('- assignee:', p.get('assignee')?.get('name'));
+  console.log('- department:', p.get('department')?.get('name'));
+  console.log('- leader:', p.get('department')?.get('leader')?.get('name'));
+});
+```
+
+### 2. 检查项目列表
+
+1. 访问:`http://localhost:4200/admin/project-management`
+2. 查看"负责人"列
+3. **预期**:应显示组长名字,而不是"未分配"
+
+---
+
+## 📝 迁移脚本逻辑
+
+```
+开始迁移
+  ↓
+查询所有项目
+  ↓
+遍历每个项目
+  ↓
+已有assignee? → 跳过
+  ↓ No
+有department? → 使用其leader作为assignee
+  ↓ No
+查找默认项目组 → 使用其leader作为assignee
+  ↓
+保存项目
+  ↓
+下一个项目
+```
+
+---
+
+## 🔧 迁移脚本特性
+
+1. **安全性**:
+   - 已有assignee的项目不会被覆盖
+   - 支持回滚(手动删除assignee字段)
+
+2. **智能分配**:
+   - 优先使用项目的department.leader
+   - 如果没有department,使用默认项目组的leader
+   - 如果项目组没有leader,标记为失败
+
+3. **详细日志**:
+   - 每个项目的处理状态
+   - 成功/失败统计
+   - 错误原因
+
+4. **批量处理**:
+   - 一次处理最多1000个项目
+   - 支持断点续传(重新运行只处理未完成的)
+
+---
+
+## ⚠️ 注意事项
+
+### 执行前
+
+1. **备份数据**:
+   - 在Parse Dashboard中导出Project表数据
+   - 保存备份,以防需要回滚
+
+2. **检查项目组**:
+   - 确保至少有一个项目组(Department表)
+   - 确保项目组有组长(leader字段)
+
+### 执行中
+
+1. **不要关闭浏览器**:迁移过程中保持页面打开
+2. **观察控制台**:查看进度和错误信息
+3. **网络稳定**:确保网络连接正常
+
+### 执行后
+
+1. **验证数据**:检查几个项目的assignee字段
+2. **刷新页面**:强制刷新浏览器缓存
+3. **查看日志**:确认成功/失败数量
+
+---
+
+## 🎯 预期结果
+
+### 迁移前
+
+```
+Project表
+- project1: { title: "项目A", assignee: null, department: null }
+- project2: { title: "项目B", assignee: null, department: null }
+```
+
+项目列表显示:
+```
+项目A    | 未分配
+项目B    | 未分配
+```
+
+### 迁移后
+
+```
+Project表
+- project1: { 
+    title: "项目A", 
+    assignee: Pointer<Profile>汪奥, 
+    department: Pointer<Department>汪奥组 
+  }
+- project2: { 
+    title: "项目B", 
+    assignee: Pointer<Profile>汪奥, 
+    department: Pointer<Department>汪奥组 
+  }
+```
+
+项目列表显示:
+```
+项目A    | 汪奥
+项目B    | 汪奥
+```
+
+---
+
+## 📚 相关文件
+
+- 迁移服务:`src/app/pages/admin/services/project-migration.service.ts`
+- 项目服务:`src/app/pages/admin/services/project.service.ts`
+- 分配组件:`src/modules/project/components/team-assign/team-assign.component.ts`
+
+---
+
+## 💡 常见问题
+
+### Q1: 迁移后仍显示"未分配"
+
+**A**: 
+1. 强制刷新浏览器(Ctrl+Shift+R)
+2. 检查浏览器缓存
+3. 验证数据库中的assignee字段是否已设置
+
+### Q2: 部分项目迁移失败
+
+**A**: 
+1. 查看控制台错误信息
+2. 检查失败项目是否有可用的项目组
+3. 手动在Parse Dashboard中修复
+
+### Q3: 没有可用的项目组
+
+**A**: 
+1. 先创建项目组(Department表)
+2. 设置项目组的leader字段
+3. 重新运行迁移脚本
+
+---
+
+**准备好了吗?开始迁移吧!** 🚀
+
+

+ 239 - 0
docs/task/2025102221-quick-guide.md

@@ -0,0 +1,239 @@
+# 快速使用指南 - 项目负责人和空间场景
+
+**日期**: 2025-10-24
+
+---
+
+## 🎯 已修复的问题
+
+1. ✅ **项目列表负责人显示"未分配"** → 现在自动显示组长名字
+2. ⚠️ **分配设计师时没有空间场景可选** → 需要先添加空间
+
+---
+
+## 📝 使用说明
+
+### 1. 如何让项目显示组长作为负责人
+
+**方法1: 在项目详情页选择项目组(推荐)**
+
+1. 访问项目详情页:
+   ```
+   http://localhost:4200/admin/project-detail/{项目ID}/order
+   ```
+
+2. 在"设计师分配"卡片中,点击选择一个项目组
+
+3. **自动效果**:
+   - ✅ 项目的 `assignee` 自动设置为该项目组的组长
+   - ✅ 控制台输出:`✅ 项目负责人已设置为组长: xxx`
+   - ✅ 数据保存到数据库
+
+4. 刷新项目管理页面:
+   ```
+   http://localhost:4200/admin/project-management
+   ```
+
+5. **验证**:该项目的"负责人"列应显示组长名字
+
+---
+
+**方法2: 创建项目时指定项目组**
+
+```typescript
+await projectService.createProject({
+  title: '新项目',
+  departmentId: '项目组ID', // ✅ 指定项目组ID
+  customerId: '客户ID',
+  status: '待分配'
+});
+```
+
+效果:自动将该项目组的组长设置为项目负责人。
+
+---
+
+### 2. 如何添加空间场景
+
+**问题**:分配设计师时,"指派空间场景"没有选项。
+
+**原因**:该项目在 `Product` 表中没有空间记录。
+
+**解决方案A: 在订单分配阶段添加空间(推荐)**
+
+1. 进入项目详情页的"订单分配"阶段
+2. 找到"空间配置"或类似区域
+3. 点击"添加空间"按钮
+4. 填写空间信息:
+   - 空间名称:客厅、卧室、厨房等
+   - 空间类型:living_room、bedroom、kitchen等
+   - 面积、预算等
+5. **保存** → 系统会调用 `ProductSpaceService.createProductSpace()` 创建记录
+
+---
+
+**解决方案B: Parse Dashboard 手动添加**
+
+1. 打开Parse Dashboard
+
+2. 进入 `Product` 表
+
+3. 点击"Add Row"添加新行
+
+4. 设置字段:
+   ```
+   project: Pointer<Project> → 选择项目
+   productName: "客厅"
+   productType: "living_room"
+   space: {
+     "spaceName": "客厅",
+     "area": 30,
+     "priority": 5,
+     "complexity": "medium"
+   }
+   status: "not_started"
+   ```
+
+5. 保存
+
+6. 刷新项目详情页,现在分配设计师时应该能看到"客厅"选项了
+
+---
+
+## 🧪 测试步骤
+
+### 测试1: 验证负责人显示
+
+1. **打开项目详情页**
+   ```
+   http://localhost:4200/admin/project-detail/APwk78jnrh/order
+   ```
+
+2. **选择项目组**
+   - 在"设计师分配"区域
+   - 选择任意项目组(如"汪奥组")
+
+3. **查看控制台**
+   - 应该看到:`✅ 项目负责人已设置为组长: 汪奥`
+
+4. **刷新项目列表**
+   ```
+   http://localhost:4200/admin/project-management
+   ```
+
+5. **验证**
+   - "负责人"列应显示"汪奥"(组长名字)
+   - 不再显示"未分配"
+
+---
+
+### 测试2: 验证空间场景
+
+1. **添加空间**(使用解决方案A或B)
+
+2. **刷新项目详情页**
+   ```
+   http://localhost:4200/admin/project-detail/APwk78jnrh/order
+   ```
+
+3. **选择项目组和设计师**
+   - 选择项目组
+   - 选择一个设计师
+
+4. **查看弹窗**
+   - "指派空间场景"区域应该显示空间列表
+   - 例如:☑ 客厅、☐ 卧室、☐ 厨房
+
+5. **选择空间并确认分配**
+
+---
+
+## 💡 重要提示
+
+### 关于项目负责人
+
+- **自动规则**:选择项目组时,自动将组长设置为项目负责人
+- **优先级**:
+  1. 明确指定的 `assignee`(最高优先级)
+  2. 项目组的 `department.leader`(自动设置)
+  3. "未分配"(兜底)
+
+### 关于空间场景
+
+- **数据来源**:从 `Product` 表查询
+- **创建时机**:订单分配阶段添加空间
+- **必要性**:分配设计师前必须先有空间
+
+---
+
+## 🔧 排查问题
+
+### 问题1: 项目列表仍显示"未分配"
+
+**可能原因**:
+- 项目没有 `department` 字段
+- 项目组没有 `leader` 字段
+
+**解决方法**:
+1. 访问项目详情页,选择一个项目组
+2. 或在Parse Dashboard中手动设置:
+   ```
+   Project表 → 找到该项目
+   → department字段 → 选择一个Department
+   → assignee字段 → 选择该Department的leader
+   → 保存
+   ```
+
+---
+
+### 问题2: 没有空间场景可选
+
+**可能原因**:
+- `Product` 表中该项目没有记录
+
+**解决方法**:
+1. 在项目详情页的订单分配阶段添加空间
+2. 或使用Parse Dashboard手动添加Product记录(见解决方案B)
+
+---
+
+### 问题3: 控制台报错
+
+**如果看到**:
+```
+❌ 设置项目负责人失败: ...
+```
+
+**检查**:
+- 项目组是否有组长(`department.leader` 不为空)
+- Parse权限配置是否正确
+- 网络连接是否正常
+
+---
+
+## 📚 相关文档
+
+- 详细分析:`docs/task/2025102221-fix-project-assignee-and-spaces.md`
+- 实现总结:`docs/task/2025102221-implementation-summary.md`
+
+---
+
+## 🎉 总结
+
+### 核心改进
+
+1. **自动化**:选择项目组 → 自动设置组长为负责人
+2. **智能降级**:优先使用assignee,没有则使用leader
+3. **数据完整**:查询时include department和leader
+
+### 用户体验
+
+- ✅ 项目列表清晰显示负责人
+- ✅ 无需手动分配负责人
+- ✅ 数据一致性保证
+
+---
+
+**修改完成!刷新浏览器测试吧!** 🚀
+
+

+ 343 - 0
docs/task/2025102221-simple-solution.md

@@ -0,0 +1,343 @@
+# 简单方案:通过 department.leader 显示负责人
+
+**日期**: 2025-10-24  
+**核心思路**: 不修改数据库,只通过 `department.leader` 动态获取组长信息
+
+---
+
+## 🎯 方案对比
+
+### 方案1: 在 Project 表存储 assignee(之前的方案)
+
+```
+Project表
+├─ assignee: Pointer<Profile>  ← 需要迁移数据
+├─ department: Pointer<Department>
+└─ ...
+```
+
+**缺点**:
+- ❌ 需要迁移现有项目数据
+- ❌ 数据冗余(assignee 和 department.leader 重复)
+- ❌ 数据一致性问题(组长变更时需要同步更新)
+
+---
+
+### 方案2: 动态从 department.leader 获取(推荐)✅
+
+```
+Project表
+├─ department: Pointer<Department>  ← 只需要这个
+└─ ...
+
+Department表
+├─ leader: Pointer<Profile>  ← 从这里获取组长
+└─ ...
+```
+
+**优点**:
+- ✅ **不需要迁移数据**
+- ✅ 数据一致性好(组长只存在一个地方)
+- ✅ 逻辑清晰(项目属于项目组,项目组有组长)
+
+---
+
+## ✅ 实现方案
+
+### 修改1: 确保查询时包含 department.leader
+
+**文件**: `src/app/pages/admin/services/project.service.ts`
+
+已修改(第27行):
+```typescript
+async findProjects(options?: {
+  status?: string;
+  keyword?: string;
+  skip?: number;
+  limit?: number;
+}): Promise<FmodeObject[]> {
+  return await this.adminData.findAll('Project', {
+    include: ['customer', 'assignee', 'department', 'department.leader'], // ✅ 包含leader
+    skip: options?.skip || 0,
+    limit: options?.limit || 20,
+    descending: 'updatedAt',
+    // ...
+  });
+}
+```
+
+---
+
+### 修改2: toJSON 方法优先使用 department.leader
+
+**文件**: `src/app/pages/admin/services/project.service.ts`
+
+已修改(第243-256行):
+```typescript
+toJSON(project: FmodeObject): any {
+  const json = this.adminData.toJSON(project);
+
+  // 处理客户
+  if (json.customer && typeof json.customer === 'object') {
+    json.customerName = json.customer.name || '';
+    json.customerId = json.customer.objectId;
+  }
+
+  // ✅ 处理负责人:优先使用assignee,如果为空则使用department.leader
+  if (json.assignee && typeof json.assignee === 'object') {
+    // 如果有明确指定的assignee,使用它
+    json.assigneeName = json.assignee.name || '';
+    json.assigneeId = json.assignee.objectId;
+    json.assigneeRole = json.assignee.roleName || '';
+  } else if (json.department && typeof json.department === 'object') {
+    // 如果没有assignee,使用department的leader(组长)
+    const leader = json.department.leader;
+    if (leader && typeof leader === 'object') {
+      json.assigneeName = leader.name || '';
+      json.assigneeId = leader.objectId;
+      json.assigneeRole = '组长';  // ✅ 标记为组长
+    }
+  }
+
+  return json;
+}
+```
+
+---
+
+## 🔧 需要做的事情
+
+### 唯一需要做的:确保项目有 department 字段
+
+**检查方法**(在浏览器控制台执行):
+
+```javascript
+(async function() {
+  const Parse = window.FmodeParse.with('nova');
+  const company = localStorage.getItem('company');
+  
+  const query = new Parse.Query('Project');
+  query.equalTo('company', company);
+  query.notEqualTo('isDeleted', true);
+  query.include('department', 'department.leader');
+  query.limit(20);
+  
+  const projects = await query.find();
+  
+  let hasDept = 0;
+  let noDept = 0;
+  
+  projects.forEach(p => {
+    if (p.get('department')) {
+      hasDept++;
+    } else {
+      noDept++;
+    }
+  });
+  
+  console.log(`✅ 有department: ${hasDept} 个`);
+  console.log(`❌ 没有department: ${noDept} 个`);
+  
+  if (noDept > 0) {
+    console.log('\n⚠️ 有项目没有department,需要分配项目组');
+  } else {
+    console.log('\n🎉 所有项目都有department,可以直接显示组长!');
+  }
+})();
+```
+
+---
+
+### 如果有项目没有 department,执行以下脚本分配默认项目组:
+
+```javascript
+(async function() {
+  const Parse = window.FmodeParse.with('nova');
+  const company = localStorage.getItem('company');
+  
+  console.log('🚀 开始为项目分配默认项目组...');
+  
+  // 1. 查找没有department的项目
+  const projectQuery = new Parse.Query('Project');
+  projectQuery.equalTo('company', company);
+  projectQuery.notEqualTo('isDeleted', true);
+  projectQuery.doesNotExist('department');
+  projectQuery.limit(1000);
+  
+  const projects = await projectQuery.find();
+  console.log(`📊 找到 ${projects.length} 个没有项目组的项目`);
+  
+  if (projects.length === 0) {
+    console.log('✅ 所有项目都已有项目组!');
+    return;
+  }
+  
+  // 2. 获取默认项目组(第一个项目组)
+  const deptQuery = new Parse.Query('Department');
+  deptQuery.equalTo('company', company);
+  deptQuery.equalTo('type', 'project');
+  deptQuery.notEqualTo('isDeleted', true);
+  deptQuery.include('leader');
+  deptQuery.ascending('createdAt');
+  
+  const dept = await deptQuery.first();
+  
+  if (!dept) {
+    console.error('❌ 没有找到项目组,请先创建项目组');
+    return;
+  }
+  
+  const leader = dept.get('leader');
+  console.log(`✅ 使用默认项目组: ${dept.get('name')}, 组长: ${leader?.get('name') || '无'}`);
+  
+  // 3. 批量分配
+  let success = 0;
+  let failed = 0;
+  
+  for (let i = 0; i < projects.length; i++) {
+    const project = projects[i];
+    const title = project.get('title') || '未命名项目';
+    
+    try {
+      project.set('department', dept);
+      await project.save();
+      success++;
+      console.log(`✅ [${i+1}/${projects.length}] "${title}" 已分配到 ${dept.get('name')}`);
+    } catch (error) {
+      failed++;
+      console.error(`❌ [${i+1}/${projects.length}] "${title}" 失败:`, error);
+    }
+  }
+  
+  console.log('\n' + '='.repeat(60));
+  console.log('🎉 分配完成!');
+  console.log('='.repeat(60));
+  console.log(`📊 总计: ${projects.length} | ✅ 成功: ${success} | ❌ 失败: ${failed}`);
+  console.log('='.repeat(60));
+  console.log('\n💡 请刷新页面(Ctrl+Shift+R)查看结果');
+})();
+```
+
+---
+
+## 🎯 完整流程
+
+### 步骤1: 检查项目状态
+
+打开 `http://localhost:4200/admin/project-management`,按 F12,执行:
+
+```javascript
+(async function() {
+  const Parse = window.FmodeParse.with('nova');
+  const company = localStorage.getItem('company');
+  
+  const query = new Parse.Query('Project');
+  query.equalTo('company', company);
+  query.include('department', 'department.leader');
+  query.limit(20);
+  
+  const projects = await query.find();
+  
+  console.table(projects.map(p => ({
+    '项目名称': p.get('title'),
+    '有项目组': p.get('department') ? '✅' : '❌',
+    '组长': p.get('department')?.get('leader')?.get('name') || '无'
+  })));
+})();
+```
+
+---
+
+### 步骤2: 如果有项目缺少 department,执行分配脚本
+
+复制上面的"为项目分配默认项目组"脚本,粘贴到控制台执行。
+
+---
+
+### 步骤3: 刷新页面验证
+
+按 `Ctrl+Shift+R` 刷新页面,查看项目列表的"负责人"列。
+
+**预期结果**:
+- 所有项目的"负责人"列应显示组长名字
+- 不再显示"未分配"
+
+---
+
+## 📊 数据结构
+
+### Project 表(需要的字段)
+
+```javascript
+{
+  objectId: "APwk78jnrh",
+  title: "张家界凤凰城三期项目",
+  department: Pointer<Department> { objectId: "xxx" },  // ✅ 只需要这个
+  company: "cDL6R1hgSi",
+  status: "进行中",
+  // 不需要 assignee 字段
+}
+```
+
+### Department 表
+
+```javascript
+{
+  objectId: "xxx",
+  name: "汪奥组",
+  type: "project",
+  leader: Pointer<Profile> { objectId: "yyy" },  // ✅ 组长信息存在这里
+  company: "cDL6R1hgSi"
+}
+```
+
+### Profile 表
+
+```javascript
+{
+  objectId: "yyy",
+  name: "汪奥",
+  roleName: "组长",
+  company: "cDL6R1hgSi"
+}
+```
+
+---
+
+## 🎉 优势总结
+
+### 方案2(推荐)vs 方案1(之前)
+
+| 对比项 | 方案1(存储assignee) | 方案2(使用leader)✅ |
+|--------|---------------------|---------------------|
+| 需要迁移数据 | ❌ 是 | ✅ 否 |
+| 数据一致性 | ❌ 可能不一致 | ✅ 始终一致 |
+| 逻辑复杂度 | ❌ 较复杂 | ✅ 简单 |
+| 查询性能 | ✅ 稍快 | ✅ 同样快 |
+| 维护成本 | ❌ 高 | ✅ 低 |
+
+---
+
+## 💡 为什么这个方案更好?
+
+1. **逻辑清晰**
+   - 项目 → 属于项目组
+   - 项目组 → 有组长
+   - 项目的负责人 = 项目组的组长
+
+2. **数据一致性**
+   - 组长信息只存在一个地方(Department.leader)
+   - 组长变更时,所有项目的负责人自动更新
+
+3. **无需迁移**
+   - 只需确保项目有 `department` 字段
+   - 不需要为每个项目设置 `assignee` 字段
+
+4. **符合业务逻辑**
+   - 项目负责人就是项目组的组长
+   - 不需要额外的 `assignee` 字段
+
+---
+
+**现在执行检查脚本,看看是否所有项目都有 department!** 🚀
+

+ 247 - 0
src/app/pages/admin/services/project-migration.service.ts

@@ -0,0 +1,247 @@
+import { Injectable } from '@angular/core';
+import { FmodeParse } from 'fmode-ng/parse';
+
+const Parse = FmodeParse.with('nova');
+
+/**
+ * 项目数据迁移服务
+ * 用于批量更新现有项目的负责人信息
+ */
+@Injectable({
+  providedIn: 'root'
+})
+export class ProjectMigrationService {
+  
+  /**
+   * 批量更新项目负责人
+   * 为所有没有assignee的项目设置组长为负责人
+   */
+  async migrateProjectAssignees(): Promise<{
+    total: number;
+    success: number;
+    failed: number;
+    details: any[];
+  }> {
+    const company = localStorage.getItem('company');
+    if (!company) {
+      console.error('❌ 未找到公司ID');
+      return { total: 0, success: 0, failed: 0, details: [] };
+    }
+
+    try {
+      console.log('🚀 开始迁移项目负责人数据...');
+      
+      // 1. 查询所有没有assignee或department的项目
+      const projectQuery = new Parse.Query('Project');
+      projectQuery.equalTo('company', company);
+      projectQuery.notEqualTo('isDeleted', true);
+      projectQuery.limit(1000);
+      
+      const projects = await projectQuery.find();
+      console.log(`📊 找到 ${projects.length} 个项目`);
+
+      const results = {
+        total: projects.length,
+        success: 0,
+        failed: 0,
+        details: [] as any[]
+      };
+
+      // 2. 遍历每个项目
+      for (const project of projects) {
+        const projectId = project.id;
+        const projectTitle = project.get('title') || '未命名项目';
+        const existingAssignee = project.get('assignee');
+        const existingDepartment = project.get('department');
+
+        try {
+          // 如果已经有assignee,跳过
+          if (existingAssignee) {
+            console.log(`⏭️  项目 "${projectTitle}" 已有负责人,跳过`);
+            results.details.push({
+              projectId,
+              title: projectTitle,
+              status: 'skipped',
+              reason: '已有负责人'
+            });
+            continue;
+          }
+
+          // 如果有department,使用其leader
+          if (existingDepartment) {
+            const dept = existingDepartment;
+            await dept.fetch({ include: ['leader'] });
+            const leader = dept.get('leader');
+            
+            if (leader) {
+              project.set('assignee', leader);
+              await project.save();
+              results.success++;
+              console.log(`✅ 项目 "${projectTitle}" 已设置负责人: ${leader.get('name')}`);
+              results.details.push({
+                projectId,
+                title: projectTitle,
+                status: 'success',
+                assignee: leader.get('name')
+              });
+            } else {
+              results.failed++;
+              console.warn(`⚠️  项目 "${projectTitle}" 的项目组没有组长`);
+              results.details.push({
+                projectId,
+                title: projectTitle,
+                status: 'failed',
+                reason: '项目组没有组长'
+              });
+            }
+            continue;
+          }
+
+          // 如果既没有assignee也没有department,尝试查找默认项目组
+          console.log(`🔍 项目 "${projectTitle}" 没有项目组,尝试查找默认项目组...`);
+          
+          // 查找第一个项目组
+          const deptQuery = new Parse.Query('Department');
+          deptQuery.equalTo('company', company);
+          deptQuery.equalTo('type', 'project');
+          deptQuery.notEqualTo('isDeleted', true);
+          deptQuery.include('leader');
+          deptQuery.ascending('createdAt');
+          deptQuery.limit(1);
+          
+          const defaultDept = await deptQuery.first();
+          
+          if (defaultDept) {
+            const leader = defaultDept.get('leader');
+            if (leader) {
+              project.set('department', defaultDept);
+              project.set('assignee', leader);
+              await project.save();
+              results.success++;
+              console.log(`✅ 项目 "${projectTitle}" 已分配到默认项目组,负责人: ${leader.get('name')}`);
+              results.details.push({
+                projectId,
+                title: projectTitle,
+                status: 'success',
+                assignee: leader.get('name'),
+                department: defaultDept.get('name')
+              });
+            } else {
+              results.failed++;
+              console.warn(`⚠️  默认项目组没有组长`);
+              results.details.push({
+                projectId,
+                title: projectTitle,
+                status: 'failed',
+                reason: '默认项目组没有组长'
+              });
+            }
+          } else {
+            results.failed++;
+            console.warn(`⚠️  项目 "${projectTitle}" 无法找到项目组`);
+            results.details.push({
+              projectId,
+              title: projectTitle,
+              status: 'failed',
+              reason: '没有可用的项目组'
+            });
+          }
+
+        } catch (error) {
+          results.failed++;
+          console.error(`❌ 更新项目 "${projectTitle}" 失败:`, error);
+          results.details.push({
+            projectId,
+            title: projectTitle,
+            status: 'error',
+            error: (error as Error).message
+          });
+        }
+      }
+
+      console.log('🎉 迁移完成!');
+      console.log(`📊 总计: ${results.total}, 成功: ${results.success}, 失败: ${results.failed}`);
+      
+      return results;
+
+    } catch (error) {
+      console.error('❌ 迁移失败:', error);
+      throw error;
+    }
+  }
+
+  /**
+   * 为单个项目设置负责人
+   */
+  async setProjectAssignee(projectId: string, assigneeId: string): Promise<boolean> {
+    try {
+      const projectQuery = new Parse.Query('Project');
+      const project = await projectQuery.get(projectId);
+      
+      const assigneeQuery = new Parse.Query('Profile');
+      const assignee = await assigneeQuery.get(assigneeId);
+      
+      project.set('assignee', assignee);
+      await project.save();
+      
+      console.log('✅ 项目负责人设置成功');
+      return true;
+    } catch (error) {
+      console.error('❌ 设置项目负责人失败:', error);
+      return false;
+    }
+  }
+
+  /**
+   * 检查项目数据状态
+   */
+  async checkProjectsStatus(): Promise<{
+    total: number;
+    hasAssignee: number;
+    hasDepartment: number;
+    needsMigration: number;
+  }> {
+    const company = localStorage.getItem('company');
+    if (!company) {
+      console.error('❌ 未找到公司ID');
+      return { total: 0, hasAssignee: 0, hasDepartment: 0, needsMigration: 0 };
+    }
+
+    try {
+      const projectQuery = new Parse.Query('Project');
+      projectQuery.equalTo('company', company);
+      projectQuery.notEqualTo('isDeleted', true);
+      projectQuery.include('assignee');
+      projectQuery.include('department');
+      projectQuery.limit(1000);
+      
+      const projects = await projectQuery.find();
+      
+      let hasAssignee = 0;
+      let hasDepartment = 0;
+      let needsMigration = 0;
+      
+      projects.forEach(project => {
+        if (project.get('assignee')) hasAssignee++;
+        if (project.get('department')) hasDepartment++;
+        if (!project.get('assignee')) needsMigration++;
+      });
+      
+      const status = {
+        total: projects.length,
+        hasAssignee,
+        hasDepartment,
+        needsMigration
+      };
+      
+      console.log('📊 项目状态检查:', status);
+      return status;
+      
+    } catch (error) {
+      console.error('❌ 检查失败:', error);
+      throw error;
+    }
+  }
+}
+
+

+ 42 - 3
src/app/pages/admin/services/project.service.ts

@@ -1,6 +1,9 @@
 import { Injectable } from '@angular/core';
 import { AdminDataService } from './admin-data.service';
 import { FmodeObject } from 'fmode-ng/core';
+import { FmodeParse } from 'fmode-ng/parse';
+
+const Parse = FmodeParse.with('nova');
 
 /**
  * 项目管理数据服务
@@ -21,7 +24,7 @@ export class ProjectService {
     limit?: number;
   }): Promise<FmodeObject[]> {
     return await this.adminData.findAll('Project', {
-      include: ['customer', 'assignee'],
+      include: ['customer', 'assignee', 'department', 'department.leader'], // ✅ 添加department和leader
       skip: options?.skip || 0,
       limit: options?.limit || 20,
       descending: 'updatedAt',
@@ -57,7 +60,9 @@ export class ProjectService {
   async getProject(objectId: string): Promise<FmodeObject | null> {
     return await this.adminData.getById('Project', objectId, [
       'customer',
-      'assignee'
+      'assignee',
+      'department',
+      'department.leader' // ✅ 添加department和leader
     ]);
   }
 
@@ -68,6 +73,7 @@ export class ProjectService {
     title: string;
     customerId?: string;
     assigneeId?: string;
+    departmentId?: string; // ✅ 新增:项目组ID
     status?: string;
     currentStage?: string;
     deadline?: Date;
@@ -88,7 +94,30 @@ export class ProjectService {
       };
     }
 
-    // 设置负责人指针
+    // ✅ 新增:如果提供了项目组,获取组长作为默认负责人
+    if (data.departmentId) {
+      try {
+        const departmentQuery = new Parse.Query('Department');
+        departmentQuery.include('leader');
+        const department = await departmentQuery.get(data.departmentId);
+        
+        if (department) {
+          projectData.department = department.toPointer();
+          
+          // 获取组长
+          const leader = department.get('leader');
+          if (leader && !data.assigneeId) {
+            // 如果没有明确指定负责人,使用组长
+            projectData.assignee = leader.toPointer();
+            console.log('✅ 项目负责人默认为组长:', leader.get('name'));
+          }
+        }
+      } catch (error) {
+        console.error('❌ 获取项目组失败:', error);
+      }
+    }
+
+    // 设置负责人指针(如果明确指定,覆盖组长)
     if (data.assigneeId) {
       projectData.assignee = {
         __type: 'Pointer',
@@ -211,9 +240,19 @@ export class ProjectService {
       json.customerId = json.customer.objectId;
     }
 
+    // ✅ 处理负责人:优先使用assignee,如果为空则使用department.leader
     if (json.assignee && typeof json.assignee === 'object') {
       json.assigneeName = json.assignee.name || '';
       json.assigneeId = json.assignee.objectId;
+      json.assigneeRole = json.assignee.roleName || '';
+    } else if (json.department && typeof json.department === 'object') {
+      // 如果没有assignee,但有department和leader,使用leader(组长)
+      const leader = json.department.leader;
+      if (leader && typeof leader === 'object') {
+        json.assigneeName = leader.name || '';
+        json.assigneeId = leader.objectId;
+        json.assigneeRole = '组长';
+      }
     }
 
     return json;

+ 20 - 5
src/app/pages/team-leader/dashboard/dashboard.ts

@@ -1866,24 +1866,39 @@ export class Dashboard implements OnInit, OnDestroy {
       };
     }
 
-    // 获取所有设计师
-    const assigned = this.filteredProjects.filter(p => !!p.designerName);
-    const designers = Array.from(new Set(assigned.map(p => p.designerName)));
+    // ✅ 获取所有真实设计师(优先使用realDesigners)
+    let designers: string[] = [];
+    
+    if (this.realDesigners && this.realDesigners.length > 0) {
+      // 使用真实的设计师列表
+      designers = this.realDesigners.map(d => d.name);
+      console.log('✅ 使用真实设计师列表:', designers.length, '人');
+    } else {
+      // 降级:从已分配的项目中提取设计师(过滤掉"未分配")
+      const assigned = this.filteredProjects.filter(p => p.designerName && p.designerName !== '未分配');
+      designers = Array.from(new Set(assigned.map(p => p.designerName)));
+      console.warn('⚠️ 使用项目中提取的设计师列表:', designers.length, '人');
+    }
     
     if (designers.length === 0) {
       // 没有设计师数据,显示空状态
       const emptyOption = {
         title: {
-          text: '暂无项目数据',
+          text: '暂无组员数据',
+          subtext: '请先在系统中添加设计师(组员角色)',
           left: 'center',
           top: 'center',
-          textStyle: { fontSize: 16, color: '#9ca3af' }
+          textStyle: { fontSize: 16, color: '#9ca3af' },
+          subtextStyle: { fontSize: 13, color: '#d1d5db' }
         }
       };
       this.workloadGanttChart.setOption(emptyOption, true);
       return;
     }
     
+    // 获取所有已分配的项目(过滤掉"未分配")
+    const assigned = this.filteredProjects.filter(p => p.designerName && p.designerName !== '未分配');
+    
     // 计算每个设计师的每日工作状态
     const workloadByDesigner: Record<string, any[]> = {};
     designers.forEach(name => {

+ 39 - 4
src/app/pages/team-leader/services/designer.service.ts

@@ -70,27 +70,62 @@ export class DesignerService {
     if (!Parse) return [];
     
     try {
+      // 查询所有组员(设计师角色)
       const query = new Parse.Query('Profile');
       query.equalTo('company', this.cid);
-      query.equalTo('roleName', '组员'); // 设计师角色
+      query.equalTo('roleName', '组员'); // 只查询组员
       query.notEqualTo('isDeleted', true);
       query.include('department'); // 关联部门
+      query.include('user'); // 🔧 关联用户信息,尝试获取名字
       query.limit(1000);
       
       const profiles = await query.find();
       
-      console.log(`✅ 获取到 ${profiles.length} 个设计师`);
+      console.log(`✅ 获取到 ${profiles.length} 个设计师(组员)`);
       
-      return profiles.map((p: any) => {
+      return profiles.map((p: any, index: number) => {
         const data = p.get('data') || {};
         const tags = data.tags || this.getDefaultTags();
         
+        // 🔧 尝试多种方式获取名字(优先级从高到低)
+        let name = p.get('name'); // 1. 优先使用 Profile.name
+        
+        if (!name) {
+          // 2. 尝试从 user 获取
+          const user = p.get('user');
+          if (user) {
+            name = user.get ? user.get('name') : user.name;
+            if (!name && user.get) {
+              name = user.get('nickname') || user.get('realName');
+            }
+          }
+        }
+        
+        if (!name) {
+          // 3. 使用手机号作为备用显示
+          const mobile = p.get('mobile');
+          if (mobile) {
+            name = `设计师-${mobile.slice(-4)}`; // 显示为 "设计师-1234"
+          }
+        }
+        
+        if (!name) {
+          // 4. 最后使用 ID 后4位
+          name = `设计师-${p.id.slice(-4)}`;
+        }
+        
+        // 如果使用了备用名称,发出警告
+        if (!p.get('name')) {
+          console.warn(`⚠️ Profile ${p.id} 缺少 name 字段,使用备用显示: ${name}`);
+        }
+        
         return {
           id: p.id,
-          name: p.get('name'),
+          name: name,
           mobile: p.get('mobile') || '',
           department: p.get('department')?.get?.('name') || '未分组',
           departmentId: p.get('department')?.id || '',
+          roleName: p.get('roleName'),
           tags,
           data,
           profile: p

+ 17 - 0
src/modules/project/components/team-assign/team-assign.component.ts

@@ -130,6 +130,23 @@ export class TeamAssignComponent implements OnInit {
     this.selectedDesigner = null;
     this.departmentMembers = [];
 
+    // ✅ 自动设置组长为项目负责人
+    const leader = department.get('leader');
+    if (leader && this.project) {
+      try {
+        // 更新项目的assignee字段为组长
+        this.project.set('assignee', leader);
+        this.project.set('department', department);
+        await this.project.save();
+        console.log('✅ 项目负责人已设置为组长:', leader.get('name'));
+        
+        // 触发界面更新
+        this.cdr.markForCheck();
+      } catch (error) {
+        console.error('❌ 设置项目负责人失败:', error);
+      }
+    }
+
     await this.loadDepartmentMembers(department);
   }