diff --git a/.DS_Store b/.DS_Store index fd0ca7a..b1a77dd 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..4caf1a0 --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/encodings.xml b/.idea/encodings.xml new file mode 100644 index 0000000..dfc8d74 --- /dev/null +++ b/.idea/encodings.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml new file mode 100644 index 0000000..712ab9d --- /dev/null +++ b/.idea/jarRepositories.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..1d351d1 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,14 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..6f4a674 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/unis_crm.iml b/.idea/unis_crm.iml new file mode 100644 index 0000000..d6ebd48 --- /dev/null +++ b/.idea/unis_crm.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/workspace.xml b/.idea/workspace.xml new file mode 100644 index 0000000..0a37f6d --- /dev/null +++ b/.idea/workspace.xml @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1773970620258 + + + + + + \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..e0f15db --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "java.configuration.updateBuildConfiguration": "automatic" +} \ No newline at end of file diff --git a/backend/README.md b/backend/README.md index 7a7e535..b034844 100644 --- a/backend/README.md +++ b/backend/README.md @@ -1,17 +1,65 @@ # Backend -该目录用于存放后端代码,当前仅完成结构预留,方便后续继续开发。 +当前已经补齐首页所需的 Java 后端基础工程,并调整为常规 Spring Boot 单体项目目录结构,技术栈如下: -建议后续可按下面方式扩展: +- Java 17 +- Spring Boot 3.2.2 +- MyBatis Plus 3.5.6 +- PostgreSQL +- Redis + +## 已实现内容 + +- 首页聚合接口:`GET /api/dashboard/home` +- 首页欢迎信息:姓名、职位、部门、入职天数 +- 首页统计卡片 +- 首页待办列表 +- 首页最新动态 +- 通用响应体与全局异常处理 + +## 目录结构 ```text backend/ -├── src/ -│ ├── controllers/ -│ ├── routes/ -│ ├── services/ -│ ├── models/ -│ └── app.ts -├── package.json -└── README.md +├── pom.xml +└── src/main + ├── java/com/unis/crm + │ ├── common + │ ├── controller + │ ├── dto + │ ├── mapper + │ ├── service + │ └── UnisCrmBackendApplication.java + └── resources + ├── application.yml + └── mapper/dashboard ``` + +## 启动前准备 + +1. 执行数据库脚本: + +```bash +psql -h 127.0.0.1 -U postgres -d nex_auth -f sql/init_pg17.sql +``` + +2. 确保 `sys_user`、`work_todo`、`sys_activity_log`、`crm_customer`、`crm_opportunity`、`work_checkin` 中有业务数据。 + +## 启动项目 + +```bash +cd backend +mvn spring-boot:run +``` + +默认启动在 `8081` 端口,供前端开发环境通过 Vite 代理访问;`8080` 可继续保留给现有认证/系统服务。 + +## 首页接口 + +请求示例: + +```bash +curl -H "X-User-Id: 1" "http://127.0.0.1:8081/api/dashboard/home" +``` + +首页接口只允许查询当前登录用户自己的数据,必须通过 `X-User-Id` 传入当前用户ID,不支持指定其他用户查询。 diff --git a/backend/cp.txt b/backend/cp.txt new file mode 100644 index 0000000..1fa194c --- /dev/null +++ b/backend/cp.txt @@ -0,0 +1 @@ +/Users/kangwenjing/.m2/repository/org/springframework/boot/spring-boot-starter-web/3.2.2/spring-boot-starter-web-3.2.2.jar:/Users/kangwenjing/.m2/repository/org/springframework/boot/spring-boot-starter/3.2.2/spring-boot-starter-3.2.2.jar:/Users/kangwenjing/.m2/repository/org/springframework/boot/spring-boot/3.2.2/spring-boot-3.2.2.jar:/Users/kangwenjing/.m2/repository/org/springframework/boot/spring-boot-starter-logging/3.2.2/spring-boot-starter-logging-3.2.2.jar:/Users/kangwenjing/.m2/repository/ch/qos/logback/logback-classic/1.4.14/logback-classic-1.4.14.jar:/Users/kangwenjing/.m2/repository/ch/qos/logback/logback-core/1.4.14/logback-core-1.4.14.jar:/Users/kangwenjing/.m2/repository/org/apache/logging/log4j/log4j-to-slf4j/2.21.1/log4j-to-slf4j-2.21.1.jar:/Users/kangwenjing/.m2/repository/org/apache/logging/log4j/log4j-api/2.21.1/log4j-api-2.21.1.jar:/Users/kangwenjing/.m2/repository/org/slf4j/jul-to-slf4j/2.0.11/jul-to-slf4j-2.0.11.jar:/Users/kangwenjing/.m2/repository/jakarta/annotation/jakarta.annotation-api/2.1.1/jakarta.annotation-api-2.1.1.jar:/Users/kangwenjing/.m2/repository/org/yaml/snakeyaml/2.2/snakeyaml-2.2.jar:/Users/kangwenjing/.m2/repository/org/springframework/boot/spring-boot-starter-json/3.2.2/spring-boot-starter-json-3.2.2.jar:/Users/kangwenjing/.m2/repository/com/fasterxml/jackson/core/jackson-databind/2.15.3/jackson-databind-2.15.3.jar:/Users/kangwenjing/.m2/repository/com/fasterxml/jackson/core/jackson-annotations/2.15.3/jackson-annotations-2.15.3.jar:/Users/kangwenjing/.m2/repository/com/fasterxml/jackson/core/jackson-core/2.15.3/jackson-core-2.15.3.jar:/Users/kangwenjing/.m2/repository/com/fasterxml/jackson/datatype/jackson-datatype-jdk8/2.15.3/jackson-datatype-jdk8-2.15.3.jar:/Users/kangwenjing/.m2/repository/com/fasterxml/jackson/datatype/jackson-datatype-jsr310/2.15.3/jackson-datatype-jsr310-2.15.3.jar:/Users/kangwenjing/.m2/repository/com/fasterxml/jackson/module/jackson-module-parameter-names/2.15.3/jackson-module-parameter-names-2.15.3.jar:/Users/kangwenjing/.m2/repository/org/springframework/boot/spring-boot-starter-tomcat/3.2.2/spring-boot-starter-tomcat-3.2.2.jar:/Users/kangwenjing/.m2/repository/org/apache/tomcat/embed/tomcat-embed-core/10.1.18/tomcat-embed-core-10.1.18.jar:/Users/kangwenjing/.m2/repository/org/apache/tomcat/embed/tomcat-embed-websocket/10.1.18/tomcat-embed-websocket-10.1.18.jar:/Users/kangwenjing/.m2/repository/org/springframework/spring-web/6.1.3/spring-web-6.1.3.jar:/Users/kangwenjing/.m2/repository/org/springframework/spring-webmvc/6.1.3/spring-webmvc-6.1.3.jar:/Users/kangwenjing/.m2/repository/org/springframework/boot/spring-boot-starter-validation/3.2.2/spring-boot-starter-validation-3.2.2.jar:/Users/kangwenjing/.m2/repository/org/apache/tomcat/embed/tomcat-embed-el/10.1.18/tomcat-embed-el-10.1.18.jar:/Users/kangwenjing/.m2/repository/org/hibernate/validator/hibernate-validator/8.0.1.Final/hibernate-validator-8.0.1.Final.jar:/Users/kangwenjing/.m2/repository/jakarta/validation/jakarta.validation-api/3.0.2/jakarta.validation-api-3.0.2.jar:/Users/kangwenjing/.m2/repository/org/jboss/logging/jboss-logging/3.5.3.Final/jboss-logging-3.5.3.Final.jar:/Users/kangwenjing/.m2/repository/com/fasterxml/classmate/1.6.0/classmate-1.6.0.jar:/Users/kangwenjing/.m2/repository/org/springframework/boot/spring-boot-starter-aop/3.2.2/spring-boot-starter-aop-3.2.2.jar:/Users/kangwenjing/.m2/repository/org/springframework/spring-aop/6.1.3/spring-aop-6.1.3.jar:/Users/kangwenjing/.m2/repository/org/aspectj/aspectjweaver/1.9.21/aspectjweaver-1.9.21.jar:/Users/kangwenjing/.m2/repository/org/springframework/security/spring-security-core/6.2.1/spring-security-core-6.2.1.jar:/Users/kangwenjing/.m2/repository/org/springframework/security/spring-security-crypto/6.2.1/spring-security-crypto-6.2.1.jar:/Users/kangwenjing/.m2/repository/org/springframework/spring-beans/6.1.3/spring-beans-6.1.3.jar:/Users/kangwenjing/.m2/repository/org/springframework/spring-context/6.1.3/spring-context-6.1.3.jar:/Users/kangwenjing/.m2/repository/org/springframework/spring-core/6.1.3/spring-core-6.1.3.jar:/Users/kangwenjing/.m2/repository/org/springframework/spring-jcl/6.1.3/spring-jcl-6.1.3.jar:/Users/kangwenjing/.m2/repository/org/springframework/spring-expression/6.1.3/spring-expression-6.1.3.jar:/Users/kangwenjing/.m2/repository/io/micrometer/micrometer-observation/1.12.2/micrometer-observation-1.12.2.jar:/Users/kangwenjing/.m2/repository/io/micrometer/micrometer-commons/1.12.2/micrometer-commons-1.12.2.jar:/Users/kangwenjing/.m2/repository/com/baomidou/mybatis-plus-spring-boot3-starter/3.5.6/mybatis-plus-spring-boot3-starter-3.5.6.jar:/Users/kangwenjing/.m2/repository/com/baomidou/mybatis-plus/3.5.6/mybatis-plus-3.5.6.jar:/Users/kangwenjing/.m2/repository/com/baomidou/mybatis-plus-core/3.5.6/mybatis-plus-core-3.5.6.jar:/Users/kangwenjing/.m2/repository/com/baomidou/mybatis-plus-annotation/3.5.6/mybatis-plus-annotation-3.5.6.jar:/Users/kangwenjing/.m2/repository/com/baomidou/mybatis-plus-extension/3.5.6/mybatis-plus-extension-3.5.6.jar:/Users/kangwenjing/.m2/repository/org/mybatis/mybatis/3.5.16/mybatis-3.5.16.jar:/Users/kangwenjing/.m2/repository/com/github/jsqlparser/jsqlparser/4.9/jsqlparser-4.9.jar:/Users/kangwenjing/.m2/repository/org/mybatis/mybatis-spring/3.0.3/mybatis-spring-3.0.3.jar:/Users/kangwenjing/.m2/repository/com/baomidou/mybatis-plus-spring-boot-autoconfigure/3.5.6/mybatis-plus-spring-boot-autoconfigure-3.5.6.jar:/Users/kangwenjing/.m2/repository/org/springframework/boot/spring-boot-autoconfigure/3.2.2/spring-boot-autoconfigure-3.2.2.jar:/Users/kangwenjing/.m2/repository/org/springframework/boot/spring-boot-starter-jdbc/3.2.2/spring-boot-starter-jdbc-3.2.2.jar:/Users/kangwenjing/.m2/repository/com/zaxxer/HikariCP/5.0.1/HikariCP-5.0.1.jar:/Users/kangwenjing/.m2/repository/org/springframework/spring-jdbc/6.1.3/spring-jdbc-6.1.3.jar:/Users/kangwenjing/.m2/repository/org/postgresql/postgresql/42.6.0/postgresql-42.6.0.jar:/Users/kangwenjing/.m2/repository/org/checkerframework/checker-qual/3.31.0/checker-qual-3.31.0.jar:/Users/kangwenjing/.m2/repository/org/springframework/boot/spring-boot-starter-data-redis/3.2.2/spring-boot-starter-data-redis-3.2.2.jar:/Users/kangwenjing/.m2/repository/io/lettuce/lettuce-core/6.3.1.RELEASE/lettuce-core-6.3.1.RELEASE.jar:/Users/kangwenjing/.m2/repository/io/netty/netty-common/4.1.105.Final/netty-common-4.1.105.Final.jar:/Users/kangwenjing/.m2/repository/io/netty/netty-handler/4.1.105.Final/netty-handler-4.1.105.Final.jar:/Users/kangwenjing/.m2/repository/io/netty/netty-resolver/4.1.105.Final/netty-resolver-4.1.105.Final.jar:/Users/kangwenjing/.m2/repository/io/netty/netty-buffer/4.1.105.Final/netty-buffer-4.1.105.Final.jar:/Users/kangwenjing/.m2/repository/io/netty/netty-transport-native-unix-common/4.1.105.Final/netty-transport-native-unix-common-4.1.105.Final.jar:/Users/kangwenjing/.m2/repository/io/netty/netty-codec/4.1.105.Final/netty-codec-4.1.105.Final.jar:/Users/kangwenjing/.m2/repository/io/netty/netty-transport/4.1.105.Final/netty-transport-4.1.105.Final.jar:/Users/kangwenjing/.m2/repository/io/projectreactor/reactor-core/3.6.2/reactor-core-3.6.2.jar:/Users/kangwenjing/.m2/repository/org/reactivestreams/reactive-streams/1.0.4/reactive-streams-1.0.4.jar:/Users/kangwenjing/.m2/repository/org/springframework/data/spring-data-redis/3.2.2/spring-data-redis-3.2.2.jar:/Users/kangwenjing/.m2/repository/org/springframework/data/spring-data-keyvalue/3.2.2/spring-data-keyvalue-3.2.2.jar:/Users/kangwenjing/.m2/repository/org/springframework/data/spring-data-commons/3.2.2/spring-data-commons-3.2.2.jar:/Users/kangwenjing/.m2/repository/org/springframework/spring-tx/6.1.3/spring-tx-6.1.3.jar:/Users/kangwenjing/.m2/repository/org/springframework/spring-oxm/6.1.3/spring-oxm-6.1.3.jar:/Users/kangwenjing/.m2/repository/org/springframework/spring-context-support/6.1.3/spring-context-support-6.1.3.jar:/Users/kangwenjing/.m2/repository/org/slf4j/slf4j-api/2.0.11/slf4j-api-2.0.11.jar:/Users/kangwenjing/.m2/repository/org/springframework/boot/spring-boot-starter-test/3.2.2/spring-boot-starter-test-3.2.2.jar:/Users/kangwenjing/.m2/repository/org/springframework/boot/spring-boot-test/3.2.2/spring-boot-test-3.2.2.jar:/Users/kangwenjing/.m2/repository/org/springframework/boot/spring-boot-test-autoconfigure/3.2.2/spring-boot-test-autoconfigure-3.2.2.jar:/Users/kangwenjing/.m2/repository/com/jayway/jsonpath/json-path/2.8.0/json-path-2.8.0.jar:/Users/kangwenjing/.m2/repository/jakarta/xml/bind/jakarta.xml.bind-api/4.0.1/jakarta.xml.bind-api-4.0.1.jar:/Users/kangwenjing/.m2/repository/jakarta/activation/jakarta.activation-api/2.1.2/jakarta.activation-api-2.1.2.jar:/Users/kangwenjing/.m2/repository/net/minidev/json-smart/2.5.0/json-smart-2.5.0.jar:/Users/kangwenjing/.m2/repository/net/minidev/accessors-smart/2.5.0/accessors-smart-2.5.0.jar:/Users/kangwenjing/.m2/repository/org/ow2/asm/asm/9.3/asm-9.3.jar:/Users/kangwenjing/.m2/repository/org/assertj/assertj-core/3.24.2/assertj-core-3.24.2.jar:/Users/kangwenjing/.m2/repository/net/bytebuddy/byte-buddy/1.14.11/byte-buddy-1.14.11.jar:/Users/kangwenjing/.m2/repository/org/awaitility/awaitility/4.2.0/awaitility-4.2.0.jar:/Users/kangwenjing/.m2/repository/org/hamcrest/hamcrest/2.2/hamcrest-2.2.jar:/Users/kangwenjing/.m2/repository/org/junit/jupiter/junit-jupiter/5.10.1/junit-jupiter-5.10.1.jar:/Users/kangwenjing/.m2/repository/org/junit/jupiter/junit-jupiter-api/5.10.1/junit-jupiter-api-5.10.1.jar:/Users/kangwenjing/.m2/repository/org/opentest4j/opentest4j/1.3.0/opentest4j-1.3.0.jar:/Users/kangwenjing/.m2/repository/org/junit/platform/junit-platform-commons/1.10.1/junit-platform-commons-1.10.1.jar:/Users/kangwenjing/.m2/repository/org/apiguardian/apiguardian-api/1.1.2/apiguardian-api-1.1.2.jar:/Users/kangwenjing/.m2/repository/org/junit/jupiter/junit-jupiter-params/5.10.1/junit-jupiter-params-5.10.1.jar:/Users/kangwenjing/.m2/repository/org/junit/jupiter/junit-jupiter-engine/5.10.1/junit-jupiter-engine-5.10.1.jar:/Users/kangwenjing/.m2/repository/org/junit/platform/junit-platform-engine/1.10.1/junit-platform-engine-1.10.1.jar:/Users/kangwenjing/.m2/repository/org/mockito/mockito-core/5.7.0/mockito-core-5.7.0.jar:/Users/kangwenjing/.m2/repository/net/bytebuddy/byte-buddy-agent/1.14.11/byte-buddy-agent-1.14.11.jar:/Users/kangwenjing/.m2/repository/org/objenesis/objenesis/3.3/objenesis-3.3.jar:/Users/kangwenjing/.m2/repository/org/mockito/mockito-junit-jupiter/5.7.0/mockito-junit-jupiter-5.7.0.jar:/Users/kangwenjing/.m2/repository/org/skyscreamer/jsonassert/1.5.1/jsonassert-1.5.1.jar:/Users/kangwenjing/.m2/repository/com/vaadin/external/google/android-json/0.0.20131108.vaadin1/android-json-0.0.20131108.vaadin1.jar:/Users/kangwenjing/.m2/repository/org/springframework/spring-test/6.1.3/spring-test-6.1.3.jar:/Users/kangwenjing/.m2/repository/org/xmlunit/xmlunit-core/2.9.1/xmlunit-core-2.9.1.jar:/Users/kangwenjing/.m2/repository/com/unisbase/unisbase-spring-boot-starter/0.1.0/unisbase-spring-boot-starter-0.1.0.jar:/Users/kangwenjing/.m2/repository/com/unisbase/unisbase-core/0.1.0/unisbase-core-0.1.0.jar:/Users/kangwenjing/.m2/repository/com/unisbase/unisbase-api/0.1.0/unisbase-api-0.1.0.jar:/Users/kangwenjing/.m2/repository/com/unisbase/unisbase-common/0.1.0/unisbase-common-0.1.0.jar:/Users/kangwenjing/.m2/repository/org/springframework/boot/spring-boot-starter-cache/3.2.2/spring-boot-starter-cache-3.2.2.jar:/Users/kangwenjing/.m2/repository/org/mapstruct/mapstruct/1.5.5.Final/mapstruct-1.5.5.Final.jar:/Users/kangwenjing/.m2/repository/io/jsonwebtoken/jjwt-api/0.11.5/jjwt-api-0.11.5.jar:/Users/kangwenjing/.m2/repository/io/jsonwebtoken/jjwt-impl/0.11.5/jjwt-impl-0.11.5.jar:/Users/kangwenjing/.m2/repository/io/jsonwebtoken/jjwt-jackson/0.11.5/jjwt-jackson-0.11.5.jar:/Users/kangwenjing/.m2/repository/com/github/whvcse/easy-captcha/1.6.2/easy-captcha-1.6.2.jar \ No newline at end of file diff --git a/backend/pom.xml b/backend/pom.xml new file mode 100644 index 0000000..f7b8180 --- /dev/null +++ b/backend/pom.xml @@ -0,0 +1,80 @@ + + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 3.2.2 + + + + com.unis.crm + unis-crm-backend + 1.0.0-SNAPSHOT + unis-crm-backend + UNIS CRM backend + + + 17 + 3.5.6 + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-validation + + + org.springframework.boot + spring-boot-starter-aop + + + org.springframework.security + spring-security-core + + + com.baomidou + mybatis-plus-spring-boot3-starter + ${mybatis-plus.version} + + + org.postgresql + postgresql + runtime + + + org.springframework.boot + spring-boot-starter-data-redis + + + org.springframework.boot + spring-boot-starter-test + test + + + + com.unisbase + unisbase-spring-boot-starter + 0.1.0 + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + com.unis.crm.UnisCrmBackendApplication + + + + + diff --git a/backend/src/main/java/com/unis/crm/UnisCrmBackendApplication.java b/backend/src/main/java/com/unis/crm/UnisCrmBackendApplication.java new file mode 100644 index 0000000..4c995da --- /dev/null +++ b/backend/src/main/java/com/unis/crm/UnisCrmBackendApplication.java @@ -0,0 +1,14 @@ +package com.unis.crm; + +import org.mybatis.spring.annotation.MapperScan; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication(scanBasePackages = "com.unis.crm") +@MapperScan("com.unis.crm.mapper") +public class UnisCrmBackendApplication { + + public static void main(String[] args) { + SpringApplication.run(UnisCrmBackendApplication.class, args); + } +} diff --git a/backend/src/main/java/com/unis/crm/common/ApiResponse.java b/backend/src/main/java/com/unis/crm/common/ApiResponse.java new file mode 100644 index 0000000..0d01131 --- /dev/null +++ b/backend/src/main/java/com/unis/crm/common/ApiResponse.java @@ -0,0 +1,49 @@ +package com.unis.crm.common; + +public class ApiResponse { + + private String code; + private String msg; + private T data; + + public ApiResponse() { + } + + public ApiResponse(String code, String msg, T data) { + this.code = code; + this.msg = msg; + this.data = data; + } + + public static ApiResponse success(T data) { + return new ApiResponse<>("0", "success", data); + } + + public static ApiResponse fail(String msg) { + return new ApiResponse<>("-1", msg, null); + } + + public String getCode() { + return code; + } + + public void setCode(String code) { + this.code = code; + } + + public String getMsg() { + return msg; + } + + public void setMsg(String msg) { + this.msg = msg; + } + + public T getData() { + return data; + } + + public void setData(T data) { + this.data = data; + } +} diff --git a/backend/src/main/java/com/unis/crm/common/BusinessException.java b/backend/src/main/java/com/unis/crm/common/BusinessException.java new file mode 100644 index 0000000..191d282 --- /dev/null +++ b/backend/src/main/java/com/unis/crm/common/BusinessException.java @@ -0,0 +1,8 @@ +package com.unis.crm.common; + +public class BusinessException extends RuntimeException { + + public BusinessException(String message) { + super(message); + } +} diff --git a/backend/src/main/java/com/unis/crm/common/CrmGlobalExceptionHandler.java b/backend/src/main/java/com/unis/crm/common/CrmGlobalExceptionHandler.java new file mode 100644 index 0000000..70ff527 --- /dev/null +++ b/backend/src/main/java/com/unis/crm/common/CrmGlobalExceptionHandler.java @@ -0,0 +1,43 @@ +package com.unis.crm.common; + +import jakarta.servlet.http.HttpServletRequest; +import java.time.OffsetDateTime; +import java.util.LinkedHashMap; +import java.util.Map; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@RestControllerAdvice +public class CrmGlobalExceptionHandler { + + @ExceptionHandler(BusinessException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ApiResponse handleBusinessException(BusinessException ex) { + return ApiResponse.fail(ex.getMessage()); + } + + @ExceptionHandler(MethodArgumentNotValidException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ApiResponse handleValidationException(MethodArgumentNotValidException ex) { + String message = ex.getBindingResult().getFieldErrors().stream() + .findFirst() + .map(error -> error.getField() + " " + error.getDefaultMessage()) + .orElse("请求参数校验失败"); + return ApiResponse.fail(message); + } + + @ExceptionHandler(Exception.class) + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + public Map handleUnexpectedException(Exception ex, HttpServletRequest request) { + Map body = new LinkedHashMap<>(); + body.put("timestamp", OffsetDateTime.now().toString()); + body.put("status", HttpStatus.INTERNAL_SERVER_ERROR.value()); + body.put("error", HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase()); + body.put("message", ex.getMessage()); + body.put("path", request.getRequestURI()); + return body; + } +} diff --git a/backend/src/main/java/com/unis/crm/common/CurrentUserUtils.java b/backend/src/main/java/com/unis/crm/common/CurrentUserUtils.java new file mode 100644 index 0000000..5e015c0 --- /dev/null +++ b/backend/src/main/java/com/unis/crm/common/CurrentUserUtils.java @@ -0,0 +1,14 @@ +package com.unis.crm.common; + +public final class CurrentUserUtils { + + private CurrentUserUtils() { + } + + public static Long requireCurrentUserId(Long headerUserId) { + if (headerUserId == null || headerUserId <= 0) { + throw new BusinessException("未识别到当前登录用户"); + } + return headerUserId; + } +} diff --git a/backend/src/main/java/com/unis/crm/controller/DashboardController.java b/backend/src/main/java/com/unis/crm/controller/DashboardController.java new file mode 100644 index 0000000..031768f --- /dev/null +++ b/backend/src/main/java/com/unis/crm/controller/DashboardController.java @@ -0,0 +1,29 @@ +package com.unis.crm.controller; + +import com.unis.crm.common.ApiResponse; +import com.unis.crm.dto.dashboard.DashboardHomeDTO; +import com.unis.crm.service.DashboardService; +import jakarta.validation.constraints.Min; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@Validated +@RestController +@RequestMapping("/api/dashboard") +public class DashboardController { + + private final DashboardService dashboardService; + + public DashboardController(DashboardService dashboardService) { + this.dashboardService = dashboardService; + } + + @GetMapping("/home") + public ApiResponse getHome( + @RequestHeader("X-User-Id") @Min(1) Long userId) { + return ApiResponse.success(dashboardService.getHome(userId)); + } +} diff --git a/backend/src/main/java/com/unis/crm/controller/ExpansionController.java b/backend/src/main/java/com/unis/crm/controller/ExpansionController.java new file mode 100644 index 0000000..1a62017 --- /dev/null +++ b/backend/src/main/java/com/unis/crm/controller/ExpansionController.java @@ -0,0 +1,87 @@ +package com.unis.crm.controller; + +import com.unis.crm.common.ApiResponse; +import com.unis.crm.common.CurrentUserUtils; +import com.unis.crm.dto.expansion.CreateChannelExpansionRequest; +import com.unis.crm.dto.expansion.CreateExpansionFollowUpRequest; +import com.unis.crm.dto.expansion.CreateSalesExpansionRequest; +import com.unis.crm.dto.expansion.ExpansionMetaDTO; +import com.unis.crm.dto.expansion.ExpansionOverviewDTO; +import com.unis.crm.dto.expansion.UpdateChannelExpansionRequest; +import com.unis.crm.dto.expansion.UpdateSalesExpansionRequest; +import com.unis.crm.service.ExpansionService; +import jakarta.validation.Valid; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/expansion") +public class ExpansionController { + + private final ExpansionService expansionService; + + public ExpansionController(ExpansionService expansionService) { + this.expansionService = expansionService; + } + + @GetMapping("/meta") + public ApiResponse getMeta(@RequestHeader("X-User-Id") Long userId) { + CurrentUserUtils.requireCurrentUserId(userId); + return ApiResponse.success(expansionService.getMeta()); + } + + @GetMapping("/overview") + public ApiResponse getOverview( + @RequestHeader("X-User-Id") Long userId, + @RequestParam(value = "keyword", required = false) String keyword) { + return ApiResponse.success(expansionService.getOverview(CurrentUserUtils.requireCurrentUserId(userId), keyword)); + } + + @PostMapping("/sales") + public ApiResponse createSales( + @RequestHeader("X-User-Id") Long userId, + @Valid @RequestBody CreateSalesExpansionRequest request) { + return ApiResponse.success(expansionService.createSalesExpansion(CurrentUserUtils.requireCurrentUserId(userId), request)); + } + + @PostMapping("/channel") + public ApiResponse createChannel( + @RequestHeader("X-User-Id") Long userId, + @Valid @RequestBody CreateChannelExpansionRequest request) { + return ApiResponse.success(expansionService.createChannelExpansion(CurrentUserUtils.requireCurrentUserId(userId), request)); + } + + @PutMapping("/sales/{id}") + public ApiResponse updateSales( + @RequestHeader("X-User-Id") Long userId, + @PathVariable("id") Long id, + @Valid @RequestBody UpdateSalesExpansionRequest request) { + expansionService.updateSalesExpansion(CurrentUserUtils.requireCurrentUserId(userId), id, request); + return ApiResponse.success(null); + } + + @PutMapping("/channel/{id}") + public ApiResponse updateChannel( + @RequestHeader("X-User-Id") Long userId, + @PathVariable("id") Long id, + @Valid @RequestBody UpdateChannelExpansionRequest request) { + expansionService.updateChannelExpansion(CurrentUserUtils.requireCurrentUserId(userId), id, request); + return ApiResponse.success(null); + } + + @PostMapping("/{bizType}/{bizId}/followups") + public ApiResponse createFollowUp( + @RequestHeader("X-User-Id") Long userId, + @PathVariable("bizType") String bizType, + @PathVariable("bizId") Long bizId, + @Valid @RequestBody CreateExpansionFollowUpRequest request) { + return ApiResponse.success(expansionService.createFollowUp(CurrentUserUtils.requireCurrentUserId(userId), bizType, bizId, request)); + } +} diff --git a/backend/src/main/java/com/unis/crm/controller/OpportunityController.java b/backend/src/main/java/com/unis/crm/controller/OpportunityController.java new file mode 100644 index 0000000..d4d19bc --- /dev/null +++ b/backend/src/main/java/com/unis/crm/controller/OpportunityController.java @@ -0,0 +1,60 @@ +package com.unis.crm.controller; + +import com.unis.crm.common.ApiResponse; +import com.unis.crm.common.CurrentUserUtils; +import com.unis.crm.dto.opportunity.CreateOpportunityFollowUpRequest; +import com.unis.crm.dto.opportunity.CreateOpportunityRequest; +import com.unis.crm.dto.opportunity.OpportunityOverviewDTO; +import com.unis.crm.service.OpportunityService; +import jakarta.validation.Valid; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/opportunities") +public class OpportunityController { + + private final OpportunityService opportunityService; + + public OpportunityController(OpportunityService opportunityService) { + this.opportunityService = opportunityService; + } + + @GetMapping("/overview") + public ApiResponse getOverview( + @RequestHeader("X-User-Id") Long userId, + @RequestParam(value = "keyword", required = false) String keyword, + @RequestParam(value = "stage", required = false) String stage) { + return ApiResponse.success(opportunityService.getOverview(CurrentUserUtils.requireCurrentUserId(userId), keyword, stage)); + } + + @PostMapping + public ApiResponse createOpportunity( + @RequestHeader("X-User-Id") Long userId, + @Valid @RequestBody CreateOpportunityRequest request) { + return ApiResponse.success(opportunityService.createOpportunity(CurrentUserUtils.requireCurrentUserId(userId), request)); + } + + @PutMapping("/{opportunityId}") + public ApiResponse updateOpportunity( + @RequestHeader("X-User-Id") Long userId, + @PathVariable("opportunityId") Long opportunityId, + @Valid @RequestBody CreateOpportunityRequest request) { + return ApiResponse.success(opportunityService.updateOpportunity(CurrentUserUtils.requireCurrentUserId(userId), opportunityId, request)); + } + + @PostMapping("/{opportunityId}/followups") + public ApiResponse createFollowUp( + @RequestHeader("X-User-Id") Long userId, + @PathVariable("opportunityId") Long opportunityId, + @Valid @RequestBody CreateOpportunityFollowUpRequest request) { + return ApiResponse.success(opportunityService.createFollowUp(CurrentUserUtils.requireCurrentUserId(userId), opportunityId, request)); + } +} diff --git a/backend/src/main/java/com/unis/crm/controller/WorkController.java b/backend/src/main/java/com/unis/crm/controller/WorkController.java new file mode 100644 index 0000000..9c1f3d3 --- /dev/null +++ b/backend/src/main/java/com/unis/crm/controller/WorkController.java @@ -0,0 +1,58 @@ +package com.unis.crm.controller; + +import com.unis.crm.common.ApiResponse; +import com.unis.crm.common.CurrentUserUtils; +import com.unis.crm.dto.work.CreateWorkCheckInRequest; +import com.unis.crm.dto.work.CreateWorkDailyReportRequest; +import com.unis.crm.dto.work.WorkOverviewDTO; +import com.unis.crm.service.WorkService; +import jakarta.validation.constraints.DecimalMax; +import jakarta.validation.constraints.DecimalMin; +import jakarta.validation.Valid; +import java.math.BigDecimal; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/work") +public class WorkController { + + private final WorkService workService; + + public WorkController(WorkService workService) { + this.workService = workService; + } + + @GetMapping("/overview") + public ApiResponse getOverview(@RequestHeader("X-User-Id") Long userId) { + return ApiResponse.success(workService.getOverview(CurrentUserUtils.requireCurrentUserId(userId))); + } + + @GetMapping("/reverse-geocode") + public ApiResponse reverseGeocode( + @RequestHeader("X-User-Id") Long userId, + @RequestParam("lat") @DecimalMin(value = "-90.0") @DecimalMax(value = "90.0") BigDecimal latitude, + @RequestParam("lon") @DecimalMin(value = "-180.0") @DecimalMax(value = "180.0") BigDecimal longitude) { + CurrentUserUtils.requireCurrentUserId(userId); + return ApiResponse.success(workService.resolveLocationName(latitude, longitude)); + } + + @PostMapping("/checkins") + public ApiResponse saveCheckIn( + @RequestHeader("X-User-Id") Long userId, + @Valid @RequestBody CreateWorkCheckInRequest request) { + return ApiResponse.success(workService.saveCheckIn(CurrentUserUtils.requireCurrentUserId(userId), request)); + } + + @PostMapping("/daily-reports") + public ApiResponse saveDailyReport( + @RequestHeader("X-User-Id") Long userId, + @Valid @RequestBody CreateWorkDailyReportRequest request) { + return ApiResponse.success(workService.saveDailyReport(CurrentUserUtils.requireCurrentUserId(userId), request)); + } +} diff --git a/backend/src/main/java/com/unis/crm/dto/dashboard/DashboardActivityDTO.java b/backend/src/main/java/com/unis/crm/dto/dashboard/DashboardActivityDTO.java new file mode 100644 index 0000000..3d25111 --- /dev/null +++ b/backend/src/main/java/com/unis/crm/dto/dashboard/DashboardActivityDTO.java @@ -0,0 +1,97 @@ +package com.unis.crm.dto.dashboard; + +import java.time.OffsetDateTime; + +public class DashboardActivityDTO { + + private Long id; + private String bizType; + private Long bizId; + private String actionType; + private String title; + private String content; + private Long operatorUserId; + private String operatorName; + private OffsetDateTime createdAt; + private String timeText; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getBizType() { + return bizType; + } + + public void setBizType(String bizType) { + this.bizType = bizType; + } + + public Long getBizId() { + return bizId; + } + + public void setBizId(Long bizId) { + this.bizId = bizId; + } + + public String getActionType() { + return actionType; + } + + public void setActionType(String actionType) { + this.actionType = actionType; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } + + public Long getOperatorUserId() { + return operatorUserId; + } + + public void setOperatorUserId(Long operatorUserId) { + this.operatorUserId = operatorUserId; + } + + public String getOperatorName() { + return operatorName; + } + + public void setOperatorName(String operatorName) { + this.operatorName = operatorName; + } + + public OffsetDateTime getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(OffsetDateTime createdAt) { + this.createdAt = createdAt; + } + + public String getTimeText() { + return timeText; + } + + public void setTimeText(String timeText) { + this.timeText = timeText; + } +} diff --git a/backend/src/main/java/com/unis/crm/dto/dashboard/DashboardHomeDTO.java b/backend/src/main/java/com/unis/crm/dto/dashboard/DashboardHomeDTO.java new file mode 100644 index 0000000..15a0f78 --- /dev/null +++ b/backend/src/main/java/com/unis/crm/dto/dashboard/DashboardHomeDTO.java @@ -0,0 +1,95 @@ +package com.unis.crm.dto.dashboard; + +import java.util.List; + +public class DashboardHomeDTO { + + private Long userId; + private String realName; + private String jobTitle; + private String deptName; + private Long onboardingDays; + private List stats; + private List todos; + private List activities; + + public DashboardHomeDTO() { + } + + public DashboardHomeDTO(Long userId, String realName, String jobTitle, String deptName, Long onboardingDays, + List stats, List todos, + List activities) { + this.userId = userId; + this.realName = realName; + this.jobTitle = jobTitle; + this.deptName = deptName; + this.onboardingDays = onboardingDays; + this.stats = stats; + this.todos = todos; + this.activities = activities; + } + + public Long getUserId() { + return userId; + } + + public void setUserId(Long userId) { + this.userId = userId; + } + + public String getRealName() { + return realName; + } + + public void setRealName(String realName) { + this.realName = realName; + } + + public String getJobTitle() { + return jobTitle; + } + + public void setJobTitle(String jobTitle) { + this.jobTitle = jobTitle; + } + + public String getDeptName() { + return deptName; + } + + public void setDeptName(String deptName) { + this.deptName = deptName; + } + + public Long getOnboardingDays() { + return onboardingDays; + } + + public void setOnboardingDays(Long onboardingDays) { + this.onboardingDays = onboardingDays; + } + + public List getStats() { + return stats; + } + + public void setStats(List stats) { + this.stats = stats; + } + + public List getTodos() { + return todos; + } + + public void setTodos(List todos) { + this.todos = todos; + } + + public List getActivities() { + return activities; + } + + public void setActivities(List activities) { + this.activities = activities; + } +} diff --git a/backend/src/main/java/com/unis/crm/dto/dashboard/DashboardStatDTO.java b/backend/src/main/java/com/unis/crm/dto/dashboard/DashboardStatDTO.java new file mode 100644 index 0000000..0b35ae0 --- /dev/null +++ b/backend/src/main/java/com/unis/crm/dto/dashboard/DashboardStatDTO.java @@ -0,0 +1,32 @@ +package com.unis.crm.dto.dashboard; + +public class DashboardStatDTO { + + private String name; + private Long value; + private String metricKey; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Long getValue() { + return value; + } + + public void setValue(Long value) { + this.value = value; + } + + public String getMetricKey() { + return metricKey; + } + + public void setMetricKey(String metricKey) { + this.metricKey = metricKey; + } +} diff --git a/backend/src/main/java/com/unis/crm/dto/dashboard/DashboardTodoDTO.java b/backend/src/main/java/com/unis/crm/dto/dashboard/DashboardTodoDTO.java new file mode 100644 index 0000000..0e559c5 --- /dev/null +++ b/backend/src/main/java/com/unis/crm/dto/dashboard/DashboardTodoDTO.java @@ -0,0 +1,79 @@ +package com.unis.crm.dto.dashboard; + +import java.time.OffsetDateTime; + +public class DashboardTodoDTO { + + private Long id; + private String title; + private String bizType; + private Long bizId; + private String priority; + private String status; + private OffsetDateTime dueDate; + private OffsetDateTime createdAt; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getBizType() { + return bizType; + } + + public void setBizType(String bizType) { + this.bizType = bizType; + } + + public Long getBizId() { + return bizId; + } + + public void setBizId(Long bizId) { + this.bizId = bizId; + } + + public String getPriority() { + return priority; + } + + public void setPriority(String priority) { + this.priority = priority; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + public OffsetDateTime getDueDate() { + return dueDate; + } + + public void setDueDate(OffsetDateTime dueDate) { + this.dueDate = dueDate; + } + + public OffsetDateTime getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(OffsetDateTime createdAt) { + this.createdAt = createdAt; + } +} diff --git a/backend/src/main/java/com/unis/crm/dto/dashboard/UserWelcomeDTO.java b/backend/src/main/java/com/unis/crm/dto/dashboard/UserWelcomeDTO.java new file mode 100644 index 0000000..d6b5cd4 --- /dev/null +++ b/backend/src/main/java/com/unis/crm/dto/dashboard/UserWelcomeDTO.java @@ -0,0 +1,52 @@ +package com.unis.crm.dto.dashboard; + +import java.time.LocalDate; + +public class UserWelcomeDTO { + + private Long userId; + private String realName; + private String jobTitle; + private String deptName; + private LocalDate hireDate; + + public Long getUserId() { + return userId; + } + + public void setUserId(Long userId) { + this.userId = userId; + } + + public String getRealName() { + return realName; + } + + public void setRealName(String realName) { + this.realName = realName; + } + + public String getJobTitle() { + return jobTitle; + } + + public void setJobTitle(String jobTitle) { + this.jobTitle = jobTitle; + } + + public String getDeptName() { + return deptName; + } + + public void setDeptName(String deptName) { + this.deptName = deptName; + } + + public LocalDate getHireDate() { + return hireDate; + } + + public void setHireDate(LocalDate hireDate) { + this.hireDate = hireDate; + } +} diff --git a/backend/src/main/java/com/unis/crm/dto/expansion/ChannelExpansionItemDTO.java b/backend/src/main/java/com/unis/crm/dto/expansion/ChannelExpansionItemDTO.java new file mode 100644 index 0000000..6aec9fc --- /dev/null +++ b/backend/src/main/java/com/unis/crm/dto/expansion/ChannelExpansionItemDTO.java @@ -0,0 +1,161 @@ +package com.unis.crm.dto.expansion; + +import java.util.ArrayList; +import java.util.List; + +public class ChannelExpansionItemDTO { + + private Long id; + private String type; + private String name; + private String province; + private String industry; + private String annualRevenue; + private String revenue; + private Integer size; + private String contact; + private String contactTitle; + private String phone; + private String stageCode; + private String stage; + private Boolean landed; + private String expectedSignDate; + private String notes; + private List followUps = new ArrayList<>(); + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getProvince() { + return province; + } + + public void setProvince(String province) { + this.province = province; + } + + public String getIndustry() { + return industry; + } + + public void setIndustry(String industry) { + this.industry = industry; + } + + public String getAnnualRevenue() { + return annualRevenue; + } + + public void setAnnualRevenue(String annualRevenue) { + this.annualRevenue = annualRevenue; + } + + public String getRevenue() { + return revenue; + } + + public void setRevenue(String revenue) { + this.revenue = revenue; + } + + public Integer getSize() { + return size; + } + + public void setSize(Integer size) { + this.size = size; + } + + public String getContact() { + return contact; + } + + public void setContact(String contact) { + this.contact = contact; + } + + public String getContactTitle() { + return contactTitle; + } + + public void setContactTitle(String contactTitle) { + this.contactTitle = contactTitle; + } + + public String getPhone() { + return phone; + } + + public void setPhone(String phone) { + this.phone = phone; + } + + public String getStageCode() { + return stageCode; + } + + public void setStageCode(String stageCode) { + this.stageCode = stageCode; + } + + public String getStage() { + return stage; + } + + public void setStage(String stage) { + this.stage = stage; + } + + public Boolean getLanded() { + return landed; + } + + public void setLanded(Boolean landed) { + this.landed = landed; + } + + public String getExpectedSignDate() { + return expectedSignDate; + } + + public void setExpectedSignDate(String expectedSignDate) { + this.expectedSignDate = expectedSignDate; + } + + public String getNotes() { + return notes; + } + + public void setNotes(String notes) { + this.notes = notes; + } + + public List getFollowUps() { + return followUps; + } + + public void setFollowUps(List followUps) { + this.followUps = followUps; + } +} diff --git a/backend/src/main/java/com/unis/crm/dto/expansion/CreateChannelExpansionRequest.java b/backend/src/main/java/com/unis/crm/dto/expansion/CreateChannelExpansionRequest.java new file mode 100644 index 0000000..445dc52 --- /dev/null +++ b/backend/src/main/java/com/unis/crm/dto/expansion/CreateChannelExpansionRequest.java @@ -0,0 +1,131 @@ +package com.unis.crm.dto.expansion; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import java.math.BigDecimal; +import java.time.LocalDate; + +public class CreateChannelExpansionRequest { + + private Long id; + + @NotBlank(message = "渠道名称不能为空") + @Size(max = 200, message = "渠道名称不能超过200字符") + private String channelName; + + private String province; + private String industry; + private BigDecimal annualRevenue; + private Integer staffSize; + private String contactName; + private String contactTitle; + private String contactMobile; + private String stage; + private Boolean landedFlag; + private LocalDate expectedSignDate; + private String remark; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getChannelName() { + return channelName; + } + + public void setChannelName(String channelName) { + this.channelName = channelName; + } + + public String getProvince() { + return province; + } + + public void setProvince(String province) { + this.province = province; + } + + public String getIndustry() { + return industry; + } + + public void setIndustry(String industry) { + this.industry = industry; + } + + public BigDecimal getAnnualRevenue() { + return annualRevenue; + } + + public void setAnnualRevenue(BigDecimal annualRevenue) { + this.annualRevenue = annualRevenue; + } + + public Integer getStaffSize() { + return staffSize; + } + + public void setStaffSize(Integer staffSize) { + this.staffSize = staffSize; + } + + public String getContactName() { + return contactName; + } + + public void setContactName(String contactName) { + this.contactName = contactName; + } + + public String getContactTitle() { + return contactTitle; + } + + public void setContactTitle(String contactTitle) { + this.contactTitle = contactTitle; + } + + public String getContactMobile() { + return contactMobile; + } + + public void setContactMobile(String contactMobile) { + this.contactMobile = contactMobile; + } + + public String getStage() { + return stage; + } + + public void setStage(String stage) { + this.stage = stage; + } + + public Boolean getLandedFlag() { + return landedFlag; + } + + public void setLandedFlag(Boolean landedFlag) { + this.landedFlag = landedFlag; + } + + public LocalDate getExpectedSignDate() { + return expectedSignDate; + } + + public void setExpectedSignDate(LocalDate expectedSignDate) { + this.expectedSignDate = expectedSignDate; + } + + public String getRemark() { + return remark; + } + + public void setRemark(String remark) { + this.remark = remark; + } +} diff --git a/backend/src/main/java/com/unis/crm/dto/expansion/CreateExpansionFollowUpRequest.java b/backend/src/main/java/com/unis/crm/dto/expansion/CreateExpansionFollowUpRequest.java new file mode 100644 index 0000000..2fc90cb --- /dev/null +++ b/backend/src/main/java/com/unis/crm/dto/expansion/CreateExpansionFollowUpRequest.java @@ -0,0 +1,51 @@ +package com.unis.crm.dto.expansion; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import java.time.OffsetDateTime; + +public class CreateExpansionFollowUpRequest { + + @NotBlank(message = "跟进类型不能为空") + private String followUpType; + + @NotBlank(message = "跟进内容不能为空") + private String content; + + private String nextAction; + + @NotNull(message = "跟进时间不能为空") + private OffsetDateTime followUpTime; + + public String getFollowUpType() { + return followUpType; + } + + public void setFollowUpType(String followUpType) { + this.followUpType = followUpType; + } + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } + + public String getNextAction() { + return nextAction; + } + + public void setNextAction(String nextAction) { + this.nextAction = nextAction; + } + + public OffsetDateTime getFollowUpTime() { + return followUpTime; + } + + public void setFollowUpTime(OffsetDateTime followUpTime) { + this.followUpTime = followUpTime; + } +} diff --git a/backend/src/main/java/com/unis/crm/dto/expansion/CreateSalesExpansionRequest.java b/backend/src/main/java/com/unis/crm/dto/expansion/CreateSalesExpansionRequest.java new file mode 100644 index 0000000..4e00298 --- /dev/null +++ b/backend/src/main/java/com/unis/crm/dto/expansion/CreateSalesExpansionRequest.java @@ -0,0 +1,139 @@ +package com.unis.crm.dto.expansion; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import java.time.LocalDate; + +public class CreateSalesExpansionRequest { + + private Long id; + + @NotBlank(message = "候选人姓名不能为空") + @Size(max = 50, message = "候选人姓名不能超过50字符") + private String candidateName; + + private String mobile; + private String email; + private Long targetDeptId; + private String industry; + private String title; + private String intentLevel; + private String stage; + private Boolean hasDesktopExp; + private Boolean inProgress; + private String employmentStatus; + private LocalDate expectedJoinDate; + private String remark; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getCandidateName() { + return candidateName; + } + + public void setCandidateName(String candidateName) { + this.candidateName = candidateName; + } + + public String getMobile() { + return mobile; + } + + public void setMobile(String mobile) { + this.mobile = mobile; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public Long getTargetDeptId() { + return targetDeptId; + } + + public void setTargetDeptId(Long targetDeptId) { + this.targetDeptId = targetDeptId; + } + + public String getIndustry() { + return industry; + } + + public void setIndustry(String industry) { + this.industry = industry; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getIntentLevel() { + return intentLevel; + } + + public void setIntentLevel(String intentLevel) { + this.intentLevel = intentLevel; + } + + public String getStage() { + return stage; + } + + public void setStage(String stage) { + this.stage = stage; + } + + public Boolean getHasDesktopExp() { + return hasDesktopExp; + } + + public void setHasDesktopExp(Boolean hasDesktopExp) { + this.hasDesktopExp = hasDesktopExp; + } + + public Boolean getInProgress() { + return inProgress; + } + + public void setInProgress(Boolean inProgress) { + this.inProgress = inProgress; + } + + public String getEmploymentStatus() { + return employmentStatus; + } + + public void setEmploymentStatus(String employmentStatus) { + this.employmentStatus = employmentStatus; + } + + public LocalDate getExpectedJoinDate() { + return expectedJoinDate; + } + + public void setExpectedJoinDate(LocalDate expectedJoinDate) { + this.expectedJoinDate = expectedJoinDate; + } + + public String getRemark() { + return remark; + } + + public void setRemark(String remark) { + this.remark = remark; + } +} diff --git a/backend/src/main/java/com/unis/crm/dto/expansion/DepartmentOptionDTO.java b/backend/src/main/java/com/unis/crm/dto/expansion/DepartmentOptionDTO.java new file mode 100644 index 0000000..7938d64 --- /dev/null +++ b/backend/src/main/java/com/unis/crm/dto/expansion/DepartmentOptionDTO.java @@ -0,0 +1,23 @@ +package com.unis.crm.dto.expansion; + +public class DepartmentOptionDTO { + + private Long id; + private String name; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/backend/src/main/java/com/unis/crm/dto/expansion/ExpansionFollowUpDTO.java b/backend/src/main/java/com/unis/crm/dto/expansion/ExpansionFollowUpDTO.java new file mode 100644 index 0000000..6f2e4cf --- /dev/null +++ b/backend/src/main/java/com/unis/crm/dto/expansion/ExpansionFollowUpDTO.java @@ -0,0 +1,79 @@ +package com.unis.crm.dto.expansion; + +import java.time.OffsetDateTime; + +public class ExpansionFollowUpDTO { + + private Long id; + private Long bizId; + private String bizType; + private OffsetDateTime followUpTime; + private String date; + private String type; + private String content; + private String user; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Long getBizId() { + return bizId; + } + + public void setBizId(Long bizId) { + this.bizId = bizId; + } + + public String getBizType() { + return bizType; + } + + public void setBizType(String bizType) { + this.bizType = bizType; + } + + public OffsetDateTime getFollowUpTime() { + return followUpTime; + } + + public void setFollowUpTime(OffsetDateTime followUpTime) { + this.followUpTime = followUpTime; + } + + public String getDate() { + return date; + } + + public void setDate(String date) { + this.date = date; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } + + public String getUser() { + return user; + } + + public void setUser(String user) { + this.user = user; + } +} diff --git a/backend/src/main/java/com/unis/crm/dto/expansion/ExpansionMetaDTO.java b/backend/src/main/java/com/unis/crm/dto/expansion/ExpansionMetaDTO.java new file mode 100644 index 0000000..1d225ec --- /dev/null +++ b/backend/src/main/java/com/unis/crm/dto/expansion/ExpansionMetaDTO.java @@ -0,0 +1,23 @@ +package com.unis.crm.dto.expansion; + +import java.util.List; + +public class ExpansionMetaDTO { + + private List departments; + + public ExpansionMetaDTO() { + } + + public ExpansionMetaDTO(List departments) { + this.departments = departments; + } + + public List getDepartments() { + return departments; + } + + public void setDepartments(List departments) { + this.departments = departments; + } +} diff --git a/backend/src/main/java/com/unis/crm/dto/expansion/ExpansionOverviewDTO.java b/backend/src/main/java/com/unis/crm/dto/expansion/ExpansionOverviewDTO.java new file mode 100644 index 0000000..63cf1d0 --- /dev/null +++ b/backend/src/main/java/com/unis/crm/dto/expansion/ExpansionOverviewDTO.java @@ -0,0 +1,33 @@ +package com.unis.crm.dto.expansion; + +import java.util.List; + +public class ExpansionOverviewDTO { + + private List salesItems; + private List channelItems; + + public ExpansionOverviewDTO() { + } + + public ExpansionOverviewDTO(List salesItems, List channelItems) { + this.salesItems = salesItems; + this.channelItems = channelItems; + } + + public List getSalesItems() { + return salesItems; + } + + public void setSalesItems(List salesItems) { + this.salesItems = salesItems; + } + + public List getChannelItems() { + return channelItems; + } + + public void setChannelItems(List channelItems) { + this.channelItems = channelItems; + } +} diff --git a/backend/src/main/java/com/unis/crm/dto/expansion/SalesExpansionItemDTO.java b/backend/src/main/java/com/unis/crm/dto/expansion/SalesExpansionItemDTO.java new file mode 100644 index 0000000..1e8d353 --- /dev/null +++ b/backend/src/main/java/com/unis/crm/dto/expansion/SalesExpansionItemDTO.java @@ -0,0 +1,188 @@ +package com.unis.crm.dto.expansion; + +import java.util.ArrayList; +import java.util.List; + +public class SalesExpansionItemDTO { + + private Long id; + private String type; + private String name; + private String phone; + private String email; + private Long targetDeptId; + private String dept; + private String industry; + private String title; + private String intentLevel; + private String intent; + private String stageCode; + private String stage; + private Boolean hasExp; + private Boolean inProgress; + private Boolean active; + private String employmentStatus; + private String expectedJoinDate; + private String notes; + private List followUps = new ArrayList<>(); + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getPhone() { + return phone; + } + + public void setPhone(String phone) { + this.phone = phone; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public Long getTargetDeptId() { + return targetDeptId; + } + + public void setTargetDeptId(Long targetDeptId) { + this.targetDeptId = targetDeptId; + } + + public String getDept() { + return dept; + } + + public void setDept(String dept) { + this.dept = dept; + } + + public String getIndustry() { + return industry; + } + + public void setIndustry(String industry) { + this.industry = industry; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getIntentLevel() { + return intentLevel; + } + + public void setIntentLevel(String intentLevel) { + this.intentLevel = intentLevel; + } + + public String getIntent() { + return intent; + } + + public void setIntent(String intent) { + this.intent = intent; + } + + public String getStageCode() { + return stageCode; + } + + public void setStageCode(String stageCode) { + this.stageCode = stageCode; + } + + public String getStage() { + return stage; + } + + public void setStage(String stage) { + this.stage = stage; + } + + public Boolean getHasExp() { + return hasExp; + } + + public void setHasExp(Boolean hasExp) { + this.hasExp = hasExp; + } + + public Boolean getInProgress() { + return inProgress; + } + + public void setInProgress(Boolean inProgress) { + this.inProgress = inProgress; + } + + public Boolean getActive() { + return active; + } + + public void setActive(Boolean active) { + this.active = active; + } + + public String getEmploymentStatus() { + return employmentStatus; + } + + public void setEmploymentStatus(String employmentStatus) { + this.employmentStatus = employmentStatus; + } + + public String getExpectedJoinDate() { + return expectedJoinDate; + } + + public void setExpectedJoinDate(String expectedJoinDate) { + this.expectedJoinDate = expectedJoinDate; + } + + public String getNotes() { + return notes; + } + + public void setNotes(String notes) { + this.notes = notes; + } + + public List getFollowUps() { + return followUps; + } + + public void setFollowUps(List followUps) { + this.followUps = followUps; + } +} diff --git a/backend/src/main/java/com/unis/crm/dto/expansion/UpdateChannelExpansionRequest.java b/backend/src/main/java/com/unis/crm/dto/expansion/UpdateChannelExpansionRequest.java new file mode 100644 index 0000000..9176ac6 --- /dev/null +++ b/backend/src/main/java/com/unis/crm/dto/expansion/UpdateChannelExpansionRequest.java @@ -0,0 +1,121 @@ +package com.unis.crm.dto.expansion; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import java.math.BigDecimal; +import java.time.LocalDate; + +public class UpdateChannelExpansionRequest { + + @NotBlank(message = "渠道名称不能为空") + @Size(max = 200, message = "渠道名称不能超过200字符") + private String channelName; + + private String province; + private String industry; + private BigDecimal annualRevenue; + private Integer staffSize; + private String contactName; + private String contactTitle; + private String contactMobile; + private String stage; + private Boolean landedFlag; + private LocalDate expectedSignDate; + private String remark; + + public String getChannelName() { + return channelName; + } + + public void setChannelName(String channelName) { + this.channelName = channelName; + } + + public String getProvince() { + return province; + } + + public void setProvince(String province) { + this.province = province; + } + + public String getIndustry() { + return industry; + } + + public void setIndustry(String industry) { + this.industry = industry; + } + + public BigDecimal getAnnualRevenue() { + return annualRevenue; + } + + public void setAnnualRevenue(BigDecimal annualRevenue) { + this.annualRevenue = annualRevenue; + } + + public Integer getStaffSize() { + return staffSize; + } + + public void setStaffSize(Integer staffSize) { + this.staffSize = staffSize; + } + + public String getContactName() { + return contactName; + } + + public void setContactName(String contactName) { + this.contactName = contactName; + } + + public String getContactTitle() { + return contactTitle; + } + + public void setContactTitle(String contactTitle) { + this.contactTitle = contactTitle; + } + + public String getContactMobile() { + return contactMobile; + } + + public void setContactMobile(String contactMobile) { + this.contactMobile = contactMobile; + } + + public String getStage() { + return stage; + } + + public void setStage(String stage) { + this.stage = stage; + } + + public Boolean getLandedFlag() { + return landedFlag; + } + + public void setLandedFlag(Boolean landedFlag) { + this.landedFlag = landedFlag; + } + + public LocalDate getExpectedSignDate() { + return expectedSignDate; + } + + public void setExpectedSignDate(LocalDate expectedSignDate) { + this.expectedSignDate = expectedSignDate; + } + + public String getRemark() { + return remark; + } + + public void setRemark(String remark) { + this.remark = remark; + } +} diff --git a/backend/src/main/java/com/unis/crm/dto/expansion/UpdateSalesExpansionRequest.java b/backend/src/main/java/com/unis/crm/dto/expansion/UpdateSalesExpansionRequest.java new file mode 100644 index 0000000..385bd25 --- /dev/null +++ b/backend/src/main/java/com/unis/crm/dto/expansion/UpdateSalesExpansionRequest.java @@ -0,0 +1,129 @@ +package com.unis.crm.dto.expansion; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import java.time.LocalDate; + +public class UpdateSalesExpansionRequest { + + @NotBlank(message = "候选人姓名不能为空") + @Size(max = 50, message = "候选人姓名不能超过50字符") + private String candidateName; + + private String mobile; + private String email; + private Long targetDeptId; + private String industry; + private String title; + private String intentLevel; + private String stage; + private Boolean hasDesktopExp; + private Boolean inProgress; + private String employmentStatus; + private LocalDate expectedJoinDate; + private String remark; + + public String getCandidateName() { + return candidateName; + } + + public void setCandidateName(String candidateName) { + this.candidateName = candidateName; + } + + public String getMobile() { + return mobile; + } + + public void setMobile(String mobile) { + this.mobile = mobile; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public Long getTargetDeptId() { + return targetDeptId; + } + + public void setTargetDeptId(Long targetDeptId) { + this.targetDeptId = targetDeptId; + } + + public String getIndustry() { + return industry; + } + + public void setIndustry(String industry) { + this.industry = industry; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getIntentLevel() { + return intentLevel; + } + + public void setIntentLevel(String intentLevel) { + this.intentLevel = intentLevel; + } + + public String getStage() { + return stage; + } + + public void setStage(String stage) { + this.stage = stage; + } + + public Boolean getHasDesktopExp() { + return hasDesktopExp; + } + + public void setHasDesktopExp(Boolean hasDesktopExp) { + this.hasDesktopExp = hasDesktopExp; + } + + public Boolean getInProgress() { + return inProgress; + } + + public void setInProgress(Boolean inProgress) { + this.inProgress = inProgress; + } + + public String getEmploymentStatus() { + return employmentStatus; + } + + public void setEmploymentStatus(String employmentStatus) { + this.employmentStatus = employmentStatus; + } + + public LocalDate getExpectedJoinDate() { + return expectedJoinDate; + } + + public void setExpectedJoinDate(LocalDate expectedJoinDate) { + this.expectedJoinDate = expectedJoinDate; + } + + public String getRemark() { + return remark; + } + + public void setRemark(String remark) { + this.remark = remark; + } +} diff --git a/backend/src/main/java/com/unis/crm/dto/opportunity/CreateOpportunityFollowUpRequest.java b/backend/src/main/java/com/unis/crm/dto/opportunity/CreateOpportunityFollowUpRequest.java new file mode 100644 index 0000000..8ffc91c --- /dev/null +++ b/backend/src/main/java/com/unis/crm/dto/opportunity/CreateOpportunityFollowUpRequest.java @@ -0,0 +1,51 @@ +package com.unis.crm.dto.opportunity; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import java.time.OffsetDateTime; + +public class CreateOpportunityFollowUpRequest { + + @NotBlank(message = "跟进类型不能为空") + private String followUpType; + + @NotBlank(message = "跟进内容不能为空") + private String content; + + private String nextAction; + + @NotNull(message = "跟进时间不能为空") + private OffsetDateTime followUpTime; + + public String getFollowUpType() { + return followUpType; + } + + public void setFollowUpType(String followUpType) { + this.followUpType = followUpType; + } + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } + + public String getNextAction() { + return nextAction; + } + + public void setNextAction(String nextAction) { + this.nextAction = nextAction; + } + + public OffsetDateTime getFollowUpTime() { + return followUpTime; + } + + public void setFollowUpTime(OffsetDateTime followUpTime) { + this.followUpTime = followUpTime; + } +} diff --git a/backend/src/main/java/com/unis/crm/dto/opportunity/CreateOpportunityRequest.java b/backend/src/main/java/com/unis/crm/dto/opportunity/CreateOpportunityRequest.java new file mode 100644 index 0000000..04237d9 --- /dev/null +++ b/backend/src/main/java/com/unis/crm/dto/opportunity/CreateOpportunityRequest.java @@ -0,0 +1,135 @@ +package com.unis.crm.dto.opportunity; + +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import java.math.BigDecimal; +import java.time.LocalDate; + +public class CreateOpportunityRequest { + + private Long id; + + @NotBlank(message = "商机名称不能为空") + @Size(max = 200, message = "商机名称不能超过200字符") + private String opportunityName; + + @NotBlank(message = "客户名称不能为空") + @Size(max = 200, message = "客户名称不能超过200字符") + private String customerName; + + @NotNull(message = "商机金额不能为空") + private BigDecimal amount; + + private LocalDate expectedCloseDate; + + @NotNull(message = "把握度不能为空") + @Min(value = 0, message = "把握度不能低于0") + @Max(value = 100, message = "把握度不能高于100") + private Integer confidencePct; + + private String stage; + private String opportunityType; + private String productType; + private String source; + private Boolean pushedToOms; + private String description; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getOpportunityName() { + return opportunityName; + } + + public void setOpportunityName(String opportunityName) { + this.opportunityName = opportunityName; + } + + public String getCustomerName() { + return customerName; + } + + public void setCustomerName(String customerName) { + this.customerName = customerName; + } + + public BigDecimal getAmount() { + return amount; + } + + public void setAmount(BigDecimal amount) { + this.amount = amount; + } + + public LocalDate getExpectedCloseDate() { + return expectedCloseDate; + } + + public void setExpectedCloseDate(LocalDate expectedCloseDate) { + this.expectedCloseDate = expectedCloseDate; + } + + public Integer getConfidencePct() { + return confidencePct; + } + + public void setConfidencePct(Integer confidencePct) { + this.confidencePct = confidencePct; + } + + public String getStage() { + return stage; + } + + public void setStage(String stage) { + this.stage = stage; + } + + public String getOpportunityType() { + return opportunityType; + } + + public void setOpportunityType(String opportunityType) { + this.opportunityType = opportunityType; + } + + public String getProductType() { + return productType; + } + + public void setProductType(String productType) { + this.productType = productType; + } + + public String getSource() { + return source; + } + + public void setSource(String source) { + this.source = source; + } + + public Boolean getPushedToOms() { + return pushedToOms; + } + + public void setPushedToOms(Boolean pushedToOms) { + this.pushedToOms = pushedToOms; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } +} diff --git a/backend/src/main/java/com/unis/crm/dto/opportunity/OpportunityFollowUpDTO.java b/backend/src/main/java/com/unis/crm/dto/opportunity/OpportunityFollowUpDTO.java new file mode 100644 index 0000000..7e37a0a --- /dev/null +++ b/backend/src/main/java/com/unis/crm/dto/opportunity/OpportunityFollowUpDTO.java @@ -0,0 +1,59 @@ +package com.unis.crm.dto.opportunity; + +public class OpportunityFollowUpDTO { + + private Long id; + private Long opportunityId; + private String date; + private String type; + private String content; + private String user; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Long getOpportunityId() { + return opportunityId; + } + + public void setOpportunityId(Long opportunityId) { + this.opportunityId = opportunityId; + } + + public String getDate() { + return date; + } + + public void setDate(String date) { + this.date = date; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } + + public String getUser() { + return user; + } + + public void setUser(String user) { + this.user = user; + } +} diff --git a/backend/src/main/java/com/unis/crm/dto/opportunity/OpportunityItemDTO.java b/backend/src/main/java/com/unis/crm/dto/opportunity/OpportunityItemDTO.java new file mode 100644 index 0000000..6e0d58a --- /dev/null +++ b/backend/src/main/java/com/unis/crm/dto/opportunity/OpportunityItemDTO.java @@ -0,0 +1,144 @@ +package com.unis.crm.dto.opportunity; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; + +public class OpportunityItemDTO { + + private Long id; + private String code; + private String name; + private String client; + private String owner; + private BigDecimal amount; + private String date; + private Integer confidence; + private String stage; + private String type; + private Boolean pushedToOms; + private String product; + private String source; + private String notes; + private List followUps = new ArrayList<>(); + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getCode() { + return code; + } + + public void setCode(String code) { + this.code = code; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getClient() { + return client; + } + + public void setClient(String client) { + this.client = client; + } + + public String getOwner() { + return owner; + } + + public void setOwner(String owner) { + this.owner = owner; + } + + public BigDecimal getAmount() { + return amount; + } + + public void setAmount(BigDecimal amount) { + this.amount = amount; + } + + public String getDate() { + return date; + } + + public void setDate(String date) { + this.date = date; + } + + public Integer getConfidence() { + return confidence; + } + + public void setConfidence(Integer confidence) { + this.confidence = confidence; + } + + public String getStage() { + return stage; + } + + public void setStage(String stage) { + this.stage = stage; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public Boolean getPushedToOms() { + return pushedToOms; + } + + public void setPushedToOms(Boolean pushedToOms) { + this.pushedToOms = pushedToOms; + } + + public String getProduct() { + return product; + } + + public void setProduct(String product) { + this.product = product; + } + + public String getSource() { + return source; + } + + public void setSource(String source) { + this.source = source; + } + + public String getNotes() { + return notes; + } + + public void setNotes(String notes) { + this.notes = notes; + } + + public List getFollowUps() { + return followUps; + } + + public void setFollowUps(List followUps) { + this.followUps = followUps; + } +} diff --git a/backend/src/main/java/com/unis/crm/dto/opportunity/OpportunityOverviewDTO.java b/backend/src/main/java/com/unis/crm/dto/opportunity/OpportunityOverviewDTO.java new file mode 100644 index 0000000..3c2bd80 --- /dev/null +++ b/backend/src/main/java/com/unis/crm/dto/opportunity/OpportunityOverviewDTO.java @@ -0,0 +1,23 @@ +package com.unis.crm.dto.opportunity; + +import java.util.List; + +public class OpportunityOverviewDTO { + + private List items; + + public OpportunityOverviewDTO() { + } + + public OpportunityOverviewDTO(List items) { + this.items = items; + } + + public List getItems() { + return items; + } + + public void setItems(List items) { + this.items = items; + } +} diff --git a/backend/src/main/java/com/unis/crm/dto/work/CreateWorkCheckInRequest.java b/backend/src/main/java/com/unis/crm/dto/work/CreateWorkCheckInRequest.java new file mode 100644 index 0000000..d23d773 --- /dev/null +++ b/backend/src/main/java/com/unis/crm/dto/work/CreateWorkCheckInRequest.java @@ -0,0 +1,50 @@ +package com.unis.crm.dto.work; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import java.math.BigDecimal; + +public class CreateWorkCheckInRequest { + + @NotBlank(message = "打卡地点不能为空") + @Size(max = 500, message = "打卡地点不能超过500字符") + private String locationText; + + @Size(max = 500, message = "备注不能超过500字符") + private String remark; + + private BigDecimal longitude; + private BigDecimal latitude; + + public String getLocationText() { + return locationText; + } + + public void setLocationText(String locationText) { + this.locationText = locationText; + } + + public String getRemark() { + return remark; + } + + public void setRemark(String remark) { + this.remark = remark; + } + + public BigDecimal getLongitude() { + return longitude; + } + + public void setLongitude(BigDecimal longitude) { + this.longitude = longitude; + } + + public BigDecimal getLatitude() { + return latitude; + } + + public void setLatitude(BigDecimal latitude) { + this.latitude = latitude; + } +} diff --git a/backend/src/main/java/com/unis/crm/dto/work/CreateWorkDailyReportRequest.java b/backend/src/main/java/com/unis/crm/dto/work/CreateWorkDailyReportRequest.java new file mode 100644 index 0000000..afc27f7 --- /dev/null +++ b/backend/src/main/java/com/unis/crm/dto/work/CreateWorkDailyReportRequest.java @@ -0,0 +1,42 @@ +package com.unis.crm.dto.work; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; + +public class CreateWorkDailyReportRequest { + + @NotBlank(message = "今日工作内容不能为空") + @Size(max = 4000, message = "今日工作内容不能超过4000字符") + private String workContent; + + @NotBlank(message = "明日工作计划不能为空") + @Size(max = 4000, message = "明日工作计划不能超过4000字符") + private String tomorrowPlan; + + @Size(max = 50, message = "来源类型不能超过50字符") + private String sourceType; + + public String getWorkContent() { + return workContent; + } + + public void setWorkContent(String workContent) { + this.workContent = workContent; + } + + public String getTomorrowPlan() { + return tomorrowPlan; + } + + public void setTomorrowPlan(String tomorrowPlan) { + this.tomorrowPlan = tomorrowPlan; + } + + public String getSourceType() { + return sourceType; + } + + public void setSourceType(String sourceType) { + this.sourceType = sourceType; + } +} diff --git a/backend/src/main/java/com/unis/crm/dto/work/WorkCheckInDTO.java b/backend/src/main/java/com/unis/crm/dto/work/WorkCheckInDTO.java new file mode 100644 index 0000000..690e27e --- /dev/null +++ b/backend/src/main/java/com/unis/crm/dto/work/WorkCheckInDTO.java @@ -0,0 +1,79 @@ +package com.unis.crm.dto.work; + +import java.math.BigDecimal; + +public class WorkCheckInDTO { + + private Long id; + private String date; + private String time; + private String locationText; + private String remark; + private String status; + private BigDecimal longitude; + private BigDecimal latitude; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getDate() { + return date; + } + + public void setDate(String date) { + this.date = date; + } + + public String getTime() { + return time; + } + + public void setTime(String time) { + this.time = time; + } + + public String getLocationText() { + return locationText; + } + + public void setLocationText(String locationText) { + this.locationText = locationText; + } + + public String getRemark() { + return remark; + } + + public void setRemark(String remark) { + this.remark = remark; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + public BigDecimal getLongitude() { + return longitude; + } + + public void setLongitude(BigDecimal longitude) { + this.longitude = longitude; + } + + public BigDecimal getLatitude() { + return latitude; + } + + public void setLatitude(BigDecimal latitude) { + this.latitude = latitude; + } +} diff --git a/backend/src/main/java/com/unis/crm/dto/work/WorkDailyReportDTO.java b/backend/src/main/java/com/unis/crm/dto/work/WorkDailyReportDTO.java new file mode 100644 index 0000000..6641f85 --- /dev/null +++ b/backend/src/main/java/com/unis/crm/dto/work/WorkDailyReportDTO.java @@ -0,0 +1,86 @@ +package com.unis.crm.dto.work; + +public class WorkDailyReportDTO { + + private Long id; + private String date; + private String submitTime; + private String workContent; + private String tomorrowPlan; + private String sourceType; + private String status; + private Integer score; + private String comment; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getDate() { + return date; + } + + public void setDate(String date) { + this.date = date; + } + + public String getSubmitTime() { + return submitTime; + } + + public void setSubmitTime(String submitTime) { + this.submitTime = submitTime; + } + + public String getWorkContent() { + return workContent; + } + + public void setWorkContent(String workContent) { + this.workContent = workContent; + } + + public String getTomorrowPlan() { + return tomorrowPlan; + } + + public void setTomorrowPlan(String tomorrowPlan) { + this.tomorrowPlan = tomorrowPlan; + } + + public String getSourceType() { + return sourceType; + } + + public void setSourceType(String sourceType) { + this.sourceType = sourceType; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + public Integer getScore() { + return score; + } + + public void setScore(Integer score) { + this.score = score; + } + + public String getComment() { + return comment; + } + + public void setComment(String comment) { + this.comment = comment; + } +} diff --git a/backend/src/main/java/com/unis/crm/dto/work/WorkHistoryItemDTO.java b/backend/src/main/java/com/unis/crm/dto/work/WorkHistoryItemDTO.java new file mode 100644 index 0000000..fec76b4 --- /dev/null +++ b/backend/src/main/java/com/unis/crm/dto/work/WorkHistoryItemDTO.java @@ -0,0 +1,77 @@ +package com.unis.crm.dto.work; + +public class WorkHistoryItemDTO { + + private Long id; + private String type; + private String date; + private String time; + private String content; + private String status; + private Integer score; + private String comment; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getDate() { + return date; + } + + public void setDate(String date) { + this.date = date; + } + + public String getTime() { + return time; + } + + public void setTime(String time) { + this.time = time; + } + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + public Integer getScore() { + return score; + } + + public void setScore(Integer score) { + this.score = score; + } + + public String getComment() { + return comment; + } + + public void setComment(String comment) { + this.comment = comment; + } +} diff --git a/backend/src/main/java/com/unis/crm/dto/work/WorkOverviewDTO.java b/backend/src/main/java/com/unis/crm/dto/work/WorkOverviewDTO.java new file mode 100644 index 0000000..ea8d017 --- /dev/null +++ b/backend/src/main/java/com/unis/crm/dto/work/WorkOverviewDTO.java @@ -0,0 +1,57 @@ +package com.unis.crm.dto.work; + +import java.util.List; + +public class WorkOverviewDTO { + + private WorkCheckInDTO todayCheckIn; + private WorkDailyReportDTO todayReport; + private String suggestedWorkContent; + private List history; + + public WorkOverviewDTO() { + } + + public WorkOverviewDTO( + WorkCheckInDTO todayCheckIn, + WorkDailyReportDTO todayReport, + String suggestedWorkContent, + List history) { + this.todayCheckIn = todayCheckIn; + this.todayReport = todayReport; + this.suggestedWorkContent = suggestedWorkContent; + this.history = history; + } + + public WorkCheckInDTO getTodayCheckIn() { + return todayCheckIn; + } + + public void setTodayCheckIn(WorkCheckInDTO todayCheckIn) { + this.todayCheckIn = todayCheckIn; + } + + public WorkDailyReportDTO getTodayReport() { + return todayReport; + } + + public void setTodayReport(WorkDailyReportDTO todayReport) { + this.todayReport = todayReport; + } + + public String getSuggestedWorkContent() { + return suggestedWorkContent; + } + + public void setSuggestedWorkContent(String suggestedWorkContent) { + this.suggestedWorkContent = suggestedWorkContent; + } + + public List getHistory() { + return history; + } + + public void setHistory(List history) { + this.history = history; + } +} diff --git a/backend/src/main/java/com/unis/crm/mapper/DashboardMapper.java b/backend/src/main/java/com/unis/crm/mapper/DashboardMapper.java new file mode 100644 index 0000000..53ae3e2 --- /dev/null +++ b/backend/src/main/java/com/unis/crm/mapper/DashboardMapper.java @@ -0,0 +1,23 @@ +package com.unis.crm.mapper; + +import com.unis.crm.dto.dashboard.DashboardActivityDTO; +import com.unis.crm.dto.dashboard.DashboardStatDTO; +import com.unis.crm.dto.dashboard.DashboardTodoDTO; +import com.unis.crm.dto.dashboard.UserWelcomeDTO; +import java.util.List; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +@Mapper +public interface DashboardMapper { + + Long selectDefaultUserId(); + + UserWelcomeDTO selectUserWelcome(@Param("userId") Long userId); + + List selectDashboardStats(@Param("userId") Long userId); + + List selectPendingTodos(@Param("userId") Long userId); + + List selectLatestActivities(@Param("userId") Long userId); +} diff --git a/backend/src/main/java/com/unis/crm/mapper/ExpansionMapper.java b/backend/src/main/java/com/unis/crm/mapper/ExpansionMapper.java new file mode 100644 index 0000000..cc7f037 --- /dev/null +++ b/backend/src/main/java/com/unis/crm/mapper/ExpansionMapper.java @@ -0,0 +1,46 @@ +package com.unis.crm.mapper; + +import com.unis.crm.dto.expansion.ChannelExpansionItemDTO; +import com.unis.crm.dto.expansion.CreateChannelExpansionRequest; +import com.unis.crm.dto.expansion.CreateExpansionFollowUpRequest; +import com.unis.crm.dto.expansion.CreateSalesExpansionRequest; +import com.unis.crm.dto.expansion.DepartmentOptionDTO; +import com.unis.crm.dto.expansion.ExpansionFollowUpDTO; +import com.unis.crm.dto.expansion.SalesExpansionItemDTO; +import com.unis.crm.dto.expansion.UpdateChannelExpansionRequest; +import com.unis.crm.dto.expansion.UpdateSalesExpansionRequest; +import java.util.List; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +@Mapper +public interface ExpansionMapper { + + List selectDepartments(); + + List selectSalesExpansions(@Param("userId") Long userId, @Param("keyword") String keyword); + + List selectChannelExpansions(@Param("userId") Long userId, @Param("keyword") String keyword); + + List selectSalesFollowUps(@Param("userId") Long userId, @Param("bizIds") List bizIds); + + List selectChannelFollowUps(@Param("userId") Long userId, @Param("bizIds") List bizIds); + + int insertSalesExpansion(@Param("userId") Long userId, @Param("request") CreateSalesExpansionRequest request); + + int insertChannelExpansion(@Param("userId") Long userId, @Param("request") CreateChannelExpansionRequest request); + + int updateSalesExpansion(@Param("userId") Long userId, @Param("id") Long id, @Param("request") UpdateSalesExpansionRequest request); + + int updateChannelExpansion(@Param("userId") Long userId, @Param("id") Long id, @Param("request") UpdateChannelExpansionRequest request); + + int countOwnedSalesExpansion(@Param("userId") Long userId, @Param("id") Long id); + + int countOwnedChannelExpansion(@Param("userId") Long userId, @Param("id") Long id); + + int insertExpansionFollowUp( + @Param("bizType") String bizType, + @Param("bizId") Long bizId, + @Param("userId") Long userId, + @Param("request") CreateExpansionFollowUpRequest request); +} diff --git a/backend/src/main/java/com/unis/crm/mapper/OpportunityMapper.java b/backend/src/main/java/com/unis/crm/mapper/OpportunityMapper.java new file mode 100644 index 0000000..dd73a96 --- /dev/null +++ b/backend/src/main/java/com/unis/crm/mapper/OpportunityMapper.java @@ -0,0 +1,48 @@ +package com.unis.crm.mapper; + +import com.unis.crm.dto.opportunity.CreateOpportunityFollowUpRequest; +import com.unis.crm.dto.opportunity.CreateOpportunityRequest; +import com.unis.crm.dto.opportunity.OpportunityFollowUpDTO; +import com.unis.crm.dto.opportunity.OpportunityItemDTO; +import java.util.List; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +@Mapper +public interface OpportunityMapper { + + List selectOpportunities( + @Param("userId") Long userId, + @Param("keyword") String keyword, + @Param("stage") String stage); + + List selectOpportunityFollowUps( + @Param("userId") Long userId, + @Param("opportunityIds") List opportunityIds); + + Long selectOwnedCustomerIdByName(@Param("userId") Long userId, @Param("customerName") String customerName); + + int insertCustomer( + @Param("id") Long id, + @Param("userId") Long userId, + @Param("customerName") String customerName, + @Param("source") String source); + + int insertOpportunity( + @Param("userId") Long userId, + @Param("customerId") Long customerId, + @Param("request") CreateOpportunityRequest request); + + int countOwnedOpportunity(@Param("userId") Long userId, @Param("id") Long id); + + int updateOpportunity( + @Param("userId") Long userId, + @Param("opportunityId") Long opportunityId, + @Param("customerId") Long customerId, + @Param("request") CreateOpportunityRequest request); + + int insertOpportunityFollowUp( + @Param("userId") Long userId, + @Param("opportunityId") Long opportunityId, + @Param("request") CreateOpportunityFollowUpRequest request); +} diff --git a/backend/src/main/java/com/unis/crm/mapper/WorkMapper.java b/backend/src/main/java/com/unis/crm/mapper/WorkMapper.java new file mode 100644 index 0000000..2c98f1a --- /dev/null +++ b/backend/src/main/java/com/unis/crm/mapper/WorkMapper.java @@ -0,0 +1,47 @@ +package com.unis.crm.mapper; + +import com.unis.crm.dto.work.CreateWorkCheckInRequest; +import com.unis.crm.dto.work.CreateWorkDailyReportRequest; +import com.unis.crm.dto.work.WorkCheckInDTO; +import com.unis.crm.dto.work.WorkDailyReportDTO; +import com.unis.crm.dto.work.WorkHistoryItemDTO; +import java.util.List; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +@Mapper +public interface WorkMapper { + + WorkCheckInDTO selectTodayCheckIn(@Param("userId") Long userId); + + WorkDailyReportDTO selectTodayReport(@Param("userId") Long userId); + + List selectTodayWorkContentLines(@Param("userId") Long userId); + + List selectHistory(@Param("userId") Long userId); + + Long selectTodayCheckInId(@Param("userId") Long userId); + + int insertCheckIn(@Param("userId") Long userId, @Param("request") CreateWorkCheckInRequest request); + + int updateCheckIn(@Param("checkInId") Long checkInId, @Param("request") CreateWorkCheckInRequest request); + + Long selectTodayReportId(@Param("userId") Long userId); + + int insertDailyReport(@Param("userId") Long userId, @Param("request") CreateWorkDailyReportRequest request); + + int updateDailyReport(@Param("reportId") Long reportId, @Param("request") CreateWorkDailyReportRequest request); + + Long selectTodoIdByBiz(@Param("userId") Long userId, @Param("bizType") String bizType, @Param("bizId") Long bizId); + + int insertTodo( + @Param("todoId") Long todoId, + @Param("userId") Long userId, + @Param("title") String title, + @Param("bizType") String bizType, + @Param("bizId") Long bizId); + + int updateTodo( + @Param("todoId") Long todoId, + @Param("title") String title); +} diff --git a/backend/src/main/java/com/unis/crm/service/DashboardService.java b/backend/src/main/java/com/unis/crm/service/DashboardService.java new file mode 100644 index 0000000..20eb113 --- /dev/null +++ b/backend/src/main/java/com/unis/crm/service/DashboardService.java @@ -0,0 +1,8 @@ +package com.unis.crm.service; + +import com.unis.crm.dto.dashboard.DashboardHomeDTO; + +public interface DashboardService { + + DashboardHomeDTO getHome(Long userId); +} diff --git a/backend/src/main/java/com/unis/crm/service/ExpansionService.java b/backend/src/main/java/com/unis/crm/service/ExpansionService.java new file mode 100644 index 0000000..6be1f84 --- /dev/null +++ b/backend/src/main/java/com/unis/crm/service/ExpansionService.java @@ -0,0 +1,26 @@ +package com.unis.crm.service; + +import com.unis.crm.dto.expansion.CreateChannelExpansionRequest; +import com.unis.crm.dto.expansion.CreateExpansionFollowUpRequest; +import com.unis.crm.dto.expansion.CreateSalesExpansionRequest; +import com.unis.crm.dto.expansion.ExpansionMetaDTO; +import com.unis.crm.dto.expansion.ExpansionOverviewDTO; +import com.unis.crm.dto.expansion.UpdateChannelExpansionRequest; +import com.unis.crm.dto.expansion.UpdateSalesExpansionRequest; + +public interface ExpansionService { + + ExpansionMetaDTO getMeta(); + + ExpansionOverviewDTO getOverview(Long userId, String keyword); + + Long createSalesExpansion(Long userId, CreateSalesExpansionRequest request); + + Long createChannelExpansion(Long userId, CreateChannelExpansionRequest request); + + void updateSalesExpansion(Long userId, Long id, UpdateSalesExpansionRequest request); + + void updateChannelExpansion(Long userId, Long id, UpdateChannelExpansionRequest request); + + Long createFollowUp(Long userId, String bizType, Long bizId, CreateExpansionFollowUpRequest request); +} diff --git a/backend/src/main/java/com/unis/crm/service/OpportunityService.java b/backend/src/main/java/com/unis/crm/service/OpportunityService.java new file mode 100644 index 0000000..14102c1 --- /dev/null +++ b/backend/src/main/java/com/unis/crm/service/OpportunityService.java @@ -0,0 +1,16 @@ +package com.unis.crm.service; + +import com.unis.crm.dto.opportunity.CreateOpportunityFollowUpRequest; +import com.unis.crm.dto.opportunity.CreateOpportunityRequest; +import com.unis.crm.dto.opportunity.OpportunityOverviewDTO; + +public interface OpportunityService { + + OpportunityOverviewDTO getOverview(Long userId, String keyword, String stage); + + Long createOpportunity(Long userId, CreateOpportunityRequest request); + + Long updateOpportunity(Long userId, Long opportunityId, CreateOpportunityRequest request); + + Long createFollowUp(Long userId, Long opportunityId, CreateOpportunityFollowUpRequest request); +} diff --git a/backend/src/main/java/com/unis/crm/service/WorkService.java b/backend/src/main/java/com/unis/crm/service/WorkService.java new file mode 100644 index 0000000..bca612a --- /dev/null +++ b/backend/src/main/java/com/unis/crm/service/WorkService.java @@ -0,0 +1,17 @@ +package com.unis.crm.service; + +import com.unis.crm.dto.work.CreateWorkCheckInRequest; +import com.unis.crm.dto.work.CreateWorkDailyReportRequest; +import com.unis.crm.dto.work.WorkOverviewDTO; +import java.math.BigDecimal; + +public interface WorkService { + + WorkOverviewDTO getOverview(Long userId); + + Long saveCheckIn(Long userId, CreateWorkCheckInRequest request); + + Long saveDailyReport(Long userId, CreateWorkDailyReportRequest request); + + String resolveLocationName(BigDecimal latitude, BigDecimal longitude); +} diff --git a/backend/src/main/java/com/unis/crm/service/impl/DashboardServiceImpl.java b/backend/src/main/java/com/unis/crm/service/impl/DashboardServiceImpl.java new file mode 100644 index 0000000..08a5808 --- /dev/null +++ b/backend/src/main/java/com/unis/crm/service/impl/DashboardServiceImpl.java @@ -0,0 +1,102 @@ +package com.unis.crm.service.impl; + +import com.unis.crm.common.BusinessException; +import com.unis.crm.dto.dashboard.DashboardActivityDTO; +import com.unis.crm.dto.dashboard.DashboardHomeDTO; +import com.unis.crm.dto.dashboard.DashboardStatDTO; +import com.unis.crm.dto.dashboard.DashboardTodoDTO; +import com.unis.crm.dto.dashboard.UserWelcomeDTO; +import com.unis.crm.mapper.DashboardMapper; +import com.unis.crm.service.DashboardService; +import java.time.LocalDate; +import java.time.OffsetDateTime; +import java.time.ZoneId; +import java.time.temporal.ChronoUnit; +import java.util.List; +import org.springframework.stereotype.Service; + +@Service +public class DashboardServiceImpl implements DashboardService { + + private final DashboardMapper dashboardMapper; + + public DashboardServiceImpl(DashboardMapper dashboardMapper) { + this.dashboardMapper = dashboardMapper; + } + + @Override + public DashboardHomeDTO getHome(Long userId) { + if (userId == null) { + throw new BusinessException("未获取到当前登录用户,禁止查询他人数据"); + } + + UserWelcomeDTO user = dashboardMapper.selectUserWelcome(userId); + if (user == null) { + throw new BusinessException("未找到当前用户对应数据"); + } + + List stats = dashboardMapper.selectDashboardStats(userId); + List todos = dashboardMapper.selectPendingTodos(userId); + List activities = dashboardMapper.selectLatestActivities(userId); + enrichActivityTimeText(activities); + + long onboardingDays = 0; + LocalDate hireDate = user.getHireDate(); + if (hireDate != null) { + onboardingDays = ChronoUnit.DAYS.between(hireDate, LocalDate.now()) + 1; + } + + return new DashboardHomeDTO( + user.getUserId(), + user.getRealName(), + user.getJobTitle(), + user.getDeptName(), + onboardingDays, + stats, + todos, + activities + ); + } + + private void enrichActivityTimeText(List activities) { + if (activities == null || activities.isEmpty()) { + return; + } + + OffsetDateTime now = OffsetDateTime.now(); + for (DashboardActivityDTO activity : activities) { + activity.setTimeText(formatRelativeTime(activity.getCreatedAt(), now)); + } + } + + private String formatRelativeTime(OffsetDateTime createdAt, OffsetDateTime now) { + if (createdAt == null) { + return ""; + } + + long minutes = ChronoUnit.MINUTES.between(createdAt, now); + if (minutes < 1) { + return "刚刚"; + } + if (minutes < 60) { + return minutes + "分钟前"; + } + + long hours = ChronoUnit.HOURS.between(createdAt, now); + if (hours < 24) { + return hours + "小时前"; + } + + LocalDate createdDate = createdAt.atZoneSameInstant(ZoneId.systemDefault()).toLocalDate(); + LocalDate today = now.atZoneSameInstant(ZoneId.systemDefault()).toLocalDate(); + long days = ChronoUnit.DAYS.between(createdDate, today); + if (days == 1) { + return "昨天"; + } + if (days < 7) { + return days + "天前"; + } + + return createdAt.toLocalDate().toString(); + } +} diff --git a/backend/src/main/java/com/unis/crm/service/impl/ExpansionServiceImpl.java b/backend/src/main/java/com/unis/crm/service/impl/ExpansionServiceImpl.java new file mode 100644 index 0000000..622f018 --- /dev/null +++ b/backend/src/main/java/com/unis/crm/service/impl/ExpansionServiceImpl.java @@ -0,0 +1,248 @@ +package com.unis.crm.service.impl; + +import com.unis.crm.common.BusinessException; +import com.unis.crm.dto.expansion.ChannelExpansionItemDTO; +import com.unis.crm.dto.expansion.CreateChannelExpansionRequest; +import com.unis.crm.dto.expansion.CreateExpansionFollowUpRequest; +import com.unis.crm.dto.expansion.CreateSalesExpansionRequest; +import com.unis.crm.dto.expansion.ExpansionMetaDTO; +import com.unis.crm.dto.expansion.ExpansionFollowUpDTO; +import com.unis.crm.dto.expansion.ExpansionOverviewDTO; +import com.unis.crm.dto.expansion.SalesExpansionItemDTO; +import com.unis.crm.dto.expansion.UpdateChannelExpansionRequest; +import com.unis.crm.dto.expansion.UpdateSalesExpansionRequest; +import com.unis.crm.mapper.ExpansionMapper; +import com.unis.crm.service.ExpansionService; +import java.time.format.DateTimeFormatter; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; + +@Service +public class ExpansionServiceImpl implements ExpansionService { + + private static final DateTimeFormatter FOLLOW_UP_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); + private static final Logger log = LoggerFactory.getLogger(ExpansionServiceImpl.class); + + private final ExpansionMapper expansionMapper; + + public ExpansionServiceImpl(ExpansionMapper expansionMapper) { + this.expansionMapper = expansionMapper; + } + + @Override + public ExpansionMetaDTO getMeta() { + try { + return new ExpansionMetaDTO(expansionMapper.selectDepartments()); + } catch (Exception ex) { + log.warn("Failed to load expansion departments, fallback to empty list", ex); + return new ExpansionMetaDTO(Collections.emptyList()); + } + } + + @Override + public ExpansionOverviewDTO getOverview(Long userId, String keyword) { + String normalizedKeyword = normalizeKeyword(keyword); + List salesItems = expansionMapper.selectSalesExpansions(userId, normalizedKeyword); + List channelItems = expansionMapper.selectChannelExpansions(userId, normalizedKeyword); + + attachSalesFollowUps(userId, salesItems); + attachChannelFollowUps(userId, channelItems); + + return new ExpansionOverviewDTO(salesItems, channelItems); + } + + @Override + public Long createSalesExpansion(Long userId, CreateSalesExpansionRequest request) { + fillSalesDefaults(request); + expansionMapper.insertSalesExpansion(userId, request); + if (request.getId() == null) { + throw new BusinessException("销售拓展新增失败"); + } + return request.getId(); + } + + @Override + public Long createChannelExpansion(Long userId, CreateChannelExpansionRequest request) { + fillChannelDefaults(request); + expansionMapper.insertChannelExpansion(userId, request); + if (request.getId() == null) { + throw new BusinessException("渠道拓展新增失败"); + } + return request.getId(); + } + + @Override + public void updateSalesExpansion(Long userId, Long id, UpdateSalesExpansionRequest request) { + fillSalesDefaults(request); + int updated = expansionMapper.updateSalesExpansion(userId, id, request); + if (updated <= 0) { + throw new BusinessException("未找到可编辑的销售拓展记录"); + } + } + + @Override + public void updateChannelExpansion(Long userId, Long id, UpdateChannelExpansionRequest request) { + fillChannelDefaults(request); + int updated = expansionMapper.updateChannelExpansion(userId, id, request); + if (updated <= 0) { + throw new BusinessException("未找到可编辑的渠道拓展记录"); + } + } + + @Override + public Long createFollowUp(Long userId, String bizType, Long bizId, CreateExpansionFollowUpRequest request) { + String normalizedBizType = normalizeBizType(bizType); + ensureOwnedExpansion(userId, normalizedBizType, bizId); + int inserted = expansionMapper.insertExpansionFollowUp(normalizedBizType, bizId, userId, request); + if (inserted <= 0) { + throw new BusinessException("跟进记录新增失败"); + } + return Long.valueOf(inserted); + } + + private void attachSalesFollowUps(Long userId, List salesItems) { + List bizIds = salesItems.stream() + .map(SalesExpansionItemDTO::getId) + .filter(Objects::nonNull) + .toList(); + if (bizIds.isEmpty()) { + return; + } + + Map> grouped = expansionMapper.selectSalesFollowUps(userId, bizIds).stream() + .peek(this::fillFollowUpDisplayFields) + .collect(Collectors.groupingBy(ExpansionFollowUpDTO::getBizId)); + + for (SalesExpansionItemDTO item : salesItems) { + item.setFollowUps(grouped.getOrDefault(item.getId(), Collections.emptyList())); + } + } + + private void attachChannelFollowUps(Long userId, List channelItems) { + List bizIds = channelItems.stream() + .map(ChannelExpansionItemDTO::getId) + .filter(Objects::nonNull) + .toList(); + if (bizIds.isEmpty()) { + return; + } + + Map> grouped = expansionMapper.selectChannelFollowUps(userId, bizIds).stream() + .peek(this::fillFollowUpDisplayFields) + .collect(Collectors.groupingBy(ExpansionFollowUpDTO::getBizId)); + + for (ChannelExpansionItemDTO item : channelItems) { + item.setFollowUps(grouped.getOrDefault(item.getId(), Collections.emptyList())); + } + } + + private void fillFollowUpDisplayFields(ExpansionFollowUpDTO followUp) { + if (followUp.getFollowUpTime() != null) { + followUp.setDate(followUp.getFollowUpTime().format(FOLLOW_UP_TIME_FORMATTER)); + } + if (isBlank(followUp.getType())) { + followUp.setType("无"); + } + if (isBlank(followUp.getContent())) { + followUp.setContent("无"); + } + if (isBlank(followUp.getUser())) { + followUp.setUser("无"); + } + } + + private String normalizeKeyword(String keyword) { + if (keyword == null) { + return null; + } + String trimmed = keyword.trim(); + return trimmed.isEmpty() ? null : trimmed; + } + + private boolean isBlank(String value) { + return value == null || value.trim().isEmpty(); + } + + private void fillSalesDefaults(CreateSalesExpansionRequest request) { + if (isBlank(request.getIntentLevel())) { + request.setIntentLevel("medium"); + } + if (isBlank(request.getStage())) { + request.setStage("initial_contact"); + } + if (request.getHasDesktopExp() == null) { + request.setHasDesktopExp(Boolean.FALSE); + } + if (request.getInProgress() == null) { + request.setInProgress(Boolean.TRUE); + } + if (isBlank(request.getEmploymentStatus())) { + request.setEmploymentStatus("active"); + } + } + + private void fillSalesDefaults(UpdateSalesExpansionRequest request) { + if (isBlank(request.getIntentLevel())) { + request.setIntentLevel("medium"); + } + if (isBlank(request.getStage())) { + request.setStage("initial_contact"); + } + if (request.getHasDesktopExp() == null) { + request.setHasDesktopExp(Boolean.FALSE); + } + if (request.getInProgress() == null) { + request.setInProgress(Boolean.TRUE); + } + if (isBlank(request.getEmploymentStatus())) { + request.setEmploymentStatus("active"); + } + } + + private void fillChannelDefaults(CreateChannelExpansionRequest request) { + if (isBlank(request.getStage())) { + request.setStage("initial_contact"); + } + if (request.getLandedFlag() == null) { + request.setLandedFlag(Boolean.FALSE); + } + } + + private void fillChannelDefaults(UpdateChannelExpansionRequest request) { + if (isBlank(request.getStage())) { + request.setStage("initial_contact"); + } + if (request.getLandedFlag() == null) { + request.setLandedFlag(Boolean.FALSE); + } + } + + private String normalizeBizType(String bizType) { + if ("sales".equalsIgnoreCase(bizType)) { + return "sales"; + } + if ("channel".equalsIgnoreCase(bizType)) { + return "channel"; + } + throw new BusinessException("不支持的拓展类型"); + } + + private void ensureOwnedExpansion(Long userId, String bizType, Long bizId) { + if (bizId == null || bizId <= 0) { + throw new BusinessException("拓展记录不存在"); + } + + int count = "sales".equals(bizType) + ? expansionMapper.countOwnedSalesExpansion(userId, bizId) + : expansionMapper.countOwnedChannelExpansion(userId, bizId); + if (count <= 0) { + throw new BusinessException("无权操作该拓展记录"); + } + } +} diff --git a/backend/src/main/java/com/unis/crm/service/impl/OpportunityServiceImpl.java b/backend/src/main/java/com/unis/crm/service/impl/OpportunityServiceImpl.java new file mode 100644 index 0000000..f706671 --- /dev/null +++ b/backend/src/main/java/com/unis/crm/service/impl/OpportunityServiceImpl.java @@ -0,0 +1,188 @@ +package com.unis.crm.service.impl; + +import com.baomidou.mybatisplus.core.toolkit.IdWorker; +import com.unis.crm.common.BusinessException; +import com.unis.crm.dto.opportunity.CreateOpportunityFollowUpRequest; +import com.unis.crm.dto.opportunity.CreateOpportunityRequest; +import com.unis.crm.dto.opportunity.OpportunityFollowUpDTO; +import com.unis.crm.dto.opportunity.OpportunityItemDTO; +import com.unis.crm.dto.opportunity.OpportunityOverviewDTO; +import com.unis.crm.mapper.OpportunityMapper; +import com.unis.crm.service.OpportunityService; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; +import org.springframework.stereotype.Service; + +@Service +public class OpportunityServiceImpl implements OpportunityService { + + private final OpportunityMapper opportunityMapper; + + public OpportunityServiceImpl(OpportunityMapper opportunityMapper) { + this.opportunityMapper = opportunityMapper; + } + + @Override + public OpportunityOverviewDTO getOverview(Long userId, String keyword, String stage) { + String normalizedKeyword = normalizeKeyword(keyword); + String normalizedStage = normalizeStage(stage); + List items = opportunityMapper.selectOpportunities(userId, normalizedKeyword, normalizedStage); + attachFollowUps(userId, items); + return new OpportunityOverviewDTO(items); + } + + @Override + public Long createOpportunity(Long userId, CreateOpportunityRequest request) { + fillDefaults(request); + Long customerId = opportunityMapper.selectOwnedCustomerIdByName(userId, request.getCustomerName().trim()); + if (customerId == null) { + customerId = IdWorker.getId(); + opportunityMapper.insertCustomer(customerId, userId, request.getCustomerName().trim(), request.getSource()); + } + + opportunityMapper.insertOpportunity(userId, customerId, request); + if (request.getId() == null) { + throw new BusinessException("商机新增失败"); + } + return request.getId(); + } + + @Override + public Long updateOpportunity(Long userId, Long opportunityId, CreateOpportunityRequest request) { + if (opportunityId == null || opportunityId <= 0) { + throw new BusinessException("商机不存在"); + } + if (opportunityMapper.countOwnedOpportunity(userId, opportunityId) <= 0) { + throw new BusinessException("无权编辑该商机"); + } + + fillDefaults(request); + Long customerId = opportunityMapper.selectOwnedCustomerIdByName(userId, request.getCustomerName().trim()); + if (customerId == null) { + customerId = IdWorker.getId(); + opportunityMapper.insertCustomer(customerId, userId, request.getCustomerName().trim(), request.getSource()); + } + + int updated = opportunityMapper.updateOpportunity(userId, opportunityId, customerId, request); + if (updated <= 0) { + throw new BusinessException("商机更新失败"); + } + return opportunityId; + } + + @Override + public Long createFollowUp(Long userId, Long opportunityId, CreateOpportunityFollowUpRequest request) { + if (opportunityId == null || opportunityId <= 0) { + throw new BusinessException("商机不存在"); + } + if (opportunityMapper.countOwnedOpportunity(userId, opportunityId) <= 0) { + throw new BusinessException("无权操作该商机"); + } + int inserted = opportunityMapper.insertOpportunityFollowUp(userId, opportunityId, request); + if (inserted <= 0) { + throw new BusinessException("商机跟进新增失败"); + } + return Long.valueOf(inserted); + } + + private void attachFollowUps(Long userId, List items) { + List opportunityIds = items.stream() + .map(OpportunityItemDTO::getId) + .filter(Objects::nonNull) + .toList(); + if (opportunityIds.isEmpty()) { + return; + } + + Map> grouped = opportunityMapper + .selectOpportunityFollowUps(userId, opportunityIds) + .stream() + .peek(this::fillFollowUpDisplayFields) + .collect(Collectors.groupingBy(OpportunityFollowUpDTO::getOpportunityId)); + + for (OpportunityItemDTO item : items) { + item.setFollowUps(grouped.getOrDefault(item.getId(), Collections.emptyList())); + } + } + + private void fillFollowUpDisplayFields(OpportunityFollowUpDTO followUp) { + if (isBlank(followUp.getType())) { + followUp.setType("无"); + } + if (isBlank(followUp.getContent())) { + followUp.setContent("无"); + } + if (isBlank(followUp.getUser())) { + followUp.setUser("无"); + } + if (!isBlank(followUp.getDate())) { + followUp.setDate(followUp.getDate()); + } + } + + private void fillDefaults(CreateOpportunityRequest request) { + request.setCustomerName(request.getCustomerName().trim()); + request.setOpportunityName(request.getOpportunityName().trim()); + if (request.getExpectedCloseDate() == null) { + throw new BusinessException("预计结单日期不能为空"); + } + if (isBlank(request.getStage())) { + request.setStage("initial_contact"); + } else { + request.setStage(toStageCode(request.getStage())); + } + if (isBlank(request.getOpportunityType())) { + request.setOpportunityType("新建"); + } + if (isBlank(request.getProductType())) { + request.setProductType("VDI云桌面"); + } + if (isBlank(request.getSource())) { + request.setSource("主动开发"); + } + if (request.getPushedToOms() == null) { + request.setPushedToOms(Boolean.FALSE); + } + if (request.getConfidencePct() == null) { + request.setConfidencePct(50); + } + } + + private String normalizeKeyword(String keyword) { + if (keyword == null) { + return null; + } + String trimmed = keyword.trim(); + return trimmed.isEmpty() ? null : trimmed; + } + + private String normalizeStage(String stage) { + if (stage == null) { + return null; + } + String trimmed = stage.trim(); + if (trimmed.isEmpty() || "全部".equals(trimmed)) { + return null; + } + return toStageCode(trimmed); + } + + private boolean isBlank(String value) { + return value == null || value.trim().isEmpty(); + } + + private String toStageCode(String value) { + return switch (value) { + case "初步沟通", "initial_contact" -> "initial_contact"; + case "方案交流", "solution_discussion" -> "solution_discussion"; + case "招投标", "bidding" -> "bidding"; + case "商务谈判", "business_negotiation" -> "business_negotiation"; + case "已成交", "won" -> "won"; + case "已放弃", "lost" -> "lost"; + default -> throw new BusinessException("不支持的商机阶段"); + }; + } +} diff --git a/backend/src/main/java/com/unis/crm/service/impl/WorkServiceImpl.java b/backend/src/main/java/com/unis/crm/service/impl/WorkServiceImpl.java new file mode 100644 index 0000000..c0090b4 --- /dev/null +++ b/backend/src/main/java/com/unis/crm/service/impl/WorkServiceImpl.java @@ -0,0 +1,333 @@ +package com.unis.crm.service.impl; + +import com.baomidou.mybatisplus.core.toolkit.IdWorker; +import com.unis.crm.common.BusinessException; +import com.unis.crm.dto.work.CreateWorkCheckInRequest; +import com.unis.crm.dto.work.CreateWorkDailyReportRequest; +import com.unis.crm.dto.work.WorkCheckInDTO; +import com.unis.crm.dto.work.WorkDailyReportDTO; +import com.unis.crm.dto.work.WorkHistoryItemDTO; +import com.unis.crm.dto.work.WorkOverviewDTO; +import com.unis.crm.mapper.WorkMapper; +import com.unis.crm.service.WorkService; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.math.BigDecimal; +import java.net.URI; +import java.net.URLEncoder; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.List; +import org.springframework.stereotype.Service; + +@Service +public class WorkServiceImpl implements WorkService { + + private static final String NOMINATIM_BASE_URL = "https://nominatim.openstreetmap.org/reverse"; + + private final WorkMapper workMapper; + private final HttpClient httpClient; + private final ObjectMapper objectMapper; + + public WorkServiceImpl(WorkMapper workMapper, ObjectMapper objectMapper) { + this.workMapper = workMapper; + this.objectMapper = objectMapper; + this.httpClient = HttpClient.newBuilder() + .connectTimeout(Duration.ofSeconds(8)) + .build(); + } + + @Override + public WorkOverviewDTO getOverview(Long userId) { + requireUser(userId); + WorkCheckInDTO todayCheckIn = workMapper.selectTodayCheckIn(userId); + enrichCheckInLocation(todayCheckIn); + WorkDailyReportDTO todayReport = workMapper.selectTodayReport(userId); + String suggestedWorkContent = buildSuggestedWorkContent(workMapper.selectTodayWorkContentLines(userId)); + List history = workMapper.selectHistory(userId); + return new WorkOverviewDTO(todayCheckIn, todayReport, suggestedWorkContent, history); + } + + @Override + public Long saveCheckIn(Long userId, CreateWorkCheckInRequest request) { + requireUser(userId); + request.setLocationText(request.getLocationText().trim()); + request.setRemark(normalizeOptionalText(request.getRemark())); + + int affectedRows = workMapper.insertCheckIn(userId, request); + Long checkInId = workMapper.selectTodayCheckInId(userId); + + if (affectedRows <= 0 || checkInId == null) { + throw new BusinessException("外勤打卡保存失败"); + } + return checkInId; + } + + @Override + public Long saveDailyReport(Long userId, CreateWorkDailyReportRequest request) { + requireUser(userId); + request.setWorkContent(request.getWorkContent().trim()); + request.setTomorrowPlan(request.getTomorrowPlan().trim()); + request.setSourceType(normalizeOptionalText(request.getSourceType())); + if (request.getSourceType() == null) { + request.setSourceType("manual"); + } + + Long reportId = workMapper.selectTodayReportId(userId); + int affectedRows; + if (reportId == null) { + affectedRows = workMapper.insertDailyReport(userId, request); + reportId = workMapper.selectTodayReportId(userId); + } else { + affectedRows = workMapper.updateDailyReport(reportId, request); + } + + if (affectedRows <= 0 || reportId == null) { + throw new BusinessException("日报保存失败"); + } + syncTomorrowPlanTodo(userId, reportId, request.getTomorrowPlan()); + return reportId; + } + + @Override + public String resolveLocationName(BigDecimal latitude, BigDecimal longitude) { + if (latitude == null || longitude == null) { + throw new BusinessException("定位坐标不能为空"); + } + + try { + String requestUrl = NOMINATIM_BASE_URL + + "?format=jsonv2&addressdetails=1&namedetails=1&extratags=1&zoom=18" + + "&lat=" + URLEncoder.encode(latitude.stripTrailingZeros().toPlainString(), StandardCharsets.UTF_8) + + "&lon=" + URLEncoder.encode(longitude.stripTrailingZeros().toPlainString(), StandardCharsets.UTF_8) + + "&accept-language=" + URLEncoder.encode("zh-CN,zh;q=0.9,en;q=0.8", StandardCharsets.UTF_8); + + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(requestUrl)) + .header("Accept", "application/json") + .header("User-Agent", "unis-crm-backend/1.0 (workbench reverse geocoding)") + .timeout(Duration.ofSeconds(10)) + .GET() + .build(); + + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); + if (response.statusCode() < 200 || response.statusCode() >= 300) { + throw new BusinessException("地点解析失败,请稍后重试"); + } + + JsonNode root = objectMapper.readTree(response.body()); + JsonNode addressNode = root.path("address"); + String orderedLocation = buildOrderedLocationName(addressNode); + if (orderedLocation != null) { + return orderedLocation; + } + + String displayName = textValue(root, "display_name"); + if (displayName != null) { + String normalizedDisplayName = normalizeDisplayName(displayName); + if (normalizedDisplayName != null) { + return normalizedDisplayName; + } + return displayName; + } + } catch (BusinessException exception) { + throw exception; + } catch (Exception exception) { + throw new BusinessException("地点解析失败,请检查网络后重试"); + } + + throw new BusinessException("未能解析出具体地点名称"); + } + + private void requireUser(Long userId) { + if (userId == null || userId <= 0) { + throw new BusinessException("未获取到当前登录用户"); + } + } + + private String buildSuggestedWorkContent(List lines) { + if (lines == null || lines.isEmpty()) { + return ""; + } + + StringBuilder builder = new StringBuilder(); + int index = 1; + for (String line : lines) { + String normalized = normalizeOptionalText(line); + if (normalized == null) { + continue; + } + if (builder.length() > 0) { + builder.append('\n'); + } + builder.append(index).append(". ").append(normalized); + index++; + } + return builder.toString(); + } + + private String normalizeOptionalText(String value) { + if (value == null) { + return null; + } + String trimmed = value.trim(); + return trimmed.isEmpty() ? null : trimmed; + } + + private String textValue(JsonNode node, String fieldName) { + if (node == null || node.isMissingNode()) { + return null; + } + String value = normalizeOptionalText(node.path(fieldName).asText(null)); + return value; + } + + private String joinNonBlank(String... values) { + StringBuilder builder = new StringBuilder(); + for (String value : values) { + String normalized = normalizeOptionalText(value); + if (normalized == null) { + continue; + } + if (builder.indexOf(normalized) >= 0) { + continue; + } + builder.append(normalized); + } + return builder.length() == 0 ? null : builder.toString(); + } + + private String buildOrderedLocationName(JsonNode addressNode) { + if (addressNode == null || addressNode.isMissingNode()) { + return null; + } + + return joinNonBlank( + textValue(addressNode, "state"), + firstNonBlank( + textValue(addressNode, "province"), + textValue(addressNode, "region"), + textValue(addressNode, "locality")), + firstNonBlank( + textValue(addressNode, "city"), + textValue(addressNode, "municipality"), + textValue(addressNode, "town"), + textValue(addressNode, "county")), + firstNonBlank( + textValue(addressNode, "district"), + textValue(addressNode, "city_district"), + textValue(addressNode, "borough")), + firstNonBlank( + textValue(addressNode, "suburb"), + textValue(addressNode, "township"), + textValue(addressNode, "residential"), + textValue(addressNode, "commercial"), + textValue(addressNode, "industrial"), + textValue(addressNode, "retail"), + textValue(addressNode, "quarter"), + textValue(addressNode, "neighbourhood"), + textValue(addressNode, "village"), + textValue(addressNode, "hamlet")), + firstNonBlank( + textValue(addressNode, "city_block"), + textValue(addressNode, "residential"), + textValue(addressNode, "commercial"), + textValue(addressNode, "allotments")), + firstNonBlank( + textValue(addressNode, "road"), + textValue(addressNode, "street"), + textValue(addressNode, "pedestrian")), + joinNonBlank( + textValue(addressNode, "house_number"), + textValue(addressNode, "house_name")), + firstNonBlank( + textValue(addressNode, "building"), + textValue(addressNode, "amenity"), + textValue(addressNode, "office"), + textValue(addressNode, "shop")) + ); + } + + private String normalizeDisplayName(String displayName) { + String normalized = normalizeOptionalText(displayName); + if (normalized == null) { + return null; + } + + String[] segments = normalized.split("\\s*,\\s*"); + if (segments.length == 0) { + return normalized; + } + + StringBuilder builder = new StringBuilder(); + for (int index = segments.length - 1; index >= 0; index--) { + String segment = normalizeOptionalText(segments[index]); + if (segment == null + || "中国".equals(segment) + || segment.matches("\\d{6}") + || builder.indexOf(segment) >= 0) { + continue; + } + builder.append(segment); + } + return builder.length() == 0 ? normalized : builder.toString(); + } + + private void enrichCheckInLocation(WorkCheckInDTO todayCheckIn) { + if (todayCheckIn == null || todayCheckIn.getLatitude() == null || todayCheckIn.getLongitude() == null) { + return; + } + if (!shouldRefreshLocationText(todayCheckIn.getLocationText())) { + return; + } + + try { + todayCheckIn.setLocationText(resolveLocationName(todayCheckIn.getLatitude(), todayCheckIn.getLongitude())); + } catch (Exception ignored) { + // Keep the stored location text when reverse geocoding is unavailable. + } + } + + private boolean shouldRefreshLocationText(String locationText) { + String normalized = normalizeOptionalText(locationText); + if (normalized == null) { + return true; + } + return normalized.contains(",") + || normalized.contains(",") + || normalized.length() < 14; + } + + private void syncTomorrowPlanTodo(Long userId, Long reportId, String tomorrowPlan) { + String todoTitle = buildTomorrowPlanTodoTitle(tomorrowPlan); + Long todoId = workMapper.selectTodoIdByBiz(userId, "report", reportId); + if (todoId == null) { + workMapper.insertTodo(IdWorker.getId(), userId, todoTitle, "report", reportId); + return; + } + workMapper.updateTodo(todoId, todoTitle); + } + + private String buildTomorrowPlanTodoTitle(String tomorrowPlan) { + String normalized = normalizeOptionalText(tomorrowPlan); + if (normalized == null) { + return "明日工作计划"; + } + String singleLine = normalized.replace("\r", "\n").replace("\n", " "); + String title = "明日计划:" + singleLine; + return title.length() > 200 ? title.substring(0, 200) : title; + } + + private String firstNonBlank(String... values) { + for (String value : values) { + String normalized = normalizeOptionalText(value); + if (normalized != null) { + return normalized; + } + } + return null; + } +} diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml new file mode 100644 index 0000000..49479e7 --- /dev/null +++ b/backend/src/main/resources/application.yml @@ -0,0 +1,56 @@ +server: + port: 8080 + +spring: + application: + name: unis-crm-backend + datasource: + url: jdbc:postgresql://127.0.0.1:5432/nex_auth + username: postgres + password: 199628 + driver-class-name: org.postgresql.Driver + data: + redis: + host: 127.0.0.1 + port: 6379 + password: 199628@tlw + database: 14 + +mybatis-plus: + mapper-locations: classpath*:/mapper/**/*.xml + type-aliases-package: com.unis.crm.dto.dashboard + configuration: + map-underscore-to-camel-case: true + log-impl: org.apache.ibatis.logging.stdout.StdOutImpl + +logging: + level: + com.unis.crm: info + +unisbase: + tenant: + enabled: false + web: + auth-endpoints-enabled: true + management-endpoints-enabled: true + security: + enabled: true + mode: embedded + jwt-secret: change-me-please-change-me-32bytes + auth-header: Authorization + token-prefix: "Bearer " + permit-all-urls: + - /actuator/health + internal-auth: + enabled: true + secret: change-me-internal-secret + header-name: X-Internal-Secret + app: + upload-path: /Users/kangwenjing/Downloads/crm/uploads + resource-prefix: /sys/api/static/ + captcha: + ttl-seconds: 120 + max-attempts: 5 + token: + access-default-minutes: 30 + refresh-default-days: 7 diff --git a/backend/src/main/resources/mapper/dashboard/DashboardMapper.xml b/backend/src/main/resources/mapper/dashboard/DashboardMapper.xml new file mode 100644 index 0000000..b5872b9 --- /dev/null +++ b/backend/src/main/resources/mapper/dashboard/DashboardMapper.xml @@ -0,0 +1,206 @@ + + + + + + + + + + + + + + diff --git a/backend/src/main/resources/mapper/expansion/ExpansionMapper.xml b/backend/src/main/resources/mapper/expansion/ExpansionMapper.xml new file mode 100644 index 0000000..c1777d0 --- /dev/null +++ b/backend/src/main/resources/mapper/expansion/ExpansionMapper.xml @@ -0,0 +1,279 @@ + + + + + + + + + + + + + + + + insert into crm_sales_expansion ( + candidate_name, + mobile, + email, + target_dept_id, + industry, + title, + intent_level, + stage, + has_desktop_exp, + in_progress, + employment_status, + expected_join_date, + owner_user_id, + remark + ) values ( + #{request.candidateName}, + #{request.mobile}, + #{request.email}, + #{request.targetDeptId}, + #{request.industry}, + #{request.title}, + #{request.intentLevel}, + #{request.stage}, + #{request.hasDesktopExp}, + #{request.inProgress}, + #{request.employmentStatus}, + #{request.expectedJoinDate}, + #{userId}, + #{request.remark} + ) + + + + insert into crm_channel_expansion ( + channel_name, + province, + industry, + annual_revenue, + staff_size, + contact_name, + contact_title, + contact_mobile, + stage, + landed_flag, + expected_sign_date, + owner_user_id, + remark + ) values ( + #{request.channelName}, + #{request.province}, + #{request.industry}, + #{request.annualRevenue}, + #{request.staffSize}, + #{request.contactName}, + #{request.contactTitle}, + #{request.contactMobile}, + #{request.stage}, + #{request.landedFlag}, + #{request.expectedSignDate}, + #{userId}, + #{request.remark} + ) + + + + update crm_sales_expansion + set candidate_name = #{request.candidateName}, + mobile = #{request.mobile}, + email = #{request.email}, + target_dept_id = #{request.targetDeptId}, + industry = #{request.industry}, + title = #{request.title}, + intent_level = #{request.intentLevel}, + stage = #{request.stage}, + has_desktop_exp = #{request.hasDesktopExp}, + in_progress = #{request.inProgress}, + employment_status = #{request.employmentStatus}, + expected_join_date = #{request.expectedJoinDate}, + remark = #{request.remark} + where id = #{id} + and owner_user_id = #{userId} + + + + update crm_channel_expansion + set channel_name = #{request.channelName}, + province = #{request.province}, + industry = #{request.industry}, + annual_revenue = #{request.annualRevenue}, + staff_size = #{request.staffSize}, + contact_name = #{request.contactName}, + contact_title = #{request.contactTitle}, + contact_mobile = #{request.contactMobile}, + stage = #{request.stage}, + landed_flag = #{request.landedFlag}, + expected_sign_date = #{request.expectedSignDate}, + remark = #{request.remark} + where id = #{id} + and owner_user_id = #{userId} + + + + + + + + insert into crm_expansion_followup ( + biz_type, + biz_id, + followup_time, + followup_type, + content, + next_action, + followup_user_id + ) values ( + #{bizType}, + #{bizId}, + #{request.followUpTime}, + #{request.followUpType}, + #{request.content}, + #{request.nextAction}, + #{userId} + ) + + diff --git a/backend/src/main/resources/mapper/opportunity/OpportunityMapper.xml b/backend/src/main/resources/mapper/opportunity/OpportunityMapper.xml new file mode 100644 index 0000000..2c6f383 --- /dev/null +++ b/backend/src/main/resources/mapper/opportunity/OpportunityMapper.xml @@ -0,0 +1,197 @@ + + + + + + + + + + + + insert into crm_customer ( + id, + customer_code, + customer_name, + owner_user_id, + source, + status, + created_at, + updated_at + ) values ( + #{id}, + 'CUS-' || to_char(current_date, 'YYYYMMDD') || '-' || lpad((coalesce((select count(1) from crm_customer), 0) + 1)::text, 3, '0'), + #{customerName}, + #{userId}, + coalesce(#{source}, '主动开发'), + 'following', + now(), + now() + ) + + + + insert into crm_opportunity ( + opportunity_code, + opportunity_name, + customer_id, + owner_user_id, + amount, + expected_close_date, + confidence_pct, + stage, + opportunity_type, + product_type, + source, + pushed_to_oms, + oms_push_time, + description, + status, + created_at, + updated_at + ) values ( + 'OPP-' || to_char(current_date, 'YYYYMMDD') || '-' || lpad((coalesce((select count(1) from crm_opportunity), 0) + 1)::text, 3, '0'), + #{request.opportunityName}, + #{customerId}, + #{userId}, + #{request.amount}, + #{request.expectedCloseDate}, + #{request.confidencePct}, + #{request.stage}, + #{request.opportunityType}, + #{request.productType}, + #{request.source}, + #{request.pushedToOms}, + case when #{request.pushedToOms} then now() else null end, + #{request.description}, + case + when #{request.stage} = 'won' then 'won' + when #{request.stage} = 'lost' then 'lost' + else 'active' + end, + now(), + now() + ) + + + + + + update crm_opportunity + set opportunity_name = #{request.opportunityName}, + customer_id = #{customerId}, + amount = #{request.amount}, + expected_close_date = #{request.expectedCloseDate}, + confidence_pct = #{request.confidencePct}, + stage = #{request.stage}, + opportunity_type = #{request.opportunityType}, + product_type = #{request.productType}, + source = #{request.source}, + pushed_to_oms = #{request.pushedToOms}, + oms_push_time = case + when #{request.pushedToOms} then coalesce(oms_push_time, now()) + else null + end, + description = #{request.description}, + status = case + when #{request.stage} = 'won' then 'won' + when #{request.stage} = 'lost' then 'lost' + else 'active' + end, + updated_at = now() + where id = #{opportunityId} + and owner_user_id = #{userId} + + + + insert into crm_opportunity_followup ( + opportunity_id, + followup_time, + followup_type, + content, + next_action, + followup_user_id, + created_at, + updated_at + ) values ( + #{opportunityId}, + #{request.followUpTime}, + #{request.followUpType}, + #{request.content}, + #{request.nextAction}, + #{userId}, + now(), + now() + ) + + + diff --git a/backend/src/main/resources/mapper/work/WorkMapper.xml b/backend/src/main/resources/mapper/work/WorkMapper.xml new file mode 100644 index 0000000..3c219e7 --- /dev/null +++ b/backend/src/main/resources/mapper/work/WorkMapper.xml @@ -0,0 +1,374 @@ + + + + + + + + + + + + + + + + insert into work_checkin ( + id, + user_id, + checkin_date, + checkin_time, + longitude, + latitude, + location_text, + remark, + status, + created_at, + updated_at + ) values ( + (select coalesce(max(id), 0) + 1 from work_checkin), + #{userId}, + current_date, + now(), + #{request.longitude}, + #{request.latitude}, + #{request.locationText}, + #{request.remark}, + 'normal', + now(), + now() + ) + + + + update work_checkin + set checkin_time = now(), + longitude = #{request.longitude}, + latitude = #{request.latitude}, + location_text = #{request.locationText}, + remark = #{request.remark}, + status = 'normal', + updated_at = now() + where id = #{checkInId} + + + + + + insert into work_daily_report ( + user_id, + report_date, + work_content, + tomorrow_plan, + source_type, + submit_time, + status, + created_at, + updated_at + ) values ( + #{userId}, + current_date, + #{request.workContent}, + #{request.tomorrowPlan}, + #{request.sourceType}, + now(), + 'submitted', + now(), + now() + ) + + + + update work_daily_report + set work_content = #{request.workContent}, + tomorrow_plan = #{request.tomorrowPlan}, + source_type = #{request.sourceType}, + submit_time = now(), + status = 'submitted', + updated_at = now() + where id = #{reportId} + + + + + + insert into work_todo ( + id, + user_id, + title, + biz_type, + biz_id, + due_date, + status, + priority, + created_at, + updated_at + ) values ( + #{todoId}, + #{userId}, + #{title}, + #{bizType}, + #{bizId}, + current_date::timestamp + interval '1 day' + time '09:00', + 'todo', + 'medium', + now(), + now() + ) + + + + update work_todo + set title = #{title}, + due_date = current_date::timestamp + interval '1 day' + time '09:00', + status = 'todo', + priority = 'medium', + updated_at = now() + where id = #{todoId} + + + diff --git a/backend/target/classes/application.yml b/backend/target/classes/application.yml new file mode 100644 index 0000000..49479e7 --- /dev/null +++ b/backend/target/classes/application.yml @@ -0,0 +1,56 @@ +server: + port: 8080 + +spring: + application: + name: unis-crm-backend + datasource: + url: jdbc:postgresql://127.0.0.1:5432/nex_auth + username: postgres + password: 199628 + driver-class-name: org.postgresql.Driver + data: + redis: + host: 127.0.0.1 + port: 6379 + password: 199628@tlw + database: 14 + +mybatis-plus: + mapper-locations: classpath*:/mapper/**/*.xml + type-aliases-package: com.unis.crm.dto.dashboard + configuration: + map-underscore-to-camel-case: true + log-impl: org.apache.ibatis.logging.stdout.StdOutImpl + +logging: + level: + com.unis.crm: info + +unisbase: + tenant: + enabled: false + web: + auth-endpoints-enabled: true + management-endpoints-enabled: true + security: + enabled: true + mode: embedded + jwt-secret: change-me-please-change-me-32bytes + auth-header: Authorization + token-prefix: "Bearer " + permit-all-urls: + - /actuator/health + internal-auth: + enabled: true + secret: change-me-internal-secret + header-name: X-Internal-Secret + app: + upload-path: /Users/kangwenjing/Downloads/crm/uploads + resource-prefix: /sys/api/static/ + captcha: + ttl-seconds: 120 + max-attempts: 5 + token: + access-default-minutes: 30 + refresh-default-days: 7 diff --git a/backend/target/classes/com/unis/crm/UnisCrmBackendApplication.class b/backend/target/classes/com/unis/crm/UnisCrmBackendApplication.class new file mode 100644 index 0000000..8787ff6 Binary files /dev/null and b/backend/target/classes/com/unis/crm/UnisCrmBackendApplication.class differ diff --git a/backend/target/classes/com/unis/crm/common/ApiResponse.class b/backend/target/classes/com/unis/crm/common/ApiResponse.class new file mode 100644 index 0000000..cf62802 Binary files /dev/null and b/backend/target/classes/com/unis/crm/common/ApiResponse.class differ diff --git a/backend/target/classes/com/unis/crm/common/BusinessException.class b/backend/target/classes/com/unis/crm/common/BusinessException.class new file mode 100644 index 0000000..a833b9e Binary files /dev/null and b/backend/target/classes/com/unis/crm/common/BusinessException.class differ diff --git a/backend/target/classes/com/unis/crm/common/CrmGlobalExceptionHandler.class b/backend/target/classes/com/unis/crm/common/CrmGlobalExceptionHandler.class new file mode 100644 index 0000000..4991bf7 Binary files /dev/null and b/backend/target/classes/com/unis/crm/common/CrmGlobalExceptionHandler.class differ diff --git a/backend/target/classes/com/unis/crm/common/CurrentUserUtils.class b/backend/target/classes/com/unis/crm/common/CurrentUserUtils.class new file mode 100644 index 0000000..25bb9b9 Binary files /dev/null and b/backend/target/classes/com/unis/crm/common/CurrentUserUtils.class differ diff --git a/backend/target/classes/com/unis/crm/controller/DashboardController.class b/backend/target/classes/com/unis/crm/controller/DashboardController.class new file mode 100644 index 0000000..2b7d129 Binary files /dev/null and b/backend/target/classes/com/unis/crm/controller/DashboardController.class differ diff --git a/backend/target/classes/com/unis/crm/controller/ExpansionController.class b/backend/target/classes/com/unis/crm/controller/ExpansionController.class new file mode 100644 index 0000000..9f93e06 Binary files /dev/null and b/backend/target/classes/com/unis/crm/controller/ExpansionController.class differ diff --git a/backend/target/classes/com/unis/crm/controller/OpportunityController.class b/backend/target/classes/com/unis/crm/controller/OpportunityController.class new file mode 100644 index 0000000..6406542 Binary files /dev/null and b/backend/target/classes/com/unis/crm/controller/OpportunityController.class differ diff --git a/backend/target/classes/com/unis/crm/controller/WorkController.class b/backend/target/classes/com/unis/crm/controller/WorkController.class new file mode 100644 index 0000000..90692c0 Binary files /dev/null and b/backend/target/classes/com/unis/crm/controller/WorkController.class differ diff --git a/backend/target/classes/com/unis/crm/dto/dashboard/DashboardActivityDTO.class b/backend/target/classes/com/unis/crm/dto/dashboard/DashboardActivityDTO.class new file mode 100644 index 0000000..8a049a9 Binary files /dev/null and b/backend/target/classes/com/unis/crm/dto/dashboard/DashboardActivityDTO.class differ diff --git a/backend/target/classes/com/unis/crm/dto/dashboard/DashboardHomeDTO.class b/backend/target/classes/com/unis/crm/dto/dashboard/DashboardHomeDTO.class new file mode 100644 index 0000000..9342df1 Binary files /dev/null and b/backend/target/classes/com/unis/crm/dto/dashboard/DashboardHomeDTO.class differ diff --git a/backend/target/classes/com/unis/crm/dto/dashboard/DashboardStatDTO.class b/backend/target/classes/com/unis/crm/dto/dashboard/DashboardStatDTO.class new file mode 100644 index 0000000..844080f Binary files /dev/null and b/backend/target/classes/com/unis/crm/dto/dashboard/DashboardStatDTO.class differ diff --git a/backend/target/classes/com/unis/crm/dto/dashboard/DashboardTodoDTO.class b/backend/target/classes/com/unis/crm/dto/dashboard/DashboardTodoDTO.class new file mode 100644 index 0000000..c67d312 Binary files /dev/null and b/backend/target/classes/com/unis/crm/dto/dashboard/DashboardTodoDTO.class differ diff --git a/backend/target/classes/com/unis/crm/dto/dashboard/UserWelcomeDTO.class b/backend/target/classes/com/unis/crm/dto/dashboard/UserWelcomeDTO.class new file mode 100644 index 0000000..95468cf Binary files /dev/null and b/backend/target/classes/com/unis/crm/dto/dashboard/UserWelcomeDTO.class differ diff --git a/backend/target/classes/com/unis/crm/dto/expansion/ChannelExpansionItemDTO.class b/backend/target/classes/com/unis/crm/dto/expansion/ChannelExpansionItemDTO.class new file mode 100644 index 0000000..68f8104 Binary files /dev/null and b/backend/target/classes/com/unis/crm/dto/expansion/ChannelExpansionItemDTO.class differ diff --git a/backend/target/classes/com/unis/crm/dto/expansion/CreateChannelExpansionRequest.class b/backend/target/classes/com/unis/crm/dto/expansion/CreateChannelExpansionRequest.class new file mode 100644 index 0000000..9afcf46 Binary files /dev/null and b/backend/target/classes/com/unis/crm/dto/expansion/CreateChannelExpansionRequest.class differ diff --git a/backend/target/classes/com/unis/crm/dto/expansion/CreateExpansionFollowUpRequest.class b/backend/target/classes/com/unis/crm/dto/expansion/CreateExpansionFollowUpRequest.class new file mode 100644 index 0000000..4ee0075 Binary files /dev/null and b/backend/target/classes/com/unis/crm/dto/expansion/CreateExpansionFollowUpRequest.class differ diff --git a/backend/target/classes/com/unis/crm/dto/expansion/CreateSalesExpansionRequest.class b/backend/target/classes/com/unis/crm/dto/expansion/CreateSalesExpansionRequest.class new file mode 100644 index 0000000..1adfd7c Binary files /dev/null and b/backend/target/classes/com/unis/crm/dto/expansion/CreateSalesExpansionRequest.class differ diff --git a/backend/target/classes/com/unis/crm/dto/expansion/DepartmentOptionDTO.class b/backend/target/classes/com/unis/crm/dto/expansion/DepartmentOptionDTO.class new file mode 100644 index 0000000..7926c2b Binary files /dev/null and b/backend/target/classes/com/unis/crm/dto/expansion/DepartmentOptionDTO.class differ diff --git a/backend/target/classes/com/unis/crm/dto/expansion/ExpansionFollowUpDTO.class b/backend/target/classes/com/unis/crm/dto/expansion/ExpansionFollowUpDTO.class new file mode 100644 index 0000000..feb5d38 Binary files /dev/null and b/backend/target/classes/com/unis/crm/dto/expansion/ExpansionFollowUpDTO.class differ diff --git a/backend/target/classes/com/unis/crm/dto/expansion/ExpansionMetaDTO.class b/backend/target/classes/com/unis/crm/dto/expansion/ExpansionMetaDTO.class new file mode 100644 index 0000000..010110c Binary files /dev/null and b/backend/target/classes/com/unis/crm/dto/expansion/ExpansionMetaDTO.class differ diff --git a/backend/target/classes/com/unis/crm/dto/expansion/ExpansionOverviewDTO.class b/backend/target/classes/com/unis/crm/dto/expansion/ExpansionOverviewDTO.class new file mode 100644 index 0000000..2c09abf Binary files /dev/null and b/backend/target/classes/com/unis/crm/dto/expansion/ExpansionOverviewDTO.class differ diff --git a/backend/target/classes/com/unis/crm/dto/expansion/SalesExpansionItemDTO.class b/backend/target/classes/com/unis/crm/dto/expansion/SalesExpansionItemDTO.class new file mode 100644 index 0000000..37a4fd0 Binary files /dev/null and b/backend/target/classes/com/unis/crm/dto/expansion/SalesExpansionItemDTO.class differ diff --git a/backend/target/classes/com/unis/crm/dto/expansion/UpdateChannelExpansionRequest.class b/backend/target/classes/com/unis/crm/dto/expansion/UpdateChannelExpansionRequest.class new file mode 100644 index 0000000..e45f3a1 Binary files /dev/null and b/backend/target/classes/com/unis/crm/dto/expansion/UpdateChannelExpansionRequest.class differ diff --git a/backend/target/classes/com/unis/crm/dto/expansion/UpdateSalesExpansionRequest.class b/backend/target/classes/com/unis/crm/dto/expansion/UpdateSalesExpansionRequest.class new file mode 100644 index 0000000..f73883e Binary files /dev/null and b/backend/target/classes/com/unis/crm/dto/expansion/UpdateSalesExpansionRequest.class differ diff --git a/backend/target/classes/com/unis/crm/dto/opportunity/CreateOpportunityFollowUpRequest.class b/backend/target/classes/com/unis/crm/dto/opportunity/CreateOpportunityFollowUpRequest.class new file mode 100644 index 0000000..46ed9b5 Binary files /dev/null and b/backend/target/classes/com/unis/crm/dto/opportunity/CreateOpportunityFollowUpRequest.class differ diff --git a/backend/target/classes/com/unis/crm/dto/opportunity/CreateOpportunityRequest.class b/backend/target/classes/com/unis/crm/dto/opportunity/CreateOpportunityRequest.class new file mode 100644 index 0000000..11d6019 Binary files /dev/null and b/backend/target/classes/com/unis/crm/dto/opportunity/CreateOpportunityRequest.class differ diff --git a/backend/target/classes/com/unis/crm/dto/opportunity/OpportunityFollowUpDTO.class b/backend/target/classes/com/unis/crm/dto/opportunity/OpportunityFollowUpDTO.class new file mode 100644 index 0000000..55a77d4 Binary files /dev/null and b/backend/target/classes/com/unis/crm/dto/opportunity/OpportunityFollowUpDTO.class differ diff --git a/backend/target/classes/com/unis/crm/dto/opportunity/OpportunityItemDTO.class b/backend/target/classes/com/unis/crm/dto/opportunity/OpportunityItemDTO.class new file mode 100644 index 0000000..c424175 Binary files /dev/null and b/backend/target/classes/com/unis/crm/dto/opportunity/OpportunityItemDTO.class differ diff --git a/backend/target/classes/com/unis/crm/dto/opportunity/OpportunityOverviewDTO.class b/backend/target/classes/com/unis/crm/dto/opportunity/OpportunityOverviewDTO.class new file mode 100644 index 0000000..a8998d4 Binary files /dev/null and b/backend/target/classes/com/unis/crm/dto/opportunity/OpportunityOverviewDTO.class differ diff --git a/backend/target/classes/com/unis/crm/dto/work/CreateWorkCheckInRequest.class b/backend/target/classes/com/unis/crm/dto/work/CreateWorkCheckInRequest.class new file mode 100644 index 0000000..5b1d7e4 Binary files /dev/null and b/backend/target/classes/com/unis/crm/dto/work/CreateWorkCheckInRequest.class differ diff --git a/backend/target/classes/com/unis/crm/dto/work/CreateWorkDailyReportRequest.class b/backend/target/classes/com/unis/crm/dto/work/CreateWorkDailyReportRequest.class new file mode 100644 index 0000000..16d7cf6 Binary files /dev/null and b/backend/target/classes/com/unis/crm/dto/work/CreateWorkDailyReportRequest.class differ diff --git a/backend/target/classes/com/unis/crm/dto/work/WorkCheckInDTO.class b/backend/target/classes/com/unis/crm/dto/work/WorkCheckInDTO.class new file mode 100644 index 0000000..2869898 Binary files /dev/null and b/backend/target/classes/com/unis/crm/dto/work/WorkCheckInDTO.class differ diff --git a/backend/target/classes/com/unis/crm/dto/work/WorkDailyReportDTO.class b/backend/target/classes/com/unis/crm/dto/work/WorkDailyReportDTO.class new file mode 100644 index 0000000..e590131 Binary files /dev/null and b/backend/target/classes/com/unis/crm/dto/work/WorkDailyReportDTO.class differ diff --git a/backend/target/classes/com/unis/crm/dto/work/WorkHistoryItemDTO.class b/backend/target/classes/com/unis/crm/dto/work/WorkHistoryItemDTO.class new file mode 100644 index 0000000..665efaa Binary files /dev/null and b/backend/target/classes/com/unis/crm/dto/work/WorkHistoryItemDTO.class differ diff --git a/backend/target/classes/com/unis/crm/dto/work/WorkOverviewDTO.class b/backend/target/classes/com/unis/crm/dto/work/WorkOverviewDTO.class new file mode 100644 index 0000000..9792256 Binary files /dev/null and b/backend/target/classes/com/unis/crm/dto/work/WorkOverviewDTO.class differ diff --git a/backend/target/classes/com/unis/crm/mapper/DashboardMapper.class b/backend/target/classes/com/unis/crm/mapper/DashboardMapper.class new file mode 100644 index 0000000..de620f9 Binary files /dev/null and b/backend/target/classes/com/unis/crm/mapper/DashboardMapper.class differ diff --git a/backend/target/classes/com/unis/crm/mapper/ExpansionMapper.class b/backend/target/classes/com/unis/crm/mapper/ExpansionMapper.class new file mode 100644 index 0000000..1c544bd Binary files /dev/null and b/backend/target/classes/com/unis/crm/mapper/ExpansionMapper.class differ diff --git a/backend/target/classes/com/unis/crm/mapper/OpportunityMapper.class b/backend/target/classes/com/unis/crm/mapper/OpportunityMapper.class new file mode 100644 index 0000000..7ba3df4 Binary files /dev/null and b/backend/target/classes/com/unis/crm/mapper/OpportunityMapper.class differ diff --git a/backend/target/classes/com/unis/crm/mapper/WorkMapper.class b/backend/target/classes/com/unis/crm/mapper/WorkMapper.class new file mode 100644 index 0000000..a1246ff Binary files /dev/null and b/backend/target/classes/com/unis/crm/mapper/WorkMapper.class differ diff --git a/backend/target/classes/com/unis/crm/service/DashboardService.class b/backend/target/classes/com/unis/crm/service/DashboardService.class new file mode 100644 index 0000000..affffe0 Binary files /dev/null and b/backend/target/classes/com/unis/crm/service/DashboardService.class differ diff --git a/backend/target/classes/com/unis/crm/service/ExpansionService.class b/backend/target/classes/com/unis/crm/service/ExpansionService.class new file mode 100644 index 0000000..028b367 Binary files /dev/null and b/backend/target/classes/com/unis/crm/service/ExpansionService.class differ diff --git a/backend/target/classes/com/unis/crm/service/OpportunityService.class b/backend/target/classes/com/unis/crm/service/OpportunityService.class new file mode 100644 index 0000000..a38aab7 Binary files /dev/null and b/backend/target/classes/com/unis/crm/service/OpportunityService.class differ diff --git a/backend/target/classes/com/unis/crm/service/WorkService.class b/backend/target/classes/com/unis/crm/service/WorkService.class new file mode 100644 index 0000000..865ba9a Binary files /dev/null and b/backend/target/classes/com/unis/crm/service/WorkService.class differ diff --git a/backend/target/classes/com/unis/crm/service/impl/DashboardServiceImpl.class b/backend/target/classes/com/unis/crm/service/impl/DashboardServiceImpl.class new file mode 100644 index 0000000..abd6fbf Binary files /dev/null and b/backend/target/classes/com/unis/crm/service/impl/DashboardServiceImpl.class differ diff --git a/backend/target/classes/com/unis/crm/service/impl/ExpansionServiceImpl.class b/backend/target/classes/com/unis/crm/service/impl/ExpansionServiceImpl.class new file mode 100644 index 0000000..8c66802 Binary files /dev/null and b/backend/target/classes/com/unis/crm/service/impl/ExpansionServiceImpl.class differ diff --git a/backend/target/classes/com/unis/crm/service/impl/OpportunityServiceImpl.class b/backend/target/classes/com/unis/crm/service/impl/OpportunityServiceImpl.class new file mode 100644 index 0000000..990e2fb Binary files /dev/null and b/backend/target/classes/com/unis/crm/service/impl/OpportunityServiceImpl.class differ diff --git a/backend/target/classes/com/unis/crm/service/impl/WorkServiceImpl.class b/backend/target/classes/com/unis/crm/service/impl/WorkServiceImpl.class new file mode 100644 index 0000000..f24c6c3 Binary files /dev/null and b/backend/target/classes/com/unis/crm/service/impl/WorkServiceImpl.class differ diff --git a/backend/target/classes/mapper/dashboard/DashboardMapper.xml b/backend/target/classes/mapper/dashboard/DashboardMapper.xml new file mode 100644 index 0000000..b5872b9 --- /dev/null +++ b/backend/target/classes/mapper/dashboard/DashboardMapper.xml @@ -0,0 +1,206 @@ + + + + + + + + + + + + + + diff --git a/backend/target/classes/mapper/expansion/ExpansionMapper.xml b/backend/target/classes/mapper/expansion/ExpansionMapper.xml new file mode 100644 index 0000000..c1777d0 --- /dev/null +++ b/backend/target/classes/mapper/expansion/ExpansionMapper.xml @@ -0,0 +1,279 @@ + + + + + + + + + + + + + + + + insert into crm_sales_expansion ( + candidate_name, + mobile, + email, + target_dept_id, + industry, + title, + intent_level, + stage, + has_desktop_exp, + in_progress, + employment_status, + expected_join_date, + owner_user_id, + remark + ) values ( + #{request.candidateName}, + #{request.mobile}, + #{request.email}, + #{request.targetDeptId}, + #{request.industry}, + #{request.title}, + #{request.intentLevel}, + #{request.stage}, + #{request.hasDesktopExp}, + #{request.inProgress}, + #{request.employmentStatus}, + #{request.expectedJoinDate}, + #{userId}, + #{request.remark} + ) + + + + insert into crm_channel_expansion ( + channel_name, + province, + industry, + annual_revenue, + staff_size, + contact_name, + contact_title, + contact_mobile, + stage, + landed_flag, + expected_sign_date, + owner_user_id, + remark + ) values ( + #{request.channelName}, + #{request.province}, + #{request.industry}, + #{request.annualRevenue}, + #{request.staffSize}, + #{request.contactName}, + #{request.contactTitle}, + #{request.contactMobile}, + #{request.stage}, + #{request.landedFlag}, + #{request.expectedSignDate}, + #{userId}, + #{request.remark} + ) + + + + update crm_sales_expansion + set candidate_name = #{request.candidateName}, + mobile = #{request.mobile}, + email = #{request.email}, + target_dept_id = #{request.targetDeptId}, + industry = #{request.industry}, + title = #{request.title}, + intent_level = #{request.intentLevel}, + stage = #{request.stage}, + has_desktop_exp = #{request.hasDesktopExp}, + in_progress = #{request.inProgress}, + employment_status = #{request.employmentStatus}, + expected_join_date = #{request.expectedJoinDate}, + remark = #{request.remark} + where id = #{id} + and owner_user_id = #{userId} + + + + update crm_channel_expansion + set channel_name = #{request.channelName}, + province = #{request.province}, + industry = #{request.industry}, + annual_revenue = #{request.annualRevenue}, + staff_size = #{request.staffSize}, + contact_name = #{request.contactName}, + contact_title = #{request.contactTitle}, + contact_mobile = #{request.contactMobile}, + stage = #{request.stage}, + landed_flag = #{request.landedFlag}, + expected_sign_date = #{request.expectedSignDate}, + remark = #{request.remark} + where id = #{id} + and owner_user_id = #{userId} + + + + + + + + insert into crm_expansion_followup ( + biz_type, + biz_id, + followup_time, + followup_type, + content, + next_action, + followup_user_id + ) values ( + #{bizType}, + #{bizId}, + #{request.followUpTime}, + #{request.followUpType}, + #{request.content}, + #{request.nextAction}, + #{userId} + ) + + diff --git a/backend/target/classes/mapper/opportunity/OpportunityMapper.xml b/backend/target/classes/mapper/opportunity/OpportunityMapper.xml new file mode 100644 index 0000000..2c6f383 --- /dev/null +++ b/backend/target/classes/mapper/opportunity/OpportunityMapper.xml @@ -0,0 +1,197 @@ + + + + + + + + + + + + insert into crm_customer ( + id, + customer_code, + customer_name, + owner_user_id, + source, + status, + created_at, + updated_at + ) values ( + #{id}, + 'CUS-' || to_char(current_date, 'YYYYMMDD') || '-' || lpad((coalesce((select count(1) from crm_customer), 0) + 1)::text, 3, '0'), + #{customerName}, + #{userId}, + coalesce(#{source}, '主动开发'), + 'following', + now(), + now() + ) + + + + insert into crm_opportunity ( + opportunity_code, + opportunity_name, + customer_id, + owner_user_id, + amount, + expected_close_date, + confidence_pct, + stage, + opportunity_type, + product_type, + source, + pushed_to_oms, + oms_push_time, + description, + status, + created_at, + updated_at + ) values ( + 'OPP-' || to_char(current_date, 'YYYYMMDD') || '-' || lpad((coalesce((select count(1) from crm_opportunity), 0) + 1)::text, 3, '0'), + #{request.opportunityName}, + #{customerId}, + #{userId}, + #{request.amount}, + #{request.expectedCloseDate}, + #{request.confidencePct}, + #{request.stage}, + #{request.opportunityType}, + #{request.productType}, + #{request.source}, + #{request.pushedToOms}, + case when #{request.pushedToOms} then now() else null end, + #{request.description}, + case + when #{request.stage} = 'won' then 'won' + when #{request.stage} = 'lost' then 'lost' + else 'active' + end, + now(), + now() + ) + + + + + + update crm_opportunity + set opportunity_name = #{request.opportunityName}, + customer_id = #{customerId}, + amount = #{request.amount}, + expected_close_date = #{request.expectedCloseDate}, + confidence_pct = #{request.confidencePct}, + stage = #{request.stage}, + opportunity_type = #{request.opportunityType}, + product_type = #{request.productType}, + source = #{request.source}, + pushed_to_oms = #{request.pushedToOms}, + oms_push_time = case + when #{request.pushedToOms} then coalesce(oms_push_time, now()) + else null + end, + description = #{request.description}, + status = case + when #{request.stage} = 'won' then 'won' + when #{request.stage} = 'lost' then 'lost' + else 'active' + end, + updated_at = now() + where id = #{opportunityId} + and owner_user_id = #{userId} + + + + insert into crm_opportunity_followup ( + opportunity_id, + followup_time, + followup_type, + content, + next_action, + followup_user_id, + created_at, + updated_at + ) values ( + #{opportunityId}, + #{request.followUpTime}, + #{request.followUpType}, + #{request.content}, + #{request.nextAction}, + #{userId}, + now(), + now() + ) + + + diff --git a/backend/target/classes/mapper/work/WorkMapper.xml b/backend/target/classes/mapper/work/WorkMapper.xml new file mode 100644 index 0000000..3c219e7 --- /dev/null +++ b/backend/target/classes/mapper/work/WorkMapper.xml @@ -0,0 +1,374 @@ + + + + + + + + + + + + + + + + insert into work_checkin ( + id, + user_id, + checkin_date, + checkin_time, + longitude, + latitude, + location_text, + remark, + status, + created_at, + updated_at + ) values ( + (select coalesce(max(id), 0) + 1 from work_checkin), + #{userId}, + current_date, + now(), + #{request.longitude}, + #{request.latitude}, + #{request.locationText}, + #{request.remark}, + 'normal', + now(), + now() + ) + + + + update work_checkin + set checkin_time = now(), + longitude = #{request.longitude}, + latitude = #{request.latitude}, + location_text = #{request.locationText}, + remark = #{request.remark}, + status = 'normal', + updated_at = now() + where id = #{checkInId} + + + + + + insert into work_daily_report ( + user_id, + report_date, + work_content, + tomorrow_plan, + source_type, + submit_time, + status, + created_at, + updated_at + ) values ( + #{userId}, + current_date, + #{request.workContent}, + #{request.tomorrowPlan}, + #{request.sourceType}, + now(), + 'submitted', + now(), + now() + ) + + + + update work_daily_report + set work_content = #{request.workContent}, + tomorrow_plan = #{request.tomorrowPlan}, + source_type = #{request.sourceType}, + submit_time = now(), + status = 'submitted', + updated_at = now() + where id = #{reportId} + + + + + + insert into work_todo ( + id, + user_id, + title, + biz_type, + biz_id, + due_date, + status, + priority, + created_at, + updated_at + ) values ( + #{todoId}, + #{userId}, + #{title}, + #{bizType}, + #{bizId}, + current_date::timestamp + interval '1 day' + time '09:00', + 'todo', + 'medium', + now(), + now() + ) + + + + update work_todo + set title = #{title}, + due_date = current_date::timestamp + interval '1 day' + time '09:00', + status = 'todo', + priority = 'medium', + updated_at = now() + where id = #{todoId} + + + diff --git a/backend/target/maven-archiver/pom.properties b/backend/target/maven-archiver/pom.properties new file mode 100644 index 0000000..5a7f665 --- /dev/null +++ b/backend/target/maven-archiver/pom.properties @@ -0,0 +1,3 @@ +artifactId=unis-crm-backend +groupId=com.unis.crm +version=1.0.0-SNAPSHOT diff --git a/backend/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst b/backend/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst new file mode 100644 index 0000000..ad3152b --- /dev/null +++ b/backend/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst @@ -0,0 +1,29 @@ +com/unis/crm/service/impl/ExpansionServiceImpl.class +com/unis/crm/dto/dashboard/UserWelcomeDTO.class +com/unis/crm/dto/expansion/ExpansionOverviewDTO.class +com/unis/crm/service/impl/DashboardServiceImpl.class +com/unis/crm/controller/ExpansionController.class +com/unis/crm/dto/expansion/ExpansionFollowUpDTO.class +com/unis/crm/dto/expansion/CreateSalesExpansionRequest.class +com/unis/crm/common/ApiResponse.class +com/unis/crm/UnisCrmBackendApplication.class +com/unis/crm/common/CurrentUserUtils.class +com/unis/crm/service/DashboardService.class +com/unis/crm/dto/expansion/UpdateChannelExpansionRequest.class +com/unis/crm/common/CrmGlobalExceptionHandler.class +com/unis/crm/dto/dashboard/DashboardStatDTO.class +com/unis/crm/dto/expansion/CreateChannelExpansionRequest.class +com/unis/crm/dto/expansion/DepartmentOptionDTO.class +com/unis/crm/dto/expansion/SalesExpansionItemDTO.class +com/unis/crm/dto/dashboard/DashboardActivityDTO.class +com/unis/crm/service/ExpansionService.class +com/unis/crm/dto/expansion/UpdateSalesExpansionRequest.class +com/unis/crm/dto/dashboard/DashboardTodoDTO.class +com/unis/crm/controller/DashboardController.class +com/unis/crm/dto/expansion/ExpansionMetaDTO.class +com/unis/crm/mapper/ExpansionMapper.class +com/unis/crm/dto/dashboard/DashboardHomeDTO.class +com/unis/crm/common/BusinessException.class +com/unis/crm/mapper/DashboardMapper.class +com/unis/crm/dto/expansion/CreateExpansionFollowUpRequest.class +com/unis/crm/dto/expansion/ChannelExpansionItemDTO.class diff --git a/backend/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst b/backend/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst new file mode 100644 index 0000000..2e2027f --- /dev/null +++ b/backend/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst @@ -0,0 +1,48 @@ +/Users/kangwenjing/Downloads/crm/unis_crm/backend/src/main/java/com/unis/crm/mapper/ExpansionMapper.java +/Users/kangwenjing/Downloads/crm/unis_crm/backend/src/main/java/com/unis/crm/dto/work/WorkDailyReportDTO.java +/Users/kangwenjing/Downloads/crm/unis_crm/backend/src/main/java/com/unis/crm/dto/dashboard/DashboardHomeDTO.java +/Users/kangwenjing/Downloads/crm/unis_crm/backend/src/main/java/com/unis/crm/dto/opportunity/OpportunityFollowUpDTO.java +/Users/kangwenjing/Downloads/crm/unis_crm/backend/src/main/java/com/unis/crm/dto/opportunity/CreateOpportunityFollowUpRequest.java +/Users/kangwenjing/Downloads/crm/unis_crm/backend/src/main/java/com/unis/crm/dto/dashboard/DashboardActivityDTO.java +/Users/kangwenjing/Downloads/crm/unis_crm/backend/src/main/java/com/unis/crm/dto/expansion/ExpansionOverviewDTO.java +/Users/kangwenjing/Downloads/crm/unis_crm/backend/src/main/java/com/unis/crm/dto/work/CreateWorkCheckInRequest.java +/Users/kangwenjing/Downloads/crm/unis_crm/backend/src/main/java/com/unis/crm/mapper/DashboardMapper.java +/Users/kangwenjing/Downloads/crm/unis_crm/backend/src/main/java/com/unis/crm/common/CurrentUserUtils.java +/Users/kangwenjing/Downloads/crm/unis_crm/backend/src/main/java/com/unis/crm/dto/dashboard/DashboardStatDTO.java +/Users/kangwenjing/Downloads/crm/unis_crm/backend/src/main/java/com/unis/crm/controller/DashboardController.java +/Users/kangwenjing/Downloads/crm/unis_crm/backend/src/main/java/com/unis/crm/service/DashboardService.java +/Users/kangwenjing/Downloads/crm/unis_crm/backend/src/main/java/com/unis/crm/service/OpportunityService.java +/Users/kangwenjing/Downloads/crm/unis_crm/backend/src/main/java/com/unis/crm/common/ApiResponse.java +/Users/kangwenjing/Downloads/crm/unis_crm/backend/src/main/java/com/unis/crm/service/impl/WorkServiceImpl.java +/Users/kangwenjing/Downloads/crm/unis_crm/backend/src/main/java/com/unis/crm/UnisCrmBackendApplication.java +/Users/kangwenjing/Downloads/crm/unis_crm/backend/src/main/java/com/unis/crm/dto/expansion/DepartmentOptionDTO.java +/Users/kangwenjing/Downloads/crm/unis_crm/backend/src/main/java/com/unis/crm/controller/WorkController.java +/Users/kangwenjing/Downloads/crm/unis_crm/backend/src/main/java/com/unis/crm/dto/expansion/CreateSalesExpansionRequest.java +/Users/kangwenjing/Downloads/crm/unis_crm/backend/src/main/java/com/unis/crm/dto/expansion/ExpansionMetaDTO.java +/Users/kangwenjing/Downloads/crm/unis_crm/backend/src/main/java/com/unis/crm/dto/dashboard/DashboardTodoDTO.java +/Users/kangwenjing/Downloads/crm/unis_crm/backend/src/main/java/com/unis/crm/dto/work/WorkHistoryItemDTO.java +/Users/kangwenjing/Downloads/crm/unis_crm/backend/src/main/java/com/unis/crm/controller/ExpansionController.java +/Users/kangwenjing/Downloads/crm/unis_crm/backend/src/main/java/com/unis/crm/common/BusinessException.java +/Users/kangwenjing/Downloads/crm/unis_crm/backend/src/main/java/com/unis/crm/service/impl/DashboardServiceImpl.java +/Users/kangwenjing/Downloads/crm/unis_crm/backend/src/main/java/com/unis/crm/service/ExpansionService.java +/Users/kangwenjing/Downloads/crm/unis_crm/backend/src/main/java/com/unis/crm/dto/work/CreateWorkDailyReportRequest.java +/Users/kangwenjing/Downloads/crm/unis_crm/backend/src/main/java/com/unis/crm/service/impl/ExpansionServiceImpl.java +/Users/kangwenjing/Downloads/crm/unis_crm/backend/src/main/java/com/unis/crm/mapper/OpportunityMapper.java +/Users/kangwenjing/Downloads/crm/unis_crm/backend/src/main/java/com/unis/crm/dto/expansion/UpdateChannelExpansionRequest.java +/Users/kangwenjing/Downloads/crm/unis_crm/backend/src/main/java/com/unis/crm/dto/expansion/ExpansionFollowUpDTO.java +/Users/kangwenjing/Downloads/crm/unis_crm/backend/src/main/java/com/unis/crm/service/WorkService.java +/Users/kangwenjing/Downloads/crm/unis_crm/backend/src/main/java/com/unis/crm/dto/opportunity/OpportunityOverviewDTO.java +/Users/kangwenjing/Downloads/crm/unis_crm/backend/src/main/java/com/unis/crm/dto/work/WorkCheckInDTO.java +/Users/kangwenjing/Downloads/crm/unis_crm/backend/src/main/java/com/unis/crm/mapper/WorkMapper.java +/Users/kangwenjing/Downloads/crm/unis_crm/backend/src/main/java/com/unis/crm/dto/expansion/UpdateSalesExpansionRequest.java +/Users/kangwenjing/Downloads/crm/unis_crm/backend/src/main/java/com/unis/crm/dto/expansion/ChannelExpansionItemDTO.java +/Users/kangwenjing/Downloads/crm/unis_crm/backend/src/main/java/com/unis/crm/controller/OpportunityController.java +/Users/kangwenjing/Downloads/crm/unis_crm/backend/src/main/java/com/unis/crm/dto/opportunity/CreateOpportunityRequest.java +/Users/kangwenjing/Downloads/crm/unis_crm/backend/src/main/java/com/unis/crm/dto/expansion/CreateChannelExpansionRequest.java +/Users/kangwenjing/Downloads/crm/unis_crm/backend/src/main/java/com/unis/crm/dto/work/WorkOverviewDTO.java +/Users/kangwenjing/Downloads/crm/unis_crm/backend/src/main/java/com/unis/crm/dto/expansion/SalesExpansionItemDTO.java +/Users/kangwenjing/Downloads/crm/unis_crm/backend/src/main/java/com/unis/crm/service/impl/OpportunityServiceImpl.java +/Users/kangwenjing/Downloads/crm/unis_crm/backend/src/main/java/com/unis/crm/dto/expansion/CreateExpansionFollowUpRequest.java +/Users/kangwenjing/Downloads/crm/unis_crm/backend/src/main/java/com/unis/crm/dto/dashboard/UserWelcomeDTO.java +/Users/kangwenjing/Downloads/crm/unis_crm/backend/src/main/java/com/unis/crm/dto/opportunity/OpportunityItemDTO.java +/Users/kangwenjing/Downloads/crm/unis_crm/backend/src/main/java/com/unis/crm/common/CrmGlobalExceptionHandler.java diff --git a/backend/target/unis-crm-backend-1.0.0-SNAPSHOT.jar b/backend/target/unis-crm-backend-1.0.0-SNAPSHOT.jar new file mode 100644 index 0000000..d89acc0 Binary files /dev/null and b/backend/target/unis-crm-backend-1.0.0-SNAPSHOT.jar differ diff --git a/backend/target/unis-crm-backend-1.0.0-SNAPSHOT.jar.original b/backend/target/unis-crm-backend-1.0.0-SNAPSHOT.jar.original new file mode 100644 index 0000000..6388141 Binary files /dev/null and b/backend/target/unis-crm-backend-1.0.0-SNAPSHOT.jar.original differ diff --git a/frontend/.DS_Store b/frontend/.DS_Store new file mode 100644 index 0000000..43544f0 Binary files /dev/null and b/frontend/.DS_Store differ diff --git a/frontend/dist/assets/index-BCZw0F7c.js b/frontend/dist/assets/index-BCZw0F7c.js new file mode 100644 index 0000000..2775243 --- /dev/null +++ b/frontend/dist/assets/index-BCZw0F7c.js @@ -0,0 +1,279 @@ +(function(){const n=document.createElement("link").relList;if(n&&n.supports&&n.supports("modulepreload"))return;for(const o of document.querySelectorAll('link[rel="modulepreload"]'))r(o);new MutationObserver(o=>{for(const f of o)if(f.type==="childList")for(const h of f.addedNodes)h.tagName==="LINK"&&h.rel==="modulepreload"&&r(h)}).observe(document,{childList:!0,subtree:!0});function l(o){const f={};return o.integrity&&(f.integrity=o.integrity),o.referrerPolicy&&(f.referrerPolicy=o.referrerPolicy),o.crossOrigin==="use-credentials"?f.credentials="include":o.crossOrigin==="anonymous"?f.credentials="omit":f.credentials="same-origin",f}function r(o){if(o.ep)return;o.ep=!0;const f=l(o);fetch(o.href,f)}})();var Zc={exports:{}},$l={};/** + * @license React + * react-jsx-runtime.production.js + * + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */var Xp;function W2(){if(Xp)return $l;Xp=1;var a=Symbol.for("react.transitional.element"),n=Symbol.for("react.fragment");function l(r,o,f){var h=null;if(f!==void 0&&(h=""+f),o.key!==void 0&&(h=""+o.key),"key"in o){f={};for(var m in o)m!=="key"&&(f[m]=o[m])}else f=o;return o=f.ref,{$$typeof:a,type:r,key:h,ref:o!==void 0?o:null,props:f}}return $l.Fragment=n,$l.jsx=l,$l.jsxs=l,$l}var Pp;function I2(){return Pp||(Pp=1,Zc.exports=W2()),Zc.exports}var d=I2(),Jc={exports:{}},be={};/** + * @license React + * react.production.js + * + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */var Qp;function eS(){if(Qp)return be;Qp=1;var a=Symbol.for("react.transitional.element"),n=Symbol.for("react.portal"),l=Symbol.for("react.fragment"),r=Symbol.for("react.strict_mode"),o=Symbol.for("react.profiler"),f=Symbol.for("react.consumer"),h=Symbol.for("react.context"),m=Symbol.for("react.forward_ref"),p=Symbol.for("react.suspense"),g=Symbol.for("react.memo"),y=Symbol.for("react.lazy"),v=Symbol.for("react.activity"),S=Symbol.iterator;function k(T){return T===null||typeof T!="object"?null:(T=S&&T[S]||T["@@iterator"],typeof T=="function"?T:null)}var N={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},E=Object.assign,M={};function A(T,G,O){this.props=T,this.context=G,this.refs=M,this.updater=O||N}A.prototype.isReactComponent={},A.prototype.setState=function(T,G){if(typeof T!="object"&&typeof T!="function"&&T!=null)throw Error("takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,T,G,"setState")},A.prototype.forceUpdate=function(T){this.updater.enqueueForceUpdate(this,T,"forceUpdate")};function z(){}z.prototype=A.prototype;function U(T,G,O){this.props=T,this.context=G,this.refs=M,this.updater=O||N}var Y=U.prototype=new z;Y.constructor=U,E(Y,A.prototype),Y.isPureReactComponent=!0;var Q=Array.isArray;function ae(){}var K={H:null,A:null,T:null,S:null},B=Object.prototype.hasOwnProperty;function I(T,G,O){var ne=O.ref;return{$$typeof:a,type:T,key:G,ref:ne!==void 0?ne:null,props:O}}function se(T,G){return I(T.type,G,T.props)}function de(T){return typeof T=="object"&&T!==null&&T.$$typeof===a}function pe(T){var G={"=":"=0",":":"=2"};return"$"+T.replace(/[=:]/g,function(O){return G[O]})}var Ae=/\/+/g;function ve(T,G){return typeof T=="object"&&T!==null&&T.key!=null?pe(""+T.key):G.toString(36)}function $(T){switch(T.status){case"fulfilled":return T.value;case"rejected":throw T.reason;default:switch(typeof T.status=="string"?T.then(ae,ae):(T.status="pending",T.then(function(G){T.status==="pending"&&(T.status="fulfilled",T.value=G)},function(G){T.status==="pending"&&(T.status="rejected",T.reason=G)})),T.status){case"fulfilled":return T.value;case"rejected":throw T.reason}}throw T}function R(T,G,O,ne,le){var Z=typeof T;(Z==="undefined"||Z==="boolean")&&(T=null);var ce=!1;if(T===null)ce=!0;else switch(Z){case"bigint":case"string":case"number":ce=!0;break;case"object":switch(T.$$typeof){case a:case n:ce=!0;break;case y:return ce=T._init,R(ce(T._payload),G,O,ne,le)}}if(ce)return le=le(T),ce=ne===""?"."+ve(T,0):ne,Q(le)?(O="",ce!=null&&(O=ce.replace(Ae,"$&/")+"/"),R(le,G,O,"",function(ft){return ft})):le!=null&&(de(le)&&(le=se(le,O+(le.key==null||T&&T.key===le.key?"":(""+le.key).replace(Ae,"$&/")+"/")+ce)),G.push(le)),1;ce=0;var De=ne===""?".":ne+":";if(Q(T))for(var ge=0;ge>>1,W=R[he];if(0>>1;heo(O,ee))neo(le,O)?(R[he]=le,R[ne]=ee,he=ne):(R[he]=O,R[G]=ee,he=G);else if(neo(le,ee))R[he]=le,R[ne]=ee,he=ne;else break e}}return J}function o(R,J){var ee=R.sortIndex-J.sortIndex;return ee!==0?ee:R.id-J.id}if(a.unstable_now=void 0,typeof performance=="object"&&typeof performance.now=="function"){var f=performance;a.unstable_now=function(){return f.now()}}else{var h=Date,m=h.now();a.unstable_now=function(){return h.now()-m}}var p=[],g=[],y=1,v=null,S=3,k=!1,N=!1,E=!1,M=!1,A=typeof setTimeout=="function"?setTimeout:null,z=typeof clearTimeout=="function"?clearTimeout:null,U=typeof setImmediate<"u"?setImmediate:null;function Y(R){for(var J=l(g);J!==null;){if(J.callback===null)r(g);else if(J.startTime<=R)r(g),J.sortIndex=J.expirationTime,n(p,J);else break;J=l(g)}}function Q(R){if(E=!1,Y(R),!N)if(l(p)!==null)N=!0,ae||(ae=!0,pe());else{var J=l(g);J!==null&&$(Q,J.startTime-R)}}var ae=!1,K=-1,B=5,I=-1;function se(){return M?!0:!(a.unstable_now()-IR&&se());){var he=v.callback;if(typeof he=="function"){v.callback=null,S=v.priorityLevel;var W=he(v.expirationTime<=R);if(R=a.unstable_now(),typeof W=="function"){v.callback=W,Y(R),J=!0;break t}v===l(p)&&r(p),Y(R)}else r(p);v=l(p)}if(v!==null)J=!0;else{var T=l(g);T!==null&&$(Q,T.startTime-R),J=!1}}break e}finally{v=null,S=ee,k=!1}J=void 0}}finally{J?pe():ae=!1}}}var pe;if(typeof U=="function")pe=function(){U(de)};else if(typeof MessageChannel<"u"){var Ae=new MessageChannel,ve=Ae.port2;Ae.port1.onmessage=de,pe=function(){ve.postMessage(null)}}else pe=function(){A(de,0)};function $(R,J){K=A(function(){R(a.unstable_now())},J)}a.unstable_IdlePriority=5,a.unstable_ImmediatePriority=1,a.unstable_LowPriority=4,a.unstable_NormalPriority=3,a.unstable_Profiling=null,a.unstable_UserBlockingPriority=2,a.unstable_cancelCallback=function(R){R.callback=null},a.unstable_forceFrameRate=function(R){0>R||125he?(R.sortIndex=ee,n(g,R),l(p)===null&&R===l(g)&&(E?(z(K),K=-1):E=!0,$(Q,ee-he))):(R.sortIndex=W,n(p,R),N||k||(N=!0,ae||(ae=!0,pe()))),R},a.unstable_shouldYield=se,a.unstable_wrapCallback=function(R){var J=S;return function(){var ee=S;S=J;try{return R.apply(this,arguments)}finally{S=ee}}}})(Ic)),Ic}var Zp;function aS(){return Zp||(Zp=1,Wc.exports=tS()),Wc.exports}var ed={exports:{}},St={};/** + * @license React + * react-dom.production.js + * + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */var Jp;function nS(){if(Jp)return St;Jp=1;var a=nf();function n(p){var g="https://react.dev/errors/"+p;if(1"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(a)}catch(n){console.error(n)}}return a(),ed.exports=nS(),ed.exports}/** + * @license React + * react-dom-client.production.js + * + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */var Wp;function lS(){if(Wp)return Wl;Wp=1;var a=aS(),n=nf(),l=sS();function r(e){var t="https://react.dev/errors/"+e;if(1W||(e.current=he[W],he[W]=null,W--)}function O(e,t){W++,he[W]=e.current,e.current=t}var ne=T(null),le=T(null),Z=T(null),ce=T(null);function De(e,t){switch(O(Z,t),O(le,e),O(ne,null),t.nodeType){case 9:case 11:e=(e=t.documentElement)&&(e=e.namespaceURI)?hp(e):0;break;default:if(e=t.tagName,t=t.namespaceURI)t=hp(t),e=mp(t,e);else switch(e){case"svg":e=1;break;case"math":e=2;break;default:e=0}}G(ne),O(ne,e)}function ge(){G(ne),G(le),G(Z)}function ft(e){e.memoizedState!==null&&O(ce,e);var t=ne.current,s=mp(t,e.type);t!==s&&(O(le,e),O(ne,s))}function bt(e){le.current===e&&(G(ne),G(le)),ce.current===e&&(G(ce),Kl._currentValue=ee)}var Ot,tt;function wt(e){if(Ot===void 0)try{throw Error()}catch(s){var t=s.stack.trim().match(/\n( *(at )?)/);Ot=t&&t[1]||"",tt=-1)":-1u||j[i]!==V[u]){var q=` +`+j[i].replace(" at new "," at ");return e.displayName&&q.includes("")&&(q=q.replace("",e.displayName)),q}while(1<=i&&0<=u);break}}}finally{al=!1,Error.prepareStackTrace=s}return(s=e?e.displayName||e.name:"")?wt(s):""}function Vo(e,t){switch(e.tag){case 26:case 27:case 5:return wt(e.type);case 16:return wt("Lazy");case 13:return e.child!==t&&t!==null?wt("Suspense Fallback"):wt("Suspense");case 19:return wt("SuspenseList");case 0:case 15:return nl(e.type,!1);case 11:return nl(e.type.render,!1);case 1:return nl(e.type,!0);case 31:return wt("Activity");default:return""}}function sl(e){try{var t="",s=null;do t+=Vo(e,s),s=e,e=e.return;while(e);return t}catch(i){return` +Error generating stack: `+i.message+` +`+i.stack}}var es=Object.prototype.hasOwnProperty,ts=a.unstable_scheduleCallback,as=a.unstable_cancelCallback,F=a.unstable_shouldYield,ye=a.unstable_requestPaint,te=a.unstable_now,Rb=a.unstable_getCurrentPriorityLevel,Pf=a.unstable_ImmediatePriority,Qf=a.unstable_UserBlockingPriority,Ni=a.unstable_NormalPriority,Ob=a.unstable_LowPriority,Kf=a.unstable_IdlePriority,zb=a.log,_b=a.unstable_setDisableYieldValue,ll=null,zt=null;function Ga(e){if(typeof zb=="function"&&_b(e),zt&&typeof zt.setStrictMode=="function")try{zt.setStrictMode(ll,e)}catch{}}var _t=Math.clz32?Math.clz32:Lb,Vb=Math.log,Ub=Math.LN2;function Lb(e){return e>>>=0,e===0?32:31-(Vb(e)/Ub|0)|0}var Ti=256,Ei=262144,Ci=4194304;function En(e){var t=e&42;if(t!==0)return t;switch(e&-e){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:return 64;case 128:return 128;case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:return e&261888;case 262144:case 524288:case 1048576:case 2097152:return e&3932160;case 4194304:case 8388608:case 16777216:case 33554432:return e&62914560;case 67108864:return 67108864;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 0;default:return e}}function Mi(e,t,s){var i=e.pendingLanes;if(i===0)return 0;var u=0,c=e.suspendedLanes,x=e.pingedLanes;e=e.warmLanes;var b=i&134217727;return b!==0?(i=b&~c,i!==0?u=En(i):(x&=b,x!==0?u=En(x):s||(s=b&~e,s!==0&&(u=En(s))))):(b=i&~c,b!==0?u=En(b):x!==0?u=En(x):s||(s=i&~e,s!==0&&(u=En(s)))),u===0?0:t!==0&&t!==u&&(t&c)===0&&(c=u&-u,s=t&-t,c>=s||c===32&&(s&4194048)!==0)?t:u}function il(e,t){return(e.pendingLanes&~(e.suspendedLanes&~e.pingedLanes)&t)===0}function Bb(e,t){switch(e){case 1:case 2:case 4:case 8:case 64:return t+250;case 16:case 32:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return t+5e3;case 4194304:case 8388608:case 16777216:case 33554432:return-1;case 67108864:case 134217728:case 268435456:case 536870912:case 1073741824:return-1;default:return-1}}function Ff(){var e=Ci;return Ci<<=1,(Ci&62914560)===0&&(Ci=4194304),e}function Uo(e){for(var t=[],s=0;31>s;s++)t.push(e);return t}function rl(e,t){e.pendingLanes|=t,t!==268435456&&(e.suspendedLanes=0,e.pingedLanes=0,e.warmLanes=0)}function Hb(e,t,s,i,u,c){var x=e.pendingLanes;e.pendingLanes=s,e.suspendedLanes=0,e.pingedLanes=0,e.warmLanes=0,e.expiredLanes&=s,e.entangledLanes&=s,e.errorRecoveryDisabledLanes&=s,e.shellSuspendCounter=0;var b=e.entanglements,j=e.expirationTimes,V=e.hiddenUpdates;for(s=x&~s;0"u")return null;try{return e.activeElement||e.body}catch{return e.body}}var Qb=/[\n"\\]/g;function Qt(e){return e.replace(Qb,function(t){return"\\"+t.charCodeAt(0).toString(16)+" "})}function Go(e,t,s,i,u,c,x,b){e.name="",x!=null&&typeof x!="function"&&typeof x!="symbol"&&typeof x!="boolean"?e.type=x:e.removeAttribute("type"),t!=null?x==="number"?(t===0&&e.value===""||e.value!=t)&&(e.value=""+Pt(t)):e.value!==""+Pt(t)&&(e.value=""+Pt(t)):x!=="submit"&&x!=="reset"||e.removeAttribute("value"),t!=null?Xo(e,x,Pt(t)):s!=null?Xo(e,x,Pt(s)):i!=null&&e.removeAttribute("value"),u==null&&c!=null&&(e.defaultChecked=!!c),u!=null&&(e.checked=u&&typeof u!="function"&&typeof u!="symbol"),b!=null&&typeof b!="function"&&typeof b!="symbol"&&typeof b!="boolean"?e.name=""+Pt(b):e.removeAttribute("name")}function rh(e,t,s,i,u,c,x,b){if(c!=null&&typeof c!="function"&&typeof c!="symbol"&&typeof c!="boolean"&&(e.type=c),t!=null||s!=null){if(!(c!=="submit"&&c!=="reset"||t!=null)){qo(e);return}s=s!=null?""+Pt(s):"",t=t!=null?""+Pt(t):s,b||t===e.value||(e.value=t),e.defaultValue=t}i=i??u,i=typeof i!="function"&&typeof i!="symbol"&&!!i,e.checked=b?e.checked:!!i,e.defaultChecked=!!i,x!=null&&typeof x!="function"&&typeof x!="symbol"&&typeof x!="boolean"&&(e.name=x),qo(e)}function Xo(e,t,s){t==="number"&&Ri(e.ownerDocument)===e||e.defaultValue===""+s||(e.defaultValue=""+s)}function os(e,t,s,i){if(e=e.options,t){t={};for(var u=0;u"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),Zo=!1;if(ka)try{var dl={};Object.defineProperty(dl,"passive",{get:function(){Zo=!0}}),window.addEventListener("test",dl,dl),window.removeEventListener("test",dl,dl)}catch{Zo=!1}var Pa=null,Jo=null,zi=null;function mh(){if(zi)return zi;var e,t=Jo,s=t.length,i,u="value"in Pa?Pa.value:Pa.textContent,c=u.length;for(e=0;e=ml),bh=" ",Sh=!1;function wh(e,t){switch(e){case"keyup":return b1.indexOf(t.keyCode)!==-1;case"keydown":return t.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function kh(e){return e=e.detail,typeof e=="object"&&"data"in e?e.data:null}var fs=!1;function w1(e,t){switch(e){case"compositionend":return kh(t);case"keypress":return t.which!==32?null:(Sh=!0,bh);case"textInput":return e=t.data,e===bh&&Sh?null:e;default:return null}}function k1(e,t){if(fs)return e==="compositionend"||!tu&&wh(e,t)?(e=mh(),zi=Jo=Pa=null,fs=!1,e):null;switch(e){case"paste":return null;case"keypress":if(!(t.ctrlKey||t.altKey||t.metaKey)||t.ctrlKey&&t.altKey){if(t.char&&1=t)return{node:s,offset:t-e};e=i}e:{for(;s;){if(s.nextSibling){s=s.nextSibling;break e}s=s.parentNode}s=void 0}s=Dh(s)}}function Oh(e,t){return e&&t?e===t?!0:e&&e.nodeType===3?!1:t&&t.nodeType===3?Oh(e,t.parentNode):"contains"in e?e.contains(t):e.compareDocumentPosition?!!(e.compareDocumentPosition(t)&16):!1:!1}function zh(e){e=e!=null&&e.ownerDocument!=null&&e.ownerDocument.defaultView!=null?e.ownerDocument.defaultView:window;for(var t=Ri(e.document);t instanceof e.HTMLIFrameElement;){try{var s=typeof t.contentWindow.location.href=="string"}catch{s=!1}if(s)e=t.contentWindow;else break;t=Ri(e.document)}return t}function su(e){var t=e&&e.nodeName&&e.nodeName.toLowerCase();return t&&(t==="input"&&(e.type==="text"||e.type==="search"||e.type==="tel"||e.type==="url"||e.type==="password")||t==="textarea"||e.contentEditable==="true")}var D1=ka&&"documentMode"in document&&11>=document.documentMode,hs=null,lu=null,yl=null,iu=!1;function _h(e,t,s){var i=s.window===s?s.document:s.nodeType===9?s:s.ownerDocument;iu||hs==null||hs!==Ri(i)||(i=hs,"selectionStart"in i&&su(i)?i={start:i.selectionStart,end:i.selectionEnd}:(i=(i.ownerDocument&&i.ownerDocument.defaultView||window).getSelection(),i={anchorNode:i.anchorNode,anchorOffset:i.anchorOffset,focusNode:i.focusNode,focusOffset:i.focusOffset}),yl&&xl(yl,i)||(yl=i,i=Er(lu,"onSelect"),0>=x,u-=x,ha=1<<32-_t(t)+u|s<we?(Ce=fe,fe=null):Ce=fe.sibling;var Oe=L(D,fe,_[we],X);if(Oe===null){fe===null&&(fe=Ce);break}e&&fe&&Oe.alternate===null&&t(D,fe),C=c(Oe,C,we),Re===null?me=Oe:Re.sibling=Oe,Re=Oe,fe=Ce}if(we===_.length)return s(D,fe),Me&&Na(D,we),me;if(fe===null){for(;we<_.length;we++)fe=P(D,_[we],X),fe!==null&&(C=c(fe,C,we),Re===null?me=fe:Re.sibling=fe,Re=fe);return Me&&Na(D,we),me}for(fe=i(fe);we<_.length;we++)Ce=H(fe,D,we,_[we],X),Ce!==null&&(e&&Ce.alternate!==null&&fe.delete(Ce.key===null?we:Ce.key),C=c(Ce,C,we),Re===null?me=Ce:Re.sibling=Ce,Re=Ce);return e&&fe.forEach(function(mn){return t(D,mn)}),Me&&Na(D,we),me}function xe(D,C,_,X){if(_==null)throw Error(r(151));for(var me=null,Re=null,fe=C,we=C=0,Ce=null,Oe=_.next();fe!==null&&!Oe.done;we++,Oe=_.next()){fe.index>we?(Ce=fe,fe=null):Ce=fe.sibling;var mn=L(D,fe,Oe.value,X);if(mn===null){fe===null&&(fe=Ce);break}e&&fe&&mn.alternate===null&&t(D,fe),C=c(mn,C,we),Re===null?me=mn:Re.sibling=mn,Re=mn,fe=Ce}if(Oe.done)return s(D,fe),Me&&Na(D,we),me;if(fe===null){for(;!Oe.done;we++,Oe=_.next())Oe=P(D,Oe.value,X),Oe!==null&&(C=c(Oe,C,we),Re===null?me=Oe:Re.sibling=Oe,Re=Oe);return Me&&Na(D,we),me}for(fe=i(fe);!Oe.done;we++,Oe=_.next())Oe=H(fe,D,we,Oe.value,X),Oe!==null&&(e&&Oe.alternate!==null&&fe.delete(Oe.key===null?we:Oe.key),C=c(Oe,C,we),Re===null?me=Oe:Re.sibling=Oe,Re=Oe);return e&&fe.forEach(function($2){return t(D,$2)}),Me&&Na(D,we),me}function He(D,C,_,X){if(typeof _=="object"&&_!==null&&_.type===E&&_.key===null&&(_=_.props.children),typeof _=="object"&&_!==null){switch(_.$$typeof){case k:e:{for(var me=_.key;C!==null;){if(C.key===me){if(me=_.type,me===E){if(C.tag===7){s(D,C.sibling),X=u(C,_.props.children),X.return=D,D=X;break e}}else if(C.elementType===me||typeof me=="object"&&me!==null&&me.$$typeof===B&&Ln(me)===C.type){s(D,C.sibling),X=u(C,_.props),jl(X,_),X.return=D,D=X;break e}s(D,C);break}else t(D,C);C=C.sibling}_.type===E?(X=On(_.props.children,D.mode,X,_.key),X.return=D,D=X):(X=Xi(_.type,_.key,_.props,null,D.mode,X),jl(X,_),X.return=D,D=X)}return x(D);case N:e:{for(me=_.key;C!==null;){if(C.key===me)if(C.tag===4&&C.stateNode.containerInfo===_.containerInfo&&C.stateNode.implementation===_.implementation){s(D,C.sibling),X=u(C,_.children||[]),X.return=D,D=X;break e}else{s(D,C);break}else t(D,C);C=C.sibling}X=hu(_,D.mode,X),X.return=D,D=X}return x(D);case B:return _=Ln(_),He(D,C,_,X)}if($(_))return ie(D,C,_,X);if(pe(_)){if(me=pe(_),typeof me!="function")throw Error(r(150));return _=me.call(_),xe(D,C,_,X)}if(typeof _.then=="function")return He(D,C,$i(_),X);if(_.$$typeof===U)return He(D,C,Ki(D,_),X);Wi(D,_)}return typeof _=="string"&&_!==""||typeof _=="number"||typeof _=="bigint"?(_=""+_,C!==null&&C.tag===6?(s(D,C.sibling),X=u(C,_),X.return=D,D=X):(s(D,C),X=fu(_,D.mode,X),X.return=D,D=X),x(D)):s(D,C)}return function(D,C,_,X){try{kl=0;var me=He(D,C,_,X);return js=null,me}catch(fe){if(fe===ks||fe===Zi)throw fe;var Re=Ut(29,fe,null,D.mode);return Re.lanes=X,Re.return=D,Re}finally{}}}var Hn=sm(!0),lm=sm(!1),Ja=!1;function Nu(e){e.updateQueue={baseState:e.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,lanes:0,hiddenCallbacks:null},callbacks:null}}function Tu(e,t){e=e.updateQueue,t.updateQueue===e&&(t.updateQueue={baseState:e.baseState,firstBaseUpdate:e.firstBaseUpdate,lastBaseUpdate:e.lastBaseUpdate,shared:e.shared,callbacks:null})}function $a(e){return{lane:e,tag:0,payload:null,callback:null,next:null}}function Wa(e,t,s){var i=e.updateQueue;if(i===null)return null;if(i=i.shared,(ze&2)!==0){var u=i.pending;return u===null?t.next=t:(t.next=u.next,u.next=t),i.pending=t,t=Gi(e),qh(e,null,s),t}return qi(e,i,t,s),Gi(e)}function Nl(e,t,s){if(t=t.updateQueue,t!==null&&(t=t.shared,(s&4194048)!==0)){var i=t.lanes;i&=e.pendingLanes,s|=i,t.lanes=s,Jf(e,s)}}function Eu(e,t){var s=e.updateQueue,i=e.alternate;if(i!==null&&(i=i.updateQueue,s===i)){var u=null,c=null;if(s=s.firstBaseUpdate,s!==null){do{var x={lane:s.lane,tag:s.tag,payload:s.payload,callback:null,next:null};c===null?u=c=x:c=c.next=x,s=s.next}while(s!==null);c===null?u=c=t:c=c.next=t}else u=c=t;s={baseState:i.baseState,firstBaseUpdate:u,lastBaseUpdate:c,shared:i.shared,callbacks:i.callbacks},e.updateQueue=s;return}e=s.lastBaseUpdate,e===null?s.firstBaseUpdate=t:e.next=t,s.lastBaseUpdate=t}var Cu=!1;function Tl(){if(Cu){var e=ws;if(e!==null)throw e}}function El(e,t,s,i){Cu=!1;var u=e.updateQueue;Ja=!1;var c=u.firstBaseUpdate,x=u.lastBaseUpdate,b=u.shared.pending;if(b!==null){u.shared.pending=null;var j=b,V=j.next;j.next=null,x===null?c=V:x.next=V,x=j;var q=e.alternate;q!==null&&(q=q.updateQueue,b=q.lastBaseUpdate,b!==x&&(b===null?q.firstBaseUpdate=V:b.next=V,q.lastBaseUpdate=j))}if(c!==null){var P=u.baseState;x=0,q=V=j=null,b=c;do{var L=b.lane&-536870913,H=L!==b.lane;if(H?(Ee&L)===L:(i&L)===L){L!==0&&L===Ss&&(Cu=!0),q!==null&&(q=q.next={lane:0,tag:b.tag,payload:b.payload,callback:null,next:null});e:{var ie=e,xe=b;L=t;var He=s;switch(xe.tag){case 1:if(ie=xe.payload,typeof ie=="function"){P=ie.call(He,P,L);break e}P=ie;break e;case 3:ie.flags=ie.flags&-65537|128;case 0:if(ie=xe.payload,L=typeof ie=="function"?ie.call(He,P,L):ie,L==null)break e;P=v({},P,L);break e;case 2:Ja=!0}}L=b.callback,L!==null&&(e.flags|=64,H&&(e.flags|=8192),H=u.callbacks,H===null?u.callbacks=[L]:H.push(L))}else H={lane:L,tag:b.tag,payload:b.payload,callback:b.callback,next:null},q===null?(V=q=H,j=P):q=q.next=H,x|=L;if(b=b.next,b===null){if(b=u.shared.pending,b===null)break;H=b,b=H.next,H.next=null,u.lastBaseUpdate=H,u.shared.pending=null}}while(!0);q===null&&(j=P),u.baseState=j,u.firstBaseUpdate=V,u.lastBaseUpdate=q,c===null&&(u.shared.lanes=0),nn|=x,e.lanes=x,e.memoizedState=P}}function im(e,t){if(typeof e!="function")throw Error(r(191,e));e.call(t)}function rm(e,t){var s=e.callbacks;if(s!==null)for(e.callbacks=null,e=0;ec?c:8;var x=R.T,b={};R.T=b,Ku(e,!1,t,s);try{var j=u(),V=R.S;if(V!==null&&V(b,j),j!==null&&typeof j=="object"&&typeof j.then=="function"){var q=H1(j,i);Al(e,t,q,qt(e))}else Al(e,t,i,qt(e))}catch(P){Al(e,t,{then:function(){},status:"rejected",reason:P},qt())}finally{J.p=c,x!==null&&b.types!==null&&(x.types=b.types),R.T=x}}function Q1(){}function Pu(e,t,s,i){if(e.tag!==5)throw Error(r(476));var u=Bm(e).queue;Lm(e,u,t,ee,s===null?Q1:function(){return Hm(e),s(i)})}function Bm(e){var t=e.memoizedState;if(t!==null)return t;t={memoizedState:ee,baseState:ee,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:Ma,lastRenderedState:ee},next:null};var s={};return t.next={memoizedState:s,baseState:s,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:Ma,lastRenderedState:s},next:null},e.memoizedState=t,e=e.alternate,e!==null&&(e.memoizedState=t),t}function Hm(e){var t=Bm(e);t.next===null&&(t=e.alternate.memoizedState),Al(e,t.next.queue,{},qt())}function Qu(){return pt(Kl)}function Ym(){return et().memoizedState}function qm(){return et().memoizedState}function K1(e){for(var t=e.return;t!==null;){switch(t.tag){case 24:case 3:var s=qt();e=$a(s);var i=Wa(t,e,s);i!==null&&(Rt(i,t,s),Nl(i,t,s)),t={cache:Su()},e.payload=t;return}t=t.return}}function F1(e,t,s){var i=qt();s={lane:i,revertLane:0,gesture:null,action:s,hasEagerState:!1,eagerState:null,next:null},or(e)?Xm(t,s):(s=cu(e,t,s,i),s!==null&&(Rt(s,e,i),Pm(s,t,i)))}function Gm(e,t,s){var i=qt();Al(e,t,s,i)}function Al(e,t,s,i){var u={lane:i,revertLane:0,gesture:null,action:s,hasEagerState:!1,eagerState:null,next:null};if(or(e))Xm(t,u);else{var c=e.alternate;if(e.lanes===0&&(c===null||c.lanes===0)&&(c=t.lastRenderedReducer,c!==null))try{var x=t.lastRenderedState,b=c(x,s);if(u.hasEagerState=!0,u.eagerState=b,Vt(b,x))return qi(e,t,u,0),Ye===null&&Yi(),!1}catch{}finally{}if(s=cu(e,t,u,i),s!==null)return Rt(s,e,i),Pm(s,t,i),!0}return!1}function Ku(e,t,s,i){if(i={lane:2,revertLane:Nc(),gesture:null,action:i,hasEagerState:!1,eagerState:null,next:null},or(e)){if(t)throw Error(r(479))}else t=cu(e,s,i,2),t!==null&&Rt(t,e,2)}function or(e){var t=e.alternate;return e===Se||t!==null&&t===Se}function Xm(e,t){Ts=tr=!0;var s=e.pending;s===null?t.next=t:(t.next=s.next,s.next=t),e.pending=t}function Pm(e,t,s){if((s&4194048)!==0){var i=t.lanes;i&=e.pendingLanes,s|=i,t.lanes=s,Jf(e,s)}}var Dl={readContext:pt,use:sr,useCallback:Je,useContext:Je,useEffect:Je,useImperativeHandle:Je,useLayoutEffect:Je,useInsertionEffect:Je,useMemo:Je,useReducer:Je,useRef:Je,useState:Je,useDebugValue:Je,useDeferredValue:Je,useTransition:Je,useSyncExternalStore:Je,useId:Je,useHostTransitionStatus:Je,useFormState:Je,useActionState:Je,useOptimistic:Je,useMemoCache:Je,useCacheRefresh:Je};Dl.useEffectEvent=Je;var Qm={readContext:pt,use:sr,useCallback:function(e,t){return kt().memoizedState=[e,t===void 0?null:t],e},useContext:pt,useEffect:Mm,useImperativeHandle:function(e,t,s){s=s!=null?s.concat([e]):null,ir(4194308,4,Om.bind(null,t,e),s)},useLayoutEffect:function(e,t){return ir(4194308,4,e,t)},useInsertionEffect:function(e,t){ir(4,2,e,t)},useMemo:function(e,t){var s=kt();t=t===void 0?null:t;var i=e();if(Yn){Ga(!0);try{e()}finally{Ga(!1)}}return s.memoizedState=[i,t],i},useReducer:function(e,t,s){var i=kt();if(s!==void 0){var u=s(t);if(Yn){Ga(!0);try{s(t)}finally{Ga(!1)}}}else u=t;return i.memoizedState=i.baseState=u,e={pending:null,lanes:0,dispatch:null,lastRenderedReducer:e,lastRenderedState:u},i.queue=e,e=e.dispatch=F1.bind(null,Se,e),[i.memoizedState,e]},useRef:function(e){var t=kt();return e={current:e},t.memoizedState=e},useState:function(e){e=Hu(e);var t=e.queue,s=Gm.bind(null,Se,t);return t.dispatch=s,[e.memoizedState,s]},useDebugValue:Gu,useDeferredValue:function(e,t){var s=kt();return Xu(s,e,t)},useTransition:function(){var e=Hu(!1);return e=Lm.bind(null,Se,e.queue,!0,!1),kt().memoizedState=e,[!1,e]},useSyncExternalStore:function(e,t,s){var i=Se,u=kt();if(Me){if(s===void 0)throw Error(r(407));s=s()}else{if(s=t(),Ye===null)throw Error(r(349));(Ee&127)!==0||hm(i,t,s)}u.memoizedState=s;var c={value:s,getSnapshot:t};return u.queue=c,Mm(pm.bind(null,i,c,e),[e]),i.flags|=2048,Cs(9,{destroy:void 0},mm.bind(null,i,c,s,t),null),s},useId:function(){var e=kt(),t=Ye.identifierPrefix;if(Me){var s=ma,i=ha;s=(i&~(1<<32-_t(i)-1)).toString(32)+s,t="_"+t+"R_"+s,s=ar++,0<\/script>",c=c.removeChild(c.firstChild);break;case"select":c=typeof i.is=="string"?x.createElement("select",{is:i.is}):x.createElement("select"),i.multiple?c.multiple=!0:i.size&&(c.size=i.size);break;default:c=typeof i.is=="string"?x.createElement(u,{is:i.is}):x.createElement(u)}}c[ht]=t,c[Tt]=i;e:for(x=t.child;x!==null;){if(x.tag===5||x.tag===6)c.appendChild(x.stateNode);else if(x.tag!==4&&x.tag!==27&&x.child!==null){x.child.return=x,x=x.child;continue}if(x===t)break e;for(;x.sibling===null;){if(x.return===null||x.return===t)break e;x=x.return}x.sibling.return=x.return,x=x.sibling}t.stateNode=c;e:switch(xt(c,u,i),u){case"button":case"input":case"select":case"textarea":i=!!i.autoFocus;break e;case"img":i=!0;break e;default:i=!1}i&&Da(t)}}return Xe(t),rc(t,t.type,e===null?null:e.memoizedProps,t.pendingProps,s),null;case 6:if(e&&t.stateNode!=null)e.memoizedProps!==i&&Da(t);else{if(typeof i!="string"&&t.stateNode===null)throw Error(r(166));if(e=Z.current,vs(t)){if(e=t.stateNode,s=t.memoizedProps,i=null,u=mt,u!==null)switch(u.tag){case 27:case 5:i=u.memoizedProps}e[ht]=t,e=!!(e.nodeValue===s||i!==null&&i.suppressHydrationWarning===!0||dp(e.nodeValue,s)),e||Fa(t,!0)}else e=Cr(e).createTextNode(i),e[ht]=t,t.stateNode=e}return Xe(t),null;case 31:if(s=t.memoizedState,e===null||e.memoizedState!==null){if(i=vs(t),s!==null){if(e===null){if(!i)throw Error(r(318));if(e=t.memoizedState,e=e!==null?e.dehydrated:null,!e)throw Error(r(557));e[ht]=t}else zn(),(t.flags&128)===0&&(t.memoizedState=null),t.flags|=4;Xe(t),e=!1}else s=xu(),e!==null&&e.memoizedState!==null&&(e.memoizedState.hydrationErrors=s),e=!0;if(!e)return t.flags&256?(Bt(t),t):(Bt(t),null);if((t.flags&128)!==0)throw Error(r(558))}return Xe(t),null;case 13:if(i=t.memoizedState,e===null||e.memoizedState!==null&&e.memoizedState.dehydrated!==null){if(u=vs(t),i!==null&&i.dehydrated!==null){if(e===null){if(!u)throw Error(r(318));if(u=t.memoizedState,u=u!==null?u.dehydrated:null,!u)throw Error(r(317));u[ht]=t}else zn(),(t.flags&128)===0&&(t.memoizedState=null),t.flags|=4;Xe(t),u=!1}else u=xu(),e!==null&&e.memoizedState!==null&&(e.memoizedState.hydrationErrors=u),u=!0;if(!u)return t.flags&256?(Bt(t),t):(Bt(t),null)}return Bt(t),(t.flags&128)!==0?(t.lanes=s,t):(s=i!==null,e=e!==null&&e.memoizedState!==null,s&&(i=t.child,u=null,i.alternate!==null&&i.alternate.memoizedState!==null&&i.alternate.memoizedState.cachePool!==null&&(u=i.alternate.memoizedState.cachePool.pool),c=null,i.memoizedState!==null&&i.memoizedState.cachePool!==null&&(c=i.memoizedState.cachePool.pool),c!==u&&(i.flags|=2048)),s!==e&&s&&(t.child.flags|=8192),hr(t,t.updateQueue),Xe(t),null);case 4:return ge(),e===null&&Mc(t.stateNode.containerInfo),Xe(t),null;case 10:return Ea(t.type),Xe(t),null;case 19:if(G(Ie),i=t.memoizedState,i===null)return Xe(t),null;if(u=(t.flags&128)!==0,c=i.rendering,c===null)if(u)Ol(i,!1);else{if($e!==0||e!==null&&(e.flags&128)!==0)for(e=t.child;e!==null;){if(c=er(e),c!==null){for(t.flags|=128,Ol(i,!1),e=c.updateQueue,t.updateQueue=e,hr(t,e),t.subtreeFlags=0,e=s,s=t.child;s!==null;)Gh(s,e),s=s.sibling;return O(Ie,Ie.current&1|2),Me&&Na(t,i.treeForkCount),t.child}e=e.sibling}i.tail!==null&&te()>yr&&(t.flags|=128,u=!0,Ol(i,!1),t.lanes=4194304)}else{if(!u)if(e=er(c),e!==null){if(t.flags|=128,u=!0,e=e.updateQueue,t.updateQueue=e,hr(t,e),Ol(i,!0),i.tail===null&&i.tailMode==="hidden"&&!c.alternate&&!Me)return Xe(t),null}else 2*te()-i.renderingStartTime>yr&&s!==536870912&&(t.flags|=128,u=!0,Ol(i,!1),t.lanes=4194304);i.isBackwards?(c.sibling=t.child,t.child=c):(e=i.last,e!==null?e.sibling=c:t.child=c,i.last=c)}return i.tail!==null?(e=i.tail,i.rendering=e,i.tail=e.sibling,i.renderingStartTime=te(),e.sibling=null,s=Ie.current,O(Ie,u?s&1|2:s&1),Me&&Na(t,i.treeForkCount),e):(Xe(t),null);case 22:case 23:return Bt(t),Au(),i=t.memoizedState!==null,e!==null?e.memoizedState!==null!==i&&(t.flags|=8192):i&&(t.flags|=8192),i?(s&536870912)!==0&&(t.flags&128)===0&&(Xe(t),t.subtreeFlags&6&&(t.flags|=8192)):Xe(t),s=t.updateQueue,s!==null&&hr(t,s.retryQueue),s=null,e!==null&&e.memoizedState!==null&&e.memoizedState.cachePool!==null&&(s=e.memoizedState.cachePool.pool),i=null,t.memoizedState!==null&&t.memoizedState.cachePool!==null&&(i=t.memoizedState.cachePool.pool),i!==s&&(t.flags|=2048),e!==null&&G(Un),null;case 24:return s=null,e!==null&&(s=e.memoizedState.cache),t.memoizedState.cache!==s&&(t.flags|=2048),Ea(at),Xe(t),null;case 25:return null;case 30:return null}throw Error(r(156,t.tag))}function I1(e,t){switch(pu(t),t.tag){case 1:return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 3:return Ea(at),ge(),e=t.flags,(e&65536)!==0&&(e&128)===0?(t.flags=e&-65537|128,t):null;case 26:case 27:case 5:return bt(t),null;case 31:if(t.memoizedState!==null){if(Bt(t),t.alternate===null)throw Error(r(340));zn()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 13:if(Bt(t),e=t.memoizedState,e!==null&&e.dehydrated!==null){if(t.alternate===null)throw Error(r(340));zn()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 19:return G(Ie),null;case 4:return ge(),null;case 10:return Ea(t.type),null;case 22:case 23:return Bt(t),Au(),e!==null&&G(Un),e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 24:return Ea(at),null;case 25:return null;default:return null}}function g0(e,t){switch(pu(t),t.tag){case 3:Ea(at),ge();break;case 26:case 27:case 5:bt(t);break;case 4:ge();break;case 31:t.memoizedState!==null&&Bt(t);break;case 13:Bt(t);break;case 19:G(Ie);break;case 10:Ea(t.type);break;case 22:case 23:Bt(t),Au(),e!==null&&G(Un);break;case 24:Ea(at)}}function zl(e,t){try{var s=t.updateQueue,i=s!==null?s.lastEffect:null;if(i!==null){var u=i.next;s=u;do{if((s.tag&e)===e){i=void 0;var c=s.create,x=s.inst;i=c(),x.destroy=i}s=s.next}while(s!==u)}}catch(b){Ve(t,t.return,b)}}function tn(e,t,s){try{var i=t.updateQueue,u=i!==null?i.lastEffect:null;if(u!==null){var c=u.next;i=c;do{if((i.tag&e)===e){var x=i.inst,b=x.destroy;if(b!==void 0){x.destroy=void 0,u=t;var j=s,V=b;try{V()}catch(q){Ve(u,j,q)}}}i=i.next}while(i!==c)}}catch(q){Ve(t,t.return,q)}}function x0(e){var t=e.updateQueue;if(t!==null){var s=e.stateNode;try{rm(t,s)}catch(i){Ve(e,e.return,i)}}}function y0(e,t,s){s.props=qn(e.type,e.memoizedProps),s.state=e.memoizedState;try{s.componentWillUnmount()}catch(i){Ve(e,t,i)}}function _l(e,t){try{var s=e.ref;if(s!==null){switch(e.tag){case 26:case 27:case 5:var i=e.stateNode;break;case 30:i=e.stateNode;break;default:i=e.stateNode}typeof s=="function"?e.refCleanup=s(i):s.current=i}}catch(u){Ve(e,t,u)}}function pa(e,t){var s=e.ref,i=e.refCleanup;if(s!==null)if(typeof i=="function")try{i()}catch(u){Ve(e,t,u)}finally{e.refCleanup=null,e=e.alternate,e!=null&&(e.refCleanup=null)}else if(typeof s=="function")try{s(null)}catch(u){Ve(e,t,u)}else s.current=null}function v0(e){var t=e.type,s=e.memoizedProps,i=e.stateNode;try{e:switch(t){case"button":case"input":case"select":case"textarea":s.autoFocus&&i.focus();break e;case"img":s.src?i.src=s.src:s.srcSet&&(i.srcset=s.srcSet)}}catch(u){Ve(e,e.return,u)}}function oc(e,t,s){try{var i=e.stateNode;S2(i,e.type,s,t),i[Tt]=t}catch(u){Ve(e,e.return,u)}}function b0(e){return e.tag===5||e.tag===3||e.tag===26||e.tag===27&&un(e.type)||e.tag===4}function uc(e){e:for(;;){for(;e.sibling===null;){if(e.return===null||b0(e.return))return null;e=e.return}for(e.sibling.return=e.return,e=e.sibling;e.tag!==5&&e.tag!==6&&e.tag!==18;){if(e.tag===27&&un(e.type)||e.flags&2||e.child===null||e.tag===4)continue e;e.child.return=e,e=e.child}if(!(e.flags&2))return e.stateNode}}function cc(e,t,s){var i=e.tag;if(i===5||i===6)e=e.stateNode,t?(s.nodeType===9?s.body:s.nodeName==="HTML"?s.ownerDocument.body:s).insertBefore(e,t):(t=s.nodeType===9?s.body:s.nodeName==="HTML"?s.ownerDocument.body:s,t.appendChild(e),s=s._reactRootContainer,s!=null||t.onclick!==null||(t.onclick=wa));else if(i!==4&&(i===27&&un(e.type)&&(s=e.stateNode,t=null),e=e.child,e!==null))for(cc(e,t,s),e=e.sibling;e!==null;)cc(e,t,s),e=e.sibling}function mr(e,t,s){var i=e.tag;if(i===5||i===6)e=e.stateNode,t?s.insertBefore(e,t):s.appendChild(e);else if(i!==4&&(i===27&&un(e.type)&&(s=e.stateNode),e=e.child,e!==null))for(mr(e,t,s),e=e.sibling;e!==null;)mr(e,t,s),e=e.sibling}function S0(e){var t=e.stateNode,s=e.memoizedProps;try{for(var i=e.type,u=t.attributes;u.length;)t.removeAttributeNode(u[0]);xt(t,i,s),t[ht]=e,t[Tt]=s}catch(c){Ve(e,e.return,c)}}var Ra=!1,lt=!1,dc=!1,w0=typeof WeakSet=="function"?WeakSet:Set,dt=null;function e2(e,t){if(e=e.containerInfo,Rc=_r,e=zh(e),su(e)){if("selectionStart"in e)var s={start:e.selectionStart,end:e.selectionEnd};else e:{s=(s=e.ownerDocument)&&s.defaultView||window;var i=s.getSelection&&s.getSelection();if(i&&i.rangeCount!==0){s=i.anchorNode;var u=i.anchorOffset,c=i.focusNode;i=i.focusOffset;try{s.nodeType,c.nodeType}catch{s=null;break e}var x=0,b=-1,j=-1,V=0,q=0,P=e,L=null;t:for(;;){for(var H;P!==s||u!==0&&P.nodeType!==3||(b=x+u),P!==c||i!==0&&P.nodeType!==3||(j=x+i),P.nodeType===3&&(x+=P.nodeValue.length),(H=P.firstChild)!==null;)L=P,P=H;for(;;){if(P===e)break t;if(L===s&&++V===u&&(b=x),L===c&&++q===i&&(j=x),(H=P.nextSibling)!==null)break;P=L,L=P.parentNode}P=H}s=b===-1||j===-1?null:{start:b,end:j}}else s=null}s=s||{start:0,end:0}}else s=null;for(Oc={focusedElem:e,selectionRange:s},_r=!1,dt=t;dt!==null;)if(t=dt,e=t.child,(t.subtreeFlags&1028)!==0&&e!==null)e.return=t,dt=e;else for(;dt!==null;){switch(t=dt,c=t.alternate,e=t.flags,t.tag){case 0:if((e&4)!==0&&(e=t.updateQueue,e=e!==null?e.events:null,e!==null))for(s=0;s title"))),xt(c,i,s),c[ht]=e,ct(c),i=c;break e;case"link":var x=Cp("link","href",u).get(i+(s.href||""));if(x){for(var b=0;bHe&&(x=He,He=xe,xe=x);var D=Rh(b,xe),C=Rh(b,He);if(D&&C&&(H.rangeCount!==1||H.anchorNode!==D.node||H.anchorOffset!==D.offset||H.focusNode!==C.node||H.focusOffset!==C.offset)){var _=P.createRange();_.setStart(D.node,D.offset),H.removeAllRanges(),xe>He?(H.addRange(_),H.extend(C.node,C.offset)):(_.setEnd(C.node,C.offset),H.addRange(_))}}}}for(P=[],H=b;H=H.parentNode;)H.nodeType===1&&P.push({element:H,left:H.scrollLeft,top:H.scrollTop});for(typeof b.focus=="function"&&b.focus(),b=0;bs?32:s,R.T=null,s=yc,yc=null;var c=ln,x=Ua;if(rt=0,Os=ln=null,Ua=0,(ze&6)!==0)throw Error(r(331));var b=ze;if(ze|=4,O0(c.current),A0(c,c.current,x,s),ze=b,Yl(0,!1),zt&&typeof zt.onPostCommitFiberRoot=="function")try{zt.onPostCommitFiberRoot(ll,c)}catch{}return!0}finally{J.p=u,R.T=i,$0(e,t)}}function I0(e,t,s){t=Ft(s,t),t=$u(e.stateNode,t,2),e=Wa(e,t,2),e!==null&&(rl(e,2),ga(e))}function Ve(e,t,s){if(e.tag===3)I0(e,e,s);else for(;t!==null;){if(t.tag===3){I0(t,e,s);break}else if(t.tag===1){var i=t.stateNode;if(typeof t.type.getDerivedStateFromError=="function"||typeof i.componentDidCatch=="function"&&(sn===null||!sn.has(i))){e=Ft(s,e),s=e0(2),i=Wa(t,s,2),i!==null&&(t0(s,i,t,e),rl(i,2),ga(i));break}}t=t.return}}function wc(e,t,s){var i=e.pingCache;if(i===null){i=e.pingCache=new n2;var u=new Set;i.set(t,u)}else u=i.get(t),u===void 0&&(u=new Set,i.set(t,u));u.has(s)||(mc=!0,u.add(s),e=o2.bind(null,e,t,s),t.then(e,e))}function o2(e,t,s){var i=e.pingCache;i!==null&&i.delete(t),e.pingedLanes|=e.suspendedLanes&s,e.warmLanes&=~s,Ye===e&&(Ee&s)===s&&($e===4||$e===3&&(Ee&62914560)===Ee&&300>te()-xr?(ze&2)===0&&zs(e,0):pc|=s,Rs===Ee&&(Rs=0)),ga(e)}function ep(e,t){t===0&&(t=Ff()),e=Rn(e,t),e!==null&&(rl(e,t),ga(e))}function u2(e){var t=e.memoizedState,s=0;t!==null&&(s=t.retryLane),ep(e,s)}function c2(e,t){var s=0;switch(e.tag){case 31:case 13:var i=e.stateNode,u=e.memoizedState;u!==null&&(s=u.retryLane);break;case 19:i=e.stateNode;break;case 22:i=e.stateNode._retryCache;break;default:throw Error(r(314))}i!==null&&i.delete(t),ep(e,s)}function d2(e,t){return ts(e,t)}var jr=null,Vs=null,kc=!1,Nr=!1,jc=!1,on=0;function ga(e){e!==Vs&&e.next===null&&(Vs===null?jr=Vs=e:Vs=Vs.next=e),Nr=!0,kc||(kc=!0,h2())}function Yl(e,t){if(!jc&&Nr){jc=!0;do for(var s=!1,i=jr;i!==null;){if(e!==0){var u=i.pendingLanes;if(u===0)var c=0;else{var x=i.suspendedLanes,b=i.pingedLanes;c=(1<<31-_t(42|e)+1)-1,c&=u&~(x&~b),c=c&201326741?c&201326741|1:c?c|2:0}c!==0&&(s=!0,sp(i,c))}else c=Ee,c=Mi(i,i===Ye?c:0,i.cancelPendingCommit!==null||i.timeoutHandle!==-1),(c&3)===0||il(i,c)||(s=!0,sp(i,c));i=i.next}while(s);jc=!1}}function f2(){tp()}function tp(){Nr=kc=!1;var e=0;on!==0&&k2()&&(e=on);for(var t=te(),s=null,i=jr;i!==null;){var u=i.next,c=ap(i,t);c===0?(i.next=null,s===null?jr=u:s.next=u,u===null&&(Vs=s)):(s=i,(e!==0||(c&3)!==0)&&(Nr=!0)),i=u}rt!==0&&rt!==5||Yl(e),on!==0&&(on=0)}function ap(e,t){for(var s=e.suspendedLanes,i=e.pingedLanes,u=e.expirationTimes,c=e.pendingLanes&-62914561;0b)break;var q=j.transferSize,P=j.initiatorType;q&&fp(P)&&(j=j.responseEnd,x+=q*(j"u"?null:document;function jp(e,t,s){var i=Us;if(i&&typeof t=="string"&&t){var u=Qt(t);u='link[rel="'+e+'"][href="'+u+'"]',typeof s=="string"&&(u+='[crossorigin="'+s+'"]'),kp.has(u)||(kp.add(u),e={rel:e,crossOrigin:s,href:t},i.querySelector(u)===null&&(t=i.createElement("link"),xt(t,"link",e),ct(t),i.head.appendChild(t)))}}function R2(e){La.D(e),jp("dns-prefetch",e,null)}function O2(e,t){La.C(e,t),jp("preconnect",e,t)}function z2(e,t,s){La.L(e,t,s);var i=Us;if(i&&e&&t){var u='link[rel="preload"][as="'+Qt(t)+'"]';t==="image"&&s&&s.imageSrcSet?(u+='[imagesrcset="'+Qt(s.imageSrcSet)+'"]',typeof s.imageSizes=="string"&&(u+='[imagesizes="'+Qt(s.imageSizes)+'"]')):u+='[href="'+Qt(e)+'"]';var c=u;switch(t){case"style":c=Ls(e);break;case"script":c=Bs(e)}ea.has(c)||(e=v({rel:"preload",href:t==="image"&&s&&s.imageSrcSet?void 0:e,as:t},s),ea.set(c,e),i.querySelector(u)!==null||t==="style"&&i.querySelector(Pl(c))||t==="script"&&i.querySelector(Ql(c))||(t=i.createElement("link"),xt(t,"link",e),ct(t),i.head.appendChild(t)))}}function _2(e,t){La.m(e,t);var s=Us;if(s&&e){var i=t&&typeof t.as=="string"?t.as:"script",u='link[rel="modulepreload"][as="'+Qt(i)+'"][href="'+Qt(e)+'"]',c=u;switch(i){case"audioworklet":case"paintworklet":case"serviceworker":case"sharedworker":case"worker":case"script":c=Bs(e)}if(!ea.has(c)&&(e=v({rel:"modulepreload",href:e},t),ea.set(c,e),s.querySelector(u)===null)){switch(i){case"audioworklet":case"paintworklet":case"serviceworker":case"sharedworker":case"worker":case"script":if(s.querySelector(Ql(c)))return}i=s.createElement("link"),xt(i,"link",e),ct(i),s.head.appendChild(i)}}}function V2(e,t,s){La.S(e,t,s);var i=Us;if(i&&e){var u=is(i).hoistableStyles,c=Ls(e);t=t||"default";var x=u.get(c);if(!x){var b={loading:0,preload:null};if(x=i.querySelector(Pl(c)))b.loading=5;else{e=v({rel:"stylesheet",href:e,"data-precedence":t},s),(s=ea.get(c))&&Hc(e,s);var j=x=i.createElement("link");ct(j),xt(j,"link",e),j._p=new Promise(function(V,q){j.onload=V,j.onerror=q}),j.addEventListener("load",function(){b.loading|=1}),j.addEventListener("error",function(){b.loading|=2}),b.loading|=4,Ar(x,t,i)}x={type:"stylesheet",instance:x,count:1,state:b},u.set(c,x)}}}function U2(e,t){La.X(e,t);var s=Us;if(s&&e){var i=is(s).hoistableScripts,u=Bs(e),c=i.get(u);c||(c=s.querySelector(Ql(u)),c||(e=v({src:e,async:!0},t),(t=ea.get(u))&&Yc(e,t),c=s.createElement("script"),ct(c),xt(c,"link",e),s.head.appendChild(c)),c={type:"script",instance:c,count:1,state:null},i.set(u,c))}}function L2(e,t){La.M(e,t);var s=Us;if(s&&e){var i=is(s).hoistableScripts,u=Bs(e),c=i.get(u);c||(c=s.querySelector(Ql(u)),c||(e=v({src:e,async:!0,type:"module"},t),(t=ea.get(u))&&Yc(e,t),c=s.createElement("script"),ct(c),xt(c,"link",e),s.head.appendChild(c)),c={type:"script",instance:c,count:1,state:null},i.set(u,c))}}function Np(e,t,s,i){var u=(u=Z.current)?Mr(u):null;if(!u)throw Error(r(446));switch(e){case"meta":case"title":return null;case"style":return typeof s.precedence=="string"&&typeof s.href=="string"?(t=Ls(s.href),s=is(u).hoistableStyles,i=s.get(t),i||(i={type:"style",instance:null,count:0,state:null},s.set(t,i)),i):{type:"void",instance:null,count:0,state:null};case"link":if(s.rel==="stylesheet"&&typeof s.href=="string"&&typeof s.precedence=="string"){e=Ls(s.href);var c=is(u).hoistableStyles,x=c.get(e);if(x||(u=u.ownerDocument||u,x={type:"stylesheet",instance:null,count:0,state:{loading:0,preload:null}},c.set(e,x),(c=u.querySelector(Pl(e)))&&!c._p&&(x.instance=c,x.state.loading=5),ea.has(e)||(s={rel:"preload",as:"style",href:s.href,crossOrigin:s.crossOrigin,integrity:s.integrity,media:s.media,hrefLang:s.hrefLang,referrerPolicy:s.referrerPolicy},ea.set(e,s),c||B2(u,e,s,x.state))),t&&i===null)throw Error(r(528,""));return x}if(t&&i!==null)throw Error(r(529,""));return null;case"script":return t=s.async,s=s.src,typeof s=="string"&&t&&typeof t!="function"&&typeof t!="symbol"?(t=Bs(s),s=is(u).hoistableScripts,i=s.get(t),i||(i={type:"script",instance:null,count:0,state:null},s.set(t,i)),i):{type:"void",instance:null,count:0,state:null};default:throw Error(r(444,e))}}function Ls(e){return'href="'+Qt(e)+'"'}function Pl(e){return'link[rel="stylesheet"]['+e+"]"}function Tp(e){return v({},e,{"data-precedence":e.precedence,precedence:null})}function B2(e,t,s,i){e.querySelector('link[rel="preload"][as="style"]['+t+"]")?i.loading=1:(t=e.createElement("link"),i.preload=t,t.addEventListener("load",function(){return i.loading|=1}),t.addEventListener("error",function(){return i.loading|=2}),xt(t,"link",s),ct(t),e.head.appendChild(t))}function Bs(e){return'[src="'+Qt(e)+'"]'}function Ql(e){return"script[async]"+e}function Ep(e,t,s){if(t.count++,t.instance===null)switch(t.type){case"style":var i=e.querySelector('style[data-href~="'+Qt(s.href)+'"]');if(i)return t.instance=i,ct(i),i;var u=v({},s,{"data-href":s.href,"data-precedence":s.precedence,href:null,precedence:null});return i=(e.ownerDocument||e).createElement("style"),ct(i),xt(i,"style",u),Ar(i,s.precedence,e),t.instance=i;case"stylesheet":u=Ls(s.href);var c=e.querySelector(Pl(u));if(c)return t.state.loading|=4,t.instance=c,ct(c),c;i=Tp(s),(u=ea.get(u))&&Hc(i,u),c=(e.ownerDocument||e).createElement("link"),ct(c);var x=c;return x._p=new Promise(function(b,j){x.onload=b,x.onerror=j}),xt(c,"link",i),t.state.loading|=4,Ar(c,s.precedence,e),t.instance=c;case"script":return c=Bs(s.src),(u=e.querySelector(Ql(c)))?(t.instance=u,ct(u),u):(i=s,(u=ea.get(c))&&(i=v({},s),Yc(i,u)),e=e.ownerDocument||e,u=e.createElement("script"),ct(u),xt(u,"link",i),e.head.appendChild(u),t.instance=u);case"void":return null;default:throw Error(r(443,t.type))}else t.type==="stylesheet"&&(t.state.loading&4)===0&&(i=t.instance,t.state.loading|=4,Ar(i,s.precedence,e));return t.instance}function Ar(e,t,s){for(var i=s.querySelectorAll('link[rel="stylesheet"][data-precedence],style[data-precedence]'),u=i.length?i[i.length-1]:null,c=u,x=0;x title"):null)}function H2(e,t,s){if(s===1||t.itemProp!=null)return!1;switch(e){case"meta":case"title":return!0;case"style":if(typeof t.precedence!="string"||typeof t.href!="string"||t.href==="")break;return!0;case"link":if(typeof t.rel!="string"||typeof t.href!="string"||t.href===""||t.onLoad||t.onError)break;switch(t.rel){case"stylesheet":return e=t.disabled,typeof t.precedence=="string"&&e==null;default:return!0}case"script":if(t.async&&typeof t.async!="function"&&typeof t.async!="symbol"&&!t.onLoad&&!t.onError&&t.src&&typeof t.src=="string")return!0}return!1}function Ap(e){return!(e.type==="stylesheet"&&(e.state.loading&3)===0)}function Y2(e,t,s,i){if(s.type==="stylesheet"&&(typeof i.media!="string"||matchMedia(i.media).matches!==!1)&&(s.state.loading&4)===0){if(s.instance===null){var u=Ls(i.href),c=t.querySelector(Pl(u));if(c){t=c._p,t!==null&&typeof t=="object"&&typeof t.then=="function"&&(e.count++,e=Rr.bind(e),t.then(e,e)),s.state.loading|=4,s.instance=c,ct(c);return}c=t.ownerDocument||t,i=Tp(i),(u=ea.get(u))&&Hc(i,u),c=c.createElement("link"),ct(c);var x=c;x._p=new Promise(function(b,j){x.onload=b,x.onerror=j}),xt(c,"link",i),s.instance=c}e.stylesheets===null&&(e.stylesheets=new Map),e.stylesheets.set(s,t),(t=s.state.preload)&&(s.state.loading&3)===0&&(e.count++,s=Rr.bind(e),t.addEventListener("load",s),t.addEventListener("error",s))}}var qc=0;function q2(e,t){return e.stylesheets&&e.count===0&&zr(e,e.stylesheets),0qc?50:800)+t);return e.unsuspend=s,function(){e.unsuspend=null,clearTimeout(i),clearTimeout(u)}}:null}function Rr(){if(this.count--,this.count===0&&(this.imgCount===0||!this.waitingForImages)){if(this.stylesheets)zr(this,this.stylesheets);else if(this.unsuspend){var e=this.unsuspend;this.unsuspend=null,e()}}}var Or=null;function zr(e,t){e.stylesheets=null,e.unsuspend!==null&&(e.count++,Or=new Map,t.forEach(G2,e),Or=null,Rr.call(e))}function G2(e,t){if(!(t.state.loading&4)){var s=Or.get(e);if(s)var i=s.get(null);else{s=new Map,Or.set(e,s);for(var u=e.querySelectorAll("link[data-precedence],style[data-precedence]"),c=0;c"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(a)}catch(n){console.error(n)}}return a(),$c.exports=lS(),$c.exports}var rS=iS();/** + * react-router v7.13.1 + * + * Copyright (c) Remix Software Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE.md file in the root directory of this source tree. + * + * @license MIT + */var eg="popstate";function tg(a){return typeof a=="object"&&a!=null&&"pathname"in a&&"search"in a&&"hash"in a&&"state"in a&&"key"in a}function oS(a={}){function n(r,o){var g;let f=(g=o.state)==null?void 0:g.masked,{pathname:h,search:m,hash:p}=f||r.location;return Td("",{pathname:h,search:m,hash:p},o.state&&o.state.usr||null,o.state&&o.state.key||"default",f?{pathname:r.location.pathname,search:r.location.search,hash:r.location.hash}:void 0)}function l(r,o){return typeof o=="string"?o:ci(o)}return cS(n,l,null,a)}function Qe(a,n){if(a===!1||a===null||typeof a>"u")throw new Error(n)}function na(a,n){if(!a){typeof console<"u"&&console.warn(n);try{throw new Error(n)}catch{}}}function uS(){return Math.random().toString(36).substring(2,10)}function ag(a,n){return{usr:a.state,key:a.key,idx:n,masked:a.unstable_mask?{pathname:a.pathname,search:a.search,hash:a.hash}:void 0}}function Td(a,n,l=null,r,o){return{pathname:typeof a=="string"?a:a.pathname,search:"",hash:"",...typeof n=="string"?Js(n):n,state:l,key:n&&n.key||r||uS(),unstable_mask:o}}function ci({pathname:a="/",search:n="",hash:l=""}){return n&&n!=="?"&&(a+=n.charAt(0)==="?"?n:"?"+n),l&&l!=="#"&&(a+=l.charAt(0)==="#"?l:"#"+l),a}function Js(a){let n={};if(a){let l=a.indexOf("#");l>=0&&(n.hash=a.substring(l),a=a.substring(0,l));let r=a.indexOf("?");r>=0&&(n.search=a.substring(r),a=a.substring(0,r)),a&&(n.pathname=a)}return n}function cS(a,n,l,r={}){let{window:o=document.defaultView,v5Compat:f=!1}=r,h=o.history,m="POP",p=null,g=y();g==null&&(g=0,h.replaceState({...h.state,idx:g},""));function y(){return(h.state||{idx:null}).idx}function v(){m="POP";let M=y(),A=M==null?null:M-g;g=M,p&&p({action:m,location:E.location,delta:A})}function S(M,A){m="PUSH";let z=tg(M)?M:Td(E.location,M,A);g=y()+1;let U=ag(z,g),Y=E.createHref(z.unstable_mask||z);try{h.pushState(U,"",Y)}catch(Q){if(Q instanceof DOMException&&Q.name==="DataCloneError")throw Q;o.location.assign(Y)}f&&p&&p({action:m,location:E.location,delta:1})}function k(M,A){m="REPLACE";let z=tg(M)?M:Td(E.location,M,A);g=y();let U=ag(z,g),Y=E.createHref(z.unstable_mask||z);h.replaceState(U,"",Y),f&&p&&p({action:m,location:E.location,delta:0})}function N(M){return dS(M)}let E={get action(){return m},get location(){return a(o,h)},listen(M){if(p)throw new Error("A history only accepts one active listener");return o.addEventListener(eg,v),p=M,()=>{o.removeEventListener(eg,v),p=null}},createHref(M){return n(o,M)},createURL:N,encodeLocation(M){let A=N(M);return{pathname:A.pathname,search:A.search,hash:A.hash}},push:S,replace:k,go(M){return h.go(M)}};return E}function dS(a,n=!1){let l="http://localhost";typeof window<"u"&&(l=window.location.origin!=="null"?window.location.origin:window.location.href),Qe(l,"No window.location.(origin|href) available to create URL");let r=typeof a=="string"?a:ci(a);return r=r.replace(/ $/,"%20"),!n&&r.startsWith("//")&&(r=l+r),new URL(r,l)}function Jx(a,n,l="/"){return fS(a,n,l,!1)}function fS(a,n,l,r){let o=typeof n=="string"?Js(n):n,f=qa(o.pathname||"/",l);if(f==null)return null;let h=$x(a);hS(h);let m=null;for(let p=0;m==null&&p{let y={relativePath:g===void 0?h.path||"":g,caseSensitive:h.caseSensitive===!0,childrenIndex:m,route:h};if(y.relativePath.startsWith("/")){if(!y.relativePath.startsWith(r)&&p)return;Qe(y.relativePath.startsWith(r),`Absolute route path "${y.relativePath}" nested under path "${r}" is not valid. An absolute child route path must start with the combined path of all its parent routes.`),y.relativePath=y.relativePath.slice(r.length)}let v=ya([r,y.relativePath]),S=l.concat(y);h.children&&h.children.length>0&&(Qe(h.index!==!0,`Index routes must not have child routes. Please remove all child routes from route path "${v}".`),$x(h.children,n,S,v,p)),!(h.path==null&&!h.index)&&n.push({path:v,score:bS(v,h.index),routesMeta:S})};return a.forEach((h,m)=>{var p;if(h.path===""||!((p=h.path)!=null&&p.includes("?")))f(h,m);else for(let g of Wx(h.path))f(h,m,!0,g)}),n}function Wx(a){let n=a.split("/");if(n.length===0)return[];let[l,...r]=n,o=l.endsWith("?"),f=l.replace(/\?$/,"");if(r.length===0)return o?[f,""]:[f];let h=Wx(r.join("/")),m=[];return m.push(...h.map(p=>p===""?f:[f,p].join("/"))),o&&m.push(...h),m.map(p=>a.startsWith("/")&&p===""?"/":p)}function hS(a){a.sort((n,l)=>n.score!==l.score?l.score-n.score:SS(n.routesMeta.map(r=>r.childrenIndex),l.routesMeta.map(r=>r.childrenIndex)))}var mS=/^:[\w-]+$/,pS=3,gS=2,xS=1,yS=10,vS=-2,ng=a=>a==="*";function bS(a,n){let l=a.split("/"),r=l.length;return l.some(ng)&&(r+=vS),n&&(r+=gS),l.filter(o=>!ng(o)).reduce((o,f)=>o+(mS.test(f)?pS:f===""?xS:yS),r)}function SS(a,n){return a.length===n.length&&a.slice(0,-1).every((r,o)=>r===n[o])?a[a.length-1]-n[n.length-1]:0}function wS(a,n,l=!1){let{routesMeta:r}=a,o={},f="/",h=[];for(let m=0;m{if(y==="*"){let N=m[S]||"";h=f.slice(0,f.length-N.length).replace(/(.)\/+$/,"$1")}const k=m[S];return v&&!k?g[y]=void 0:g[y]=(k||"").replace(/%2F/g,"/"),g},{}),pathname:f,pathnameBase:h,pattern:a}}function kS(a,n=!1,l=!0){na(a==="*"||!a.endsWith("*")||a.endsWith("/*"),`Route path "${a}" will be treated as if it were "${a.replace(/\*$/,"/*")}" because the \`*\` character must always follow a \`/\` in the pattern. To get rid of this warning, please change the route path to "${a.replace(/\*$/,"/*")}".`);let r=[],o="^"+a.replace(/\/*\*?$/,"").replace(/^\/*/,"/").replace(/[\\.*+^${}|()[\]]/g,"\\$&").replace(/\/:([\w-]+)(\?)?/g,(h,m,p,g,y)=>{if(r.push({paramName:m,isOptional:p!=null}),p){let v=y.charAt(g+h.length);return v&&v!=="/"?"/([^\\/]*)":"(?:/([^\\/]*))?"}return"/([^\\/]+)"}).replace(/\/([\w-]+)\?(\/|$)/g,"(/$1)?$2");return a.endsWith("*")?(r.push({paramName:"*"}),o+=a==="*"||a==="/*"?"(.*)$":"(?:\\/(.+)|\\/*)$"):l?o+="\\/*$":a!==""&&a!=="/"&&(o+="(?:(?=\\/|$))"),[new RegExp(o,n?void 0:"i"),r]}function jS(a){try{return a.split("/").map(n=>decodeURIComponent(n).replace(/\//g,"%2F")).join("/")}catch(n){return na(!1,`The URL path "${a}" could not be decoded because it is a malformed URL segment. This is probably due to a bad percent encoding (${n}).`),a}}function qa(a,n){if(n==="/")return a;if(!a.toLowerCase().startsWith(n.toLowerCase()))return null;let l=n.endsWith("/")?n.length-1:n.length,r=a.charAt(l);return r&&r!=="/"?null:a.slice(l)||"/"}var NS=/^(?:[a-z][a-z0-9+.-]*:|\/\/)/i;function TS(a,n="/"){let{pathname:l,search:r="",hash:o=""}=typeof a=="string"?Js(a):a,f;return l?(l=l.replace(/\/\/+/g,"/"),l.startsWith("/")?f=sg(l.substring(1),"/"):f=sg(l,n)):f=n,{pathname:f,search:MS(r),hash:AS(o)}}function sg(a,n){let l=n.replace(/\/+$/,"").split("/");return a.split("/").forEach(o=>{o===".."?l.length>1&&l.pop():o!=="."&&l.push(o)}),l.length>1?l.join("/"):"/"}function td(a,n,l,r){return`Cannot include a '${a}' character in a manually specified \`to.${n}\` field [${JSON.stringify(r)}]. Please separate it out to the \`to.${l}\` field. Alternatively you may provide the full path as a string in and the router will parse it for you.`}function ES(a){return a.filter((n,l)=>l===0||n.route.path&&n.route.path.length>0)}function sf(a){let n=ES(a);return n.map((l,r)=>r===n.length-1?l.pathname:l.pathnameBase)}function Co(a,n,l,r=!1){let o;typeof a=="string"?o=Js(a):(o={...a},Qe(!o.pathname||!o.pathname.includes("?"),td("?","pathname","search",o)),Qe(!o.pathname||!o.pathname.includes("#"),td("#","pathname","hash",o)),Qe(!o.search||!o.search.includes("#"),td("#","search","hash",o)));let f=a===""||o.pathname==="",h=f?"/":o.pathname,m;if(h==null)m=l;else{let v=n.length-1;if(!r&&h.startsWith("..")){let S=h.split("/");for(;S[0]==="..";)S.shift(),v-=1;o.pathname=S.join("/")}m=v>=0?n[v]:"/"}let p=TS(o,m),g=h&&h!=="/"&&h.endsWith("/"),y=(f||h===".")&&l.endsWith("/");return!p.pathname.endsWith("/")&&(g||y)&&(p.pathname+="/"),p}var ya=a=>a.join("/").replace(/\/\/+/g,"/"),CS=a=>a.replace(/\/+$/,"").replace(/^\/*/,"/"),MS=a=>!a||a==="?"?"":a.startsWith("?")?a:"?"+a,AS=a=>!a||a==="#"?"":a.startsWith("#")?a:"#"+a,DS=class{constructor(a,n,l,r=!1){this.status=a,this.statusText=n||"",this.internal=r,l instanceof Error?(this.data=l.toString(),this.error=l):this.data=l}};function RS(a){return a!=null&&typeof a.status=="number"&&typeof a.statusText=="string"&&typeof a.internal=="boolean"&&"data"in a}function OS(a){return a.map(n=>n.route.path).filter(Boolean).join("/").replace(/\/\/*/g,"/")||"/"}var Ix=typeof window<"u"&&typeof window.document<"u"&&typeof window.document.createElement<"u";function ey(a,n){let l=a;if(typeof l!="string"||!NS.test(l))return{absoluteURL:void 0,isExternal:!1,to:l};let r=l,o=!1;if(Ix)try{let f=new URL(window.location.href),h=l.startsWith("//")?new URL(f.protocol+l):new URL(l),m=qa(h.pathname,n);h.origin===f.origin&&m!=null?l=m+h.search+h.hash:o=!0}catch{na(!1,` contains an invalid URL which will probably break when clicked - please update to a valid URL path.`)}return{absoluteURL:r,isExternal:o,to:l}}Object.getOwnPropertyNames(Object.prototype).sort().join("\0");var ty=["POST","PUT","PATCH","DELETE"];new Set(ty);var zS=["GET",...ty];new Set(zS);var $s=w.createContext(null);$s.displayName="DataRouter";var Mo=w.createContext(null);Mo.displayName="DataRouterState";var _S=w.createContext(!1),ay=w.createContext({isTransitioning:!1});ay.displayName="ViewTransition";var VS=w.createContext(new Map);VS.displayName="Fetchers";var US=w.createContext(null);US.displayName="Await";var Xt=w.createContext(null);Xt.displayName="Navigation";var xi=w.createContext(null);xi.displayName="Location";var ca=w.createContext({outlet:null,matches:[],isDataRoute:!1});ca.displayName="Route";var lf=w.createContext(null);lf.displayName="RouteError";var ny="REACT_ROUTER_ERROR",LS="REDIRECT",BS="ROUTE_ERROR_RESPONSE";function HS(a){if(a.startsWith(`${ny}:${LS}:{`))try{let n=JSON.parse(a.slice(28));if(typeof n=="object"&&n&&typeof n.status=="number"&&typeof n.statusText=="string"&&typeof n.location=="string"&&typeof n.reloadDocument=="boolean"&&typeof n.replace=="boolean")return n}catch{}}function YS(a){if(a.startsWith(`${ny}:${BS}:{`))try{let n=JSON.parse(a.slice(40));if(typeof n=="object"&&n&&typeof n.status=="number"&&typeof n.statusText=="string")return new DS(n.status,n.statusText,n.data)}catch{}}function qS(a,{relative:n}={}){Qe(Ws(),"useHref() may be used only in the context of a component.");let{basename:l,navigator:r}=w.useContext(Xt),{hash:o,pathname:f,search:h}=vi(a,{relative:n}),m=f;return l!=="/"&&(m=f==="/"?l:ya([l,f])),r.createHref({pathname:m,search:h,hash:o})}function Ws(){return w.useContext(xi)!=null}function da(){return Qe(Ws(),"useLocation() may be used only in the context of a component."),w.useContext(xi).location}var sy="You should call navigate() in a React.useEffect(), not when your component is first rendered.";function ly(a){w.useContext(Xt).static||w.useLayoutEffect(a)}function yi(){let{isDataRoute:a}=w.useContext(ca);return a?nw():GS()}function GS(){Qe(Ws(),"useNavigate() may be used only in the context of a component.");let a=w.useContext($s),{basename:n,navigator:l}=w.useContext(Xt),{matches:r}=w.useContext(ca),{pathname:o}=da(),f=JSON.stringify(sf(r)),h=w.useRef(!1);return ly(()=>{h.current=!0}),w.useCallback((p,g={})=>{if(na(h.current,sy),!h.current)return;if(typeof p=="number"){l.go(p);return}let y=Co(p,JSON.parse(f),o,g.relative==="path");a==null&&n!=="/"&&(y.pathname=y.pathname==="/"?n:ya([n,y.pathname])),(g.replace?l.replace:l.push)(y,g.state,g)},[n,l,f,o,a])}var XS=w.createContext(null);function PS(a){let n=w.useContext(ca).outlet;return w.useMemo(()=>n&&w.createElement(XS.Provider,{value:a},n),[n,a])}function vi(a,{relative:n}={}){let{matches:l}=w.useContext(ca),{pathname:r}=da(),o=JSON.stringify(sf(l));return w.useMemo(()=>Co(a,JSON.parse(o),r,n==="path"),[a,o,r,n])}function QS(a,n){return iy(a,n)}function iy(a,n,l){var M;Qe(Ws(),"useRoutes() may be used only in the context of a component.");let{navigator:r}=w.useContext(Xt),{matches:o}=w.useContext(ca),f=o[o.length-1],h=f?f.params:{},m=f?f.pathname:"/",p=f?f.pathnameBase:"/",g=f&&f.route;{let A=g&&g.path||"";oy(m,!g||A.endsWith("*")||A.endsWith("*?"),`You rendered descendant (or called \`useRoutes()\`) at "${m}" (under ) but the parent route path has no trailing "*". This means if you navigate deeper, the parent won't match anymore and therefore the child routes will never render. + +Please change the parent to .`)}let y=da(),v;if(n){let A=typeof n=="string"?Js(n):n;Qe(p==="/"||((M=A.pathname)==null?void 0:M.startsWith(p)),`When overriding the location using \`\` or \`useRoutes(routes, location)\`, the location pathname must begin with the portion of the URL pathname that was matched by all parent routes. The current pathname base is "${p}" but pathname "${A.pathname}" was given in the \`location\` prop.`),v=A}else v=y;let S=v.pathname||"/",k=S;if(p!=="/"){let A=p.replace(/^\//,"").split("/");k="/"+S.replace(/^\//,"").split("/").slice(A.length).join("/")}let N=Jx(a,{pathname:k});na(g||N!=null,`No routes matched location "${v.pathname}${v.search}${v.hash}" `),na(N==null||N[N.length-1].route.element!==void 0||N[N.length-1].route.Component!==void 0||N[N.length-1].route.lazy!==void 0,`Matched leaf route at location "${v.pathname}${v.search}${v.hash}" does not have an element or Component. This means it will render an with a null value by default resulting in an "empty" page.`);let E=$S(N&&N.map(A=>Object.assign({},A,{params:Object.assign({},h,A.params),pathname:ya([p,r.encodeLocation?r.encodeLocation(A.pathname.replace(/\?/g,"%3F").replace(/#/g,"%23")).pathname:A.pathname]),pathnameBase:A.pathnameBase==="/"?p:ya([p,r.encodeLocation?r.encodeLocation(A.pathnameBase.replace(/\?/g,"%3F").replace(/#/g,"%23")).pathname:A.pathnameBase])})),o,l);return n&&E?w.createElement(xi.Provider,{value:{location:{pathname:"/",search:"",hash:"",state:null,key:"default",unstable_mask:void 0,...v},navigationType:"POP"}},E):E}function KS(){let a=aw(),n=RS(a)?`${a.status} ${a.statusText}`:a instanceof Error?a.message:JSON.stringify(a),l=a instanceof Error?a.stack:null,r="rgba(200,200,200, 0.5)",o={padding:"0.5rem",backgroundColor:r},f={padding:"2px 4px",backgroundColor:r},h=null;return console.error("Error handled by React Router default ErrorBoundary:",a),h=w.createElement(w.Fragment,null,w.createElement("p",null,"💿 Hey developer 👋"),w.createElement("p",null,"You can provide a way better UX than this when your app throws errors by providing your own ",w.createElement("code",{style:f},"ErrorBoundary")," or"," ",w.createElement("code",{style:f},"errorElement")," prop on your route.")),w.createElement(w.Fragment,null,w.createElement("h2",null,"Unexpected Application Error!"),w.createElement("h3",{style:{fontStyle:"italic"}},n),l?w.createElement("pre",{style:o},l):null,h)}var FS=w.createElement(KS,null),ry=class extends w.Component{constructor(a){super(a),this.state={location:a.location,revalidation:a.revalidation,error:a.error}}static getDerivedStateFromError(a){return{error:a}}static getDerivedStateFromProps(a,n){return n.location!==a.location||n.revalidation!=="idle"&&a.revalidation==="idle"?{error:a.error,location:a.location,revalidation:a.revalidation}:{error:a.error!==void 0?a.error:n.error,location:n.location,revalidation:a.revalidation||n.revalidation}}componentDidCatch(a,n){this.props.onError?this.props.onError(a,n):console.error("React Router caught the following error during render",a)}render(){let a=this.state.error;if(this.context&&typeof a=="object"&&a&&"digest"in a&&typeof a.digest=="string"){const l=YS(a.digest);l&&(a=l)}let n=a!==void 0?w.createElement(ca.Provider,{value:this.props.routeContext},w.createElement(lf.Provider,{value:a,children:this.props.component})):this.props.children;return this.context?w.createElement(ZS,{error:a},n):n}};ry.contextType=_S;var ad=new WeakMap;function ZS({children:a,error:n}){let{basename:l}=w.useContext(Xt);if(typeof n=="object"&&n&&"digest"in n&&typeof n.digest=="string"){let r=HS(n.digest);if(r){let o=ad.get(n);if(o)throw o;let f=ey(r.location,l);if(Ix&&!ad.get(n))if(f.isExternal||r.reloadDocument)window.location.href=f.absoluteURL||f.to;else{const h=Promise.resolve().then(()=>window.__reactRouterDataRouter.navigate(f.to,{replace:r.replace}));throw ad.set(n,h),h}return w.createElement("meta",{httpEquiv:"refresh",content:`0;url=${f.absoluteURL||f.to}`})}}return a}function JS({routeContext:a,match:n,children:l}){let r=w.useContext($s);return r&&r.static&&r.staticContext&&(n.route.errorElement||n.route.ErrorBoundary)&&(r.staticContext._deepestRenderedBoundaryId=n.route.id),w.createElement(ca.Provider,{value:a},l)}function $S(a,n=[],l){let r=l==null?void 0:l.state;if(a==null){if(!r)return null;if(r.errors)a=r.matches;else if(n.length===0&&!r.initialized&&r.matches.length>0)a=r.matches;else return null}let o=a,f=r==null?void 0:r.errors;if(f!=null){let y=o.findIndex(v=>v.route.id&&(f==null?void 0:f[v.route.id])!==void 0);Qe(y>=0,`Could not find a matching route for errors on route IDs: ${Object.keys(f).join(",")}`),o=o.slice(0,Math.min(o.length,y+1))}let h=!1,m=-1;if(l&&r){h=r.renderFallback;for(let y=0;y=0?o=o.slice(0,m+1):o=[o[0]];break}}}}let p=l==null?void 0:l.onError,g=r&&p?(y,v)=>{var S,k;p(y,{location:r.location,params:((k=(S=r.matches)==null?void 0:S[0])==null?void 0:k.params)??{},unstable_pattern:OS(r.matches),errorInfo:v})}:void 0;return o.reduceRight((y,v,S)=>{let k,N=!1,E=null,M=null;r&&(k=f&&v.route.id?f[v.route.id]:void 0,E=v.route.errorElement||FS,h&&(m<0&&S===0?(oy("route-fallback",!1,"No `HydrateFallback` element provided to render during initial hydration"),N=!0,M=null):m===S&&(N=!0,M=v.route.hydrateFallbackElement||null)));let A=n.concat(o.slice(0,S+1)),z=()=>{let U;return k?U=E:N?U=M:v.route.Component?U=w.createElement(v.route.Component,null):v.route.element?U=v.route.element:U=y,w.createElement(JS,{match:v,routeContext:{outlet:y,matches:A,isDataRoute:r!=null},children:U})};return r&&(v.route.ErrorBoundary||v.route.errorElement||S===0)?w.createElement(ry,{location:r.location,revalidation:r.revalidation,component:E,error:k,children:z(),routeContext:{outlet:null,matches:A,isDataRoute:!0},onError:g}):z()},null)}function rf(a){return`${a} must be used within a data router. See https://reactrouter.com/en/main/routers/picking-a-router.`}function WS(a){let n=w.useContext($s);return Qe(n,rf(a)),n}function IS(a){let n=w.useContext(Mo);return Qe(n,rf(a)),n}function ew(a){let n=w.useContext(ca);return Qe(n,rf(a)),n}function of(a){let n=ew(a),l=n.matches[n.matches.length-1];return Qe(l.route.id,`${a} can only be used on routes that contain a unique "id"`),l.route.id}function tw(){return of("useRouteId")}function aw(){var r;let a=w.useContext(lf),n=IS("useRouteError"),l=of("useRouteError");return a!==void 0?a:(r=n.errors)==null?void 0:r[l]}function nw(){let{router:a}=WS("useNavigate"),n=of("useNavigate"),l=w.useRef(!1);return ly(()=>{l.current=!0}),w.useCallback(async(o,f={})=>{na(l.current,sy),l.current&&(typeof o=="number"?await a.navigate(o):await a.navigate(o,{fromRouteId:n,...f}))},[a,n])}var lg={};function oy(a,n,l){!n&&!lg[a]&&(lg[a]=!0,na(!1,l))}w.memo(sw);function sw({routes:a,future:n,state:l,isStatic:r,onError:o}){return iy(a,void 0,{state:l,isStatic:r,onError:o})}function uy({to:a,replace:n,state:l,relative:r}){Qe(Ws()," may be used only in the context of a component.");let{static:o}=w.useContext(Xt);na(!o," must not be used on the initial render in a . This is a no-op, but you should modify your code so the is only ever rendered in response to some user interaction or state change.");let{matches:f}=w.useContext(ca),{pathname:h}=da(),m=yi(),p=Co(a,sf(f),h,r==="path"),g=JSON.stringify(p);return w.useEffect(()=>{m(JSON.parse(g),{replace:n,state:l,relative:r})},[m,g,r,n,l]),null}function lw(a){return PS(a.context)}function yn(a){Qe(!1,"A is only ever to be used as the child of element, never rendered directly. Please wrap your in a .")}function iw({basename:a="/",children:n=null,location:l,navigationType:r="POP",navigator:o,static:f=!1,unstable_useTransitions:h}){Qe(!Ws(),"You cannot render a inside another . You should never have more than one in your app.");let m=a.replace(/^\/*/,"/"),p=w.useMemo(()=>({basename:m,navigator:o,static:f,unstable_useTransitions:h,future:{}}),[m,o,f,h]);typeof l=="string"&&(l=Js(l));let{pathname:g="/",search:y="",hash:v="",state:S=null,key:k="default",unstable_mask:N}=l,E=w.useMemo(()=>{let M=qa(g,m);return M==null?null:{location:{pathname:M,search:y,hash:v,state:S,key:k,unstable_mask:N},navigationType:r}},[m,g,y,v,S,k,r,N]);return na(E!=null,` is not able to match the URL "${g}${y}${v}" because it does not start with the basename, so the won't render anything.`),E==null?null:w.createElement(Xt.Provider,{value:p},w.createElement(xi.Provider,{children:n,value:E}))}function rw({children:a,location:n}){return QS(Ed(a),n)}function Ed(a,n=[]){let l=[];return w.Children.forEach(a,(r,o)=>{if(!w.isValidElement(r))return;let f=[...n,o];if(r.type===w.Fragment){l.push.apply(l,Ed(r.props.children,f));return}Qe(r.type===yn,`[${typeof r.type=="string"?r.type:r.type.name}] is not a component. All component children of must be a or `),Qe(!r.props.index||!r.props.children,"An index route cannot have child routes.");let h={id:r.props.id||f.join("-"),caseSensitive:r.props.caseSensitive,element:r.props.element,Component:r.props.Component,index:r.props.index,path:r.props.path,middleware:r.props.middleware,loader:r.props.loader,action:r.props.action,hydrateFallbackElement:r.props.hydrateFallbackElement,HydrateFallback:r.props.HydrateFallback,errorElement:r.props.errorElement,ErrorBoundary:r.props.ErrorBoundary,hasErrorBoundary:r.props.hasErrorBoundary===!0||r.props.ErrorBoundary!=null||r.props.errorElement!=null,shouldRevalidate:r.props.shouldRevalidate,handle:r.props.handle,lazy:r.props.lazy};r.props.children&&(h.children=Ed(r.props.children,f)),l.push(h)}),l}var eo="get",to="application/x-www-form-urlencoded";function Ao(a){return typeof HTMLElement<"u"&&a instanceof HTMLElement}function ow(a){return Ao(a)&&a.tagName.toLowerCase()==="button"}function uw(a){return Ao(a)&&a.tagName.toLowerCase()==="form"}function cw(a){return Ao(a)&&a.tagName.toLowerCase()==="input"}function dw(a){return!!(a.metaKey||a.altKey||a.ctrlKey||a.shiftKey)}function fw(a,n){return a.button===0&&(!n||n==="_self")&&!dw(a)}function Cd(a=""){return new URLSearchParams(typeof a=="string"||Array.isArray(a)||a instanceof URLSearchParams?a:Object.keys(a).reduce((n,l)=>{let r=a[l];return n.concat(Array.isArray(r)?r.map(o=>[l,o]):[[l,r]])},[]))}function hw(a,n){let l=Cd(a);return n&&n.forEach((r,o)=>{l.has(o)||n.getAll(o).forEach(f=>{l.append(o,f)})}),l}var qr=null;function mw(){if(qr===null)try{new FormData(document.createElement("form"),0),qr=!1}catch{qr=!0}return qr}var pw=new Set(["application/x-www-form-urlencoded","multipart/form-data","text/plain"]);function nd(a){return a!=null&&!pw.has(a)?(na(!1,`"${a}" is not a valid \`encType\` for \`
\`/\`\` and will default to "${to}"`),null):a}function gw(a,n){let l,r,o,f,h;if(uw(a)){let m=a.getAttribute("action");r=m?qa(m,n):null,l=a.getAttribute("method")||eo,o=nd(a.getAttribute("enctype"))||to,f=new FormData(a)}else if(ow(a)||cw(a)&&(a.type==="submit"||a.type==="image")){let m=a.form;if(m==null)throw new Error('Cannot submit a + ) : null}

最新动态

- {[ - { title: "商机阶段更新", desc: "D 项目已推进至方案交流阶段", time: "10分钟前" }, - { title: "日报已点评", desc: "主管对你昨天的日报给出了 95 分", time: "2小时前" }, - { title: "新渠道录入", desc: "成功录入 E 渠道商信息", time: "昨天" }, - ].map((news, i) => ( -
+ {visibleActivities.map((news: DashboardActivity, i: number) => ( +
-

{news.title}

-

{news.desc}

-

{news.time}

+

{news.title || "无"}

+

{news.content || "无"}

+

{news.timeText || "无"}

))}
+ {hasMoreActivities && !showAllActivities ? ( + + ) : null}
diff --git a/frontend/src/pages/Expansion.tsx b/frontend/src/pages/Expansion.tsx index f87aeec..6d63099 100644 --- a/frontend/src/pages/Expansion.tsx +++ b/frontend/src/pages/Expansion.tsx @@ -1,131 +1,633 @@ -import { useState } from "react"; +import { useEffect, useState, type ReactNode } from "react"; import { Search, Plus, MapPin, Building2, User, Phone, X, Clock, FileText, Mail, Calendar } from "lucide-react"; import { motion, AnimatePresence } from "motion/react"; +import { + createChannelExpansion, + createExpansionFollowUp, + createSalesExpansion, + getExpansionMeta, + getExpansionOverview, + updateChannelExpansion, + updateSalesExpansion, + type ChannelExpansionItem, + type CreateChannelExpansionPayload, + type CreateExpansionFollowUpPayload, + type CreateSalesExpansionPayload, + type DepartmentOption, + type ExpansionFollowUp, + type SalesExpansionItem, +} from "@/lib/auth"; + +type ExpansionItem = SalesExpansionItem | ChannelExpansionItem; +type ExpansionTab = "sales" | "channel"; + +const defaultSalesForm: CreateSalesExpansionPayload = { + candidateName: "", + mobile: "", + email: "", + industry: "", + title: "", + intentLevel: "medium", + stage: "initial_contact", + hasDesktopExp: false, + inProgress: true, + employmentStatus: "active", + expectedJoinDate: "", + remark: "", +}; + +const defaultChannelForm: CreateChannelExpansionPayload = { + channelName: "", + province: "", + industry: "", + contactName: "", + contactTitle: "", + contactMobile: "", + stage: "initial_contact", + landedFlag: false, + expectedSignDate: "", + remark: "", +}; + +function toDateTimeLocalValue(date = new Date()) { + const timezoneOffset = date.getTimezoneOffset() * 60000; + return new Date(date.getTime() - timezoneOffset).toISOString().slice(0, 16); +} + +const defaultFollowUpForm: CreateExpansionFollowUpPayload = { + followUpType: "电话沟通", + content: "", + nextAction: "", + followUpTime: toDateTimeLocalValue(), +}; + +function ModalShell({ + title, + subtitle, + onClose, + children, + footer, +}: { + title: string; + subtitle: string; + onClose: () => void; + children: ReactNode; + footer: ReactNode; +}) { + return ( + <> + + +
+
+
+
+

{title}

+

{subtitle}

+
+ +
+
{children}
+
{footer}
+
+
+
+ + ); +} export default function Expansion() { - const [activeTab, setActiveTab] = useState<"sales" | "channel">("sales"); - const [selectedItem, setSelectedItem] = useState(null); + const [activeTab, setActiveTab] = useState("sales"); + const [selectedItem, setSelectedItem] = useState(null); + const [keyword, setKeyword] = useState(""); + const [salesData, setSalesData] = useState([]); + const [channelData, setChannelData] = useState([]); + const [departments, setDepartments] = useState([]); + const [refreshTick, setRefreshTick] = useState(0); - const salesData = [ - { - id: 1, type: "sales", name: "李四", phone: "13812345678", email: "lisi@example.com", - dept: "华东大区", industry: "教育", title: "高级销售", intent: "高", stage: "初步沟通", - hasExp: true, inProgress: true, active: true, expectedJoinDate: "2024-05-01", - notes: "候选人对提成机制比较关注,在教育行业有5年以上的客户资源积累。" - }, - { - id: 2, type: "sales", name: "王五", phone: "13987654321", email: "wangwu@example.com", - dept: "华北大区", industry: "医疗", title: "销售经理", intent: "中", stage: "方案交流", - hasExp: false, inProgress: false, active: true, expectedJoinDate: "待定", - notes: "需要进一步沟通产品线细节,目前在看其他几家竞品的机会。" - }, - ]; + const [createOpen, setCreateOpen] = useState(false); + const [editOpen, setEditOpen] = useState(false); + const [followUpOpen, setFollowUpOpen] = useState(false); + const [submitting, setSubmitting] = useState(false); + const [createError, setCreateError] = useState(""); + const [editError, setEditError] = useState(""); + const [followUpError, setFollowUpError] = useState(""); - const channelData = [ - { - id: 1, type: "channel", name: "某某科技代理商", province: "浙江", industry: "政府", - revenue: "500万", size: 50, contact: "张总", contactTitle: "总经理", phone: "13800138000", - stage: "合作洽谈", landed: true, expectedSignDate: "2024-04-15", - notes: "对方在政务云桌面领域有深厚资源,希望能拿到省级独家代理权。" - }, - { - id: 2, type: "channel", name: "云端服务提供商", province: "江苏", industry: "金融", - revenue: "1000万", size: 120, contact: "李总", contactTitle: "业务总监", phone: "13900139000", - stage: "初步接触", landed: false, expectedSignDate: "待定", - notes: "初步接触,对方正在评估多家供应商,对我们的售后响应速度有较高要求。" - }, - ]; + const [salesForm, setSalesForm] = useState(defaultSalesForm); + const [channelForm, setChannelForm] = useState(defaultChannelForm); + const [editSalesForm, setEditSalesForm] = useState(defaultSalesForm); + const [editChannelForm, setEditChannelForm] = useState(defaultChannelForm); + const [followUpForm, setFollowUpForm] = useState(defaultFollowUpForm); + const hasForegroundModal = createOpen || editOpen || followUpOpen; - const followUpRecords = [ - { id: 1, date: "2024-03-15 14:30", type: "电话沟通", content: "初步沟通了合作意向,对方对我们的产品比较感兴趣,约定下周进行详细的产品演示。", user: "张三" }, - { id: 2, date: "2024-03-10 10:00", type: "微信沟通", content: "发送了公司介绍和产品白皮书,对方表示会内部评估。", user: "张三" }, - ]; + useEffect(() => { + let cancelled = false; + + async function loadMeta() { + try { + const data = await getExpansionMeta(); + if (!cancelled) { + setDepartments(data.departments ?? []); + } + } catch { + if (!cancelled) { + setDepartments([]); + } + } + } + + void loadMeta(); + + return () => { + cancelled = true; + }; + }, []); + + useEffect(() => { + let cancelled = false; + + async function loadExpansionData() { + try { + const data = await getExpansionOverview(keyword); + if (cancelled) { + return; + } + + setSalesData(data.salesItems ?? []); + setChannelData(data.channelItems ?? []); + setSelectedItem(null); + } catch { + if (!cancelled) { + setSalesData([]); + setChannelData([]); + setSelectedItem(null); + } + } + } + + void loadExpansionData(); + + return () => { + cancelled = true; + }; + }, [keyword, refreshTick]); + + const followUpRecords: ExpansionFollowUp[] = selectedItem?.followUps ?? []; + + const handleSalesChange = (key: K, value: CreateSalesExpansionPayload[K]) => { + setSalesForm((current) => ({ ...current, [key]: value })); + }; + + const handleChannelChange = (key: K, value: CreateChannelExpansionPayload[K]) => { + setChannelForm((current) => ({ ...current, [key]: value })); + }; + + const handleEditSalesChange = (key: K, value: CreateSalesExpansionPayload[K]) => { + setEditSalesForm((current) => ({ ...current, [key]: value })); + }; + + const handleEditChannelChange = (key: K, value: CreateChannelExpansionPayload[K]) => { + setEditChannelForm((current) => ({ ...current, [key]: value })); + }; + + const handleFollowUpChange = (key: K, value: CreateExpansionFollowUpPayload[K]) => { + setFollowUpForm((current) => ({ ...current, [key]: value })); + }; + + const resetCreateState = () => { + setCreateOpen(false); + setCreateError(""); + setSalesForm(defaultSalesForm); + setChannelForm(defaultChannelForm); + }; + + const resetEditState = () => { + setEditOpen(false); + setEditError(""); + setEditSalesForm(defaultSalesForm); + setEditChannelForm(defaultChannelForm); + }; + + const resetFollowUpState = () => { + setFollowUpOpen(false); + setFollowUpError(""); + setFollowUpForm({ + ...defaultFollowUpForm, + followUpTime: toDateTimeLocalValue(), + }); + }; + + const handleOpenCreate = () => { + setCreateError(""); + setCreateOpen(true); + }; + + const handleOpenEdit = () => { + if (!selectedItem) { + return; + } + + setEditError(""); + if (selectedItem.type === "sales") { + setEditSalesForm({ + candidateName: selectedItem.name ?? "", + mobile: selectedItem.phone === "无" ? "" : selectedItem.phone ?? "", + email: selectedItem.email === "无" ? "" : selectedItem.email ?? "", + targetDeptId: selectedItem.targetDeptId, + industry: selectedItem.industry === "无" ? "" : selectedItem.industry ?? "", + title: selectedItem.title === "无" ? "" : selectedItem.title ?? "", + intentLevel: selectedItem.intentLevel ?? "medium", + stage: selectedItem.stageCode ?? "initial_contact", + hasDesktopExp: Boolean(selectedItem.hasExp), + inProgress: Boolean(selectedItem.inProgress), + employmentStatus: selectedItem.employmentStatus ?? "active", + expectedJoinDate: selectedItem.expectedJoinDate === "无" ? "" : selectedItem.expectedJoinDate ?? "", + remark: selectedItem.notes === "无" ? "" : selectedItem.notes ?? "", + }); + } else { + setEditChannelForm({ + channelName: selectedItem.name ?? "", + province: selectedItem.province === "无" ? "" : selectedItem.province ?? "", + industry: selectedItem.industry === "无" ? "" : selectedItem.industry ?? "", + annualRevenue: selectedItem.annualRevenue ? Number(selectedItem.annualRevenue) : undefined, + staffSize: selectedItem.size ?? undefined, + contactName: selectedItem.contact === "无" ? "" : selectedItem.contact ?? "", + contactTitle: selectedItem.contactTitle === "无" ? "" : selectedItem.contactTitle ?? "", + contactMobile: selectedItem.phone === "无" ? "" : selectedItem.phone ?? "", + stage: selectedItem.stageCode ?? "initial_contact", + landedFlag: Boolean(selectedItem.landed), + expectedSignDate: selectedItem.expectedSignDate === "无" ? "" : selectedItem.expectedSignDate ?? "", + remark: selectedItem.notes === "无" ? "" : selectedItem.notes ?? "", + }); + } + setEditOpen(true); + }; + + const handleOpenFollowUp = () => { + if (!selectedItem) { + return; + } + setFollowUpError(""); + setFollowUpForm({ + followUpType: "电话沟通", + content: "", + nextAction: "", + followUpTime: toDateTimeLocalValue(), + }); + setFollowUpOpen(true); + }; + + const handleCreateSubmit = async () => { + if (submitting) { + return; + } + + setSubmitting(true); + setCreateError(""); + + try { + if (activeTab === "sales") { + await createSalesExpansion({ + ...salesForm, + expectedJoinDate: salesForm.expectedJoinDate || undefined, + targetDeptId: salesForm.targetDeptId || undefined, + }); + } else { + await createChannelExpansion({ + ...channelForm, + annualRevenue: channelForm.annualRevenue || undefined, + staffSize: channelForm.staffSize || undefined, + expectedSignDate: channelForm.expectedSignDate || undefined, + }); + } + + resetCreateState(); + setRefreshTick((current) => current + 1); + } catch (error) { + setCreateError(error instanceof Error ? error.message : "新增失败"); + } finally { + setSubmitting(false); + } + }; + + const handleEditSubmit = async () => { + if (!selectedItem || submitting) { + return; + } + + setSubmitting(true); + setEditError(""); + + try { + if (selectedItem.type === "sales") { + await updateSalesExpansion(selectedItem.id, { + ...editSalesForm, + expectedJoinDate: editSalesForm.expectedJoinDate || undefined, + targetDeptId: editSalesForm.targetDeptId || undefined, + }); + } else { + await updateChannelExpansion(selectedItem.id, { + ...editChannelForm, + annualRevenue: editChannelForm.annualRevenue || undefined, + staffSize: editChannelForm.staffSize || undefined, + expectedSignDate: editChannelForm.expectedSignDate || undefined, + }); + } + + resetEditState(); + setSelectedItem(null); + setRefreshTick((current) => current + 1); + } catch (error) { + setEditError(error instanceof Error ? error.message : "编辑失败"); + } finally { + setSubmitting(false); + } + }; + + const handleFollowUpSubmit = async () => { + if (!selectedItem || submitting) { + return; + } + + setSubmitting(true); + setFollowUpError(""); + + try { + await createExpansionFollowUp(selectedItem.type, selectedItem.id, { + ...followUpForm, + nextAction: followUpForm.nextAction || undefined, + followUpTime: new Date(followUpForm.followUpTime).toISOString(), + }); + + resetFollowUpState(); + setSelectedItem(null); + setRefreshTick((current) => current + 1); + } catch (error) { + setFollowUpError(error instanceof Error ? error.message : "新增跟进失败"); + } finally { + setSubmitting(false); + } + }; + + const renderEmpty = () => ( +
+ 暂无 +
+ ); + + const handleTabChange = (tab: ExpansionTab) => { + setActiveTab(tab); + setSelectedItem(null); + }; + + const renderSalesForm = ( + form: CreateSalesExpansionPayload, + onChange: (key: K, value: CreateSalesExpansionPayload[K]) => void, + ) => ( +
+ + + + + + + + + + + + +