@aiquants/daily-report 0.5.0 → 0.6.1

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 (42) hide show
  1. package/README.md +38 -38
  2. package/dist/client.d.mts +11 -5
  3. package/dist/client.d.ts +11 -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 +35 -5
  15. package/dist/server.d.ts +35 -5
  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/{types-BNhYC2j9.d.mts → types-D1PKubyo.d.mts} +26 -26
  23. package/dist/{types-BNhYC2j9.d.ts → types-D1PKubyo.d.ts} +26 -26
  24. package/package.json +4 -4
  25. package/src/client/components/daily-report-detail-list.tsx +29 -7
  26. package/src/client/components/daily-report-list.tsx +26 -5
  27. package/src/client/components/daily-report-resolved-content.tsx +28 -4
  28. package/src/client/config-context.tsx +2 -0
  29. package/src/client/contexts/daily-report-action-context.tsx +6 -2
  30. package/src/client/hooks/use-daily-report-sse-connection.ts +1 -1
  31. package/src/client/hooks/use-daily-report.ts +2 -2
  32. package/src/client/route-helpers.ts +8 -2
  33. package/src/server/authz.spec.ts +57 -0
  34. package/src/server/authz.ts +99 -0
  35. package/src/server/handlers.ts +5 -4
  36. package/src/server/service.ts +31 -13
  37. package/src/server.ts +1 -0
  38. package/src/shared/comment-adapter.ts +3 -3
  39. package/src/shared/sse-schema.ts +27 -26
  40. package/src/shared/types.ts +26 -26
  41. package/dist/sse-schema-ChCypDPj.d.ts +0 -2019
  42. package/dist/sse-schema-Cs1oC2DP.d.mts +0 -2019
@@ -10,49 +10,49 @@ type DailyReportItem = {
10
10
  };
11
11
  /** 面談者情報。 */
12
12
  type DailyReportInterviewer = {
13
- name: string;
14
- affiliation: string | null;
13
+ name?: string | null;
14
+ affiliation?: string | null;
15
15
  };
16
16
  /** レガシー形式 (JSON 埋め込み) のコメント。 */
17
17
  type DailyReportComment = {
18
- name: string | null;
19
- text: string | null;
20
- color: string | null;
18
+ name?: string | null;
19
+ text?: string | null;
20
+ color?: string | null;
21
21
  };
22
22
  /** ラベル定義。 */
23
23
  type DailyReportLabelDef = {
24
24
  id: number;
25
- name: string;
26
- color: string | null;
25
+ name?: string | null;
26
+ color?: string | null;
27
27
  };
28
28
  /** リレーショナルコメント 1 件。`userId` は難読化済み文字列。 */
29
29
  type DailyReportCommentItem = {
30
30
  id: number;
31
- userId: string;
32
- userName: string;
33
- content: string;
34
- createdAt: string;
31
+ userId?: string | null;
32
+ userName?: string | null;
33
+ content?: string | null;
34
+ createdAt?: string | null;
35
35
  isMine: boolean;
36
36
  };
37
37
  /** 日報詳細 (一覧アイテムの遅延解決結果)。`userId` は難読化済み文字列。 */
38
38
  type DailyReportDetail = {
39
39
  reportHubId: number;
40
- date: string | null;
41
- createdAt: string | null;
42
- author: string;
43
- userId: string;
44
- sourceType: string | null;
45
- employeeName: string | null;
46
- updatedBy: string | null;
47
- updatedAt: string | null;
48
- category: string | null;
49
- creationCategory: string | null;
50
- visitTimeFrom: string | null;
51
- visitTimeTo: string | null;
52
- customerName: string | null;
40
+ date?: string | null;
41
+ createdAt?: string | null;
42
+ author?: string | null;
43
+ userId?: string | null;
44
+ sourceType?: string | null;
45
+ employeeName?: string | null;
46
+ updatedBy?: string | null;
47
+ updatedAt?: string | null;
48
+ category?: string | null;
49
+ creationCategory?: string | null;
50
+ visitTimeFrom?: string | null;
51
+ visitTimeTo?: string | null;
52
+ customerName?: string | null;
53
53
  interviewers: DailyReportInterviewer[];
54
- subject: string | null;
55
- content: string | null;
54
+ subject?: string | null;
55
+ content?: string | null;
56
56
  comments: DailyReportComment[];
57
57
  isRead: boolean;
58
58
  isStarred: boolean;
@@ -10,49 +10,49 @@ type DailyReportItem = {
10
10
  };
11
11
  /** 面談者情報。 */
12
12
  type DailyReportInterviewer = {
13
- name: string;
14
- affiliation: string | null;
13
+ name?: string | null;
14
+ affiliation?: string | null;
15
15
  };
16
16
  /** レガシー形式 (JSON 埋め込み) のコメント。 */
17
17
  type DailyReportComment = {
18
- name: string | null;
19
- text: string | null;
20
- color: string | null;
18
+ name?: string | null;
19
+ text?: string | null;
20
+ color?: string | null;
21
21
  };
22
22
  /** ラベル定義。 */
23
23
  type DailyReportLabelDef = {
24
24
  id: number;
25
- name: string;
26
- color: string | null;
25
+ name?: string | null;
26
+ color?: string | null;
27
27
  };
28
28
  /** リレーショナルコメント 1 件。`userId` は難読化済み文字列。 */
29
29
  type DailyReportCommentItem = {
30
30
  id: number;
31
- userId: string;
32
- userName: string;
33
- content: string;
34
- createdAt: string;
31
+ userId?: string | null;
32
+ userName?: string | null;
33
+ content?: string | null;
34
+ createdAt?: string | null;
35
35
  isMine: boolean;
36
36
  };
37
37
  /** 日報詳細 (一覧アイテムの遅延解決結果)。`userId` は難読化済み文字列。 */
38
38
  type DailyReportDetail = {
39
39
  reportHubId: number;
40
- date: string | null;
41
- createdAt: string | null;
42
- author: string;
43
- userId: string;
44
- sourceType: string | null;
45
- employeeName: string | null;
46
- updatedBy: string | null;
47
- updatedAt: string | null;
48
- category: string | null;
49
- creationCategory: string | null;
50
- visitTimeFrom: string | null;
51
- visitTimeTo: string | null;
52
- customerName: string | null;
40
+ date?: string | null;
41
+ createdAt?: string | null;
42
+ author?: string | null;
43
+ userId?: string | null;
44
+ sourceType?: string | null;
45
+ employeeName?: string | null;
46
+ updatedBy?: string | null;
47
+ updatedAt?: string | null;
48
+ category?: string | null;
49
+ creationCategory?: string | null;
50
+ visitTimeFrom?: string | null;
51
+ visitTimeTo?: string | null;
52
+ customerName?: string | null;
53
53
  interviewers: DailyReportInterviewer[];
54
- subject: string | null;
55
- content: string | null;
54
+ subject?: string | null;
55
+ content?: string | null;
56
56
  comments: DailyReportComment[];
57
57
  isRead: boolean;
58
58
  isStarred: boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiquants/daily-report",
3
- "version": "0.5.0",
3
+ "version": "0.6.1",
4
4
  "description": "Reusable daily-report feature package: shared types/schemas, React (Router v7) UI with virtual scrolling + optimistic updates + SSE sync, and a drizzle (mssql) server layer with DI ports (auth, user resolution, redis, external sources).",
5
5
  "sideEffects": [
6
6
  "**/*.css"
@@ -72,8 +72,8 @@
72
72
  "typescript": "^5.9.3",
73
73
  "vitest": "^4.1.8",
74
74
  "zod": "^3.25.76",
75
- "@aiquants/virtualscroll": "1.19.1",
76
- "@aiquants/swipe-overlay": "1.2.4"
75
+ "@aiquants/swipe-overlay": "1.2.4",
76
+ "@aiquants/virtualscroll": "1.19.2"
77
77
  },
78
78
  "peerDependencies": {
79
79
  "drizzle-orm": ">=1.0.0-beta.4 <2.0.0",
@@ -82,7 +82,7 @@
82
82
  "react-router": "^7.0.0",
83
83
  "zod": ">=3.25.0 <5.0.0",
84
84
  "@aiquants/swipe-overlay": "^1.2.4",
85
- "@aiquants/virtualscroll": "^1.19.1"
85
+ "@aiquants/virtualscroll": "^1.19.2"
86
86
  },
87
87
  "peerDependenciesMeta": {
88
88
  "drizzle-orm": {
@@ -70,7 +70,7 @@ const formatInterviewerLabel = (interviewer: DailyReportDetail["interviewers"][n
70
70
  if (interviewer.affiliation?.trim()) {
71
71
  return `${interviewer.name}(${interviewer.affiliation})`
72
72
  }
73
- return interviewer.name
73
+ return interviewer.name ?? ""
74
74
  }
75
75
 
76
76
  type DailyReportDetailListProps = {
@@ -78,13 +78,15 @@ type DailyReportDetailListProps = {
78
78
  userId?: string | null
79
79
  selectedItemId?: number | null
80
80
  onSelectItem?: (id: number | null) => void
81
+ initialScrollOffset?: number
82
+ onScrollOffsetChange?: (offset: number) => void
81
83
  }
82
84
 
83
85
  /**
84
86
  * Daily report detail cards rendered with dynamic height virtual scroll.
85
87
  * 動的高さの仮想スクロールで日報詳細カードを描画するコンポーネント。
86
88
  */
87
- export const DailyReportDetailList = ({ dailyReportItems, userId, selectedItemId, onSelectItem }: DailyReportDetailListProps) => {
89
+ export const DailyReportDetailList = ({ dailyReportItems, userId, selectedItemId, onSelectItem, initialScrollOffset, onScrollOffsetChange }: DailyReportDetailListProps) => {
88
90
  const [itemHeights, setItemHeights] = useState<Map<number, number>>(new Map())
89
91
  const scrollContainerRef = useRef<HTMLDivElement | null>(null)
90
92
  const virtualScrollRef = useRef<VirtualScrollHandle>(null)
@@ -182,6 +184,10 @@ export const DailyReportDetailList = ({ dailyReportItems, userId, selectedItemId
182
184
  )
183
185
 
184
186
  const handleScroll = useCallback(() => {
187
+ if (virtualScrollRef.current && onScrollOffsetChange) {
188
+ onScrollOffsetChange(virtualScrollRef.current.getScrollPosition())
189
+ }
190
+
185
191
  const container = scrollContainerRef.current
186
192
  if (!container) return
187
193
 
@@ -199,7 +205,15 @@ export const DailyReportDetailList = ({ dailyReportItems, userId, selectedItemId
199
205
  }
200
206
  scrollEndTimerRef.current = null
201
207
  }, 150)
202
- }, [])
208
+ }, [onScrollOffsetChange])
209
+
210
+ useEffect(() => {
211
+ return () => {
212
+ if (virtualScrollRef.current && onScrollOffsetChange) {
213
+ onScrollOffsetChange(virtualScrollRef.current.getScrollPosition())
214
+ }
215
+ }
216
+ }, [onScrollOffsetChange])
203
217
 
204
218
  const renderThumbOverlay = useCallback((props: ScrollBarThumbOverlayRenderProps) => <BusinessDayThumbOverlay {...props} label={overlayLabel} isWheelScrollActive={isWheelScrollActive} />, [overlayLabel, isWheelScrollActive])
205
219
 
@@ -294,6 +308,7 @@ export const DailyReportDetailList = ({ dailyReportItems, userId, selectedItemId
294
308
  getItemKey={getItemKey}
295
309
  getItemHeight={getItemHeight}
296
310
  viewportSize={viewportHeight}
311
+ initialScrollOffset={initialScrollOffset}
297
312
  overscanCount={VIRTUAL_SCROLL_OVERSCAN_COUNT}
298
313
  scrollBarOptions={{
299
314
  width: 8,
@@ -563,7 +578,12 @@ const DailyReportResolvedItem = ({ item, report, onCommentAdded, userId, onDoubl
563
578
  // 表示中ユーザーはアクションコンテキストから取得する (root loader 非依存)
564
579
  const { toggleStar, toggleRead, pendingStarUpdates, pendingReadUpdates, deleteReport, user } = useDailyReportActionContext()
565
580
  const { uiComments, handleAddComment, handleDeleteComment, pendingDeleteId, setPendingDeleteId, resolvedIdMap } = useDailyReportComments(report, user, userId)
566
- const { fieldLabels } = useDailyReportConfig()
581
+ const { fieldLabels, sourceTypeConfigs } = useDailyReportConfig()
582
+
583
+ const sourceType = item.sourceType ?? report?.sourceType
584
+ const sourceConfig = resolveSourceTypeConfig(sourceType, sourceTypeConfigs)
585
+ const isExternalSource = sourceType ? sourceType.toLowerCase() !== "internal" : false
586
+ const canComment = sourceConfig?.allowComment ?? !isExternalSource
567
587
 
568
588
  const handleAddCommentWrapper = async (content: string) => {
569
589
  await handleAddComment(content)
@@ -667,9 +687,11 @@ const DailyReportResolvedItem = ({ item, report, onCommentAdded, userId, onDoubl
667
687
  />
668
688
  </div>
669
689
  )}
670
- <div className="mt-auto" ref={formContainerRef}>
671
- <DailyReportCommentForm reportHubId={report.reportHubId} businessDate={report.date ?? ""} onAddComment={handleAddCommentWrapper} />
672
- </div>
690
+ {canComment && (
691
+ <div className="mt-auto" ref={formContainerRef}>
692
+ <DailyReportCommentForm reportHubId={report.reportHubId} businessDate={report.date ?? ""} onAddComment={handleAddCommentWrapper} />
693
+ </div>
694
+ )}
673
695
  </div>
674
696
  <MetadataPanel report={report} visitTimeDisplay={visitTimeDisplay} categoryLabel={categoryLabel} fieldLabels={fieldLabels} />
675
697
  </div>
@@ -47,6 +47,8 @@ type DailyReportListProps = {
47
47
  selectedReportHubId: number | null
48
48
  onSelectItem: (reportHubId: number | null) => void
49
49
  userId?: string | null
50
+ initialScrollOffset?: number
51
+ onScrollOffsetChange?: (offset: number) => void
50
52
  }
51
53
 
52
54
  /**
@@ -228,7 +230,7 @@ const useAutoRead = (report: DailyReportDetail | null, isLoading: boolean, autoM
228
230
  * Daily report list cards with key metadata and preview content via virtual scrolling.
229
231
  * 主なメタデータと内容プレビューを仮想スクロール表示する日報リスト。
230
232
  */
231
- export const DailyReportList = ({ dailyReportItems, initialSelectedBusinessDate = null, initialSelectedReportHubId = null, autoMarkRead = false, selectedReportHubId, onSelectItem, userId }: DailyReportListProps) => {
233
+ export const DailyReportList = ({ dailyReportItems, initialSelectedBusinessDate = null, initialSelectedReportHubId = null, autoMarkRead = false, selectedReportHubId, onSelectItem, userId, initialScrollOffset, onScrollOffsetChange }: DailyReportListProps) => {
232
234
  const scrollContainerRef = useRef<HTMLDivElement | null>(null)
233
235
  const virtualScrollRef = useRef<VirtualScrollHandle>(null)
234
236
  const { viewportHeight, updateViewportHeight } = useDynamicViewportHeight(scrollContainerRef)
@@ -400,6 +402,10 @@ export const DailyReportList = ({ dailyReportItems, initialSelectedBusinessDate
400
402
  )
401
403
 
402
404
  const handleScroll = useCallback(() => {
405
+ if (virtualScrollRef.current && onScrollOffsetChange) {
406
+ onScrollOffsetChange(virtualScrollRef.current.getScrollPosition())
407
+ }
408
+
403
409
  const container = scrollContainerRef.current
404
410
  if (!container) return
405
411
 
@@ -417,7 +423,15 @@ export const DailyReportList = ({ dailyReportItems, initialSelectedBusinessDate
417
423
  }
418
424
  scrollEndTimerRef.current = null
419
425
  }, 150)
420
- }, [])
426
+ }, [onScrollOffsetChange])
427
+
428
+ useEffect(() => {
429
+ return () => {
430
+ if (virtualScrollRef.current && onScrollOffsetChange) {
431
+ onScrollOffsetChange(virtualScrollRef.current.getScrollPosition())
432
+ }
433
+ }
434
+ }, [onScrollOffsetChange])
421
435
 
422
436
  const renderThumbOverlay = useCallback((props: ScrollBarThumbOverlayRenderProps) => <BusinessDayThumbOverlay {...props} label={overlayLabel} isWheelScrollActive={isWheelScrollActive} />, [overlayLabel, isWheelScrollActive])
423
437
 
@@ -431,6 +445,7 @@ export const DailyReportList = ({ dailyReportItems, initialSelectedBusinessDate
431
445
  getItem={getItem}
432
446
  getItemHeight={getItemHeight}
433
447
  viewportSize={viewportHeight}
448
+ initialScrollOffset={initialScrollOffset}
434
449
  overscanCount={VIRTUAL_SCROLL_OVERSCAN_COUNT}
435
450
  scrollBarOptions={{
436
451
  width: SCROLL_BAR_WIDTH,
@@ -648,6 +663,7 @@ const DailyReportDisplayPane = ({
648
663
  report,
649
664
  isLoading,
650
665
  error,
666
+ selectedItem,
651
667
  autoMarkRead = false,
652
668
  isActive = true,
653
669
  onCommentAdded,
@@ -666,12 +682,17 @@ const DailyReportDisplayPane = ({
666
682
  }) => {
667
683
  // 表示中ユーザーはアクションコンテキストから取得する (root loader 非依存)
668
684
  const { toggleStar, toggleRead, deleteReport, user } = useDailyReportActionContext()
669
- const { fieldLabels } = useDailyReportConfig()
685
+ const { fieldLabels, sourceTypeConfigs } = useDailyReportConfig()
670
686
 
671
687
  // Use custom hook for comments
672
688
  const { uiComments, handleAddComment, handleDeleteComment, pendingDeleteId, setPendingDeleteId, resolvedIdMap } = useDailyReportComments(report, user, userId)
673
689
  const { handleToggleRead } = useAutoRead(report, isLoading, autoMarkRead, isActive, toggleRead)
674
690
 
691
+ const sourceType = selectedItem.sourceType ?? report?.sourceType
692
+ const sourceConfig = resolveSourceTypeConfig(sourceType, sourceTypeConfigs)
693
+ const isExternalSource = sourceType ? sourceType.toLowerCase() !== "internal" : false
694
+ const canComment = sourceConfig?.allowComment ?? !isExternalSource
695
+
675
696
  // Auto-scroll to bottom when comments are added (e.g. via SSE)
676
697
  const prevReportHubIdRef = useRef<number | null>(null)
677
698
  const prevCommentsLengthRef = useRef(uiComments.length)
@@ -804,7 +825,7 @@ const DailyReportDisplayPane = ({
804
825
  resolvedIdMap={resolvedIdMap}
805
826
  />
806
827
  )}
807
- <DailyReportCommentForm reportHubId={report.reportHubId} businessDate={report.date ?? ""} onAddComment={handleAddCommentWrapper} />
828
+ {canComment && <DailyReportCommentForm reportHubId={report.reportHubId} businessDate={report.date ?? ""} onAddComment={handleAddCommentWrapper} />}
808
829
  </div>
809
830
  )}
810
831
  </div>
@@ -953,7 +974,7 @@ const ListReportItem = ({ item, isActive, onSelect }: { item: DailyReportItem; i
953
974
  )
954
975
  }
955
976
 
956
- const previewLines = extractPreviewLines(report.content).map((line, i) => ({
977
+ const previewLines = extractPreviewLines(report.content ?? null).map((line, i) => ({
957
978
  line,
958
979
  key: `${item.reportHubId}-line-${i}`,
959
980
  }))
@@ -1,6 +1,6 @@
1
1
  import { Plus, RefreshCw, RotateCw } from "lucide-react"
2
2
  import type { ReactNode } from "react"
3
- import { useEffect, useMemo, useState } from "react"
3
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react"
4
4
  import { useSearchParams } from "react-router"
5
5
  import type { DailyReportItem } from "../../shared/types"
6
6
  import { useDailyReportConfig } from "../config-context"
@@ -31,6 +31,18 @@ export const DailyReportResolvedContent = ({ dailyReportItems: _initialItems, us
31
31
  const { isSseEnabled, toggleSse, createReport, items: dailyReportItems, refetchData, clearCacheAndRefetch } = useDailyReportActionContext()
32
32
  const [searchParams] = useSearchParams()
33
33
 
34
+ // 各タブのスクロール位置をアンマウント後も保持する Ref
35
+ const listScrollOffsetRef = useRef<number>(0)
36
+ const detailListScrollOffsetRef = useRef<number>(0)
37
+
38
+ const handleListScrollOffsetChange = useCallback((offset: number) => {
39
+ listScrollOffsetRef.current = offset
40
+ }, [])
41
+
42
+ const handleDetailListScrollOffsetChange = useCallback((offset: number) => {
43
+ detailListScrollOffsetRef.current = offset
44
+ }, [])
45
+
34
46
  // 選択状態の管理 (タブ間で共有)
35
47
  const [selectedItemId, setSelectedItemId] = useState<number | null>(() => {
36
48
  if (dailyReportItems.length === 0) return null
@@ -78,7 +90,17 @@ export const DailyReportResolvedContent = ({ dailyReportItems: _initialItems, us
78
90
  {
79
91
  value: "list",
80
92
  label: "📄 List",
81
- content: <DailyReportList dailyReportItems={dailyReportItems} autoMarkRead={autoMarkRead} selectedReportHubId={selectedItemId} onSelectItem={setSelectedItemId} userId={userId} />,
93
+ content: (
94
+ <DailyReportList
95
+ dailyReportItems={dailyReportItems}
96
+ autoMarkRead={autoMarkRead}
97
+ selectedReportHubId={selectedItemId}
98
+ onSelectItem={setSelectedItemId}
99
+ userId={userId}
100
+ initialScrollOffset={listScrollOffsetRef.current}
101
+ onScrollOffsetChange={handleListScrollOffsetChange}
102
+ />
103
+ ),
82
104
  },
83
105
  {
84
106
  value: "tree",
@@ -88,10 +110,12 @@ export const DailyReportResolvedContent = ({ dailyReportItems: _initialItems, us
88
110
  {
89
111
  value: "detailList",
90
112
  label: "📋 DetailList",
91
- content: <DailyReportDetailList dailyReportItems={dailyReportItems} userId={userId} selectedItemId={selectedItemId} onSelectItem={setSelectedItemId} />,
113
+ content: (
114
+ <DailyReportDetailList dailyReportItems={dailyReportItems} userId={userId} selectedItemId={selectedItemId} onSelectItem={setSelectedItemId} initialScrollOffset={detailListScrollOffsetRef.current} onScrollOffsetChange={handleDetailListScrollOffsetChange} />
115
+ ),
92
116
  },
93
117
  ],
94
- [dailyReportItems, autoMarkRead, selectedItemId, userId],
118
+ [dailyReportItems, autoMarkRead, selectedItemId, userId, handleListScrollOffsetChange, handleDetailListScrollOffsetChange],
95
119
  )
96
120
 
97
121
  return (
@@ -42,6 +42,8 @@ export type DailyReportFieldLabels = {
42
42
  export type SourceTypeConfig = {
43
43
  label: string
44
44
  badgeClassName?: string
45
+ /** コメント投稿を許可するかどうか (既定 true)。false の場合、該当ソースの日報でコメント投稿フォームを非表示にする。 */
46
+ allowComment?: boolean
45
47
  }
46
48
 
47
49
  /** クライアント層の設定一式。 */
@@ -163,10 +163,14 @@ export const DailyReportActionProvider = ({ children, user, initialItems = [], u
163
163
  try {
164
164
  const formData = new FormData()
165
165
  formData.append("intent", "clearCache")
166
- await fetch(`${apiBasePath}/action`, {
166
+ const res = await fetch(`${apiBasePath}/action`, {
167
167
  method: "POST",
168
168
  body: formData,
169
169
  })
170
+ if (!res.ok) {
171
+ const errData = await res.json().catch(() => null)
172
+ logger.warn("[DailyReportActionProvider] Failed to clear server-side cache:", res.status, errData)
173
+ }
170
174
  } catch (e) {
171
175
  logger.warn("[DailyReportActionProvider] Failed to clear server-side cache:", e)
172
176
  }
@@ -392,7 +396,7 @@ export const DailyReportActionProvider = ({ children, user, initialItems = [], u
392
396
 
393
397
  const updatedReport = {
394
398
  ...cached,
395
- commentItems: [...cached.commentItems, newComment].sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()),
399
+ commentItems: [...cached.commentItems, newComment].sort((a, b) => new Date(a.createdAt || 0).getTime() - new Date(b.createdAt || 0).getTime()),
396
400
  }
397
401
  applyDailyReportServerUpdates(reportHubId, updatedReport)
398
402
  // Update mutation time to prevent stale fetch overwrite
@@ -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") {
@@ -170,11 +170,11 @@ export const DailyReportCache = {
170
170
  // Restore missing "my comments"
171
171
  if (cachedReport) {
172
172
  const serverIds = new Set(report.commentItems.map((c) => c.id))
173
- 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)
174
174
  if (missing.length > 0) {
175
175
  report = {
176
176
  ...report,
177
- 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()),
178
178
  }
179
179
  }
180
180
  }
@@ -35,8 +35,13 @@ export const dailyReportShouldRevalidate: ShouldRevalidateFunction = ({ actionRe
35
35
  * Fetches the deferred daily report id list from the API endpoint.
36
36
  * API エンドポイントから日報 ID 一覧を遅延取得する処理。
37
37
  */
38
- export const fetchDailyReportIds = (apiBasePath: string = defaultDailyReportClientConfig.apiBasePath): Promise<DailyReportItem[]> =>
39
- fetch(`${apiBasePath}/ids`, {
38
+ export const fetchDailyReportIds = (apiBasePath: string = defaultDailyReportClientConfig.apiBasePath, forceRefresh = false): Promise<DailyReportItem[]> => {
39
+ const url = new URL(`${apiBasePath}/ids`, typeof window !== "undefined" ? window.location.origin : "http://localhost")
40
+ if (forceRefresh) {
41
+ url.searchParams.set("forceRefresh", "true")
42
+ url.searchParams.set("_t", Date.now().toString())
43
+ }
44
+ return fetch(url.toString(), {
40
45
  method: "GET",
41
46
  headers: { "Content-Type": "application/json" },
42
47
  cache: "no-cache",
@@ -54,6 +59,7 @@ export const fetchDailyReportIds = (apiBasePath: string = defaultDailyReportClie
54
59
  }
55
60
  return data.ids as DailyReportItem[]
56
61
  })
62
+ }
57
63
 
58
64
  /**
59
65
  * Creates a React Router clientLoader that merges server data with deferred report ids.
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Unit tests for defineDailyReportAuthzResources & seedDailyReportAuthzResources in @aiquants/daily-report/server.
3
+ */
4
+ import { describe, expect, it } from "vitest"
5
+ import { defineDailyReportAuthzResources, seedDailyReportAuthzResources } from "./authz"
6
+
7
+ describe("defineDailyReportAuthzResources", () => {
8
+ it("generates neutral default resources when no sources specified", () => {
9
+ const res = defineDailyReportAuthzResources()
10
+ expect(res).toHaveLength(4) // internal + internal_comment + external + external_comment
11
+ expect(res[0].resourceKey).toBe("daily_report_internal")
12
+ expect(res[1].resourceKey).toBe("daily_report_internal_comment")
13
+ })
14
+
15
+ it("generates custom resources from source inputs without domain hardcoding", () => {
16
+ const res = defineDailyReportAuthzResources([
17
+ { key: "ORG", name: "Original", description: "Original daily reports" },
18
+ { key: "NI", name: "NI", description: "NI daily reports", includeCommentResource: false },
19
+ ])
20
+ expect(res).toHaveLength(3) // ORG + ORG_comment + NI (no comment)
21
+ expect(res[0].resourceKey).toBe("daily_report_org")
22
+ expect(res[1].resourceKey).toBe("daily_report_org_comment")
23
+ expect(res[2].resourceKey).toBe("daily_report_ni")
24
+ })
25
+ })
26
+
27
+ describe("seedDailyReportAuthzResources", () => {
28
+ it("idempotently seeds resources into TMResource table", async () => {
29
+ const seeded: Record<string, unknown>[] = []
30
+
31
+ const fakeDb = {
32
+ select: () => ({
33
+ from: () => ({
34
+ where: async () => [],
35
+ }),
36
+ }),
37
+ insert: () => ({
38
+ values: async (v: unknown) => {
39
+ seeded.push(v as Record<string, unknown>)
40
+ },
41
+ }),
42
+ }
43
+
44
+ const fakeAuthzTables = {
45
+ TMResource: { appKey: "appKey", resourceKey: "resourceKey" },
46
+ }
47
+
48
+ await seedDailyReportAuthzResources(fakeDb, fakeAuthzTables, {
49
+ appKey: "quants",
50
+ sources: [{ key: "sales", name: "Sales" }],
51
+ })
52
+
53
+ expect(seeded.length).toBe(2)
54
+ expect(seeded[0].appKey).toBe("quants")
55
+ expect(seeded[0].resourceKey).toBe("daily_report_sales")
56
+ })
57
+ })