@aiquants/daily-report 0.4.2 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/README.md +59 -0
  2. package/dist/client.d.mts +90 -5
  3. package/dist/client.d.ts +90 -5
  4. package/dist/client.js +4 -4
  5. package/dist/client.js.map +1 -1
  6. package/dist/client.mjs +4 -4
  7. package/dist/client.mjs.map +1 -1
  8. package/dist/index.d.mts +2 -2
  9. package/dist/index.d.ts +2 -2
  10. package/dist/index.js +1 -1
  11. package/dist/index.js.map +1 -1
  12. package/dist/index.mjs +1 -1
  13. package/dist/index.mjs.map +1 -1
  14. package/dist/server.d.mts +13 -1
  15. package/dist/server.d.ts +13 -1
  16. package/dist/server.js +6 -6
  17. package/dist/server.js.map +1 -1
  18. package/dist/server.mjs +6 -6
  19. package/dist/server.mjs.map +1 -1
  20. package/dist/sse-schema-eXcMG-Ej.d.ts +2025 -0
  21. package/dist/sse-schema-rbG114od.d.mts +2025 -0
  22. package/dist/styles/daily-report.standalone.css +1 -1
  23. package/dist/{types-DAyE_3R1.d.mts → types-D1PKubyo.d.mts} +26 -26
  24. package/dist/{types-DAyE_3R1.d.ts → types-D1PKubyo.d.ts} +26 -26
  25. package/package.json +3 -3
  26. package/src/client/components/daily-report-detail-list.tsx +63 -51
  27. package/src/client/components/daily-report-list.tsx +16 -4
  28. package/src/client/components/daily-report-resolved-content.tsx +33 -7
  29. package/src/client/config-context.tsx +21 -8
  30. package/src/client/contexts/daily-report-action-context.tsx +54 -2
  31. package/src/client/hooks/use-daily-report-sse-connection.ts +1 -1
  32. package/src/client/hooks/use-daily-report.ts +25 -3
  33. package/src/server/cache.spec.ts +47 -0
  34. package/src/server/cache.ts +27 -0
  35. package/src/server/handlers.ts +4 -0
  36. package/src/server/service.ts +18 -1
  37. package/src/shared/comment-adapter.ts +3 -3
  38. package/src/shared/sse-schema.ts +27 -25
  39. package/src/shared/types.ts +26 -26
  40. package/dist/sse-schema-BnQQh_Cc.d.ts +0 -1986
  41. package/dist/sse-schema-DRNjDRDy.d.mts +0 -1986
@@ -1,4 +1,4 @@
1
- import { Plus } from "lucide-react"
1
+ import { Plus, RefreshCw, RotateCw } from "lucide-react"
2
2
  import type { ReactNode } from "react"
3
3
  import { useEffect, useMemo, useState } from "react"
4
4
  import { useSearchParams } from "react-router"
@@ -28,7 +28,7 @@ export const DailyReportResolvedContent = ({ dailyReportItems: _initialItems, us
28
28
  // 自動既読設定
29
29
  const [autoMarkRead, setAutoMarkRead] = useState(false)
30
30
  const { title, renderHeader, showDevControls } = useDailyReportConfig()
31
- const { isSseEnabled, toggleSse, createReport, items: dailyReportItems } = useDailyReportActionContext()
31
+ const { isSseEnabled, toggleSse, createReport, items: dailyReportItems, refetchData, clearCacheAndRefetch } = useDailyReportActionContext()
32
32
  const [searchParams] = useSearchParams()
33
33
 
34
34
  // 選択状態の管理 (タブ間で共有)
@@ -37,6 +37,14 @@ export const DailyReportResolvedContent = ({ dailyReportItems: _initialItems, us
37
37
  return dailyReportItems[0].reportHubId
38
38
  })
39
39
 
40
+ const handleReload = async () => {
41
+ await refetchData()
42
+ }
43
+
44
+ const handleReloadClearCache = async () => {
45
+ await clearCacheAndRefetch()
46
+ }
47
+
40
48
  const handleCreateReport = async () => {
41
49
  const businessDateParam = searchParams.get("businessDate")
42
50
  const today = new Date().toISOString().split("T")[0]
@@ -113,11 +121,29 @@ export const DailyReportResolvedContent = ({ dailyReportItems: _initialItems, us
113
121
  <Plus className="h-4 w-4" />
114
122
  </Button>
115
123
  {showDevControls && (
116
- <div className="hidden items-center space-x-1 sm:flex">
117
- <Switch id="sse-mode" checked={isSseEnabled} onCheckedChange={toggleSse} className="scale-75" />
118
- <Label htmlFor="sse-mode" className="cursor-pointer text-slate-500 text-xs">
119
- 購読
120
- </Label>
124
+ <div className="hidden items-center space-x-1.5 rounded-md border border-amber-300 bg-amber-50 px-2 py-0.5 text-xs sm:flex dark:border-amber-700/50 dark:bg-amber-950/40">
125
+ <span className="rounded bg-amber-200 px-1 py-0.2 font-bold font-mono text-[10px] text-amber-900 dark:bg-amber-800 dark:text-amber-100" title="開発環境限定のコントロールです">
126
+ DEV
127
+ </span>
128
+ <div className="flex items-center space-x-1">
129
+ <Switch id="sse-mode" checked={isSseEnabled} onCheckedChange={toggleSse} className="scale-75" />
130
+ <Label htmlFor="sse-mode" className="cursor-pointer text-slate-600 text-xs dark:text-slate-300" title="開発環境のみ: Real-time SSE 購読切り替え">
131
+ 購読
132
+ </Label>
133
+ </div>
134
+ <Button variant="ghost" size="sm" onClick={handleReload} className="h-6 px-1.5 text-slate-700 text-xs hover:bg-amber-200 dark:text-slate-200 dark:hover:bg-amber-800" title="開発環境のみ: ページリロードなしでデータのみ再取得">
135
+ <RotateCw className="mr-1 h-3 w-3" />
136
+ リロード
137
+ </Button>
138
+ <Button
139
+ variant="ghost"
140
+ size="sm"
141
+ onClick={handleReloadClearCache}
142
+ className="h-6 px-1.5 text-amber-800 text-xs hover:bg-amber-200 dark:text-amber-300 dark:hover:bg-amber-800"
143
+ title="開発環境のみ: サーバー/DB & クライアントのキャッシュを破棄してデータ再取得">
144
+ <RefreshCw className="mr-1 h-3 w-3" />
145
+ キャッシュクリア
146
+ </Button>
121
147
  </div>
122
148
  )}
123
149
  <Switch id="auto-read-mode" checked={autoMarkRead} onCheckedChange={handleAutoMarkReadChange} className="scale-75" />
@@ -56,12 +56,12 @@ export type DailyReportClientConfig = {
56
56
  draftLabelName: string
57
57
  /** 詳細/一覧カード of field 見出しラベル。既定は中立英語。アプリが自ドメインの表記を注入する。 */
58
58
  fieldLabels: DailyReportFieldLabels
59
+ /** 外部ソース区分ごとのバッジ表示設定。 */
60
+ sourceTypeConfigs?: Record<string, SourceTypeConfig>
59
61
  /** ヘッダー描画スロット。アプリのヘッダーコンポーネントを差し込む。 */
60
62
  renderHeader: (props: DailyReportHeaderProps) => ReactNode
61
63
  /** 開発向けコントロール (SSE 購読トグル等) を表示するか。 */
62
64
  showDevControls: boolean
63
- /** データソースごとのバッジ表示設定。 */
64
- sourceTypeConfigs?: Record<string, SourceTypeConfig>
65
65
  }
66
66
 
67
67
  /**
@@ -70,6 +70,7 @@ export type DailyReportClientConfig = {
70
70
  */
71
71
  export type DailyReportClientConfigInput = Partial<Omit<DailyReportClientConfig, "fieldLabels">> & {
72
72
  fieldLabels?: Partial<DailyReportFieldLabels>
73
+ sourceTypeConfigs?: Record<string, SourceTypeConfig>
73
74
  }
74
75
 
75
76
  /** 中立英語のフィールドラベル既定 (ドメイン非依存)。 */
@@ -102,6 +103,12 @@ export const defaultDailyReportClientConfig: DailyReportClientConfig = {
102
103
  // 中立プレースホルダー。実運用のラベル名 (アプリ固有) は Provider / config prop で上書きする。
103
104
  draftLabelName: "draft",
104
105
  fieldLabels: defaultDailyReportFieldLabels,
106
+ sourceTypeConfigs: {
107
+ Internal: {
108
+ label: "オリジナル",
109
+ badgeClassName: "bg-emerald-100 text-emerald-800 dark:bg-emerald-900/30 dark:text-emerald-400",
110
+ },
111
+ },
105
112
  renderHeader: ({ title, annotation }) => (
106
113
  <header className="flex flex-wrap items-baseline justify-between gap-2 px-2 py-1">
107
114
  <span className="text-nowrap text-slate-700 text-sm dark:text-slate-200">{title}</span>
@@ -109,12 +116,6 @@ export const defaultDailyReportClientConfig: DailyReportClientConfig = {
109
116
  </header>
110
117
  ),
111
118
  showDevControls: false,
112
- sourceTypeConfigs: {
113
- Internal: {
114
- label: "オリジナル",
115
- badgeClassName: "bg-emerald-100 text-emerald-800 dark:bg-emerald-900/30 dark:text-emerald-400",
116
- },
117
- },
118
119
  }
119
120
 
120
121
  const DailyReportConfigContext = createContext<DailyReportClientConfig>(defaultDailyReportClientConfig)
@@ -141,3 +142,15 @@ export const DailyReportConfigProvider = ({ config, children }: { config?: Daily
141
142
  * 日報クライアント設定を読む (プロバイダー未設置時は既定値)。
142
143
  */
143
144
  export const useDailyReportConfig = (): DailyReportClientConfig => useContext(DailyReportConfigContext)
145
+
146
+ /**
147
+ * Resolves source type badge configuration with case-insensitive fallback.
148
+ * ケースインセンシティブなフォールバック付きでデータソースバッジ設定を解決する。
149
+ */
150
+ export const resolveSourceTypeConfig = (sourceType?: string | null, sourceTypeConfigs?: Record<string, SourceTypeConfig>): SourceTypeConfig | undefined => {
151
+ if (!(sourceType && sourceTypeConfigs)) return undefined
152
+ if (sourceTypeConfigs[sourceType]) return sourceTypeConfigs[sourceType]
153
+ const lower = sourceType.toLowerCase()
154
+ const matchedKey = Object.keys(sourceTypeConfigs).find((k) => k.toLowerCase() === lower)
155
+ return matchedKey ? sourceTypeConfigs[matchedKey] : undefined
156
+ }
@@ -3,11 +3,12 @@
3
3
  * 日報の楽観的 CRUD と SSE 同期を提供するアクションコンテキスト。
4
4
  */
5
5
  import { type Context, createContext, type ReactNode, useCallback, useContext, useEffect, useRef, useState } from "react"
6
+ import { useRevalidator } from "react-router"
6
7
  import { createLogger, LogLevel } from "../../shared/logger"
7
8
  import type { DailyReportSseMessage } from "../../shared/sse-schema"
8
9
  import type { DailyReportCommentItem, DailyReportDetail, DailyReportItem, DailyReportUser } from "../../shared/types"
9
10
  import { useDailyReportConfig } from "../config-context"
10
- import { acquireMutationLock, applyDailyReportServerUpdates, deleteDailyReportCache, getCachedReport, registerRecentDeletion, releaseMutationLock, subscribeCacheReady, updateDailyReportCache, writeCache } from "../hooks/use-daily-report"
11
+ import { acquireMutationLock, applyDailyReportServerUpdates, clearDailyReportCache, DailyReportCache, deleteDailyReportCache, getCachedReport, registerRecentDeletion, releaseMutationLock, subscribeCacheReady, updateDailyReportCache, writeCache } from "../hooks/use-daily-report"
11
12
  import { useDailyReportSseConnection } from "../hooks/use-daily-report-sse-connection"
12
13
 
13
14
  const logger = createLogger(LogLevel.INFO, "daily-report-action-context")
@@ -80,6 +81,10 @@ type DailyReportActionContextType = {
80
81
  deleteReport: (reportHubId: number, businessDate: string) => Promise<void>
81
82
  /** stale アイテムをリストから除去する安全弁 */
82
83
  removeStaleItem: (reportHubId: number) => void
84
+ /** データのみリロード (ページリロードなし) */
85
+ refetchData: () => Promise<void>
86
+ /** キャッシュを破棄してデータのみ再読み込み (ページリロードなし) */
87
+ clearCacheAndRefetch: () => Promise<void>
83
88
  }
84
89
 
85
90
  // HMR / dual-bundle (ESM+CJS) でもコンテキスト identity を一意に保つため、
@@ -132,6 +137,50 @@ export const DailyReportActionProvider = ({ children, user, initialItems = [], u
132
137
  /** 自分が発行したアクションのID(SSEエコー無視用) */
133
138
  const processedClientTempIds = useRef<Set<string>>(new Set())
134
139
 
140
+ const revalidator = useRevalidator()
141
+
142
+ const refetchData = useCallback(async () => {
143
+ logger.info("[DailyReportActionProvider] Refetching data without page reload...")
144
+ if (revalidator.state === "idle") {
145
+ revalidator.revalidate()
146
+ }
147
+ setVersion((v) => v + 1)
148
+ DailyReportCache.listeners.forEach((listener: (id: number) => void) => {
149
+ listener(-1)
150
+ })
151
+ }, [revalidator])
152
+
153
+ const clearCacheAndRefetch = useCallback(async () => {
154
+ logger.info("[DailyReportActionProvider] Clearing client & server/DB cache and refetching data without page reload...")
155
+ // 1. クライアント側キャッシュ破棄
156
+ clearDailyReportCache()
157
+ removedIds.current.clear()
158
+ try {
159
+ sessionStorage.clear()
160
+ } catch {}
161
+
162
+ // 2. サーバー/DB側キャッシュ破棄 API 呼び出し
163
+ try {
164
+ const formData = new FormData()
165
+ formData.append("intent", "clearCache")
166
+ await fetch(`${apiBasePath}/action`, {
167
+ method: "POST",
168
+ body: formData,
169
+ })
170
+ } catch (e) {
171
+ logger.warn("[DailyReportActionProvider] Failed to clear server-side cache:", e)
172
+ }
173
+
174
+ // 3. ルートローダーの再フェッチ & UI 再描画
175
+ if (revalidator.state === "idle") {
176
+ revalidator.revalidate()
177
+ }
178
+ setVersion((v) => v + 1)
179
+ DailyReportCache.listeners.forEach((listener: (id: number) => void) => {
180
+ listener(-1)
181
+ })
182
+ }, [apiBasePath, revalidator])
183
+
135
184
  /**
136
185
  * Queue for SSE messages that arrived before the target report was cached.
137
186
  * キャッシュ未取得時に到着した SSE メッセージの待機キュー。
@@ -343,7 +392,7 @@ export const DailyReportActionProvider = ({ children, user, initialItems = [], u
343
392
 
344
393
  const updatedReport = {
345
394
  ...cached,
346
- commentItems: [...cached.commentItems, newComment].sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()),
395
+ commentItems: [...cached.commentItems, newComment].sort((a, b) => new Date(a.createdAt || 0).getTime() - new Date(b.createdAt || 0).getTime()),
347
396
  }
348
397
  applyDailyReportServerUpdates(reportHubId, updatedReport)
349
398
  // Update mutation time to prevent stale fetch overwrite
@@ -703,6 +752,7 @@ export const DailyReportActionProvider = ({ children, user, initialItems = [], u
703
752
  createdAt: now,
704
753
  author: getUserDisplayName(user),
705
754
  userId: user?.id ?? "unknown",
755
+ sourceType: "Internal",
706
756
  employeeName: getUserDisplayName(user),
707
757
  updatedBy: getUserDisplayName(user),
708
758
  updatedAt: now,
@@ -903,6 +953,8 @@ export const DailyReportActionProvider = ({ children, user, initialItems = [], u
903
953
  publishReport,
904
954
  deleteReport,
905
955
  removeStaleItem,
956
+ refetchData,
957
+ clearCacheAndRefetch,
906
958
  }}>
907
959
  {children}
908
960
  </DailyReportActionContext.Provider>
@@ -53,7 +53,7 @@ export const useDailyReportSseConnection = ({ isSseEnabled, onMessage }: UseDail
53
53
  const raw = JSON.parse(event.data)
54
54
  const result = dailyReportSseMessageSchema.safeParse(raw)
55
55
  if (!result.success) {
56
- logger.error("SSE Validation Error", result.error.format())
56
+ logger.error("SSE Validation Error:", result.error.flatten(), "raw message:", raw)
57
57
  return
58
58
  }
59
59
  if (result.data.type === "connected") {
@@ -27,7 +27,7 @@ const FETCH_DEBOUNCE_MS = 120
27
27
  // --- Cache Store (Centralized State) ---
28
28
  type CacheEntry<T> = { data: T; expiresAt: number }
29
29
 
30
- const DailyReportCache = {
30
+ export const DailyReportCache = {
31
31
  reports: new Map<number, CacheEntry<DailyReportDetail>>(),
32
32
  lists: new Map<string, CacheEntry<DailyReportDetail[]>>(),
33
33
  pendingLists: new Map<string, Promise<DailyReportDetail[]>>(),
@@ -58,6 +58,23 @@ const DailyReportCache = {
58
58
  }
59
59
  },
60
60
 
61
+ /**
62
+ * Clear all cached entries and pending requests.
63
+ * すべてのキャッシュエントリおよび待機中リクエストをクリアする。
64
+ */
65
+ clear() {
66
+ this.reports.clear()
67
+ this.lists.clear()
68
+ this.pendingLists.clear()
69
+ this.pendingReports.clear()
70
+ this.locks.clear()
71
+ this.lastMutations.clear()
72
+ this.recentDeletes.clear()
73
+ this.listeners.forEach((listener) => {
74
+ listener(-1)
75
+ })
76
+ },
77
+
61
78
  /**
62
79
  * Check if the cache entry is still valid.
63
80
  * キャッシュエントリがまだ有効かを確認する。
@@ -153,11 +170,11 @@ const DailyReportCache = {
153
170
  // Restore missing "my comments"
154
171
  if (cachedReport) {
155
172
  const serverIds = new Set(report.commentItems.map((c) => c.id))
156
- const missing = cachedReport.commentItems.filter((c) => !serverIds.has(c.id) && c.isMine && Date.now() - new Date(c.createdAt).getTime() < 10000)
173
+ const missing = cachedReport.commentItems.filter((c) => !serverIds.has(c.id) && c.isMine && Date.now() - new Date(c.createdAt || 0).getTime() < 10000)
157
174
  if (missing.length > 0) {
158
175
  report = {
159
176
  ...report,
160
- commentItems: [...report.commentItems, ...missing].sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()),
177
+ commentItems: [...report.commentItems, ...missing].sort((a, b) => new Date(a.createdAt || 0).getTime() - new Date(b.createdAt || 0).getTime()),
161
178
  }
162
179
  }
163
180
  }
@@ -218,6 +235,11 @@ export const updateDailyReportCache = (id: number, u: Partial<DailyReportDetail>
218
235
  * キャッシュからレポートを削除する。
219
236
  */
220
237
  export const deleteDailyReportCache = (id: number, d: string) => DailyReportCache.delete(id, d)
238
+ /**
239
+ * Clear all daily report cache store entries.
240
+ * 日報の全キャッシュストアを消去する。
241
+ */
242
+ export const clearDailyReportCache = () => DailyReportCache.clear()
221
243
 
222
244
  export const acquireMutationLock = (id: number) => {
223
245
  DailyReportCache.locks.add(id)
@@ -0,0 +1,47 @@
1
+ import { describe, expect, it, vi } from "vitest"
2
+ import { createEpochStore, SqlResultCache } from "./cache"
3
+
4
+ describe("SqlResultCache Memory Cleanup", () => {
5
+ it("should clean up expired cache entries when map size exceeds 500", async () => {
6
+ // Mock epoch store
7
+ const epochStore = createEpochStore(undefined)
8
+ vi.spyOn(epochStore, "getEpoch").mockResolvedValue(0)
9
+
10
+ const cache = new SqlResultCache({ defaultTtlMs: 1000 }, epochStore)
11
+
12
+ // 1. 期限切れのキャッシュを 500 件作成 (expireAt を過去に設定)
13
+ // 直接プライベートフィールド buckets に流し込むことでモック状態を作る
14
+ const buckets = (cache as any).buckets
15
+ for (let i = 0; i < 505; i++) {
16
+ buckets.set(`expired-key-${i}`, {
17
+ records: [i],
18
+ expireAt: Date.now() - 1000, // 過去時間
19
+ epoch: 0,
20
+ })
21
+ }
22
+
23
+ // 2. 有効なキャッシュを 10 件作成 (expireAt を未来に設定)
24
+ for (let i = 0; i < 10; i++) {
25
+ buckets.set(`valid-key-${i}`, {
26
+ records: [i],
27
+ expireAt: Date.now() + 100000, // 未来時間
28
+ epoch: 0,
29
+ })
30
+ }
31
+
32
+ expect(buckets.size).toBe(515)
33
+
34
+ // 3. getOrFetch を実行して 500 件超えの閾値トリガーを引く
35
+ const fetcher = vi.fn().mockResolvedValue(["new-value"])
36
+ await cache.getOrFetch({
37
+ cacheKey: "trigger-key",
38
+ fetcher,
39
+ })
40
+
41
+ // 4. 検証: 期限切れの 505 件のデータが cleanExpired で削除され、有効な 10 件 + 新規の 1 件のみ残っていること
42
+ expect(buckets.size).toBe(11) // 10 (valid) + 1 (trigger-key)
43
+ expect(buckets.has("trigger-key")).toBe(true)
44
+ expect(buckets.has("valid-key-0")).toBe(true)
45
+ expect(buckets.has("expired-key-0")).toBe(false)
46
+ })
47
+ })
@@ -123,6 +123,10 @@ export class SqlResultCache {
123
123
  // キャッシュ保存時に現在の epoch を記録
124
124
  const epoch = opts.epochKey ? await this.epochStore.getEpoch(opts.epochKey) : 0
125
125
  this.buckets.set(cacheKey, { records, expireAt, epoch })
126
+ // 蓄積防止のため、サイズが一定値を超えたら期限切れキャッシュを一括クリーンアップ
127
+ if (this.buckets.size > 500) {
128
+ this.cleanExpired()
129
+ }
126
130
  }
127
131
  return records
128
132
  })()
@@ -146,6 +150,15 @@ export class SqlResultCache {
146
150
  this.inFlight.delete(cacheKey)
147
151
  }
148
152
 
153
+ /**
154
+ * Clears all cached buckets and in-flight requests.
155
+ * すべてのキャッシュバケットと進行中のリクエストを全クリアします。
156
+ */
157
+ clearAll = (): void => {
158
+ this.buckets.clear()
159
+ this.inFlight.clear()
160
+ }
161
+
149
162
  /**
150
163
  * Invalidates cache buckets matching a prefix.
151
164
  * 指定されたプレフィックスに一致するキャッシュバケットを無効化します。
@@ -162,4 +175,18 @@ export class SqlResultCache {
162
175
  }
163
176
  }
164
177
  }
178
+
179
+ /**
180
+ * Cleans up all expired cache buckets to prevent memory accumulation.
181
+ * メモリー蓄積を防ぐため、期限切れのキャッシュバケットをすべてクリーンアップする処理。
182
+ */
183
+ private cleanExpired(): void {
184
+ const now = Date.now()
185
+ // 期限切れのキーを削除
186
+ for (const [key, bucket] of this.buckets.entries()) {
187
+ if (bucket.expireAt <= now) {
188
+ this.buckets.delete(key)
189
+ }
190
+ }
191
+ }
165
192
  }
@@ -317,6 +317,10 @@ export function createDailyReportHandlers(config: DailyReportHandlersConfig) {
317
317
  throw e
318
318
  }
319
319
  }
320
+ case "clearCache": {
321
+ await service.clearCache()
322
+ return jsonData({ status: "OK", intent: "clearCache" })
323
+ }
320
324
  default:
321
325
  return jsonData({ error: "Invalid intent" }, { status: 400 })
322
326
  }
@@ -326,6 +326,7 @@ export function createDailyReportService(config: DailyReportServiceConfig) {
326
326
  date: formatDateValue(hub.businessDate, "YYYY-MM-DD"),
327
327
  author: row.creatorName ?? maskAuditActor(hub.createdBy) ?? "",
328
328
  userId: hub.userId ? encodeUserId(hub.userId) : "",
329
+ sourceType: hub.sourceType ?? "Internal",
329
330
  createdAt: formatDateValue(hub.createdAt, "YYYY-MM-DD HH:mm:ss"),
330
331
  updatedAt: formatDateValue(hub.updatedAt, "YYYY-MM-DD HH:mm:ss"),
331
332
  updatedBy: maskAuditActor(hub.updatedBy),
@@ -351,7 +352,6 @@ export function createDailyReportService(config: DailyReportServiceConfig) {
351
352
  userId: encodeUserId(c.userId),
352
353
  isMine: currentUserId ? c.userId === currentUserId : false,
353
354
  })),
354
- sourceType: hub.sourceType,
355
355
  }
356
356
  }
357
357
 
@@ -1036,6 +1036,7 @@ export function createDailyReportService(config: DailyReportServiceConfig) {
1036
1036
  createdAt: formatDateValue(hub.createdAt, "YYYY-MM-DD HH:mm:ss"),
1037
1037
  author: userName,
1038
1038
  userId: encodeUserId(userId),
1039
+ sourceType: "Internal",
1039
1040
  employeeName: userName,
1040
1041
  // 監査列と同様、クライアント公開時は生内部 ID を難読化する (mapHubRecord と整合)
1041
1042
  updatedBy: encodeUserId(userId),
@@ -1279,10 +1280,26 @@ export function createDailyReportService(config: DailyReportServiceConfig) {
1279
1280
  return fullDetail ?? null
1280
1281
  }
1281
1282
 
1283
+ /**
1284
+ * Clears all server-side SQL result and user ID caches.
1285
+ * サーバー側のすべての SQL 結果キャッシュおよびユーザー ID キャッシュを全消去する。
1286
+ */
1287
+ const clearCache = async (): Promise<void> => {
1288
+ sqlResultCache.clearAll()
1289
+ userIdCache.clear()
1290
+ cachedDraftLabelId = null
1291
+ if (epochs) {
1292
+ await incrementRedisEpoch(DAILY_REPORT_IDS_EPOCH_KEY)
1293
+ }
1294
+ logger.info("[DailyReportService] Server-side DB/SQL caches cleared successfully.")
1295
+ }
1296
+
1282
1297
  return {
1283
1298
  // 定数 (アプリ側の互換 export 用)
1284
1299
  streamKey: DAILY_REPORT_SSE_STREAM_KEY,
1285
1300
  streamMaxLen: DAILY_REPORT_SSE_STREAM_MAXLEN,
1301
+ // キャッシュクリア
1302
+ clearCache,
1286
1303
  // ユーザー解決
1287
1304
  getUserIdByExternalId,
1288
1305
  // 参照系
@@ -32,9 +32,9 @@ export const mergeComments = (legacyComments: DailyReportComment[], modernCommen
32
32
  // 2. Modern コメントの変換
33
33
  const modernUIComments: UIComment[] = modernComments.map((c) => ({
34
34
  id: c.id,
35
- authorName: c.userName,
36
- content: c.content,
37
- createdAt: c.createdAt,
35
+ authorName: c.userName ?? "",
36
+ content: c.content ?? "",
37
+ createdAt: c.createdAt ?? "",
38
38
  isMine: currentUserId ? c.userId === currentUserId : c.isMine,
39
39
  isLegacy: false,
40
40
  }))
@@ -8,48 +8,49 @@ import type { DailyReportCommentItem as DeclaredComment, DailyReportDetail as De
8
8
  // --- サブスキーマ(types.ts の型と構造的に一致させる) ---
9
9
 
10
10
  export const dailyReportInterviewerSchema = z.object({
11
- name: z.string(),
12
- affiliation: z.string().nullable(),
11
+ name: z.string().nullish(),
12
+ affiliation: z.string().nullish(),
13
13
  })
14
14
 
15
15
  export const dailyReportCommentSchema = z.object({
16
- name: z.string().nullable(),
17
- text: z.string().nullable(),
18
- color: z.string().nullable(),
16
+ name: z.string().nullish(),
17
+ text: z.string().nullish(),
18
+ color: z.string().nullish(),
19
19
  })
20
20
 
21
21
  export const dailyReportLabelDefSchema = z.object({
22
22
  id: z.number(),
23
- name: z.string(),
24
- color: z.string().nullable(),
23
+ name: z.string().nullish(),
24
+ color: z.string().nullish(),
25
25
  })
26
26
 
27
27
  export const dailyReportCommentItemSchema = z.object({
28
28
  id: z.number(),
29
- userId: z.string(),
30
- userName: z.string(),
31
- content: z.string(),
32
- createdAt: z.string(),
29
+ userId: z.string().nullish(),
30
+ userName: z.string().nullish(),
31
+ content: z.string().nullish(),
32
+ createdAt: z.string().nullish(),
33
33
  isMine: z.boolean(),
34
34
  })
35
35
 
36
36
  export const dailyReportDetailSchema = z.object({
37
37
  reportHubId: z.number(),
38
- date: z.string().nullable(),
39
- createdAt: z.string().nullable(),
40
- author: z.string(),
41
- userId: z.string(),
42
- employeeName: z.string().nullable(),
43
- updatedBy: z.string().nullable(),
44
- updatedAt: z.string().nullable(),
45
- category: z.string().nullable(),
46
- creationCategory: z.string().nullable(),
47
- visitTimeFrom: z.string().nullable(),
48
- visitTimeTo: z.string().nullable(),
49
- customerName: z.string().nullable(),
38
+ date: z.string().nullish(),
39
+ createdAt: z.string().nullish(),
40
+ author: z.string().nullish(),
41
+ userId: z.string().nullish(),
42
+ sourceType: z.string().nullish(),
43
+ employeeName: z.string().nullish(),
44
+ updatedBy: z.string().nullish(),
45
+ updatedAt: z.string().nullish(),
46
+ category: z.string().nullish(),
47
+ creationCategory: z.string().nullish(),
48
+ visitTimeFrom: z.string().nullish(),
49
+ visitTimeTo: z.string().nullish(),
50
+ customerName: z.string().nullish(),
50
51
  interviewers: z.array(dailyReportInterviewerSchema),
51
- subject: z.string().nullable(),
52
- content: z.string().nullable(),
52
+ subject: z.string().nullish(),
53
+ content: z.string().nullish(),
53
54
  comments: z.array(dailyReportCommentSchema),
54
55
  isRead: z.boolean(),
55
56
  isStarred: z.boolean(),
@@ -110,6 +111,7 @@ export const reportPublishMessageSchema = z.object({
110
111
  reportHubId: z.number(),
111
112
  report: dailyReportDetailSchema,
112
113
  clientTempId: z.string(),
114
+ recipientRawUserId: z.number().optional(),
113
115
  })
114
116
 
115
117
  export const reportDeleteMessageSchema = z.object({
@@ -12,58 +12,58 @@ export type DailyReportItem = {
12
12
 
13
13
  /** 面談者情報。 */
14
14
  export type DailyReportInterviewer = {
15
- name: string
16
- affiliation: string | null
15
+ name?: string | null
16
+ affiliation?: string | null
17
17
  }
18
18
 
19
19
  /** レガシー形式 (JSON 埋め込み) のコメント。 */
20
20
  export type DailyReportComment = {
21
- name: string | null
22
- text: string | null
23
- color: string | null
21
+ name?: string | null
22
+ text?: string | null
23
+ color?: string | null
24
24
  }
25
25
 
26
26
  /** ラベル定義。 */
27
27
  export type DailyReportLabelDef = {
28
28
  id: number
29
- name: string
30
- color: string | null
29
+ name?: string | null
30
+ color?: string | null
31
31
  }
32
32
 
33
33
  /** リレーショナルコメント 1 件。`userId` は難読化済み文字列。 */
34
34
  export type DailyReportCommentItem = {
35
35
  id: number
36
- userId: string
37
- userName: string
38
- content: string
39
- createdAt: string
36
+ userId?: string | null
37
+ userName?: string | null
38
+ content?: string | null
39
+ createdAt?: string | null
40
40
  isMine: boolean
41
41
  }
42
42
 
43
43
  /** 日報詳細 (一覧アイテムの遅延解決結果)。`userId` は難読化済み文字列。 */
44
44
  export type DailyReportDetail = {
45
45
  reportHubId: number
46
- date: string | null
47
- createdAt: string | null
48
- author: string
49
- userId: string
50
- employeeName: string | null
51
- updatedBy: string | null
52
- updatedAt: string | null
53
- category: string | null
54
- creationCategory: string | null
55
- visitTimeFrom: string | null
56
- visitTimeTo: string | null
57
- customerName: string | null
46
+ date?: string | null
47
+ createdAt?: string | null
48
+ author?: string | null
49
+ userId?: string | null
50
+ sourceType?: string | null
51
+ employeeName?: string | null
52
+ updatedBy?: string | null
53
+ updatedAt?: string | null
54
+ category?: string | null
55
+ creationCategory?: string | null
56
+ visitTimeFrom?: string | null
57
+ visitTimeTo?: string | null
58
+ customerName?: string | null
58
59
  interviewers: DailyReportInterviewer[]
59
- subject: string | null
60
- content: string | null
60
+ subject?: string | null
61
+ content?: string | null
61
62
  comments: DailyReportComment[]
62
63
  isRead: boolean
63
64
  isStarred: boolean
64
65
  labels: DailyReportLabelDef[]
65
66
  commentItems: DailyReportCommentItem[]
66
- sourceType?: string
67
67
  }
68
68
 
69
69
  /**