@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.
- package/README.md +38 -38
- package/dist/client.d.mts +11 -5
- package/dist/client.d.ts +11 -5
- package/dist/client.js +4 -4
- package/dist/client.js.map +1 -1
- package/dist/client.mjs +4 -4
- package/dist/client.mjs.map +1 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/dist/server.d.mts +35 -5
- package/dist/server.d.ts +35 -5
- package/dist/server.js +6 -6
- package/dist/server.js.map +1 -1
- package/dist/server.mjs +6 -6
- package/dist/server.mjs.map +1 -1
- package/dist/sse-schema-eXcMG-Ej.d.ts +2025 -0
- package/dist/sse-schema-rbG114od.d.mts +2025 -0
- package/dist/{types-BNhYC2j9.d.mts → types-D1PKubyo.d.mts} +26 -26
- package/dist/{types-BNhYC2j9.d.ts → types-D1PKubyo.d.ts} +26 -26
- package/package.json +4 -4
- package/src/client/components/daily-report-detail-list.tsx +29 -7
- package/src/client/components/daily-report-list.tsx +26 -5
- package/src/client/components/daily-report-resolved-content.tsx +28 -4
- package/src/client/config-context.tsx +2 -0
- package/src/client/contexts/daily-report-action-context.tsx +6 -2
- package/src/client/hooks/use-daily-report-sse-connection.ts +1 -1
- package/src/client/hooks/use-daily-report.ts +2 -2
- package/src/client/route-helpers.ts +8 -2
- package/src/server/authz.spec.ts +57 -0
- package/src/server/authz.ts +99 -0
- package/src/server/handlers.ts +5 -4
- package/src/server/service.ts +31 -13
- package/src/server.ts +1 -0
- package/src/shared/comment-adapter.ts +3 -3
- package/src/shared/sse-schema.ts +27 -26
- package/src/shared/types.ts +26 -26
- package/dist/sse-schema-ChCypDPj.d.ts +0 -2019
- 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
|
|
14
|
-
affiliation
|
|
13
|
+
name?: string | null;
|
|
14
|
+
affiliation?: string | null;
|
|
15
15
|
};
|
|
16
16
|
/** レガシー形式 (JSON 埋め込み) のコメント。 */
|
|
17
17
|
type DailyReportComment = {
|
|
18
|
-
name
|
|
19
|
-
text
|
|
20
|
-
color
|
|
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
|
|
26
|
-
color
|
|
25
|
+
name?: string | null;
|
|
26
|
+
color?: string | null;
|
|
27
27
|
};
|
|
28
28
|
/** リレーショナルコメント 1 件。`userId` は難読化済み文字列。 */
|
|
29
29
|
type DailyReportCommentItem = {
|
|
30
30
|
id: number;
|
|
31
|
-
userId
|
|
32
|
-
userName
|
|
33
|
-
content
|
|
34
|
-
createdAt
|
|
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
|
|
41
|
-
createdAt
|
|
42
|
-
author
|
|
43
|
-
userId
|
|
44
|
-
sourceType
|
|
45
|
-
employeeName
|
|
46
|
-
updatedBy
|
|
47
|
-
updatedAt
|
|
48
|
-
category
|
|
49
|
-
creationCategory
|
|
50
|
-
visitTimeFrom
|
|
51
|
-
visitTimeTo
|
|
52
|
-
customerName
|
|
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
|
|
55
|
-
content
|
|
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
|
|
14
|
-
affiliation
|
|
13
|
+
name?: string | null;
|
|
14
|
+
affiliation?: string | null;
|
|
15
15
|
};
|
|
16
16
|
/** レガシー形式 (JSON 埋め込み) のコメント。 */
|
|
17
17
|
type DailyReportComment = {
|
|
18
|
-
name
|
|
19
|
-
text
|
|
20
|
-
color
|
|
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
|
|
26
|
-
color
|
|
25
|
+
name?: string | null;
|
|
26
|
+
color?: string | null;
|
|
27
27
|
};
|
|
28
28
|
/** リレーショナルコメント 1 件。`userId` は難読化済み文字列。 */
|
|
29
29
|
type DailyReportCommentItem = {
|
|
30
30
|
id: number;
|
|
31
|
-
userId
|
|
32
|
-
userName
|
|
33
|
-
content
|
|
34
|
-
createdAt
|
|
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
|
|
41
|
-
createdAt
|
|
42
|
-
author
|
|
43
|
-
userId
|
|
44
|
-
sourceType
|
|
45
|
-
employeeName
|
|
46
|
-
updatedBy
|
|
47
|
-
updatedAt
|
|
48
|
-
category
|
|
49
|
-
creationCategory
|
|
50
|
-
visitTimeFrom
|
|
51
|
-
visitTimeTo
|
|
52
|
-
customerName
|
|
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
|
|
55
|
-
content
|
|
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.
|
|
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/
|
|
76
|
-
"@aiquants/
|
|
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.
|
|
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
|
-
|
|
671
|
-
<
|
|
672
|
-
|
|
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:
|
|
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:
|
|
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 (
|
|
@@ -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.
|
|
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
|
-
|
|
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
|
+
})
|