Browse Source

fix: stage err & task admin

ryanemax 23 hours ago
parent
commit
dec68e0255

+ 27 - 0
docs/task/2025101701-admin.md

@@ -0,0 +1,27 @@
+# 任务:管理系统数据对接
+所有数据 .company 指向 cDL6R1hgSi 映三色帐套
+
+请您查看./rules/schemas.md数据结构,完成./src/app/pages/admin/所有组件数据的増删查改
+
+## 各组件对接提示
+- 总览看板
+    - 数据通过各个表结构,查询进行统计
+    - 最新动态从ProjectChange获取,附加Project表根据createdAt的创建项目事件列表
+- 项目管理 Project
+- 项目组 Department
+    - 组长对应字段 .leader Pointer<Profile>
+    - 项目组默认 .type project
+    - 创建项目组,需要提供组长选择
+- 设计师管理 => 改员工管理 Profile
+    - 主要根据.roleName区分 客服 组员 组长 人事 财务 五个身份
+        - 不再通过在线、忙碌、离线统计,而是统计各个身份人员数量,点击快捷筛选
+- 客户管理 ContactInfo
+- 群组管理 GroupChat (需要创建新的组件)
+    - 注意要.include查询显示当前项目信息
+- 财务管理(隐藏)
+- 系统设置三个入口(隐藏)
+
+
+## 注意事项
+- 考虑到群组、员工,是严格从企业微信同步过来,有userid和chat_id对应关系
+    - 因此这两个数据不提供新增、删除功能,只提供编辑,禁用(isDisabled字段)

+ 17 - 14
rules/schemas.md

@@ -56,6 +56,7 @@ TABLE(Profile, "Profile\n员工档案表") {
     FIELD(objectId, String)
     FIELD(name, String)
     FIELD(mobile, String)
+    FIELD(department, Pointer→Department)
     FIELD(company, Pointer→Company)
     FIELD(userId, String)
     FIELD(roleName, String)
@@ -224,14 +225,14 @@ TABLE(ProjectIssue, "ProjectIssue\n异常记录表") {
     FIELD(isDeleted, Boolean)
 }
 
-' ============ 沟通记录 ============
-TABLE(Communication, "Communication\n沟通记录表") {
+' ============ 跟进记录 ============
+TABLE(ContactFollow, "ContactFollow\n跟进记录表") {
     FIELD(objectId, String)
     FIELD(project, Pointer→Project)
     FIELD(sender, Pointer→Profile/ContactInfo)
     FIELD(content, String)
-    FIELD(communicationType, String)
-    FIELD(relatedStage, String)
+    FIELD(type, String)
+    FIELD(stage, String)
     FIELD(attachments, Array)
     FIELD(data, Object)
     FIELD(isDeleted, Boolean)
@@ -263,7 +264,7 @@ ProjectSettlement "1" --> "n" ProjectVoucher : 付款凭证
 Project "1" --> "n" ProjectFeedback : 客户反馈
 Project "1" --> "n" ProductCheck : 质量检查
 Project "1" --> "n" ProjectIssue : 异常记录
-Project "1" --> "n" Communication : 沟通记录
+Project "1" --> "n" ContactFollow : 跟进记录
 
 ' 群聊关系
 GroupChat "n" --> "1" Company : 所属企业
@@ -322,6 +323,7 @@ GroupChat "n" --> "1" Project : 关联项目(可选)
 | objectId | String | 是 | 主键ID | "prof001" |
 | name | String | 是 | 员工姓名 | "张三" |
 | mobile | String | 否 | 手机号 | "13800138000" |
+| department | Pointer | 是 | 所属小组 | → Department |
 | company | Pointer | 是 | 所属企业 | → Company |
 | userId | String | 否 | 企微UserID | "zhangsan" |
 | roleName | String | 是 | 员工角色 | "客服" / "组员" / "组长" |
@@ -1157,7 +1159,7 @@ Product.quotation 产品报价字段
 ```json
 {
   "assignedTo": "prof003",
-  "relatedStage": "建模",
+  "stage": "建模",
   "impact": "预计延期2天",
   "preventiveMeasures": ["增加备份频率", "升级建模软件"],
   "resolvedAt": "2024-10-16T15:00:00.000Z"
@@ -1171,25 +1173,26 @@ Product.quotation 产品报价字段
 
 ---
 
-### 17. Communication(沟通记录表)
+### 17. ContactFollow(跟进记录表)
 
-**用途**: 记录项目相关的沟通历史。
+**用途**: 记录项目相关的跟进历史。
 
 | 字段名 | 类型 | 必填 | 说明 | 示例值 |
 |--------|------|------|------|--------|
 | objectId | String | 是 | 主键ID | "comm001" |
 | project | Pointer | 是 | 所属项目 | → Project |
-| sender | Pointer | 是 | 发送人 | → Profile / ContactInfo |
-| content | String | 是 | 沟通内容 | "客厅效果图已发送,请查收" |
-| communicationType | String | 是 | 沟通类型 | "message" / "call" / "meeting" |
-| relatedStage | String | 否 | 关联阶段 | "渲染" |
+| profile | Pointer | 是 | 跟进人 | → Profile |
+| contact | Pointer | 是 | 联系人 | → ContactInfo |
+| content | String | 是 | 跟进内容 | "客厅效果图已发送,请查收" |
+| type | String | 是 | 跟进类型 | "message" / "call" / "meeting" |
+| stage | String | 否 | 关联阶段 | "渲染" |
 | attachments | Array | 否 | 附件列表 | ["https://..."] |
 | data | Object | 否 | 扩展数据 | { isRead, priority, ... } |
 | isDeleted | Boolean | 否 | 软删除标记 | false |
 | createdAt | Date | 自动 | 创建时间 | 2024-01-01T00:00:00.000Z |
 | updatedAt | Date | 自动 | 更新时间 | 2024-01-01T00:00:00.000Z |
 
-**communicationType 枚举值**:
+**type 枚举值**:
 - `message`: 文字消息
 - `call`: 电话沟通
 - `meeting`: 会议
@@ -1199,7 +1202,7 @@ Product.quotation 产品报价字段
 **索引建议**:
 - `project + createdAt`
 - `sender + project`
-- `relatedStage`
+- `stage`
 
 ---
 

+ 2 - 0
src/app/app.config.ts

@@ -10,6 +10,8 @@ import { routes } from './app.routes';
 import { provideIonicAngular } from '@ionic/angular/standalone';
 import { Diagnostic } from '@awesome-cordova-plugins/diagnostic/ngx';
 
+localStorage.setItem("company","cDL6R1hgSi")
+
 export const appConfig: ApplicationConfig = {
   providers: [
     Diagnostic,

+ 3 - 3
src/modules/project/config/quotation-rules.ts

@@ -250,7 +250,7 @@ export const QUOTATION_PRICE_TABLE = {
 /**
  * 加价规则配置
  */
-export const ADJUSTMENT_RULES = {
+export const ADJUSTMENT_RULES:any = {
   // 家装加价规则
   家装: {
     extraFunction: {
@@ -366,8 +366,8 @@ export function calculateFinalPrice(
   const rules = ADJUSTMENT_RULES[projectType];
 
   // 功能区加价
-  if (adjustments.extraFunction && rules.extraFunction) {
-    price += (rules.extraFunction as any).amount * adjustments.extraFunction;
+  if (adjustments.extraFunction && rules?.extraFunction) {
+    price += (rules?.extraFunction as any).amount * adjustments.extraFunction;
   }
 
   // 造型复杂度加价

+ 5 - 6
src/modules/project/pages/project-detail/stages/stage-aftercare.component.ts

@@ -3,6 +3,7 @@ import { CommonModule } from '@angular/common';
 import { FormsModule } from '@angular/forms';
 import { ActivatedRoute } from '@angular/router';
 import { FmodeObject, FmodeParse } from 'fmode-ng/parse';
+import { ProjectUploadService } from '../../../services/upload.service';
 
 const Parse = FmodeParse.with('nova');
 
@@ -84,7 +85,8 @@ export class StageAftercareComponent implements OnInit {
   saving: boolean = false;
 
   constructor(
-    private route: ActivatedRoute
+    private route: ActivatedRoute,
+    private uploadService:ProjectUploadService
   ) {}
 
   async ngOnInit() {
@@ -104,7 +106,7 @@ export class StageAftercareComponent implements OnInit {
       this.loading = true;
 
       if (!this.project && this.projectId) {
-        const query = Parse.Query.from('Project');
+        const query = new Parse.Query('Project');
         query.include('customer', 'assignee');
         this.project = await query.get(this.projectId);
         this.customer = this.project.get('customer');
@@ -180,10 +182,7 @@ export class StageAftercareComponent implements OnInit {
     try {
       this.uploading = true;
 
-      // 直接使用Parse File上传
-      const parseFile = new Parse.File(file.name, file);
-      await parseFile.save();
-      const url = parseFile.url();
+      let url = await this.uploadService?.uploadFile(file)
 
       // 暂时不使用OCR,需要手动输入金额和支付方式
       this.finalPayment.paymentVouchers.push({

+ 7 - 6
src/modules/project/pages/project-detail/stages/stage-delivery.component.ts

@@ -3,6 +3,7 @@ import { CommonModule } from '@angular/common';
 import { FormsModule } from '@angular/forms';
 import { ActivatedRoute } from '@angular/router';
 import { FmodeObject, FmodeParse } from 'fmode-ng/parse';
+import { ProjectUploadService } from '../../../services/upload.service';
 
 const Parse = FmodeParse.with('nova');
 
@@ -114,7 +115,10 @@ export class StageDeliveryComponent implements OnInit {
   uploading: boolean = false;
   saving: boolean = false;
 
-  constructor(private route: ActivatedRoute) {}
+  constructor(
+    private route: ActivatedRoute,
+    private uploadService: ProjectUploadService
+  ) {}
 
   async ngOnInit() {
     // 尝试从父组件获取数据(如果通过@Input传入)
@@ -136,7 +140,7 @@ export class StageDeliveryComponent implements OnInit {
 
       // 如果没有传入project,从路由参数加载
       if (!this.project && this.projectId) {
-        const query = Parse.Query.from('Project');
+        const query = new Parse.Query('Project');
         query.include('customer', 'assignee');
         this.project = await query.get(this.projectId);
         this.customer = this.project.get('customer');
@@ -231,10 +235,7 @@ export class StageDeliveryComponent implements OnInit {
     try {
       this.uploading = true;
 
-      // 使用Parse File上传
-      const parseFile = new Parse.File(file.name, file);
-      await parseFile.save();
-      const url = parseFile.url();
+      let url = await this.uploadService?.uploadFile(file)
 
       const deliverable = this.getDeliverable(spaceName, processType);
       if (deliverable) {

+ 3 - 3
src/modules/project/pages/project-detail/stages/stage-order.component.html

@@ -173,7 +173,7 @@
             <div class="form-group">
               <label class="form-label">风格等级 <span class="required">*</span></label>
               <div class="radio-group">
-                @for (styleLevel of Object.keys(styleLevels); track styleLevel) {
+                @for (styleLevel of getLevels(styleLevels); track styleLevel) {
                   <label class="radio-label">
                     <input
                       type="radio"
@@ -253,7 +253,7 @@
               <button
                 class="btn btn-primary"
                 (click)="generateQuotation()"
-                [disabled]="!canEdit || !homeScenes.rooms.some(r => r.selected)">
+                [disabled]="hasRoomsDisabled()">
                 生成报价表
               </button>
             </div>
@@ -364,7 +364,7 @@
               <button
                 class="btn btn-primary"
                 (click)="generateQuotation()"
-                [disabled]="!canEdit || !commercialScenes.spaces.some(s => s.selected)">
+                [disabled]="hasSpacesDisabled()">
                 生成报价表
               </button>
             </div>

+ 18 - 9
src/modules/project/pages/project-detail/stages/stage-order.component.ts

@@ -49,6 +49,16 @@ export class StageOrderComponent implements OnInit {
     priceLevel: '一级' // 一级(老客户) | 二级(中端组) | 三级(高端组)
   };
 
+  hasSpacesDisabled(){
+    return !this.canEdit || !this.commercialScenes?.spaces?.some(s => s?.selected)
+  }
+  hasRoomsDisabled(){
+    return !this.canEdit || !this.homeScenes?.rooms?.some((r:any) => r?.selected)
+  }
+  getLevels(levels:any){
+    if(!levels) return []
+    return Object.keys(levels)
+  }
   // 场景选择(家装)
   homeScenes = {
     spaceType: '', // 平层 | 跃层 | 挑空
@@ -150,7 +160,7 @@ export class StageOrderComponent implements OnInit {
 
       // 使用FmodeParse加载项目、客户、当前用户
       if (!this.project && this.projectId) {
-        const query = Parse.Query.from('Project');
+        const query = new Parse.Query('Project');
         query.include('customer', 'assignee', 'department');
         this.project = await query.get(this.projectId);
         this.customer = this.project.get('customer');
@@ -205,7 +215,7 @@ export class StageOrderComponent implements OnInit {
       }
 
       // 使用FmodeParse加载项目组列表(Department表)
-      const deptQuery = Parse.Query.from('Department');
+      const deptQuery = new Parse.Query('Department');
       deptQuery.equalTo('type', 'project');
       deptQuery.notEqualTo('isDeleted', true);
       deptQuery.ascending('name');
@@ -417,27 +427,26 @@ export class StageOrderComponent implements OnInit {
   /**
    * 加载项目组成员
    */
-  async loadDepartmentMembers(departmentId: string) {
+  async loadDepartmentMembers(departmentId: string|undefined) {
+    if(!departmentId) return []
     try {
       this.loadingMembers = true;
 
       // 使用FmodeParse查询项目组的组员(Profile表,roleName为"组员")
-      const query = Parse.Query.from('Profile');
+      const query = new Parse.Query('Profile');
+      query.equalTo('department', departmentId);
       query.equalTo('roleName', '组员');
       query.notEqualTo('isDeleted', true);
       query.ascending('name');
 
-      // TODO: 根据实际数据结构过滤特定项目组的成员
-      // 如果Profile表有department字段,可以这样查询:
-      // const deptPointer = Parse.Object.from('Department', departmentId);
-      // query.equalTo('department', deptPointer);
-
       this.departmentMembers = await query.find();
+      return this.departmentMembers
     } catch (err) {
       console.error('加载项目组成员失败:', err);
     } finally {
       this.loadingMembers = false;
     }
+    return []
   }
 
   /**

+ 5 - 10
src/modules/project/pages/project-detail/stages/stage-requirements.component.ts

@@ -227,12 +227,10 @@ export class StageRequirementsComponent implements OnInit {
     try {
       this.uploading = true;
 
-      // 直接使用Parse File上传
-      const parseFile = new Parse.File(file.name, file);
-      await parseFile.save();
-      const url = parseFile.url();
+      //
+      let url = await this.uploadService?.uploadFile(file)
 
-      this.referenceImages.push({
+      url&&this.referenceImages.push({
         url: url,
         name: file.name,
         type: 'style',
@@ -281,12 +279,9 @@ export class StageRequirementsComponent implements OnInit {
     try {
       this.uploading = true;
 
-      // 直接使用Parse File上传
-      const parseFile = new Parse.File(file.name, file);
-      await parseFile.save();
-      const url = parseFile.url();
+      let url = await this.uploadService?.uploadFile(file)
 
-      this.cadFiles.push({
+      url&&this.cadFiles.push({
         url: url,
         name: file.name,
         uploadTime: new Date(),