@aiquants/daily-report 0.3.1 → 0.4.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.
- package/README.md +24 -22
- package/dist/client.d.mts +3 -10
- package/dist/client.d.ts +3 -10
- 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 +1 -6
- package/dist/server.d.ts +1 -6
- package/dist/server.js +4 -4
- package/dist/server.js.map +1 -1
- package/dist/server.mjs +4 -4
- package/dist/server.mjs.map +1 -1
- package/dist/{sse-schema-DO9Qe8XF.d.ts → sse-schema-CK7cUnEo.d.ts} +1 -34
- package/dist/{sse-schema-DKUjXMW3.d.mts → sse-schema-yl5AaSsj.d.mts} +1 -34
- package/dist/styles/daily-report.standalone.css +3 -0
- package/dist/{types-DrjeK3_G.d.mts → types-CVhwLhSN.d.mts} +0 -1
- package/dist/{types-DrjeK3_G.d.ts → types-CVhwLhSN.d.ts} +0 -1
- package/package.json +21 -13
- package/src/client/components/daily-report-comment-item.tsx +4 -1
- package/src/client/components/daily-report-detail-list.tsx +16 -25
- package/src/client/components/daily-report-list.tsx +17 -24
- package/src/client/config-context.tsx +0 -9
- package/src/client/contexts/daily-report-action-context.tsx +0 -1
- package/src/server/cache.ts +0 -18
- package/src/server/service.ts +0 -2
- package/src/shared/sse-schema.ts +0 -1
- package/src/shared/types.ts +0 -1
- package/src/styles/standalone.entry.css +51 -0
- package/src/server/cache.spec.ts +0 -47
package/package.json
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aiquants/daily-report",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
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
|
-
"sideEffects":
|
|
5
|
+
"sideEffects": [
|
|
6
|
+
"**/*.css"
|
|
7
|
+
],
|
|
6
8
|
"main": "dist/index.js",
|
|
7
9
|
"module": "dist/index.mjs",
|
|
8
10
|
"types": "dist/index.d.ts",
|
|
@@ -21,7 +23,8 @@
|
|
|
21
23
|
"types": "./dist/server.d.ts",
|
|
22
24
|
"import": "./dist/server.mjs",
|
|
23
25
|
"require": "./dist/server.js"
|
|
24
|
-
}
|
|
26
|
+
},
|
|
27
|
+
"./styles/daily-report.standalone.css": "./dist/styles/daily-report.standalone.css"
|
|
25
28
|
},
|
|
26
29
|
"files": [
|
|
27
30
|
"dist",
|
|
@@ -52,6 +55,7 @@
|
|
|
52
55
|
"tailwind-merge": "^3.5.0"
|
|
53
56
|
},
|
|
54
57
|
"devDependencies": {
|
|
58
|
+
"@tailwindcss/cli": "^4.1.18",
|
|
55
59
|
"@testing-library/jest-dom": "^6.9.1",
|
|
56
60
|
"@testing-library/react": "^16.3.2",
|
|
57
61
|
"@types/node": "^25.9.1",
|
|
@@ -62,12 +66,14 @@
|
|
|
62
66
|
"react-dom": "^19.2.7",
|
|
63
67
|
"react-router": "^7.13.0",
|
|
64
68
|
"rimraf": "^6.1.2",
|
|
69
|
+
"tailwindcss": "^4.1.18",
|
|
70
|
+
"tailwindcss-animate": "^1.0.7",
|
|
65
71
|
"tsup": "^8.5.1",
|
|
66
72
|
"typescript": "^5.9.3",
|
|
67
73
|
"vitest": "^4.1.8",
|
|
68
74
|
"zod": "^3.25.76",
|
|
69
|
-
"@aiquants/swipe-overlay": "1.2.
|
|
70
|
-
"@aiquants/virtualscroll": "1.
|
|
75
|
+
"@aiquants/swipe-overlay": "1.2.4",
|
|
76
|
+
"@aiquants/virtualscroll": "1.19.0"
|
|
71
77
|
},
|
|
72
78
|
"peerDependencies": {
|
|
73
79
|
"drizzle-orm": ">=1.0.0-beta.4 <2.0.0",
|
|
@@ -75,8 +81,8 @@
|
|
|
75
81
|
"react-dom": ">=18",
|
|
76
82
|
"react-router": "^7.0.0",
|
|
77
83
|
"zod": ">=3.25.0 <5.0.0",
|
|
78
|
-
"@aiquants/
|
|
79
|
-
"@aiquants/
|
|
84
|
+
"@aiquants/virtualscroll": "^1.19.0",
|
|
85
|
+
"@aiquants/swipe-overlay": "^1.2.4"
|
|
80
86
|
},
|
|
81
87
|
"peerDependenciesMeta": {
|
|
82
88
|
"drizzle-orm": {
|
|
@@ -91,14 +97,16 @@
|
|
|
91
97
|
"access": "public"
|
|
92
98
|
},
|
|
93
99
|
"scripts": {
|
|
94
|
-
"build": "tsup",
|
|
95
|
-
"build:
|
|
96
|
-
"
|
|
100
|
+
"build": "tsup && pnpm build:css",
|
|
101
|
+
"build:css": "tailwindcss -i src/styles/standalone.entry.css -o dist/styles/daily-report.standalone.css --minify",
|
|
102
|
+
"watch:css": "tailwindcss -i src/styles/standalone.entry.css -o dist/styles/daily-report.standalone.css --minify --watch",
|
|
103
|
+
"build:watch": "tsup --watch --onSuccess \"pnpm build:css\"",
|
|
104
|
+
"dev": "tsup --watch --onSuccess \"pnpm build:css\"",
|
|
97
105
|
"typecheck": "tsc --noEmit",
|
|
98
106
|
"clean": "rimraf dist",
|
|
99
|
-
"publish:patch": "pnpm version patch --no-git-tag-version --no-git-checks && pnpm publish --no-git-checks",
|
|
100
|
-
"publish:minor": "pnpm version minor --no-git-tag-version --no-git-checks && pnpm publish --no-git-checks",
|
|
101
|
-
"publish:major": "pnpm version major --no-git-tag-version --no-git-checks && pnpm publish --no-git-checks",
|
|
107
|
+
"publish:patch": "pnpm run typecheck && pnpm run --if-present test && pnpm version patch --no-git-tag-version --no-git-checks && pnpm publish --no-git-checks",
|
|
108
|
+
"publish:minor": "pnpm run typecheck && pnpm run --if-present test && pnpm version minor --no-git-tag-version --no-git-checks && pnpm publish --no-git-checks",
|
|
109
|
+
"publish:major": "pnpm run typecheck && pnpm run --if-present test && pnpm version major --no-git-tag-version --no-git-checks && pnpm publish --no-git-checks",
|
|
102
110
|
"lint": "biome lint src/",
|
|
103
111
|
"check": "biome check src/",
|
|
104
112
|
"check:fix": "biome check --write src/",
|
|
@@ -51,7 +51,10 @@ export const DailyReportCommentItem = ({ comment, reportHubId: _reportHubId, bus
|
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
return (
|
|
54
|
-
<li
|
|
54
|
+
<li
|
|
55
|
+
data-comment-id={comment.id}
|
|
56
|
+
data-deleted={isDeleted ? "true" : undefined}
|
|
57
|
+
className={cn("group relative rounded-xl border border-slate-200 bg-white p-3", comment.isMine && "border-blue-200 bg-blue-50", isDeleted && "hidden", isPendingDelete ? "z-50" : "z-0 hover:z-10")}>
|
|
55
58
|
<div className="mb-1 flex items-center justify-between">
|
|
56
59
|
<div className={cn("font-semibold text-xs", resolveCommentColorClass(comment.color))}>{fallbackText(comment.authorName)}</div>
|
|
57
60
|
<div className="flex items-center gap-2">
|
|
@@ -19,12 +19,11 @@ import { Check, Star, Trash2 } from "lucide-react"
|
|
|
19
19
|
import { type RefObject, useCallback, useEffect, useMemo, useRef, useState } from "react"
|
|
20
20
|
import { fallbackText, formatToIsoDate } from "../../shared/text-utils"
|
|
21
21
|
import type { DailyReportDetail, DailyReportItem } from "../../shared/types"
|
|
22
|
-
import { type DailyReportFieldLabels,
|
|
22
|
+
import { type DailyReportFieldLabels, useDailyReportConfig } from "../config-context"
|
|
23
23
|
import { useDailyReportActionContext } from "../contexts/daily-report-action-context"
|
|
24
24
|
import { useDailyReportDetail, useDailyReportPrefetch } from "../hooks/use-daily-report"
|
|
25
25
|
import { useDailyReportComments } from "../hooks/use-daily-report-comments"
|
|
26
26
|
import { useDynamicViewportHeight } from "../hooks/use-dynamic-viewport-height"
|
|
27
|
-
import { cn } from "../ui/cn"
|
|
28
27
|
import { EMPTY_ITEM, FAST_SCROLL_INERTIA_OPTIONS, FAST_SCROLL_WHEEL_MULTIPLIER, PREFETCH_LOOKAHEAD, PREFETCH_MAX_DATES, VIRTUAL_SCROLL_OVERSCAN_COUNT } from "../utils/constants"
|
|
29
28
|
import { BusinessDayThumbOverlay } from "./business-day-thumb-overlay"
|
|
30
29
|
import { DailyReportCommentForm, DailyReportCommentList } from "./daily-report-comment-section"
|
|
@@ -120,7 +119,7 @@ export const DailyReportDetailList = ({ dailyReportItems, userId, selectedItemId
|
|
|
120
119
|
|
|
121
120
|
useEffect(() => {
|
|
122
121
|
if (virtualScrollRef.current) {
|
|
123
|
-
const el = document.querySelector("
|
|
122
|
+
const el = document.querySelector('[data-testid="daily-report-detail-list"]')
|
|
124
123
|
if (el) {
|
|
125
124
|
const handle = virtualScrollRef.current
|
|
126
125
|
const extendedHandle: DailyReportVirtualScrollHandle = Object.assign({}, handle, {
|
|
@@ -185,8 +184,8 @@ export const DailyReportDetailList = ({ dailyReportItems, userId, selectedItemId
|
|
|
185
184
|
const container = scrollContainerRef.current
|
|
186
185
|
if (!container) return
|
|
187
186
|
|
|
188
|
-
if (
|
|
189
|
-
container.
|
|
187
|
+
if (container.dataset.scrolling !== "true") {
|
|
188
|
+
container.dataset.scrolling = "true"
|
|
190
189
|
}
|
|
191
190
|
|
|
192
191
|
if (scrollEndTimerRef.current) {
|
|
@@ -195,7 +194,7 @@ export const DailyReportDetailList = ({ dailyReportItems, userId, selectedItemId
|
|
|
195
194
|
|
|
196
195
|
scrollEndTimerRef.current = setTimeout(() => {
|
|
197
196
|
if (container) {
|
|
198
|
-
container.
|
|
197
|
+
delete container.dataset.scrolling
|
|
199
198
|
}
|
|
200
199
|
scrollEndTimerRef.current = null
|
|
201
200
|
}, 150)
|
|
@@ -306,7 +305,8 @@ export const DailyReportDetailList = ({ dailyReportItems, userId, selectedItemId
|
|
|
306
305
|
}}
|
|
307
306
|
onRangeChange={handleRangeChange}
|
|
308
307
|
onScroll={handleScroll}
|
|
309
|
-
|
|
308
|
+
testId="daily-report-detail-list"
|
|
309
|
+
className="relative w-full">
|
|
310
310
|
{renderItem}
|
|
311
311
|
</VirtualScroll>
|
|
312
312
|
</div>
|
|
@@ -439,7 +439,6 @@ const DailyReportHeader = ({
|
|
|
439
439
|
visitTimeDisplay,
|
|
440
440
|
categoryLabel,
|
|
441
441
|
fieldLabels,
|
|
442
|
-
sourceTypeConfigs,
|
|
443
442
|
}: {
|
|
444
443
|
report: DailyReportDetail
|
|
445
444
|
isRead: boolean
|
|
@@ -451,22 +450,8 @@ const DailyReportHeader = ({
|
|
|
451
450
|
visitTimeDisplay: string
|
|
452
451
|
categoryLabel: string
|
|
453
452
|
fieldLabels: DailyReportFieldLabels
|
|
454
|
-
sourceTypeConfigs?: Record<string, SourceTypeConfig>
|
|
455
453
|
}) => (
|
|
456
454
|
<div className="flex flex-wrap items-center gap-2 font-medium text-slate-600 text-xs">
|
|
457
|
-
{(() => {
|
|
458
|
-
const sourceType = report.sourceType
|
|
459
|
-
const sourceConfig = sourceType ? sourceTypeConfigs?.[sourceType] : undefined
|
|
460
|
-
return sourceConfig ? <span className={cn("inline-flex items-center rounded px-2 py-0.5 font-semibold text-[10px] tracking-wide", sourceConfig.badgeClassName)}>{sourceConfig.label}</span> : null
|
|
461
|
-
})()}
|
|
462
|
-
<button
|
|
463
|
-
type="button"
|
|
464
|
-
onClick={onToggleStar}
|
|
465
|
-
className="flex h-6 w-6 items-center justify-center rounded-full transition-colors hover:bg-slate-200 focus:outline-none dark:hover:bg-slate-700"
|
|
466
|
-
title={isStarred ? "スターを外す" : "スターを付ける"}
|
|
467
|
-
data-detail-star-button={report.reportHubId}>
|
|
468
|
-
{isStarred ? <Star className="h-3.5 w-3.5 fill-yellow-400 text-yellow-400" /> : <Star className="h-3.5 w-3.5 text-slate-300 dark:text-slate-500" />}
|
|
469
|
-
</button>
|
|
470
455
|
<button
|
|
471
456
|
type="button"
|
|
472
457
|
onClick={onToggleRead}
|
|
@@ -475,6 +460,14 @@ const DailyReportHeader = ({
|
|
|
475
460
|
data-detail-read-button={report.reportHubId}>
|
|
476
461
|
{isRead ? <Check className="h-3.5 w-3.5 text-blue-500" /> : <UnreadIndicator className="text-xs" />}
|
|
477
462
|
</button>
|
|
463
|
+
<button
|
|
464
|
+
type="button"
|
|
465
|
+
onClick={onToggleStar}
|
|
466
|
+
className="flex h-6 w-6 items-center justify-center rounded-full transition-colors hover:bg-slate-200 focus:outline-none dark:hover:bg-slate-700"
|
|
467
|
+
title={isStarred ? "スターを外す" : "スターを付ける"}
|
|
468
|
+
data-detail-star-button={report.reportHubId}>
|
|
469
|
+
{isStarred ? <Star className="h-3.5 w-3.5 fill-yellow-400 text-yellow-400" /> : <Star className="h-3.5 w-3.5 text-slate-300 dark:text-slate-500" />}
|
|
470
|
+
</button>
|
|
478
471
|
{isMine && (
|
|
479
472
|
<button type="button" onClick={onDelete} data-detail-delete-button={report.reportHubId} className="text-lg leading-none transition-transform focus:outline-none active:scale-125" title="削除する">
|
|
480
473
|
<Trash2 className="h-4 w-4 text-slate-300 hover:text-red-500" />
|
|
@@ -484,7 +477,6 @@ const DailyReportHeader = ({
|
|
|
484
477
|
<span className="text-[10px] text-blue-500 uppercase tracking-wide">{fieldLabels.id}</span>
|
|
485
478
|
{report.reportHubId}
|
|
486
479
|
</span>
|
|
487
|
-
|
|
488
480
|
<span className="inline-flex items-center gap-1 whitespace-nowrap rounded-full bg-slate-100 px-3 py-1">
|
|
489
481
|
<span className="text-[10px] text-slate-400 uppercase tracking-wide">{fieldLabels.businessDate}</span>
|
|
490
482
|
{formatToIsoDate(report.date)}
|
|
@@ -560,7 +552,7 @@ const DailyReportResolvedItem = ({ item, report, onCommentAdded, userId, onDoubl
|
|
|
560
552
|
// 表示中ユーザーはアクションコンテキストから取得する (root loader 非依存)
|
|
561
553
|
const { toggleStar, toggleRead, pendingStarUpdates, pendingReadUpdates, deleteReport, user } = useDailyReportActionContext()
|
|
562
554
|
const { uiComments, handleAddComment, handleDeleteComment, pendingDeleteId, setPendingDeleteId, resolvedIdMap } = useDailyReportComments(report, user, userId)
|
|
563
|
-
const { fieldLabels
|
|
555
|
+
const { fieldLabels } = useDailyReportConfig()
|
|
564
556
|
|
|
565
557
|
const handleAddCommentWrapper = async (content: string) => {
|
|
566
558
|
await handleAddComment(content)
|
|
@@ -618,7 +610,6 @@ const DailyReportResolvedItem = ({ item, report, onCommentAdded, userId, onDoubl
|
|
|
618
610
|
visitTimeDisplay={visitTimeDisplay}
|
|
619
611
|
categoryLabel={categoryLabel}
|
|
620
612
|
fieldLabels={fieldLabels}
|
|
621
|
-
sourceTypeConfigs={sourceTypeConfigs}
|
|
622
613
|
/>
|
|
623
614
|
<div className="grid gap-4 md:grid-cols-[minmax(0,1fr)_240px]">
|
|
624
615
|
<div className="flex flex-col gap-4">
|
|
@@ -13,7 +13,6 @@ import { useDailyReportActionContext } from "../contexts/daily-report-action-con
|
|
|
13
13
|
import { useDailyReportDetail } from "../hooks/use-daily-report"
|
|
14
14
|
import { useDailyReportComments } from "../hooks/use-daily-report-comments"
|
|
15
15
|
import { useDynamicViewportHeight } from "../hooks/use-dynamic-viewport-height"
|
|
16
|
-
import { cn } from "../ui/cn"
|
|
17
16
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "../ui/tabs"
|
|
18
17
|
import {
|
|
19
18
|
DEFAULT_ITEM_HEIGHT,
|
|
@@ -353,7 +352,7 @@ export const DailyReportList = ({ dailyReportItems, initialSelectedBusinessDate
|
|
|
353
352
|
|
|
354
353
|
useEffect(() => {
|
|
355
354
|
if (virtualScrollRef.current) {
|
|
356
|
-
const el = document.querySelector("
|
|
355
|
+
const el = document.querySelector('[data-testid="daily-report-list"]') as (Element & { __virtualScroll?: TestableVirtualScrollHandle }) | null
|
|
357
356
|
if (el) {
|
|
358
357
|
el.__virtualScroll = {
|
|
359
358
|
...virtualScrollRef.current,
|
|
@@ -403,8 +402,8 @@ export const DailyReportList = ({ dailyReportItems, initialSelectedBusinessDate
|
|
|
403
402
|
const container = scrollContainerRef.current
|
|
404
403
|
if (!container) return
|
|
405
404
|
|
|
406
|
-
if (
|
|
407
|
-
container.
|
|
405
|
+
if (container.dataset.scrolling !== "true") {
|
|
406
|
+
container.dataset.scrolling = "true"
|
|
408
407
|
}
|
|
409
408
|
|
|
410
409
|
if (scrollEndTimerRef.current) {
|
|
@@ -413,7 +412,7 @@ export const DailyReportList = ({ dailyReportItems, initialSelectedBusinessDate
|
|
|
413
412
|
|
|
414
413
|
scrollEndTimerRef.current = setTimeout(() => {
|
|
415
414
|
if (container) {
|
|
416
|
-
container.
|
|
415
|
+
delete container.dataset.scrolling
|
|
417
416
|
}
|
|
418
417
|
scrollEndTimerRef.current = null
|
|
419
418
|
}, 150)
|
|
@@ -445,7 +444,8 @@ export const DailyReportList = ({ dailyReportItems, initialSelectedBusinessDate
|
|
|
445
444
|
onScroll={handleScroll}
|
|
446
445
|
onItemFocus={handleItemFocus}
|
|
447
446
|
contentInsets={{ top: ITEM_VERTICAL_PADDING, bottom: ITEM_VERTICAL_PADDING }}
|
|
448
|
-
|
|
447
|
+
testId="daily-report-list"
|
|
448
|
+
className="relative w-full">
|
|
449
449
|
{(item) => <ListReportItem key={item.reportHubId} item={item} isActive={item.reportHubId === selectedReportHubId} onSelect={handleItemSelect} />}
|
|
450
450
|
</VirtualScroll>
|
|
451
451
|
</div>
|
|
@@ -467,7 +467,7 @@ export const DailyReportList = ({ dailyReportItems, initialSelectedBusinessDate
|
|
|
467
467
|
*/
|
|
468
468
|
const ListSkeletonItem = () => (
|
|
469
469
|
<div className="px-2 pb-3" style={{ height: ITEM_CONTAINER_HEIGHT }}>
|
|
470
|
-
<div className="flex h-full animate-pulse flex-col justify-between rounded-2xl border border-slate-200 bg-white p-4 shadow-sm dark:border-slate-700 dark:bg-slate-900/70">
|
|
470
|
+
<div data-skeleton className="flex h-full animate-pulse flex-col justify-between rounded-2xl border border-slate-200 bg-white p-4 shadow-sm dark:border-slate-700 dark:bg-slate-900/70">
|
|
471
471
|
<div className="mb-3 flex flex-wrap items-center gap-3">
|
|
472
472
|
<div className="h-4 w-24 rounded bg-slate-200"></div>
|
|
473
473
|
<div className="h-4 w-24 rounded bg-slate-200"></div>
|
|
@@ -505,7 +505,7 @@ const DailyReportSidePane = ({
|
|
|
505
505
|
}) => {
|
|
506
506
|
const [activeTab, setActiveTab] = useState<DailyReportSidePaneTab>("article")
|
|
507
507
|
const [isEditing, setIsEditing] = useState(false)
|
|
508
|
-
const { draftLabelName, fieldLabels
|
|
508
|
+
const { draftLabelName, fieldLabels } = useDailyReportConfig()
|
|
509
509
|
const { report, error, isLoading } = useDailyReportDetail(selectedItem?.reportHubId ?? -1, selectedItem?.businessDate ?? null)
|
|
510
510
|
|
|
511
511
|
const scrollContainerRef = useRef<HTMLDivElement>(null)
|
|
@@ -570,12 +570,7 @@ const DailyReportSidePane = ({
|
|
|
570
570
|
<div className="text-slate-900 text-sm dark:text-slate-100">{headerDate}</div>
|
|
571
571
|
<div className="flex min-h-4 items-center gap-1 text-slate-500 text-xs dark:text-slate-300">
|
|
572
572
|
<span>{fieldLabels.author}:</span>
|
|
573
|
-
{showHeaderSkeleton ? <div className="h-3 w-24 animate-pulse rounded bg-slate-200 dark:bg-slate-700" /> : <span>{headerCreator}</span>}
|
|
574
|
-
{(() => {
|
|
575
|
-
const sourceType = report?.sourceType
|
|
576
|
-
const sourceConfig = sourceType ? sourceTypeConfigs?.[sourceType] : undefined
|
|
577
|
-
return sourceConfig ? <span className={cn("ml-2 inline-flex shrink-0 items-center rounded px-2 py-0.5 font-semibold text-[10px] tracking-wide", sourceConfig.badgeClassName)}>{sourceConfig.label}</span> : null
|
|
578
|
-
})()}
|
|
573
|
+
{showHeaderSkeleton ? <div data-skeleton className="h-3 w-24 animate-pulse rounded bg-slate-200 dark:bg-slate-700" /> : <span>{headerCreator}</span>}
|
|
579
574
|
</div>
|
|
580
575
|
</div>
|
|
581
576
|
<Tabs value={activeTab} onValueChange={(v) => setActiveTab(v as DailyReportSidePaneTab)} className="flex min-h-0 flex-1 flex-col">
|
|
@@ -622,14 +617,18 @@ const DailyReportSidePane = ({
|
|
|
622
617
|
// If rendered as a standalone pane (desktop)
|
|
623
618
|
if (dailyReportItems) {
|
|
624
619
|
return (
|
|
625
|
-
<aside
|
|
620
|
+
<aside
|
|
621
|
+
data-testid="daily-report-side-pane"
|
|
622
|
+
data-report-id={selectedItem?.reportHubId}
|
|
623
|
+
className="hidden min-h-0 min-w-0 flex-1 flex-col gap-3 rounded-2xl border border-slate-200 bg-white p-4 shadow-sm sm:flex dark:border-slate-700 dark:bg-slate-900/70"
|
|
624
|
+
style={heightStyle}>
|
|
626
625
|
{content}
|
|
627
626
|
</aside>
|
|
628
627
|
)
|
|
629
628
|
}
|
|
630
629
|
// If rendered inside mobile overlay
|
|
631
630
|
return (
|
|
632
|
-
<div data-report-id={selectedItem?.reportHubId} className="
|
|
631
|
+
<div data-testid="daily-report-side-pane" data-report-id={selectedItem?.reportHubId} className="flex min-h-0 flex-1 flex-col gap-3 overflow-hidden">
|
|
633
632
|
{content}
|
|
634
633
|
</div>
|
|
635
634
|
)
|
|
@@ -728,7 +727,7 @@ const DailyReportDisplayPane = ({
|
|
|
728
727
|
const isMine = !!(userId && report?.userId === userId)
|
|
729
728
|
const hasComments = uiComments.length > 0
|
|
730
729
|
|
|
731
|
-
const SkeletonText = ({ className = "w-32" }: { className?: string }) => <div className={`h-4 animate-pulse rounded bg-slate-200 dark:bg-slate-700 ${className}`} />
|
|
730
|
+
const SkeletonText = ({ className = "w-32" }: { className?: string }) => <div data-skeleton className={`h-4 animate-pulse rounded bg-slate-200 dark:bg-slate-700 ${className}`} />
|
|
732
731
|
|
|
733
732
|
return (
|
|
734
733
|
// biome-ignore lint/a11y/noStaticElementInteractions: Double click to edit feature
|
|
@@ -910,7 +909,6 @@ const extractPreviewLines = (content: string | null): string[] => {
|
|
|
910
909
|
const ListReportItem = ({ item, isActive, onSelect }: { item: DailyReportItem; isActive: boolean; onSelect: (id: number) => void }) => {
|
|
911
910
|
const { report, error, isLoading, isRefetching } = useDailyReportDetail(item.reportHubId, item.businessDate)
|
|
912
911
|
const { toggleStar, toggleRead, removeStaleItem } = useDailyReportActionContext()
|
|
913
|
-
const { sourceTypeConfigs } = useDailyReportConfig()
|
|
914
912
|
|
|
915
913
|
// フェッチが一度でも試行されたかを追跡する ref
|
|
916
914
|
const hasEverFetched = useRef(false)
|
|
@@ -980,7 +978,7 @@ const ListReportItem = ({ item, isActive, onSelect }: { item: DailyReportItem; i
|
|
|
980
978
|
{/* biome-ignore lint/a11y/useSemanticElements: Card contains nested interactive elements (Star/Read buttons) */}
|
|
981
979
|
<div
|
|
982
980
|
data-daily-report-button={item.reportHubId}
|
|
983
|
-
className={`flex h-full w-full in-[
|
|
981
|
+
className={`flex h-full w-full in-[[data-scrolling]]:transform-none cursor-pointer select-none flex-col rounded-2xl border in-[[data-scrolling]]:border-slate-200 bg-white p-4 text-left in-[[data-scrolling]]:shadow-sm shadow-sm in-[[data-scrolling]]:transition-none transition-all duration-200 hover:-translate-y-0.5 hover:border-blue-300 hover:shadow-lg dark:bg-slate-900/70 ${
|
|
984
982
|
isActive ? "border-blue-300 shadow-md ring-2 ring-blue-200 dark:border-blue-500/70 dark:ring-blue-500/40" : "border-slate-200 dark:border-slate-700"
|
|
985
983
|
}`}
|
|
986
984
|
style={{ touchAction: "manipulation" }}
|
|
@@ -996,11 +994,6 @@ const ListReportItem = ({ item, isActive, onSelect }: { item: DailyReportItem; i
|
|
|
996
994
|
<span className="truncate font-medium text-slate-900 dark:text-slate-100">{creator}</span>
|
|
997
995
|
</span>
|
|
998
996
|
<div className="ml-auto flex items-center gap-1.5">
|
|
999
|
-
{(() => {
|
|
1000
|
-
const sourceType = report?.sourceType
|
|
1001
|
-
const sourceConfig = sourceType ? sourceTypeConfigs?.[sourceType] : undefined
|
|
1002
|
-
return sourceConfig ? <span className={cn("inline-flex shrink-0 items-center rounded px-2 py-0.5 font-semibold text-[10px] tracking-wide", sourceConfig.badgeClassName)}>{sourceConfig.label}</span> : null
|
|
1003
|
-
})()}
|
|
1004
997
|
<button
|
|
1005
998
|
type="button"
|
|
1006
999
|
title={report.isStarred ? "スターを外す" : "スターを付ける"}
|
|
@@ -38,11 +38,6 @@ export type DailyReportFieldLabels = {
|
|
|
38
38
|
relationsEmpty: string
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
export type SourceTypeConfig = {
|
|
42
|
-
label: string
|
|
43
|
-
badgeClassName?: string
|
|
44
|
-
}
|
|
45
|
-
|
|
46
41
|
/** クライアント層の設定一式。 */
|
|
47
42
|
export type DailyReportClientConfig = {
|
|
48
43
|
/** 画面タイトル (ヘッダーへ渡す)。 */
|
|
@@ -55,8 +50,6 @@ export type DailyReportClientConfig = {
|
|
|
55
50
|
draftLabelName: string
|
|
56
51
|
/** 詳細/一覧カードのフィールド見出しラベル。既定は中立英語。アプリが自ドメインの表記を注入する。 */
|
|
57
52
|
fieldLabels: DailyReportFieldLabels
|
|
58
|
-
/** 外部ソース区分ごとのバッジ表示設定。 */
|
|
59
|
-
sourceTypeConfigs?: Record<string, SourceTypeConfig>
|
|
60
53
|
/** ヘッダー描画スロット。アプリのヘッダーコンポーネントを差し込む。 */
|
|
61
54
|
renderHeader: (props: DailyReportHeaderProps) => ReactNode
|
|
62
55
|
/** 開発向けコントロール (SSE 購読トグル等) を表示するか。 */
|
|
@@ -69,7 +62,6 @@ export type DailyReportClientConfig = {
|
|
|
69
62
|
*/
|
|
70
63
|
export type DailyReportClientConfigInput = Partial<Omit<DailyReportClientConfig, "fieldLabels">> & {
|
|
71
64
|
fieldLabels?: Partial<DailyReportFieldLabels>
|
|
72
|
-
sourceTypeConfigs?: Record<string, SourceTypeConfig>
|
|
73
65
|
}
|
|
74
66
|
|
|
75
67
|
/** 中立英語のフィールドラベル既定 (ドメイン非依存)。 */
|
|
@@ -102,7 +94,6 @@ export const defaultDailyReportClientConfig: DailyReportClientConfig = {
|
|
|
102
94
|
// 中立プレースホルダー。実運用のラベル名 (アプリ固有) は Provider / config prop で上書きする。
|
|
103
95
|
draftLabelName: "draft",
|
|
104
96
|
fieldLabels: defaultDailyReportFieldLabels,
|
|
105
|
-
sourceTypeConfigs: {},
|
|
106
97
|
renderHeader: ({ title, annotation }) => (
|
|
107
98
|
<header className="flex flex-wrap items-baseline justify-between gap-2 px-2 py-1">
|
|
108
99
|
<span className="text-nowrap text-slate-700 text-sm dark:text-slate-200">{title}</span>
|
|
@@ -703,7 +703,6 @@ export const DailyReportActionProvider = ({ children, user, initialItems = [], u
|
|
|
703
703
|
createdAt: now,
|
|
704
704
|
author: getUserDisplayName(user),
|
|
705
705
|
userId: user?.id ?? "unknown",
|
|
706
|
-
sourceType: "Internal",
|
|
707
706
|
employeeName: getUserDisplayName(user),
|
|
708
707
|
updatedBy: getUserDisplayName(user),
|
|
709
708
|
updatedAt: now,
|
package/src/server/cache.ts
CHANGED
|
@@ -123,10 +123,6 @@ 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
|
-
}
|
|
130
126
|
}
|
|
131
127
|
return records
|
|
132
128
|
})()
|
|
@@ -166,18 +162,4 @@ export class SqlResultCache {
|
|
|
166
162
|
}
|
|
167
163
|
}
|
|
168
164
|
}
|
|
169
|
-
|
|
170
|
-
/**
|
|
171
|
-
* Cleans up all expired cache buckets to prevent memory accumulation.
|
|
172
|
-
* メモリー蓄積を防ぐため、期限切れのキャッシュバケットをすべてクリーンアップする処理。
|
|
173
|
-
*/
|
|
174
|
-
private cleanExpired(): void {
|
|
175
|
-
const now = Date.now()
|
|
176
|
-
// 期限切れのキーを削除
|
|
177
|
-
for (const [key, bucket] of this.buckets.entries()) {
|
|
178
|
-
if (bucket.expireAt <= now) {
|
|
179
|
-
this.buckets.delete(key)
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
165
|
}
|
package/src/server/service.ts
CHANGED
|
@@ -326,7 +326,6 @@ 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",
|
|
330
329
|
createdAt: formatDateValue(hub.createdAt, "YYYY-MM-DD HH:mm:ss"),
|
|
331
330
|
updatedAt: formatDateValue(hub.updatedAt, "YYYY-MM-DD HH:mm:ss"),
|
|
332
331
|
updatedBy: maskAuditActor(hub.updatedBy),
|
|
@@ -1035,7 +1034,6 @@ export function createDailyReportService(config: DailyReportServiceConfig) {
|
|
|
1035
1034
|
createdAt: formatDateValue(hub.createdAt, "YYYY-MM-DD HH:mm:ss"),
|
|
1036
1035
|
author: userName,
|
|
1037
1036
|
userId: encodeUserId(userId),
|
|
1038
|
-
sourceType: "Internal",
|
|
1039
1037
|
employeeName: userName,
|
|
1040
1038
|
// 監査列と同様、クライアント公開時は生内部 ID を難読化する (mapHubRecord と整合)
|
|
1041
1039
|
updatedBy: encodeUserId(userId),
|
package/src/shared/sse-schema.ts
CHANGED
|
@@ -39,7 +39,6 @@ export const dailyReportDetailSchema = z.object({
|
|
|
39
39
|
createdAt: z.string().nullable(),
|
|
40
40
|
author: z.string(),
|
|
41
41
|
userId: z.string(),
|
|
42
|
-
sourceType: z.string().nullable(),
|
|
43
42
|
employeeName: z.string().nullable(),
|
|
44
43
|
updatedBy: z.string().nullable(),
|
|
45
44
|
updatedAt: z.string().nullable(),
|
package/src/shared/types.ts
CHANGED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/*! @aiquants/daily-report standalone CSS — 非 Tailwind ホスト専用。ホストの Tailwind ビルドと混在させないこと */
|
|
2
|
+
@layer theme, base, components, utilities;
|
|
3
|
+
@import "tailwindcss/theme.css" layer(theme);
|
|
4
|
+
/* source(none): 自動ソース検出を無効化し、src 外のクラス混入を防ぐ */
|
|
5
|
+
@import "tailwindcss/utilities.css" layer(utilities) source(none);
|
|
6
|
+
/*
|
|
7
|
+
* virtualscroll の CSS は同梱しない: ホスト側で import 済みの新しいコピーを、
|
|
8
|
+
* ここに焼き込んだ古いコピーが後勝ちで上書きする事故 (stale-copy-wins) を防ぐため。
|
|
9
|
+
* ホストアプリは @aiquants/virtualscroll/styles/virtualscroll.css を自前で import すること。
|
|
10
|
+
*/
|
|
11
|
+
@source "../**/*.{ts,tsx}";
|
|
12
|
+
|
|
13
|
+
/* JSX は .dark クラス切替の dark: バリアントを使うため、ホストアプリと同じ定義が必要 */
|
|
14
|
+
@plugin "tailwindcss-animate";
|
|
15
|
+
@custom-variant dark (&:where(.dark, .dark *));
|
|
16
|
+
|
|
17
|
+
/*
|
|
18
|
+
* ホストアプリ (quants-react-router) の @theme と同じトークン定義を複製する。
|
|
19
|
+
* bg-background / text-muted-foreground などのセマンティックトークンは
|
|
20
|
+
* Tailwind 既定テーマに存在しないため、定義しないとユーティリティ自体が生成されない。
|
|
21
|
+
* 値はホストの CSS 変数 (--background 等) を参照し、実行時にホスト側で解決される。
|
|
22
|
+
*/
|
|
23
|
+
@theme {
|
|
24
|
+
--color-border: hsl(var(--border));
|
|
25
|
+
--color-input: hsl(var(--input));
|
|
26
|
+
--color-ring: hsl(var(--ring));
|
|
27
|
+
--color-background: hsl(var(--background));
|
|
28
|
+
--color-foreground: hsl(var(--foreground));
|
|
29
|
+
--color-primary: hsl(var(--primary));
|
|
30
|
+
--color-primary-foreground: hsl(var(--primary-foreground));
|
|
31
|
+
--color-secondary: hsl(var(--secondary));
|
|
32
|
+
--color-secondary-foreground: hsl(var(--secondary-foreground));
|
|
33
|
+
--color-destructive: hsl(var(--destructive));
|
|
34
|
+
--color-destructive-foreground: hsl(var(--destructive-foreground));
|
|
35
|
+
--color-muted: hsl(var(--muted));
|
|
36
|
+
--color-muted-foreground: hsl(var(--muted-foreground));
|
|
37
|
+
--color-accent: hsl(var(--accent));
|
|
38
|
+
--color-accent-foreground: hsl(var(--accent-foreground));
|
|
39
|
+
--color-popover: hsl(var(--popover));
|
|
40
|
+
--color-popover-foreground: hsl(var(--popover-foreground));
|
|
41
|
+
--color-card: hsl(var(--card));
|
|
42
|
+
--color-card-foreground: hsl(var(--card-foreground));
|
|
43
|
+
--breakpoint-sm: 640px;
|
|
44
|
+
--breakpoint-md: 768px;
|
|
45
|
+
--breakpoint-lg: 1024px;
|
|
46
|
+
--breakpoint-xl: 1280px;
|
|
47
|
+
--breakpoint-2xl: 1536px;
|
|
48
|
+
--radius-lg: var(--radius, 0.5rem);
|
|
49
|
+
--radius-md: calc(var(--radius, 0.5rem) - 2px);
|
|
50
|
+
--radius-sm: calc(var(--radius, 0.5rem) - 4px);
|
|
51
|
+
}
|
package/src/server/cache.spec.ts
DELETED
|
@@ -1,47 +0,0 @@
|
|
|
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
|
-
})
|