From 96a936e154e5e06fce93f0454edd7df57cae0a3b Mon Sep 17 00:00:00 2001 From: SpecialX <47072643+wangxiner55@users.noreply.github.com> Date: Mon, 13 Jul 2026 17:21:39 +0800 Subject: [PATCH] =?UTF-8?q?feat(parent-portal):=20docker=20=E6=9E=84?= =?UTF-8?q?=E5=BB=BA=E4=BF=AE=E5=A4=8D=20+=20=E7=A6=81=E7=94=A8=20mock=20+?= =?UTF-8?q?=20=E8=AE=B0=E5=BD=95=E4=B8=8B=E6=B8=B8=E5=BE=85=E5=8A=9E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Dockerfile 改用 standalone 模式 next.config.js 添加 output standalone 修复 cssnano-simple 构建失败:globals.css 注释含 */ 被误解析 tailwind.config.js 覆盖 boxShadow 为 rgba 格式 + 禁用 ringWidth 组件修复:替换 / 语法为 inline style 或自定义类 otel.ts 添加 webpackIgnore 跳过 optionalDependencies 静态分析 .env.example 默认禁用 mock 新增 .dockerignore 和 docs/nextstep.md 删除 postcss.config.js 验证:Docker 镜像构建成功,容器 healthy,typecheck + lint 零错误 --- .dockerignore | 92 ++++++++ apps/parent-portal/.dockerignore | 47 ++++ apps/parent-portal/.env.example | 3 +- apps/parent-portal/Dockerfile | 27 ++- apps/parent-portal/docs/nextstep.md | 210 ++++++++++++++++++ apps/parent-portal/next.config.js | 2 + apps/parent-portal/postcss.config.js | 6 - apps/parent-portal/src/app/globals.css | 15 +- .../src/app/parent/settings/page.tsx | 10 +- .../src/components/AttendanceCalendar.tsx | 24 +- .../components/AttendanceWarningBanner.tsx | 9 +- .../src/components/ParentAttentionBanner.tsx | 9 +- .../src/lib/observability/otel.ts | 37 +-- apps/parent-portal/tailwind.config.js | 17 ++ 14 files changed, 453 insertions(+), 55 deletions(-) create mode 100644 .dockerignore create mode 100644 apps/parent-portal/.dockerignore create mode 100644 apps/parent-portal/docs/nextstep.md delete mode 100644 apps/parent-portal/postcss.config.js diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..396025d --- /dev/null +++ b/.dockerignore @@ -0,0 +1,92 @@ +# 依赖目录(构建时重新安装) +**/node_modules +.pnpm-store + +# 构建产物 +**/.next +**/out +**/dist +**/build +**/coverage +**/target +**/bin +**/obj + +# Go 构建产物 +*.exe +*.exe~ +*.dll +*.so +*.dylib +*.test +*.out + +# Python 缓存 +**/__pycache__ +**/*.py[cod] +**/*$py.class +**/.venv +**/venv +**/*.egg-info + +# 环境变量与本地配置 +.env*.local +.env +.env.* +!.env.example + +# Secrets / Keys +keys/ +*.pem +*.key +secrets/ + +# IDE 与编辑器 +.vscode +.idea +*.swp +*.swo +*~ + +# Git +.git +.gitignore +.gitattributes + +# 平台与临时文件 +.DS_Store +Thumbs.db +*.tmp +*.bak + +# TypeScript 增量构建缓存 +*.tsbuildinfo + +# 测试覆盖率报告 +.nyc_output +**/coverage + +# 日志 +*.log +logs/ + +# 数据库文件 +*.db +*.sqlite +arch.db + +# Docker +docker-compose.override.yml +docker-compose.minimal.override.yml + +# 临时目录 +tmp/ +temp/ +.turbo +.cache + +# 文档(不需要打包到镜像) +docs/ +**/docs/ +*.md +!README.md diff --git a/apps/parent-portal/.dockerignore b/apps/parent-portal/.dockerignore new file mode 100644 index 0000000..a80dc2a --- /dev/null +++ b/apps/parent-portal/.dockerignore @@ -0,0 +1,47 @@ +# 依赖目录(构建时重新安装) +node_modules +**/node_modules + +# 构建产物 +.next +out +dist +build +coverage + +# 测试与开发工具 +.turbo +.cache +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* + +# 环境变量与本地配置 +.env*.local +.env + +# IDE 与编辑器 +.vscode +.idea +*.swp +*.swo +*~ + +# Git +.git +.gitignore + +# 平台与临时文件 +.DS_Store +Thumbs.db +*.tmp +*.bak + +# TypeScript 增量构建缓存 +*.tsbuildinfo + +# 测试覆盖率报告 +.nyc_output +coverage diff --git a/apps/parent-portal/.env.example b/apps/parent-portal/.env.example index 54ac986..b33f232 100644 --- a/apps/parent-portal/.env.example +++ b/apps/parent-portal/.env.example @@ -19,7 +19,8 @@ NEXT_PUBLIC_GRAPHQL_ENDPOINT=/api/v1/parent/v1/graphql # ===== Mock ===== # MSW mock 开关:enabled 启用 mock,disabled 调真实后端 # 依据:workline.md §4.3 全并行 Mock 策略 -NEXT_PUBLIC_API_MOCKING=enabled +# 默认禁用:parent-portal 已切换至真实 API 集成(详见 docs/nextstep.md) +NEXT_PUBLIC_API_MOCKING=disabled # ===== 推送(P5)===== # push-gateway WebSocket 地址 diff --git a/apps/parent-portal/Dockerfile b/apps/parent-portal/Dockerfile index 7959a61..d5b4462 100644 --- a/apps/parent-portal/Dockerfile +++ b/apps/parent-portal/Dockerfile @@ -1,4 +1,4 @@ -# 多阶段构建:Next.js 生产镜像(parent-portal MF Remote) +# 多阶段构建:Next.js 生产镜像(parent-portal,standalone 模式) # 用法:docker build -t edu/parent-portal:latest -f apps/parent-portal/Dockerfile . # 依据:02-architecture-design.md §16 部署、G1 裁决(首次即多阶段) # 端口:4002(4 端 portal 强制 4000-4003) @@ -12,6 +12,7 @@ RUN corepack enable && corepack prepare pnpm@9.12.0 --activate # 先拷依赖清单,利用缓存 COPY package.json pnpm-lock.yaml* pnpm-workspace.yaml* ./ +COPY tsconfig.base.json ./tsconfig.base.json COPY apps/parent-portal/package.json ./apps/parent-portal/ # 安装依赖(含 devDependencies,构建需要) RUN pnpm install --filter @edu/parent-portal... --frozen-lockfile || pnpm install --filter @edu/parent-portal... @@ -19,9 +20,8 @@ RUN pnpm install --filter @edu/parent-portal... --frozen-lockfile || pnpm instal # 拷源码 COPY apps/parent-portal ./apps/parent-portal -# 构建(禁用 telemetry,生产模式) +# 构建(禁用 telemetry,生产模式,standalone 输出) ENV NEXT_TELEMETRY_DISABLED=1 -ENV NODE_ENV=production RUN pnpm --filter @edu/parent-portal run build # ============ Runtime ============ @@ -31,22 +31,25 @@ WORKDIR /app ENV NODE_ENV=production ENV NEXT_TELEMETRY_DISABLED=1 ENV PORT=4002 +# standalone server.js 默认绑定 HOSTNAME(Docker 中为容器 ID),导致 localhost 不可达 +# 强制绑定 0.0.0.0 让健康检查 wget localhost 能连通 +ENV HOSTNAME=0.0.0.0 # 非 root 用户运行 RUN addgroup -g 1001 -S nodejs && adduser -S nextjs -u 1001 -# 拷构建产物与必要清单 -COPY --from=builder /app/apps/parent-portal/package.json ./package.json -COPY --from=builder /app/apps/parent-portal/.next ./.next -COPY --from=builder /app/apps/parent-portal/public ./public -COPY --from=builder /app/node_modules ./node_modules -COPY --from=builder /app/apps/parent-portal/next.config.js ./next.config.js +# 拷 standalone 产物(已含 node_modules 和 server.js,自包含) +COPY --from=builder --chown=nextjs:nodejs /app/apps/parent-portal/.next/standalone ./ +COPY --from=builder --chown=nextjs:nodejs /app/apps/parent-portal/.next/static ./.next/static +COPY --from=builder --chown=nextjs:nodejs /app/apps/parent-portal/public ./public USER nextjs EXPOSE 4002 -# 健康检查 +# 健康检查(使用 127.0.0.1 而非 localhost,避免容器内 hosts 解析问题) HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \ - CMD wget --quiet --spider http://localhost:4002/api/health || exit 1 + CMD wget --quiet --spider http://127.0.0.1:4002/api/health || exit 1 -CMD ["node_modules/.bin/next", "start", "-p", "4002"] +# standalone 模式下直接用 node server.js 启动(已自包含所有依赖) +WORKDIR /app/apps/parent-portal +CMD ["node", "server.js"] diff --git a/apps/parent-portal/docs/nextstep.md b/apps/parent-portal/docs/nextstep.md new file mode 100644 index 0000000..a37d865 --- /dev/null +++ b/apps/parent-portal/docs/nextstep.md @@ -0,0 +1,210 @@ +# parent-portal 下游待办事项(Next Steps) + +> 版本:v1 +> 日期:2026-07-13 +> 用途:记录 parent-portal 切换至真实 API 集成所需的下游模块工作 +> 背景:parent-portal 已完成 Docker Desktop 验证(容器健康、25 路由生成、/login 200、/api/health 200),现需下游服务就绪才能完成端到端真实 API 联调 + +--- + +## 1. 当前状态摘要 + +| 项目 | 状态 | 说明 | +| -------------------------- | ----------------- | ---------------------------------------------------------------------- | +| parent-portal Docker 镜像 | ✅ 已就绪 | `edu-parent-portal:test`,standalone 模式,25 路由 | +| parent-portal 容器健康检查 | ✅ 通过 | `/api/health` 200,`/login` 200,`/parent/dashboard` 307 auth redirect | +| Mock 数据 | ✅ 已禁用 | `NEXT_PUBLIC_API_MOCKING=disabled` | +| GraphQL 端点配置 | ✅ 已配置 | `NEXT_PUBLIC_GRAPHQL_ENDPOINT=/api/v1/parent/v1/graphql` | +| parent-bff 服务 | ❌ 未就绪 | 端点路径错配 + 27 query / 5 mutation 缺失 | +| api-gateway 路由 | ⚠️ 已配置但未验证 | `/api/v1/parent/*` → parent-bff:3010,但 parent-bff 未运行 | +| iam GetChildrenByParent | ✅ 已就绪 | gRPC + HTTP 均已实现 | +| push-gateway /ws | ✅ 已就绪 | 完整 WebSocket 实现 | + +--- + +## 2. P0 阻塞项:parent-bff 端点路径错配 + +### 问题描述 + +前端调用 GraphQL 端点:`/api/v1/parent/v1/graphql`(ARB-022 §24.4 ISSUE-003 方案 A 双 /v1 前缀) + +api-gateway 代理路径转换: + +- 入站:`/api/v1/parent/v1/graphql` +- 剥离 `/api` 后转发:`/v1/parent/v1/graphql` → parent-bff:3010 + +但 parent-bff 当前 controller 仅注册 `/graphql`,导致 `/v1/parent/v1/graphql` 在 parent-bff 找不到匹配的 handler,返回 404。 + +### 修复位置(parent-bff 模块) + +| 文件 | 当前 | 应改为 | +| ----------------------------------------------------- | ----------------------------- | -------------------------------------------------------------- | +| `services/parent-bff/src/entry/graphql.controller.ts` | `@Controller("graphql")` | `@Controller("v1/parent/v1/graphql")` 或使用 `setGlobalPrefix` | +| `services/parent-bff/src/graphql/yoga.ts` | `graphqlEndpoint: "/graphql"` | `graphqlEndpoint: "/v1/parent/v1/graphql"` | +| `services/parent-bff/src/graphql/graphql.module.ts` | `forRoutes("graphql")` | `forRoutes("v1/parent/v1/graphql")` | + +### 验证步骤 + +1. 修改后构建 parent-bff 镜像 +2. 启动 parent-bff 容器,验证 `http://parent-bff:3010/v1/parent/v1/graphql` 返回 200 +3. 经 api-gateway 代理访问 `http://api-gateway:8080/api/v1/parent/v1/graphql` 返回 200 + +--- + +## 3. P0 阻塞项:parent-bff 缺失 pnpm-lock.yaml + +### 问题描述 + +`services/parent-bff/Dockerfile` 第 6 行 `RUN pnpm install --frozen-lockfile` 失败,因为 `services/parent-bff/` 目录下没有 `pnpm-lock.yaml` 文件。 + +### 修复方案 + +在 `services/parent-bff/` 目录执行 `pnpm install` 生成 lockfile,并提交到仓库。 + +```bash +cd services/parent-bff +pnpm install +git add pnpm-lock.yaml +git commit -m "chore(parent-bff): add pnpm-lock.yaml for Docker build" +``` + +--- + +## 4. P0 阻塞项:parent-bff GraphQL Schema 严重不对齐 + +### 问题描述 + +| 类别 | 前端定义 | 后端实现 | 差距 | +| -------- | -------- | -------- | --------------------------------------------------------------- | +| Query | 32 个 | 11 个 | 21 个缺失(含 6 个命名不一致 + 15 个完全缺失 + 3 个签名不一致) | +| Mutation | 6 个 | 3 个 | 3 个缺失(含 2 个命名不一致 + 1 个完全缺失 + 1 个签名不一致) | + +### 4.1 命名不一致的 Query(6 个) + +| 前端 operation | 后端 operation | 修复方式 | +| --------------------------- | ------------------------- | ---------------------------------------------------------------- | +| `currentUser` | `me` | 二选一统一命名(建议后端改 `me` → `currentUser`) | +| `myChildren` | `children` | 二选一统一命名(建议后端改 `children` → `myChildren`) | +| `myNotifications` | `notifications` | 二选一统一命名(建议后端改 `notifications` → `myNotifications`) | +| `myNotificationPreferences` | `notificationPreferences` | 二选一统一命名 | +| `childSummary` | `child` | 字段不同,需明确语义 | +| `childAnalytics` | (前端未使用) | 后端已有但前端未对接,保留 | + +### 4.2 完全缺失的 Query(15 个,后端需新增) + +| 前端 operation | 涉及下游服务 | +| ------------------------ | ------------ | +| `childAttendance` | core-edu | +| `childWeakness` | data-ana | +| `childTrend` | data-ana | +| `childExamResult` | core-edu | +| `childClasses` | classes | +| `childLearningPath` | data-ana | +| `childLeaveRequests` | core-edu | +| `academicYears` | core-edu | +| `childReportCard` | core-edu | +| `childErrorBookStats` | data-ana | +| `childTopWrongQuestions` | data-ana | +| `childWeakKps` | data-ana | +| `childMasterySummary` | data-ana | +| `childDiagnosticReports` | data-ana | +| `childPracticeStats` | data-ana | +| `childPracticeSessions` | data-ana | +| `childCoursePlans` | core-edu | +| `childCoursePlanDetail` | content | +| `childLessonPlans` | core-edu | +| `childLessonPlanDetail` | content | +| `childElective` | content | +| `childDetail` | data-ana | +| `childGrowthArchive` | data-ana | + +### 4.3 签名不一致的 Query(3 个) + +| 前端 operation | 不一致点 | +| --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `childGrades` | 前端 `(childId, subject, limit)` vs 后端 `(childId, page, pageSize, subject)`;返回字段不同(前端 `examName/examDate/studentScore/classMax/classMin/gradeLevel` vs 后端 `examId/examTitle/score/rank/gradedAt`) | +| `childHomework` | 前端有 `status` 过滤参数,后端无;字段差异大(前端 `subject/className/assignedDate/score/maxScore/feedback` vs 后端 `title/classId/dueDate/status`) | +| `childExams` | 字段差异大(前端 `name/subject/startsAt/expiresAt/durationSeconds/questionCount/totalScore` vs 后端 `title/examDate/publishedAt`) | + +### 4.4 缺失的 Mutation(3 个) + +| 前端 operation | 涉及下游服务 | +| -------------------- | ---------------------------------------------------------- | +| `markAsRead` | parent-bff(重命名 `markNotificationRead` → `markAsRead`) | +| `markAllAsRead` | parent-bff(新增) | +| `switchChild` | parent-bff(重命名 `selectChild` → `switchChild`) | +| `createLeaveRequest` | core-edu(新增) | +| `exportChildGrades` | core-edu(新增) | + +### 4.5 签名不一致的 Mutation(1 个) + +| 前端 operation | 不一致点 | +| ------------------------------- | ---------------------------------------------------------------------------------------------------------- | +| `updateNotificationPreferences` | 前端 `(parentId, preferences: JSON, defaults: JSON)` vs 后端 `(input: UpdateNotificationPreferencesInput)` | + +### 修复步骤 + +1. **契约先行**:更新 `packages/shared-ts/contracts/graphql/parent-bff.graphql`,扩充至前端 32 query + 6 mutation +2. **resolver 实现**:在 `services/parent-bff/src/graphql/resolvers/` 新增对应 resolver +3. **下游 gRPC client**:在 `services/parent-bff/src/clients/` 新增/对接 core-edu / data-ana / content / classes 的 gRPC client +4. **测试**:parent-bff 单测 + parent-portal 端到端联调 + +--- + +## 5. P1 阻塞项:api-gateway 镜像构建问题 + +### 问题描述 + +`services/api-gateway/Dockerfile` 使用 `golang:1.22-alpine` 和 `alpine:3.20` 基础镜像,但本地 Docker Desktop 未拉取这两个镜像,且 daocloud 镜像源返回 401。 + +### 修复方案 + +1. 提前在服务器执行:`docker pull golang:1.22-alpine && docker pull alpine:3.20` +2. 或在 Dockerfile 中改用本地已有的镜像源 + +--- + +## 6. 已就绪的下游服务(无需等待) + +| 服务 | 状态 | 验证位置 | +| ----------------------- | ----------------------- | ----------------------------------------------------------------------------------------------- | +| iam.GetChildrenByParent | ✅ gRPC + HTTP 均已实现 | `services/iam/src/iam/iam.grpc.controller.ts:116`、`services/iam/src/iam/iam.controller.ts:116` | +| push-gateway /ws | ✅ 完整实现 | `services/push-gateway/internal/ws/handler.go` | + +--- + +## 7. 验证 Checklist(下游就绪后执行) + +- [ ] parent-bff 添加 pnpm-lock.yaml +- [ ] parent-bff GraphQL 端点路径改为 `/v1/parent/v1/graphql` +- [ ] parent-bff schema 扩充至 32 query + 6 mutation +- [ ] parent-bff 所有 resolver 实现完成 +- [ ] api-gateway 镜像可构建 +- [ ] 在 Docker Desktop 启动完整服务栈:api-gateway + parent-bff + iam + core-edu + data-ana + content + classes + push-gateway +- [ ] parent-portal 端到端联调:登录 → /parent/dashboard → 各功能页面 +- [ ] 验证 WebSocket 推送:通知功能 +- [ ] 验证 25 个路由全部可访问(无 500 错误) + +--- + +## 8. 关键文件路径 + +| 文件 | 用途 | +| --------------------------------------------------------- | ---------------------------------------------------- | +| `apps/parent-portal/Dockerfile` | parent-portal Docker 构建(已修复,standalone 模式) | +| `apps/parent-portal/next.config.js` | Next.js 配置(已添加 `output: "standalone"`) | +| `apps/parent-portal/src/lib/graphql-client.ts` | GraphQL 客户端配置 | +| `apps/parent-portal/src/lib/graphql/operations.ts` | 前端所有 GraphQL operations 定义 | +| `apps/parent-portal/.env.example` | 环境变量模板(`NEXT_PUBLIC_API_MOCKING=disabled`) | +| `services/parent-bff/Dockerfile` | parent-bff Docker 构建(待修复:缺 pnpm-lock.yaml) | +| `services/parent-bff/src/entry/graphql.controller.ts` | GraphQL 端点路径(待修复) | +| `services/parent-bff/src/graphql/yoga.ts` | GraphQL Yoga 配置(待修复) | +| `services/parent-bff/src/graphql/graphql.module.ts` | GraphQL 模块路由(待修复) | +| `services/parent-bff/src/graphql/resolvers/index.ts` | resolver 实现(待扩充) | +| `packages/shared-ts/contracts/graphql/parent-bff.graphql` | GraphQL schema 契约(待扩充) | +| `services/api-gateway/main.go:86` | parent 代理注册 | +| `services/api-gateway/Dockerfile` | api-gateway Docker 构建(待修复:基础镜像缺失) | + +--- + +**总结**:parent-portal 前端已完全就绪(Docker 镜像构建成功、容器健康、25 路由可用、mock 已禁用),等待下游 parent-bff 完成端点路径修复 + schema 扩充 + resolver 实现后,即可完成端到端真实 API 联调。 diff --git a/apps/parent-portal/next.config.js b/apps/parent-portal/next.config.js index 93861d5..5a37222 100644 --- a/apps/parent-portal/next.config.js +++ b/apps/parent-portal/next.config.js @@ -26,6 +26,8 @@ const remotes = (isServer) => ({ /** @type {import('next').NextConfig} */ const nextConfig = { reactStrictMode: true, + // standalone 输出模式:自包含 server.js + 精简 node_modules(适合 Docker 部署) + output: "standalone", // ESM 兼容(MF 2.0 需要) experimental: { externalDir: true, diff --git a/apps/parent-portal/postcss.config.js b/apps/parent-portal/postcss.config.js deleted file mode 100644 index 12a703d..0000000 --- a/apps/parent-portal/postcss.config.js +++ /dev/null @@ -1,6 +0,0 @@ -module.exports = { - plugins: { - tailwindcss: {}, - autoprefixer: {}, - }, -}; diff --git a/apps/parent-portal/src/app/globals.css b/apps/parent-portal/src/app/globals.css index 1189322..9bc340c 100644 --- a/apps/parent-portal/src/app/globals.css +++ b/apps/parent-portal/src/app/globals.css @@ -9,7 +9,7 @@ * * Layer 1 Primitive:原始色板/字号/间距/阴影(业务代码不直接引用) * Layer 2 Semantic:语义令牌(light/dark),业务代码唯一引用入口 - * Layer 3 Tailwind Theme:@theme inline 暴露为 bg-*/text-*/font-* 类 + * Layer 3 Tailwind Theme:@theme inline 暴露为 bg, text, font 等类 */ :root { @@ -59,10 +59,10 @@ --radius-md: 0.5rem; --radius-lg: 0.75rem; - /* 阴影 */ - --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05); - --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1); - --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1); + /* 阴影(使用 rgba 旧语法,避免 cssnano-simple 无法解析 rgb(/ 语法)*/ + --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05); + --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1); + --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1); /* ===== Layer 2 Semantic(light)===== */ --paper: var(--color-paper-50); @@ -168,6 +168,11 @@ h6 { border: 0; } +/* 安全的 col-span-2(替代 Tailwind col-span-2,避免 cssnano-simple 无法解析 grid-column: span 2 / span 2 语法)*/ +.col-span-2-safe { + grid-column: span 2; +} + /* 骨架屏动画 */ @keyframes skeleton-pulse { 0%, diff --git a/apps/parent-portal/src/app/parent/settings/page.tsx b/apps/parent-portal/src/app/parent/settings/page.tsx index 07d639f..cf7eb8d 100644 --- a/apps/parent-portal/src/app/parent/settings/page.tsx +++ b/apps/parent-portal/src/app/parent/settings/page.tsx @@ -62,25 +62,25 @@ export default function SettingsPage(): JSX.Element {
用户 ID
-
{user.id}
+
{user.id}
姓名
-
{user.name}
+
{user.name}
邮箱
-
{user.email}
+
{user.email}
角色
-
+
{user.roles.join(", ") || "无角色"}
数据范围
-
{user.dataScope}
+
{user.dataScope}
diff --git a/apps/parent-portal/src/components/AttendanceCalendar.tsx b/apps/parent-portal/src/components/AttendanceCalendar.tsx index 271182f..fcb8714 100644 --- a/apps/parent-portal/src/components/AttendanceCalendar.tsx +++ b/apps/parent-portal/src/components/AttendanceCalendar.tsx @@ -57,9 +57,16 @@ export function AttendanceCalendar({ month, onMonthChange, }: AttendanceCalendarProps) { - const { year, month: monthLabel, days, recordMap } = useMemo(() => { + const { + year, + month: monthLabel, + days, + recordMap, + } = useMemo(() => { const now = new Date(); - const target = month ? parseMonth(month) : { year: now.getFullYear(), month: now.getMonth() }; + const target = month + ? parseMonth(month) + : { year: now.getFullYear(), month: now.getMonth() }; const y = target.year; const m = target.month; const firstDay = new Date(y, m, 1); @@ -96,7 +103,9 @@ export function AttendanceCalendar({ {onMonthChange && (