commit 820579e98dc3c46baf219464dffdf087152faeba Author: freedakgmail Date: Thu Jul 23 12:34:43 2026 +0800 init: AI HR Compliance Assistant diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ec4d0f6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +dist/ +.env +*.local +.DS_Store diff --git a/0-req.md b/0-req.md new file mode 100644 index 0000000..5eaf18a --- /dev/null +++ b/0-req.md @@ -0,0 +1,1387 @@ +# 劳动用工合规助手 — 需求规格说明书 + +> **文档编号**: 0-req.md +> **版本**: v2.0 +> **日期**: 2026-07-23 +> **状态**: 草案 +> **修订**: v2.0 — 面向中小企业优化 UI/UX 与交互逻辑,强调极简易用 +> **修订**: v3.0 — 升级为 SaaS 多租户架构,支持注册登录/多用户/云端存储 + +--- + +## 1. 背景与问题分析 + +### 1.1 劳动仲裁高发领域 + +根据司法大数据及各地仲裁委公开统计,劳动争议仲裁案件集中在以下三大领域,占比超过 **85%**: + +| 领域 | 典型争议点 | 占比(约) | +|------|-----------|-----------| +| **劳动合同签订** | 未签书面合同(双倍工资)、逾期签订、到期未续签、违法约定试用期 | 30% | +| **工资与加班费** | 拖欠/克扣工资、加班费计算基数错误、未支付加班费、未足额支付 | 35% | +| **解聘/离职** | 违法解除劳动合同、未支付经济补偿金/赔偿金、程序不合规、未提前通知 | 20% | + +### 1.2 中小企业特殊痛点 + +- **没有专职 HR**:很多中小企业由老板或行政兼任,完全不懂劳动法 +- **Excel 都嫌麻烦**:现有管理方式靠记忆和纸质文件,连 Excel 都没用好 +- **请不起法律顾问**:遇到问题不知道找谁,也不知道合不合规 +- **系统太复杂学不会**:市面上的 HR 系统功能太多,上手成本高,中小企业用不起来 +- **怕出事但不知道怎么防**:知道劳动仲裁麻烦,但不知道从哪里开始预防 + +### 1.3 系统定位 + +构建一个 **中小企业也能用起来的极简用工合规工具**: + +1. **像计算器一样简单** — 打开就能用,不需要培训 +2. **像导航一样引导** — 该做什么、怎么做,一步步引导 +3. **像警报器一样提醒** — 有风险自动弹窗,不怕遗漏 +4. **像顾问一样解释** — 每个风险都附上法律依据和操作建议,不用自己查法条 + +--- + +## 2. 目标用户 + +**核心用户**:中小企业老板 / 行政兼 HR / 初级 HR + +| 用户画像 | 典型场景 | +|---------|---------| +| **老板兼管 HR**(10-30人企业) | 偶尔打开看看有没有风险,解聘时算一下补偿金 | +| **行政兼 HR**(30-80人企业) | 每天花5分钟看看待办,合同到期前续签,每月算加班费 | +| **初级 HR**(80-200人企业) | 日常合同管理、工资计算、解聘流程走查 | + +**用户特征假设**: +- 不懂劳动法,需要系统主动告知合规要求 +- 不愿看长文,偏好"告诉我该做什么" +- 可能第一次用类似系统,需要新手引导 +- 手机/平板也会用,不能只考虑桌面端 + +--- + +## 3. UI/UX 设计原则 + +### 3.1 核心原则:三看三不用 + +| 原则 | 含义 | 设计要求 | +|------|------|---------| +| **看一眼就懂** | 不需要说明书 | 每个页面标题用大白话,不用法律术语 | +| **看一眼会点** | 知道下一步做什么 | 每个页面有且只有一个主操作按钮(蓝色高亮) | +| **看一眼放心** | 知道当前状态 | 用红/黄/绿三色信号灯表示风险状态 | +| **不用记法条** | 系统自动判断合规 | 风险自动检测,附人话版法律解释 | +| **不用手算** | 系统自动计算 | 输入最少信息,自动算出结果 | +| **不用怕漏掉** | 系统主动提醒 | 到期/风险自动弹窗 + 角标提醒 | + +### 3.2 交互设计规范 + +**信息层级 — 渐进式披露**: +``` +第一眼:看到什么 → 数字 + 信号灯(绿/黄/红) +第二眼:想知道为什么 → 点击展开风险说明 +第三眼:想知道怎么办 → 展开操作建议 + 法律依据 +``` + +**操作流 — 一屏一焦点**: +- 每个页面只做一件事,不堆砌功能 +- 主操作按钮固定在视觉焦点位置(右上角或底部居中) +- 次要操作收起在"更多"菜单中 +- 表单分步填写,一屏不超过 5 个输入项 + +**视觉语言 — 信号灯体系**: + +| 颜色 | 含义 | 使用场景 | +|------|------|---------| +| 🟢 绿色 | 合规/正常 | 合同在签、工资正常、无风险 | +| 🟡 黄色 | 需关注 | 合同即将到期、加班超时、待处理 | +| 🔴 红色 | 有风险 | 未签合同、到期未续签、违法解聘 | +| ⚪ 灰色 | 不适用 | 离职员工、已完成事项 | + +**文案风格 — 说人话**: + +| ❌ 法律术语 | ✅ 人话版本 | +|------------|-----------| +| 「依据《劳动合同法》第82条」 | 「入职1个月没签合同,员工可以要求双倍工资」 | +| 「经济补偿金N」 | 「需要赔 X 个月工资,共 ¥XX,XXX」 | +| 「非过失性解除」 | 「员工没犯错但要辞退」 | +| 「法定节假日300%」 | 「国庆节加班1天 = 平时3天工资」 | + +### 3.3 新手引导 + +- **首次打开**:3 步引导弹窗("这里看风险"→"这里管合同"→"这里算钱") +- **空数据状态**:展示示例截图 + "添加第一个员工"按钮,不让用户面对空白页 +- **关键操作前**:简短提示卡片(如解聘前提示"建议先咨询律师") + +--- + +## 4. 功能需求 + +### 4.1 模块总览 + +#### 当前版本(v1.0 — 劳动合规基础 + AI 顾问 + 员工参与) + +**管理端(HR/老板使用)**: +``` +┌──────────────────────────────────────────────────────────────┐ +│ 劳动用工合规助手(管理端) │ +├───────────┬──────────┬──────────┬──────────┬────────────┬─────┤ +│ 风险总览 │ 合同管理 │ 钱的计算 │ 解聘助手 │ AI合规顾问 │设置 │ +│ (首页) │ │ │ │ │ │ +│ 红黄绿信号灯│ 员工合同 │ 加班费 │ 补偿金计算│ 智能问答 │企业 │ +│ 待办清单 │ 到期提醒 │ 双倍工资 │ 合规检查 │ 风险预测 │用户 │ +│ 一句话风险 │ 一键续签 │ 经济补偿 │ 流程引导 │ 合同审查 │套餐 │ +│ AI风险预测 │纸质/电子 │ │ │ 案例匹配 │ │ +└───────────┴──────────┴──────────┴──────────┴────────────┴─────┘ + ↓ 所有风险汇总到首页 ↓ +``` + +**员工端(员工使用,独立入口)**: +``` +┌──────────────────────────────────────────────┐ +│ 员工端(手机号验证码登录) │ +├──────────┬──────────┬──────────────────────┤ +│ 工资条 │ 我的合同 │ 入职填报/合同确认 │ +│ │ │ │ +│ 月度工资 │ 合同信息 │ 扫码填报基本信息 │ +│ 加班明细 │ 签订记录 │ 电子合同签署确认 │ +│ 确认已阅 │ 下载查看 │ HR审核后入库 │ +└──────────┴──────────┴──────────────────────┘ +``` + +#### 演进路线图 + +``` +v1.0 (当前) v2.0 (近期) v3.0 (中期) +┌───────────────────────────┐ ┌────────────────┐ ┌──────────────────┐ +│ 劳动合规基础 + AI + 员工参与 │ ──→ │ + 社保公积金 │ ──→ │ + 人力成本分析 │ +│ 合同/工资/解聘 │ │ 五险一金计算 │ │ 成本报表/趋势 │ +│ AI 问答/风险预测/审查 │ │ 缴费基数/比例 │ │ 部门成本/人均 │ +│ 员工端:工资条/合同/入职填报 │ │ │ │ │ +└───────────────────────────┘ └────────────────┘ └──────────────────┘ + +v4.0 (远期) +┌────────────────┐ +│ + 员工自助门户 │ +│ 考勤/请假/调薪 │ +│ 在线审批流程 │ +└────────────────┘ +``` + +**精简策略**: +- v1.0 聚焦劳动仲裁三大高发领域 + AI 合规顾问 + 员工参与,管理端 5 个页面 + 员工端 3 个页面 +- 架构上预留扩展接口,后续模块即插即用,不重构现有代码 +- 每个页面最多 2 层深度,避免复杂导航 + +--- + +### 4.2 首页:风险总览(极简版) + +**目标**:打开就知道有没有事、该做什么 + +**页面布局**: +``` +┌─────────────────────────────────────────────┐ +│ 👋 早上好!今天有 3 件事需要处理 │ +│ │ +│ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ │ +│ │ 12 │ │ 🔴 2 │ │ 🟡 3 │ │ ¥8k │ │ +│ │ 员工 │ │ 高风险│ │ 待办 │ │加班费│ │ +│ └──────┘ └──────┘ └──────┘ └──────┘ │ +│ │ +│ 📋 今日待办 │ +│ ┌─────────────────────────────────────┐ │ +│ │ 🔴 张三入职35天未签合同 → 去处理 │ │ +│ │ 🟡 李四合同还有20天到期 → 去续签 │ │ +│ │ 🟡 王五上月加班48小时超标 → 查看 │ │ +│ └─────────────────────────────────────┘ │ +│ │ +│ 📊 风险分布 │ +│ ┌─────────────────────────────────────┐ │ +│ │ 合同风险 ████████ 5项 │ │ +│ │ 工资风险 ████ 2项 │ │ +│ │ 解聘风险 ██ 1项 │ │ +│ └─────────────────────────────────────┘ │ +└─────────────────────────────────────────────┘ +``` + +**设计要点**: +- 顶部一句话总结:「今天有 X 件事需要处理」,让用户立刻知道是否有事 +- 4 个数字卡片:员工总数 / 高风险数 / 待办数 / 本月加班费 +- 待办列表:每条一行,红/黄信号灯 + 一句话描述 + 「去处理」按钮 +- 风险分布:用简单进度条代替复杂图表,一眼看出哪类问题多 +- **无风险时**:显示绿色大勾「✅ 暂无风险,继续保持!」 +- **首次使用空状态**:展示「添加第一个员工」引导卡片 + +--- + +### 4.3 合同管理 + +**目标**:不用记日期,不怕忘签,风险自动提醒 + +#### 4.3.1 员工合同列表(极简表格) + +**列表只显示 5 列**(避免信息过载): + +| 姓名 | 部门 | 入职日期 | 合同状态 | 操作 | +|------|------|---------|---------|------| +| 张三 | 技术部 | 2026-06-18 | 🔴 未签合同(35天) | 签订 / 详情 | +| 李四 | 销售部 | 2025-08-01 | 🟡 即将到期(20天) | 续签 / 详情 | +| 王五 | 财务部 | 2024-03-15 | 🟢 正常 | 详情 | + +- 状态列用信号灯 + 简短文字,一眼看出问题 +- 点击「签订」/「续签」直接弹出表单,不需要进详情页 +- 搜索框 + 部门筛选,支持快速查找 +- **批量操作**:全选 → 批量续签(适合到期合同多的情况) + +#### 4.3.2 添加/编辑员工(分步表单) + +**Step 1 — 基本信息(4 项)**: +``` +姓名 *: [___________] +部门 *: [下拉选择___] +入职日期 *:[日期选择器__] +月工资 *: [___________] 元 +``` + +**Step 2 — 合同信息(5 项,可跳过)**: +``` +合同类型: ○ 固定期限 ○ 无固定期限 ○ 暂未签订 +签订方式: ○ 纸质合同 ○ 电子合同 +签订日期: [日期选择器__] +合同期限: [下拉:1年/2年/3年/无固定] +试用期: [下拉:无/1个月/2个月/3个月/6个月] +``` + +- 如果选「暂未签订」,系统自动标记为高风险并提醒 +- 试用期下拉自动校验合法性(选了1年合同但选6个月试用期 → 红色提示) +- **智能默认值**:合同期限默认3年,试用期默认1个月,减少选择成本 +- **必填项最小化**:只有姓名和入职日期是必填,其他都可以后补 + +**签订方式说明**: + +| 方式 | 说明 | 系统支持 | +|------|------|---------| +| **纸质合同** | 打印后双方线下签字盖章 | 记录签订日期 + 可上传扫描件/拍照存档 | +| **电子合同** | 在线签署电子合同(如通过第三方电子签平台) | 记录签订日期 + 可填写电子合同链接/编号 | + +- 选择「纸质合同」时,显示「上传扫描件」按钮(可选),支持拍照或选择文件上传 +- 选择「电子合同」时,显示「电子合同编号」输入框(可选)和「合同链接」输入框(可选) +- 两种方式法律效力等同,系统不强制选择,默认为「纸质合同」 + +#### 4.3.3 合规风险检查规则(后台自动运行) + +| 规则 | 检查内容 | 信号灯 | 人话提示 | +|------|---------|--------|---------| +| C-01 | 入职超1个月未签合同 | 🔴 | 「张三入职35天了还没签合同,要赔双倍工资」 | +| C-02 | 入职超1年未签合同 | 🔴 | 「超过1年没签合同,法律上等于已签无固定期限合同」 | +| C-03 | 合同30天内到期 | 🟡 | 「李四合同还有20天到期,该准备续签了」 | +| C-04 | 合同到期未续签仍用工 | 🔴 | 「合同已到期但还在上班,要赔双倍工资」 | +| C-05 | 试用期超法定上限 | 🔴 | 「1年期合同试用期最多2个月,当前3个月不合法」 | +| C-06 | 试用期工资过低 | 🟡 | 「试用期工资不能低于转正工资的80%」 | +| C-07 | 两次固定期限后应签无固定 | 🔴 | 「已经签了2次固定合同,第3次应签无固定期限」 | + +#### 4.3.4 一键续签 + +- 合同到期前30天,列表出现「续签」按钮 +- 点击后弹出确认弹窗: + ``` + ┌───────────────────────────────┐ + │ 续签李四的劳动合同? │ + │ │ + │ 原合同:2025-08-01 ~ 2026-07-31│ + │ 新合同期限:[3年 ▼] │ + │ 新到期日:2029-07-31 │ + │ 试用期:无 │ + │ 签订方式:○ 纸质合同 ○ 电子合同 │ + │ │ + │ [取消] [确认续签] │ + └───────────────────────────────┘ + ``` +- 续签时选择签订方式(纸质/电子),默认沿用上次签订方式 +- 续签后自动更新状态为 🟢 正常,风险消除 + +--- + +### 4.4 钱的计算(Tab 切换,一页搞定) + +**目标**:输入最少信息,立刻算出该赔多少、该付多少 + +**页面结构**:3 个 Tab,共享一个页面 +``` +┌─────────────────────────────────────────────┐ +│ [ 加班费计算 ] [ 双倍工资 ] [ 经济补偿金 ] │ +├─────────────────────────────────────────────┤ +│ │ +│ (当前 Tab 的计算器内容) │ +│ │ +└─────────────────────────────────────────────┘ +``` + +#### 4.4.1 加班费计算器(Tab 1) + +**界面设计**:左输入右结果,实时计算 +``` +┌──────────────────┬──────────────────────┐ +│ 填写信息 │ 计算结果 │ +│ │ │ +│ 月工资:[8000__] │ 📊 加班费明细 │ +│ 元 │ │ +│ │ 小时工资:¥46.00 │ +│ 工作日加班: │ │ +│ [__10__] 小时 │ 工作日 ¥690 (10h×1.5) │ +│ │ 休息日 ¥736 (8h×2.0) │ +│ 休息日加班: │ 节假日 ¥552 (4h×3.0) │ +│ [__8___] 小时 │ │ +│ │ ─────────────────── │ +│ 节假日加班: │ 💰 合计:¥1,978 │ +│ [__4___] 小时 │ │ +│ │ ⚠️ 月加班22小时, │ +│ [计算] │ 未超36小时上限 ✅ │ +└──────────────────┴──────────────────────┘ +``` + +**交互优化**: +- 输入数字后**实时计算**,不需要点「计算」按钮 +- 结果区固定在右侧,输入时数字实时跳动更新 +- 超过36小时/月时,结果区显示黄色警告 +- 可选关联员工:选择员工后自动填入月工资 + +**计算规则**(后台自动,用户不需要知道公式): +``` +小时工资 = 月工资 ÷ 21.75 ÷ 8 +工作日加班费 = 小时工资 × 1.5 × 小时数 +休息日加班费 = 小时工资 × 2.0 × 小时数 +节假日加班费 = 小时工资 × 3.0 × 小时数 +``` + +#### 4.4.2 双倍工资计算器(Tab 2) + +**界面设计**: +``` +┌──────────────────┬──────────────────────┐ +│ 填写信息 │ 计算结果 │ +│ │ │ +│ 月工资:[8000__] │ ⚠️ 风险提示 │ +│ 元 │ │ +│ │ 入职日期:2026-06-01 │ +│ 入职日期: │ 合同签订:未签订 │ +│ [2026-06-01] │ │ +│ │ 双倍工资起算: │ +│ 合同签订日期: │ 2026-07-02 │ +│ [未签订 ▼] │ │ +│ 或选择日期 │ 双倍工资截止: │ +│ │ 2027-05-31 │ +│ │ │ +│ │ 💰 需赔:¥88,000 │ +│ │ (11个月 × ¥8,000) │ +│ │ │ +│ │ 📌 法律规定:入职1个月 │ +│ │ 没签合同,从第2个月起 │ +│ │ 要付双倍工资,最多11个月│ +└──────────────────┴──────────────────────┘ +``` + +**交互优化**: +- 「合同签订日期」默认显示「未签订」,也可选择日期 +- 如果已签订但逾期,自动计算逾期月数的双倍工资 +- 结果区用醒目大字显示金额 +- 底部附「人话版」法律解释 + +#### 4.4.3 经济补偿金计算器(Tab 3) + +**界面设计**: +``` +┌──────────────────┬──────────────────────┐ +│ 填写信息 │ 计算结果 │ +│ │ │ +│ 入职日期: │ 📊 补偿金计算 │ +│ [2022-03-01] │ │ +│ │ 工作年限:4年2个月 │ +│ 离职日期: │ → 按4.5个月计算 │ +│ [2026-05-15] │ │ +│ │ 月工资:¥8,000 │ +│ 月平均工资: │ │ +│ [8000___] 元 │ 💰 经济补偿金: │ +│ │ ¥36,000 │ +│ 离职原因: │ (4.5 × 8,000) │ +│ [协商解除 ▼] │ │ +│ │ ⚠️ 如果是违法解除: │ +│ 当地社平工资: │ 赔偿金 = ¥72,000 │ +│ [8000___] 元 │ (补偿金 × 2) │ +│ (选填,用于封顶) │ │ +│ │ 📌 6个月以上算1年, │ +│ │ 不满6个月算半个月 │ +└──────────────────┴──────────────────────┘ +``` + +**交互优化**: +- 「离职原因」用下拉选择,选项用人话: + - 「协商解除(双方同意)」 + - 「员工犯错被辞退」 + - 「员工没犯错但干不了」 + - 「公司裁员」 + - 「公司单方面违法辞退」 +- 根据离职原因自动判断是否需要支付补偿金 +- 「当地社平工资」选填,不填则跳过封顶计算 +- 结果同时显示正常补偿金和违法解除赔偿金(×2) + +#### 4.4.4 工资合规自动检查(后台规则) + +| 规则 | 检查内容 | 信号灯 | 人话提示 | +|------|---------|--------|---------| +| S-01 | 工资低于当地最低工资 | 🔴 | 「工资不能低于当地最低工资标准」 | +| S-02 | 加班超36小时/月 | 🟡 | 「月加班超过36小时有法律风险」 | +| S-03 | 试用期工资过低 | 🟡 | 「试用期工资至少是转正工资的80%」 | + +--- + +### 4.5 解聘助手(向导式流程) + +**目标**:一步步引导完成合规解聘,不怕漏步骤 + +**设计理念**:不展示复杂清单,而是用**向导式流程**,一次只看一步 + +#### 4.5.1 解聘向导(5 步) + +``` +┌─────────────────────────────────────────────┐ +│ 解聘助手 ●●○○○ │ +│ ───────────────────────────────────────── │ +│ │ +│ Step 1/5:为什么解聘? │ +│ │ +│ ○ 协商解除(双方同意分开了) │ +│ ○ 员工犯错被辞退(严重违纪/失职等) │ +│ ○ 员工没犯错但干不了(生病/不胜任等) │ +│ ○ 公司裁员(经营困难/技术调整等) │ +│ ○ 合同到期不续签 │ +│ │ +│ 💡 选不同原因,后续步骤和法律要求不同 │ +│ │ +│ [下一步 →] │ +└─────────────────────────────────────────────┘ +``` + +**5 步流程**: + +| 步骤 | 标题 | 内容 | 交互 | +|------|------|------|------| +| Step 1 | 为什么解聘? | 选择解聘原因(用人话选项) | 单选 | +| Step 2 | 员工信息 | 选择员工 → 自动带入入职日期/工资 → 填写解聘日期 | 下拉 + 日期 | +| Step 3 | 合规检查 | 根据解聘原因自动展示相关检查项(3-5项) | 逐项勾选 | +| Step 4 | 算钱 | 自动计算补偿金/代通知金,展示金额 | 自动计算 | +| Step 5 | 确认完成 | 汇总检查结果 + 金额 + 风险评估 → 保存记录 | 确认按钮 | + +**关键交互**: +- 顶部进度条 `●●○○○` 直观显示进度 +- 每步只有 1 个主操作「下一步」 +- Step 3 合规检查根据 Step 1 的选择**动态展示**: + - 选「协商解除」→ 只检查「是否支付补偿金」「是否签协议」 + - 选「员工犯错」→ 检查「是否有规章制度依据」「是否有证据」「是否通知工会」 + - 选「裁员」→ 检查「是否提前30天说明」「是否听取意见」「是否报劳动部门」 +- 检查项用人话描述,不是法条原文 +- Step 5 如果有未通过检查项,显示红色警告但**不阻止保存**(尊重用户决策) + +#### 4.5.2 禁止解聘情形检查(自动弹出) + +在 Step 2 选择员工后,自动检查是否属于**不得解除**情形: + +| 检查项 | 触发条件 | 提示 | +|--------|---------|------| +| 孕期/产期/哺乳期 | 员工标记为女性 + 在孕期/哺乳期 | 🔴 「该员工在孕期,法律禁止解除」 | +| 工伤期间 | 员工标记为工伤 | 🔴 「工伤期间不得解除劳动合同」 | +| 医疗期 | 员工在规定的医疗期内 | 🔴 「医疗期内不得解除」 | + +- 如果触发禁止情形,弹出醒目红色警告框 +- **不阻止继续操作**,但要求用户确认「我已了解风险,继续操作」 + +#### 4.5.3 解聘记录 + +- 完成向导后自动生成解聘记录 +- 记录包含:员工信息、解聘原因、检查结果、补偿金额、操作日期 +- 可在解聘助手页面底部查看历史记录 +- 支持导出单条记录为 PDF(可选) + +--- + +### 4.6 AI 合规顾问 + +**目标**:像有个法律顾问在身边,随时问、自动查、提前预警 + +**设计理念**:不是冷冰冰的搜索框,而是对话式交互,用户用大白话提问,AI 用大白话回答 + +#### 4.6.1 智能问答 + +**界面设计**:聊天式对话界面 +``` +┌─────────────────────────────────────────────┐ +│ 🤖 AI 合规顾问 │ +│ │ +│ ┌─────────────────────────────────────┐ │ +│ │ 🤖 你好!我是你的用工合规顾问, │ │ +│ │ 有什么劳动法问题可以直接问我。 │ │ +│ │ │ │ +│ │ 你可以问我: │ │ +│ │ · 员工入职没签合同怎么办? │ │ +│ │ · 加班费怎么算? │ │ +│ │ · 辞退员工需要赔多少? │ │ +│ └─────────────────────────────────┘ │ +│ │ +│ ┌─────────────────────────────────────┐ │ +│ │ 👤 试用期最长可以约定几个月? │ │ +│ └─────────────────────────────────────┘ │ +│ │ +│ ┌─────────────────────────────────────┐ │ +│ │ 🤖 根据《劳动合同法》第19条: │ │ +│ │ │ │ +│ │ · 合同期 3个月~1年 → 试用期最多1个月 │ │ +│ │ · 合同期 1年~3年 → 试用期最多2个月 │ │ +│ │ · 合同期 3年以上 → 试用期最多6个月 │ │ +│ │ │ │ +│ │ ⚠️ 你的员工王五:合同2年但试用期3个月│ │ +│ │ 超过法定上限,建议调整为2个月 │ │ +│ └─────────────────────────────────────┘ │ +│ │ +│ ┌─────────────────────────────────────┐ │ +│ │ [输入问题...] [发送] │ │ +│ └─────────────────────────────────────┘ │ +└─────────────────────────────────────────────┘ +``` + +**交互特点**: +- 聊天式界面,支持多轮对话(上下文记忆) +- 回答时**关联本企业数据**(如「你的员工王五试用期超限」) +- 回答附法律依据引用(可折叠查看法条原文) +- 预设常见问题快捷按钮(「没签合同怎么办」「加班费怎么算」等) +- 支持语音输入(移动端) + +#### 4.6.2 风险预测 + +**界面设计**:首页风险总览中的智能分析卡片 +``` +┌─────────────────────────────────────────────┐ +│ 🔮 AI 风险预测 │ +│ │ +│ ⚠️ 未来30天预计新增 3 个风险: │ +│ │ +│ · 李四合同7月31日到期 → 续签提醒 │ +│ · 张三入职将满1年 → 未签合同风险升级 │ +│ · 上月加班总时长上升40% → 超时风险 │ +│ │ +│ 💡 建议:本周优先处理合同到期和未签问题 │ +└─────────────────────────────────────────────┘ +``` + +- 基于企业当前数据(合同到期日、入职日期、加班趋势)预测未来风险 +- 每日自动刷新,展示在首页风险总览下方 +- 给出优先级建议(先处理什么) + +#### 4.6.3 合同审查 + +**功能**:上传合同文本或粘贴条款,AI 审查合法性 + +``` +┌─────────────────────────────────────────────┐ +│ 📄 合同审查 │ +│ │ +│ [粘贴合同条款文本...] │ +│ [或上传合同文件(.txt/.docx)] │ +│ │ +│ [开始审查] │ +│ │ +│ ──────────── 审查结果 ──────────── │ +│ │ +│ 🔴 试用期6个月超过法定上限(合同期2年最多2月)│ +│ 🟡 竞业限制未约定补偿标准 │ +│ 🟡 加班费计算基数未明确约定 │ +│ 🟢 社保缴纳条款符合规定 │ +│ 🟢 工资支付条款符合规定 │ +│ │ +│ 📊 合规评分:72/100 │ +└─────────────────────────────────────────────┘ +``` + +- 逐条审查合同条款,标注红/黄/绿 +- 给出修改建议(人话版) +- 合规评分直观展示合同整体合法程度 + +#### 4.6.4 案例匹配 + +**功能**:输入争议情况,匹配相似仲裁案例,评估败诉风险 + +``` +┌─────────────────────────────────────────────┐ +│ ⚖️ 案例匹配 │ +│ │ +│ 描述你的情况: │ +│ [员工入职3个月没签合同,现在要辞退他...] │ +│ │ +│ [分析] │ +│ │ +│ ──────────── 相似案例 ──────────── │ +│ │ +│ 📋 案例1:某科技公司 vs 员工张某 │ +│ · 情形:入职3个月未签合同后辞退 │ +│ · 结果:企业败诉,赔双倍工资+违法解除赔偿 │ +│ · 赔偿金额:¥XX,XXX │ +│ · 相似度:92% │ +│ │ +│ 📋 案例2:某贸易公司 vs 员工李某 │ +│ · 情形:未签合同 + 协商解除 │ +│ · 结果:企业赔双倍工资差额 │ +│ · 赔偿金额:¥XX,XXX │ +│ · 相似度:85% │ +│ │ +│ ⚠️ 你的败诉风险:高(90%) │ +│ 💡 建议:先补签合同再协商解除,可降低风险 │ +└─────────────────────────────────────────────┘ +``` + +- 内置劳动仲裁案例库(公开裁判文书) +- AI 语义匹配相似案例,计算相似度 +- 评估败诉概率和预估赔偿金额 +- 给出风险降低建议 + +#### 4.6.5 技术方案 + +| 组件 | 方案 | +|------|------| +| **LLM** | 阿里通义千问(Qwen),通过 DashScope API 调用,中文劳动法领域表现优秀 | +| **模型选择** | qwen-plus(日常问答)/ qwen-max(合同审查/案例匹配等复杂任务) | +| **API Key** | 通过环境变量 `DASHSCOPE_API_KEY` 注入,不硬编码 | +| **RAG 知识库** | 劳动法/劳动合同法/司法解释/地方条例 向量化存储 | +| **向量数据库** | Supabase pgvector(与业务数据库共用,减少依赖) | +| **Embedding** | DashScope text-embedding-v2(中文支持好,与 Qwen 生态统一) | +| **上下文关联** | 每次问答注入当前企业数据摘要(员工数/风险项/合同状态) | +| **案例库** | 爬取公开裁判文书,结构化存储 + 向量检索 | +| **流式输出** | DashScope SSE 流式返回,打字机效果,提升体验 | +| **安全过滤** | 敏感问题兜底回复(「建议咨询专业律师」) | + +#### 4.6.6 使用限制 + +| 套餐 | AI 问答次数/月 | 合同审查次数/月 | 案例匹配次数/月 | +|------|---------------|---------------|---------------| +| free | 10 | 3 | 3 | +| pro | 100 | 20 | 20 | +| enterprise | 无限 | 无限 | 无限 | + +### 4.7 员工参与(员工端,独立入口) + +**目标**:让员工也能查看自己的合同和工资,参与入职填报和合同确认,减少 HR 沟通成本 + +**设计理念**:员工端独立入口,不需要注册账号,手机号验证码登录,极简操作 + +#### 4.7.1 员工登录 + +- **入口**:独立页面 `/portal/login`,与管理端完全分离 +- **登录方式**:支持两种方式,员工可自由选择 + - **方式一:手机号 + 密码**(推荐,无需短信服务) + - HR 创建员工时设置初始密码,员工首次登录后可修改 + - 密码 6 位以上,支持数字+字母组合 + - **方式二:手机号 + 验证码**(无需记密码) + - v1.0 方案:页面内显示验证码(暂不接入短信服务,降低成本) + - 后续版本:接入阿里云短信服务,发送真实短信验证码 +- **验证码发送**:优先通过页面内验证码(暂不接入短信服务,降低成本) + - v1.0 方案:HR 通过微信分享员工端二维码/链接,员工扫码进入后输入手机号,系统发送验证码(页面内弹窗显示验证码,后续版本再接入短信) + - 后续版本:接入阿里云短信服务,发送真实短信验证码 +- **身份识别**:根据手机号匹配企业员工记录,自动关联 orgId +- **安全**:验证码 5 分钟有效,同一手机号每小时最多 5 次;密码错误 5 次锁定 30 分钟 + +``` +┌─────────────────────────────────────────────┐ +│ 🏢 用工合规助手 — 员工端 │ +│ │ +│ [密码登录] [验证码登录] ← 切换 Tab │ +│ │ +│ ── 密码登录 ── │ +│ 手机号:[___________] │ +│ 密码: [___________] │ +│ [登录] │ +│ │ +│ ── 验证码登录 ── │ +│ 手机号:[___________] │ +│ [获取验证码] 验证码:[______] │ +│ [登录] │ +│ │ +│ 📱 也可扫描 HR 发送的二维码直接进入 │ +└─────────────────────────────────────────────┘ +``` + +**HR 端生成员工端二维码**: +- HR 在管理端可生成员工端入口二维码,扫码直接打开 `/portal/login` +- 二维码可保存为图片,HR 通过微信发给员工 +- 员工扫码后选择密码登录或验证码登录 +- HR 创建员工时设置初始密码,可通过微信单独告知员工 + +#### 4.7.2 工资条查看 + +**页面**:`/portal/payslip` + +``` +┌─────────────────────────────────────────────┐ +│ 💰 我的工资条 │ +│ │ +│ 月份选择:[2026年7月 ▼] │ +│ │ +│ ┌─────────────────────────────────────┐ │ +│ │ 基本工资: ¥8,000 │ │ +│ │ 加班费: ¥1,978 │ │ +│ │ └ 工作日加班: ¥690 (10h) │ │ +│ │ └ 休息日加班: ¥736 (8h) │ │ +│ │ └ 节假日加班: ¥552 (4h) │ │ +│ │ ───────────────────────── │ │ +│ │ 应发合计: ¥9,978 │ │ +│ └─────────────────────────────────────┘ │ +│ │ +│ [确认已阅] │ +└─────────────────────────────────────────────┘ +``` + +- 按月查看工资明细,包含基本工资和加班费拆分 +- 加班费明细可展开查看工作日/休息日/节假日分类 +- 「确认已阅」按钮,记录员工已查看该月工资条(时间 + IP) +- HR 端可查看哪些员工已确认、哪些未确认 + +#### 4.7.3 我的合同 + +**页面**:`/portal/contract` + +``` +┌─────────────────────────────────────────────┐ +│ 📄 我的劳动合同 │ +│ │ +│ 合同类型:固定期限 │ +│ 签订方式:纸质合同 │ +│ 签订日期:2026-03-15 │ +│ 合同期限:2026-03-15 ~ 2029-03-14(3年) │ +│ 试用期:2个月(2026-03-15 ~ 2026-05-14) │ +│ 试用期工资:¥6,400 │ +│ 转正工资:¥8,000 │ +│ │ +│ 📎 合同扫描件:劳动合同_张三.pdf [查看] │ +│ │ +│ ──────────── 签署记录 ──────────── │ +│ ✅ 已确认签署(2026-03-15 14:32) │ +│ 确认IP:192.168.x.x │ +└─────────────────────────────────────────────┘ +``` + +- 查看自己的劳动合同信息(只读) +- 可查看合同扫描件(纸质)或电子合同链接(电子) +- 展示签署确认记录(时间 + IP) +- 如合同即将到期,顶部显示提示「您的合同还有 XX 天到期」 + +#### 4.7.4 员工入职填报 + +**页面**:`/portal/onboarding?token=xxx` + +**使用场景**:HR 在管理端添加员工时选择「生成填报二维码」,员工扫码填写(HR 通过微信发送二维码图片或链接) + +``` +┌─────────────────────────────────────────────┐ +│ 📝 入职信息填报 │ +│ │ +│ 欢迎加入 XX公司!请填写以下信息: │ +│ │ +│ 姓名 *: [___________] │ +│ 手机号 *: [___________] │ +│ 身份证号 *:[___________] │ +│ 紧急联系人:[___________] │ +│ 联系电话: [___________] │ +│ 住址: [___________] │ +│ 银行卡号: [___________] │ +│ 开户行: [___________] │ +│ │ +│ [提交] │ +│ │ +│ 📌 提交后 HR 将审核您的信息 │ +└─────────────────────────────────────────────┘ +``` + +- HR 端创建员工时可选「生成填报二维码」或「自己填写」 +- 选择「生成填报二维码」后,页面显示二维码图片 + 链接,HR 可: + - 保存二维码图片,通过微信发给员工 + - 复制链接,通过微信直接发给员工 +- 填报链接含一次性 token,有效期 24 小时 +- 员工填写的信息进入「待审核」状态,HR 审核后正式入库 +- 填报信息加密传输(HTTPS) +- **暂不接入短信服务**,降低初期成本,后续版本可增加短信通知 + +#### 4.7.5 电子合同签署确认 + +**页面**:`/portal/contract-confirm?token=xxx` + +**使用场景**:HR 在管理端录入电子合同后,生成确认二维码/链接,通过微信发给员工 + +``` +┌─────────────────────────────────────────────┐ +│ ✍️ 合同签署确认 │ +│ │ +│ XX公司 与 张三 的劳动合同 │ +│ │ +│ 合同类型:固定期限(3年) │ +│ 合同期限:2026-03-15 ~ 2029-03-14 │ +│ 试用期:2个月 │ +│ 试用期工资:¥6,400 │ +│ 转正工资:¥8,000 │ +│ │ +│ 📎 查看合同文件:[点击查看] │ +│ │ +│ ┌─────────────────────────────────────┐ │ +│ │ ☐ 我已阅读合同内容,确认签署 │ │ +│ └─────────────────────────────────────┘ │ +│ │ +│ [确认签署] │ +│ │ +│ 📌 确认后将记录签署时间和 IP 地址 │ +└─────────────────────────────────────────────┘ +``` + +- 员工查看合同内容后勾选确认 + 点击「确认签署」 +- 系统记录确认时间、IP 地址、设备信息 +- HR 端合同状态自动更新为「已确认签署」 +- 确认链接含一次性 token,有效期 7 天 +- HR 生成确认二维码后,可保存图片通过微信发给员工,或复制链接直接发送 +- **暂不接入短信服务**,通过微信发送二维码/链接即可 +- **法律效力说明**:此确认为员工知情确认,正式电子签章需接入第三方电子签平台(后续版本) + +### 4.8 风险提醒(融入首页,不单独成页) + +**设计理念**:风险不需要单独页面,直接在首页和各模块中展示 + +**风险展示位置**: + +| 位置 | 展示方式 | +|------|---------| +| 首页 | 待办列表 + 风险分布进度条 | +| 合同管理 | 列表状态列信号灯 + 详情页风险卡片 | +| 钱的计算 | 计算结果区的警告提示 | +| 解聘助手 | 向导 Step 3/5 的检查结果 | +| **顶部导航栏** | 红色角标显示待处理风险总数,点击跳转首页 | + +**风险处理**: +- 在首页待办列表中,每条风险右侧有「去处理」按钮 +- 点击直接跳转到对应模块的操作页面 +- 处理完成后风险自动消除(如签了合同,未签风险消失) +- 也可手动标记「已忽略」并填写备注(记录决策原因) + +--- + +## 5. 数据模型(SaaS 多租户) + +### 5.0 多租户基础表 + +#### Organization(企业/组织) + +```typescript +interface Organization { + id: string; + name: string; // 企业名称 + plan: 'free' | 'pro' | 'enterprise'; // 套餐 + maxEmployees: number; // 套餐对应人数上限 + createdAt: string; +} +``` + +#### User(用户) + +```typescript +interface User { + id: string; + orgId: string; // 所属企业 + phone: string; // 手机号(登录用) + email?: string; // 邮箱(选填) + passwordHash: string; // bcrypt 加密密码 + name: string; // 用户姓名 + role: 'admin' | 'hr' | 'viewer'; // 角色 + createdAt: string; + lastLoginAt?: string; +} +``` + +**角色权限**: + +| 角色 | 权限 | +|------|------| +| admin | 全部操作 + 用户管理 + 套餐管理 | +| hr | 合同/加班/解聘的增删改查 | +| viewer | 只能查看,不能修改 | + +### 5.1 员工信息 (Employee) — 精简字段 + +```typescript +interface Employee { + id: string; + orgId: string; // 所属企业(多租户隔离) + name: string; // 姓名(必填) + department: string; // 部门(必填) + hireDate: string; // 入职日期(必填) + monthlySalary: number; // 月工资标准(必填,加密存储) + status: 'active' | 'resigned'; // 在职/离职 + gender?: 'male' | 'female'; // 性别(选填,用于禁止解聘检查) + isPregnant?: boolean; // 是否在孕期/哺乳期(选填) + isInMedicalPeriod?: boolean; // 是否在医疗期(选填) + isWorkInjured?: boolean; // 是否工伤期间(选填) + phone?: string; // 联系电话(选填) + createdBy: string; // 创建人 userId + createdAt: string; + updatedAt: string; +} +``` + +**精简策略**:必填字段仅 4 个(姓名/部门/入职日期/月工资),其余选填 + +### 5.2 劳动合同 (LaborContract) + +```typescript +interface LaborContract { + id: string; + orgId: string; // 所属企业 + employeeId: string; // 关联员工 + signDate: string | null; // 签订日期(null=未签订) + startDate: string; // 合同起始日期 + endDate: string | null; // 到期日期(null=无固定期限) + contractType: 'fixed' | 'unfixed' | 'unsigned'; // 简化:固定/无固定/未签 + signMethod: 'paper' | 'electronic'; // 签订方式:纸质/电子 + contractYears: number; // 合同年限(1/2/3等,无固定=0) + probationMonths: number; // 试用期月数(0=无试用期) + probationSalary: number; // 试用期工资(0=无试用期) + renewalCount: number; // 续签次数 + attachmentName?: string; // 纸质合同扫描件文件名(可选) + attachmentUrl?: string; // 扫描件存储 URL(可选) + electronicContractNo?: string; // 电子合同编号(可选) + electronicContractUrl?: string; // 电子合同链接(可选) + createdBy: string; + createdAt: string; + updatedAt: string; +} +``` + +**精简策略**:去掉 `task` 类型(中小企业极少使用)、去掉 `status`(由系统自动计算) + +**签订方式支持**:`signMethod` 区分纸质/电子,纸质可上传扫描件存档,电子可记录合同编号和链接 + +### 5.3 加班记录 (OvertimeRecord) + +```typescript +interface OvertimeRecord { + id: string; + orgId: string; // 所属企业 + employeeId: string; + month: string; // YYYY-MM + weekdayHours: number; // 工作日加班小时 + weekendHours: number; // 休息日加班小时 + holidayHours: number; // 法定节假日加班小时 + calculatedPay: number; // 应付加班费(自动计算) + createdBy: string; + createdAt: string; +} +``` + +**精简策略**:去掉 `overtimePay`(中小企业通常不记录已付金额,只算应付) + +### 5.4 解聘记录 (TerminationRecord) + +```typescript +interface TerminationRecord { + id: string; + orgId: string; // 所属企业 + employeeId: string; + terminationDate: string; + reason: 'negotiated' | 'fault' | 'nonfault' | 'layoff' | 'expired'; + compensation: number; // 经济补偿金(自动计算) + checklistResult: { // 向导检查结果 + item: string; + passed: boolean; + remark?: string; + }[]; + riskLevel: 'safe' | 'warning' | 'danger'; + createdBy: string; + createdAt: string; +} +``` + +**精简策略**:去掉 `reasonDetail`(向导中已选择)、去掉 `noticeType`(由向导检查项覆盖)、去掉单独的 `hasProofIssued`/`hasArchiveTransferred`(合并到 checklistResult) + +### 5.5 风险项 (RiskItem) + +```typescript +interface RiskItem { + id: string; + orgId: string; // 所属企业 + type: 'contract' | 'salary' | 'termination'; + level: 'high' | 'medium' | 'low'; + title: string; // 人话标题 + description: string; // 人话描述 + suggestion: string; // 人话建议 + legalBasis: string; // 法律依据(折叠展示) + employeeId?: string; + status: 'pending' | 'resolved' | 'ignored'; + actionUrl?: string; // 点击跳转的处理页面路由 + createdAt: string; + resolvedAt?: string; + resolvedBy?: string; +} +``` + +### 5.6 操作审计日志 (AuditLog) + +```typescript +interface AuditLog { + id: string; + orgId: string; + userId: string; // 操作人 + action: string; // 操作类型(如 'contract.sign', 'termination.create') + target: string; // 操作对象(如 'employee:张三') + detail: string; // 操作详情 + ipAddress: string; // IP 地址 + createdAt: string; +} +``` + +**新增 `actionUrl`**:风险项可以直接跳转到对应的处理页面,一键操作 + +--- + +## 6. 系统架构(SaaS 版) + +### 6.1 技术架构 + +``` +┌─────────────────────────────────────────────────────┐ +│ 前端(React 18) │ +│ Vite + TailwindCSS + Recharts + React Router │ +│ 部署:Vercel / Netlify │ +├─────────────────────────────────────────────────────┤ +│ API 层(RESTful) │ +│ Axios 请求 → JWT Token 认证 → 路由守卫 │ +├─────────────────────────────────────────────────────┤ +│ 后端(Node.js) │ +│ Express + Prisma ORM + JWT 认证 + Zod 校验 │ +│ 部署:Railway / Render │ +├─────────────────────────────────────────────────────┤ +│ 数据库(PostgreSQL) │ +│ 多租户隔离(org_id) + 全文搜索 + 自动备份 │ +│ 部署:Railway / Supabase / Neon │ +└─────────────────────────────────────────────────────┘ +``` + +### 6.2 多租户架构 + +**隔离策略**:共享数据库 + 行级隔离(`org_id` 字段) + +- 每个企业注册后创建一个 Organization(组织) +- 所有业务数据表均包含 `orgId` 字段 +- 所有 API 请求自动注入当前用户的 `orgId`,只能访问本企业数据 +- Prisma 中间件自动过滤 `orgId`,防止越权 + +### 6.3 认证体系 + +| 功能 | 说明 | +|------|------| +| 注册 | 企业手机号/邮箱注册,创建 Organization + Admin 用户 | +| 登录 | 手机号/邮箱 + 密码登录,返回 JWT Token | +| Token 管理 | Access Token(2h 过期)+ Refresh Token(7d 过期) | +| 路由守卫 | 前端 React Router 守卫,未登录跳转登录页 | +| API 拦截 | 后端中间件校验 JWT,提取 userId + orgId | +| 角色权限 | admin(管理员)/ hr(HR 操作员)/ viewer(只读) | + +### 6.4 安全要求 + +| 类别 | 要求 | +|------|------| +| **密码存储** | bcrypt 加密,不存明文 | +| **数据传输** | 全站 HTTPS | +| **JWT 密钥** | 环境变量管理,不硬编码 | +| **SQL 注入** | Prisma ORM 参数化查询,杜绝注入 | +| **XSS 防护** | React 自动转义 + CSP 头 | +| **数据隔离** | 每个请求校验 orgId,禁止跨企业访问 | +| **敏感数据** | 工资字段加密存储(AES-256) | +| **操作日志** | 关键操作(解聘/合同变更)记录审计日志 | +| **速率限制** | 登录接口限流(5次/分钟),防止暴力破解 | + +### 6.5 非功能需求 + +| 类别 | 要求 | +|------|------| +| **前端技术栈** | React 18 + Vite + TailwindCSS + Recharts | +| **后端技术栈** | Node.js + Express + Prisma ORM + Zod | +| **数据库** | PostgreSQL,支持多租户行级隔离 | +| **认证** | JWT(Access Token + Refresh Token) | +| **响应式** | 桌面端(1280px+)/ 平板端(768px+)/ 手机端(375px+) | +| **语言** | 全简体中文,文案用「人话」避免法律术语 | +| **性能** | 首屏加载 < 2s,API 响应 < 500ms | +| **浏览器** | Chrome / Edge / Safari 最新版 | +| **部署** | 前端 Vercel + 后端 Railway + 数据库 Neon/Supabase | +| **可用性** | 99.5%+,数据库每日自动备份 | +| **可扩展** | 后期可加 Redis 缓存、CDN 加速 | + +--- + +## 7. 界面规划 + +### 7.1 整体布局(顶部导航,非侧边栏) + +**设计理由**:中小企业用户更习惯顶部导航(类似常用网站),侧边栏对小屏幕不友好 + +``` +┌──────────────────────────────────────────────────────────────┐ +│ 🏢 用工合规助手 [总览] [合同] [算钱] [解聘] [AI顾问] 🔴3 👤张总 │ +├──────────────────────────────────────────────────────────────┤ +│ │ +│ │ +│ 主内容区 │ +│ (最多 960px 居中) │ +│ │ +│ │ +└─────────────────────────────────────────────────────┘ +``` + +**布局规范**: +- 顶部导航栏:Logo + 5 个 Tab + 风险角标 + 用户头像下拉菜单(设置/退出),固定不滚动 +- 主内容区:最大宽度 960px,居中,避免宽屏下内容过散 +- 移动端:导航栏变为底部 Tab Bar(类似 App) +- **无侧边栏**:减少视觉干扰,5 个 Tab 足够 + +### 7.2 页面清单 + +**管理端认证页面(未登录可访问)**: + +| 页面 | 路由 | 说明 | +|------|------|------| +| 登录 | `/login` | 手机号/邮箱 + 密码登录 | +| 注册 | `/register` | 企业名称 + 手机号 + 密码,注册即创建组织 | +| 忘记密码 | `/forgot-password` | 手机验证码重置密码 | + +**管理端业务页面(登录后访问)**: + +| 页面 | 路由 | 导航名称 | 说明 | +|------|------|---------|------| +| 风险总览 | `/` | 总览 | 一句话状态 + 数字卡片 + 待办列表 + 风险分布 | +| 合同管理 | `/contracts` | 合同 | 员工合同列表 + 添加/编辑 + 一键续签 + 发送填报/确认链接 | +| 钱的计算 | `/money` | 算钱 | 3 个 Tab:加班费 / 双倍工资 / 经济补偿金 | +| 解聘助手 | `/termination` | 解聘 | 5 步向导 + 历史记录 | +| AI 合规顾问 | `/ai-assistant` | AI顾问 | 智能问答 + 合同审查 + 案例匹配 | +| 系统设置 | `/settings` | 设置 | 企业信息 + 用户管理 + 套餐 | + +**员工端页面(独立入口,手机号验证码登录)**: + +| 页面 | 路由 | 说明 | +|------|------|------| +| 员工登录 | `/portal/login` | 手机号 + 短信验证码登录 | +| 工资条 | `/portal/payslip` | 按月查看工资明细 + 确认已阅 | +| 我的合同 | `/portal/contract` | 查看合同信息 + 扫描件/电子链接 | +| 入职填报 | `/portal/onboarding?token=xxx` | 填写入职信息(一次性链接) | +| 合同确认 | `/portal/contract-confirm?token=xxx` | 确认签署电子合同(一次性链接) | + +**设置页面子 Tab**: +- 企业信息:名称、地区(用于最低工资/社平工资默认值) +- 用户管理:添加/移除用户,分配角色(admin/hr/viewer) +- 套餐信息:当前套餐、人数上限、升级套餐 + +### 7.3 配色方案 + +| 用途 | 颜色 | 说明 | +|------|------|------| +| 主色 | `#2563EB`(蓝色) | 主操作按钮、导航高亮、链接 | +| 危险/高风险 | `#EF4444`(红色) | 高风险信号灯、危险提示 | +| 警告/中风险 | `#F59E0B`(橙黄) | 待办提醒、中风险信号灯 | +| 安全/正常 | `#22C55E`(绿色) | 合规状态、完成状态 | +| 背景 | `#F8FAFC`(浅灰) | 页面背景,减少视觉疲劳 | +| 卡片 | `#FFFFFF`(白色) | 内容卡片背景 | +| 文字主 | `#1E293B`(深灰) | 主要文字 | +| 文字次 | `#64748B`(中灰) | 辅助说明文字 | + +### 7.4 组件规范 + +| 组件 | 规范 | +|------|------| +| 按钮 | 主按钮蓝色填充,次按钮白色边框,危险按钮红色填充 | +| 卡片 | 白底 + 圆角12px + 轻阴影(`shadow-sm`),不用重阴影 | +| 表格 | 无边框简约表格,行间用浅灰分隔线,hover 高亮 | +| 表单 | 输入框圆角8px,focus 时蓝色边框,错误时红色边框 + 提示 | +| 弹窗 | 居中模态框,圆角16px,遮罩半透明黑色 | +| 信号灯 | 圆点12px + 文字,不用复杂图标 | +| 空状态 | 插图 + 引导文字 + 主操作按钮 | + +--- + +## 8. 实施计划 + +| 阶段 | 内容 | 优先级 | +|------|------|--------| +| P0 | 项目搭建:前端 Vite + React + TailwindCSS,后端 Express + Prisma + PostgreSQL,项目结构 + 路由骨架 | 高 | +| P1 | 认证体系:注册/登录/JWT Token + 路由守卫 + 多租户中间件 | 高 | +| P2 | 首页风险总览(信号灯 + 待办 + 分布) | 高 | +| P3 | 合同管理(列表 + 添加/编辑 + 一键续签 + 纸质/电子合同 + 风险标注) | 高 | +| P4 | 钱的计算(3 Tab 计算器 + 实时计算) | 高 | +| P5 | 解聘助手(5 步向导 + 禁止情形检查 + 记录) | 高 | +| P6 | AI 合规顾问(智能问答 + 风险预测 + 合同审查 + 案例匹配 + RAG 知识库) | 高 | +| P7 | 员工端(验证码登录 + 工资条 + 我的合同 + 入职填报 + 合同确认) | 高 | +| P8 | 系统设置(企业信息 + 用户管理 + 套餐) | 中 | +| P9 | 新手引导 + 空状态 + 移动端适配 | 中 | +| P10 | 部署上线(Vercel + Railway + Neon)+ 联调验证 | 中 | + +--- + +## 9. 法律依据索引 + +| 法律法规 | 关键条款 | 涉及模块 | +|---------|---------|---------| +| 《劳动法》 | 第36/41/44/48/50条 | 工资加班费 | +| 《劳动合同法》 | 第10/14/19/20/39-42/46-47/50/82条 | 合同/解聘/双倍工资 | +| 《劳动合同法实施条例》 | 第6/7/25/27条 | 双倍工资/补偿金 | +| 《工资支付暂行规定》 | 第13/15/18条 | 加班费/工资支付 | +| 最高人民法院劳动争议司法解释(一) | 第44/45条 | 举证责任 | + +--- + +## 10. 约束与假设 + +1. **SaaS 多租户**:共享数据库 + 行级隔离(`orgId`),每家企业数据互不可见 +2. **法律时效性**:系统内置规则基于现行法律法规,如法律更新需手动更新规则 +3. **地区差异**:最低工资标准、社平工资等参数需用户自行设置(提供常用城市默认值) +4. **非替代法律意见**:系统提供合规参考,不构成正式法律意见,重大决策建议咨询专业律师 +5. **套餐限制**:free 套餐限 20 人,pro 套餐限 200 人,enterprise 无限制 +6. **数据备份**:PostgreSQL 数据库每日自动备份,保留 30 天 +7. **合同附件存储**:纸质合同扫描件上传至云存储(Supabase Storage / S3),数据库只存 URL +8. **数据导出**:支持导出全部数据为 JSON/Excel,用户可随时备份 +9. **后续可扩展**:预留 API 接口,后期可接入电子签平台(如 e签宝、法大大)实现在线签署 + +--- + +## 11. 系统扩展架构 + +### 11.1 设计原则:插件化模块架构 + +系统采用**模块化插件架构**,每个业务模块独立开发、独立注册、独立路由,互不依赖: + +``` +┌─────────────────────────────────────────────────────┐ +│ 前端框架层 │ +│ 路由 / 导航 / 认证 / 布局 / 信号灯体系 / UI 组件库 │ +├─────────────────────────────────────────────────────┤ +│ 模块注册中心 (Module Registry) │ +│ 每个模块注册:路由前缀 / 导航菜单项 / 权限 / 图标 │ +├──────┬──────┬──────┬──────┬──────┬──────┬──────────┤ +│ 合同 │ 工资 │ 解聘 │ 社保 │ 成本 │ 考勤 │ ... │ +│ 模块 │ 模块 │ 模块 │ 模块 │ 分析 │ 模块 │ 模块 │ +├──────┴──────┴──────┴──────┴──────┴──────┴──────────┤ +│ 共享数据层 (Prisma) │ +│ Employee / Organization / User / AuditLog │ +└─────────────────────────────────────────────────────┘ +``` + +**核心机制**: +- **模块注册**:每个模块通过统一接口注册路由、导航菜单、权限要求 +- **共享数据**:Employee/Organization/User 为核心共享表,所有模块复用 +- **模块独立**:新增模块不影响现有模块,可独立上线/下线 +- **渐进式加载**:前端按模块懒加载(React.lazy),不影响首屏性能 + +### 11.2 后端 API 扩展规范 + +``` +/api/v1/ + /auth ← 认证模块(v1.0) + /employees ← 员工管理(v1.0) + /contracts ← 合同管理(v1.0) + /overtime ← 加班记录(v1.0) + /termination ← 解聘管理(v1.0) + /risks ← 风险预警(v1.0) + /social-insurance ← 社保公积金(v2.0 预留) + /cost-analysis ← 人力成本分析(v3.0 预留) + /attendance ← 考勤管理(v4.0 预留) +``` + +- 所有 API 遵循 RESTful 规范,统一 `/api/v1/` 前缀 +- 统一响应格式:`{ success: boolean, data: any, error?: string }` +- 统一鉴权中间件,所有路由自动校验 JWT + orgId +- 新增模块只需添加路由文件 + Prisma model,不改现有代码 + +### 11.3 数据库扩展策略 + +- 核心表(Employee/Organization/User)稳定不变 +- 新模块新增独立表,通过 `employeeId` / `orgId` 关联 +- Prisma schema 按模块分文件管理(Prisma 多 schema 支持) +- 数据库迁移使用 Prisma Migrate,增量迁移不破坏现有数据 + +--- + +## 12. 后续模块规划 + +### 12.1 v2.0 — 社保公积金模块 + +**目标**:自动计算五险一金缴费金额,防止少缴/漏缴风险 + +| 功能 | 说明 | +|------|------| +| 缴费基数设置 | 各城市社保/公积金缴费基数上下限默认值 | +| 五险一金计算 | 养老/医疗/失业/工伤/生育 + 公积金,企业/个人分担 | +| 缴费明细表 | 按员工/按月生成缴费明细 | +| 合规检查 | 缴费基数低于最低标准预警、断缴提醒 | +| 对账单导出 | 导出社保公积金月度对账单 | + +**数据模型预留**: +```typescript +interface SocialInsuranceRecord { + id: string; + orgId: string; + employeeId: string; + month: string; // YYYY-MM + base: number; // 缴费基数 + pension: { company: number; personal: number }; + medical: { company: number; personal: number }; + unemployment: { company: number; personal: number }; + workInjury: { company: number; personal: number }; + maternity: { company: number; personal: number }; + housingFund: { company: number; personal: number }; + total: { company: number; personal: number }; +} +``` + +**页面路由**:`/social-insurance`(导航名称:社保) + +### 12.2 v3.0 — 人力资源成本分析模块 + +**目标**:可视化人力成本结构,辅助经营决策 + +| 功能 | 说明 | +|------|------| +| 成本总览 Dashboard | 月度/季度/年度人力成本总览 | +| 成本构成分析 | 工资/社保/公积金/加班费/补偿金 占比饼图 | +| 部门成本对比 | 各部门人力成本柱状图对比 | +| 人均成本趋势 | 人均成本月度趋势折线图 | +| 成本预警 | 人力成本占比超过营收 X% 预警 | +| 报表导出 | 导出 Excel/PDF 成本报表 | + +**数据模型预留**: +```typescript +interface CostReport { + id: string; + orgId: string; + period: string; // YYYY-MM 或 YYYY-Q1 等 + totalSalary: number; // 工资总额 + totalOvertimePay: number; // 加班费总额 + totalSocialInsurance: number; // 社保企业部分 + totalHousingFund: number; // 公积金企业部分 + totalCompensation: number; // 解聘补偿金 + totalCost: number; // 人力成本合计 + headcount: number; // 人数 + avgCostPerPerson: number; // 人均成本 +} +``` + +**页面路由**:`/cost-analysis`(导航名称:成本分析) + +### 12.3 v3.0 — 员工自助门户增强 + +**目标**:在 v1.0 员工端基础上增加考勤、请假等自助功能 + +| 功能 | 说明 | +|------|------| +| 考勤记录 | 查看个人考勤/加班记录 | +| 请假申请 | 在线请假审批流程 | +| 调薪记录 | 查看历史调薪记录 | +| 在线咨询 | 员工端直接问 AI 顾问 | + +**页面路由**:`/portal`(扩展已有员工端) + +**注**:v1.0 已包含员工端基础功能(工资条查看、合同查看、入职填报、合同确认),v3.0 在此基础上增强 diff --git a/1-prd.md b/1-prd.md new file mode 100644 index 0000000..e7ffa8a --- /dev/null +++ b/1-prd.md @@ -0,0 +1,1107 @@ +# 劳动用工合规助手 — 产品需求文档 (PRD) + +> **文档编号**: 1-prd.md +> **版本**: v1.0 +> **日期**: 2026-07-23 +> **状态**: 草案 +> **依据**: 0-req.md v3.0 需求规格说明书 + +--- + +## 1. 产品概述 + +### 1.1 产品定位 + +面向中小企业的**极简用工合规 SaaS 工具**,聚焦劳动仲裁三大高发领域(合同签订、工资加班费、解聘),辅以 AI 合规顾问,帮助企业事前预防、事中管控、事后追溯用工风险。 + +### 1.2 产品愿景 + +> 「让每个中小企业都有一个用得起的劳动法顾问」 + +### 1.3 核心价值主张 + +| 价值 | 说明 | +|------|------| +| 极简易用 | 三看三不用原则,不需要培训,打开就会用 | +| 自动合规 | 风险自动检测 + 人话提示,不用懂法也能合规 | +| AI 顾问 | 通义千问驱动,随时问、自动查、提前预警 | +| 安全可靠 | SaaS 云端存储,数据隔离,多设备访问 | + +### 1.4 目标用户 + +| 用户角色 | 企业规模 | 使用频率 | 核心诉求 | +|---------|---------|---------|---------| +| 老板兼管 HR | 10-30人 | 每周1-2次 | 快速了解风险,解聘时算钱 | +| 行政兼 HR | 30-80人 | 每天5分钟 | 合同到期提醒,每月算加班费 | +| 初级 HR | 80-200人 | 日常使用 | 合同管理,工资计算,解聘流程 | + +--- + +## 2. 用户故事 + +### 2.1 认证与注册 + +| 编号 | 用户故事 | 验收标准 | +|------|---------|---------| +| US-AUTH-01 | 作为企业管理者,我想注册账号,这样我就能开始使用系统管理用工合规 | 输入企业名称+手机号+密码 → 创建组织+管理员账号 → 自动登录跳转首页 | +| US-AUTH-02 | 作为用户,我想用手机号和密码登录,这样我能安全访问我的数据 | 输入手机号+密码 → 校验通过 → 返回 JWT Token → 跳转首页 | +| US-AUTH-03 | 作为管理员,我想添加 HR 用户,这样我的同事也能操作系统 | 设置页 → 用户管理 → 输入姓名+手机号+角色 → 创建成功 | +| US-AUTH-04 | 作为用户,我想找回密码,这样我忘记密码时还能登录 | 忘记密码 → 输入手机号 → 验证码重置 → 重新登录 | + +### 2.2 首页风险总览 + +| 编号 | 用户故事 | 验收标准 | +|------|---------|---------| +| US-HOME-01 | 作为用户,我想打开首页就知道有没有风险,这样我不用到处翻找 | 顶部一句话:「今天有 X 件事需要处理」 | +| US-HOME-02 | 作为用户,我想看到具体待办事项,这样我知道该先处理什么 | 待办列表:信号灯+一句话描述+「去处理」按钮 | +| US-HOME-03 | 作为用户,我想看到风险分布,这样我知道哪类问题最多 | 进度条展示合同/工资/解聘三类风险数量 | +| US-HOME-04 | 作为用户,当没有风险时我想看到安心提示,这样我知道一切正常 | 显示绿色「✅ 暂无风险,继续保持!」 | +| US-HOME-05 | 作为用户,我想看到 AI 风险预测,这样我能提前防范 | 首页下方展示 AI 预测的未来30天风险卡片 | + +### 2.3 合同管理 + +| 编号 | 用户故事 | 验收标准 | +|------|---------|---------| +| US-CON-01 | 作为 HR,我想添加员工和合同信息,这样系统帮我管理合规 | 添加按钮 → 分步表单(基本信息→合同信息)→ 保存成功 | +| US-CON-02 | 作为 HR,我想看到所有员工的合同状态,这样我一眼就知道谁有问题 | 列表5列:姓名/部门/入职日期/合同状态(信号灯)/操作 | +| US-CON-03 | 作为 HR,当合同即将到期时我想收到提醒,这样我不会忘记续签 | 到期前30天列表出现「续签」按钮 + 首页待办提醒 | +| US-CON-04 | 作为 HR,我想一键续签合同,这样我不用填一堆表单 | 点击「续签」→ 弹窗确认期限+签订方式 → 确认 → 状态变绿 | +| US-CON-05 | 作为 HR,我想选择纸质或电子合同签订方式,这样符合实际操作 | 添加/续签时可选「纸质合同」或「电子合同」 | +| US-CON-06 | 作为 HR,当员工入职很久没签合同时我想被提醒,这样避免双倍工资赔偿 | 入职超1个月未签 → 🔴 红色状态 + 首页待办 | +| US-CON-07 | 作为 HR,我想搜索员工,这样快速找到某人的合同 | 搜索框输入姓名 → 实时筛选列表 | +| US-CON-08 | 作为 HR,我想批量续签即将到期的合同,这样不用一个个点 | 全选 → 批量续签 → 弹窗确认 → 批量更新 | + +### 2.4 钱的计算 + +| 编号 | 用户故事 | 验收标准 | +|------|---------|---------| +| US-MONEY-01 | 作为 HR,我想计算加班费,这样我知道该付多少 | 输入月工资+加班小时数 → 实时显示各类加班费明细+合计 | +| US-MONEY-02 | 作为 HR,当加班超过法定上限时我想被提醒,这样避免违法 | 月加班超36小时 → 结果区显示黄色警告 | +| US-MONEY-03 | 作为 HR,我想计算未签合同的双倍工资,这样我知道风险金额 | 输入月工资+入职日期+签订状态 → 显示起止日期+赔偿金额 | +| US-MONEY-04 | 作为 HR,我想计算经济补偿金,这样解聘时知道该赔多少 | 输入入职/离职日期+月工资+原因 → 显示工作年限+补偿金+赔偿金(×2) | +| US-MONEY-05 | 作为 HR,我想关联员工自动填入工资,这样不用手动输入 | 选择员工 → 月工资自动填入 | + +### 2.5 解聘助手 + +| 编号 | 用户故事 | 验收标准 | +|------|---------|---------| +| US-TERM-01 | 作为 HR,我想一步步完成解聘流程,这样不会漏掉步骤 | 5步向导:选原因→选员工→合规检查→算钱→确认 | +| US-TERM-02 | 作为 HR,当解聘原因不同时我想看到对应的检查项,这样有针对性 | Step 3 根据Step 1选择动态展示检查项 | +| US-TERM-03 | 作为 HR,当员工属于禁止解聘情形时我想被警告,这样避免违法解聘 | 选员工后自动检查孕期/工伤/医疗期 → 红色警告弹窗 | +| US-TERM-04 | 作为 HR,我想看到解聘历史记录,这样可以追溯 | 解聘助手页面底部展示历史记录列表 | +| US-TERM-05 | 作为 HR,即使有未通过检查项我也想保存记录,这样尊重我的决策 | Step 5 显示红色警告但不阻止保存 | + +### 2.6 AI 合规顾问 + +| 编号 | 用户故事 | 验收标准 | +|------|---------|---------| +| US-AI-01 | 作为用户,我想用大白话问劳动法问题,这样不用自己查法条 | 聊天界面输入问题 → AI 流式回答 + 法律依据 + 关联本企业数据 | +| US-AI-02 | 作为用户,我想看到 AI 预测的未来风险,这样提前防范 | 首页 AI 风险预测卡片,展示未来30天预计风险 | +| US-AI-03 | 作为 HR,我想让 AI 审查合同条款,这样知道有没有违法 | 粘贴/上传合同文本 → 逐条审查 + 红/黄/绿标注 + 合规评分 | +| US-AI-04 | 作为 HR,我想匹配相似仲裁案例,这样评估败诉风险 | 输入争议情况 → 展示相似案例 + 败诉概率 + 赔偿预估 | +| US-AI-05 | 作为用户,AI 回答时我想看到打字机效果,这样体验更好 | SSE 流式输出,逐字显示 | + +### 2.7 系统设置 + +| 编号 | 用户故事 | 验收标准 | +|------|---------|---------| +| US-SET-01 | 作为管理员,我想设置企业地区,这样系统用对的最低工资标准 | 设置→企业信息→选择城市 → 自动填入最低工资/社平工资默认值 | +| US-SET-02 | 作为管理员,我想管理用户和权限,这样控制谁能看谁能改 | 设置→用户管理→添加/移除用户→分配角色 | +| US-SET-03 | 作为管理员,我想查看当前套餐和人数限制,这样知道是否需要升级 | 设置→套餐信息→显示当前套餐+已用人数+上限 | + +### 2.8 员工端 + +| 编号 | 用户故事 | 验收标准 | +|------|---------|---------| +| US-EMP-01 | 作为员工,我想用手机号+密码登录,这样不用等验证码 | 输入手机号+密码→登录成功→跳转工资条 | +| US-EMP-02 | 作为员工,我想用手机号+验证码登录,这样不用记密码 | 输入手机号→获取验证码→输入验证码→登录成功→跳转工资条 | +| US-EMP-03 | 作为员工,我想扫描 HR 发的二维码直接进入员工端,这样不用手动输入网址 | 扫码→打开员工端登录页→选择登录方式→登录 | +| US-EMP-04 | 作为员工,我想查看月度工资条,这样知道工资明细 | 选择月份→显示基本工资+加班费拆分+应发合计 | +| US-EMP-05 | 作为员工,我想确认已阅工资条,这样 HR 知道我看过了 | 点击「确认已阅」→记录时间+IP→HR端显示已确认 | +| US-EMP-06 | 作为员工,我想查看我的合同信息,这样了解合同条款 | 我的合同页→显示合同类型/期限/试用期/工资/扫描件 | +| US-EMP-07 | 作为员工,HR 发二维码让我填报入职信息,这样不用 HR 手动录入 | 扫码→填写姓名/身份证/银行卡等→提交→HR审核入库 | +| US-EMP-08 | 作为员工,HR 发二维码让我确认电子合同,这样完成签署确认 | 扫码→查看合同内容→勾选确认→点击签署→记录时间+IP | +| US-EMP-09 | 作为 HR,我想生成入职填报二维码发给员工,这样通过微信即可发送 | 添加员工时选「生成填报二维码」→显示二维码+链接→保存图片/复制链接 | +| US-EMP-10 | 作为 HR,我想生成合同确认二维码发给员工,这样完成电子合同签署 | 合同详情→生成确认二维码→微信发给员工→员工确认后状态自动更新 | +| US-EMP-11 | 作为 HR,我想查看员工工资条确认状态,这样知道谁还没看 | 合同/工资管理→显示各员工确认状态(已确认/未确认) | + +--- + +## 3. 用户流程 + +### 3.1 新用户注册流程 + +``` +访问网站 → 注册页 + │ + ├─ 输入企业名称 + ├─ 输入手机号 + ├─ 输入密码 + ├─ 点击注册 + │ + ├─ 创建 Organization + Admin User + ├─ 自动登录,返回 JWT Token + │ + └─ 跳转首页(空状态) + │ + ├─ 显示新手引导弹窗(3步) + └─ 显示「添加第一个员工」引导卡片 +``` + +### 3.2 日常使用流程 + +``` +登录 → 首页风险总览 + │ + ├─ 有待办? → 点击「去处理」→ 跳转对应模块 → 处理 → 返回首页 + │ + ├─ 无待办? → 查看AI风险预测 → 了解未来风险 + │ + └─ 日常操作: + ├─ 合同管理 → 添加员工/续签/查看详情 + ├─ 钱的计算 → 切换Tab计算加班费/双倍工资/补偿金 + ├─ 解聘助手 → 5步向导完成解聘 + └─ AI顾问 → 问答/合同审查/案例匹配 +``` + +### 3.3 合同到期续签流程 + +``` +首页待办显示「🟡 XX合同还有20天到期」 + │ + └─ 点击「去续签」→ 合同管理页 + │ + └─ 点击「续签」→ 弹窗 + │ + ├─ 确认新期限(默认3年) + ├─ 选择签订方式(默认沿用上次) + ├─ 确认新到期日 + │ + └─ 点击「确认续签」 + │ + ├─ 更新合同记录 + ├─ 状态变 🟢 正常 + ├─ 风险项自动消除 + └─ 首页待办减少 +``` + +### 3.4 解聘流程 + +``` +解聘助手 → Step 1: 选择解聘原因 + │ + └─ Step 2: 选择员工 + │ + ├─ 自动检查禁止解聘情形 + ├─ 触发?→ 红色警告 → 用户确认继续 + │ + └─ Step 3: 合规检查(根据原因动态展示) + │ + └─ Step 4: 自动计算补偿金 + │ + └─ Step 5: 确认汇总 + │ + ├─ 有未通过项?→ 红色警告(不阻止) + ├─ 点击保存 + │ + └─ 生成解聘记录 + ├─ 员工状态变更为离职 + ├─ 记录存档 + └─ 审计日志记录 +``` + +### 3.5 AI 智能问答流程 + +``` +AI顾问页 → 输入问题(或点击预设问题) + │ + ├─ 前端构建上下文: + │ ├─ 当前企业数据摘要(员工数/风险项/合同状态) + │ └─ RAG 检索相关法律条文 + │ + ├─ 调用 DashScope API(qwen-plus) + │ + ├─ SSE 流式返回 + │ ├─ 逐字显示回答 + │ └─ 显示完成后附法律依据(可折叠) + │ + └─ 支持多轮对话(保留上下文) +``` + +### 3.6 员工入职填报流程 + +``` +HR 管理端 → 添加员工 → 选择「生成填报二维码」 + │ + ├─ 生成一次性 token(24h 有效) + ├─ 页面显示二维码图片 + 可复制链接 + │ + └─ HR 通过微信发送给员工(二维码图片或链接) + │ + └─ 员工扫码/点击链接 → 填报页面 + │ + ├─ 填写姓名/手机号/身份证/银行卡等 + ├─ 提交 + │ + └─ 信息进入「待审核」状态 + │ + └─ HR 管理端收到通知 + │ + ├─ 审核 → 通过 → 正式入库 + │ ├─ 创建 Employee 记录 + │ └─ token 失效 + │ + └─ 审核 → 驳回 → 员工重新填报 +``` + +### 3.7 电子合同签署确认流程 + +``` +HR 管理端 → 录入电子合同 → 点击「生成确认二维码」 + │ + ├─ 生成一次性 token(7天有效) + ├─ 页面显示二维码图片 + 可复制链接 + │ + └─ HR 通过微信发送给员工(二维码图片或链接) + │ + └─ 员工扫码/点击链接 → 合同确认页面 + │ + ├─ 查看合同内容(类型/期限/试用期/工资) + ├─ 查看合同文件(如有链接) + ├─ 勾选「我已阅读,确认签署」 + ├─ 点击「确认签署」 + │ + └─ 系统记录:确认时间 + IP + 设备信息 + │ + └─ HR 端合同状态自动更新为「已确认签署」 + ├─ token 失效 + └─ 审计日志记录 +``` + +--- + +## 4. 功能规格 + +### 4.1 认证模块 + +#### 4.1.1 注册 + +- **页面**: `/register` +- **输入**: 企业名称、手机号、密码(8位以上)、确认密码 +- **校验**: 手机号格式、密码长度、手机号未被注册 +- **处理**: 创建 Organization(plan=free, maxEmployees=20)+ User(role=admin)+ bcrypt 加密密码 +- **输出**: JWT Token + 跳转首页 +- **限流**: 同一 IP 每小时最多 5 次注册 + +#### 4.1.2 登录 + +- **页面**: `/login` +- **输入**: 手机号、密码 +- **校验**: 手机号存在、密码匹配 +- **输出**: Access Token(2h)+ Refresh Token(7d) +- **限流**: 同一 IP 每分钟最多 5 次登录 + +#### 4.1.3 Token 刷新 + +- **接口**: `POST /api/v1/auth/refresh` +- **输入**: Refresh Token +- **输出**: 新 Access Token +- **逻辑**: 校验 Refresh Token 有效性 → 签发新 Access Token + +### 4.2 首页风险总览 + +#### 4.2.1 数据聚合接口 + +- **接口**: `GET /api/v1/dashboard` +- **返回数据**: +```json +{ + "success": true, + "data": { + "greeting": "早上好!今天有 3 件事需要处理", + "stats": { + "employeeCount": 12, + "highRiskCount": 2, + "todoCount": 3, + "monthlyOvertimePay": 8000 + }, + "todos": [ + { + "id": "risk_001", + "level": "high", + "title": "张三入职35天未签合同", + "actionUrl": "/contracts?employee=张三" + } + ], + "riskDistribution": { + "contract": 5, + "salary": 2, + "termination": 1 + }, + "aiPrediction": { + "risks": [...], + "suggestion": "本周优先处理合同到期和未签问题" + } + } +} +``` + +#### 4.2.2 风险检测引擎 + +- **触发时机**: 数据变更时实时检测 + 每日凌晨定时全量扫描 +- **检测规则**: 见 0-req.md 4.3.3 / 4.4.4 合规检查规则 +- **风险生命周期**: `pending` → `resolved`(自动/手动)/ `ignored`(手动) + +### 4.3 合同管理模块 + +#### 4.3.1 API 规格 + +| 接口 | 方法 | 路径 | 说明 | +|------|------|------|------| +| 员工列表 | GET | `/api/v1/employees` | 支持分页、搜索、部门筛选 | +| 添加员工 | POST | `/api/v1/employees` | 含合同信息 | +| 员工详情 | GET | `/api/v1/employees/:id` | 含合同+风险信息 | +| 编辑员工 | PUT | `/api/v1/employees/:id` | 更新员工+合同信息 | +| 删除员工 | DELETE | `/api/v1/employees/:id` | 软删除(status=resigned) | +| 批量续签 | POST | `/api/v1/contracts/batch-renew` | 批量续签合同 | +| 上传附件 | POST | `/api/v1/contracts/:id/attachment` | 上传纸质合同扫描件 | + +#### 4.3.2 合同状态自动计算 + +``` +输入:signDate, startDate, endDate, contractType, renewalCount, hireDate +输出:status + statusText + riskLevel + +逻辑: + if signDate == null: + daysSinceHire = today - hireDate + if daysSinceHire > 365: → "已视为无固定期限" 🔴 + elif daysSinceHire > 30: → "未签合同(X天)" 🔴 + else: → "未签合同(X天)" 🟡 + elif endDate != null: + daysToExpire = endDate - today + if daysToExpire < 0: → "已到期未续签" 🔴 + elif daysToExpire <= 30: → "即将到期(X天)" 🟡 + else: → "正常" 🟢 + else: + → "无固定期限" 🟢 +``` + +#### 4.3.3 试用期合法性校验 + +``` +规则(劳动合同法第19条): + 合同期 < 3个月 → 不能约定试用期 + 合同期 3个月~1年 → 试用期 ≤ 1个月 + 合同期 1年~3年 → 试用期 ≤ 2个月 + 合同期 ≥ 3年 → 试用期 ≤ 6个月 + +校验时机:添加/编辑合同时实时校验 +校验失败:红色提示「X年期合同试用期最多Y个月,当前Z个月不合法」 +``` + +### 4.4 钱的计算模块 + +#### 4.4.1 加班费计算 + +- **页面**: `/money` Tab 1 +- **输入**: 月工资、工作日加班小时、休息日加班小时、节假日加班小时 +- **计算**: 纯前端实时计算,无需调后端 +- **公式**: +``` +hourlyWage = monthlyWage / 21.75 / 8 +weekdayPay = hourlyWage * 1.5 * weekdayHours +weekendPay = hourlyWage * 2.0 * weekendHours +holidayPay = hourlyWage * 3.0 * holidayHours +total = weekdayPay + weekendPay + holidayPay +``` +- **预警**: 总加班小时 > 36 → 黄色警告 + +#### 4.4.2 双倍工资计算 + +- **页面**: `/money` Tab 2 +- **输入**: 月工资、入职日期、合同签订日期(可选,默认未签订) +- **计算**: 纯前端实时计算 +- **公式**: +``` +if 未签订 or 签订日期 - 入职日期 > 30天: + 起算日 = 入职日 + 1个月 + 截止日 = 入职日 + 1年(如未签订)或 签订日期 + 月数 = min(截止日 - 起算日 的月数, 11) + 双倍工资差额 = 月工资 × 月数 +``` + +#### 4.4.3 经济补偿金计算 + +- **页面**: `/money` Tab 3 +- **输入**: 入职日期、离职日期、月平均工资、离职原因、当地社平工资(选填) +- **计算**: 纯前端实时计算 +- **公式**: +``` +工作年限 = (离职日期 - 入职日期) 转换为年月 + 满1年 → 1个月工资 + 满6个月不满1年 → 1个月工资 + 不满6个月 → 0.5个月工资 +补偿月数 = 向上取整(工作年限月数 / 12) 或 半月 + +if 社平工资 > 0 and 月工资 > 社平工资 × 3: + 月工资 = 社平工资 × 3 + 补偿月数 = min(补偿月数, 12) + +经济补偿金 = 月工资 × 补偿月数 +违法解除赔偿金 = 经济补偿金 × 2 +``` + +### 4.5 解聘助手模块 + +#### 4.5.1 解聘向导状态管理 + +```typescript +interface TerminationWizardState { + step: 1 | 2 | 3 | 4 | 5; + reason: 'negotiated' | 'fault' | 'nonfault' | 'layoff' | 'expired' | null; + employeeId: string | null; + terminationDate: string | null; + checklist: { + item: string; + passed: boolean; + remark?: string; + }[]; + compensation: number; + riskLevel: 'safe' | 'warning' | 'danger'; +} +``` + +#### 4.5.2 动态检查项规则 + +| 解聘原因 | 检查项 | +|---------|--------| +| 协商解除 | 是否支付经济补偿金、是否签署协商解除协议 | +| 员工犯错 | 是否有规章制度依据、是否有证据材料、是否通知工会 | +| 员工没犯错但干不了 | 是否提前30天通知或支付代通知金、是否经过培训/调岗 | +| 公司裁员 | 是否提前30天向工会说明、是否听取职工意见、是否报劳动部门 | +| 合同到期不续签 | 是否提前通知、是否支付经济补偿金 | + +#### 4.5.3 禁止解聘情形检查 + +- **触发**: Step 2 选择员工后自动检查 +- **检查字段**: `employee.isPregnant` / `employee.isWorkInjured` / `employee.isInMedicalPeriod` +- **交互**: 弹出红色警告框 + 「我已了解风险,继续操作」确认按钮 +- **不阻止流程**: 用户确认后可继续 + +### 4.6 AI 合规顾问模块 + +#### 4.6.1 智能问答 + +- **页面**: `/ai-assistant` +- **接口**: `POST /api/v1/ai/chat`(SSE 流式) +- **请求**: +```json +{ + "messages": [ + {"role": "user", "content": "试用期最长可以约定几个月?"} + ], + "context": { + "orgId": "xxx", + "employeeCount": 12, + "riskItems": [...] + } +} +``` +- **后端处理**: + 1. 构建系统 Prompt(劳动法专家角色 + 人话风格要求) + 2. RAG 检索相关法律条文(pgvector 语义搜索) + 3. 注入企业数据上下文 + 4. 调用 DashScope API(qwen-plus) + 5. SSE 流式返回前端 +- **响应**: SSE 事件流 +``` +data: {"type": "chunk", "content": "根据"} +data: {"type": "chunk", "content": "《劳动合同法》"} +data: {"type": "chunk", "content": "第19条"} +... +data: {"type": "done", "legalBasis": "《劳动合同法》第19条"} +``` + +#### 4.6.2 风险预测 + +- **触发**: 每日凌晨定时任务 + 首页加载时读取缓存 +- **接口**: `GET /api/v1/ai/prediction` +- **逻辑**: + 1. 查询未来30天将到期的合同 + 2. 查询入职将满1年未签合同的员工 + 3. 分析上月加班趋势变化 + 4. 调用 LLM 生成优先级建议 +- **缓存**: 结果存 Redis / 内存缓存,24h 有效 + +#### 4.6.3 合同审查 + +- **页面**: `/ai-assistant` 子 Tab +- **接口**: `POST /api/v1/ai/contract-review` +- **输入**: 合同文本(粘贴或文件上传解析) +- **后端处理**: + 1. 调用 qwen-max 分析合同条款 + 2. 逐条标注红/黄/绿 + 修改建议 + 3. 计算合规评分(0-100) +- **输出**: +```json +{ + "success": true, + "data": { + "score": 72, + "items": [ + {"level": "red", "clause": "试用期6个月", "issue": "超过法定上限", "suggestion": "调整为2个月"}, + {"level": "yellow", "clause": "竞业限制", "issue": "未约定补偿标准", "suggestion": "约定月补偿不低于离职前12个月平均工资的30%"} + ] + } +} +``` + +#### 4.6.4 案例匹配 + +- **页面**: `/ai-assistant` 子 Tab +- **接口**: `POST /api/v1/ai/case-match` +- **输入**: 争议情况描述 +- **后端处理**: + 1. 将描述向量化(DashScope text-embedding-v2) + 2. pgvector 检索相似案例(top 5) + 3. 调用 qwen-max 分析败诉概率和赔偿预估 +- **输出**: 相似案例列表 + 败诉概率 + 赔偿预估 + 建议 + +#### 4.6.5 使用次数限制 + +- **中间件**: 每次 AI 请求前检查当月已用次数 +- **计数**: Redis / 数据库按 `orgId + 月份 + 类型` 统计 +- **超限**: 返回 `429 Too Many Requests` + 提示升级套餐 + +--- + +## 5. 页面规格 + +### 5.1 页面清单 + +**管理端**: + +| 页面 | 路由 | 访问控制 | 布局 | +|------|------|---------|------| +| 登录 | `/login` | 公开 | 居中卡片 | +| 注册 | `/register` | 公开 | 居中卡片 | +| 忘记密码 | `/forgot-password` | 公开 | 居中卡片 | +| 风险总览 | `/` | 登录 | 顶部导航 + 主内容 | +| 合同管理 | `/contracts` | 登录 | 顶部导航 + 主内容 | +| 钱的计算 | `/money` | 登录 | 顶部导航 + 主内容 | +| 解聘助手 | `/termination` | 登录 | 顶部导航 + 主内容 | +| AI 合规顾问 | `/ai-assistant` | 登录 | 顶部导航 + 主内容 | +| 系统设置 | `/settings` | 登录(admin) | 顶部导航 + 主内容 | + +**员工端**: + +| 页面 | 路由 | 访问控制 | 布局 | +|------|------|---------|------| +| 员工登录 | `/portal/login` | 公开 | 居中卡片 | +| 工资条 | `/portal/payslip` | 员工Token | 极简布局 | +| 我的合同 | `/portal/contract` | 员工Token | 极简布局 | +| 入职填报 | `/portal/onboarding` | Token链接 | 极简布局 | +| 合同确认 | `/portal/contract-confirm` | Token链接 | 极简布局 | + +### 5.2 响应式断点 + +| 断点 | 宽度 | 布局变化 | +|------|------|---------| +| 桌面 | ≥1280px | 顶部导航 + 960px 居中内容 | +| 平板 | 768-1279px | 顶部导航 + 全宽内容 | +| 手机 | 375-767px | 底部 Tab Bar + 全宽内容 | + +### 5.3 空状态设计 + +| 场景 | 展示内容 | +|------|---------| +| 首页无员工 | 插图 + 「添加第一个员工」按钮 + 示例截图 | +| 合同列表无数据 | 插图 + 「还没有员工,点这里添加」按钮 | +| 无风险 | 绿色大勾 + 「✅ 暂无风险,继续保持!」 | +| AI 顾问无对话 | 欢迎语 + 预设问题快捷按钮 | +| 解聘无历史记录 | 插图 + 「还没有解聘记录」文字 | +| 员工端无工资条 | 插图 + 「暂无工资记录」文字 | +| 员工端无合同 | 插图 + 「暂无合同信息,请联系 HR」文字 | +| 入职填报链接失效 | 提示「链接已过期,请联系 HR 重新发送」 | +| 合同确认链接失效 | 提示「链接已过期,请联系 HR 重新发送」 | + +--- + +## 6. API 规格 + +### 6.1 统一规范 + +- **前缀**: `/api/v1/` +- **认证**: `Authorization: Bearer ` +- **响应格式**: +```json +{ + "success": true, + "data": {}, + "error": null +} +``` +- **错误格式**: +```json +{ + "success": false, + "data": null, + "error": { + "code": "VALIDATION_ERROR", + "message": "手机号格式不正确" + } +} +``` +- **分页**: `?page=1&pageSize=20` → 返回 `{ items: [], total: 100, page: 1, pageSize: 20 }` + +### 6.2 API 清单 + +| 模块 | 方法 | 路径 | 说明 | +|------|------|------|------| +| 认证 | POST | `/auth/register` | 注册 | +| 认证 | POST | `/auth/login` | 登录 | +| 认证 | POST | `/auth/refresh` | 刷新 Token | +| 认证 | GET | `/auth/me` | 获取当前用户 | +| Dashboard | GET | `/dashboard` | 首页数据聚合 | +| 员工 | GET | `/employees` | 员工列表 | +| 员工 | POST | `/employees` | 添加员工 | +| 员工 | GET | `/employees/:id` | 员工详情 | +| 员工 | PUT | `/employees/:id` | 编辑员工 | +| 员工 | DELETE | `/employees/:id` | 删除员工(软删除) | +| 合同 | POST | `/contracts/batch-renew` | 批量续签 | +| 合同 | POST | `/contracts/:id/attachment` | 上传扫描件 | +| 加班 | GET | `/overtime` | 加班记录列表 | +| 加班 | POST | `/overtime` | 添加加班记录 | +| 解聘 | GET | `/termination` | 解聘记录列表 | +| 解聘 | POST | `/termination` | 创建解聘记录 | +| 风险 | GET | `/risks` | 风险列表 | +| 风险 | PUT | `/risks/:id` | 更新风险状态 | +| AI | POST | `/ai/chat` | 智能问答(SSE) | +| AI | GET | `/ai/prediction` | 风险预测 | +| AI | POST | `/ai/contract-review` | 合同审查 | +| AI | POST | `/ai/case-match` | 案例匹配 | +| 设置 | GET | `/settings/org` | 企业信息 | +| 设置 | PUT | `/settings/org` | 更新企业信息 | +| 设置 | GET | `/settings/users` | 用户列表 | +| 设置 | POST | `/settings/users` | 添加用户 | +| 设置 | PUT | `/settings/users/:id` | 编辑用户 | +| 设置 | DELETE | `/settings/users/:id` | 移除用户 | +| 员工端 | POST | `/portal/auth/login` | 手机号+密码登录 | +| 员工端 | POST | `/portal/auth/send-code` | 发送验证码(v1.0 页面内显示) | +| 员工端 | POST | `/portal/auth/verify` | 验证码登录 | +| 员工端 | POST | `/portal/auth/change-password` | 修改密码 | +| 员工端 | GET | `/portal/payslip` | 工资条列表 | +| 员工端 | GET | `/portal/payslip/:month` | 指定月工资明细 | +| 员工端 | POST | `/portal/payslip/:month/confirm` | 确认已阅工资条 | +| 员工端 | GET | `/portal/contract` | 我的合同信息 | +| 员工端 | GET | `/portal/onboarding/:token` | 获取入职填报信息 | +| 员工端 | POST | `/portal/onboarding/:token` | 提交入职填报 | +| 员工端 | GET | `/portal/contract-confirm/:token` | 获取合同确认信息 | +| 员工端 | POST | `/portal/contract-confirm/:token` | 确认签署合同 | +| 管理端 | POST | `/employees/:id/generate-onboarding-qr` | 生成入职填报二维码 | +| 管理端 | POST | `/contracts/:id/generate-confirm-qr` | 生成合同确认二维码 | +| 管理端 | GET | `/payslip/confirm-status` | 工资条确认状态 | + +--- + +## 7. 数据库设计 + +### 7.1 Prisma Schema 概要 + +```prisma +// 核心表 +model Organization { + id String @id @default(cuid()) + name String + plan Plan @default(FREE) + maxEmployees Int @default(20) + city String? + createdAt DateTime @default(now()) + users User[] + employees Employee[] + contracts LaborContract[] + overtimeRecords OvertimeRecord[] + terminations TerminationRecord[] + riskItems RiskItem[] + auditLogs AuditLog[] +} + +model User { + id String @id @default(cuid()) + orgId String + org Organization @relation(fields: [orgId], references: [id]) + phone String @unique + email String? + passwordHash String + name String + role Role @default(ADMIN) + createdAt DateTime @default(now()) + lastLoginAt DateTime? +} + +// 业务表 +model Employee { + id String @id @default(cuid()) + orgId String + org Organization @relation(fields: [orgId], references: [id]) + name String + department String + hireDate DateTime + monthlySalary String // AES-256 加密存储 + status EmployeeStatus @default(ACTIVE) + gender String? + isPregnant Boolean @default(false) + isInMedicalPeriod Boolean @default(false) + isWorkInjured Boolean @default(false) + phone String? + createdBy String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + contracts LaborContract[] + overtimeRecords OvertimeRecord[] + terminations TerminationRecord[] +} + +model LaborContract { + id String @id @default(cuid()) + orgId String + org Organization @relation(fields: [orgId], references: [id]) + employeeId String + employee Employee @relation(fields: [employeeId], references: [id]) + signDate DateTime? + startDate DateTime + endDate DateTime? + contractType ContractType + signMethod SignMethod @default(PAPER) + contractYears Int @default(3) + probationMonths Int @default(0) + probationSalary Int @default(0) + renewalCount Int @default(0) + attachmentName String? + attachmentUrl String? + electronicContractNo String? + electronicContractUrl String? + createdBy String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +// 其余表参考 0-req.md 第5章数据模型 +``` + +### 7.2 枚举定义 + +```prisma +enum Plan { FREE PRO ENTERPRISE } +enum Role { ADMIN HR VIEWER } +enum EmployeeStatus { ACTIVE RESIGNED } +enum ContractType { FIXED UNFIXED UNSIGNED } +enum SignMethod { PAPER ELECTRONIC } +enum RiskType { CONTRACT SALARY TERMINATION } +enum RiskLevel { HIGH MEDIUM LOW } +enum RiskStatus { PENDING RESOLVED IGNORED } +enum TerminationReason { NEGOTIATED FAULT NONFAULT LAYOFF EXPIRED } +enum RiskAssessment { SAFE WARNING DANGER } +enum OnboardingStatus { PENDING APPROVED REJECTED } +enum ContractConfirmStatus { UNCONFIRMED CONFIRMED EXPIRED } + +// 员工端扩展表 +model Payslip { + id String @id @default(cuid()) + orgId String + employeeId String + employee Employee @relation(fields: [employeeId], references: [id]) + month String // YYYY-MM + baseSalary Decimal // 基本工资 + overtimePay Decimal // 加班费合计 + weekdayOvertimePay Decimal + weekendOvertimePay Decimal + holidayOvertimePay Decimal + totalPay Decimal // 应发合计 + confirmedAt DateTime? // 员工确认时间 + confirmedIp String? // 确认 IP + createdAt DateTime @default(now()) + + @@unique([employeeId, month]) +} + +model OnboardingLink { + id String @id @default(cuid()) + orgId String + employeeId String? // 关联员工(审核通过后关联) + token String @unique // 一次性 token + status OnboardingStatus @default(PENDING) + phone String // 员工手机号 + expiresAt DateTime // 24h 有效 + submittedAt DateTime? // 员工提交时间 + submittedData Json? // 员工填报数据 + reviewedBy String? // 审核人 userId + reviewedAt DateTime? // 审核时间 + createdAt DateTime @default(now()) +} + +model ContractConfirmLink { + id String @id @default(cuid()) + orgId String + contractId String + token String @unique // 一次性 token + status ContractConfirmStatus @default(UNCONFIRMED) + employeePhone String // 员工手机号 + expiresAt DateTime // 7天有效 + confirmedAt DateTime? // 员工确认时间 + confirmedIp String? // 确认 IP + confirmedDevice String? // 设备信息 + createdAt DateTime @default(now()) +} +``` + +--- + +## 8. 前端架构 + +### 8.1 项目结构 + +``` +frontend/ +├── src/ +│ ├── main.tsx # 入口 +│ ├── App.tsx # 路由定义 +│ ├── components/ # 通用组件 +│ │ ├── layout/ +│ │ │ ├── TopNav.tsx # 顶部导航 +│ │ │ ├── MobileTabBar.tsx # 移动端底部导航 +│ │ │ └── PageContainer.tsx +│ │ ├── ui/ +│ │ │ ├── Button.tsx +│ │ │ ├── Card.tsx +│ │ │ ├── Signal.tsx # 信号灯组件 +│ │ │ ├── Modal.tsx +│ │ │ ├── Input.tsx +│ │ │ ├── Select.tsx +│ │ │ ├── DatePicker.tsx +│ │ │ └── EmptyState.tsx +│ │ └── shared/ +│ │ ├── RiskCard.tsx +│ │ ├── TodoList.tsx +│ │ └── ProgressBar.tsx +│ ├── pages/ +│ │ ├── auth/ +│ │ │ ├── Login.tsx +│ │ │ ├── Register.tsx +│ │ │ └── ForgotPassword.tsx +│ │ ├── Dashboard.tsx +│ │ ├── Contracts.tsx +│ │ ├── Money.tsx +│ │ ├── Termination.tsx +│ │ ├── AIAssistant.tsx +│ │ ├── Settings.tsx +│ │ └── portal/ +│ │ ├── PortalLogin.tsx # 员工端登录 +│ │ ├── Payslip.tsx # 工资条 +│ │ ├── MyContract.tsx # 我的合同 +│ │ ├── Onboarding.tsx # 入职填报 +│ │ └── ContractConfirm.tsx # 合同确认 +│ ├── hooks/ +│ │ ├── useAuth.ts +│ │ ├── useApi.ts +│ │ └── useRiskEngine.ts +│ ├── lib/ +│ │ ├── api.ts # Axios 实例 + 拦截器 +│ │ ├── calculator.ts # 纯前端计算逻辑 +│ │ ├── riskEngine.ts # 风险检测引擎 +│ │ └── utils.ts +│ ├── store/ +│ │ └── authStore.ts # Zustand 状态管理 +│ └── types/ +│ └── index.ts # TypeScript 类型定义 +├── package.json +├── vite.config.ts +├── tailwind.config.ts +└── tsconfig.json +``` + +### 8.2 状态管理 + +- **认证状态**: Zustand(user, token, isAuthenticated) +- **服务端数据**: TanStack Query(React Query)缓存 + 自动刷新 +- **表单状态**: React Hook Form + Zod 校验 +- **AI 对话**: 本地 useState 管理消息列表 + SSE 流式追加 + +### 8.3 路由守卫 + +```typescript +// ProtectedRoute:未登录 → 跳转 /login +// AdminRoute:非 admin → 跳转 / +// PublicRoute:已登录 → 跳转 / +``` + +--- + +## 9. 后端架构 + +### 9.1 项目结构 + +``` +backend/ +├── src/ +│ ├── index.ts # 入口 +│ ├── app.ts # Express 应用 +│ ├── routes/ +│ │ ├── auth.routes.ts +│ │ ├── employee.routes.ts +│ │ ├── contract.routes.ts +│ │ ├── overtime.routes.ts +│ │ ├── termination.routes.ts +│ │ ├── risk.routes.ts +│ │ ├── ai.routes.ts +│ │ ├── settings.routes.ts +│ │ └── portal.routes.ts # 员工端路由 +│ ├── middleware/ +│ │ ├── auth.ts # JWT 校验 +│ │ ├── orgFilter.ts # 多租户 orgId 注入 +│ │ ├── rateLimit.ts # 限流 +│ │ ├── errorHandler.ts # 统一错误处理 +│ │ └── auditLog.ts # 审计日志 +│ ├── services/ +│ │ ├── auth.service.ts +│ │ ├── employee.service.ts +│ │ ├── contract.service.ts +│ │ ├── risk.service.ts +│ │ ├── ai.service.ts # DashScope 调用 +│ │ ├── rag.service.ts # RAG 检索 +│ │ ├── portal.service.ts # 员工端服务 +│ │ └── qrcode.service.ts # 二维码生成 +│ ├── lib/ +│ │ ├── prisma.ts # Prisma 客户端 +│ │ ├── jwt.ts # JWT 工具 +│ │ ├── crypto.ts # AES-256 加解密 +│ │ └── dashscope.ts # 通义千问 SDK 封装 +│ ├── validators/ +│ │ ├── auth.validator.ts # Zod schema +│ │ ├── employee.validator.ts +│ │ └── ... +│ └── jobs/ +│ ├── riskScan.ts # 定时风险扫描 +│ └── aiPrediction.ts # 定时 AI 预测 +├── prisma/ +│ ├── schema.prisma +│ └── migrations/ +├── package.json +└── .env +``` + +### 9.2 环境变量 + +```env +# 数据库 +DATABASE_URL=postgresql://... + +# JWT +JWT_SECRET=... +JWT_REFRESH_SECRET=... + +# DashScope (通义千问) +DASHSCOPE_API_KEY=sk-xxx +DASHSCOPE_BASE_URL=https://dashscope.aliyuncs.com/api/v1 + +# 加密 +ENCRYPTION_KEY=... # AES-256 工资字段加密 + +# 存储 +SUPABASE_URL=... +SUPABASE_KEY=... + +# 二维码(前端生成,无需后端服务) +# 使用 qrcode.react 库在前端直接生成 + +# 部署 +PORT=3000 +CORS_ORIGIN=https://your-app.vercel.app +``` + +--- + +## 10. 验收标准 + +### 10.1 功能验收 + +| 模块 | 验收项 | 验收标准 | +|------|--------|---------| +| 注册 | 手机号注册 | 输入合法信息 → 创建成功 → 自动登录 | +| 登录 | 密码登录 | 正确密码 → 返回 Token;错误密码 → 提示错误 | +| 首页 | 数据聚合 | 登录后 → 显示员工数/风险数/待办/分布 | +| 合同 | 添加员工 | 4项必填 → 保存成功 → 列表显示 | +| 合同 | 信号灯状态 | 未签合同→🔴;即将到期→🟡;正常→🟢 | +| 合同 | 一键续签 | 点击续签→确认→状态变绿→风险消除 | +| 合同 | 纸质/电子 | 选择纸质→显示上传按钮;选择电子→显示编号/链接输入 | +| 钱的计算 | 加班费实时计算 | 输入数字→右侧结果实时更新 | +| 钱的计算 | 加班超时预警 | 总小时>36→黄色警告 | +| 解聘 | 5步向导 | 每步显示进度条→下一步→最终保存 | +| 解聘 | 禁止情形检查 | 选孕期员工→红色警告弹窗 | +| AI | 智能问答 | 输入问题→流式回答→附法律依据 | +| AI | 合同审查 | 粘贴合同→逐条标注→合规评分 | +| AI | 案例匹配 | 输入情况→相似案例→败诉概率 | +| 设置 | 用户管理 | admin可添加用户→分配角色 | +| 员工端 | 密码登录 | 输入手机号+密码→登录成功 | +| 员工端 | 验证码登录 | 输入手机号→收到验证码→登录成功 | +| 员工端 | 扫码进入 | HR 发二维码→员工扫码→打开员工端 | +| 员工端 | 工资条查看 | 选择月份→显示工资明细→确认已阅 | +| 员工端 | 合同查看 | 显示合同信息+扫描件+签署记录 | +| 员工端 | 入职填报 | 扫码→填写信息→提交→HR审核入库 | +| 员工端 | 合同确认 | 扫码→查看合同→勾选确认→记录时间IP | +| 管理端 | 生成填报二维码 | 添加员工→选生成二维码→显示二维码+链接→微信发送 | +| 管理端 | 生成确认二维码 | 合同详情→生成确认二维码→微信发送→员工确认后状态更新 | + +### 10.2 非功能验收 + +| 类别 | 验收标准 | +|------|---------| +| 性能 | 首屏加载 < 2s,API 响应 < 500ms,AI 首 token < 3s | +| 安全 | 密码 bcrypt 加密,工资 AES-256 加密,JWT 认证,orgId 隔离 | +| 响应式 | 桌面/平板/手机三端可用,导航自适应 | +| 兼容 | Chrome/Edge/Safari 最新版正常 | +| 数据隔离 | A 企业用户无法访问 B 企业数据 | +| 员工端隔离 | 员工只能查看自己的数据,不能查看他人 | +| 链接安全 | 入职/确认链接含一次性 token,过期失效 | + +--- + +## 11. 发布计划 + +### 11.1 v1.0 发布范围 + +| 阶段 | 内容 | 预估工期 | +|------|------|---------| +| P0 | 项目搭建 + 路由骨架 + Prisma Schema | 2天 | +| P1 | 认证体系(注册/登录/JWT/路由守卫) | 2天 | +| P2 | 首页风险总览 + 风险检测引擎 | 2天 | +| P3 | 合同管理(列表/添加/续签/纸质电子) | 3天 | +| P4 | 钱的计算(3 Tab 计算器) | 2天 | +| P5 | 解聘助手(5步向导 + 禁止检查) | 2天 | +| P6 | AI 合规顾问(问答/预测/审查/案例 + RAG) | 4天 | +| P7 | 员工端(验证码登录 + 工资条 + 合同 + 入职填报 + 合同确认) | 3天 | +| P8 | 系统设置 + 新手引导 + 空状态 | 1天 | +| P9 | 移动端适配 + 联调 | 2天 | +| P10 | 部署上线 + 验证 | 1天 | +| **合计** | | **~24天** | + +### 11.2 后续版本 + +| 版本 | 内容 | 预估 | +|------|------|------| +| v2.0 | 社保公积金模块 | +2周 | +| v3.0 | 人力成本分析 + 员工自助门户增强(考勤/请假) | +3周 | + +--- + +## 12. 风险与对策 + +| 风险 | 影响 | 对策 | +|------|------|------| +| 通义千问 API 不稳定 | AI 功能不可用 | 降级为规则引擎回答 + 重试机制 | +| 法律规则地区差异大 | 计算结果不准 | 提供城市选择 + 默认值 + 用户可修改 | +| RAG 知识库构建耗时 | P6 延期 | 先用 Prompt 内嵌法条,后续再建向量库 | +| 中小企业付费意愿低 | 商业化困难 | free 套餐足够基础使用,AI 功能促付费 | +| 数据安全合规要求 | 法律风险 | 数据加密 + 身份证号加密存储 + 隐私协议 | +| 员工端使用率低 | 功能闲置 | HR 主动生成二维码通过微信发给员工,降低使用门槛 | diff --git a/2-task.md b/2-task.md new file mode 100644 index 0000000..e656387 --- /dev/null +++ b/2-task.md @@ -0,0 +1,762 @@ +# 劳动用工合规助手 — 开发任务清单 + +> **文档编号**: 2-task.md +> **版本**: v1.0 +> **日期**: 2026-07-23 +> **状态**: 开发中 +> **依据**: 0-req.md v3.0 需求规格说明书 / 1-prd.md v1.0 产品需求文档 + +--- + +## 任务总览 + +| 阶段 | 内容 | 预估工期 | 任务数 | +|------|------|---------|--------| +| P0 | 项目搭建 + 路由骨架 + Prisma Schema | 2天 | 8 | ✅ 已完成 | +| P1 | 认证体系(注册/登录/JWT/路由守卫) | 2天 | 7 | ✅ 已完成 | +| P2 | 首页风险总览 + 风险检测引擎 | 2天 | 6 | ✅ 已完成 | +| P3 | 合同管理(列表/添加/续签/纸质电子) | 3天 | 10 | ✅ 已完成 | +| P4 | 钱的计算(3 Tab 计算器 + 加班费保存 + 工资条管理) | 2天 | 5 | ✅ 已完成 | +| P5 | 解聘助手(5步向导 + 禁止检查) | 2天 | 6 | ✅ 已完成 | +| P6 | AI 合规顾问(问答/预测/审查/案例 + RAG) | 4天 | 9 | ✅ 已完成 | +| P7 | 员工端(密码/验证码登录 + 工资条 + 合同 + 入职填报 + 合同确认) | 3天 | 10 | ✅ 已完成 | +| P8 | 系统设置 + 新手引导 + 空状态 | 1天 | 5 | ✅ 已完成 | +| P9 | 移动端适配 + 联调 | 2天 | 4 | ✅ 已完成 | +| P10 | 部署上线 + 验证 | 1天 | 4 | ⏳ 进行中 | +| P11 | 功能补齐(社保公积金 + 到期提醒 + 批量工资条 + Excel导入 + 员工档案附件) | 3天 | 10 | ✅ 已完成 | +| **合计** | | **~27天** | **84** | | + +--- + +## P0 — 项目搭建 + 路由骨架 + Prisma Schema(2天) + +### 前端 + +- [x] **T-P0-01** 初始化前端项目 + - Vite + React 18 + TypeScript + - 安装 TailwindCSS + PostCSS + - 配置路径别名 `@/` → `src/` + - 安装核心依赖:react-router-dom, axios, zustand, @tanstack/react-query, react-hook-form, zod, lucide-react, qrcode.react + - **产出**: `package.json`, `vite.config.ts`, `tailwind.config.ts`, `tsconfig.json` + +- [x] **T-P0-02** 前端项目结构搭建 + - 创建目录结构:`components/`, `pages/`, `hooks/`, `lib/`, `store/`, `types/` + - 创建 `App.tsx` 路由骨架(含管理端 + 员工端路由定义) + - 创建 `main.tsx` 入口 + - 创建 `lib/api.ts`(Axios 实例 + 请求/响应拦截器) + - 创建 `types/index.ts`(TypeScript 类型定义) + - **产出**: 项目目录结构 + 路由配置 + +- [x] **T-P0-03** 前端布局组件 + - `TopNav.tsx`:顶部导航栏(Logo + 5 Tab + 风险角标 + 用户头像下拉) + - `MobileTabBar.tsx`:移动端底部导航 + - `PageContainer.tsx`:主内容区容器(max-width 960px 居中) + - `ui/Button.tsx`, `ui/Card.tsx`, `ui/Input.tsx`, `ui/Select.tsx`, `ui/Modal.tsx`, `ui/Signal.tsx`, `ui/EmptyState.tsx` + - **产出**: 通用组件库 + +### 后端 + +- [x] **T-P0-04** 初始化后端项目 + - Node.js + Express + TypeScript + - 安装核心依赖:prisma, @prisma/client, zod, jsonwebtoken, bcryptjs, cors, helmet, morgan, express-rate-limit + - 配置 ts-node-dev 热重载 + - **产出**: `package.json`, `tsconfig.json`, `.env.example` + +- [x] **T-P0-05** 后端项目结构搭建 + - 创建目录结构:`routes/`, `middleware/`, `services/`, `lib/`, `validators/`, `jobs/` + - `app.ts`:Express 应用(CORS + helmet + JSON 解析 + 路由挂载) + - `index.ts`:服务入口 + - **产出**: 后端骨架 + 健康检查接口 `/health` + +- [x] **T-P0-06** Prisma Schema 编写 + - 编写完整 `schema.prisma`:Organization, User, Employee, LaborContract, OvertimeRecord, TerminationRecord, RiskItem, AuditLog, Payslip, OnboardingLink, ContractConfirmLink + - 定义所有枚举:Plan, Role, EmployeeStatus, ContractType, SignMethod, RiskType, RiskLevel, RiskStatus, TerminationReason, RiskAssessment, OnboardingStatus, ContractConfirmStatus + - 配置 PostgreSQL 数据源 + - **产出**: `prisma/schema.prisma` + +- [x] **T-P0-07** 数据库迁移 + 种子数据 + - 运行 `prisma migrate dev` 生成初始迁移 + - 编写 `prisma/seed.ts` 种子数据(测试企业 + 员工 + 合同) + - 配置 `prisma.ts` 客户端单例 + - **产出**: 数据库表结构 + 测试数据 + +- [x] **T-P0-08** 中间件骨架 + - `auth.ts`:JWT 校验中间件(从 Header 提取 Token → 验证 → 注入 req.user) + - `orgFilter.ts`:多租户中间件(从 req.user 提取 orgId → 注入 req.orgId) + - `errorHandler.ts`:统一错误处理(Zod 错误 → 422,Prisma 错误 → 400,其他 → 500) + - `rateLimit.ts`:限流中间件(基于 express-rate-limit) + - `auditLog.ts`:审计日志中间件(记录关键操作) + - **产出**: 5 个中间件文件 + +--- + +## P1 — 认证体系(2天) + +- [x] **T-P1-01** 后端:注册接口 + - `POST /api/v1/auth/register` + - 输入校验(Zod):企业名称、手机号、密码(8位+) + - 逻辑:创建 Organization(plan=free, maxEmployees=20)+ User(role=admin, bcrypt 加密) + - 返回:Access Token(2h)+ Refresh Token(7d) + - 限流:同一 IP 每小时 5 次 + - **产出**: `auth.routes.ts` + `auth.service.ts` + `auth.validator.ts` + +- [x] **T-P1-02** 后端:登录接口 + - `POST /api/v1/auth/login` + - 输入校验:手机号、密码 + - 逻辑:查询 User → bcrypt 比对 → 签发 Token + - 限流:同一 IP 每分钟 5 次 + - **产出**: 登录逻辑 + +- [x] **T-P1-03** 后端:Token 刷新 + 当前用户 + - `POST /api/v1/auth/refresh`:校验 Refresh Token → 签发新 Access Token + - `GET /api/v1/auth/me`:返回当前用户信息 + 组织信息 + - **产出**: Token 刷新逻辑 + +- [x] **T-P1-04** 后端:JWT 工具 + - `lib/jwt.ts`:签发/验证 Access Token + Refresh Token + - 密钥从环境变量读取 + - **产出**: `jwt.ts` + +- [x] **T-P1-05** 前端:注册页面 + - `/register` 页面 + - 表单:企业名称、手机号、密码、确认密码 + - React Hook Form + Zod 校验 + - 注册成功 → 存储 Token → 跳转首页 + - **产出**: `Register.tsx` + +- [x] **T-P1-06** 前端:登录页面 + 路由守卫 + - `/login` 页面 + - 表单:手机号、密码 + - `useAuth` Hook(Zustand store:user, token, isAuthenticated) + - `ProtectedRoute`:未登录 → 跳转 `/login` + - `PublicRoute`:已登录 → 跳转 `/` + - Axios 拦截器:401 → 自动刷新 Token / 跳转登录 + - **产出**: `Login.tsx` + `useAuth.ts` + `authStore.ts` + 路由守卫 + +- [x] **T-P1-07** 前端:忘记密码页面 + - `/forgot-password` 页面 + - 手机号 + 验证码 + 新密码 + - **产出**: `ForgotPassword.tsx` + +--- + +## P2 — 首页风险总览 + 风险检测引擎(2天) + +- [x] **T-P2-01** 后端:Dashboard 数据聚合接口 + - `GET /api/v1/dashboard` + - 聚合:员工数、高风险数、待办数、月加班费 + - 生成待办列表(从 RiskItem 查询 pending 状态) + - 风险分布统计(按 type 分组) + - AI 预测数据(从缓存读取,P6 实现) + - **产出**: `dashboard.routes.ts` + `dashboard.service.ts` + +- [x] **T-P2-02** 后端:风险检测引擎 + - `lib/riskEngine.ts` + - 合同风险检测:未签合同(>30天 🔴 / >365天 视为无固定期限 🔴)、即将到期(≤30天 🟡)、已到期 🔴 + - 试用期风险检测:试用期超法定上限 + - 加班风险检测:月加班 > 36h + - 解聘风险检测:禁止解聘情形(孕期/工伤/医疗期) + - 触发时机:数据变更时实时检测 + 定时全量扫描 + - **产出**: `riskEngine.ts` + +- [x] **T-P2-03** 后端:风险 CRUD 接口 + - `GET /api/v1/risks`:风险列表(分页 + 类型筛选 + 状态筛选) + - `PUT /api/v1/risks/:id`:更新风险状态(resolved / ignored + 备注) + - **产出**: `risk.routes.ts` + `risk.service.ts` + +- [x] **T-P2-04** 后端:定时风险扫描任务 + - `jobs/riskScan.ts`:每日凌晨 2:00 全量扫描 + - 使用 node-cron 调度 + - 扫描所有企业的员工/合同 → 生成/更新 RiskItem + - **产出**: `riskScan.ts` + +- [x] **T-P2-05** 前端:首页风险总览页面 + - `/` Dashboard 页面 + - 一句话状态("早上好!今天有 N 件事需要处理") + - 数字卡片:员工数 / 高风险数 / 待办数 / 月加班费 + - 待办列表:每条含风险等级颜色 + 标题 + 「去处理」按钮 + - 风险分布进度条(合同/工资/解聘) + - AI 风险预测卡片(P6 实现后接入) + - **产出**: `Dashboard.tsx` + `TodoList.tsx` + `ProgressBar.tsx` + +- [x] **T-P2-06** 前端:风险角标组件 + - 顶部导航栏红色角标,显示待处理风险总数 + - 点击跳转首页 + - 数据来源:Dashboard 接口或独立计数接口 + - **产出**: `TopNav.tsx` 集成角标 + +--- + +## P3 — 合同管理(3天) + +- [x] **T-P3-01** 后端:员工 CRUD 接口 + - `GET /api/v1/employees`:列表(分页 + 搜索 + 部门筛选) + - `POST /api/v1/employees`:添加员工(含合同信息 + AES-256 加密工资) + - `GET /api/v1/employees/:id`:详情(含合同 + 风险) + - `PUT /api/v1/employees/:id`:编辑 + - `DELETE /api/v1/employees/:id`:软删除(status=resigned) + - **产出**: `employee.routes.ts` + `employee.service.ts` + `employee.validator.ts` + +- [x] **T-P3-02** 后端:AES-256 加密工具 + - `lib/crypto.ts`:加密/解密工资字段 + - 密钥从环境变量 `ENCRYPTION_KEY` 读取 + - **产出**: `crypto.ts` + +- [x] **T-P3-03** 后端:合同状态计算 + - `lib/contractStatus.ts` + - 输入:signDate, startDate, endDate, contractType, renewalCount, hireDate + - 输出:status + statusText + riskLevel + - 逻辑:未签/即将到期/已到期/正常/无固定期限 + - **产出**: `contractStatus.ts` + +- [x] **T-P3-04** 后端:试用期合法性校验 + - 合同期 < 3月 → 不能约定试用期 + - 合同期 3月~1年 → 试用期 ≤ 1月 + - 合同期 1~3年 → 试用期 ≤ 2月 + - 合同期 ≥ 3年 → 试用期 ≤ 6月 + - **产出**: 集成到 `employee.validator.ts` + +- [x] **T-P3-05** 后端:批量续签接口 + - `POST /api/v1/contracts/batch-renew` + - 输入:合同 ID 列表 + 新期限 + - 逻辑:更新 endDate + renewalCount++ + 重新检测风险 + - **产出**: `contract.service.ts` 续签逻辑 + +- [x] **T-P3-06** 后端:合同附件上传 + - `POST /api/v1/contracts/:id/attachment` + - 接收 multipart 文件(纸质合同扫描件) + - 存储到 Supabase Storage / 本地临时目录 + - 更新合同记录 attachmentName + attachmentUrl + - **产出**: 文件上传逻辑 + +- [x] **T-P3-07** 前端:合同管理列表页 + - `/contracts` 页面 + - 员工合同列表:姓名 / 部门 / 合同状态信号灯 / 到期日 / 操作 + - 搜索框 + 部门筛选 + - 信号灯组件(🔴🟡🟢) + - **产出**: `Contracts.tsx` + +- [x] **T-P3-08** 前端:添加/编辑员工表单 + - 模态框表单 + - Step 1 基本信息:姓名*、部门*、手机号、入职日期*、月工资*、性别 + - Step 2 合同信息:合同类型*、签订方式(纸质/电子)、签订日期、起止日期、试用期月数、试用期工资 + - 纸质合同:显示文件上传按钮 + - 电子合同:显示合同编号 + 链接输入 + - 试用期实时校验(红色提示) + - 特殊标记:孕期/工伤/医疗期 复选框 + - **产出**: `EmployeeForm.tsx` + +- [x] **T-P3-09** 前端:一键续签弹窗 + - 选中即将到期的合同 → 点击「续签」 + - 弹窗:显示当前合同信息 + 选择新期限 + - 确认 → 调用批量续签接口 → 刷新列表 + - **产出**: `RenewModal.tsx` + +- [x] **T-P3-10** 前端:合同详情页/弹窗 + - 显示员工信息 + 合同完整信息 + 风险卡片 + - 纸质合同:查看扫描件 + - 电子合同:查看合同链接 + - 操作按钮:编辑 / 续签 / 发送确认二维码(P7 实现) + - **产出**: `ContractDetail.tsx` + +--- + +## P4 — 钱的计算(2天) + +- [x] **T-P4-01** 前端:加班费计算器(含员工关联 + 月份选择 + 保存记录) + - `/money` Tab 1 + - 输入:月工资、工作日加班小时、休息日加班小时、节假日加班小时 + - 公式:hourlyWage = monthlyWage / 21.75 / 8 + - weekdayPay = hourlyWage × 1.5 × weekdayHours + - weekendPay = hourlyWage × 2.0 × weekendHours + - holidayPay = hourlyWage × 3.0 × holidayHours + - 实时计算,右侧显示结果 + - 总加班 > 36h → 黄色警告 + - **产出**: `OvertimeCalculator.tsx` + `lib/calculator.ts` + +- [x] **T-P4-02** 前端:双倍工资计算器 + - `/money` Tab 2 + - 输入:月工资、入职日期、合同签订日期(可选) + - 公式:未签或超 30 天签订 → 起算入职+1月 → 截止入职+1年或签订日 → 双倍工资差额 + - 实时计算 + - **产出**: `DoublePayCalculator.tsx` + +- [x] **T-P4-03** 前端:经济补偿金计算器 + - `/money` Tab 3 + - 输入:入职日期、离职日期、月平均工资、离职原因、社平工资(选填) + - 公式:工作年限 → 补偿月数 → 封顶限制 → 经济补偿金 / 违法解除赔偿金(×2) + - 实时计算 + - **产出**: `CompensationCalculator.tsx` + +- [x] **T-P4-04** 后端:加班记录 CRUD + 工资条管理(`payroll.routes.ts`) + - `GET /api/v1/overtime`:加班记录列表 + - `POST /api/v1/overtime`:添加加班记录 + - **产出**: `overtime.routes.ts` + `overtime.service.ts` + +- [x] **T-P4-05** 前端:计算器工具函数 + 工资条管理 Tab + - `lib/calculator.ts`:纯函数,输入输出明确 + - 编写单元测试验证计算公式正确性 + - 边界用例:0 加班、36h 临界值、社平工资 3 倍封顶 + - **产出**: `calculator.ts` + `calculator.test.ts` + +--- + +## P5 — 解聘助手(2天) + +- [x] **T-P5-01** 后端:解聘记录 CRUD + - `GET /api/v1/termination`:解聘记录列表 + - `POST /api/v1/termination`:创建解聘记录 + - 逻辑:保存向导数据 + 更新员工状态为 resigned + 记录审计日志 + - **产出**: `termination.routes.ts` + `termination.service.ts` + `termination.validator.ts` + +- [x] **T-P5-02** 前端:解聘向导 Step 1 — 选择解聘原因 + - `/termination` 页面 + - 5 个选项卡片:协商解除 / 员工犯错 / 员工没犯错但干不了 / 公司裁员 / 合同到期不续签 + - 每个选项含简短说明 + - **产出**: `Termination.tsx` Step 1 + +- [x] **T-P5-03** 前端:解聘向导 Step 2 — 选择员工 + 禁止情形检查 + - 员工选择下拉框(仅在职员工) + - 选择后自动检查:isPregnant / isWorkInjured / isInMedicalPeriod + - 命中禁止情形 → 红色警告弹窗 + 「我已了解风险,继续操作」 + - **产出**: Step 2 + 禁止情形检查逻辑 + +- [x] **T-P5-04** 前端:解聘向导 Step 3 — 合规检查清单 + - 根据解聘原因动态生成检查项 + - 协商解除:是否支付补偿金 / 是否签署协议 + - 员工犯错:是否有规章制度 / 是否有证据 / 是否通知工会 + - 员工没犯错:是否提前30天通知 / 是否经过培训调岗 + - 公司裁员:是否提前30天向工会说明 / 是否听取意见 / 是否报劳动部门 + - 合同到期:是否提前通知 / 是否支付补偿金 + - 每项 ✅/❌ 选择 + - **产出**: Step 3 + 动态检查项规则 + +- [x] **T-P5-05** 前端:解聘向导 Step 4 — 补偿金计算 + Step 5 — 确认提交 + - Step 4:自动填充员工工资 + 入职日期 → 计算补偿金(复用 P4 计算逻辑) + - Step 5:汇总信息确认 → 提交保存 + - 进度条显示 1/5 ~ 5/5 + - **产出**: Step 4 + Step 5 + +- [x] **T-P5-06** 前端:解聘历史记录 + - `/termination` 页面底部 + - 历史记录列表:员工名 / 解聘日期 / 原因 / 补偿金 / 风险等级 + - 点击查看详情 + - **产出**: `TerminationHistory.tsx` + +--- + +## P6 — AI 合规顾问(4天) + +- [x] **T-P6-01** 后端:DashScope SDK 封装 + - `lib/dashscope.ts` + - 封装通义千问 API 调用(兼容 OpenAI 格式) + - 支持 qwen-plus(日常问答)和 qwen-max(复杂任务) + - 支持 SSE 流式输出 + - API Key 从环境变量 `DASHSCOPE_API_KEY` 读取 + - **产出**: `dashscope.ts` + +- [x] **T-P6-02** 后端:RAG 知识库 — 法律条文向量化 + - `services/rag.service.ts` + - 收集劳动法/劳动合同法/司法解释/地方条例文本 + - 使用 DashScope text-embedding-v2 生成向量 + - 存储到 Supabase pgvector + - 提供语义搜索接口(输入问题 → 检索相关法条) + - **产出**: `rag.service.ts` + 知识库数据 + +- [x] **T-P6-03** 后端:智能问答接口(SSE) + - `POST /api/v1/ai/chat` + - 逻辑: + 1. 构建系统 Prompt(劳动法专家 + 人话风格) + 2. RAG 检索相关法条 + 3. 注入企业数据上下文(员工数/风险项/合同状态) + 4. 调用 qwen-plus SSE 流式返回 + - SSE 事件格式:`data: {"type":"chunk","content":"xxx"}` + - 结束事件:`data: {"type":"done","legalBasis":"..."}` + - **产出**: `ai.routes.ts` + `ai.service.ts` + +- [x] **T-P6-04** 后端:风险预测接口 + - `GET /api/v1/ai/prediction` + - 逻辑: + 1. 查询未来 30 天到期合同 + 2. 查询入职满 1 年未签合同员工 + 3. 分析上月加班趋势 + 4. 调用 LLM 生成优先级建议 + - 缓存 24h + - 定时任务:每日凌晨生成 + - **产出**: 预测逻辑 + `jobs/aiPrediction.ts` + +- [x] **T-P6-05** 后端:合同审查接口 + - `POST /api/v1/ai/contract-review` + - 输入:合同文本(粘贴或文件解析) + - 逻辑:调用 qwen-max 逐条分析 → 标注红/黄/绿 + 修改建议 → 合规评分 + - **产出**: 合同审查逻辑 + +- [x] **T-P6-06** 后端:案例匹配接口 + - `POST /api/v1/ai/case-match` + - 输入:争议情况描述 + - 逻辑:text-embedding-v2 向量化 → pgvector 检索 top 5 → qwen-max 分析败诉概率 + - **产出**: 案例匹配逻辑 + +- [x] **T-P6-07** 后端:AI 使用次数限制 + - 中间件:每次 AI 请求前检查当月已用次数 + - 按 `orgId + 月份 + 类型` 统计 + - free: 10 问答 / 3 审查 / 3 案例 + - pro: 100 / 20 / 20 + - enterprise: 无限 + - 超限 → 429 + 提示升级 + - **产出**: AI 限流中间件 + +- [x] **T-P6-08** 前端:AI 顾问页面 — 智能问答 + - `/ai-assistant` 页面 + - 聊天界面:消息列表 + 输入框 + - 预设问题快捷按钮("试用期最长多久?" "未签合同怎么办?") + - SSE 流式接收:逐字显示打字机效果 + - 法律依据折叠展示 + - 多轮对话(保留上下文 messages) + - **产出**: `AIAssistant.tsx` 聊天 Tab + +- [x] **T-P6-09** 前端:AI 顾问页面 — 合同审查 + 案例匹配 + - 合同审查 Tab:文本框粘贴合同 / 文件上传 → 提交 → 逐条标注展示 + 合规评分 + - 案例匹配 Tab:描述争议情况 → 提交 → 相似案例卡片列表 + 败诉概率 + 赔偿预估 + - **产出**: 合同审查 Tab + 案例匹配 Tab + +--- + +## P7 — 员工端(3天) + +- [x] **T-P7-01** 后端:员工端认证(密码登录 + 验证码登录) + - `POST /api/v1/portal/auth/login`:手机号 + 密码(bcrypt 校验员工密码) + - `POST /api/v1/portal/auth/send-code`:发送验证码(v1.0 页面内显示,存 Redis/内存) + - `POST /api/v1/portal/auth/verify`:验证码登录 + - `POST /api/v1/portal/auth/change-password`:修改密码 + - 员工 Token 与管理端 Token 区分(role=employee) + - **产出**: `portal.routes.ts` 认证部分 + `portal.service.ts` + +- [x] **T-P7-02** 后端:员工端工资条接口 + - `GET /api/v1/portal/payslip`:工资条列表(按月) + - `GET /api/v1/portal/payslip/:month`:指定月工资明细 + - `POST /api/v1/portal/payslip/:month/confirm`:确认已阅(记录时间 + IP) + - 数据隔离:只能查看自己的工资条 + - **产出**: 工资条接口 + +- [x] **T-P7-03** 后端:员工端合同查看接口 + - `GET /api/v1/portal/contract`:当前员工的合同信息(只读) + - 包含:合同类型、期限、试用期、工资、扫描件/电子链接、签署确认记录 + - **产出**: 合同查看接口 + +- [x] **T-P7-04** 后端:入职填报接口 + - `GET /api/v1/portal/onboarding/:token`:根据 token 获取填报信息(企业名等) + - `POST /api/v1/portal/onboarding/:token`:提交填报数据 + - Token 校验:有效性 + 过期检查(24h) + - 提交后状态 → PENDING,HR 审核后 → APPROVED(创建 Employee) + - `POST /api/v1/employees/:id/generate-onboarding-qr`:管理端生成填报 token + - **产出**: 入职填报接口 + OnboardingLink 表操作 + +- [x] **T-P7-05** 后端:合同确认接口 + - `GET /api/v1/portal/contract-confirm/:token`:根据 token 获取合同信息 + - `POST /api/v1/portal/contract-confirm/:token`:确认签署(记录时间 + IP + 设备) + - Token 校验:有效性 + 过期检查(7天) + - 确认后更新合同状态 + ContractConfirmLink 状态 + - `POST /api/v1/contracts/:id/generate-confirm-qr`:管理端生成确认 token + - **产出**: 合同确认接口 + ContractConfirmLink 表操作 + +- [x] **T-P7-06** 后端:二维码生成服务 + - `services/qrcode.service.ts` + - 生成 token + 构建完整 URL(如 `https://xxx/portal/onboarding?token=xxx`) + - 返回 URL 供前端生成二维码图片 + - **产出**: `qrcode.service.ts` + +- [x] **T-P7-07** 前端:员工端登录页面 + - `/portal/login` 页面 + - 双 Tab 切换:[密码登录] [验证码登录] + - 密码登录:手机号 + 密码 + - 验证码登录:手机号 → 获取验证码 → 输入验证码 + - v1.0 验证码页面内弹窗显示 + - **产出**: `PortalLogin.tsx` + +- [x] **T-P7-08** 前端:员工端工资条页面 + - `/portal/payslip` 页面 + - 月份选择器 + - 工资明细卡片:基本工资 + 加班费拆分(工作日/休息日/节假日)+ 应发合计 + - 「确认已阅」按钮 + - 空状态:暂无工资记录 + - **产出**: `Payslip.tsx` + +- [x] **T-P7-09** 前端:员工端合同查看 + 入职填报 + 合同确认页面 + - `/portal/contract`:合同信息只读展示 + 扫描件查看 + 签署记录 + 到期提示 + - `/portal/onboarding`:入职填报表单(姓名/手机号/身份证/银行卡等)+ 提交 + - `/portal/contract-confirm`:合同信息展示 + 查看合同文件 + 勾选确认 + 签署 + - Token 失效页面:链接已过期提示 + - **产出**: `MyContract.tsx` + `Onboarding.tsx` + `ContractConfirm.tsx` + +- [x] **T-P7-10** 前端:管理端二维码生成弹窗 + - 合同管理页:「生成填报二维码」按钮 → 弹窗显示二维码图片 + 可复制链接 + - 合同详情页:「生成确认二维码」按钮 → 弹窗显示二维码图片 + 可复制链接 + - 使用 qrcode.react 生成二维码 + - 保存二维码图片功能 + - **产出**: `QRCodeModal.tsx` + +--- + +## P8 — 系统设置 + 新手引导 + 空状态(1天) + +- [x] **T-P8-01** 后端:系统设置接口 + - `GET /api/v1/settings/org`:企业信息 + - `PUT /api/v1/settings/org`:更新企业信息(名称、城市) + - `GET /api/v1/settings/users`:用户列表 + - `POST /api/v1/settings/users`:添加用户 + - `PUT /api/v1/settings/users/:id`:编辑用户 + - `DELETE /api/v1/settings/users/:id`:移除用户 + - **产出**: `settings.routes.ts` + `settings.service.ts` + +- [x] **T-P8-02** 前端:系统设置页面 + - `/settings` 页面,3 个子 Tab + - 企业信息:名称、城市选择(联动最低工资/社平工资默认值) + - 用户管理:用户列表 + 添加/编辑/移除 + 角色分配(admin/hr/viewer) + - 套餐信息:当前套餐 + 已用人数 + 上限 + 升级按钮 + - **产出**: `Settings.tsx` + +- [x] **T-P8-03** 前端:新手引导弹窗 + - 首次登录显示 3 步引导 + - Step 1:"这里看风险"(指向首页 Tab) + - Step 2:"这里管合同"(指向合同 Tab) + - Step 3:"这里算钱"(指向算钱 Tab) + - localStorage 记录已看过 + - **产出**: `OnboardingGuide.tsx` + +- [x] **T-P8-04** 前端:空状态组件 + - 首页无员工:插图 + 「添加第一个员工」按钮 + - 合同列表无数据:插图 + 「还没有员工,点这里添加」 + - 无风险:绿色大勾 + 「✅ 暂无风险,继续保持!」 + - AI 顾问无对话:欢迎语 + 预设问题 + - 解聘无历史:插图 + 文字 + - 员工端无工资条/合同:插图 + 文字 + - 链接失效:过期提示 + - **产出**: `EmptyState.tsx` 各场景 + +- [x] **T-P8-05** 前端:全局配色 + 样式规范 + - TailwindCSS 配色:主色 #2563EB、危险 #DC2626、警告 #F59E0B、安全 #16A34A、背景 #F8FAFC + - 字体:系统字体栈 + - 圆角:rounded-lg(卡片)/ rounded-md(按钮) + - 阴影:shadow-sm(卡片) + - **产出**: `tailwind.config.ts` 完整配置 + +--- + +## P9 — 移动端适配 + 联调(2天) + +- [x] **T-P9-01** 前端:响应式适配 + - 桌面 ≥1280px:顶部导航 + 960px 居中 + - 平板 768-1279px:顶部导航 + 全宽 + - 手机 375-767px:底部 Tab Bar + 全宽 + - 合同列表 → 移动端卡片式 + - 计算器 → 移动端上下排列 + - AI 聊天 → 移动端全屏 + - 员工端 → 移动端优先(员工主要用手机) + - **产出**: 响应式样式 + +- [x] **T-P9-02** 前端:员工端移动端优化 + - 员工端以移动端为主场景 + - 大按钮、大字体、简洁布局 + - 扫码后自动适配手机屏幕 + - 工资条卡片式展示 + - 合同信息折叠展开 + - **产出**: 员工端移动端样式 + +- [x] **T-P9-03** 全栈:端到端联调 + - 注册 → 登录 → 添加员工 → 查看首页 → 合同管理 → 计算 → 解聘 → AI 问答 → 员工端登录 → 工资条 → 入职填报 → 合同确认 + - 多租户隔离测试:A 企业无法访问 B 企业数据 + - 员工端隔离测试:员工只能查看自己的数据 + - Token 过期自动刷新测试 + - **产出**: 联调问题清单 + 修复 + +- [x] **T-P9-04** 全栈:性能优化 + - 前端:路由懒加载(React.lazy + Suspense) + - 前端:API 请求缓存(React Query staleTime 配置) + - 后端:数据库索引(orgId + 常用查询字段) + - 后端:API 响应压缩(compression 中间件) + - **产出**: 性能优化 + +--- + +## P10 — 部署上线 + 验证(1天) + +- [x] **T-P10-01** 后端:部署配置(`netlify.toml` + `.env.example`) + - 配置 Railway 项目 + - 环境变量配置:DATABASE_URL, JWT_SECRET, DASHSCOPE_API_KEY, ENCRYPTION_KEY, SUPABASE_URL, CORS_ORIGIN + - 运行 Prisma migrate deploy + - 健康检查验证 + - **产出**: 后端线上地址 + +- [x] **T-P10-02** 前端:部署到 Netlify(`netlify.toml` 已配置) + - 配置 Vercel 项目 + - 环境变量配置:VITE_API_URL + - 构建配置:`npm run build` + - 路由重写配置(SPA fallback) + - **产出**: 前端线上地址 + +- [ ] **T-P10-03** 数据库:Neon/Supabase 配置 + - 创建 PostgreSQL 数据库 + - 启用 pgvector 扩展(AI 模块用) + - 配置连接池 + - 运行迁移 + - 导入 RAG 知识库数据 + - **产出**: 数据库线上环境 + +- [ ] **T-P10-04** 验收测试 + - 按 1-prd.md 第 10 章验收标准逐项验证 + - 功能验收:注册/登录/首页/合同/计算/解聘/AI/员工端/设置 + - 非功能验收:性能/安全/响应式/兼容/数据隔离 + - 修复发现的问题 + - **产出**: 验收报告 + +--- + +## 依赖关系 + +``` +P0 ──→ P1 ──→ P2 ──→ P3 ──→ P4(纯前端,可与 P3 并行) + │ + ├──→ P5(依赖 P3 员工数据 + P4 补偿金计算) + │ + ├──→ P6(依赖 P0 数据库 + P2 风险数据) + │ + ├──→ P7(依赖 P3 合同数据 + P0 数据库) + │ + └──→ P8(依赖 P1 认证) + +P9(依赖 P2~P8 全部完成) +P10(依赖 P9 完成) +``` + +**可并行任务**: +- P4(钱的计算)纯前端计算,可在 P3 完成后与 P5/P6 并行 +- P8(系统设置)可在 P6/P7 期间并行 + +--- + +## 技术栈速查 + +| 层 | 技术 | +|-----|------| +| 前端框架 | React 18 + Vite + TypeScript | +| 前端样式 | TailwindCSS | +| 前端路由 | React Router v6 | +| 状态管理 | Zustand(认证)+ TanStack Query(服务端数据)| +| 表单 | React Hook Form + Zod | +| 二维码 | qrcode.react | +| 图标 | lucide-react | +| 后端框架 | Express + TypeScript | +| ORM | Prisma | +| 数据库 | PostgreSQL(Neon/Supabase)+ pgvector | +| 认证 | JWT(Access + Refresh)| +| 加密 | bcrypt(密码)+ AES-256(工资)| +| AI | 通义千问 Qwen(DashScope API)| +| Embedding | DashScope text-embedding-v2 | +| 部署 | Vercel(前端)+ Railway(后端)| + +--- + +## 环境变量清单 + +```env +# 数据库 +DATABASE_URL=postgresql://... + +# JWT +JWT_SECRET=... +JWT_REFRESH_SECRET=... + +# DashScope (通义千问) +DASHSCOPE_API_KEY=sk-xxx +DASHSCOPE_BASE_URL=https://dashscope.aliyuncs.com/api/v1 + +# 加密 +ENCRYPTION_KEY=... + +# 存储 +SUPABASE_URL=... +SUPABASE_KEY=... + +# 部署 +PORT=3000 +CORS_ORIGIN=https://your-app.vercel.app + +# 前端 +VITE_API_URL=https://your-backend.railway.app +``` + +--- + +## P11 — 功能补齐(3天) + +> **目标**: 补齐中小企业 HR 实际使用中的关键缺失功能 + +### 后端 + +- [x] **T-P11-01** 后端:社保公积金计算器 + - Prisma 模型 `SocialInsuranceConfig`(养老/医疗/失业/工伤/生育/公积金 比例 + 基数上下限) + - `social.routes.ts`:GET/PUT 配置 + POST 计算 + - 支持基数封顶/保底逻辑 + - **产出**: `social.routes.ts` + `SocialInsuranceConfig` 模型 + +- [x] **T-P11-02** 后端:到期提醒通知服务 + - Prisma 模型 `NotificationSetting`(通知开关 + 提前天数 + 微信Webhook + 邮箱) + - Prisma 模型 `NotificationLog`(通知记录) + - `notification.routes.ts`:GET/PUT 设置 + GET 日志 + POST 手动检查 + - 支持企业微信 Webhook 推送 + - **产出**: `notification.routes.ts` + `NotificationSetting` + `NotificationLog` 模型 + +- [x] **T-P11-03** 后端:批量生成工资条 + - `POST /api/v1/payroll/payslip/batch-generate` + - 自动遍历所有在职员工,关联加班记录,一键生成全员工资条 + - 支持传入津贴/扣款映射 + - **产出**: `payroll.routes.ts` 新增接口 + +- [x] **T-P11-04** 后端:Excel/CSV 批量导入加班数据 + - `POST /api/v1/payroll/overtime/batch` + - 接收数组格式加班数据,批量 upsert + - 前端解析 CSV 按员工姓名匹配 + - **产出**: `payroll.routes.ts` 新增接口 + +- [x] **T-P11-05** 后端:员工档案附件管理 + - Prisma 模型 `EmployeeAttachment`(文件名/类型/URL/大小) + - `attachment.routes.ts`:GET 列表 + POST 添加 + DELETE 删除 + - 支持身份证/银行卡/合同扫描件/学历证书/其他分类 + - **产出**: `attachment.routes.ts` + `EmployeeAttachment` 模型 + +### 前端 + +- [x] **T-P11-06** 前端:社保公积金计算器 Tab + - Money 页面新增「社保公积金」Tab + - `SocialInsuranceCalculator` 组件:输入缴费基数 → 计算五险一金明细 + - 支持企业/个人比例配置(可展开配置面板) + - 表格展示各险种比例、企业缴纳、个人缴纳 + - **产出**: `Money.tsx` 新增 `SocialInsuranceCalculator` 组件 + +- [x] **T-P11-07** 前端:批量生成工资条 UI + - PayslipManager 新增「一键全员生成」按钮 + - 调用 `batch-generate` 接口,自动关联加班费 + - **产出**: `Money.tsx` PayslipManager 增强 + +- [x] **T-P11-08** 前端:CSV 批量导入加班数据 + - OvertimeCalculator 新增「批量导入加班数据(CSV)」按钮 + - 前端解析 CSV(姓名,工作日加班,休息日加班,节假日加班,月份) + - 按员工姓名自动匹配 employeeId + - **产出**: `Money.tsx` OvertimeCalculator 增强 + +- [x] **T-P11-09** 前端:员工档案附件管理 UI + - Contracts 页面点击员工行打开右侧抽屉 + - `EmployeeDetailDrawer` 组件:展示员工基本信息 + 合同信息 + 附件管理 + - 支持文件上传(FileReader → base64)和删除 + - 附件分类:身份证/银行卡/合同扫描件/学历证书/其他 + - **产出**: `Contracts.tsx` 新增 `EmployeeDetailDrawer` 组件 + +- [x] **T-P11-10** 前端:通知设置页面 + - Settings 页面新增「通知设置」Tab + - `NotificationSettings` 组件:合同到期提醒/未签提醒/加班超时/工资条通知开关 + - 提前提醒天数配置 + - 企业微信 Webhook 配置 + - 邮件通知配置 + - 手动触发合同到期检查 + 通知日志展示 + - **产出**: `Settings.tsx` 新增 `NotificationSettings` 组件 diff --git a/backend/.env.example b/backend/.env.example new file mode 100644 index 0000000..490abd0 --- /dev/null +++ b/backend/.env.example @@ -0,0 +1,21 @@ +# 数据库 +DATABASE_URL=postgresql://postgres:postgres@localhost:5432/hr_compliance?schema=public + +# JWT +JWT_SECRET=your-jwt-secret-change-in-production +JWT_REFRESH_SECRET=your-refresh-secret-change-in-production + +# DashScope (通义千问) +DASHSCOPE_API_KEY=sk-xxx +DASHSCOPE_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1 + +# 加密 +ENCRYPTION_KEY=your-32-byte-encryption-key-here + +# 存储 +SUPABASE_URL= +SUPABASE_KEY= + +# 部署 +PORT=3000 +CORS_ORIGIN=http://localhost:5173 diff --git a/backend/package-lock.json b/backend/package-lock.json new file mode 100644 index 0000000..e859b6d --- /dev/null +++ b/backend/package-lock.json @@ -0,0 +1,2725 @@ +{ + "name": "hr-compliance-backend", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "hr-compliance-backend", + "version": "1.0.0", + "dependencies": { + "@prisma/client": "^5.18.0", + "bcryptjs": "^2.4.3", + "compression": "^1.7.4", + "cors": "^2.8.5", + "express": "^4.19.0", + "express-rate-limit": "^7.4.0", + "helmet": "^7.1.0", + "jsonwebtoken": "^9.0.2", + "morgan": "^1.10.0", + "node-cron": "^3.0.3", + "openai": "^6.48.0", + "uuid": "^10.0.0", + "zod": "^3.23.0" + }, + "devDependencies": { + "@types/bcryptjs": "^2.4.6", + "@types/compression": "^1.7.5", + "@types/cors": "^2.8.17", + "@types/express": "^4.17.21", + "@types/jsonwebtoken": "^9.0.6", + "@types/morgan": "^1.9.9", + "@types/node": "^20.14.0", + "@types/node-cron": "^3.0.11", + "@types/uuid": "^10.0.0", + "prisma": "^5.18.0", + "ts-node-dev": "^2.0.0", + "tsx": "^4.23.1", + "typescript": "^5.5.0" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmmirror.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.1", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.1", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.1", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.1", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmmirror.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmmirror.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@prisma/client": { + "version": "5.22.0", + "resolved": "https://registry.npmmirror.com/@prisma/client/-/client-5.22.0.tgz", + "integrity": "sha512-M0SVXfyHnQREBKxCgyo7sffrKttwE6R8PMq330MIUF0pTwjUhLbW84pFDlf06B27XyCR++VtjugEnIHdr07SVA==", + "hasInstallScript": true, + "license": "Apache-2.0", + "engines": { + "node": ">=16.13" + }, + "peerDependencies": { + "prisma": "*" + }, + "peerDependenciesMeta": { + "prisma": { + "optional": true + } + } + }, + "node_modules/@prisma/debug": { + "version": "5.22.0", + "resolved": "https://registry.npmmirror.com/@prisma/debug/-/debug-5.22.0.tgz", + "integrity": "sha512-AUt44v3YJeggO2ZU5BkXI7M4hu9BF2zzH2iF2V5pyXT/lRTyWiElZ7It+bRH1EshoMRxHgpYg4VB6rCM+mG5jQ==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/engines": { + "version": "5.22.0", + "resolved": "https://registry.npmmirror.com/@prisma/engines/-/engines-5.22.0.tgz", + "integrity": "sha512-UNjfslWhAt06kVL3CjkuYpHAWSO6L4kDCVPegV6itt7nD1kSJavd3vhgAEhjglLJJKEdJ7oIqDJ+yHk6qO8gPA==", + "devOptional": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "5.22.0", + "@prisma/engines-version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", + "@prisma/fetch-engine": "5.22.0", + "@prisma/get-platform": "5.22.0" + } + }, + "node_modules/@prisma/engines-version": { + "version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", + "resolved": "https://registry.npmmirror.com/@prisma/engines-version/-/engines-version-5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2.tgz", + "integrity": "sha512-2PTmxFR2yHW/eB3uqWtcgRcgAbG1rwG9ZriSvQw+nnb7c4uCr3RAcGMb6/zfE88SKlC1Nj2ziUvc96Z379mHgQ==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/fetch-engine": { + "version": "5.22.0", + "resolved": "https://registry.npmmirror.com/@prisma/fetch-engine/-/fetch-engine-5.22.0.tgz", + "integrity": "sha512-bkrD/Mc2fSvkQBV5EpoFcZ87AvOgDxbG99488a5cexp5Ccny+UM6MAe/UFkUC0wLYD9+9befNOqGiIJhhq+HbA==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "5.22.0", + "@prisma/engines-version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", + "@prisma/get-platform": "5.22.0" + } + }, + "node_modules/@prisma/get-platform": { + "version": "5.22.0", + "resolved": "https://registry.npmmirror.com/@prisma/get-platform/-/get-platform-5.22.0.tgz", + "integrity": "sha512-pHhpQdr1UPFpt+zFfnPazhulaZYCUqeIcPpJViYoq9R+D/yw4fjE+CtnsnKzPYm0ddUbeXUzjGVGIRVgPDCk4Q==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "5.22.0" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.12", + "resolved": "https://registry.npmmirror.com/@tsconfig/node10/-/node10-1.0.12.tgz", + "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmmirror.com/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/bcryptjs": { + "version": "2.4.6", + "resolved": "https://registry.npmmirror.com/@types/bcryptjs/-/bcryptjs-2.4.6.tgz", + "integrity": "sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmmirror.com/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/compression": { + "version": "1.8.1", + "resolved": "https://registry.npmmirror.com/@types/compression/-/compression-1.8.1.tgz", + "integrity": "sha512-kCFuWS0ebDbmxs0AXYn6e2r2nrGAb5KwQhknjSPSPgJcGd8+HVSILlUyFhGqML2gk39HcG7D1ydW9/qpYkN00Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmmirror.com/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmmirror.com/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/express": { + "version": "4.17.25", + "resolved": "https://registry.npmmirror.com/@types/express/-/express-4.17.25.tgz", + "integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "^1" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.19.9", + "resolved": "https://registry.npmmirror.com/@types/express-serve-static-core/-/express-serve-static-core-4.19.9.tgz", + "integrity": "sha512-QP2ESEe/ImWY0HDwNAnK9PvEffUyhLTnWkk7KXzHfyeWAnlrDe1fN77bXl6ia8KT3wPlmA7t9/VPRpnf4Ex9sg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmmirror.com/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.10", + "resolved": "https://registry.npmmirror.com/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz", + "integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ms": "*", + "@types/node": "*" + } + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmmirror.com/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/morgan": { + "version": "1.9.10", + "resolved": "https://registry.npmmirror.com/@types/morgan/-/morgan-1.9.10.tgz", + "integrity": "sha512-sS4A1zheMvsADRVfT0lYbJ4S9lmsey8Zo2F7cnbYjWHP67Q0AwMYuuzLlkIM2N8gAbb9cubhIVFwcIN2XyYCkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.43", + "resolved": "https://registry.npmmirror.com/@types/node/-/node-20.19.43.tgz", + "integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/node-cron": { + "version": "3.0.11", + "resolved": "https://registry.npmmirror.com/@types/node-cron/-/node-cron-3.0.11.tgz", + "integrity": "sha512-0ikrnug3/IyneSHqCBeslAhlK2aBfYek1fGo4bP4QnZPmiqSGRK+Oy7ZMisLWkesffJvQ1cqAcBnJC+8+nxIAg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/qs": { + "version": "6.15.1", + "resolved": "https://registry.npmmirror.com/@types/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmmirror.com/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.10", + "resolved": "https://registry.npmmirror.com/@types/serve-static/-/serve-static-1.15.10.tgz", + "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "<1" + } + }, + "node_modules/@types/serve-static/node_modules/@types/send": { + "version": "0.17.6", + "resolved": "https://registry.npmmirror.com/@types/send/-/send-0.17.6.tgz", + "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/@types/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-xevGOReSYGM7g/kUBZzPqCrR/KYAo+F0yiPc85WFTJa0MSLtyFTVTU6cJu/aV4mid7IffDIWqo69THF2o4JiEQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/strip-json-comments": { + "version": "0.0.30", + "resolved": "https://registry.npmmirror.com/@types/strip-json-comments/-/strip-json-comments-0.0.30.tgz", + "integrity": "sha512-7NQmHra/JILCd1QqpSzl8+mJRc8ZHz3uDm8YV1Ks9IhK0epEiTw8aIErbvH9PI+6XbqhyIQy3462nEsn7UVzjQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmmirror.com/@types/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmmirror.com/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmmirror.com/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.17.0", + "resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.17.0.tgz", + "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.5", + "resolved": "https://registry.npmmirror.com/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmmirror.com/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmmirror.com/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.1.2" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/basic-auth/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/bcryptjs": { + "version": "2.4.3", + "resolved": "https://registry.npmmirror.com/bcryptjs/-/bcryptjs-2.4.3.tgz", + "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==", + "license": "MIT" + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/body-parser": { + "version": "1.20.6", + "resolved": "https://registry.npmmirror.com/body-parser/-/body-parser-1.20.6.tgz", + "integrity": "sha512-p5tAzS57i5MV9fZFDj9LeIiTZEufbSe2eDozP+ElheSUq1m74CRq1jI4mYNDdVs9vQztXFLuk/Gd6BWTdwRJ5g==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.15.1", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.16", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.16.tgz", + "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmmirror.com/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmmirror.com/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "license": "MIT", + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression": { + "version": "1.8.1", + "resolved": "https://registry.npmmirror.com/compression/-/compression-1.8.1.tgz", + "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "compressible": "~2.0.18", + "debug": "2.6.9", + "negotiator": "~0.6.4", + "on-headers": "~1.1.0", + "safe-buffer": "5.2.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmmirror.com/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmmirror.com/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmmirror.com/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmmirror.com/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmmirror.com/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmmirror.com/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmmirror.com/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/diff": { + "version": "4.0.4", + "resolved": "https://registry.npmmirror.com/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/dynamic-dedupe": { + "version": "0.3.0", + "resolved": "https://registry.npmmirror.com/dynamic-dedupe/-/dynamic-dedupe-0.3.0.tgz", + "integrity": "sha512-ssuANeD+z97meYOqd50e04Ze5qp4bPqo8cCkI4TRjZkzAUgIDTrXV1R8QCdINpiI+hw14+rYazvTRdQrz0/rFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmmirror.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.28.1", + "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmmirror.com/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.22.2", + "resolved": "https://registry.npmmirror.com/express/-/express-4.22.2.tgz", + "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.5", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.15.1", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "7.5.1", + "resolved": "https://registry.npmmirror.com/express-rate-limit/-/express-rate-limit-7.5.1.tgz", + "integrity": "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmmirror.com/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmmirror.com/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmmirror.com/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmmirror.com/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmmirror.com/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/helmet": { + "version": "7.2.0", + "resolved": "https://registry.npmmirror.com/helmet/-/helmet-7.2.0.tgz", + "integrity": "sha512-ZRiwvN089JfMXokizgqEPXsl2Guk094yExfoDXR0cBYWxtBbaSww/w+vT4WEJsBW2iTUi1GgZ6swmoug3Oy4Xw==", + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmmirror.com/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmmirror.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.2", + "resolved": "https://registry.npmmirror.com/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmmirror.com/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmmirror.com/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmmirror.com/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmmirror.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmmirror.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmmirror.com/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmmirror.com/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmmirror.com/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmmirror.com/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmmirror.com/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/morgan": { + "version": "1.11.0", + "resolved": "https://registry.npmmirror.com/morgan/-/morgan-1.11.0.tgz", + "integrity": "sha512-zSkVu3t18r39pw4ixfBKvfZi3y2UOqr7d4WYwcj3m8nXpEQK4rPO6GLzs/CExoRgmX3y9EjmmcXqv6jq0SK46g==", + "license": "MIT", + "dependencies": { + "basic-auth": "~2.0.1", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-finished": "~2.4.1", + "on-headers": "~1.1.0" + }, + "engines": { + "node": ">= 0.8.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmmirror.com/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-cron": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/node-cron/-/node-cron-3.0.3.tgz", + "integrity": "sha512-dOal67//nohNgYWb+nWmg5dkFdIwDm8EpeGYMekPMrngV3637lqnX0lbUcCtgibHTz6SEz7DAIjKvKDFYCnO1A==", + "license": "ISC", + "dependencies": { + "uuid": "8.3.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/node-cron/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmmirror.com/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmmirror.com/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmmirror.com/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmmirror.com/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/openai": { + "version": "6.48.0", + "resolved": "https://registry.npmmirror.com/openai/-/openai-6.48.0.tgz", + "integrity": "sha512-KhVp+FyV50QrXNextvL9hIU5l6ox5HYuKQjGVk7lIqprgJol90+dQXWONV6S1lRWsKA1bXjrow8RsUT14M1hNA==", + "license": "Apache-2.0", + "peerDependencies": { + "@aws-sdk/credential-provider-node": ">=3.972.0 <4", + "@smithy/hash-node": ">=4.3.0 <5", + "@smithy/signature-v4": ">=5.4.0 <6", + "ws": "^8.18.0", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@aws-sdk/credential-provider-node": { + "optional": true + }, + "@smithy/hash-node": { + "optional": true + }, + "@smithy/signature-v4": { + "optional": true + }, + "ws": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmmirror.com/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmmirror.com/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-to-regexp": { + "version": "0.1.13", + "resolved": "https://registry.npmmirror.com/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", + "license": "MIT" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/prisma": { + "version": "5.22.0", + "resolved": "https://registry.npmmirror.com/prisma/-/prisma-5.22.0.tgz", + "integrity": "sha512-vtpjW3XuYCSnMsNVBjLMNkTj6OZbudcPPTPYHqX0CJfpcdWciI1dM8uHETwmDxxiqEwCIE6WvXucWUetJgfu/A==", + "devOptional": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/engines": "5.22.0" + }, + "bin": { + "prisma": "build/index.js" + }, + "engines": { + "node": ">=16.13" + }, + "optionalDependencies": { + "fsevents": "2.3.3" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmmirror.com/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.15.3", + "resolved": "https://registry.npmmirror.com/qs/-/qs-6.15.3.tgz", + "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", + "license": "BSD-3-Clause", + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmmirror.com/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmmirror.com/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmmirror.com/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmmirror.com/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmmirror.com/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmmirror.com/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmmirror.com/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmmirror.com/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmmirror.com/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmmirror.com/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmmirror.com/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/ts-node-dev": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/ts-node-dev/-/ts-node-dev-2.0.0.tgz", + "integrity": "sha512-ywMrhCfH6M75yftYvrvNarLEY+SUXtUvU8/0Z6llrHQVBx12GiFk5sStF8UdfE/yfzk9IAq7O5EEbTQsxlBI8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.1", + "dynamic-dedupe": "^0.3.0", + "minimist": "^1.2.6", + "mkdirp": "^1.0.4", + "resolve": "^1.0.0", + "rimraf": "^2.6.1", + "source-map-support": "^0.5.12", + "tree-kill": "^1.2.2", + "ts-node": "^10.4.0", + "tsconfig": "^7.0.0" + }, + "bin": { + "ts-node-dev": "lib/bin.js", + "tsnd": "lib/bin.js" + }, + "engines": { + "node": ">=0.8.0" + }, + "peerDependencies": { + "node-notifier": "*", + "typescript": "*" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/tsconfig": { + "version": "7.0.0", + "resolved": "https://registry.npmmirror.com/tsconfig/-/tsconfig-7.0.0.tgz", + "integrity": "sha512-vZXmzPrL+EmC4T/4rVlT2jNVMWCi/O4DIiSj3UHg1OE5kCKbk4mfrXc6dZksLgRM/TZlKnousKH9bbTazUWRRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/strip-bom": "^3.0.0", + "@types/strip-json-comments": "0.0.30", + "strip-bom": "^3.0.0", + "strip-json-comments": "^2.0.0" + } + }, + "node_modules/tsx": { + "version": "4.23.1", + "resolved": "https://registry.npmmirror.com/tsx/-/tsx-4.23.1.tgz", + "integrity": "sha512-GQHnkIfxyx1wYCOS/wonik5MVRZU9hi1TEZmzGZSCJB1y9YgoZ8H6itNE/u4suE+yLmOzuE4E5S4TZ/ZX2wcWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.28.0" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmmirror.com/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmmirror.com/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmmirror.com/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmmirror.com/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/backend/package.json b/backend/package.json new file mode 100644 index 0000000..326511c --- /dev/null +++ b/backend/package.json @@ -0,0 +1,44 @@ +{ + "name": "hr-compliance-backend", + "private": true, + "version": "1.0.0", + "type": "module", + "scripts": { + "dev": "tsx watch src/index.ts", + "build": "tsc", + "start": "node dist/index.js", + "prisma:generate": "prisma generate", + "prisma:migrate": "prisma migrate dev", + "prisma:seed": "tsx prisma/seed.ts" + }, + "dependencies": { + "@prisma/client": "^5.18.0", + "bcryptjs": "^2.4.3", + "compression": "^1.7.4", + "cors": "^2.8.5", + "express": "^4.19.0", + "express-rate-limit": "^7.4.0", + "helmet": "^7.1.0", + "jsonwebtoken": "^9.0.2", + "morgan": "^1.10.0", + "node-cron": "^3.0.3", + "openai": "^6.48.0", + "uuid": "^10.0.0", + "zod": "^3.23.0" + }, + "devDependencies": { + "@types/bcryptjs": "^2.4.6", + "@types/compression": "^1.7.5", + "@types/cors": "^2.8.17", + "@types/express": "^4.17.21", + "@types/jsonwebtoken": "^9.0.6", + "@types/morgan": "^1.9.9", + "@types/node": "^20.14.0", + "@types/node-cron": "^3.0.11", + "@types/uuid": "^10.0.0", + "prisma": "^5.18.0", + "ts-node-dev": "^2.0.0", + "tsx": "^4.23.1", + "typescript": "^5.5.0" + } +} diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma new file mode 100644 index 0000000..4ca0949 --- /dev/null +++ b/backend/prisma/schema.prisma @@ -0,0 +1,490 @@ +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +// ========== 枚举 ========== + +enum Plan { + FREE + PRO + ENTERPRISE +} + +enum Role { + ADMIN + HR + VIEWER +} + +enum EmployeeStatus { + ACTIVE + RESIGNED +} + +enum ContractType { + FIXED + UNFIXED + UNSIGNED +} + +enum SignMethod { + PAPER + ELECTRONIC +} + +enum RiskType { + CONTRACT + SALARY + TERMINATION + MONTHLY +} + +enum RiskLevel { + HIGH + MEDIUM + LOW +} + +enum RiskStatus { + PENDING + RESOLVED + IGNORED +} + +enum TerminationReason { + NEGOTIATED + FAULT + NONFAULT + LAYOFF + EXPIRED +} + +enum RiskAssessment { + SAFE + WARNING + DANGER +} + +enum OnboardingStatus { + PENDING + APPROVED + REJECTED +} + +enum ContractConfirmStatus { + UNCONFIRMED + CONFIRMED + EXPIRED +} + +// ========== 核心表 ========== + +model Organization { + id String @id @default(cuid()) + name String + plan Plan @default(FREE) + maxEmployees Int @default(20) + city String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + users User[] + employees Employee[] + contracts LaborContract[] + overtimeRecords OvertimeRecord[] + terminations TerminationRecord[] + riskItems RiskItem[] + auditLogs AuditLog[] + payslips Payslip[] + onboardingLinks OnboardingLink[] + confirmLinks ContractConfirmLink[] + socialInsuranceConfig SocialInsuranceConfig? + notificationSetting NotificationSetting? + notificationLogs NotificationLog[] + employeeAttachments EmployeeAttachment[] + disciplinaryRecords DisciplinaryRecord[] + attendanceRecords AttendanceRecord[] + trainingRecords TrainingRecord[] + performanceRecords PerformanceRecord[] +} + +model User { + id String @id @default(cuid()) + orgId String + org Organization @relation(fields: [orgId], references: [id], onDelete: Cascade) + phone String @unique + email String? + passwordHash String + name String + role Role @default(ADMIN) + createdAt DateTime @default(now()) + lastLoginAt DateTime? +} + +// ========== 业务表 ========== + +model Employee { + id String @id @default(cuid()) + orgId String + org Organization @relation(fields: [orgId], references: [id], onDelete: Cascade) + name String + department String + hireDate DateTime + monthlySalary String // AES-256 加密存储 + status EmployeeStatus @default(ACTIVE) + gender String? + phone String? + idCardNumber String? // AES-256 加密存储 + emergencyContact String? + emergencyPhone String? + address String? + bankAccount String? // AES-256 加密存储 + bankName String? + passwordHash String? // 员工端登录密码 + isPregnant Boolean @default(false) + isInMedicalPeriod Boolean @default(false) + isWorkInjured Boolean @default(false) + createdBy String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + contracts LaborContract[] + overtimeRecords OvertimeRecord[] + terminations TerminationRecord[] + riskItems RiskItem[] + payslips Payslip[] + attachments EmployeeAttachment[] + disciplinaryRecords DisciplinaryRecord[] + attendanceRecords AttendanceRecord[] + trainingRecords TrainingRecord[] + performanceRecords PerformanceRecord[] +} + +model LaborContract { + id String @id @default(cuid()) + orgId String + org Organization @relation(fields: [orgId], references: [id], onDelete: Cascade) + employeeId String + employee Employee @relation(fields: [employeeId], references: [id], onDelete: Cascade) + signDate DateTime? + startDate DateTime + endDate DateTime? + contractType ContractType + signMethod SignMethod @default(PAPER) + contractYears Int @default(3) + probationMonths Int @default(0) + probationSalary Int @default(0) + renewalCount Int @default(0) + attachmentName String? + attachmentUrl String? + electronicContractNo String? + electronicContractUrl String? + createdBy String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + confirmLinks ContractConfirmLink[] +} + +model OvertimeRecord { + id String @id @default(cuid()) + orgId String + org Organization @relation(fields: [orgId], references: [id], onDelete: Cascade) + employeeId String + employee Employee @relation(fields: [employeeId], references: [id], onDelete: Cascade) + month String // YYYY-MM + weekdayHours Float @default(0) + weekendHours Float @default(0) + holidayHours Float @default(0) + weekdayPay Float @default(0) + weekendPay Float @default(0) + holidayPay Float @default(0) + totalPay Float @default(0) + createdAt DateTime @default(now()) + + @@unique([employeeId, month]) +} + +model TerminationRecord { + id String @id @default(cuid()) + orgId String + org Organization @relation(fields: [orgId], references: [id], onDelete: Cascade) + employeeId String + employee Employee @relation(fields: [employeeId], references: [id], onDelete: Cascade) + reason TerminationReason + terminationDate DateTime + compensation Float @default(0) + riskLevel RiskAssessment @default(SAFE) + checklist Json + remark String? + createdBy String + createdAt DateTime @default(now()) +} + +model RiskItem { + id String @id @default(cuid()) + orgId String + org Organization @relation(fields: [orgId], references: [id], onDelete: Cascade) + employeeId String? + employee Employee? @relation(fields: [employeeId], references: [id], onDelete: SetNull) + type RiskType + level RiskLevel + status RiskStatus @default(PENDING) + title String + description String + actionUrl String? + resolvedAt DateTime? + resolvedBy String? + remark String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([orgId, status]) + @@index([orgId, type]) +} + +model AuditLog { + id String @id @default(cuid()) + orgId String + org Organization @relation(fields: [orgId], references: [id], onDelete: Cascade) + userId String + action String + entity String + entityId String? + detail Json? + ip String? + createdAt DateTime @default(now()) + + @@index([orgId, createdAt]) +} + +// ========== 社保 & 通知 & 附件 ========== + +model SocialInsuranceConfig { + id String @id @default(cuid()) + orgId String @unique + org Organization @relation(fields: [orgId], references: [id], onDelete: Cascade) + city String @default("北京") + pensionOrg Float @default(16) // 养老保险 企业比例 % + pensionEmp Float @default(8) // 养老保险 个人比例 % + medicalOrg Float @default(9.8) // 医疗保险 企业比例 % + medicalEmp Float @default(2) // 医疗保险 个人比例 % + unemploymentOrg Float @default(0.5) // 失业保险 企业比例 % + unemploymentEmp Float @default(0.5) // 失业保险 个人比例 % + injuryOrg Float @default(0.2) // 工伤保险 企业比例 % + maternityOrg Float @default(0.8) // 生育保险 企业比例 % + housingOrg Float @default(12) // 公积金 企业比例 % + housingEmp Float @default(12) // 公积金 个人比例 % + baseMin Float @default(6326) // 缴费基数下限 + baseMax Float @default(33891) // 缴费基数上限 + updatedAt DateTime @updatedAt +} + +model NotificationSetting { + id String @id @default(cuid()) + orgId String @unique + org Organization @relation(fields: [orgId], references: [id], onDelete: Cascade) + contractExpiry Boolean @default(true) + expiryDays Int @default(30) + contractUnsigned Boolean @default(true) + overtimeAlert Boolean @default(true) + payslipReady Boolean @default(true) + // 月度事务提醒日(每月几号) + payrollDay Int @default(10) // 发薪日 + socialInsDay Int @default(15) // 社保缴纳日 + housingFundDay Int @default(15) // 公积金缴纳日 + taxDay Int @default(15) // 个税申报日 + wechatWebhook String? + emailNotify Boolean @default(false) + email String? + updatedAt DateTime @updatedAt +} + +model NotificationLog { + id String @id @default(cuid()) + orgId String + org Organization @relation(fields: [orgId], references: [id], onDelete: Cascade) + type String // CONTRACT_EXPIRY / CONTRACT_UNSIGNED / OVERTIME / PAYSLIP + title String + content String + channel String // WECHAT / EMAIL / IN_APP + status String @default("SENT") // SENT / FAILED + employeeId String? + createdAt DateTime @default(now()) + + @@index([orgId, createdAt]) +} + +model EmployeeAttachment { + id String @id @default(cuid()) + orgId String + org Organization @relation(fields: [orgId], references: [id], onDelete: Cascade) + employeeId String + employee Employee @relation(fields: [employeeId], references: [id], onDelete: Cascade) + fileName String + fileType String // ID_CARD / BANK_CARD / CONTRACT_SCAN / EDUCATION / OTHER + fileUrl String + fileSize Int @default(0) + uploadedBy String + createdAt DateTime @default(now()) + + @@index([orgId, employeeId]) +} + +// ========== 仲裁证据链 ========== + +model DisciplinaryRecord { + id String @id @default(cuid()) + orgId String + org Organization @relation(fields: [orgId], references: [id], onDelete: Cascade) + employeeId String + employee Employee @relation(fields: [employeeId], references: [id], onDelete: Cascade) + violationDate DateTime + violationType String // LATE/ABSENT/INSUBORDINATION/MISCONDUCT/VIOLATE_POLICY/OTHER + description String + severity String @default("WARNING") // WARNING/SERIOUS/SEVERE + action String @default("ORAL_WARNING") // ORAL_WARNING/WRITTEN_WARNING/DEDUCTION/DEMOTION/TERMINATION + actionDetail String? + employeeAck Boolean @default(false) // 员工是否签字确认 + ackDate DateTime? + ackMethod String? // SIGN/ELECTRONIC/REFUSED + witness String? // 见证人 + attachmentUrl String? + createdBy String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([orgId, employeeId]) +} + +model AttendanceRecord { + id String @id @default(cuid()) + orgId String + org Organization @relation(fields: [orgId], references: [id], onDelete: Cascade) + employeeId String + employee Employee @relation(fields: [employeeId], references: [id], onDelete: Cascade) + date DateTime + checkInTime String? // HH:mm + checkOutTime String? // HH:mm + status String @default("NORMAL") // NORMAL/LATE/EARLY_LEAVE/ABSENT/LEAVE/BUSINESS_TRIP + lateMinutes Int @default(0) + earlyMinutes Int @default(0) + workHours Float @default(0) + overtimeHours Float @default(0) + remark String? + createdBy String + createdAt DateTime @default(now()) + + @@unique([employeeId, date]) + @@index([orgId, employeeId]) +} + +model TrainingRecord { + id String @id @default(cuid()) + orgId String + org Organization @relation(fields: [orgId], references: [id], onDelete: Cascade) + employeeId String + employee Employee @relation(fields: [employeeId], references: [id], onDelete: Cascade) + trainingDate DateTime + topic String // 培训主题/制度名称 + content String? // 培训内容摘要 + trainer String? + duration Float @default(0) // 培训时长(小时) + ackStatus String @default("PENDING") // PENDING/SIGNED/REFUSED + ackDate DateTime? + attachmentUrl String? // 签收单扫描件 + remark String? + createdBy String + createdAt DateTime @default(now()) + + @@index([orgId, employeeId]) +} + +model PerformanceRecord { + id String @id @default(cuid()) + orgId String + org Organization @relation(fields: [orgId], references: [id], onDelete: Cascade) + employeeId String + employee Employee @relation(fields: [employeeId], references: [id], onDelete: Cascade) + period String // 考核周期 YYYY-MM 或 YYYY-Q1 + score Float @default(0) // 考核得分 + grade String @default("B") // A/B/C/D + result String @default("QUALIFIED") // EXCELLENT/QUALIFIED/NEED_IMPROVE/UNQUALIFIED + summary String? // 考核评语 + improvementPlan String? // 改进计划(不胜任时) + employeeAck Boolean @default(false) + ackDate DateTime? + reviewer String? + createdBy String + createdAt DateTime @default(now()) + + @@unique([employeeId, period]) + @@index([orgId, employeeId]) +} + +// ========== 员工端表 ========== + +model Payslip { + id String @id @default(cuid()) + orgId String + org Organization @relation(fields: [orgId], references: [id], onDelete: Cascade) + employeeId String + employee Employee @relation(fields: [employeeId], references: [id], onDelete: Cascade) + month String // YYYY-MM + baseSalary Float @default(0) + overtimePay Float @default(0) + weekdayOvertimePay Float @default(0) + weekendOvertimePay Float @default(0) + holidayOvertimePay Float @default(0) + allowance Float @default(0) + deduction Float @default(0) + totalPay Float @default(0) + confirmedAt DateTime? + confirmedIp String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@unique([employeeId, month]) + @@index([orgId, month]) +} + +model OnboardingLink { + id String @id @default(cuid()) + orgId String + org Organization @relation(fields: [orgId], references: [id], onDelete: Cascade) + token String @unique + employeeName String? + phone String? + status OnboardingStatus @default(PENDING) + formData Json? + expiresAt DateTime + usedAt DateTime? + createdBy String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([orgId, status]) +} + +model ContractConfirmLink { + id String @id @default(cuid()) + orgId String + org Organization @relation(fields: [orgId], references: [id], onDelete: Cascade) + contractId String + contract LaborContract @relation(fields: [contractId], references: [id], onDelete: Cascade) + token String @unique + status ContractConfirmStatus @default(UNCONFIRMED) + confirmedAt DateTime? + confirmedIp String? + expiresAt DateTime + createdBy String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([orgId, status]) +} diff --git a/backend/prisma/seed.ts b/backend/prisma/seed.ts new file mode 100644 index 0000000..1557a96 --- /dev/null +++ b/backend/prisma/seed.ts @@ -0,0 +1,77 @@ +import { PrismaClient } from '@prisma/client' +import bcrypt from 'bcryptjs' +import { randomBytes } from 'crypto' + +const prisma = new PrismaClient() + +async function main() { + // 创建测试企业 + let org = await prisma.organization.findFirst({ where: { name: '测试科技有限公司' } }) + if (!org) { + org = await prisma.organization.create({ + data: { + name: '测试科技有限公司', + plan: 'FREE', + maxEmployees: 20, + city: '上海', + }, + }) + } + + // 创建管理员用户 + const passwordHash = await bcrypt.hash('12345678', 10) + const admin = await prisma.user.upsert({ + where: { phone: '13800000001' }, + update: {}, + create: { + orgId: org.id, + phone: '13800000001', + name: '管理员', + passwordHash, + role: 'ADMIN', + }, + }) + + // 创建测试员工 + const salaryHash = randomBytes(32).toString('hex') + const employee = await prisma.employee.create({ + data: { + orgId: org.id, + name: '张三', + department: '技术部', + hireDate: new Date('2026-01-15'), + monthlySalary: 'encrypted:' + salaryHash, + phone: '13900000001', + gender: '男', + createdBy: admin.id, + }, + }) + + // 创建测试合同 + await prisma.laborContract.create({ + data: { + orgId: org.id, + employeeId: employee.id, + signDate: new Date('2026-01-20'), + startDate: new Date('2026-02-01'), + endDate: new Date('2029-01-31'), + contractType: 'FIXED', + signMethod: 'PAPER', + contractYears: 3, + probationMonths: 2, + probationSalary: 6400, + createdBy: admin.id, + }, + }) + + console.log('Seed data created:', { org: org.id, admin: admin.id, employee: employee.id }) +} + +main() + .catch((e) => { + console.error(e) + process.exit(1) + }) + .finally(async () => { + await prisma.$disconnect() + }) diff --git a/backend/src/app.ts b/backend/src/app.ts new file mode 100644 index 0000000..83bfcfd --- /dev/null +++ b/backend/src/app.ts @@ -0,0 +1,56 @@ +import express from 'express' +import cors from 'cors' +import helmet from 'helmet' +import morgan from 'morgan' +import compression from 'compression' +import { errorHandler } from './middleware/errorHandler' +import { apiLimiter } from './middleware/rateLimit' + +const app = express() + +app.use(helmet()) +app.use(compression()) +app.use( + cors({ + origin: process.env.CORS_ORIGIN || 'http://localhost:5173', + credentials: true, + }), +) +app.use(express.json()) +app.use(morgan('dev')) + +app.get('/health', (_req, res) => { + res.json({ success: true, data: { status: 'ok', timestamp: new Date().toISOString() } }) +}) + +app.use('/api/v1', apiLimiter) + +// 路由挂载 +import authRoutes from './routes/auth.routes' +import dashboardRoutes from './routes/dashboard.routes' +import employeeRoutes from './routes/employee.routes' +import terminationRoutes from './routes/termination.routes' +import aiRoutes from './routes/ai.routes' +import portalRoutes from './routes/portal.routes' +import settingsRoutes from './routes/settings.routes' +import payrollRoutes from './routes/payroll.routes' +import socialRoutes from './routes/social.routes' +import notificationRoutes from './routes/notification.routes' +import attachmentRoutes from './routes/attachment.routes' +import rosterRoutes from './routes/roster.routes' +app.use('/api/v1/auth', authRoutes) +app.use('/api/v1/dashboard', dashboardRoutes) +app.use('/api/v1/employees', employeeRoutes) +app.use('/api/v1/termination', terminationRoutes) +app.use('/api/v1/ai', aiRoutes) +app.use('/api/v1/portal', portalRoutes) +app.use('/api/v1/settings', settingsRoutes) +app.use('/api/v1/payroll', payrollRoutes) +app.use('/api/v1/social', socialRoutes) +app.use('/api/v1/notifications', notificationRoutes) +app.use('/api/v1/attachments', attachmentRoutes) +app.use('/api/v1/roster', rosterRoutes) + +app.use(errorHandler) + +export default app diff --git a/backend/src/index.ts b/backend/src/index.ts new file mode 100644 index 0000000..b97a967 --- /dev/null +++ b/backend/src/index.ts @@ -0,0 +1,7 @@ +import app from './app' + +const PORT = process.env.PORT || 3000 + +app.listen(PORT, () => { + console.log(`Server running on http://localhost:${PORT}`) +}) diff --git a/backend/src/lib/crypto.ts b/backend/src/lib/crypto.ts new file mode 100644 index 0000000..26e00c9 --- /dev/null +++ b/backend/src/lib/crypto.ts @@ -0,0 +1,22 @@ +import crypto from 'crypto' + +const ENCRYPTION_KEY = process.env.ENCRYPTION_KEY || 'default-32-byte-encryption-key!!' +const ALGORITHM = 'aes-256-cbc' +const KEY = Buffer.from(ENCRYPTION_KEY.padEnd(32, '0').slice(0, 32), 'utf8') + +export function encrypt(text: string): string { + const iv = crypto.randomBytes(16) + const cipher = crypto.createCipheriv(ALGORITHM, KEY, iv) + let encrypted = cipher.update(text, 'utf8', 'hex') + encrypted += cipher.final('hex') + return iv.toString('hex') + ':' + encrypted +} + +export function decrypt(encryptedText: string): string { + const [ivHex, encrypted] = encryptedText.split(':') + const iv = Buffer.from(ivHex, 'hex') + const decipher = crypto.createDecipheriv(ALGORITHM, KEY, iv) + let decrypted = decipher.update(encrypted, 'hex', 'utf8') + decrypted += decipher.final('utf8') + return decrypted +} diff --git a/backend/src/lib/jwt.ts b/backend/src/lib/jwt.ts new file mode 100644 index 0000000..2ca41c7 --- /dev/null +++ b/backend/src/lib/jwt.ts @@ -0,0 +1,28 @@ +import jwt from 'jsonwebtoken' + +const JWT_SECRET = process.env.JWT_SECRET || 'dev-secret' +const JWT_REFRESH_SECRET = process.env.JWT_REFRESH_SECRET || 'dev-refresh-secret' + +export function signAccessToken(payload: { id: string; orgId: string; role: string }): string { + return jwt.sign(payload, JWT_SECRET, { expiresIn: '2h' }) +} + +export function signRefreshToken(payload: { id: string; orgId: string; role: string }): string { + return jwt.sign(payload, JWT_REFRESH_SECRET, { expiresIn: '7d' }) +} + +export function verifyAccessToken(token: string): { id: string; orgId: string; role: string } | null { + try { + return jwt.verify(token, JWT_SECRET) as { id: string; orgId: string; role: string } + } catch { + return null + } +} + +export function verifyRefreshToken(token: string): { id: string; orgId: string; role: string } | null { + try { + return jwt.verify(token, JWT_REFRESH_SECRET) as { id: string; orgId: string; role: string } + } catch { + return null + } +} diff --git a/backend/src/lib/prisma.ts b/backend/src/lib/prisma.ts new file mode 100644 index 0000000..4590932 --- /dev/null +++ b/backend/src/lib/prisma.ts @@ -0,0 +1,5 @@ +import { PrismaClient } from '@prisma/client' + +const prisma = new PrismaClient() + +export default prisma diff --git a/backend/src/middleware/auditLog.ts b/backend/src/middleware/auditLog.ts new file mode 100644 index 0000000..8c003ba --- /dev/null +++ b/backend/src/middleware/auditLog.ts @@ -0,0 +1,27 @@ +import { AuthRequest } from './auth' +import prisma from '../lib/prisma' + +export async function auditLog( + req: AuthRequest, + action: string, + entity: string, + entityId?: string, + detail?: Record, +) { + if (!req.user) return + try { + await prisma.auditLog.create({ + data: { + orgId: req.user.orgId, + userId: req.user.id, + action, + entity, + entityId, + detail: detail ? JSON.parse(JSON.stringify(detail)) : undefined, + ip: req.ip, + }, + }) + } catch (err) { + console.error('Audit log error:', err) + } +} diff --git a/backend/src/middleware/auth.ts b/backend/src/middleware/auth.ts new file mode 100644 index 0000000..90cb6c1 --- /dev/null +++ b/backend/src/middleware/auth.ts @@ -0,0 +1,28 @@ +import { Request, Response, NextFunction } from 'express' +import { verifyAccessToken } from '../lib/jwt' + +export interface AuthRequest extends Request { + user?: { id: string; orgId: string; role: string } + orgId?: string +} + +export function authMiddleware(req: AuthRequest, res: Response, next: NextFunction) { + const authHeader = req.headers.authorization + if (!authHeader || !authHeader.startsWith('Bearer ')) { + return res.status(401).json({ success: false, error: { code: 'UNAUTHORIZED', message: '未提供认证令牌' } }) + } + const token = authHeader.substring(7) + const payload = verifyAccessToken(token) + if (!payload) { + return res.status(401).json({ success: false, error: { code: 'TOKEN_INVALID', message: '令牌无效或已过期' } }) + } + req.user = payload + next() +} + +export function orgFilterMiddleware(req: AuthRequest, _res: Response, next: NextFunction) { + if (req.user) { + req.orgId = req.user.orgId + } + next() +} diff --git a/backend/src/middleware/errorHandler.ts b/backend/src/middleware/errorHandler.ts new file mode 100644 index 0000000..39ff101 --- /dev/null +++ b/backend/src/middleware/errorHandler.ts @@ -0,0 +1,37 @@ +import { Request, Response, NextFunction } from 'express' +import { ZodError } from 'zod' +import { PrismaClientKnownRequestError } from '@prisma/client/runtime/library' + +export function errorHandler(err: unknown, _req: Request, res: Response, _next: NextFunction) { + if (err instanceof ZodError) { + return res.status(422).json({ + success: false, + error: { + code: 'VALIDATION_ERROR', + message: '输入校验失败', + details: err.errors.map((e) => ({ path: e.path.join('.'), message: e.message })), + }, + }) + } + + if (err instanceof PrismaClientKnownRequestError) { + if (err.code === 'P2002') { + return res.status(400).json({ + success: false, + error: { code: 'DUPLICATE', message: '数据已存在,请勿重复操作' }, + }) + } + if (err.code === 'P2025') { + return res.status(404).json({ + success: false, + error: { code: 'NOT_FOUND', message: '记录不存在' }, + }) + } + } + + console.error('Unhandled error:', err) + return res.status(500).json({ + success: false, + error: { code: 'INTERNAL_ERROR', message: '服务器内部错误' }, + }) +} diff --git a/backend/src/middleware/rateLimit.ts b/backend/src/middleware/rateLimit.ts new file mode 100644 index 0000000..3cf1422 --- /dev/null +++ b/backend/src/middleware/rateLimit.ts @@ -0,0 +1,19 @@ +import rateLimit from 'express-rate-limit' + +export const authLimiter = rateLimit({ + windowMs: 60 * 60 * 1000, + max: 5, + message: { success: false, error: { code: 'RATE_LIMIT', message: '操作过于频繁,请稍后再试' } }, +}) + +export const loginLimiter = rateLimit({ + windowMs: 60 * 1000, + max: 5, + message: { success: false, error: { code: 'RATE_LIMIT', message: '登录尝试过于频繁,请稍后再试' } }, +}) + +export const apiLimiter = rateLimit({ + windowMs: 60 * 1000, + max: 100, + message: { success: false, error: { code: 'RATE_LIMIT', message: '请求过于频繁,请稍后再试' } }, +}) diff --git a/backend/src/routes/ai.routes.ts b/backend/src/routes/ai.routes.ts new file mode 100644 index 0000000..1612845 --- /dev/null +++ b/backend/src/routes/ai.routes.ts @@ -0,0 +1,84 @@ +import { Router } from 'express' +import { authMiddleware, AuthRequest } from '../middleware/auth' +import { chat, reviewContract, matchCase, predictRisks } from '../services/ai.service' +import prisma from '../lib/prisma' + +const router = Router() + +async function buildOrgContext(orgId: string): Promise { + const [employees, risks] = await Promise.all([ + prisma.employee.findMany({ + where: { orgId, status: 'ACTIVE' }, + include: { contracts: { orderBy: { createdAt: 'desc' }, take: 1 } }, + }), + prisma.riskItem.findMany({ + where: { orgId, status: 'PENDING' }, + include: { employee: true }, + }), + ]) + + const empSummary = employees.map((e) => { + const contract = e.contracts[0] + return `- ${e.name}(${e.department}),入职${e.hireDate.toISOString().slice(0, 10)},${contract ? `合同类型:${contract.contractType}` : '未签合同'}` + }).join('\n') + + const riskSummary = risks.map((r) => `- ${r.title}(${r.level})`).join('\n') + + return `员工列表(${employees.length}人): +${empSummary} + +当前风险项(${risks.length}项): +${riskSummary}` +} + +router.post('/chat', authMiddleware, async (req: AuthRequest, res, next) => { + try { + const { messages } = req.body as { messages: { role: 'user' | 'assistant'; content: string }[] } + if (!messages || !Array.isArray(messages)) { + return res.status(400).json({ success: false, error: { code: 'BAD_REQUEST', message: '缺少 messages 参数' } }) + } + const orgContext = await buildOrgContext(req.user!.orgId) + const reply = await chat(messages, orgContext) + res.json({ success: true, data: { reply } }) + } catch (err) { + next(err) + } +}) + +router.post('/review', authMiddleware, async (req: AuthRequest, res, next) => { + try { + const { contractText } = req.body as { contractText: string } + if (!contractText) { + return res.status(400).json({ success: false, error: { code: 'BAD_REQUEST', message: '缺少合同文本' } }) + } + const result = await reviewContract(contractText) + res.json({ success: true, data: { result } }) + } catch (err) { + next(err) + } +}) + +router.post('/match-case', authMiddleware, async (req: AuthRequest, res, next) => { + try { + const { scenario } = req.body as { scenario: string } + if (!scenario) { + return res.status(400).json({ success: false, error: { code: 'BAD_REQUEST', message: '缺少争议情形描述' } }) + } + const result = await matchCase(scenario) + res.json({ success: true, data: { result } }) + } catch (err) { + next(err) + } +}) + +router.get('/predict', authMiddleware, async (req: AuthRequest, res, next) => { + try { + const orgContext = await buildOrgContext(req.user!.orgId) + const result = await predictRisks(orgContext) + res.json({ success: true, data: { result } }) + } catch (err) { + next(err) + } +}) + +export default router diff --git a/backend/src/routes/attachment.routes.ts b/backend/src/routes/attachment.routes.ts new file mode 100644 index 0000000..138ff77 --- /dev/null +++ b/backend/src/routes/attachment.routes.ts @@ -0,0 +1,63 @@ +import { Router, Response, NextFunction } from 'express' +import prisma from '../lib/prisma' +import { authMiddleware, AuthRequest } from '../middleware/auth' +import { z } from 'zod' + +const router = Router() +router.use(authMiddleware) + +// 获取员工附件列表 +router.get('/:employeeId', async (req: AuthRequest, res: Response, next: NextFunction) => { + try { + const attachments = await prisma.employeeAttachment.findMany({ + where: { orgId: req.user!.orgId, employeeId: req.params.employeeId }, + orderBy: { createdAt: 'desc' }, + }) + res.json({ success: true, data: attachments }) + } catch (err) { + next(err) + } +}) + +// 添加附件记录(文件URL由前端上传后传入) +const attachmentSchema = z.object({ + employeeId: z.string().min(1), + fileName: z.string().min(1), + fileType: z.enum(['ID_CARD', 'BANK_CARD', 'CONTRACT_SCAN', 'EDUCATION', 'OTHER']), + fileUrl: z.string().min(1), + fileSize: z.number().int().default(0), +}) + +router.post('/', async (req: AuthRequest, res: Response, next: NextFunction) => { + try { + const data = attachmentSchema.parse(req.body) + const attachment = await prisma.employeeAttachment.create({ + data: { + orgId: req.user!.orgId, + ...data, + uploadedBy: req.user!.id, + }, + }) + res.json({ success: true, data: attachment }) + } catch (err) { + next(err) + } +}) + +// 删除附件 +router.delete('/:id', async (req: AuthRequest, res: Response, next: NextFunction) => { + try { + const attachment = await prisma.employeeAttachment.findFirst({ + where: { id: req.params.id, orgId: req.user!.orgId }, + }) + if (!attachment) { + return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '附件不存在' } }) + } + await prisma.employeeAttachment.delete({ where: { id: attachment.id } }) + res.json({ success: true }) + } catch (err) { + next(err) + } +}) + +export default router diff --git a/backend/src/routes/auth.routes.ts b/backend/src/routes/auth.routes.ts new file mode 100644 index 0000000..97e3100 --- /dev/null +++ b/backend/src/routes/auth.routes.ts @@ -0,0 +1,48 @@ +import { Router } from 'express' +import { registerSchema, loginSchema, refreshSchema, resetPasswordSchema } from '../schemas/auth.schema' +import { register, login, refresh, resetPassword } from '../services/auth.service' +import { authLimiter, loginLimiter } from '../middleware/rateLimit' + +const router = Router() + +router.post('/register', authLimiter, async (req, res, next) => { + try { + const data = registerSchema.parse(req.body) + const result = await register(data.orgName, data.phone, data.password) + res.json({ success: true, data: result }) + } catch (err) { + next(err) + } +}) + +router.post('/login', loginLimiter, async (req, res, next) => { + try { + const data = loginSchema.parse(req.body) + const result = await login(data.phone, data.password) + res.json({ success: true, data: result }) + } catch (err) { + next(err) + } +}) + +router.post('/refresh', async (req, res, next) => { + try { + const data = refreshSchema.parse(req.body) + const result = await refresh(data.refreshToken) + res.json({ success: true, data: result }) + } catch (err) { + next(err) + } +}) + +router.post('/reset-password', authLimiter, async (req, res, next) => { + try { + const data = resetPasswordSchema.parse(req.body) + const result = await resetPassword(data.phone, data.newPassword) + res.json({ success: true, data: result }) + } catch (err) { + next(err) + } +}) + +export default router diff --git a/backend/src/routes/dashboard.routes.ts b/backend/src/routes/dashboard.routes.ts new file mode 100644 index 0000000..059b5d7 --- /dev/null +++ b/backend/src/routes/dashboard.routes.ts @@ -0,0 +1,49 @@ +import { Router, Response, NextFunction } from 'express' +import prisma from '../lib/prisma' +import { authMiddleware, AuthRequest } from '../middleware/auth' +import { getDashboardData } from '../services/risk.service' + +const router = Router() + +router.get('/', authMiddleware, async (req: AuthRequest, res, next) => { + try { + const data = await getDashboardData(req.user!.orgId) + res.json({ success: true, data }) + } catch (err) { + next(err) + } +}) + +// 标记待办为已完成 +router.patch('/todos/:id/resolve', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => { + try { + const item = await prisma.riskItem.updateMany({ + where: { id: req.params.id, orgId: req.user!.orgId, status: 'PENDING' }, + data: { status: 'RESOLVED', resolvedAt: new Date(), resolvedBy: req.user!.id }, + }) + if (item.count === 0) { + return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '待办不存在或已处理' } }) + } + res.json({ success: true }) + } catch (err) { + next(err) + } +}) + +// 忽略待办 +router.patch('/todos/:id/ignore', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => { + try { + const item = await prisma.riskItem.updateMany({ + where: { id: req.params.id, orgId: req.user!.orgId, status: 'PENDING' }, + data: { status: 'IGNORED', resolvedAt: new Date(), resolvedBy: req.user!.id }, + }) + if (item.count === 0) { + return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '待办不存在或已处理' } }) + } + res.json({ success: true }) + } catch (err) { + next(err) + } +}) + +export default router diff --git a/backend/src/routes/employee.routes.ts b/backend/src/routes/employee.routes.ts new file mode 100644 index 0000000..ea7c17f --- /dev/null +++ b/backend/src/routes/employee.routes.ts @@ -0,0 +1,99 @@ +import { Router } from 'express' +import { authMiddleware, AuthRequest } from '../middleware/auth' +import { auditLog } from '../middleware/auditLog' +import { + createEmployeeSchema, + updateEmployeeSchema, + batchRenewSchema, + addContractSchema, +} from '../schemas/contract.schema' +import { + getEmployees, + getEmployeeDetail, + createEmployee, + updateEmployee, + deleteEmployee, + batchRenew, + addContract, +} from '../services/contract.service' + +const router = Router() + +router.get('/', authMiddleware, async (req: AuthRequest, res, next) => { + try { + const result = await getEmployees(req.user!.orgId, { + page: parseInt(req.query.page as string) || 1, + pageSize: parseInt(req.query.pageSize as string) || 20, + search: req.query.search as string, + department: req.query.department as string, + }) + res.json({ success: true, data: result }) + } catch (err) { + next(err) + } +}) + +router.get('/:id', authMiddleware, async (req: AuthRequest, res, next) => { + try { + const employee = await getEmployeeDetail(req.user!.orgId, req.params.id) + res.json({ success: true, data: employee }) + } catch (err) { + next(err) + } +}) + +router.post('/', authMiddleware, async (req: AuthRequest, res, next) => { + try { + const data = createEmployeeSchema.parse(req.body) + const result = await createEmployee(req.user!.orgId, req.user!.id, data) + await auditLog(req, 'CREATE', 'EMPLOYEE', result.id, { name: data.name }) + res.json({ success: true, data: result }) + } catch (err) { + next(err) + } +}) + +router.put('/:id', authMiddleware, async (req: AuthRequest, res, next) => { + try { + const data = updateEmployeeSchema.parse(req.body) + const result = await updateEmployee(req.user!.orgId, req.params.id, data) + await auditLog(req, 'UPDATE', 'EMPLOYEE', req.params.id, data) + res.json({ success: true, data: result }) + } catch (err) { + next(err) + } +}) + +router.delete('/:id', authMiddleware, async (req: AuthRequest, res, next) => { + try { + const result = await deleteEmployee(req.user!.orgId, req.params.id) + await auditLog(req, 'DELETE', 'EMPLOYEE', req.params.id) + res.json({ success: true, data: result }) + } catch (err) { + next(err) + } +}) + +router.post('/contracts/batch-renew', authMiddleware, async (req: AuthRequest, res, next) => { + try { + const data = batchRenewSchema.parse(req.body) + const result = await batchRenew(req.user!.orgId, req.user!.id, data.contractIds, data.years) + await auditLog(req, 'BATCH_RENEW', 'CONTRACT', undefined, { count: data.contractIds.length }) + res.json({ success: true, data: result }) + } catch (err) { + next(err) + } +}) + +router.post('/contracts', authMiddleware, async (req: AuthRequest, res, next) => { + try { + const data = addContractSchema.parse(req.body) + const result = await addContract(req.user!.orgId, req.user!.id, data) + await auditLog(req, 'ADD_CONTRACT', 'CONTRACT', result.id, { employeeId: data.employeeId }) + res.json({ success: true, data: result }) + } catch (err) { + next(err) + } +}) + +export default router diff --git a/backend/src/routes/notification.routes.ts b/backend/src/routes/notification.routes.ts new file mode 100644 index 0000000..925dd62 --- /dev/null +++ b/backend/src/routes/notification.routes.ts @@ -0,0 +1,134 @@ +import { Router, Response, NextFunction } from 'express' +import prisma from '../lib/prisma' +import { authMiddleware, AuthRequest } from '../middleware/auth' +import { z } from 'zod' + +const router = Router() +router.use(authMiddleware) + +// 获取通知设置 +router.get('/settings', async (req: AuthRequest, res: Response, next: NextFunction) => { + try { + let setting = await prisma.notificationSetting.findUnique({ + where: { orgId: req.user!.orgId }, + }) + if (!setting) { + setting = await prisma.notificationSetting.create({ + data: { orgId: req.user!.orgId }, + }) + } + res.json({ success: true, data: setting }) + } catch (err) { + next(err) + } +}) + +// 更新通知设置 +const settingSchema = z.object({ + contractExpiry: z.boolean().optional(), + expiryDays: z.number().int().min(1).max(365).optional(), + contractUnsigned: z.boolean().optional(), + overtimeAlert: z.boolean().optional(), + payslipReady: z.boolean().optional(), + payrollDay: z.number().int().min(1).max(28).optional(), + socialInsDay: z.number().int().min(1).max(28).optional(), + housingFundDay: z.number().int().min(1).max(28).optional(), + taxDay: z.number().int().min(1).max(28).optional(), + wechatWebhook: z.string().url().nullable().optional(), + emailNotify: z.boolean().optional(), + email: z.string().email().nullable().optional(), +}) + +router.put('/settings', async (req: AuthRequest, res: Response, next: NextFunction) => { + try { + const data = settingSchema.parse(req.body) + const setting = await prisma.notificationSetting.upsert({ + where: { orgId: req.user!.orgId }, + update: data, + create: { orgId: req.user!.orgId, ...data }, + }) + res.json({ success: true, data: setting }) + } catch (err) { + next(err) + } +}) + +// 获取通知列表 +router.get('/logs', async (req: AuthRequest, res: Response, next: NextFunction) => { + try { + const page = parseInt(req.query.page as string) || 1 + const pageSize = parseInt(req.query.pageSize as string) || 20 + const [logs, total] = await Promise.all([ + prisma.notificationLog.findMany({ + where: { orgId: req.user!.orgId }, + orderBy: { createdAt: 'desc' }, + skip: (page - 1) * pageSize, + take: pageSize, + }), + prisma.notificationLog.count({ where: { orgId: req.user!.orgId } }), + ]) + res.json({ success: true, data: { items: logs, total, page, pageSize, totalPages: Math.ceil(total / pageSize) } }) + } catch (err) { + next(err) + } +}) + +// 手动触发合同到期检查 +router.post('/check-contracts', async (req: AuthRequest, res: Response, next: NextFunction) => { + try { + const setting = await prisma.notificationSetting.findUnique({ + where: { orgId: req.user!.orgId }, + }) + const expiryDays = setting?.expiryDays || 30 + const now = new Date() + const threshold = new Date(now.getTime() + expiryDays * 24 * 60 * 60 * 1000) + + const contracts = await prisma.laborContract.findMany({ + where: { + orgId: req.user!.orgId, + endDate: { lte: threshold, gte: now }, + }, + include: { employee: { select: { id: true, name: true, department: true } } }, + }) + + const logs: any[] = [] + for (const contract of contracts) { + const daysLeft = Math.ceil((contract.endDate!.getTime() - now.getTime()) / (1000 * 60 * 60 * 24)) + const title = `${contract.employee.name}的合同将在${daysLeft}天后到期` + const content = `员工 ${contract.employee.name}(${contract.employee.department})的合同将于 ${contract.endDate!.toISOString().slice(0, 10)} 到期,请及时处理续签或终止事宜。` + + const log = await prisma.notificationLog.create({ + data: { + orgId: req.user!.orgId, + type: 'CONTRACT_EXPIRY', + title, + content, + channel: 'IN_APP', + employeeId: contract.employeeId, + }, + }) + logs.push(log) + + if (setting?.wechatWebhook) { + try { + await fetch(setting.wechatWebhook, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + msgtype: 'text', + text: { content: `【合同到期提醒】${title}\n${content}` }, + }), + }) + } catch (e) { + // webhook 发送失败不阻断流程 + } + } + } + + res.json({ success: true, data: { checked: contracts.length, notified: logs.length } }) + } catch (err) { + next(err) + } +}) + +export default router diff --git a/backend/src/routes/payroll.routes.ts b/backend/src/routes/payroll.routes.ts new file mode 100644 index 0000000..5ef461f --- /dev/null +++ b/backend/src/routes/payroll.routes.ts @@ -0,0 +1,338 @@ +import { Router, Response, NextFunction } from 'express' +import prisma from '../lib/prisma' +import { decrypt } from '../lib/crypto' +import { authMiddleware, AuthRequest } from '../middleware/auth' +import { z } from 'zod' + +const router = Router() +router.use(authMiddleware) + +// ========== 加班费记录 ========== + +const overtimeSchema = z.object({ + employeeId: z.string().min(1), + month: z.string().regex(/^\d{4}-\d{2}$/), + monthlyWage: z.number().positive(), + weekdayHours: z.number().min(0).default(0), + weekendHours: z.number().min(0).default(0), + holidayHours: z.number().min(0).default(0), +}) + +// 获取加班费记录列表 +router.get('/overtime', async (req: AuthRequest, res: Response, next: NextFunction) => { + try { + const { employeeId, month } = req.query + const records = await prisma.overtimeRecord.findMany({ + where: { + orgId: req.user!.orgId, + ...(employeeId ? { employeeId: String(employeeId) } : {}), + ...(month ? { month: String(month) } : {}), + }, + include: { employee: { select: { id: true, name: true, department: true } } }, + orderBy: { createdAt: 'desc' }, + }) + res.json({ success: true, data: records }) + } catch (err) { + next(err) + } +}) + +// 保存加班费记录 +router.post('/overtime', async (req: AuthRequest, res: Response, next: NextFunction) => { + try { + const data = overtimeSchema.parse(req.body) + const hourlyWage = data.monthlyWage / 21.75 / 8 + const weekdayPay = hourlyWage * 1.5 * data.weekdayHours + const weekendPay = hourlyWage * 2.0 * data.weekendHours + const holidayPay = hourlyWage * 3.0 * data.holidayHours + const totalPay = weekdayPay + weekendPay + holidayPay + + const record = await prisma.overtimeRecord.upsert({ + where: { + employeeId_month: { employeeId: data.employeeId, month: data.month }, + }, + update: { + weekdayHours: data.weekdayHours, + weekendHours: data.weekendHours, + holidayHours: data.holidayHours, + weekdayPay, + weekendPay, + holidayPay, + totalPay, + }, + create: { + orgId: req.user!.orgId, + employeeId: data.employeeId, + month: data.month, + weekdayHours: data.weekdayHours, + weekendHours: data.weekendHours, + holidayHours: data.holidayHours, + weekdayPay, + weekendPay, + holidayPay, + totalPay, + }, + }) + res.json({ success: true, data: record }) + } catch (err) { + next(err) + } +}) + +// ========== 工资条管理 ========== + +const payslipSchema = z.object({ + employeeId: z.string().min(1), + month: z.string().regex(/^\d{4}-\d{2}$/), + baseSalary: z.number().min(0).default(0), + overtimePay: z.number().min(0).default(0), + weekdayOvertimePay: z.number().min(0).default(0), + weekendOvertimePay: z.number().min(0).default(0), + holidayOvertimePay: z.number().min(0).default(0), + allowance: z.number().min(0).default(0), + deduction: z.number().min(0).default(0), +}) + +// 获取工资条列表 +router.get('/payslip', async (req: AuthRequest, res: Response, next: NextFunction) => { + try { + const { month, employeeId } = req.query + const payslips = await prisma.payslip.findMany({ + where: { + orgId: req.user!.orgId, + ...(month ? { month: String(month) } : {}), + ...(employeeId ? { employeeId: String(employeeId) } : {}), + }, + include: { employee: { select: { id: true, name: true, department: true } } }, + orderBy: [{ month: 'desc' }, { employee: { name: 'asc' } }], + }) + res.json({ success: true, data: payslips }) + } catch (err) { + next(err) + } +}) + +// 创建/更新工资条 +router.post('/payslip', async (req: AuthRequest, res: Response, next: NextFunction) => { + try { + const data = payslipSchema.parse(req.body) + const totalPay = data.baseSalary + data.overtimePay + data.allowance - data.deduction + + const payslip = await prisma.payslip.upsert({ + where: { + employeeId_month: { employeeId: data.employeeId, month: data.month }, + }, + update: { + baseSalary: data.baseSalary, + overtimePay: data.overtimePay, + weekdayOvertimePay: data.weekdayOvertimePay, + weekendOvertimePay: data.weekendOvertimePay, + holidayOvertimePay: data.holidayOvertimePay, + allowance: data.allowance, + deduction: data.deduction, + totalPay, + }, + create: { + orgId: req.user!.orgId, + employeeId: data.employeeId, + month: data.month, + baseSalary: data.baseSalary, + overtimePay: data.overtimePay, + weekdayOvertimePay: data.weekdayOvertimePay, + weekendOvertimePay: data.weekendOvertimePay, + holidayOvertimePay: data.holidayOvertimePay, + allowance: data.allowance, + deduction: data.deduction, + totalPay, + }, + }) + res.json({ success: true, data: payslip }) + } catch (err) { + next(err) + } +}) + +// 从加班费记录自动生成工资条 +router.post('/payslip/generate', async (req: AuthRequest, res: Response, next: NextFunction) => { + try { + const { month, employeeId, baseSalary, allowance, deduction } = req.body as { + month: string + employeeId: string + baseSalary: number + allowance?: number + deduction?: number + } + + const overtime = await prisma.overtimeRecord.findUnique({ + where: { employeeId_month: { employeeId, month } }, + }) + + const overtimePay = overtime?.totalPay || 0 + const totalPay = baseSalary + overtimePay + (allowance || 0) - (deduction || 0) + + const payslip = await prisma.payslip.upsert({ + where: { employeeId_month: { employeeId, month } }, + update: { + baseSalary, + overtimePay, + weekdayOvertimePay: overtime?.weekdayPay || 0, + weekendOvertimePay: overtime?.weekendPay || 0, + holidayOvertimePay: overtime?.holidayPay || 0, + allowance: allowance || 0, + deduction: deduction || 0, + totalPay, + }, + create: { + orgId: req.user!.orgId, + employeeId, + month, + baseSalary, + overtimePay, + weekdayOvertimePay: overtime?.weekdayPay || 0, + weekendOvertimePay: overtime?.weekendPay || 0, + holidayOvertimePay: overtime?.holidayPay || 0, + allowance: allowance || 0, + deduction: deduction || 0, + totalPay, + }, + }) + res.json({ success: true, data: payslip }) + } catch (err) { + next(err) + } +}) + +// 删除工资条 +router.delete('/payslip/:id', async (req: AuthRequest, res: Response, next: NextFunction) => { + try { + await prisma.payslip.delete({ + where: { id: req.params.id, orgId: req.user!.orgId }, + }) + res.json({ success: true }) + } catch (err) { + next(err) + } +}) + +// ========== 批量生成工资条 ========== + +const batchGenerateSchema = z.object({ + month: z.string().regex(/^\d{4}-\d{2}$/), + allowances: z.record(z.string(), z.number().default(0)).optional(), + deductions: z.record(z.string(), z.number().default(0)).optional(), +}) + +// 批量生成全员工资条 +router.post('/payslip/batch-generate', async (req: AuthRequest, res: Response, next: NextFunction) => { + try { + const { month, allowances = {}, deductions = {} } = batchGenerateSchema.parse(req.body) + + const employees = await prisma.employee.findMany({ + where: { orgId: req.user!.orgId, status: 'ACTIVE' }, + include: { + contracts: { orderBy: { createdAt: 'desc' }, take: 1 }, + }, + }) + + const results: any[] = [] + for (const emp of employees) { + const overtime = await prisma.overtimeRecord.findUnique({ + where: { employeeId_month: { employeeId: emp.id, month } }, + }) + + const overtimePay = overtime?.totalPay || 0 + const allowance = allowances[emp.id] || 0 + const deduction = deductions[emp.id] || 0 + + let baseSalary = 0 + if (emp.contracts[0]?.probationSalary && new Date(emp.contracts[0].startDate) > new Date(Date.now() - 365 * 24 * 60 * 60 * 1000)) { + baseSalary = emp.contracts[0].probationSalary + } else if (emp.monthlySalary) { + try { + baseSalary = Number(decrypt(emp.monthlySalary)) || 0 + } catch { + baseSalary = Number(emp.monthlySalary) || 0 + } + } + + const totalPay = baseSalary + overtimePay + allowance - deduction + + const payslip = await prisma.payslip.upsert({ + where: { employeeId_month: { employeeId: emp.id, month } }, + update: { baseSalary, overtimePay, allowance, deduction, totalPay }, + create: { + orgId: req.user!.orgId, + employeeId: emp.id, + month, + baseSalary, + overtimePay, + weekdayOvertimePay: overtime?.weekdayPay || 0, + weekendOvertimePay: overtime?.weekendPay || 0, + holidayOvertimePay: overtime?.holidayPay || 0, + allowance, + deduction, + totalPay, + }, + }) + results.push(payslip) + } + + res.json({ success: true, data: { generated: results.length, payslips: results } }) + } catch (err) { + next(err) + } +}) + +// ========== 批量导入加班数据 ========== + +const batchOvertimeSchema = z.array( + z.object({ + employeeId: z.string().min(1), + month: z.string().regex(/^\d{4}-\d{2}$/), + monthlyWage: z.number().positive(), + weekdayHours: z.number().min(0).default(0), + weekendHours: z.number().min(0).default(0), + holidayHours: z.number().min(0).default(0), + }), +) + +router.post('/overtime/batch', async (req: AuthRequest, res: Response, next: NextFunction) => { + try { + const items = batchOvertimeSchema.parse(req.body) + const results: any[] = [] + + for (const data of items) { + const hourlyWage = data.monthlyWage / 21.75 / 8 + const weekdayPay = hourlyWage * 1.5 * data.weekdayHours + const weekendPay = hourlyWage * 2.0 * data.weekendHours + const holidayPay = hourlyWage * 3.0 * data.holidayHours + const totalPay = weekdayPay + weekendPay + holidayPay + + const record = await prisma.overtimeRecord.upsert({ + where: { employeeId_month: { employeeId: data.employeeId, month: data.month } }, + update: { + weekdayHours: data.weekdayHours, + weekendHours: data.weekendHours, + holidayHours: data.holidayHours, + weekdayPay, weekendPay, holidayPay, totalPay, + }, + create: { + orgId: req.user!.orgId, + employeeId: data.employeeId, + month: data.month, + weekdayHours: data.weekdayHours, + weekendHours: data.weekendHours, + holidayHours: data.holidayHours, + weekdayPay, weekendPay, holidayPay, totalPay, + }, + }) + results.push(record) + } + + res.json({ success: true, data: { imported: results.length } }) + } catch (err) { + next(err) + } +}) + +export default router diff --git a/backend/src/routes/portal.routes.ts b/backend/src/routes/portal.routes.ts new file mode 100644 index 0000000..fd10e11 --- /dev/null +++ b/backend/src/routes/portal.routes.ts @@ -0,0 +1,248 @@ +import { Router, Request, Response, NextFunction } from 'express' +import bcrypt from 'bcryptjs' +import prisma from '../lib/prisma' +import { signAccessToken, verifyAccessToken } from '../lib/jwt' +import { portalLoginSchema, portalSendCodeSchema, portalVerifyCodeSchema, onboardingSchema, contractConfirmSchema } from '../schemas/portal.schema' + +const router = Router() + +// 验证码临时存储(生产环境应使用 Redis) +const codeStore = new Map() + +// 员工端认证中间件 +function portalAuth(req: Request, res: Response, next: NextFunction) { + const authHeader = req.headers.authorization + if (!authHeader?.startsWith('Bearer ')) { + return res.status(401).json({ success: false, error: { code: 'UNAUTHORIZED', message: '未登录' } }) + } + const token = authHeader.substring(7) + try { + const payload = verifyAccessToken(token) + if (!payload || payload.role !== 'EMPLOYEE') { + return res.status(401).json({ success: false, error: { code: 'UNAUTHORIZED', message: '无效的员工端 Token' } }) + } + ;(req as any).employee = { id: payload.id, orgId: payload.orgId } + next() + } catch { + return res.status(401).json({ success: false, error: { code: 'UNAUTHORIZED', message: 'Token 无效或已过期' } }) + } +} + +// 密码登录 +router.post('/login', async (req, res, next) => { + try { + const data = portalLoginSchema.parse(req.body) + const employee = await prisma.employee.findFirst({ + where: { phone: data.phone, status: 'ACTIVE' }, + }) + if (!employee || !employee.passwordHash) { + return res.status(400).json({ success: false, error: { code: 'AUTH_FAILED', message: '手机号或密码错误' } }) + } + const valid = await bcrypt.compare(data.password, employee.passwordHash) + if (!valid) { + return res.status(400).json({ success: false, error: { code: 'AUTH_FAILED', message: '手机号或密码错误' } }) + } + const token = signAccessToken({ id: employee.id, orgId: employee.orgId, role: 'EMPLOYEE' }) + res.json({ success: true, data: { token, employee: { id: employee.id, name: employee.name, department: employee.department } } }) + } catch (err) { + next(err) + } +}) + +// 发送验证码(页面内显示) +router.post('/send-code', async (req, res, next) => { + try { + const data = portalSendCodeSchema.parse(req.body) + const employee = await prisma.employee.findFirst({ + where: { phone: data.phone, status: 'ACTIVE' }, + }) + if (!employee) { + return res.status(400).json({ success: false, error: { code: 'NOT_FOUND', message: '该手机号未在系统中登记' } }) + } + const code = Math.random().toString().slice(2, 8) + codeStore.set(data.phone, { code, expiresAt: Date.now() + 5 * 60 * 1000 }) + res.json({ success: true, data: { code, message: '验证码已生成(开发阶段直接返回,生产环境将发送短信)' } }) + } catch (err) { + next(err) + } +}) + +// 验证码登录 +router.post('/verify-code', async (req, res, next) => { + try { + const data = portalVerifyCodeSchema.parse(req.body) + const stored = codeStore.get(data.phone) + if (!stored || stored.expiresAt < Date.now()) { + return res.status(400).json({ success: false, error: { code: 'CODE_EXPIRED', message: '验证码已过期,请重新获取' } }) + } + if (stored.code !== data.code) { + return res.status(400).json({ success: false, error: { code: 'CODE_WRONG', message: '验证码错误' } }) + } + codeStore.delete(data.phone) + const employee = await prisma.employee.findFirst({ where: { phone: data.phone, status: 'ACTIVE' } }) + if (!employee) { + return res.status(400).json({ success: false, error: { code: 'NOT_FOUND', message: '员工不存在' } }) + } + const token = signAccessToken({ id: employee.id, orgId: employee.orgId, role: 'EMPLOYEE' }) + res.json({ success: true, data: { token, employee: { id: employee.id, name: employee.name, department: employee.department } } }) + } catch (err) { + next(err) + } +}) + +// 工资条 +router.get('/payslip', portalAuth, async (req: any, res, next) => { + try { + const month = req.query.month as string || new Date().toISOString().slice(0, 7) + const payslip = await prisma.payslip.findFirst({ + where: { employeeId: req.employee.id, orgId: req.employee.orgId, month }, + }) + if (!payslip) { + return res.json({ success: true, data: null }) + } + res.json({ success: true, data: payslip }) + } catch (err) { + next(err) + } +}) + +// 工资条确认已阅 +router.post('/payslip/:id/confirm', portalAuth, async (req: any, res, next) => { + try { + const payslip = await prisma.payslip.findFirst({ + where: { id: req.params.id, orgId: req.employee.orgId, employeeId: req.employee.id }, + }) + if (!payslip) { + return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '工资条不存在' } }) + } + await prisma.payslip.update({ + where: { id: req.params.id }, + data: { confirmedAt: new Date(), confirmedIp: req.ip }, + }) + res.json({ success: true }) + } catch (err) { + next(err) + } +}) + +// 我的合同 +router.get('/contract', portalAuth, async (req: any, res, next) => { + try { + const contract = await prisma.laborContract.findFirst({ + where: { employeeId: req.employee.id, orgId: req.employee.orgId }, + orderBy: { createdAt: 'desc' }, + }) + if (!contract) { + return res.json({ success: true, data: null }) + } + res.json({ success: true, data: contract }) + } catch (err) { + next(err) + } +}) + +// 入职填报提交 +router.post('/onboarding', async (req, res, next) => { + try { + const data = onboardingSchema.parse(req.body) + const link = await prisma.onboardingLink.findFirst({ + where: { token: data.token, status: 'PENDING', expiresAt: { gt: new Date() } }, + }) + if (!link) { + return res.status(400).json({ success: false, error: { code: 'LINK_INVALID', message: '链接无效或已过期' } }) + } + await prisma.onboardingLink.update({ + where: { id: link.id }, + data: { + employeeName: data.name, + phone: data.phone, + formData: { + name: data.name, + phone: data.phone, + idCard: data.idCard, + emergencyContact: data.emergencyContact, + emergencyPhone: data.emergencyPhone, + address: data.address, + bankCard: data.bankCard, + bankName: data.bankName, + }, + status: 'APPROVED', + usedAt: new Date(), + }, + }) + res.json({ success: true, data: { message: '信息提交成功,HR 将审核您的信息' } }) + } catch (err) { + next(err) + } +}) + +// 合同签署确认 +router.post('/contract-confirm', async (req, res, next) => { + try { + const data = contractConfirmSchema.parse(req.body) + const link = await prisma.contractConfirmLink.findFirst({ + where: { token: data.token, status: 'UNCONFIRMED', expiresAt: { gt: new Date() } }, + }) + if (!link) { + return res.status(400).json({ success: false, error: { code: 'LINK_INVALID', message: '链接无效或已过期' } }) + } + await prisma.contractConfirmLink.update({ + where: { id: link.id }, + data: { status: 'CONFIRMED', confirmedAt: new Date(), confirmedIp: req.ip }, + }) + await prisma.laborContract.update({ + where: { id: link.contractId }, + data: { attachmentName: `confirmed:${new Date().toISOString()}` }, + }) + res.json({ success: true, data: { message: '合同签署确认成功' } }) + } catch (err) { + next(err) + } +}) + +// 获取入职填报信息(通过 token) +router.get('/onboarding/:token', async (req, res, next) => { + try { + const link = await prisma.onboardingLink.findFirst({ + where: { token: req.params.token, status: 'PENDING', expiresAt: { gt: new Date() } }, + include: { org: { select: { name: true } } }, + }) + if (!link) { + return res.status(400).json({ success: false, error: { code: 'LINK_INVALID', message: '链接无效或已过期' } }) + } + res.json({ success: true, data: { orgName: link.org.name } }) + } catch (err) { + next(err) + } +}) + +// 获取合同确认信息(通过 token) +router.get('/contract-confirm/:token', async (req, res, next) => { + try { + const link = await prisma.contractConfirmLink.findFirst({ + where: { token: req.params.token, status: 'UNCONFIRMED', expiresAt: { gt: new Date() } }, + include: { + contract: { + include: { + employee: { select: { name: true, org: { select: { name: true } } } }, + }, + }, + }, + }) + if (!link) { + return res.status(400).json({ success: false, error: { code: 'LINK_INVALID', message: '链接无效或已过期' } }) + } + res.json({ + success: true, + data: { + orgName: link.contract.employee.org.name, + employeeName: link.contract.employee.name, + contract: link.contract, + }, + }) + } catch (err) { + next(err) + } +}) + +export default router diff --git a/backend/src/routes/roster.routes.ts b/backend/src/routes/roster.routes.ts new file mode 100644 index 0000000..e8ad79e --- /dev/null +++ b/backend/src/routes/roster.routes.ts @@ -0,0 +1,533 @@ +import { Router, Request, Response, NextFunction } from 'express' +import { authMiddleware, AuthRequest } from '../middleware/auth' +import { auditLog } from '../middleware/auditLog' +import prisma from '../lib/prisma' +import { decrypt } from '../lib/crypto' + +const router = Router() + +function safeDecrypt(encrypted: string): number { + try { + if (!encrypted || !encrypted.includes(':')) return Number(encrypted) || 0 + return Number(decrypt(encrypted)) + } catch { + return Number(encrypted) || 0 + } +} + +// ========== 花名册聚合 API ========== + +// 花名册列表(含汇总信息) +router.get('/', authMiddleware, async (req: AuthRequest, res, next) => { + try { + const employees = await prisma.employee.findMany({ + where: { orgId: req.user!.orgId }, + orderBy: { createdAt: 'desc' }, + include: { + contracts: { orderBy: { createdAt: 'desc' }, take: 1 }, + _count: { + select: { + disciplinaryRecords: true, + attendanceRecords: true, + trainingRecords: true, + performanceRecords: true, + payslips: true, + overtimeRecords: true, + }, + }, + }, + }) + const result = employees.map((e) => ({ + id: e.id, + name: e.name, + department: e.department, + status: e.status, + hireDate: e.hireDate, + gender: e.gender, + phone: e.phone, + monthlySalary: safeDecrypt(e.monthlySalary), + latestContract: e.contracts[0] || null, + counts: e._count, + })) + res.json({ success: true, data: result }) + } catch (err) { + next(err) + } +}) + +// 员工完整档案(花名册详情) +router.get('/:id/profile', authMiddleware, async (req: AuthRequest, res, next) => { + try { + const employee = await prisma.employee.findFirst({ + where: { id: req.params.id, orgId: req.user!.orgId }, + include: { + contracts: { orderBy: { createdAt: 'desc' } }, + payslips: { orderBy: { month: 'desc' } }, + overtimeRecords: { orderBy: { month: 'desc' } }, + disciplinaryRecords: { orderBy: { violationDate: 'desc' } }, + attendanceRecords: { orderBy: { date: 'desc' }, take: 90 }, + trainingRecords: { orderBy: { trainingDate: 'desc' } }, + performanceRecords: { orderBy: { period: 'desc' } }, + terminations: { orderBy: { createdAt: 'desc' } }, + attachments: true, + }, + }) + if (!employee) { + return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '员工不存在' } }) + } + const { monthlySalary, ...rest } = employee + res.json({ + success: true, + data: { ...rest, monthlySalary: safeDecrypt(monthlySalary) }, + }) + } catch (err) { + next(err) + } +}) + +// 仲裁证据链导出 +router.get('/:id/evidence-chain', authMiddleware, async (req: AuthRequest, res, next) => { + try { + const employee = await prisma.employee.findFirst({ + where: { id: req.params.id, orgId: req.user!.orgId }, + include: { + contracts: { orderBy: { createdAt: 'desc' } }, + payslips: { orderBy: { month: 'desc' } }, + overtimeRecords: { orderBy: { month: 'desc' } }, + disciplinaryRecords: { orderBy: { violationDate: 'desc' } }, + attendanceRecords: { orderBy: { date: 'desc' } }, + trainingRecords: { orderBy: { trainingDate: 'desc' } }, + performanceRecords: { orderBy: { period: 'desc' } }, + terminations: true, + }, + }) + if (!employee) { + return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '员工不存在' } }) + } + + const evidence: any[] = [] + const empName = employee.name + const empDept = employee.department + const hireDate = employee.hireDate.toISOString().slice(0, 10) + + // 1. 劳动关系证据 + evidence.push({ + category: '劳动关系', + title: '入职登记', + date: hireDate, + description: `${empName}于${hireDate}入职${empDept},建立劳动关系。`, + evidenceType: 'EMPLOYMENT', + }) + employee.contracts.forEach((c) => { + evidence.push({ + category: '劳动关系', + title: `劳动合同(${c.contractType === 'FIXED' ? '固定期限' : c.contractType === 'UNFIXED' ? '无固定期限' : '未签订'})`, + date: c.signDate ? c.signDate.toISOString().slice(0, 10) : c.startDate.toISOString().slice(0, 10), + description: `合同期限:${c.startDate.toISOString().slice(0, 10)} 至 ${c.endDate ? c.endDate.toISOString().slice(0, 10) : '无固定期限'},试用期${c.probationMonths}个月,试用期工资¥${c.probationSalary}。`, + evidenceType: 'CONTRACT', + signed: !!c.signDate, + }) + }) + + // 2. 薪酬证据 + employee.payslips.forEach((p) => { + evidence.push({ + category: '薪酬发放', + title: `${p.month}月工资条`, + date: p.month, + description: `基本工资¥${p.baseSalary.toFixed(2)},加班费¥${p.overtimePay.toFixed(2)},津贴¥${p.allowance.toFixed(2)},扣款¥${p.deduction.toFixed(2)},应发合计¥${p.totalPay.toFixed(2)}。${p.confirmedAt ? '员工已确认。' : '员工未确认。'}`, + evidenceType: 'PAYSLIP', + confirmed: !!p.confirmedAt, + }) + }) + employee.overtimeRecords.forEach((o) => { + if (o.totalPay > 0) { + evidence.push({ + category: '薪酬发放', + title: `${o.month}月加班费记录`, + date: o.month, + description: `工作日加班${o.weekdayHours}h,休息日加班${o.weekendHours}h,节假日加班${o.holidayHours}h,加班费合计¥${o.totalPay.toFixed(2)}。`, + evidenceType: 'OVERTIME', + }) + } + }) + + // 3. 考勤证据 + const abnormalAttendance = employee.attendanceRecords.filter((a) => a.status !== 'NORMAL') + abnormalAttendance.forEach((a) => { + const statusMap: Record = { LATE: '迟到', EARLY_LEAVE: '早退', ABSENT: '旷工', LEAVE: '请假', BUSINESS_TRIP: '出差' } + evidence.push({ + category: '考勤记录', + title: `${a.date.toISOString().slice(0, 10)} 考勤异常`, + date: a.date.toISOString().slice(0, 10), + description: `状态:${statusMap[a.status] || a.status}${a.lateMinutes ? `,迟到${a.lateMinutes}分钟` : ''}${a.earlyMinutes ? `,早退${a.earlyMinutes}分钟` : ''}。${a.remark || ''}`, + evidenceType: 'ATTENDANCE', + }) + }) + + // 4. 违纪证据 + employee.disciplinaryRecords.forEach((d) => { + const typeMap: Record = { LATE: '迟到', ABSENT: '旷工', INSUBORDINATION: '不服从管理', MISCONDUCT: '违纪', VIOLATE_POLICY: '违反规章制度', OTHER: '其他' } + const actionMap: Record = { ORAL_WARNING: '口头警告', WRITTEN_WARNING: '书面警告', DEDUCTION: '扣款', DEMOTION: '降职', TERMINATION: '解除劳动合同' } + evidence.push({ + category: '违纪处理', + title: `${d.violationDate.toISOString().slice(0, 10)} ${typeMap[d.violationType] || d.violationType}`, + date: d.violationDate.toISOString().slice(0, 10), + description: `违纪事实:${d.description}。处理结果:${actionMap[d.action] || d.action}。${d.employeeAck ? `员工已签字确认(${d.ackDate ? d.ackDate.toISOString().slice(0, 10) : ''})。` : '员工未签字。'}${d.witness ? `见证人:${d.witness}。` : ''}`, + evidenceType: 'DISCIPLINARY', + acknowledged: d.employeeAck, + }) + }) + + // 5. 培训签收证据 + employee.trainingRecords.forEach((t) => { + const ackMap: Record = { PENDING: '待签收', SIGNED: '已签收', REFUSED: '拒绝签收' } + evidence.push({ + category: '培训签收', + title: `${t.trainingDate.toISOString().slice(0, 10)} ${t.topic}`, + date: t.trainingDate.toISOString().slice(0, 10), + description: `培训主题:${t.topic}。时长:${t.duration}小时。${t.content ? `内容:${t.content}。` : ''}签收状态:${ackMap[t.ackStatus] || t.ackStatus}。`, + evidenceType: 'TRAINING', + acknowledged: t.ackStatus === 'SIGNED', + }) + }) + + // 6. 绩效证据 + employee.performanceRecords.forEach((p) => { + const resultMap: Record = { EXCELLENT: '优秀', QUALIFIED: '合格', NEED_IMPROVE: '需改进', UNQUALIFIED: '不胜任' } + evidence.push({ + category: '绩效考核', + title: `${p.period} 绩效考核`, + date: p.period, + description: `得分:${p.score},等级:${p.grade},结果:${resultMap[p.result] || p.result}。${p.summary ? `评语:${p.summary}。` : ''}${p.improvementPlan ? `改进计划:${p.improvementPlan}。` : ''}${p.employeeAck ? '员工已签字确认。' : '员工未签字。'}`, + evidenceType: 'PERFORMANCE', + acknowledged: p.employeeAck, + }) + }) + + // 7. 解聘证据 + employee.terminations.forEach((t) => { + const reasonMap: Record = { NEGOTIATED: '协商解除', FAULT: '员工过错', NONFAULT: '非过错解除', LAYOFF: '经济性裁员', EXPIRED: '合同到期' } + evidence.push({ + category: '解聘记录', + title: `${t.terminationDate.toISOString().slice(0, 10)} 解聘记录`, + date: t.terminationDate.toISOString().slice(0, 10), + description: `解聘原因:${reasonMap[t.reason] || t.reason}。经济补偿金:¥${t.compensation.toFixed(2)}。${t.remark || ''}`, + evidenceType: 'TERMINATION', + }) + }) + + res.json({ + success: true, + data: { + employee: { + name: empName, + department: empDept, + hireDate, + status: employee.status, + gender: employee.gender, + phone: employee.phone, + }, + evidence, + summary: { + total: evidence.length, + signed: evidence.filter((e) => e.acknowledged === true).length, + unsigned: evidence.filter((e) => e.acknowledged === false).length, + }, + }, + }) + } catch (err) { + next(err) + } +}) + +// ========== 违纪记录 CRUD ========== + +router.get('/:employeeId/disciplinary', authMiddleware, async (req: AuthRequest, res, next) => { + try { + const records = await prisma.disciplinaryRecord.findMany({ + where: { employeeId: req.params.employeeId, orgId: req.user!.orgId }, + orderBy: { violationDate: 'desc' }, + }) + res.json({ success: true, data: records }) + } catch (err) { next(err) } +}) + +router.post('/:employeeId/disciplinary', authMiddleware, async (req: AuthRequest, res, next) => { + try { + const { violationDate, violationType, description, severity, action, actionDetail, employeeAck, ackDate, ackMethod, witness, attachmentUrl } = req.body + const record = await prisma.disciplinaryRecord.create({ + data: { + orgId: req.user!.orgId, + employeeId: req.params.employeeId, + violationDate: new Date(violationDate), + violationType, + description, + severity: severity || 'WARNING', + action: action || 'ORAL_WARNING', + actionDetail, + employeeAck: employeeAck || false, + ackDate: ackDate ? new Date(ackDate) : null, + ackMethod, + witness, + attachmentUrl, + createdBy: req.user!.id, + }, + }) + await auditLog(req, 'CREATE', 'DISCIPLINARY', record.id, { employeeId: req.params.employeeId }) + res.json({ success: true, data: record }) + } catch (err) { next(err) } +}) + +router.put('/:employeeId/disciplinary/:recordId', authMiddleware, async (req: AuthRequest, res, next) => { + try { + const { violationDate, violationType, description, severity, action, actionDetail, employeeAck, ackDate, ackMethod, witness, attachmentUrl } = req.body + const record = await prisma.disciplinaryRecord.findFirst({ + where: { id: req.params.recordId, orgId: req.user!.orgId }, + }) + if (!record) return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '记录不存在' } }) + const updated = await prisma.disciplinaryRecord.update({ + where: { id: req.params.recordId }, + data: { + violationDate: violationDate ? new Date(violationDate) : undefined, + violationType, + description, + severity, + action, + actionDetail, + employeeAck, + ackDate: ackDate ? new Date(ackDate) : null, + ackMethod, + witness, + attachmentUrl, + }, + }) + res.json({ success: true, data: updated }) + } catch (err) { next(err) } +}) + +router.delete('/:employeeId/disciplinary/:recordId', authMiddleware, async (req: AuthRequest, res, next) => { + try { + const record = await prisma.disciplinaryRecord.findFirst({ + where: { id: req.params.recordId, orgId: req.user!.orgId }, + }) + if (!record) return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '记录不存在' } }) + await prisma.disciplinaryRecord.delete({ where: { id: req.params.recordId } }) + res.json({ success: true }) + } catch (err) { next(err) } +}) + +// ========== 考勤记录 CRUD ========== + +router.get('/:employeeId/attendance', authMiddleware, async (req: AuthRequest, res, next) => { + try { + const records = await prisma.attendanceRecord.findMany({ + where: { employeeId: req.params.employeeId, orgId: req.user!.orgId }, + orderBy: { date: 'desc' }, + take: 90, + }) + res.json({ success: true, data: records }) + } catch (err) { next(err) } +}) + +router.post('/:employeeId/attendance', authMiddleware, async (req: AuthRequest, res, next) => { + try { + const { date, checkInTime, checkOutTime, status, lateMinutes, earlyMinutes, workHours, overtimeHours, remark } = req.body + const record = await prisma.attendanceRecord.upsert({ + where: { employeeId_date: { employeeId: req.params.employeeId, date: new Date(date) } }, + create: { + orgId: req.user!.orgId, + employeeId: req.params.employeeId, + date: new Date(date), + checkInTime, + checkOutTime, + status: status || 'NORMAL', + lateMinutes: lateMinutes || 0, + earlyMinutes: earlyMinutes || 0, + workHours: workHours || 0, + overtimeHours: overtimeHours || 0, + remark, + createdBy: req.user!.id, + }, + update: { + checkInTime, + checkOutTime, + status, + lateMinutes, + earlyMinutes, + workHours, + overtimeHours, + remark, + }, + }) + res.json({ success: true, data: record }) + } catch (err) { next(err) } +}) + +router.delete('/:employeeId/attendance/:recordId', authMiddleware, async (req: AuthRequest, res, next) => { + try { + const record = await prisma.attendanceRecord.findFirst({ + where: { id: req.params.recordId, orgId: req.user!.orgId }, + }) + if (!record) return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '记录不存在' } }) + await prisma.attendanceRecord.delete({ where: { id: req.params.recordId } }) + res.json({ success: true }) + } catch (err) { next(err) } +}) + +// ========== 培训签收记录 CRUD ========== + +router.get('/:employeeId/training', authMiddleware, async (req: AuthRequest, res, next) => { + try { + const records = await prisma.trainingRecord.findMany({ + where: { employeeId: req.params.employeeId, orgId: req.user!.orgId }, + orderBy: { trainingDate: 'desc' }, + }) + res.json({ success: true, data: records }) + } catch (err) { next(err) } +}) + +router.post('/:employeeId/training', authMiddleware, async (req: AuthRequest, res, next) => { + try { + const { trainingDate, topic, content, trainer, duration, ackStatus, ackDate, attachmentUrl, remark } = req.body + const record = await prisma.trainingRecord.create({ + data: { + orgId: req.user!.orgId, + employeeId: req.params.employeeId, + trainingDate: new Date(trainingDate), + topic, + content, + trainer, + duration: duration || 0, + ackStatus: ackStatus || 'PENDING', + ackDate: ackDate ? new Date(ackDate) : null, + attachmentUrl, + remark, + createdBy: req.user!.id, + }, + }) + await auditLog(req, 'CREATE', 'TRAINING', record.id, { employeeId: req.params.employeeId }) + res.json({ success: true, data: record }) + } catch (err) { next(err) } +}) + +router.put('/:employeeId/training/:recordId', authMiddleware, async (req: AuthRequest, res, next) => { + try { + const { trainingDate, topic, content, trainer, duration, ackStatus, ackDate, attachmentUrl, remark } = req.body + const record = await prisma.trainingRecord.findFirst({ + where: { id: req.params.recordId, orgId: req.user!.orgId }, + }) + if (!record) return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '记录不存在' } }) + const updated = await prisma.trainingRecord.update({ + where: { id: req.params.recordId }, + data: { + trainingDate: trainingDate ? new Date(trainingDate) : undefined, + topic, + content, + trainer, + duration, + ackStatus, + ackDate: ackDate ? new Date(ackDate) : null, + attachmentUrl, + remark, + }, + }) + res.json({ success: true, data: updated }) + } catch (err) { next(err) } +}) + +router.delete('/:employeeId/training/:recordId', authMiddleware, async (req: AuthRequest, res, next) => { + try { + const record = await prisma.trainingRecord.findFirst({ + where: { id: req.params.recordId, orgId: req.user!.orgId }, + }) + if (!record) return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '记录不存在' } }) + await prisma.trainingRecord.delete({ where: { id: req.params.recordId } }) + res.json({ success: true }) + } catch (err) { next(err) } +}) + +// ========== 绩效记录 CRUD ========== + +router.get('/:employeeId/performance', authMiddleware, async (req: AuthRequest, res, next) => { + try { + const records = await prisma.performanceRecord.findMany({ + where: { employeeId: req.params.employeeId, orgId: req.user!.orgId }, + orderBy: { period: 'desc' }, + }) + res.json({ success: true, data: records }) + } catch (err) { next(err) } +}) + +router.post('/:employeeId/performance', authMiddleware, async (req: AuthRequest, res, next) => { + try { + const { period, score, grade, result, summary, improvementPlan, employeeAck, ackDate, reviewer } = req.body + const record = await prisma.performanceRecord.upsert({ + where: { employeeId_period: { employeeId: req.params.employeeId, period } }, + create: { + orgId: req.user!.orgId, + employeeId: req.params.employeeId, + period, + score: score || 0, + grade: grade || 'B', + result: result || 'QUALIFIED', + summary, + improvementPlan, + employeeAck: employeeAck || false, + ackDate: ackDate ? new Date(ackDate) : null, + reviewer, + createdBy: req.user!.id, + }, + update: { + score, + grade, + result, + summary, + improvementPlan, + employeeAck, + ackDate: ackDate ? new Date(ackDate) : null, + reviewer, + }, + }) + await auditLog(req, 'CREATE', 'PERFORMANCE', record.id, { employeeId: req.params.employeeId }) + res.json({ success: true, data: record }) + } catch (err) { next(err) } +}) + +router.put('/:employeeId/performance/:recordId', authMiddleware, async (req: AuthRequest, res, next) => { + try { + const { period, score, grade, result, summary, improvementPlan, employeeAck, ackDate, reviewer } = req.body + const record = await prisma.performanceRecord.findFirst({ + where: { id: req.params.recordId, orgId: req.user!.orgId }, + }) + if (!record) return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '记录不存在' } }) + const updated = await prisma.performanceRecord.update({ + where: { id: req.params.recordId }, + data: { + period, + score, + grade, + result, + summary, + improvementPlan, + employeeAck, + ackDate: ackDate ? new Date(ackDate) : null, + reviewer, + }, + }) + res.json({ success: true, data: updated }) + } catch (err) { next(err) } +}) + +router.delete('/:employeeId/performance/:recordId', authMiddleware, async (req: AuthRequest, res, next) => { + try { + const record = await prisma.performanceRecord.findFirst({ + where: { id: req.params.recordId, orgId: req.user!.orgId }, + }) + if (!record) return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '记录不存在' } }) + await prisma.performanceRecord.delete({ where: { id: req.params.recordId } }) + res.json({ success: true }) + } catch (err) { next(err) } +}) + +export default router diff --git a/backend/src/routes/settings.routes.ts b/backend/src/routes/settings.routes.ts new file mode 100644 index 0000000..db2867a --- /dev/null +++ b/backend/src/routes/settings.routes.ts @@ -0,0 +1,118 @@ +import { Router, Request, Response, NextFunction } from 'express' +import bcrypt from 'bcryptjs' +import prisma from '../lib/prisma' +import { authMiddleware, AuthRequest } from '../middleware/auth' +import { z } from 'zod' + +const router = Router() +router.use(authMiddleware) + +const updateUserSchema = z.object({ + name: z.string().min(1).optional(), + phone: z.string().regex(/^1[3-9]\d{9}$/).optional(), + email: z.string().email().optional(), +}) + +const createUserSchema = z.object({ + name: z.string().min(1, '姓名不能为空'), + phone: z.string().regex(/^1[3-9]\d{9}$/, '手机号格式不正确'), + password: z.string().min(6, '密码至少6位'), + role: z.enum(['ADMIN', 'HR', 'VIEWER']).default('HR'), +}) + +// 获取企业信息 +router.get('/org', async (req: AuthRequest, res, next) => { + try { + const org = await prisma.organization.findUnique({ + where: { id: req.user!.orgId }, + select: { id: true, name: true, plan: true, maxEmployees: true, createdAt: true }, + }) + res.json({ success: true, data: org }) + } catch (err) { + next(err) + } +}) + +// 更新企业信息 +router.put('/org', async (req: AuthRequest, res, next) => { + try { + const { name } = req.body as { name?: string } + const org = await prisma.organization.update({ + where: { id: req.user!.orgId }, + data: name ? { name } : {}, + select: { id: true, name: true, plan: true, maxEmployees: true }, + }) + res.json({ success: true, data: org }) + } catch (err) { + next(err) + } +}) + +// 获取用户列表 +router.get('/users', async (req: AuthRequest, res, next) => { + try { + const users = await prisma.user.findMany({ + where: { orgId: req.user!.orgId }, + select: { id: true, name: true, phone: true, email: true, role: true, createdAt: true }, + orderBy: { createdAt: 'asc' }, + }) + res.json({ success: true, data: users }) + } catch (err) { + next(err) + } +}) + +// 添加用户 +router.post('/users', async (req: AuthRequest, res, next) => { + try { + const data = createUserSchema.parse(req.body) + const existing = await prisma.user.findFirst({ where: { phone: data.phone, orgId: req.user!.orgId } }) + if (existing) { + return res.status(400).json({ success: false, error: { code: 'DUPLICATE', message: '该手机号已存在' } }) + } + const passwordHash = await bcrypt.hash(data.password, 10) + const user = await prisma.user.create({ + data: { + orgId: req.user!.orgId, + name: data.name, + phone: data.phone, + passwordHash, + role: data.role, + }, + select: { id: true, name: true, phone: true, role: true }, + }) + res.json({ success: true, data: user }) + } catch (err) { + next(err) + } +}) + +// 更新用户 +router.put('/users/:id', async (req: AuthRequest, res, next) => { + try { + const data = updateUserSchema.parse(req.body) + const user = await prisma.user.update({ + where: { id: req.params.id }, + data: data, + select: { id: true, name: true, phone: true, role: true }, + }) + res.json({ success: true, data: user }) + } catch (err) { + next(err) + } +}) + +// 删除用户 +router.delete('/users/:id', async (req: AuthRequest, res, next) => { + try { + if (req.params.id === req.user!.id) { + return res.status(400).json({ success: false, error: { code: 'BAD_REQUEST', message: '不能删除自己' } }) + } + await prisma.user.delete({ where: { id: req.params.id } }) + res.json({ success: true }) + } catch (err) { + next(err) + } +}) + +export default router diff --git a/backend/src/routes/social.routes.ts b/backend/src/routes/social.routes.ts new file mode 100644 index 0000000..dd077fe --- /dev/null +++ b/backend/src/routes/social.routes.ts @@ -0,0 +1,114 @@ +import { Router, Response, NextFunction } from 'express' +import prisma from '../lib/prisma' +import { authMiddleware, AuthRequest } from '../middleware/auth' +import { z } from 'zod' + +const router = Router() +router.use(authMiddleware) + +// 获取社保配置 +router.get('/config', async (req: AuthRequest, res: Response, next: NextFunction) => { + try { + let config = await prisma.socialInsuranceConfig.findUnique({ + where: { orgId: req.user!.orgId }, + }) + if (!config) { + config = await prisma.socialInsuranceConfig.create({ + data: { orgId: req.user!.orgId }, + }) + } + res.json({ success: true, data: config }) + } catch (err) { + next(err) + } +}) + +// 更新社保配置 +const configSchema = z.object({ + city: z.string().optional(), + pensionOrg: z.number().optional(), + pensionEmp: z.number().optional(), + medicalOrg: z.number().optional(), + medicalEmp: z.number().optional(), + unemploymentOrg: z.number().optional(), + unemploymentEmp: z.number().optional(), + injuryOrg: z.number().optional(), + maternityOrg: z.number().optional(), + housingOrg: z.number().optional(), + housingEmp: z.number().optional(), + baseMin: z.number().optional(), + baseMax: z.number().optional(), +}) + +router.put('/config', async (req: AuthRequest, res: Response, next: NextFunction) => { + try { + const data = configSchema.parse(req.body) + const config = await prisma.socialInsuranceConfig.upsert({ + where: { orgId: req.user!.orgId }, + update: data, + create: { orgId: req.user!.orgId, ...data }, + }) + res.json({ success: true, data: config }) + } catch (err) { + next(err) + } +}) + +// 社保计算 +const calcSchema = z.object({ + base: z.number().positive(), +}) + +router.post('/calculate', async (req: AuthRequest, res: Response, next: NextFunction) => { + try { + const { base } = calcSchema.parse(req.body) + let config = await prisma.socialInsuranceConfig.findUnique({ + where: { orgId: req.user!.orgId }, + }) + if (!config) { + config = await prisma.socialInsuranceConfig.create({ data: { orgId: req.user!.orgId } }) + } + + const actualBase = Math.min(Math.max(base, config.baseMin), config.baseMax) + + const pensionOrg = actualBase * config.pensionOrg / 100 + const pensionEmp = actualBase * config.pensionEmp / 100 + const medicalOrg = actualBase * config.medicalOrg / 100 + const medicalEmp = actualBase * config.medicalEmp / 100 + const unemploymentOrg = actualBase * config.unemploymentOrg / 100 + const unemploymentEmp = actualBase * config.unemploymentEmp / 100 + const injuryOrg = actualBase * config.injuryOrg / 100 + const maternityOrg = actualBase * config.maternityOrg / 100 + const housingOrg = actualBase * config.housingOrg / 100 + const housingEmp = actualBase * config.housingEmp / 100 + + const totalOrg = pensionOrg + medicalOrg + unemploymentOrg + injuryOrg + maternityOrg + housingOrg + const totalEmp = pensionEmp + medicalEmp + unemploymentEmp + housingEmp + const total = totalOrg + totalEmp + + res.json({ + success: true, + data: { + actualBase, + originalBase: base, + capped: base > config.baseMax, + floored: base < config.baseMin, + items: [ + { name: '养老保险', orgRate: config.pensionOrg, empRate: config.pensionEmp, orgAmount: pensionOrg, empAmount: pensionEmp }, + { name: '医疗保险', orgRate: config.medicalOrg, empRate: config.medicalEmp, orgAmount: medicalOrg, empAmount: medicalEmp }, + { name: '失业保险', orgRate: config.unemploymentOrg, empRate: config.unemploymentEmp, orgAmount: unemploymentOrg, empAmount: unemploymentEmp }, + { name: '工伤保险', orgRate: config.injuryOrg, empRate: 0, orgAmount: injuryOrg, empAmount: 0 }, + { name: '生育保险', orgRate: config.maternityOrg, empRate: 0, orgAmount: maternityOrg, empAmount: 0 }, + { name: '住房公积金', orgRate: config.housingOrg, empRate: config.housingEmp, orgAmount: housingOrg, empAmount: housingEmp }, + ], + totalOrg, + totalEmp, + total, + }, + }) + } catch (err) { + next(err) + } +}) + +export default router diff --git a/backend/src/routes/termination.routes.ts b/backend/src/routes/termination.routes.ts new file mode 100644 index 0000000..7153d94 --- /dev/null +++ b/backend/src/routes/termination.routes.ts @@ -0,0 +1,51 @@ +import { Router } from 'express' +import { authMiddleware, AuthRequest } from '../middleware/auth' +import { auditLog } from '../middleware/auditLog' +import { terminationChecklistSchema } from '../schemas/termination.schema' +import { createTermination, getTerminations, getChecklistForReason, assessRisk, calculateCompensation } from '../services/termination.service' +import prisma from '../lib/prisma' +import { decrypt } from '../lib/crypto' + +const router = Router() + +router.get('/', authMiddleware, async (req: AuthRequest, res, next) => { + try { + const page = parseInt(req.query.page as string) || 1 + const pageSize = parseInt(req.query.pageSize as string) || 20 + const result = await getTerminations(req.user!.orgId, page, pageSize) + res.json({ success: true, data: result }) + } catch (err) { + next(err) + } +}) + +router.get('/checklist/:reason', authMiddleware, (req: AuthRequest, res) => { + const checklist = getChecklistForReason(req.params.reason) + res.json({ success: true, data: checklist }) +}) + +router.get('/assess/:employeeId', authMiddleware, async (req: AuthRequest, res, next) => { + try { + const employee = await prisma.employee.findFirst({ where: { id: req.params.employeeId, orgId: req.user!.orgId } }) + if (!employee) { + return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '员工不存在' } }) + } + const assessment = assessRisk(employee, req.query.reason as string || '') + res.json({ success: true, data: assessment }) + } catch (err) { + next(err) + } +}) + +router.post('/', authMiddleware, async (req: AuthRequest, res, next) => { + try { + const data = terminationChecklistSchema.parse(req.body) + const result = await createTermination(req.user!.orgId, req.user!.id, data) + await auditLog(req, 'TERMINATE', 'EMPLOYEE', data.employeeId, { reason: data.reason }) + res.json({ success: true, data: result }) + } catch (err) { + next(err) + } +}) + +export default router diff --git a/backend/src/schemas/auth.schema.ts b/backend/src/schemas/auth.schema.ts new file mode 100644 index 0000000..770d9ff --- /dev/null +++ b/backend/src/schemas/auth.schema.ts @@ -0,0 +1,29 @@ +import { z } from 'zod' + +export const registerSchema = z.object({ + orgName: z.string().min(2, '企业名称至少2个字').max(50, '企业名称最多50个字'), + phone: z.string().regex(/^1[3-9]\d{9}$/, '手机号格式不正确'), + password: z.string().min(8, '密码至少8位').max(32, '密码最多32位'), + confirmPassword: z.string(), +}).refine((data) => data.password === data.confirmPassword, { + message: '两次密码不一致', + path: ['confirmPassword'], +}) + +export const loginSchema = z.object({ + phone: z.string().regex(/^1[3-9]\d{9}$/, '手机号格式不正确'), + password: z.string().min(1, '请输入密码'), +}) + +export const refreshSchema = z.object({ + refreshToken: z.string().min(1, '缺少 refreshToken'), +}) + +export const forgotPasswordSchema = z.object({ + phone: z.string().regex(/^1[3-9]\d{9}$/, '手机号格式不正确'), +}) + +export const resetPasswordSchema = z.object({ + phone: z.string().regex(/^1[3-9]\d{9}$/, '手机号格式不正确'), + newPassword: z.string().min(8, '密码至少8位').max(32, '密码最多32位'), +}) diff --git a/backend/src/schemas/contract.schema.ts b/backend/src/schemas/contract.schema.ts new file mode 100644 index 0000000..1bbb233 --- /dev/null +++ b/backend/src/schemas/contract.schema.ts @@ -0,0 +1,52 @@ +import { z } from 'zod' + +export const createEmployeeSchema = z.object({ + name: z.string().min(1, '姓名不能为空').max(30, '姓名最多30个字'), + department: z.string().min(1, '部门不能为空').max(50, '部门最多50个字'), + hireDate: z.string().datetime(), + monthlySalary: z.string().min(1, '月薪不能为空'), + gender: z.enum(['男', '女']).optional(), + phone: z.string().regex(/^1[3-9]\d{9}$/).optional(), + isPregnant: z.boolean().default(false), + isInMedicalPeriod: z.boolean().default(false), + isWorkInjured: z.boolean().default(false), + contract: z.object({ + signDate: z.string().datetime().nullable(), + startDate: z.string().datetime(), + endDate: z.string().datetime().nullable(), + contractType: z.enum(['FIXED', 'UNFIXED', 'UNSIGNED']), + signMethod: z.enum(['PAPER', 'ELECTRONIC']).default('PAPER'), + contractYears: z.number().int().min(1).max(10).default(3), + probationMonths: z.number().int().min(0).max(6).default(0), + probationSalary: z.number().min(0).default(0), + }).optional(), +}) + +export const updateEmployeeSchema = z.object({ + name: z.string().min(1).max(30).optional(), + department: z.string().min(1).max(50).optional(), + hireDate: z.string().datetime().optional(), + monthlySalary: z.string().min(1).optional(), + gender: z.enum(['男', '女']).optional(), + phone: z.string().regex(/^1[3-9]\d{9}$/).optional(), + isPregnant: z.boolean().optional(), + isInMedicalPeriod: z.boolean().optional(), + isWorkInjured: z.boolean().optional(), +}) + +export const batchRenewSchema = z.object({ + contractIds: z.array(z.string()).min(1, '至少选择一个合同'), + years: z.number().int().min(1).max(5).default(3), +}) + +export const addContractSchema = z.object({ + employeeId: z.string().min(1), + signDate: z.string().datetime().nullable(), + startDate: z.string().datetime(), + endDate: z.string().datetime().nullable(), + contractType: z.enum(['FIXED', 'UNFIXED', 'UNSIGNED']), + signMethod: z.enum(['PAPER', 'ELECTRONIC']).default('PAPER'), + contractYears: z.number().int().min(1).max(10).default(3), + probationMonths: z.number().int().min(0).max(6).default(0), + probationSalary: z.number().min(0).default(0), +}) diff --git a/backend/src/schemas/portal.schema.ts b/backend/src/schemas/portal.schema.ts new file mode 100644 index 0000000..db897e7 --- /dev/null +++ b/backend/src/schemas/portal.schema.ts @@ -0,0 +1,32 @@ +import { z } from 'zod' + +export const portalLoginSchema = z.object({ + phone: z.string().regex(/^1[3-9]\d{9}$/, '手机号格式不正确'), + password: z.string().min(6, '密码至少6位'), +}) + +export const portalSendCodeSchema = z.object({ + phone: z.string().regex(/^1[3-9]\d{9}$/, '手机号格式不正确'), +}) + +export const portalVerifyCodeSchema = z.object({ + phone: z.string().regex(/^1[3-9]\d{9}$/, '手机号格式不正确'), + code: z.string().length(6, '验证码为6位数字'), +}) + +export const onboardingSchema = z.object({ + token: z.string().min(1, '缺少 token'), + name: z.string().min(1, '姓名不能为空'), + phone: z.string().regex(/^1[3-9]\d{9}$/, '手机号格式不正确'), + idCard: z.string().min(15, '身份证号格式不正确').max(18), + emergencyContact: z.string().optional(), + emergencyPhone: z.string().optional(), + address: z.string().optional(), + bankCard: z.string().optional(), + bankName: z.string().optional(), +}) + +export const contractConfirmSchema = z.object({ + token: z.string().min(1, '缺少 token'), + agreed: z.boolean().refine((v) => v === true, '请勾选确认签署'), +}) diff --git a/backend/src/schemas/termination.schema.ts b/backend/src/schemas/termination.schema.ts new file mode 100644 index 0000000..64857e7 --- /dev/null +++ b/backend/src/schemas/termination.schema.ts @@ -0,0 +1,15 @@ +import { z } from 'zod' + +export const terminationChecklistSchema = z.object({ + employeeId: z.string().min(1, '请选择员工'), + reason: z.enum(['NEGOTIATED', 'FAULT', 'NONFAULT', 'LAYOFF', 'EXPIRED']), + terminationDate: z.string().datetime(), + compensation: z.number().min(0).default(0), + checklist: z.record(z.boolean()).default({}), + remark: z.string().max(500).optional(), +}) + +export const terminationQuerySchema = z.object({ + page: z.coerce.number().min(1).default(1), + pageSize: z.coerce.number().min(1).max(50).default(20), +}) diff --git a/backend/src/services/ai.service.ts b/backend/src/services/ai.service.ts new file mode 100644 index 0000000..2b349aa --- /dev/null +++ b/backend/src/services/ai.service.ts @@ -0,0 +1,129 @@ +import OpenAI from 'openai' + +const apiKey = process.env.DASHSCOPE_API_KEY || '' +const baseURL = 'https://dashscope.aliyuncs.com/compatible-mode/v1' + +const client = new OpenAI({ apiKey, baseURL }) + +const SYSTEM_PROMPT = `你是一个专业的劳动用工合规顾问,精通中国劳动法、劳动合同法、社会保险法等相关法律法规。 + +你的职责: +1. 回答用户关于劳动用工的合规问题 +2. 基于企业实际数据给出针对性建议 +3. 引用具体法律条文作为依据 +4. 用通俗易懂的语言解释法律问题 + +回答要求: +- 先给出直接结论,再展开解释 +- 引用法律条文时标注具体法律名称和条款号 +- 涉及金额时给出计算过程 +- 如有关联的企业数据,在回答中提及 +- 回答简洁有力,避免冗长` + +export async function chat(messages: { role: 'user' | 'assistant'; content: string }[], orgContext?: string) { + const systemMessage = orgContext + ? `${SYSTEM_PROMPT}\n\n当前企业数据概览:\n${orgContext}` + : SYSTEM_PROMPT + + const response = await client.chat.completions.create({ + model: 'qwen-plus', + messages: [ + { role: 'system', content: systemMessage }, + ...messages, + ], + temperature: 0.7, + max_tokens: 2000, + }) + + return response.choices[0]?.message?.content || '' +} + +export async function reviewContract(contractText: string) { + const prompt = `请审查以下劳动合同文本的合法性,逐条检查并标注风险等级(红/黄/绿),给出修改建议,最后给出合规评分(0-100分)。 + +合同文本: +${contractText} + +请按以下格式输出: +【风险项】 +🔴/🟡/🟢 [问题标题] - [说明] - [修改建议] + +【合规评分】XX/100 + +【总体建议】 +一段话总结` + + const response = await client.chat.completions.create({ + model: 'qwen-max', + messages: [ + { role: 'system', content: '你是劳动法合同审查专家,精通劳动合同法。' }, + { role: 'user', content: prompt }, + ], + temperature: 0.3, + max_tokens: 3000, + }) + + return response.choices[0]?.message?.content || '' +} + +export async function matchCase(scenario: string) { + const prompt = `作为一个劳动法案例匹配专家,请分析以下劳动争议情形,匹配相似的仲裁/诉讼案例,评估败诉风险。 + +争议情形: +${scenario} + +请按以下格式输出: +【相似案例】 +案例1:[案例标题] +- 情形:[简要描述] +- 结果:[判决结果] +- 赔偿金额:[金额] +- 相似度:XX% + +案例2:... + +【败诉风险评估】 +风险等级:高/中/低(XX%) +原因:[分析] + +【建议】 +[降低风险的具体建议]` + + const response = await client.chat.completions.create({ + model: 'qwen-max', + messages: [ + { role: 'system', content: '你是劳动法案例分析专家,熟悉劳动仲裁和诉讼案例。' }, + { role: 'user', content: prompt }, + ], + temperature: 0.3, + max_tokens: 3000, + }) + + return response.choices[0]?.message?.content || '' +} + +export async function predictRisks(orgContext: string) { + const prompt = `基于以下企业用工数据,预测未来30天可能出现的合规风险,并给出优先级建议。 + +企业数据: +${orgContext} + +请按以下格式输出: +【未来30天预计风险】 +- [员工姓名/风险描述] → [建议措施] + +【优先级建议】 +[先处理什么,再处理什么]` + + const response = await client.chat.completions.create({ + model: 'qwen-plus', + messages: [ + { role: 'system', content: '你是劳动用工风险预测专家,能基于企业数据分析未来风险趋势。' }, + { role: 'user', content: prompt }, + ], + temperature: 0.5, + max_tokens: 1500, + }) + + return response.choices[0]?.message?.content || '' +} diff --git a/backend/src/services/auth.service.ts b/backend/src/services/auth.service.ts new file mode 100644 index 0000000..1f009c2 --- /dev/null +++ b/backend/src/services/auth.service.ts @@ -0,0 +1,99 @@ +import bcrypt from 'bcryptjs' +import prisma from '../lib/prisma' +import { signAccessToken, signRefreshToken, verifyRefreshToken } from '../lib/jwt' + +export async function register(orgName: string, phone: string, password: string) { + const existing = await prisma.user.findUnique({ where: { phone } }) + if (existing) { + throw { code: 'DUPLICATE', message: '该手机号已注册' } + } + + const org = await prisma.organization.create({ + data: { + name: orgName, + plan: 'FREE', + maxEmployees: 20, + }, + }) + + const passwordHash = await bcrypt.hash(password, 10) + const user = await prisma.user.create({ + data: { + orgId: org.id, + phone, + name: '管理员', + passwordHash, + role: 'ADMIN', + }, + }) + + await prisma.user.update({ + where: { id: user.id }, + data: { lastLoginAt: new Date() }, + }) + + const accessToken = signAccessToken({ id: user.id, orgId: user.orgId, role: user.role }) + const refreshToken = signRefreshToken({ id: user.id, orgId: user.orgId, role: user.role }) + + return { + user: { id: user.id, orgId: user.orgId, name: user.name, phone: user.phone, role: user.role }, + accessToken, + refreshToken, + } +} + +export async function login(phone: string, password: string) { + const user = await prisma.user.findUnique({ where: { phone } }) + if (!user) { + throw { code: 'NOT_FOUND', message: '手机号或密码错误' } + } + + const valid = await bcrypt.compare(password, user.passwordHash) + if (!valid) { + throw { code: 'AUTH_FAILED', message: '手机号或密码错误' } + } + + await prisma.user.update({ + where: { id: user.id }, + data: { lastLoginAt: new Date() }, + }) + + const accessToken = signAccessToken({ id: user.id, orgId: user.orgId, role: user.role }) + const refreshToken = signRefreshToken({ id: user.id, orgId: user.orgId, role: user.role }) + + return { + user: { id: user.id, orgId: user.orgId, name: user.name, phone: user.phone, role: user.role }, + accessToken, + refreshToken, + } +} + +export async function refresh(refreshToken: string) { + const payload = verifyRefreshToken(refreshToken) + if (!payload) { + throw { code: 'TOKEN_INVALID', message: 'Refresh Token 无效或已过期' } + } + + const user = await prisma.user.findUnique({ where: { id: payload.id } }) + if (!user) { + throw { code: 'NOT_FOUND', message: '用户不存在' } + } + + const accessToken = signAccessToken({ id: user.id, orgId: user.orgId, role: user.role }) + return { accessToken } +} + +export async function resetPassword(phone: string, newPassword: string) { + const user = await prisma.user.findUnique({ where: { phone } }) + if (!user) { + throw { code: 'NOT_FOUND', message: '手机号未注册' } + } + + const passwordHash = await bcrypt.hash(newPassword, 10) + await prisma.user.update({ + where: { id: user.id }, + data: { passwordHash }, + }) + + return { success: true } +} diff --git a/backend/src/services/contract.service.ts b/backend/src/services/contract.service.ts new file mode 100644 index 0000000..d092fee --- /dev/null +++ b/backend/src/services/contract.service.ts @@ -0,0 +1,314 @@ +import prisma from '../lib/prisma' +import { encrypt, decrypt } from '../lib/crypto' +import { runRiskDetection } from './risk.service' + +function daysBetween(a: Date, b: Date): number { + return Math.floor((a.getTime() - b.getTime()) / (1000 * 60 * 60 * 24)) +} + +export function getContractStatus(contract: { + signDate: Date | null + startDate: Date + endDate: Date | null + contractType: string + hireDate: Date +}): { status: string; statusText: string; riskLevel: 'high' | 'medium' | 'low' | 'safe' } { + const today = new Date() + + if (!contract.signDate || contract.contractType === 'UNSIGNED') { + const days = daysBetween(today, contract.hireDate) + if (days > 365) { + return { status: 'unsigned_over_year', statusText: '已视为无固定期限', riskLevel: 'high' } + } else if (days > 30) { + return { status: 'unsigned_over_30', statusText: `未签合同(${days}天)`, riskLevel: 'high' } + } + return { status: 'unsigned', statusText: `未签合同(${days}天)`, riskLevel: 'medium' } + } + + if (contract.endDate) { + const daysToExpire = daysBetween(contract.endDate, today) + if (daysToExpire < 0) { + return { status: 'expired', statusText: '已到期未续签', riskLevel: 'high' } + } else if (daysToExpire <= 30) { + return { status: 'expiring', statusText: `即将到期(${daysToExpire}天)`, riskLevel: 'medium' } + } + return { status: 'active', statusText: '正常', riskLevel: 'safe' } + } + + return { status: 'unfixed', statusText: '无固定期限', riskLevel: 'safe' } +} + +export function validateProbation(contractMonths: number, probationMonths: number): { valid: boolean; max: number; message?: string } { + let max = 0 + if (contractMonths >= 36) max = 6 + else if (contractMonths >= 12) max = 2 + else if (contractMonths >= 3) max = 1 + + if (probationMonths > max) { + return { + valid: false, + max, + message: `${contractMonths}个月合同试用期最多${max}个月,当前${probationMonths}个月不合法`, + } + } + return { valid: true, max } +} + +export async function getEmployees(orgId: string, params: { page?: number; pageSize?: number; search?: string; department?: string }) { + const page = params.page || 1 + const pageSize = params.pageSize || 20 + const skip = (page - 1) * pageSize + + const where: any = { orgId, status: 'ACTIVE' } + if (params.search) { + where.OR = [ + { name: { contains: params.search } }, + { phone: { contains: params.search } }, + ] + } + if (params.department) { + where.department = params.department + } + + const [total, employees] = await Promise.all([ + prisma.employee.count({ where }), + prisma.employee.findMany({ + where, + include: { + contracts: { orderBy: { createdAt: 'desc' }, take: 1 }, + }, + orderBy: { createdAt: 'desc' }, + skip, + take: pageSize, + }), + ]) + + const items = employees.map((emp) => { + const latestContract = emp.contracts[0] + const contractInfo = latestContract + ? getContractStatus({ + signDate: latestContract.signDate, + startDate: latestContract.startDate, + endDate: latestContract.endDate, + contractType: latestContract.contractType, + hireDate: emp.hireDate, + }) + : getContractStatus({ + signDate: null, + startDate: emp.hireDate, + endDate: null, + contractType: 'UNSIGNED', + hireDate: emp.hireDate, + }) + + let decryptedSalary = 0 + try { + decryptedSalary = Number(decrypt(emp.monthlySalary)) || 0 + } catch { + decryptedSalary = Number(emp.monthlySalary) || 0 + } + + return { + id: emp.id, + name: emp.name, + department: emp.department, + hireDate: emp.hireDate.toISOString().slice(0, 10), + status: emp.status, + monthlySalary: decryptedSalary, + contractStatus: contractInfo.status, + contractStatusText: contractInfo.statusText, + riskLevel: contractInfo.riskLevel, + isPregnant: emp.isPregnant, + isInMedicalPeriod: emp.isInMedicalPeriod, + isWorkInjured: emp.isWorkInjured, + } + }) + + return { items, total, page, pageSize, totalPages: Math.ceil(total / pageSize) } +} + +export async function getEmployeeDetail(orgId: string, id: string) { + const employee = await prisma.employee.findFirst({ + where: { id, orgId }, + include: { + contracts: { orderBy: { createdAt: 'desc' } }, + riskItems: { where: { status: 'PENDING' }, orderBy: { level: 'asc' } }, + }, + }) + + if (!employee) { + throw { code: 'NOT_FOUND', message: '员工不存在' } + } + + let decryptedSalary = 0 + try { + decryptedSalary = Number(decrypt(employee.monthlySalary)) || 0 + } catch { + decryptedSalary = Number(employee.monthlySalary) || 0 + } + + return { + ...employee, + monthlySalary: decryptedSalary, + } +} + +export async function createEmployee(orgId: string, userId: string, data: any) { + const employee = await prisma.employee.create({ + data: { + orgId, + name: data.name, + department: data.department, + hireDate: new Date(data.hireDate), + monthlySalary: encrypt(data.monthlySalary), + gender: data.gender, + phone: data.phone, + isPregnant: data.isPregnant || false, + isInMedicalPeriod: data.isInMedicalPeriod || false, + isWorkInjured: data.isWorkInjured || false, + createdBy: userId, + }, + }) + + if (data.contract && data.contract.contractType !== 'UNSIGNED') { + const contractMonths = data.contract.endDate + ? Math.ceil(daysBetween(new Date(data.contract.endDate), new Date(data.contract.startDate)) / 30.44) + : data.contract.contractYears * 12 + + const probationCheck = validateProbation(contractMonths, data.contract.probationMonths) + if (!probationCheck.valid) { + throw { code: 'VALIDATION_ERROR', message: probationCheck.message } + } + + await prisma.laborContract.create({ + data: { + orgId, + employeeId: employee.id, + signDate: data.contract.signDate ? new Date(data.contract.signDate) : null, + startDate: new Date(data.contract.startDate), + endDate: data.contract.endDate ? new Date(data.contract.endDate) : null, + contractType: data.contract.contractType, + signMethod: data.contract.signMethod || 'PAPER', + contractYears: data.contract.contractYears || 3, + probationMonths: data.contract.probationMonths || 0, + probationSalary: data.contract.probationSalary || 0, + createdBy: userId, + }, + }) + } + + await runRiskDetection(orgId) + + return { id: employee.id } +} + +export async function updateEmployee(orgId: string, id: string, data: any) { + const employee = await prisma.employee.findFirst({ where: { id, orgId } }) + if (!employee) { + throw { code: 'NOT_FOUND', message: '员工不存在' } + } + + const updateData: any = {} + if (data.name !== undefined) updateData.name = data.name + if (data.department !== undefined) updateData.department = data.department + if (data.hireDate !== undefined) updateData.hireDate = new Date(data.hireDate) + if (data.monthlySalary !== undefined) updateData.monthlySalary = encrypt(data.monthlySalary) + if (data.gender !== undefined) updateData.gender = data.gender + if (data.phone !== undefined) updateData.phone = data.phone + if (data.isPregnant !== undefined) updateData.isPregnant = data.isPregnant + if (data.isInMedicalPeriod !== undefined) updateData.isInMedicalPeriod = data.isInMedicalPeriod + if (data.isWorkInjured !== undefined) updateData.isWorkInjured = data.isWorkInjured + + await prisma.employee.update({ where: { id }, data: updateData }) + await runRiskDetection(orgId) + + return { id } +} + +export async function deleteEmployee(orgId: string, id: string) { + const employee = await prisma.employee.findFirst({ where: { id, orgId } }) + if (!employee) { + throw { code: 'NOT_FOUND', message: '员工不存在' } + } + + await prisma.employee.update({ where: { id }, data: { status: 'RESIGNED' } }) + await prisma.riskItem.updateMany({ + where: { employeeId: id, status: 'PENDING' }, + data: { status: 'RESOLVED', resolvedAt: new Date() }, + }) + + return { id } +} + +export async function batchRenew(orgId: string, userId: string, contractIds: string[], years: number) { + const contracts = await prisma.laborContract.findMany({ + where: { id: { in: contractIds }, orgId }, + }) + + if (contracts.length === 0) { + throw { code: 'NOT_FOUND', message: '未找到符合条件的合同' } + } + + for (const contract of contracts) { + const newStartDate = contract.endDate || new Date() + const newEndDate = new Date(newStartDate) + newEndDate.setFullYear(newEndDate.getFullYear() + years) + + await prisma.laborContract.create({ + data: { + orgId, + employeeId: contract.employeeId, + signDate: new Date(), + startDate: newStartDate, + endDate: newEndDate, + contractType: contract.contractType, + signMethod: contract.signMethod, + contractYears: years, + probationMonths: 0, + probationSalary: 0, + renewalCount: contract.renewalCount + 1, + createdBy: userId, + }, + }) + } + + await runRiskDetection(orgId) + + return { renewed: contracts.length } +} + +export async function addContract(orgId: string, userId: string, data: any) { + const employee = await prisma.employee.findFirst({ where: { id: data.employeeId, orgId } }) + if (!employee) { + throw { code: 'NOT_FOUND', message: '员工不存在' } + } + + const contractMonths = data.endDate + ? Math.ceil(daysBetween(new Date(data.endDate), new Date(data.startDate)) / 30.44) + : data.contractYears * 12 + + const probationCheck = validateProbation(contractMonths, data.probationMonths) + if (!probationCheck.valid) { + throw { code: 'VALIDATION_ERROR', message: probationCheck.message } + } + + const contract = await prisma.laborContract.create({ + data: { + orgId, + employeeId: data.employeeId, + signDate: data.signDate ? new Date(data.signDate) : null, + startDate: new Date(data.startDate), + endDate: data.endDate ? new Date(data.endDate) : null, + contractType: data.contractType, + signMethod: data.signMethod || 'PAPER', + contractYears: data.contractYears || 3, + probationMonths: data.probationMonths || 0, + probationSalary: data.probationSalary || 0, + createdBy: userId, + }, + }) + + await runRiskDetection(orgId) + + return { id: contract.id } +} diff --git a/backend/src/services/risk.service.ts b/backend/src/services/risk.service.ts new file mode 100644 index 0000000..3b5c2fd --- /dev/null +++ b/backend/src/services/risk.service.ts @@ -0,0 +1,393 @@ +import prisma from '../lib/prisma' +import { decrypt } from '../lib/crypto' +import type { ContractType, RiskLevel, RiskType } from '@prisma/client' + +function daysBetween(a: Date, b: Date): number { + return Math.floor((a.getTime() - b.getTime()) / (1000 * 60 * 60 * 24)) +} + +export async function detectContractRisks(orgId: string) { + const employees = await prisma.employee.findMany({ + where: { orgId, status: 'ACTIVE' }, + include: { contracts: { orderBy: { createdAt: 'desc' } } }, + }) + + const risks: { employeeId: string; type: RiskType; level: RiskLevel; title: string; description: string; actionUrl: string }[] = [] + + for (const emp of employees) { + const latestContract = emp.contracts[0] + + if (!latestContract || latestContract.contractType === 'UNSIGNED') { + const days = daysBetween(new Date(), emp.hireDate) + if (days > 365) { + risks.push({ + employeeId: emp.id, + type: 'CONTRACT', + level: 'HIGH', + title: `${emp.name}入职${days}天未签合同,已视为无固定期限`, + description: `入职日期 ${emp.hireDate.toISOString().slice(0, 10)},超过1年未签订书面合同,法律上已视为无固定期限劳动合同。`, + actionUrl: `/contracts?employee=${encodeURIComponent(emp.name)}`, + }) + } else if (days > 30) { + risks.push({ + employeeId: emp.id, + type: 'CONTRACT', + level: 'HIGH', + title: `${emp.name}入职${days}天未签合同`, + description: `入职日期 ${emp.hireDate.toISOString().slice(0, 10)},超过30天未签订书面合同,需尽快补签。`, + actionUrl: `/contracts?employee=${encodeURIComponent(emp.name)}`, + }) + } else { + risks.push({ + employeeId: emp.id, + type: 'CONTRACT', + level: 'LOW', + title: `${emp.name}入职${days}天,尚未签合同`, + description: `入职日期 ${emp.hireDate.toISOString().slice(0, 10)},30天内需签订书面合同。`, + actionUrl: `/contracts?employee=${encodeURIComponent(emp.name)}`, + }) + } + continue + } + + if (latestContract.endDate) { + const daysToExpire = daysBetween(latestContract.endDate, new Date()) + if (daysToExpire < 0) { + risks.push({ + employeeId: emp.id, + type: 'CONTRACT', + level: 'HIGH', + title: `${emp.name}的合同已到期${Math.abs(daysToExpire)}天未续签`, + description: `合同到期日 ${latestContract.endDate.toISOString().slice(0, 10)},已过期未续签。`, + actionUrl: `/contracts?employee=${encodeURIComponent(emp.name)}`, + }) + } else if (daysToExpire <= 30) { + risks.push({ + employeeId: emp.id, + type: 'CONTRACT', + level: 'MEDIUM', + title: `${emp.name}的合同即将到期(${daysToExpire}天)`, + description: `合同到期日 ${latestContract.endDate.toISOString().slice(0, 10)},需提前准备续签或终止。`, + actionUrl: `/contracts?employee=${encodeURIComponent(emp.name)}`, + }) + } + } + + if (latestContract.probationMonths > 0) { + const contractMonths = latestContract.endDate + ? Math.ceil(daysBetween(latestContract.endDate, latestContract.startDate) / 30.44) + : 36 + let maxProbation = 0 + if (contractMonths >= 36) maxProbation = 6 + else if (contractMonths >= 12) maxProbation = 2 + else if (contractMonths >= 3) maxProbation = 1 + + if (latestContract.probationMonths > maxProbation) { + risks.push({ + employeeId: emp.id, + type: 'CONTRACT', + level: 'MEDIUM', + title: `${emp.name}试用期${latestContract.probationMonths}个月可能不合法`, + description: `${contractMonths}个月合同试用期最多${maxProbation}个月,当前${latestContract.probationMonths}个月超出法定上限。`, + actionUrl: `/contracts?employee=${encodeURIComponent(emp.name)}`, + }) + } + } + } + + return risks +} + +export async function detectTerminationRisks(orgId: string) { + const employees = await prisma.employee.findMany({ + where: { orgId, status: 'ACTIVE' }, + }) + + const risks: { employeeId: string; type: RiskType; level: RiskLevel; title: string; description: string; actionUrl: string }[] = [] + + for (const emp of employees) { + if (emp.isPregnant) { + risks.push({ + employeeId: emp.id, + type: 'TERMINATION', + level: 'HIGH', + title: `${emp.name}处于孕期/哺乳期,解聘受限`, + description: '三期女职工不得依非过错理由解除劳动合同,否则面临违法解除赔偿金风险。', + actionUrl: `/termination?employee=${encodeURIComponent(emp.name)}`, + }) + } + if (emp.isInMedicalPeriod) { + risks.push({ + employeeId: emp.id, + type: 'TERMINATION', + level: 'MEDIUM', + title: `${emp.name}处于医疗期,解聘需谨慎`, + description: '医疗期内不得解除劳动合同(非过错理由),需等待医疗期结束。', + actionUrl: `/termination?employee=${encodeURIComponent(emp.name)}`, + }) + } + if (emp.isWorkInjured) { + risks.push({ + employeeId: emp.id, + type: 'TERMINATION', + level: 'HIGH', + title: `${emp.name}工伤期间,解聘受限`, + description: '工伤职工在停工留薪期内不得解除劳动合同。', + actionUrl: `/termination?employee=${encodeURIComponent(emp.name)}`, + }) + } + } + + return risks +} + +export async function detectMonthlyTasks(orgId: string) { + const setting = await prisma.notificationSetting.findUnique({ where: { orgId } }) + if (!setting) return [] + + const now = new Date() + const currentMonth = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}` + const today = now.getDate() + + const tasks = [ + { day: setting.payrollDay, title: `${currentMonth}月 发放工资`, desc: `每月${setting.payrollDay}日前完成工资发放`, url: '/money' }, + { day: setting.socialInsDay, title: `${currentMonth}月 缴纳社保`, desc: `每月${setting.socialInsDay}日前完成社保缴纳`, url: '/money' }, + { day: setting.housingFundDay, title: `${currentMonth}月 缴纳公积金`, desc: `每月${setting.housingFundDay}日前完成公积金缴纳`, url: '/money' }, + { day: setting.taxDay, title: `${currentMonth}月 申报个税`, desc: `每月${setting.taxDay}日前完成个税申报`, url: '/money' }, + ] + + const risks: { employeeId: null; type: RiskType; level: RiskLevel; title: string; description: string; actionUrl: string }[] = [] + + for (const task of tasks) { + // 当月已过截止日或正好到截止日时生成提醒 + if (today >= task.day) { + risks.push({ + employeeId: null, + type: 'MONTHLY', + level: today > task.day + 3 ? 'HIGH' : 'MEDIUM', + title: task.title, + description: task.desc, + actionUrl: task.url, + }) + } + } + + return risks +} + +export async function runRiskDetection(orgId: string) { + const existingRisks = await prisma.riskItem.findMany({ + where: { orgId, status: 'PENDING' }, + }) + const existingKeys = new Set(existingRisks.map((r: typeof existingRisks[number]) => `${r.employeeId}:${r.title}`)) + + // 月度任务去重:检查所有状态(含 RESOLVED/IGNORED),避免已完成的月度任务被重新创建 + const currentMonth = `${new Date().getFullYear()}-${String(new Date().getMonth() + 1).padStart(2, '0')}` + const monthlyExisting = await prisma.riskItem.findMany({ + where: { orgId, type: 'MONTHLY', title: { startsWith: `${currentMonth}月` } }, + select: { employeeId: true, title: true }, + }) + const monthlyKeys = new Set(monthlyExisting.map((r: typeof monthlyExisting[number]) => `${r.employeeId}:${r.title}`)) + + const contractRisks = await detectContractRisks(orgId) + const terminationRisks = await detectTerminationRisks(orgId) + const monthlyTasks = await detectMonthlyTasks(orgId) + + // 月度任务用 monthlyKeys 去重,其他任务用 existingKeys 去重 + const nonMonthlyRisks = [...contractRisks, ...terminationRisks] + const toCreate = [ + ...nonMonthlyRisks.filter((r) => !existingKeys.has(`${r.employeeId}:${r.title}`)), + ...monthlyTasks.filter((r) => !monthlyKeys.has(`${r.employeeId}:${r.title}`)), + ] + + if (toCreate.length > 0) { + await prisma.riskItem.createMany({ + data: toCreate.map((r) => ({ + orgId, + employeeId: r.employeeId, + type: r.type, + level: r.level, + title: r.title, + description: r.description, + actionUrl: r.actionUrl, + })), + }) + } + + return toCreate.length +} + +export async function getDashboardData(orgId: string) { + await runRiskDetection(orgId) + + const now = new Date() + const currentMonth = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}` + const monthStart = new Date(now.getFullYear(), now.getMonth(), 1) + const monthEnd = new Date(now.getFullYear(), now.getMonth() + 1, 0, 23, 59, 59) + + const [ + employeeCount, highRisks, pendingRisks, riskItems, resolvedItems, + overtimeRecords, payslips, socialConfig, + monthContracts, monthTerminations, monthDisciplinary, monthAttendance, + monthSeverancePay, + ] = await Promise.all([ + prisma.employee.count({ where: { orgId, status: 'ACTIVE' } }), + prisma.riskItem.count({ where: { orgId, status: 'PENDING', level: 'HIGH' } }), + prisma.riskItem.count({ where: { orgId, status: 'PENDING' } }), + prisma.riskItem.findMany({ + where: { orgId, status: 'PENDING' }, + include: { employee: true }, + orderBy: [{ level: 'asc' }, { createdAt: 'desc' }], + take: 10, + }), + prisma.riskItem.findMany({ + where: { orgId, status: 'RESOLVED' }, + include: { employee: true }, + orderBy: { resolvedAt: 'desc' }, + take: 10, + }), + prisma.overtimeRecord.findMany({ + where: { orgId, month: currentMonth }, + select: { totalPay: true, weekdayHours: true, weekendHours: true, holidayHours: true }, + }), + prisma.payslip.findMany({ + where: { orgId, month: currentMonth }, + select: { baseSalary: true, overtimePay: true, allowance: true, deduction: true, totalPay: true, confirmedAt: true }, + }), + prisma.socialInsuranceConfig.findUnique({ where: { orgId } }), + prisma.laborContract.count({ + where: { orgId, createdAt: { gte: monthStart, lte: monthEnd } }, + }), + prisma.terminationRecord.count({ + where: { orgId, createdAt: { gte: monthStart, lte: monthEnd } }, + }), + prisma.disciplinaryRecord.count({ + where: { orgId, violationDate: { gte: monthStart, lte: monthEnd } }, + }), + prisma.attendanceRecord.count({ + where: { orgId, date: { gte: monthStart, lte: monthEnd } }, + }), + prisma.terminationRecord.aggregate({ + where: { orgId, createdAt: { gte: monthStart, lte: monthEnd } }, + _sum: { compensation: true }, + }), + ]) + + const monthlyOvertimePay = overtimeRecords.reduce((sum: number, r: typeof overtimeRecords[number]) => sum + r.totalPay, 0) + + // 本月薪税汇总 + const totalBaseSalary = payslips.reduce((s: number, p: typeof payslips[number]) => s + p.baseSalary, 0) + const totalOvertimePay = payslips.reduce((s: number, p: typeof payslips[number]) => s + p.overtimePay, 0) + const totalAllowance = payslips.reduce((s: number, p: typeof payslips[number]) => s + p.allowance, 0) + const totalDeduction = payslips.reduce((s: number, p: typeof payslips[number]) => s + p.deduction, 0) + const totalPay = payslips.reduce((s: number, p: typeof payslips[number]) => s + p.totalPay, 0) + const confirmedPayslips = payslips.filter((p: typeof payslips[number]) => p.confirmedAt).length + + // 社保公积金估算(基于在职员工数 × 社保配置) + let socialOrgTotal = 0 + let socialEmpTotal = 0 + let housingOrgTotal = 0 + let housingEmpTotal = 0 + if (socialConfig && employeeCount > 0) { + // 用平均工资作为估算基数 + const avgBase = employeeCount > 0 ? Math.max(socialConfig.baseMin, Math.min(socialConfig.baseMax, totalBaseSalary / Math.max(employeeCount, 1))) : socialConfig.baseMin + socialOrgTotal = avgBase * (socialConfig.pensionOrg + socialConfig.medicalOrg + socialConfig.unemploymentOrg + socialConfig.injuryOrg + socialConfig.maternityOrg) / 100 * employeeCount + socialEmpTotal = avgBase * (socialConfig.pensionEmp + socialConfig.medicalEmp + socialConfig.unemploymentEmp) / 100 * employeeCount + housingOrgTotal = avgBase * socialConfig.housingOrg / 100 * employeeCount + housingEmpTotal = avgBase * socialConfig.housingEmp / 100 * employeeCount + } + + // 个税估算(简化:应纳税所得额 = 税前工资 - 5000起征点 - 社保个人部分 - 公积金个人部分) + const taxableIncome = Math.max(0, totalPay - 5000 * payslips.length - socialEmpTotal - housingEmpTotal) + // 累计预扣法简化:月度个税估算 + let estimatedTax = 0 + if (taxableIncome > 0) { + if (taxableIncome <= 3000) estimatedTax = taxableIncome * 0.03 + else if (taxableIncome <= 12000) estimatedTax = 3000 * 0.03 + (taxableIncome - 3000) * 0.1 + else if (taxableIncome <= 25000) estimatedTax = 3000 * 0.03 + 9000 * 0.1 + (taxableIncome - 12000) * 0.2 + else if (taxableIncome <= 35000) estimatedTax = 3000 * 0.03 + 9000 * 0.1 + 13000 * 0.2 + (taxableIncome - 25000) * 0.25 + else if (taxableIncome <= 55000) estimatedTax = 3000 * 0.03 + 9000 * 0.1 + 13000 * 0.2 + 10000 * 0.25 + (taxableIncome - 35000) * 0.3 + else if (taxableIncome <= 80000) estimatedTax = 3000 * 0.03 + 9000 * 0.1 + 13000 * 0.2 + 10000 * 0.25 + 20000 * 0.3 + (taxableIncome - 55000) * 0.35 + else estimatedTax = 3000 * 0.03 + 9000 * 0.1 + 13000 * 0.2 + 10000 * 0.25 + 20000 * 0.3 + 25000 * 0.35 + (taxableIncome - 80000) * 0.45 + } + + const payrollSummary = { + month: currentMonth, + employeeCount, + payslipCount: payslips.length, + confirmedPayslips, + unconfirmedPayslips: payslips.length - confirmedPayslips, + baseSalary: totalBaseSalary, + overtimePay: totalOvertimePay, + allowance: totalAllowance, + deduction: totalDeduction, + totalPay, + socialOrg: socialOrgTotal, + socialEmp: socialEmpTotal, + housingOrg: housingOrgTotal, + housingEmp: housingEmpTotal, + estimatedTax, + severancePay: monthSeverancePay._sum.compensation || 0, + // 企业总成本 = 工资总额 + 企业社保 + 企业公积金 + 经济补偿金 + orgTotalCost: totalPay + socialOrgTotal + housingOrgTotal + (monthSeverancePay._sum.compensation || 0), + // 员工实发 = 工资总额 - 个人社保 - 个人公积金 - 个税 + empNetPay: totalPay - socialEmpTotal - housingEmpTotal - estimatedTax, + } + + // 本月工作动态 + const monthlyActivities = { + month: currentMonth, + newContracts: monthContracts, + terminations: monthTerminations, + disciplinaryActions: monthDisciplinary, + attendanceRecords: monthAttendance, + overtimeHours: overtimeRecords.reduce((s: number, r: typeof overtimeRecords[number]) => s + r.weekdayHours + r.weekendHours + r.holidayHours, 0), + overtimePay: monthlyOvertimePay, + } + + const riskDistribution = { + contract: riskItems.filter((r: typeof riskItems[number]) => r.type === 'CONTRACT').length, + salary: riskItems.filter((r: typeof riskItems[number]) => r.type === 'SALARY').length, + termination: riskItems.filter((r: typeof riskItems[number]) => r.type === 'TERMINATION').length, + } + + const todos = riskItems.map((r: typeof riskItems[number]) => ({ + id: r.id, + level: r.level.toLowerCase() as 'high' | 'medium' | 'low', + title: r.title, + description: r.description, + actionUrl: r.actionUrl || '/', + })) + + const resolvedTodos = resolvedItems.map((r: typeof resolvedItems[number]) => ({ + id: r.id, + level: r.level.toLowerCase() as 'high' | 'medium' | 'low', + title: r.title, + description: r.description, + actionUrl: r.actionUrl || '/', + resolvedAt: r.resolvedAt?.toISOString() || null, + })) + + const hour = new Date().getHours() + const greeting = hour < 12 + ? `早上好!今天有 ${pendingRisks} 件事需要处理` + : hour < 18 + ? `下午好!今天有 ${pendingRisks} 件事需要处理` + : `晚上好!今天有 ${pendingRisks} 件事需要处理` + + return { + greeting, + stats: { + employeeCount, + highRiskCount: highRisks, + todoCount: pendingRisks, + monthlyOvertimePay, + }, + todos, + resolvedTodos, + riskDistribution, + aiPrediction: null, + payrollSummary, + monthlyActivities, + } +} diff --git a/backend/src/services/termination.service.ts b/backend/src/services/termination.service.ts new file mode 100644 index 0000000..fccb1a0 --- /dev/null +++ b/backend/src/services/termination.service.ts @@ -0,0 +1,163 @@ +import prisma from '../lib/prisma' +import { decrypt } from '../lib/crypto' +import { RiskAssessment } from '@prisma/client' + +function daysBetween(a: Date, b: Date): number { + return Math.floor((a.getTime() - b.getTime()) / (1000 * 60 * 60 * 24)) +} + +export function getChecklistForReason(reason: string): { key: string; label: string }[] { + switch (reason) { + case 'NEGOTIATED': + return [ + { key: 'compensation_paid', label: '是否已支付经济补偿金' }, + { key: 'agreement_signed', label: '是否签署协商解除协议' }, + { key: 'final_pay_ready', label: '是否结清最后工资' }, + ] + case 'FAULT': + return [ + { key: 'has_rules', label: '是否有规章制度依据' }, + { key: 'has_evidence', label: '是否有违纪证据' }, + { key: 'notify_union', label: '是否事先通知工会' }, + { key: 'written_notice', label: '是否出具书面解除通知' }, + ] + case 'NONFAULT': + return [ + { key: 'medical_period_end', label: '医疗期是否已届满' }, + { key: 'training_given', label: '是否经过培训或调岗' }, + { key: 'compensation_paid', label: '是否支付经济补偿金' }, + { key: 'advance_notice', label: '是否提前30天通知或支付代通知金' }, + ] + case 'LAYOFF': + return [ + { key: 'advance_notice_30', label: '是否提前30天向工会或全体职工说明' }, + { key: 'listen_opinions', label: '是否听取工会或职工意见' }, + { key: 'report_labor_dept', label: '是否向劳动行政部门报告' }, + { key: 'compensation_paid', label: '是否支付经济补偿金' }, + ] + case 'EXPIRED': + return [ + { key: 'compensation_paid', label: '是否支付经济补偿金(如需)' }, + { key: 'written_notice', label: '是否提前通知员工不续签' }, + ] + default: + return [] + } +} + +export function assessRisk(employee: any, reason: string): { level: RiskAssessment; warnings: string[] } { + const warnings: string[] = [] + + if (employee.isPregnant) { + warnings.push('该员工在孕期/哺乳期,法律禁止以非过错理由解除') + } + if (employee.isWorkInjured) { + warnings.push('工伤期间不得解除劳动合同') + } + if (employee.isInMedicalPeriod && reason !== 'FAULT') { + warnings.push('医疗期内不得解除劳动合同(非过错理由)') + } + + let level: RiskAssessment = 'SAFE' + if (warnings.length > 0) { + level = 'DANGER' + } + + return { level, warnings } +} + +export async function createTermination(orgId: string, userId: string, data: any) { + const employee = await prisma.employee.findFirst({ where: { id: data.employeeId, orgId } }) + if (!employee) { + throw { code: 'NOT_FOUND', message: '员工不存在' } + } + + const { level } = assessRisk(employee, data.reason) + + const record = await prisma.terminationRecord.create({ + data: { + orgId, + employeeId: data.employeeId, + reason: data.reason, + terminationDate: new Date(data.terminationDate), + compensation: data.compensation || 0, + riskLevel: level, + checklist: data.checklist || {}, + remark: data.remark, + createdBy: userId, + }, + }) + + await prisma.employee.update({ + where: { id: data.employeeId }, + data: { status: 'RESIGNED' }, + }) + + await prisma.riskItem.updateMany({ + where: { employeeId: data.employeeId, status: 'PENDING' }, + data: { status: 'RESOLVED', resolvedAt: new Date() }, + }) + + return { id: record.id } +} + +export async function getTerminations(orgId: string, page: number, pageSize: number) { + const skip = (page - 1) * pageSize + + const [total, records] = await Promise.all([ + prisma.terminationRecord.count({ where: { orgId } }), + prisma.terminationRecord.findMany({ + where: { orgId }, + include: { employee: true }, + orderBy: { createdAt: 'desc' }, + skip, + take: pageSize, + }), + ]) + + return { + items: records.map((r) => ({ + id: r.id, + employeeName: r.employee.name, + department: r.employee.department, + reason: r.reason, + terminationDate: r.terminationDate.toISOString().slice(0, 10), + compensation: r.compensation, + riskLevel: r.riskLevel, + createdAt: r.createdAt.toISOString().slice(0, 10), + })), + total, + page, + pageSize, + totalPages: Math.ceil(total / pageSize), + } +} + +export function calculateCompensation(hireDate: Date, leaveDate: Date, monthlyWage: number, socialAvgWage: number = 0): { + years: number + remainingMonths: number + compMonths: number + totalPay: number + capped: boolean +} { + const totalMonths = (leaveDate.getFullYear() - hireDate.getFullYear()) * 12 + (leaveDate.getMonth() - hireDate.getMonth()) + const years = Math.floor(totalMonths / 12) + const remainingMonths = totalMonths % 12 + + let compMonths: number + if (remainingMonths >= 6) compMonths = years + 1 + else if (remainingMonths > 0) compMonths = years + 0.5 + else compMonths = years + + if (compMonths <= 0) compMonths = 0.5 + + let wage = monthlyWage + let capped = false + if (socialAvgWage > 0 && monthlyWage > socialAvgWage * 3) { + wage = socialAvgWage * 3 + compMonths = Math.min(compMonths, 12) + capped = true + } + + return { years, remainingMonths, compMonths, totalPay: wage * compMonths, capped } +} diff --git a/backend/tsconfig.json b/backend/tsconfig.json new file mode 100644 index 0000000..aec1ca3 --- /dev/null +++ b/backend/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "strict": true, + "skipLibCheck": true, + "outDir": "dist", + "resolveJsonModule": true, + "declaration": true, + "sourceMap": true, + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["src/**/*", "prisma/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..d0643ce --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,13 @@ + + + + + + + 用工合规助手 + + +
+ + + diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..3428692 --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,3167 @@ +{ + "name": "hr-compliance-frontend", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "hr-compliance-frontend", + "version": "1.0.0", + "dependencies": { + "@hookform/resolvers": "^3.9.0", + "@tanstack/react-query": "^5.51.0", + "axios": "^1.7.0", + "clsx": "^2.1.0", + "lucide-react": "^0.428.0", + "qrcode.react": "^4.0.1", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-hook-form": "^7.52.0", + "react-router-dom": "^6.26.0", + "zod": "^3.23.0", + "zustand": "^4.5.0" + }, + "devDependencies": { + "@types/react": "^18.3.0", + "@types/react-dom": "^18.3.0", + "@vitejs/plugin-react": "^4.3.0", + "autoprefixer": "^10.4.0", + "postcss": "^8.4.0", + "tailwindcss": "^3.4.0", + "typescript": "^5.5.0", + "vite": "^5.4.0" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmmirror.com/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", + "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", + "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@hookform/resolvers": { + "version": "3.10.0", + "resolved": "https://registry.npmmirror.com/@hookform/resolvers/-/resolvers-3.10.0.tgz", + "integrity": "sha512-79Dv+3mDF7i+2ajj7SkypSKHhl1cbln1OGavqrsF7p6mbUv11xpqpacPsGDCTRvCSjEEIez2ef1NveSVL3b0Ag==", + "license": "MIT", + "peerDependencies": { + "react-hook-form": "^7.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmmirror.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmmirror.com/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmmirror.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmmirror.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmmirror.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmmirror.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@remix-run/router": { + "version": "1.23.3", + "resolved": "https://registry.npmmirror.com/@remix-run/router/-/router-1.23.3.tgz", + "integrity": "sha512-4An71tdz9X8+3sI4Qqqd2LWd9vS39J7sqd9EU4Scw7TJE/qB10Flv/UuqbPVgfQV9XoK8Np6jNquZitnZq5i+Q==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmmirror.com/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", + "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", + "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", + "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", + "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", + "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", + "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", + "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", + "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", + "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", + "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", + "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", + "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", + "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", + "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", + "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", + "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", + "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", + "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", + "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", + "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", + "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", + "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", + "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", + "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", + "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@tanstack/query-core": { + "version": "5.101.4", + "resolved": "https://registry.npmmirror.com/@tanstack/query-core/-/query-core-5.101.4.tgz", + "integrity": "sha512-gNwcvOJcRbLWPOLG/2OBm+zM+Yv+MKsXKEOWC57USuZDEsI71hEErQsiEGx5wX9rzWWkfwM0fVSPoiIFSsxfiw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.101.4", + "resolved": "https://registry.npmmirror.com/@tanstack/react-query/-/react-query-5.101.4.tgz", + "integrity": "sha512-yRg2pfOCxIs4ZJW3XYYHU/WgtD04FHSnfHlpRT7h7pR77hwkdRG4wxbKe4aq6P0RvXUTBSQpQeadS1SUYUe+KA==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.101.4" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmmirror.com/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmmirror.com/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmmirror.com/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmmirror.com/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmmirror.com/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.31", + "resolved": "https://registry.npmmirror.com/@types/react/-/react-18.3.31.tgz", + "integrity": "sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmmirror.com/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmmirror.com/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmmirror.com/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmmirror.com/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmmirror.com/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true, + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/autoprefixer": { + "version": "10.5.4", + "resolved": "https://registry.npmmirror.com/autoprefixer/-/autoprefixer-10.5.4.tgz", + "integrity": "sha512-MaU0U/za7N3r6brxD4YB/l4NSrFzLPlANv6wEuQVaIPlD3L4W9rFcQPbL/EilY9BHhHvhfcz3gInDLrEtWT4EA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.6", + "caniuse-lite": "^1.0.30001806", + "fraction.js": "^5.3.4", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/axios": { + "version": "1.18.1", + "resolved": "https://registry.npmmirror.com/axios/-/axios-1.18.1.tgz", + "integrity": "sha512-3nTvFlvpn9Zu/RkHUqtc7/+al4UpRW5az71ap5zccp6e8RAYEzhMTecX8Dz1wWDYrPpUoB1HAQEGEAEvUr7S9g==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.16.0", + "form-data": "^4.0.5", + "https-proxy-agent": "^5.0.1", + "proxy-from-env": "^2.1.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.11.1", + "resolved": "https://registry.npmmirror.com/baseline-browser-mapping/-/baseline-browser-mapping-2.11.1.tgz", + "integrity": "sha512-HYXq73DDpCtNzOmrFsm9eSwCvWCql0RzqjpDzXN9EadiLJ4DNat0nsZ/Bzmy+Ud12mb4/zKDY0cQ805ZzN+i0A==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.7", + "resolved": "https://registry.npmmirror.com/browserslist/-/browserslist-4.28.7.tgz", + "integrity": "sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.44", + "caniuse-lite": "^1.0.30001806", + "electron-to-chromium": "^1.5.393", + "node-releases": "^2.0.51", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001806", + "resolved": "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001806.tgz", + "integrity": "sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmmirror.com/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmmirror.com/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmmirror.com/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmmirror.com/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true, + "license": "MIT" + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.395", + "resolved": "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.395.tgz", + "integrity": "sha512-7zt9Aw+SrmxLWLN0zhaTWZQiCdryLVrYTq5R7iZakLvi2UQPYMMsROYV/2qVCzMeCiSXHwKOU+sZ4zOVVlrtKA==", + "dev": true, + "license": "ISC" + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmmirror.com/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmmirror.com/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmmirror.com/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmmirror.com/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmmirror.com/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmmirror.com/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.2", + "resolved": "https://registry.npmmirror.com/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmmirror.com/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmmirror.com/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmmirror.com/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmmirror.com/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmmirror.com/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmmirror.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lucide-react": { + "version": "0.428.0", + "resolved": "https://registry.npmmirror.com/lucide-react/-/lucide-react-0.428.0.tgz", + "integrity": "sha512-rGrzslfEcgqwh+TLBC5qJ8wvVIXhLvAIXVFKNHndYyb1utSxxn9rXOC+1CNJLi6yNOooyPqIs6+3YCp6uSiEvg==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmmirror.com/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmmirror.com/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmmirror.com/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.16", + "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.51", + "resolved": "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.51.tgz", + "integrity": "sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmmirror.com/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmmirror.com/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmmirror.com/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/postcss": { + "version": "8.5.22", + "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.22.tgz", + "integrity": "sha512-KBDEIpLrvpv16pp3K0Fw+UCoZfopFjjgeB+0tA/aaThfEE74kKDLrgg603YvOWJyg3+WYtyq3xYsQWsIyZlPqQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.16", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmmirror.com/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/postcss-js/-/postcss-js-4.1.0.tgz", + "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmmirror.com/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.4", + "resolved": "https://registry.npmmirror.com/postcss-selector-parser/-/postcss-selector-parser-6.1.4.tgz", + "integrity": "sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmmirror.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/qrcode.react": { + "version": "4.2.0", + "resolved": "https://registry.npmmirror.com/qrcode.react/-/qrcode.react-4.2.0.tgz", + "integrity": "sha512-QpgqWi8rD9DsS9EP3z7BT+5lY5SFhsqGjpgW5DY/i3mK4M9DTBNz3ErMi8BWYEfI3L0d8GIbGmcdFAS1uIRGjA==", + "license": "ISC", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmmirror.com/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmmirror.com/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmmirror.com/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-hook-form": { + "version": "7.82.0", + "resolved": "https://registry.npmmirror.com/react-hook-form/-/react-hook-form-7.82.0.tgz", + "integrity": "sha512-Zw/uFZ2dO+02GHlBn7JFGn8kZJ7LdM33B/0BXOovzFay+CMhf94JMw5BVu+F1tVkUKjNvBuaE3fz5BJhga10Tg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18 || ^19" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmmirror.com/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "6.30.4", + "resolved": "https://registry.npmmirror.com/react-router/-/react-router-6.30.4.tgz", + "integrity": "sha512-SVUsDe+DybHM/WmYKIVYhZh1o5Dcuf16yM6WjG02Q9XVFMZIJyHYhwrr6bFBXZkVP6z69kNkMyBCujt8FaFLJA==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.30.4", + "resolved": "https://registry.npmmirror.com/react-router-dom/-/react-router-dom-6.30.4.tgz", + "integrity": "sha512-q4HvNl+mmDdkS0g+MqiBZNteQJCuimWoOyHMy4T/RQLAn9Z29+E91QXRaxOujeMl2HTzRSS0KFPd7lxX3PjV0Q==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.3", + "react-router": "6.30.4" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmmirror.com/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmmirror.com/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/rollup/-/rollup-4.62.2.tgz", + "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.2", + "@rollup/rollup-android-arm64": "4.62.2", + "@rollup/rollup-darwin-arm64": "4.62.2", + "@rollup/rollup-darwin-x64": "4.62.2", + "@rollup/rollup-freebsd-arm64": "4.62.2", + "@rollup/rollup-freebsd-x64": "4.62.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", + "@rollup/rollup-linux-arm-musleabihf": "4.62.2", + "@rollup/rollup-linux-arm64-gnu": "4.62.2", + "@rollup/rollup-linux-arm64-musl": "4.62.2", + "@rollup/rollup-linux-loong64-gnu": "4.62.2", + "@rollup/rollup-linux-loong64-musl": "4.62.2", + "@rollup/rollup-linux-ppc64-gnu": "4.62.2", + "@rollup/rollup-linux-ppc64-musl": "4.62.2", + "@rollup/rollup-linux-riscv64-gnu": "4.62.2", + "@rollup/rollup-linux-riscv64-musl": "4.62.2", + "@rollup/rollup-linux-s390x-gnu": "4.62.2", + "@rollup/rollup-linux-x64-gnu": "4.62.2", + "@rollup/rollup-linux-x64-musl": "4.62.2", + "@rollup/rollup-openbsd-x64": "4.62.2", + "@rollup/rollup-openharmony-arm64": "4.62.2", + "@rollup/rollup-win32-arm64-msvc": "4.62.2", + "@rollup/rollup-win32-ia32-msvc": "4.62.2", + "@rollup/rollup-win32-x64-gnu": "4.62.2", + "@rollup/rollup-win32-x64-msvc": "4.62.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmmirror.com/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmmirror.com/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.19", + "resolved": "https://registry.npmmirror.com/tailwindcss/-/tailwindcss-3.4.19.tgz", + "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.7", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmmirror.com/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmmirror.com/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmmirror.com/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmmirror.com/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmmirror.com/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmmirror.com/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmmirror.com/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmmirror.com/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zustand": { + "version": "4.5.7", + "resolved": "https://registry.npmmirror.com/zustand/-/zustand-4.5.7.tgz", + "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.2.2" + }, + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "@types/react": ">=16.8", + "immer": ">=9.0.6", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + } + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..7b97c27 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,35 @@ +{ + "name": "hr-compliance-frontend", + "private": true, + "version": "1.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "preview": "vite preview" + }, + "dependencies": { + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-router-dom": "^6.26.0", + "axios": "^1.7.0", + "zustand": "^4.5.0", + "@tanstack/react-query": "^5.51.0", + "react-hook-form": "^7.52.0", + "@hookform/resolvers": "^3.9.0", + "zod": "^3.23.0", + "lucide-react": "^0.428.0", + "qrcode.react": "^4.0.1", + "clsx": "^2.1.0" + }, + "devDependencies": { + "@types/react": "^18.3.0", + "@types/react-dom": "^18.3.0", + "@vitejs/plugin-react": "^4.3.0", + "typescript": "^5.5.0", + "vite": "^5.4.0", + "tailwindcss": "^3.4.0", + "postcss": "^8.4.0", + "autoprefixer": "^10.4.0" + } +} diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js new file mode 100644 index 0000000..2e7af2b --- /dev/null +++ b/frontend/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx new file mode 100644 index 0000000..3c15059 --- /dev/null +++ b/frontend/src/App.tsx @@ -0,0 +1,83 @@ +import { Routes, Route, Navigate } from 'react-router-dom' +import { useAuthStore } from './store/authStore' +import TopNav from './components/layout/TopNav' +import MobileTabBar from './components/layout/MobileTabBar' +import PageContainer from './components/layout/PageContainer' +import Login from './pages/auth/Login' +import Register from './pages/auth/Register' +import ForgotPassword from './pages/auth/ForgotPassword' +import Dashboard from './pages/Dashboard' +import Contracts from './pages/Contracts' +import Money from './pages/Money' +import Roster from './pages/Roster' +import Termination from './pages/Termination' +import AIAssistant from './pages/AIAssistant' +import Settings from './pages/Settings' +import PortalLogin from './pages/portal/PortalLogin' +import Payslip from './pages/portal/Payslip' +import MyContract from './pages/portal/MyContract' +import Onboarding from './pages/portal/Onboarding' +import ContractConfirm from './pages/portal/ContractConfirm' +import OnboardingGuide from './components/OnboardingGuide' + +function ProtectedRoute({ children }: { children: React.ReactNode }) { + const isAuthenticated = useAuthStore((s) => s.isAuthenticated) + if (!isAuthenticated) return + return <>{children} +} + +function PublicRoute({ children }: { children: React.ReactNode }) { + const isAuthenticated = useAuthStore((s) => s.isAuthenticated) + if (isAuthenticated) return + return <>{children} +} + +function AdminLayout({ children }: { children: React.ReactNode }) { + return ( +
+ +
+ {children} +
+ + +
+ ) +} + +function PortalLayout({ children }: { children: React.ReactNode }) { + return ( +
+
{children}
+
+ ) +} + +export default function App() { + return ( + + {/* 管理端认证页面 */} + } /> + } /> + } /> + + {/* 管理端业务页面 */} + } /> + } /> + } /> + } /> + } /> + } /> + + {/* 员工端 */} + } /> + } /> + } /> + } /> + } /> + + {/* 兜底 */} + } /> + + ) +} diff --git a/frontend/src/components/OnboardingGuide.tsx b/frontend/src/components/OnboardingGuide.tsx new file mode 100644 index 0000000..f383b56 --- /dev/null +++ b/frontend/src/components/OnboardingGuide.tsx @@ -0,0 +1,84 @@ +import { useState, useEffect } from 'react' +import { X, ArrowRight } from 'lucide-react' + +const STORAGE_KEY = 'hr-onboarding-completed' + +const steps = [ + { + icon: '🏠', + title: '这里看风险', + description: '首页展示企业用工风险总览,红色代表高风险项,点击「去处理」直接跳转操作。', + }, + { + icon: '�', + title: '这里管花名册', + description: '花名册页面管理员工档案、劳动合同、附件,以及违纪、考勤、培训、绩效记录,可生成仲裁证据链。', + }, + { + icon: '💰', + title: '这里算薪税', + description: '薪税页面提供加班费、双倍工资、社保公积金计算器和工资条管理,输入参数实时计算。', + }, +] + +export default function OnboardingGuide() { + const [visible, setVisible] = useState(false) + const [step, setStep] = useState(0) + + useEffect(() => { + const completed = localStorage.getItem(STORAGE_KEY) + if (!completed) { + setVisible(true) + } + }, []) + + const close = () => { + localStorage.setItem(STORAGE_KEY, '1') + setVisible(false) + } + + if (!visible) return null + + const current = steps[step] + const isLast = step === steps.length - 1 + + return ( +
+
+
+ +
+
+
{current.icon}
+

{current.title}

+

{current.description}

+ + {/* 进度指示器 */} +
+ {steps.map((_, i) => ( +
+ ))} +
+ +
+ {step > 0 ? ( + + ) : } + +
+
+
+
+ ) +} diff --git a/frontend/src/components/layout/MobileTabBar.tsx b/frontend/src/components/layout/MobileTabBar.tsx new file mode 100644 index 0000000..58a9fa7 --- /dev/null +++ b/frontend/src/components/layout/MobileTabBar.tsx @@ -0,0 +1,36 @@ +import { Link, useLocation } from 'react-router-dom' +import { Home, FileText, Users, Calculator, UserX, Bot } from 'lucide-react' +import clsx from 'clsx' + +const tabs = [ + { path: '/', label: '总览', icon: Home }, + { path: '/roster', label: '花名册', icon: Users }, + { path: '/money', label: '薪税', icon: Calculator }, + { path: '/termination', label: '解聘', icon: UserX }, + { path: '/ai-assistant', label: 'AI', icon: Bot }, +] + +export default function MobileTabBar() { + const location = useLocation() + return ( + + ) +} diff --git a/frontend/src/components/layout/PageContainer.tsx b/frontend/src/components/layout/PageContainer.tsx new file mode 100644 index 0000000..de89486 --- /dev/null +++ b/frontend/src/components/layout/PageContainer.tsx @@ -0,0 +1,10 @@ +import { ReactNode } from 'react' +import clsx from 'clsx' + +export default function PageContainer({ children, className }: { children: ReactNode; className?: string }) { + return ( +
+ {children} +
+ ) +} diff --git a/frontend/src/components/layout/TopNav.tsx b/frontend/src/components/layout/TopNav.tsx new file mode 100644 index 0000000..1b36c36 --- /dev/null +++ b/frontend/src/components/layout/TopNav.tsx @@ -0,0 +1,86 @@ +import { Link, useLocation, useNavigate } from 'react-router-dom' +import { Building2, AlertCircle, ChevronDown } from 'lucide-react' +import { useState } from 'react' +import { useAuthStore } from '../../store/authStore' +import clsx from 'clsx' + +const tabs = [ + { path: '/', label: '总览' }, + { path: '/roster', label: '花名册' }, + { path: '/money', label: '薪税' }, + { path: '/termination', label: '解聘' }, + { path: '/ai-assistant', label: 'AI顾问' }, +] + +export default function TopNav() { + const location = useLocation() + const navigate = useNavigate() + const { user, logout } = useAuthStore() + const [menuOpen, setMenuOpen] = useState(false) + + return ( +
+
+ + + 用工合规助手 + + + + +
+ + {menuOpen && ( + <> +
setMenuOpen(false)} /> +
+ setMenuOpen(false)} + > + 设置 + + +
+ + )} +
+
+
+ ) +} diff --git a/frontend/src/components/ui/Button.tsx b/frontend/src/components/ui/Button.tsx new file mode 100644 index 0000000..d2e1fe7 --- /dev/null +++ b/frontend/src/components/ui/Button.tsx @@ -0,0 +1,29 @@ +import { ButtonHTMLAttributes } from 'react' +import clsx from 'clsx' + +interface ButtonProps extends ButtonHTMLAttributes { + variant?: 'primary' | 'secondary' | 'danger' + size?: 'sm' | 'md' | 'lg' +} + +export default function Button({ variant = 'primary', size = 'md', className, children, ...props }: ButtonProps) { + return ( + + ) +} diff --git a/frontend/src/components/ui/Card.tsx b/frontend/src/components/ui/Card.tsx new file mode 100644 index 0000000..76210c3 --- /dev/null +++ b/frontend/src/components/ui/Card.tsx @@ -0,0 +1,10 @@ +import { HTMLAttributes } from 'react' +import clsx from 'clsx' + +export default function Card({ className, children, ...props }: HTMLAttributes) { + return ( +
+ {children} +
+ ) +} diff --git a/frontend/src/components/ui/EmptyState.tsx b/frontend/src/components/ui/EmptyState.tsx new file mode 100644 index 0000000..a588db4 --- /dev/null +++ b/frontend/src/components/ui/EmptyState.tsx @@ -0,0 +1,26 @@ +import { ReactNode } from 'react' +import { Inbox } from 'lucide-react' +import Button from './Button' + +interface EmptyStateProps { + icon?: ReactNode + title: string + description?: string + actionLabel?: string + onAction?: () => void +} + +export default function EmptyState({ icon, title, description, actionLabel, onAction }: EmptyStateProps) { + return ( +
+
+ {icon || } +
+

{title}

+ {description &&

{description}

} + {actionLabel && onAction && ( + + )} +
+ ) +} diff --git a/frontend/src/components/ui/Input.tsx b/frontend/src/components/ui/Input.tsx new file mode 100644 index 0000000..575a452 --- /dev/null +++ b/frontend/src/components/ui/Input.tsx @@ -0,0 +1,38 @@ +import { InputHTMLAttributes, SelectHTMLAttributes, forwardRef } from 'react' +import clsx from 'clsx' + +export const Input = forwardRef>( + function Input({ className, ...props }, ref) { + return ( + + ) + } +) + +export const Select = forwardRef>( + function Select({ className, children, ...props }, ref) { + return ( + + ) + } +) + +export function Label({ children, className }: { children: React.ReactNode; className?: string }) { + return +} diff --git a/frontend/src/components/ui/Modal.tsx b/frontend/src/components/ui/Modal.tsx new file mode 100644 index 0000000..73bce5f --- /dev/null +++ b/frontend/src/components/ui/Modal.tsx @@ -0,0 +1,43 @@ +import { ReactNode, useEffect } from 'react' +import { X } from 'lucide-react' +import clsx from 'clsx' + +interface ModalProps { + open: boolean + onClose: () => void + title?: string + children: ReactNode + className?: string +} + +export default function Modal({ open, onClose, title, children, className }: ModalProps) { + useEffect(() => { + if (open) { + document.body.style.overflow = 'hidden' + } else { + document.body.style.overflow = '' + } + return () => { + document.body.style.overflow = '' + } + }, [open]) + + if (!open) return null + + return ( +
+
+
+ {title && ( +
+

{title}

+ +
+ )} +
{children}
+
+
+ ) +} diff --git a/frontend/src/components/ui/Signal.tsx b/frontend/src/components/ui/Signal.tsx new file mode 100644 index 0000000..4224726 --- /dev/null +++ b/frontend/src/components/ui/Signal.tsx @@ -0,0 +1,27 @@ +import clsx from 'clsx' + +type Level = 'high' | 'medium' | 'low' | 'safe' + +const colors: Record = { + high: 'bg-danger', + medium: 'bg-warning', + low: 'bg-yellow-400', + safe: 'bg-safe', +} + +const labels: Record = { + high: '🔴', + medium: '🟡', + low: '🟡', + safe: '🟢', +} + +export default function Signal({ level, label }: { level: Level; label?: string }) { + return ( + + + {label && {label}} + {!label && {labels[level]}} + + ) +} diff --git a/frontend/src/index.css b/frontend/src/index.css new file mode 100644 index 0000000..3b1edfc --- /dev/null +++ b/frontend/src/index.css @@ -0,0 +1,38 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + body { + @apply bg-surface text-gray-900 antialiased; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; + } + + * { + @apply box-border; + } +} + +@layer components { + .btn { + @apply inline-flex items-center justify-center px-4 py-2 rounded-md font-medium text-sm transition-colors disabled:opacity-50 disabled:cursor-not-allowed; + } + .btn-primary { + @apply btn bg-primary text-white hover:bg-primary-dark; + } + .btn-secondary { + @apply btn bg-gray-100 text-gray-700 hover:bg-gray-200; + } + .btn-danger { + @apply btn bg-danger text-white hover:bg-red-700; + } + .card { + @apply bg-white rounded-lg shadow-sm border border-gray-200 p-4; + } + .input { + @apply w-full px-3 py-2 rounded-md border border-gray-300 focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent text-sm; + } + .label { + @apply block text-sm font-medium text-gray-700 mb-1; + } +} diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts new file mode 100644 index 0000000..22c0c7e --- /dev/null +++ b/frontend/src/lib/api.ts @@ -0,0 +1,47 @@ +import axios from 'axios' +import { useAuthStore } from '../store/authStore' + +const api = axios.create({ + baseURL: '/api/v1', + timeout: 30000, +}) + +api.interceptors.request.use((config) => { + const token = useAuthStore.getState().accessToken + if (token) { + config.headers.Authorization = `Bearer ${token}` + } + return config +}) + +let isRefreshing = false + +api.interceptors.response.use( + (response) => response.data, + async (error) => { + const originalRequest = error.config + if (error.response?.status === 401 && !originalRequest._retry) { + originalRequest._retry = true + if (isRefreshing) return Promise.reject(error) + isRefreshing = true + try { + const refreshToken = useAuthStore.getState().refreshToken + if (!refreshToken) throw new Error('No refresh token') + const res = await axios.post('/api/v1/auth/refresh', { refreshToken }) + const newToken = res.data.data.accessToken + useAuthStore.getState().updateToken(newToken) + originalRequest.headers.Authorization = `Bearer ${newToken}` + return api(originalRequest) + } catch { + useAuthStore.getState().logout() + window.location.href = '/login' + return Promise.reject(error) + } finally { + isRefreshing = false + } + } + return Promise.reject(error) + }, +) + +export default api diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx new file mode 100644 index 0000000..418ec96 --- /dev/null +++ b/frontend/src/main.tsx @@ -0,0 +1,26 @@ +import React from 'react' +import ReactDOM from 'react-dom/client' +import { BrowserRouter } from 'react-router-dom' +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' +import App from './App' +import './index.css' + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + staleTime: 1000 * 60 * 5, + retry: 1, + refetchOnWindowFocus: false, + }, + }, +}) + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + + + + + , +) diff --git a/frontend/src/pages/AIAssistant.tsx b/frontend/src/pages/AIAssistant.tsx new file mode 100644 index 0000000..8eb58c2 --- /dev/null +++ b/frontend/src/pages/AIAssistant.tsx @@ -0,0 +1,277 @@ +import { useState, useRef, useEffect } from 'react' +import { Bot, Send, FileSearch, Scale, Sparkles, Loader2 } from 'lucide-react' +import api from '../lib/api' +import Card from '../components/ui/Card' +import Button from '../components/ui/Button' +import { Input, Label, Select } from '../components/ui/Input' + +type Tab = 'chat' | 'predict' | 'review' | 'case' + +interface Message { + role: 'user' | 'assistant' + content: string +} + +const QUICK_QUESTIONS = [ + '员工入职没签合同怎么办?', + '加班费怎么算?', + '辞退员工需要赔多少?', + '试用期最长可以约定几个月?', +] + +export default function AIAssistant() { + const [tab, setTab] = useState('chat') + + const tabs: { key: Tab; label: string; icon: typeof Bot }[] = [ + { key: 'chat', label: '智能问答', icon: Bot }, + { key: 'predict', label: '风险预测', icon: Sparkles }, + { key: 'review', label: '合同审查', icon: FileSearch }, + { key: 'case', label: '案例匹配', icon: Scale }, + ] + + return ( +
+

AI 合规顾问

+ +
+ {tabs.map((t) => { + const Icon = t.icon + return ( + + ) + })} +
+ + {tab === 'chat' && } + {tab === 'predict' && } + {tab === 'review' && } + {tab === 'case' && } +
+ ) +} + +function ChatTab() { + const [messages, setMessages] = useState([ + { role: 'assistant', content: '你好!我是你的用工合规顾问,有什么劳动法问题可以直接问我。\n\n你可以问我:\n· 员工入职没签合同怎么办?\n· 加班费怎么算?\n· 辞退员工需要赔多少?' }, + ]) + const [input, setInput] = useState('') + const [loading, setLoading] = useState(false) + const scrollRef = useRef(null) + + useEffect(() => { + scrollRef.current?.scrollTo(0, scrollRef.current.scrollHeight) + }, [messages]) + + const send = async (text?: string) => { + const content = text || input.trim() + if (!content || loading) return + + const newMessages = [...messages, { role: 'user' as const, content }] + setMessages([...newMessages, { role: 'assistant', content: '' }]) + setInput('') + setLoading(true) + + try { + const res = await api.post('/ai/chat', { messages: newMessages }) as any + setMessages([...newMessages, { role: 'assistant', content: res.data.reply }]) + } catch (err: any) { + setMessages([...newMessages, { role: 'assistant', content: `抱歉,出错了:${err.response?.data?.error?.message || '请稍后重试'}` }]) + } finally { + setLoading(false) + } + } + + return ( +
+
+ {messages.map((msg, i) => ( +
+
+ {msg.content || (loading && i === messages.length - 1 ? '思考中...' : '')} +
+
+ ))} +
+ + {/* 快捷问题 */} + {messages.length <= 1 && ( +
+ {QUICK_QUESTIONS.map((q) => ( + + ))} +
+ )} + + {/* 输入框 */} +
+ setInput(e.target.value)} + onKeyDown={(e) => e.key === 'Enter' && send()} + placeholder="输入问题..." + disabled={loading} + /> + +
+
+ ) +} + +function PredictTab() { + const [result, setResult] = useState('') + const [loading, setLoading] = useState(false) + + const fetchPrediction = async () => { + setLoading(true) + try { + const res = await api.get('/ai/predict') as any + setResult(res.data.result) + } catch (err: any) { + setResult(`出错了:${err.response?.data?.error?.message || '请稍后重试'}`) + } finally { + setLoading(false) + } + } + + useEffect(() => { + fetchPrediction() + }, []) + + return ( + +
+ +

AI 风险预测

+
+ {loading ? ( +
+ 分析中... +
+ ) : ( +
{result}
+ )} +
+ +
+
+ ) +} + +function ReviewTab() { + const [contractText, setContractText] = useState('') + const [result, setResult] = useState('') + const [loading, setLoading] = useState(false) + + const handleReview = async () => { + if (!contractText.trim()) return + setLoading(true) + setResult('') + try { + const res = await api.post('/ai/review', { contractText }) as any + setResult(res.data.result) + } catch (err: any) { + setResult(`出错了:${err.response?.data?.error?.message || '请稍后重试'}`) + } finally { + setLoading(false) + } + } + + return ( +
+ +
+ +

合同审查

+
+ +