@aiquants/daily-report 0.12.0 → 0.12.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/dist/client.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/client.ts","../src/client/components/business-day-thumb-overlay.tsx","../src/client/components/daily-report-attachment-indicator.tsx","../src/client/config-context.tsx","../src/client/components/daily-report-attachment-list.tsx","../src/client/components/daily-report-comment-item.tsx","../src/shared/comment-adapter.ts","../src/shared/text-utils.ts","../src/client/ui/cn.ts","../src/client/components/daily-report-comment-section.tsx","../src/client/components/daily-report-detail-list.tsx","../src/client/contexts/daily-report-action-context.tsx","../src/shared/ids-stream.ts","../src/shared/logger.ts","../src/client/hooks/use-daily-report.ts","../src/shared/business-date.ts","../src/client/utils/constants.ts","../src/client/hooks/use-daily-report-sse-connection.ts","../src/shared/sse-schema.ts","../src/client/streaming/daily-report-ids-stream-session.ts","../src/client/streaming/daily-report-ids-stream-client.ts","../src/client/contexts/daily-report-error-context.tsx","../src/client/hooks/use-daily-report-comments.ts","../src/client/hooks/use-dynamic-viewport-height.ts","../src/client/components/daily-report-edit-form.tsx","../src/client/ui/button.tsx","../src/client/ui/input.tsx","../src/client/ui/label.tsx","../src/client/ui/textarea.tsx","../src/client/components/unread-indicator.tsx","../src/client/components/daily-report-ids-stream-status.tsx","../src/client/hooks/use-animated-number.ts","../src/client/components/daily-report-list.tsx","../src/client/hooks/use-responsive-layout.ts","../src/client/ui/tabs.tsx","../src/client/components/daily-report-page.tsx","../src/client/components/daily-report-resolved-content.tsx","../src/client/ui/switch.tsx","../src/client/route-helpers.ts"],"sourcesContent":["/**\n * Client entry of @aiquants/daily-report: React components, hooks, and route helpers.\n * @aiquants/daily-report の client エントリ。React コンポーネント・フック・ルートヘルパーを公開。\n */\nexport * from \"./client/components/business-day-thumb-overlay\"\nexport * from \"./client/components/daily-report-attachment-indicator\"\nexport * from \"./client/components/daily-report-attachment-list\"\nexport * from \"./client/components/daily-report-comment-item\"\nexport * from \"./client/components/daily-report-comment-section\"\nexport * from \"./client/components/daily-report-detail-list\"\nexport * from \"./client/components/daily-report-edit-form\"\nexport * from \"./client/components/daily-report-ids-stream-status\"\nexport * from \"./client/components/daily-report-list\"\nexport * from \"./client/components/daily-report-page\"\nexport * from \"./client/components/daily-report-resolved-content\"\nexport * from \"./client/components/unread-indicator\"\nexport * from \"./client/config-context\"\nexport * from \"./client/contexts/daily-report-action-context\"\nexport * from \"./client/contexts/daily-report-error-context\"\nexport * from \"./client/hooks/use-animated-number\"\nexport * from \"./client/hooks/use-daily-report\"\nexport * from \"./client/hooks/use-daily-report-comments\"\nexport * from \"./client/hooks/use-daily-report-sse-connection\"\nexport * from \"./client/hooks/use-dynamic-viewport-height\"\nexport * from \"./client/route-helpers\"\nexport * from \"./client/streaming/daily-report-ids-stream-client\"\nexport * from \"./client/streaming/daily-report-ids-stream-session\"\nexport * from \"./client/utils/constants\"\n","import type { ScrollBarThumbOverlayRenderProps } from \"@aiquants/virtualscroll\"\nimport type { CSSProperties } from \"react\"\n\n/**\n * Tooltip bubble that anchors to the scrollbar thumb while interacting.\n * スクロール操作中にスクロールバーのサムへ連動表示するツールチップのコンポーネント。\n */\nexport const BusinessDayThumbOverlay = ({ orientation, thumbCenter, label, isDragging, isTapScrollActive, isWheelScrollActive }: ScrollBarThumbOverlayRenderProps & { label: string; isWheelScrollActive: boolean }) => {\n if (!(label.trim() && (isDragging || isTapScrollActive || isWheelScrollActive))) return null\n const style: CSSProperties = orientation === \"vertical\" ? { top: thumbCenter, left: -16, transform: \"translate(-100%, -50%)\" } : { left: thumbCenter, top: -16, transform: \"translate(-50%, -100%)\" }\n\n return (\n <div className=\"pointer-events-none absolute flex items-center\" style={style}>\n <div className=\"rounded-full border border-slate-200 bg-white px-3 py-1 text-slate-700 text-xs shadow-md shadow-slate-400/30 dark:border-slate-700 dark:bg-slate-900/90 dark:text-slate-100\">\n <span className=\"whitespace-nowrap font-semibold text-slate-800 dark:text-slate-100\">{label.trim()}</span>\n </div>\n </div>\n )\n}\n","/**\n * Compact \"has attachments\" marker for summary rows.\n * 一覧の要約行に出す「添付あり」の小さな目印。\n *\n * 一覧をスクロールしながら添付の有無を一目で判断できるようにするためのもの。\n * 詳細カードの添付節 (`DailyReportAttachmentList`) とは役割が異なり、\n * **ファイル名も配信 URL も出さない**。行に出すのは有無と件数だけであり、\n * リンクを置くと一覧のタップ領域と競合するうえ、配信エンドポイントを\n * 一覧から誘発しやすくなる (原寸をサーバーのヒープへ載せる経路である)。\n *\n * DOM フックは `data-*` 属性のみを使う (AGENTS.md)。\n */\nimport { Paperclip } from \"lucide-react\"\n\n/** 件数を併記し始める閾値。1 件のときは記号だけで足りる。 */\nconst COUNT_VISIBLE_FROM = 2\n\ntype DailyReportAttachmentIndicatorProps = {\n /** 添付の件数。0 なら何も描画しない。 */\n count: number\n /** 読み上げ・ツールチップ用の見出し。消費アプリの `fieldLabels.attachments` を渡す。 */\n label: string\n}\n\n/**\n * Renders the attachment marker, or nothing when the report has no attachment.\n * 添付の目印を描画する (添付が無ければ何も描画しない)。\n *\n * @param props Attachment count and the label. 添付件数と見出し。\n * @returns The marker element, or null. 目印の要素 (添付が無ければ null)。\n */\nexport const DailyReportAttachmentIndicator = ({ count, label }: DailyReportAttachmentIndicatorProps) => {\n if (count <= 0) {\n return null\n }\n\n // role=\"img\" を付けるのは aria-label を有効にするため (role の無い span では無効になる)。\n // 中身は記号 1 つで、読み上げ上も 1 つの図像として扱うのが適切。\n return (\n <span className=\"inline-flex shrink-0 items-center gap-0.5 rounded px-1 py-0.5 text-slate-500 dark:text-slate-400\" title={`${label}: ${count}`} role=\"img\" aria-label={`${label}: ${count}`} data-testid=\"daily-report-attachment-indicator\" data-attachment-count={count}>\n <Paperclip className=\"h-3.5 w-3.5\" aria-hidden=\"true\" />\n {count >= COUNT_VISIBLE_FROM && <span className=\"font-semibold text-[10px] leading-none\">{count}</span>}\n </span>\n )\n}\n","/**\n * Client-side DI configuration for the daily-report UI (paths, chrome slots, labels).\n * 日報 UI のクライアント側 DI 設定 (パス・クローム差し込み・ラベル名) を提供するコンテキスト。\n */\nimport { createContext, type ReactNode, useContext, useMemo } from \"react\"\n\n/** ヘッダー描画スロットに渡される引数。 */\nexport type DailyReportHeaderProps = {\n title: string\n annotation?: ReactNode\n}\n\n/**\n * Display labels for the report detail/list fields, sections, and side-pane tabs.\n * 日報の詳細/一覧カードのフィールド・セクション・タブ見出しの表示ラベル群。\n *\n * パッケージは中立な英語ラベルのみを既定として同梱し、ドメイン固有の表現 (言語・業務語彙) は\n * 消費アプリが `fieldLabels` で注入する。これによりパッケージ本体が業務ドメイン知識を持たない。\n */\nexport type DailyReportFieldLabels = {\n id: string\n businessDate: string\n author: string\n visitTime: string\n createdAt: string\n updatedAt: string\n updatedBy: string\n category: string\n categoryInfo: string\n creationCategory: string\n customer: string\n subject: string\n content: string\n interviewers: string\n comments: string\n attachments: string\n /** 実体の消失が確認された添付に添える注記。 */\n attachmentMissing: string\n /** 添付をダウンロードするリンクのラベル。 */\n attachmentDownload: string\n tabArticle: string\n tabRelations: string\n relationsEmpty: string\n}\n\n/**\n * Mutation-failure notification payload (for a host-supplied error surface).\n * ミューテーション失敗の通知情報 (ホストが独自の通知 UI を出すための口)。\n */\nexport type DailyReportErrorInfo = {\n /** 失敗した操作の種別 (create / update / publish / delete / addComment / deleteComment / toggleStar / toggleRead)。 */\n operation: string\n /** 表示用のメッセージ (パッケージ内蔵バナー用)。 */\n message: string\n /** 元例外 (診断用)。 */\n cause?: unknown\n}\n\n/** データソースごとのバッジ表示設定。 */\nexport type SourceTypeConfig = {\n label: string\n badgeClassName?: string\n /** コメント投稿を許可するかどうか (既定 true)。false の場合、該当ソースの日報でコメント投稿フォームを非表示にする。 */\n allowComment?: boolean\n}\n\n/** クライアント層の設定一式。 */\nexport type DailyReportClientConfig = {\n /** 画面タイトル (ヘッダーへ渡す)。 */\n title: string\n /** API ベースパス (例 `/daily_report/api`)。末尾スラッシュ無し。 */\n apiBasePath: string\n /** SSE エンドポイントパス (例 `/sse/daily_report/updates`)。 */\n ssePath: string\n /** 下書き状態を表すラベル名。UI の編集モード判定に使う。既定はプレースホルダーなので消費側で自アプリのラベル名へ上書きする。 */\n draftLabelName: string\n /** 詳細/一覧カード of field 見出しラベル。既定は中立英語。アプリが自ドメインの表記を注入する。 */\n fieldLabels: DailyReportFieldLabels\n /** 外部ソース区分ごとのバッジ表示設定。 */\n sourceTypeConfigs?: Record<string, SourceTypeConfig>\n /** ヘッダー描画スロット。アプリのヘッダーコンポーネントを差し込む。 */\n renderHeader: (props: DailyReportHeaderProps) => ReactNode\n /** 開発向けコントロール (SSE 購読トグル等) を表示するか。 */\n showDevControls: boolean\n /**\n * ミューテーション失敗時の通知ポート。未指定ならパッケージ内蔵の role=\"alert\"\n * バナーで表示する。ホストが独自トースト等を持つ場合はここへ委譲する。\n */\n onError?: (info: DailyReportErrorInfo) => void\n}\n\n/**\n * Partial client-config override. `fieldLabels` may itself be partially overridden.\n * クライアント設定の部分上書き型。`fieldLabels` も部分上書き可 (プロバイダーが深くマージする)。\n */\nexport type DailyReportClientConfigInput = Partial<Omit<DailyReportClientConfig, \"fieldLabels\">> & {\n fieldLabels?: Partial<DailyReportFieldLabels>\n sourceTypeConfigs?: Record<string, SourceTypeConfig>\n}\n\n/** 中立英語のフィールドラベル既定 (ドメイン非依存)。 */\nexport const defaultDailyReportFieldLabels: DailyReportFieldLabels = {\n id: \"ID\",\n businessDate: \"Business date\",\n author: \"Author\",\n visitTime: \"Visit time\",\n createdAt: \"Created at\",\n updatedAt: \"Updated at\",\n updatedBy: \"Updated by\",\n category: \"Category\",\n categoryInfo: \"Category\",\n creationCategory: \"Creation category\",\n customer: \"Customer\",\n subject: \"Subject\",\n content: \"Content\",\n interviewers: \"Attendees\",\n comments: \"Comments\",\n attachments: \"Attachments\",\n attachmentMissing: \"File is missing\",\n attachmentDownload: \"Download\",\n tabArticle: \"Overview\",\n tabRelations: \"Related\",\n relationsEmpty: \"No related view configured yet.\",\n}\n\n/** 既定設定 (消費側は必要なフィールドのみ部分上書きする)。 */\nexport const defaultDailyReportClientConfig: DailyReportClientConfig = {\n title: \"Daily Reports\",\n apiBasePath: \"/daily_report/api\",\n ssePath: \"/sse/daily_report/updates\",\n // 中立プレースホルダー。実運用のラベル名 (アプリ固有) は Provider / config prop で上書きする。\n draftLabelName: \"draft\",\n fieldLabels: defaultDailyReportFieldLabels,\n sourceTypeConfigs: {\n Internal: {\n label: \"オリジナル\",\n badgeClassName: \"bg-emerald-100 text-emerald-800 dark:bg-emerald-900/30 dark:text-emerald-400\",\n },\n },\n renderHeader: ({ title, annotation }) => (\n <header className=\"flex flex-wrap items-baseline justify-between gap-2 px-2 py-1\">\n <span className=\"text-nowrap text-slate-700 text-sm dark:text-slate-200\">{title}</span>\n {annotation ? <span className=\"text-slate-600 text-xs dark:text-slate-300\">{annotation}</span> : null}\n </header>\n ),\n showDevControls: false,\n}\n\nconst DailyReportConfigContext = createContext<DailyReportClientConfig>(defaultDailyReportClientConfig)\n\n/**\n * Provides a partial override of the daily-report client configuration.\n * 日報クライアント設定の部分上書きを提供するプロバイダー。\n */\nexport const DailyReportConfigProvider = ({ config, children }: { config?: DailyReportClientConfigInput; children: ReactNode }) => {\n // 部分設定を既定値へ浅くマージする (fieldLabels のみ 1 段深くマージして部分上書きを許す)\n const merged = useMemo<DailyReportClientConfig>(\n () => ({\n ...defaultDailyReportClientConfig,\n ...config,\n fieldLabels: { ...defaultDailyReportFieldLabels, ...(config?.fieldLabels ?? {}) },\n }),\n [config],\n )\n return <DailyReportConfigContext.Provider value={merged}>{children}</DailyReportConfigContext.Provider>\n}\n\n/**\n * Reads the daily-report client configuration (defaults when no provider).\n * 日報クライアント設定を読む (プロバイダー未設置時は既定値)。\n */\nexport const useDailyReportConfig = (): DailyReportClientConfig => useContext(DailyReportConfigContext)\n\n/**\n * Resolves source type badge configuration with case-insensitive fallback.\n * ケースインセンシティブなフォールバック付きでデータソースバッジ設定を解決する。\n */\nexport const resolveSourceTypeConfig = (sourceType?: string | null, sourceTypeConfigs?: Record<string, SourceTypeConfig>): SourceTypeConfig | undefined => {\n if (!(sourceType && sourceTypeConfigs)) return undefined\n if (sourceTypeConfigs[sourceType]) return sourceTypeConfigs[sourceType]\n const lower = sourceType.toLowerCase()\n const matchedKey = Object.keys(sourceTypeConfigs).find((k) => k.toLowerCase() === lower)\n return matchedKey ? sourceTypeConfigs[matchedKey] : undefined\n}\n","/**\n * Attachment list rendered inside a daily report detail card.\n * 日報詳細カード内に描画する添付ファイル一覧。\n *\n * このコンポーネントは詳細カードの本体 (`DailyReportDetailRow` の children) の内側に置くこと。\n * 行の高さは行ごとの `ResizeObserver` が測って仮想スクロールへ通知するため、\n * portal や absolute 配置で測定コンテナの外へ出すと行高が壊れ、\n * タブ切替時のスクロール位置保存 E2E も落ちる。\n *\n * **ネットワーク要求を自動発火させないこと。** 配信エンドポイントは 1 リクエストごとに\n * 実体全体をサーバーのヒープへ載せる経路で、レート上限と同時実行ゲートを持つ。\n * ここで `<img src={配信URL}>` のような自動取得を置くと、仮想スクロールの再マウントの\n * たびに実体 (数十 MiB) を取り直し、利用者自身のダウンロードを 429 / 503 で潰す。\n * サムネイルを出すなら縮小画像を返す専用経路を用意してからにする。\n *\n * DOM フックは `data-*` 属性のみを使う。クラス名はスタイリング契約であり\n * 選択子として使ってはならない (AGENTS.md)。\n */\nimport type { DailyReportAttachmentSummary } from \"../../shared/types\"\nimport { useDailyReportConfig } from \"../config-context\"\n\n/** 1 KiB。サイズ表記の基数。 */\nconst BYTES_PER_UNIT = 1024\n\n/** サイズ表記の単位列。 */\nconst SIZE_UNITS = [\"B\", \"KB\", \"MB\", \"GB\"] as const\n\n/**\n * Formats a byte count for display, or returns null when unknown.\n * バイト数を表示用に整形する。不明なら null を返す。\n *\n * サイズを報告しない取込元の添付は null になるため、0 と null を区別する\n * (0 は「空ファイル」、null は「サイズ不明」)。\n *\n * @param bytes Size in bytes, or null when unknown. バイト数 (不明なら null)。\n * @returns Human-readable size, or null. 可読なサイズ表記 (不明なら null)。\n */\nconst formatFileSize = (bytes: number | null): string | null => {\n if (bytes === null || !Number.isFinite(bytes) || bytes < 0) return null\n let value = bytes\n let unitIndex = 0\n while (value >= BYTES_PER_UNIT && unitIndex < SIZE_UNITS.length - 1) {\n value /= BYTES_PER_UNIT\n unitIndex += 1\n }\n // 1 KB 未満は小数を出さない\n return unitIndex === 0 ? `${value} ${SIZE_UNITS[unitIndex]}` : `${value.toFixed(1)} ${SIZE_UNITS[unitIndex]}`\n}\n\ntype DailyReportAttachmentListProps = {\n /** 表示する添付一覧。空配列ならこのコンポーネントは何も描画しない。 */\n attachments: DailyReportAttachmentSummary[]\n}\n\n/**\n * Renders the attachment section of a daily report detail card.\n * 日報詳細カードの添付ファイル節を描画するコンポーネント。\n *\n * 表示文言はすべて `fieldLabels` から取る (パッケージは業務ドメインの語彙も言語も持たない)。\n *\n * @param props Attachments to render. 描画する添付一覧。\n * @returns The section element, or null when there is nothing to show. 節の要素 (表示対象が無ければ null)。\n */\nexport const DailyReportAttachmentList = ({ attachments }: DailyReportAttachmentListProps) => {\n const { apiBasePath, fieldLabels } = useDailyReportConfig()\n\n if (attachments.length === 0) {\n return null\n }\n\n return (\n <div className=\"flex flex-col gap-2\" data-testid=\"daily-report-attachment-section\">\n <span className=\"font-semibold text-slate-500 text-xs uppercase tracking-wide dark:text-slate-300\">{fieldLabels.attachments}</span>\n <ul className=\"flex flex-col gap-1.5\">\n {attachments.map((attachment) => {\n // URL は必ず apiBasePath から組む。パッケージ内にルートを焼き込まない\n const href = `${apiBasePath}/attachment/${attachment.id}`\n const size = formatFileSize(attachment.fileSize)\n const isMissing = attachment.state === \"absent\"\n\n return (\n <li key={attachment.id} className=\"flex items-center gap-2 rounded-lg bg-slate-50 px-3 py-2 dark:bg-slate-800/40\" data-testid=\"daily-report-attachment-item\" data-attachment-state={attachment.state}>\n <div className=\"flex min-w-0 flex-col\">\n {isMissing ? (\n // 消失が確認された添付はリンクにしない (押しても 404 になるだけ)\n <span className=\"truncate text-slate-400 text-sm line-through dark:text-slate-500\" data-testid=\"daily-report-attachment-name\">\n {attachment.fileName}\n </span>\n ) : (\n <a href={href} className=\"truncate text-blue-600 text-sm hover:underline dark:text-blue-400\" data-testid=\"daily-report-attachment-link\">\n {attachment.fileName}\n </a>\n )}\n <span className=\"text-[10px] text-slate-400 dark:text-slate-500\">\n {size ?? \"—\"}\n {/* 実体の消失を確認できた場合のみ注記する。unknown は「未検証」であって\n 「消失」ではないため、通常表示のままにする (判定不能を偽の真偽で埋めない) */}\n {isMissing && ` · ${fieldLabels.attachmentMissing}`}\n </span>\n </div>\n {!isMissing && (\n <a href={`${href}?download=1`} className=\"ml-auto shrink-0 text-[10px] text-slate-500 hover:text-slate-700 hover:underline dark:text-slate-400 dark:hover:text-slate-200\" data-testid=\"daily-report-attachment-download\">\n {fieldLabels.attachmentDownload}\n </a>\n )}\n </li>\n )\n })}\n </ul>\n </div>\n )\n}\n","import { TrashIcon } from \"@heroicons/react/24/outline\"\nimport { useState } from \"react\"\nimport { resolveCommentColorClass, type UIComment } from \"../../shared/comment-adapter\"\nimport { fallbackText, formatToIsoDate } from \"../../shared/text-utils\"\nimport { cn } from \"../ui/cn\"\n\ntype DailyReportCommentItemProps = {\n comment: UIComment\n reportHubId: number\n businessDate: string\n onOptimisticDelete: (commentId: number) => void\n pendingDeleteId: number | string | null\n onPendingDeleteIdChange: (id: number | string | null) => void\n onTrashClick: (e: React.MouseEvent | React.TouchEvent, commentId: number | string) => void\n resolvedIdMap?: Map<number, number>\n}\n\n/**\n * Checks if the current comment is in a pending delete state, considering temporary ID resolution.\n * 現在のコメントが削除保留状態かどうかを判定する。一時IDの解決も考慮する。\n */\nconst checkIsPendingDelete = (commentId: number | string, pendingDeleteId: number | string | null, resolvedIdMap?: Map<number, number>): boolean => {\n if (pendingDeleteId === commentId) return true\n // Case: commentId is temp, pendingDeleteId is resolved real ID\n if (typeof commentId === \"number\" && commentId < 0 && resolvedIdMap?.get(commentId) === pendingDeleteId) return true\n // Case: pendingDeleteId is temp, commentId is resolved real ID\n if (typeof pendingDeleteId === \"number\" && pendingDeleteId < 0 && resolvedIdMap?.get(pendingDeleteId) === commentId) return true\n return false\n}\n\n/**\n * Renders a single comment item with delete functionality.\n * 削除機能付きの単一コメントアイテムを描画するコンポーネント。\n */\nexport const DailyReportCommentItem = ({ comment, reportHubId: _reportHubId, businessDate: _businessDate, onOptimisticDelete, pendingDeleteId, onPendingDeleteIdChange, onTrashClick, resolvedIdMap }: DailyReportCommentItemProps) => {\n const [isDeleted, setIsDeleted] = useState(false)\n\n const isPendingDelete = checkIsPendingDelete(comment.id, pendingDeleteId, resolvedIdMap)\n\n const handleConfirmDelete = (e: React.MouseEvent | React.TouchEvent) => {\n e.stopPropagation()\n e.preventDefault()\n\n onPendingDeleteIdChange(null)\n setIsDeleted(true)\n\n if (typeof comment.id === \"number\") {\n // Optimistic UI Update (API call is handled by parent hook)\n onOptimisticDelete(comment.id)\n }\n }\n\n return (\n <li\n data-comment-id={comment.id}\n data-deleted={isDeleted ? \"true\" : undefined}\n className={cn(\n \"group relative rounded-xl border border-slate-200 bg-white p-3 dark:border-slate-700 dark:bg-slate-900/70\",\n comment.isMine && \"border-blue-200 bg-blue-50 dark:border-blue-800/70 dark:bg-blue-950/40\",\n isDeleted && \"hidden\",\n isPendingDelete ? \"z-50\" : \"z-0 hover:z-10\",\n )}>\n <div className=\"mb-1 flex items-center justify-between\">\n <div className={cn(\"font-semibold text-xs\", resolveCommentColorClass(comment.color))}>{fallbackText(comment.authorName)}</div>\n <div className=\"flex items-center gap-2\">\n {comment.createdAt && <div className=\"text-[10px] text-slate-400\">{formatToIsoDate(comment.createdAt)}</div>}\n {comment.isMine && !comment.isLegacy && (\n <div className=\"relative\">\n <button type=\"button\" className={cn(\"transition-colors\", isPendingDelete ? \"text-red-500\" : \"text-slate-400 hover:text-red-500\")} title=\"削除\" aria-label=\"削除\" onClick={(e) => onTrashClick(e, comment.id)}>\n <TrashIcon className=\"h-4 w-4\" />\n </button>\n\n {isPendingDelete && (\n <button\n type=\"button\"\n data-delete-confirmation\n className=\"fade-in zoom-in-95 absolute top-full right-0 z-20 mt-1 animate-in whitespace-nowrap rounded-md bg-red-600 px-3 py-1.5 font-bold text-white text-xs shadow-lg duration-200 hover:bg-red-700 active:scale-95\"\n onClick={handleConfirmDelete}>\n 削除する\n </button>\n )}\n </div>\n )}\n </div>\n </div>\n <div className=\"whitespace-pre-wrap text-slate-700 text-sm leading-relaxed dark:text-slate-200\">{comment.content}</div>\n </li>\n )\n}\n","/**\n * Adapters merging legacy JSON comments and relational comments for the UI.\n * レガシー JSON コメントとリレーショナルコメントを UI 向けに統合するアダプタ群。\n */\nimport type { DailyReportComment, DailyReportCommentItem } from \"./types\"\n\nexport type UIComment = {\n id: string | number\n authorName: string\n content: string\n createdAt?: string\n isMine: boolean\n color?: string\n isLegacy: boolean\n}\n\n/**\n * Merges legacy JSON comments and modern relational comments into a unified UI format.\n * レガシーな JSON コメントとモダンなリレーショナルコメントを統合された UI 形式にマージする。\n */\nexport const mergeComments = (legacyComments: DailyReportComment[], modernComments: DailyReportCommentItem[], currentUserId?: string | null): UIComment[] => {\n // 1. Legacy コメントの変換\n const legacyUIComments: UIComment[] = legacyComments.map((c, index) => ({\n id: `legacy-${index}`,\n authorName: c.name || \"Unknown\",\n content: c.text || \"\",\n isMine: false,\n color: c.color || undefined,\n isLegacy: true,\n }))\n\n // 2. Modern コメントの変換\n const modernUIComments: UIComment[] = modernComments.map((c) => ({\n id: c.id,\n authorName: c.userName ?? \"\",\n content: c.content ?? \"\",\n createdAt: c.createdAt ?? \"\",\n isMine: currentUserId ? c.userId === currentUserId : c.isMine,\n isLegacy: false,\n }))\n\n // 3. 結合 (Legacy を先に表示)\n return [...legacyUIComments, ...modernUIComments]\n}\n\n/**\n * Resolves comment color tokens into Tailwind text classes.\n * コメント色のキーワードを Tailwind のテキストクラスに変換する関数。\n */\nexport const resolveCommentColorClass = (color: string | null | undefined): string => {\n // 各色にダークモードの明色対応を必ず添える。ダークカード上で暗色トークン\n // (text-gray-700 など) 単体だと文字が背景に沈んで読めない\n if (!color) {\n return \"text-gray-700 dark:text-gray-300\"\n }\n\n const normalized = color.toLowerCase()\n\n if (normalized.includes(\"red\") || normalized.includes(\"赤\")) {\n return \"text-red-600 dark:text-red-400\"\n }\n\n if (normalized.includes(\"green\") || normalized.includes(\"緑\")) {\n return \"text-green-600 dark:text-green-400\"\n }\n\n if (normalized.includes(\"blue\") || normalized.includes(\"青\")) {\n return \"text-blue-600 dark:text-blue-400\"\n }\n\n if (normalized.includes(\"yellow\") || normalized.includes(\"黄\")) {\n return \"text-yellow-600 dark:text-yellow-400\"\n }\n\n if (normalized.includes(\"orange\") || normalized.includes(\"橙\")) {\n return \"text-orange-500 dark:text-orange-400\"\n }\n\n return \"text-gray-700 dark:text-gray-300\"\n}\n","/**\n * Shared text helpers for daily report view components.\n * 日報ビューコンポーネント向けの共通テキストヘルパー。\n */\n\n/**\n * Normalizes nullable text by trimming whitespace and applying fallback.\n * null 許容の文字列をトリムしつつフォールバック値を適用する関数。\n */\nexport const fallbackText = (value: string | null | undefined): string => {\n if (!value) {\n return \"-\"\n }\n const trimmed = value.trim()\n return trimmed === \"\" ? \"-\" : trimmed\n}\n\n/**\n * Formats a date string to ISO 8601 format (YYYY-MM-DD or YYYY-MM-DDTHH:mm:ss).\n * 日付文字列を ISO 8601 形式 (YYYY-MM-DD または YYYY-MM-DDTHH:mm:ss) に整形する関数。\n */\nexport const formatToIsoDate = (value: unknown): string => {\n if (!value) {\n return \"-\"\n }\n if (typeof value !== \"string\") {\n // If value is not a string (e.g. Date object), try to convert it to string or handle it\n if (value instanceof Date) {\n return formatToIsoDate(value.toISOString())\n }\n return String(value)\n }\n const trimmed = value.trim()\n if (trimmed === \"\") {\n return \"-\"\n }\n\n // 日付のみ (YYYY-MM-DD / YYYY/MM/DD) は Date を経由せず整形する。\n // `new Date(\"2026-08-08\")` は UTC 深夜と解釈され、ローカル getter で負オフセット圏\n // (米国など) では前日へずれるため。\n const dateOnly = trimmed.match(/^(\\d{4})[-/](\\d{2})[-/](\\d{2})$/)\n if (dateOnly) {\n return `${dateOnly[1]}-${dateOnly[2]}-${dateOnly[3]}`\n }\n\n const date = new Date(trimmed)\n if (Number.isNaN(date.getTime())) {\n return trimmed\n }\n\n const year = date.getFullYear()\n const month = String(date.getMonth() + 1).padStart(2, \"0\")\n const day = String(date.getDate()).padStart(2, \"0\")\n\n const hasTime = trimmed.includes(\":\") || trimmed.includes(\"T\")\n\n if (hasTime) {\n const hours = String(date.getHours()).padStart(2, \"0\")\n const minutes = String(date.getMinutes()).padStart(2, \"0\")\n const seconds = String(date.getSeconds()).padStart(2, \"0\")\n return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}`\n }\n\n return `${year}-${month}-${day}`\n}\n","/**\n * Tailwind class combiner (clsx + tailwind-merge).\n * Tailwind クラスを結合・重複解決するユーティリティ。\n */\nimport { type ClassValue, clsx } from \"clsx\"\nimport { twMerge } from \"tailwind-merge\"\n\n/**\n * Combines class values and resolves Tailwind conflicts.\n * クラス値を結合し Tailwind の衝突を解決する処理。\n */\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs))\n}\n","import { useCallback, useEffect, useRef, useState } from \"react\"\nimport type { UIComment } from \"../../shared/comment-adapter\"\nimport { DailyReportCommentItem } from \"./daily-report-comment-item\"\n\n/**\n * Custom hook to manage the auto-reset behavior of the pending delete state.\n * 削除保留状態の自動リセット(タイマーおよび外部クリック)を管理するカスタムフック。\n */\nconst useAutoResetPendingDelete = (pendingDeleteId: number | string | null, onPendingDeleteIdChange: (id: number | string | null) => void) => {\n const timerRef = useRef<number | null>(null)\n\n /**\n * Effect to handle global clicks and reset pending delete state if clicked outside.\n * 外部クリックを監視し、削除確認ボタン以外がクリックされた場合に削除保留状態をリセットする副作用。\n *\n * Purpose: To cancel the delete confirmation mode when the user interacts with other parts of the UI.\n * Dependencies: [pendingDeleteId, onPendingDeleteIdChange] - Re-runs when the pending ID changes.\n * Cleanup: Removes event listeners for mousedown and touchstart.\n */\n useEffect(() => {\n if (pendingDeleteId === null) return\n\n const handleGlobalClick = (e: Event) => {\n const target = e.target as HTMLElement\n // Ignore clicks inside the delete confirmation button\n if (target.closest(\"[data-delete-confirmation]\")) return\n\n onPendingDeleteIdChange(null)\n }\n\n document.addEventListener(\"mousedown\", handleGlobalClick)\n document.addEventListener(\"touchstart\", handleGlobalClick)\n\n return () => {\n document.removeEventListener(\"mousedown\", handleGlobalClick)\n document.removeEventListener(\"touchstart\", handleGlobalClick)\n }\n }, [pendingDeleteId, onPendingDeleteIdChange])\n\n /**\n * Effect to clean up the timer on unmount.\n * アンマウント時にタイマーをクリーンアップする副作用。\n *\n * Purpose: To prevent memory leaks and state updates on unmounted components.\n * Dependencies: [] - Runs only on mount/unmount.\n * Cleanup: Clears the timeout if it exists.\n */\n useEffect(() => {\n return () => {\n if (timerRef.current) clearTimeout(timerRef.current)\n }\n }, [])\n\n const handleTrashClick = useCallback(\n (e: React.MouseEvent | React.TouchEvent, commentId: number | string) => {\n e.stopPropagation()\n e.preventDefault()\n\n if (pendingDeleteId === commentId) return\n\n onPendingDeleteIdChange(commentId)\n\n if (timerRef.current) clearTimeout(timerRef.current)\n // Auto-reset after 5 seconds\n timerRef.current = window.setTimeout(() => {\n onPendingDeleteIdChange(null)\n timerRef.current = null\n }, 5000)\n },\n [pendingDeleteId, onPendingDeleteIdChange],\n )\n\n return { handleTrashClick }\n}\n\ntype DailyReportCommentListProps = {\n comments: UIComment[]\n reportHubId: number\n businessDate: string\n onDeleteComment?: (commentId: number) => void\n pendingDeleteId: number | string | null\n onPendingDeleteIdChange: (id: number | string | null) => void\n resolvedIdMap?: Map<number, number>\n}\n\n/**\n * Renders a list of daily report comments.\n * 日報コメントのリストを描画するコンポーネント。\n */\nexport const DailyReportCommentList = ({ comments, reportHubId, businessDate, onDeleteComment, pendingDeleteId, onPendingDeleteIdChange, resolvedIdMap }: DailyReportCommentListProps) => {\n const { handleTrashClick } = useAutoResetPendingDelete(pendingDeleteId, onPendingDeleteIdChange)\n\n if (comments.length === 0) return null\n\n return (\n <ul className=\"mb-3 space-y-2\">\n {comments.map((comment) => (\n <DailyReportCommentItem\n key={comment.id}\n comment={comment}\n reportHubId={reportHubId}\n businessDate={businessDate}\n onOptimisticDelete={onDeleteComment || (() => {})}\n pendingDeleteId={pendingDeleteId}\n onPendingDeleteIdChange={onPendingDeleteIdChange}\n onTrashClick={handleTrashClick}\n resolvedIdMap={resolvedIdMap}\n />\n ))}\n </ul>\n )\n}\n\ntype DailyReportCommentFormProps = {\n reportHubId: number\n businessDate: string\n onAddComment?: (content: string) => Promise<void> | void\n}\n\n/**\n * Renders a form to add a new comment to a daily report.\n * 日報に新しいコメントを追加するためのフォームを描画するコンポーネント。\n */\nexport const DailyReportCommentForm = ({ reportHubId, businessDate, onAddComment }: DailyReportCommentFormProps) => {\n const [isSubmitting, setIsSubmitting] = useState(false)\n const formRef = useRef<HTMLFormElement>(null)\n\n const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {\n e.preventDefault()\n const formData = new FormData(e.currentTarget)\n const content = (formData.get(\"content\") as string)?.trim()\n\n if (!(content && onAddComment)) return\n\n setIsSubmitting(true)\n formRef.current?.reset()\n try {\n await onAddComment(content)\n } catch {\n if (formRef.current) {\n const input = formRef.current.elements.namedItem(\"content\") as HTMLInputElement\n if (input) input.value = content\n }\n } finally {\n setIsSubmitting(false)\n }\n }\n\n return (\n <form className=\"flex gap-2\" ref={formRef} onSubmit={handleSubmit}>\n <input type=\"hidden\" name=\"intent\" value=\"addComment\" />\n <input type=\"hidden\" name=\"reportHubId\" value={reportHubId} />\n <input type=\"hidden\" name=\"businessDate\" value={businessDate} />\n <input\n type=\"text\"\n name=\"content\"\n placeholder=\"コメントを追加...\"\n className=\"flex-1 rounded-lg border border-slate-300 bg-slate-50 px-3 py-2 text-sm focus:border-blue-500 focus:bg-white focus:outline-none dark:border-slate-700 dark:bg-slate-800 dark:text-slate-200\"\n autoComplete=\"off\"\n />\n <button type=\"submit\" className=\"rounded-lg bg-blue-600 px-4 py-2 font-medium text-sm text-white hover:bg-blue-700 disabled:opacity-50\" disabled={isSubmitting}>\n 送信\n </button>\n </form>\n )\n}\n","/**\n * Virtual-scroll based detailed daily report view backed by fetcher-driven data hooks.\n * useFetcher ベースのデータフックで日報詳細を提供する仮想スクロールビュー。\n */\n\nimport { type ScrollBarThumbOverlayRenderProps, VirtualScroll, type VirtualScrollHandle } from \"@aiquants/virtualscroll\"\n\n/**\n * Handle type augmented with E2E test helper methods.\n * E2E テスト用ヘルパーメソッドで拡張されたハンドル型。\n */\ntype DailyReportVirtualScrollHandle = VirtualScrollHandle & {\n findReportIndex: (id: number) => number\n getReportItem: (index: number) => DailyReportItem | undefined\n selectItem: (id: number) => void\n}\n\nimport { Check, Star, Trash2 } from \"lucide-react\"\nimport { type RefObject, useCallback, useEffect, useMemo, useRef, useState } from \"react\"\nimport { fallbackText, formatToIsoDate } from \"../../shared/text-utils\"\nimport type { DailyReportDetail, DailyReportItem } from \"../../shared/types\"\nimport { type DailyReportFieldLabels, resolveSourceTypeConfig, useDailyReportConfig } from \"../config-context\"\nimport { useDailyReportActionContext } from \"../contexts/daily-report-action-context\"\nimport { useDailyReportDetail, useDailyReportPrefetch } from \"../hooks/use-daily-report\"\nimport { useDailyReportComments } from \"../hooks/use-daily-report-comments\"\nimport { useDynamicViewportHeight } from \"../hooks/use-dynamic-viewport-height\"\nimport { cn } from \"../ui/cn\"\nimport { CONTENT_DRAG_POINTER_INPUTS, EMPTY_ITEM, FAST_SCROLL_INERTIA_OPTIONS, FAST_SCROLL_WHEEL_MULTIPLIER, PREFETCH_LOOKAHEAD, PREFETCH_MAX_DATES, SCROLL_BAR_WIDTH, TAP_SCROLL_CIRCLE_OPTIONS, VIRTUAL_SCROLL_OVERSCAN_COUNT, WHEEL_RESET_TIMEOUT_MS } from \"../utils/constants\"\nimport { BusinessDayThumbOverlay } from \"./business-day-thumb-overlay\"\nimport { DailyReportAttachmentIndicator } from \"./daily-report-attachment-indicator\"\nimport { DailyReportAttachmentList } from \"./daily-report-attachment-list\"\nimport { DailyReportCommentForm, DailyReportCommentList } from \"./daily-report-comment-section\"\nimport { DailyReportEditForm } from \"./daily-report-edit-form\"\nimport { UnreadIndicator } from \"./unread-indicator\"\n\n/**\n * Observes container height changes and reports measured values upstream.\n * コンテナの高さ変化を監視し測定結果を親へ通知するフック。\n */\nconst useReportItemHeightObserver = (ref: RefObject<HTMLDivElement | null>, itemId: number, onHeightMeasured: (id: number, height: number) => void) => {\n useEffect(() => {\n const element = ref.current\n if (!element) return\n\n const resizeObserver = new ResizeObserver(() => {\n onHeightMeasured(itemId, element.offsetHeight)\n })\n resizeObserver.observe(element)\n return () => resizeObserver.disconnect()\n }, [itemId, onHeightMeasured, ref])\n}\n\n/**\n * Wraps a daily report item row, observing its height and providing a stable test identifier.\n * 日報アイテム行をラップし、高さを監視するとともに安定したテスト識別子を提供するコンポーネント。\n */\nconst DailyReportDetailRow = ({ itemId, index, setSize, onHeightMeasured, children }: { itemId: number; index: number; setSize: number; onHeightMeasured: (id: number, height: number) => void; children: React.ReactNode }) => {\n const elementRef = useRef<HTMLDivElement | null>(null)\n useReportItemHeightObserver(elementRef, itemId, onHeightMeasured)\n return (\n // 仮想化リスト内の絶対位置を SR へ公開する (描画は窓分だけなので必須)。\n // 親は VirtualScroll のコンテンツ要素 (role=\"list\" の div) であり実 <ul> ではないため、\n // <li> にすると list 要素外の <li> になって不正。role=\"listitem\" を div に付けるのが正しい\n // biome-ignore lint/a11y/useSemanticElements: parent is a role=\"list\" div (virtualized), not a real <ul>; <li> would be invalid here\n <div ref={elementRef} className=\"px-2 pb-3\" data-daily-report-button={itemId} role=\"listitem\" aria-posinset={index + 1} aria-setsize={setSize}>\n {children}\n </div>\n )\n}\n\n/**\n * Formats interviewer labels with optional affiliation.\n * 面談者情報を所属付きのラベル文字列に整形する関数。\n */\nconst formatInterviewerLabel = (interviewer: DailyReportDetail[\"interviewers\"][number]): string => {\n if (interviewer.affiliation?.trim()) {\n return `${interviewer.name}(${interviewer.affiliation})`\n }\n return interviewer.name ?? \"\"\n}\n\n/**\n * Scroll restore slot for DailyReportDetailList that survives tab unmounting.\n * タブアンマウントをまたいで DailyReportDetailList のスクロール位置を持ち回る共有スロット。\n *\n * topItem は先頭可視カードの**同一性アンカー** (reportHubId + 行内オフセット px + 保存時 index)。\n * ❗ 生の論理 px を保存してはならない: 可変高さでは px→行変換が再マウントの推定空間で行われ、\n * 深い位置で別のカードに着地する (実機で確定した実害バグ)。同一性アンカーは一覧が留守中に\n * 変わっても findIndex で正しいカードへ再解決される。index は該当カードが消えていたときの\n * 近似フォールバック。offsetPx はカード上端がビューポート上端より上に隠れている px (論理座標)。\n * anchorReportHubId は「記録時点の選択日報」。復元はこれが現在の選択と一致するときだけ行う:\n * 選択が他ビューで変わっていたら、復元よりも「選択日報へのスクロール」(既存 UX) を優先する。\n * measuredHeights は実測カード高さ (reportHubId → px)。着地窓の行内オフセットの厳密さと\n * 総高さ推定の質を保つため、実測をアンマウント越しに持ち回る。\n */\nexport type DailyReportDetailListScrollRestore = {\n topItem: { reportHubId: number; index: number; offsetPx: number } | null\n anchorReportHubId: number | null\n measuredHeights: Map<number, number>\n}\n\ntype DailyReportDetailListProps = {\n dailyReportItems: DailyReportItem[]\n userId?: string | null\n selectedItemId?: number | null\n onSelectItem?: (id: number | null) => void\n /**\n * スクロール位置の共有スロット。マウント時に一度だけ読み、スクロールのたびに書き戻す。\n * ❗ 値をレンダー時に読んで props へ焼き込む方式は禁止 (daily-report-list.tsx 参照)\n */\n scrollRestoreRef?: RefObject<DailyReportDetailListScrollRestore>\n}\n\n/**\n * Daily report detail cards rendered with dynamic height virtual scroll.\n * 動的高さの仮想スクロールで日報詳細カードを描画するコンポーネント。\n */\nexport const DailyReportDetailList = ({ dailyReportItems, userId, selectedItemId, onSelectItem, scrollRestoreRef }: DailyReportDetailListProps) => {\n // 復元アンカーと「復元マウントかどうか」はマウント時に一度だけ共有スロットから決める。\n // 復元判定は選択アンカー (記録時の選択日報) の一致のみで行う。❗ 位置の有無や大小を復元\n // 判定に流用してはならない: 先頭まで戻してからタブを離れた復元マウントが「非復元」に\n // 誤分類され、初回の選択スクロールが復元位置 (先頭) を上書きしてしまう。\n // topItem は reportHubId の同一性で現在の一覧へ再解決する (留守中の並び替え・挿入に不変)。\n // 消えていたら保存時 index への近似フォールバック (行内オフセットは破棄 = カード上端合わせ)\n const [{ initialScrollAnchor, restored: isRestoredMount }] = useState(() => {\n const restore = scrollRestoreRef?.current\n if (!restore || restore.anchorReportHubId !== (selectedItemId ?? null)) {\n return { initialScrollAnchor: undefined, restored: false }\n }\n if (!restore.topItem) {\n return { initialScrollAnchor: undefined, restored: true }\n }\n const foundIndex = dailyReportItems.findIndex((item) => item.reportHubId === restore.topItem?.reportHubId)\n if (foundIndex !== -1) {\n return { initialScrollAnchor: { index: foundIndex, offsetPx: restore.topItem.offsetPx }, restored: true }\n }\n return { initialScrollAnchor: { index: restore.topItem.index, offsetPx: 0 }, restored: true }\n })\n // 復元マウントの初回だけ「選択日報へのスクロール」をスキップするための武装フラグと、\n // 消費条件 (マウント時と同じ選択であること) の基準値。\n // ❗ 消費条件なしで武装したままにしてはならない: アンカー日報がアンマウント中に削除されて\n // いた場合など、マウント効果が index を解決できないとフラグが残留し、その後の正当な\n // 選択変更 (新規作成ボタン等) のスクロールを 1 回無言で飲み込む\n const skipInitialScrollToSelectionRef = useRef(isRestoredMount)\n const mountSelectionIdRef = useRef(selectedItemId ?? null)\n // 実測高さは ref にインプレースで持つ。以前は測定行ごとに `new Map(prev)` を作っており\n // 行数に対して O(n^2) だった。VirtualScroll へは updateItemSize で個別に伝えるため、\n // ここで state を持って再レンダーを起こす必要はない (getItemHeight は ref を直接読む)。\n // ❗ 共有スロットがあればその Map を使う (アンマウントで実測空間を捨てない): 同一性アンカー\n // 復元の下で、実測の持ち回りは (a) アンカーカードの行内オフセットの厳密さ (そのカードの\n // 実測高さが行内 px の座標系そのもの) と (b) 総高さ推定・周辺カードの初期配置の質を保つ。\n // カードの同一性は topItem (reportHubId) が保証するため、深い復元でも別カードへは漂着しない\n const itemHeightsRef = useRef<Map<number, number>>(scrollRestoreRef?.current.measuredHeights ?? new Map())\n const scrollContainerRef = useRef<HTMLDivElement | null>(null)\n const virtualScrollRef = useRef<VirtualScrollHandle>(null)\n // list ロールの aria-label に使う (消費側注入の業務ドメイン名)\n const { title } = useDailyReportConfig()\n const { viewportHeight, updateViewportHeight } = useDynamicViewportHeight(scrollContainerRef, { enableMutationObserver: true })\n const prefetchDailyReportByDate = useDailyReportPrefetch()\n const [overlayLabel, setOverlayLabel] = useState<string>(\"\")\n const [isWheelScrollActive, setIsWheelScrollActive] = useState(false)\n const wheelResetTimerRef = useRef<number | null>(null)\n const scrollEndTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)\n const dailyReportItemsRef = useRef(dailyReportItems)\n dailyReportItemsRef.current = dailyReportItems\n\n // 同一 selectedItemId かつ同一 index への重複スクロールを防止する ref\n const lastScrolledRef = useRef<{ id: number; index: number } | null>(null)\n\n useEffect(() => {\n if (selectedItemId != null && virtualScrollRef.current) {\n const index = dailyReportItems.findIndex((item) => item.reportHubId === selectedItemId)\n if (index !== -1) {\n // ID もインデックスも同じなら再スクロール不要\n if (lastScrolledRef.current?.id === selectedItemId && lastScrolledRef.current?.index === index) return\n // 復元マウントの初回だけはスクロールせず「スクロール済み」として記録する\n // (以後の選択変更・並び替え追従は従来どおり動く)。\n // 消費できるのは「マウント時と同じ選択」のときだけ: 選択が変わっていたら\n // それは正当な選択変更なので、武装解除して通常のスクロールへ落とす\n if (skipInitialScrollToSelectionRef.current) {\n skipInitialScrollToSelectionRef.current = false\n if ((selectedItemId ?? null) === mountSelectionIdRef.current) {\n lastScrolledRef.current = { id: selectedItemId, index }\n return\n }\n }\n virtualScrollRef.current.scrollToIndex(index, { align: \"top\" })\n lastScrolledRef.current = { id: selectedItemId, index }\n }\n } else {\n lastScrolledRef.current = null\n }\n }, [selectedItemId, dailyReportItems])\n\n /**\n * Keeps the restore slot's anchor aligned with the current selection.\n * 復元スロットのアンカーを現在の選択日報と同期し続ける副作用。\n *\n * スクロールイベントに依存せず選択変更を即座に反映する (選択日報が既に上端にあり\n * scrollToIndex が onScroll を発火しないケースでもアンカーが古くならない)。\n * 依存配列: [selectedItemId, scrollRestoreRef]。クリーンアップ: なし。\n */\n useEffect(() => {\n const restore = scrollRestoreRef?.current\n if (restore) {\n restore.anchorReportHubId = selectedItemId ?? null\n }\n }, [selectedItemId, scrollRestoreRef])\n\n // onSelectItem の最新参照を保持する ref (useEffect 依存配列を不要にする)\n const onSelectItemRef = useRef(onSelectItem)\n onSelectItemRef.current = onSelectItem\n\n useEffect(() => {\n if (virtualScrollRef.current) {\n const el = document.querySelector('[data-testid=\"daily-report-detail-list\"]')\n if (el) {\n const handle = virtualScrollRef.current\n const extendedHandle: DailyReportVirtualScrollHandle = Object.assign({}, handle, {\n findReportIndex: (id: number) => dailyReportItemsRef.current.findIndex((item) => item.reportHubId === id),\n getReportItem: (index: number) => dailyReportItemsRef.current[index],\n selectItem: (id: number) => onSelectItemRef.current?.(id),\n })\n ;(el as Element & { __virtualScroll: DailyReportVirtualScrollHandle }).__virtualScroll = extendedHandle\n }\n }\n })\n\n const itemCount = dailyReportItems.length\n\n const getItem = useCallback(\n (index: number): DailyReportItem => {\n if (index >= itemCount) return EMPTY_ITEM\n return dailyReportItems[index]\n },\n [dailyReportItems, itemCount],\n )\n\n const schedulePrefetchForRange = useCallback(\n (fromIndex: number, toIndex: number) => {\n if (itemCount === 0) return\n const boundedFrom = Math.max(0, fromIndex)\n const boundedTo = Math.min(itemCount - 1, toIndex)\n if (boundedFrom > boundedTo) return\n\n const businessDates = new Set<string>()\n for (let index = boundedFrom; index <= boundedTo; index++) {\n const candidate = getItem(index)\n if (candidate.businessDate) businessDates.add(candidate.businessDate)\n if (businessDates.size >= PREFETCH_MAX_DATES) break\n }\n businessDates.forEach((date) => {\n prefetchDailyReportByDate(date)\n })\n },\n [getItem, itemCount, prefetchDailyReportByDate],\n )\n\n /**\n * Handles visible range changes to update overlay label and schedule prefetching.\n * 表示範囲の変更を処理し、オーバーレイラベルの更新とプリフェッチのスケジュールを行う。\n */\n const handleRangeChange = useCallback(\n ({ renderingStartIndex, renderingEndIndex, visibleStartIndex }: { renderingStartIndex: number; renderingEndIndex: number; visibleStartIndex: number }) => {\n if (itemCount === 0) return\n const firstVisibleItem = getItem(visibleStartIndex)\n if (firstVisibleItem?.businessDate) {\n setOverlayLabel(formatToIsoDate(firstVisibleItem.businessDate))\n }\n schedulePrefetchForRange(renderingStartIndex, renderingEndIndex)\n schedulePrefetchForRange(renderingEndIndex + 1, renderingEndIndex + PREFETCH_LOOKAHEAD)\n schedulePrefetchForRange(renderingStartIndex - PREFETCH_LOOKAHEAD, renderingStartIndex - 1)\n },\n [getItem, itemCount, schedulePrefetchForRange],\n )\n\n // ❗ teardown 用ハンドルミラー: React は削除コミットの mutation phase で virtualScrollRef を\n // null 化するため、アンマウント時の RAF flush (最終 onScroll 配信) の時点では素の ref が\n // 読めない。最後に観測した非 null ハンドルを保持し、フリング中のタブ切替でも最終アンカーを\n // 取りこぼさない (closure 経由の読取は detach 後も直近コミットの値で安全)\n const lastVirtualScrollHandleRef = useRef<VirtualScrollHandle | null>(null)\n\n const handleScroll = useCallback(() => {\n // スクロールのたびに先頭可視カードの同一性アンカーを共有スロットへ書き戻す。\n // ❗ 生の論理 px (onScroll 引数や getScrollPosition) を保存してはならない:\n // 可変高さでは再マウントの推定空間で px→行変換が狂い、別のカードに着地する。\n // getScrollAnchor は可視ウィンドウの実測が照合済みのツリーで計算されるため常に正確\n if (virtualScrollRef.current) {\n lastVirtualScrollHandleRef.current = virtualScrollRef.current\n }\n const restore = scrollRestoreRef?.current\n const handle = virtualScrollRef.current ?? lastVirtualScrollHandleRef.current\n if (restore && handle) {\n const anchor = handle.getScrollAnchor()\n const item = anchor ? dailyReportItemsRef.current[anchor.index] : undefined\n if (anchor && item) {\n restore.topItem = { reportHubId: item.reportHubId, index: anchor.index, offsetPx: anchor.offsetPx }\n }\n }\n\n const container = scrollContainerRef.current\n if (!container) return\n\n if (container.dataset.scrolling !== \"true\") {\n container.dataset.scrolling = \"true\"\n }\n\n if (scrollEndTimerRef.current) {\n clearTimeout(scrollEndTimerRef.current)\n }\n\n scrollEndTimerRef.current = setTimeout(() => {\n if (container) {\n delete container.dataset.scrolling\n }\n scrollEndTimerRef.current = null\n }, 150)\n }, [scrollRestoreRef])\n\n const renderThumbOverlay = useCallback((props: ScrollBarThumbOverlayRenderProps) => <BusinessDayThumbOverlay {...props} label={overlayLabel} isWheelScrollActive={isWheelScrollActive} />, [overlayLabel, isWheelScrollActive])\n\n /**\n * Sets up wheel event listener to toggle overlay visibility.\n * ホイールイベントリスナーを設定し、オーバーレイの表示状態を切り替える。\n */\n useEffect(() => {\n const el = scrollContainerRef.current\n if (!el) return\n const onWheel = () => {\n setIsWheelScrollActive(true)\n if (wheelResetTimerRef.current) clearTimeout(wheelResetTimerRef.current)\n wheelResetTimerRef.current = window.setTimeout(() => {\n setIsWheelScrollActive(false)\n wheelResetTimerRef.current = null\n }, WHEEL_RESET_TIMEOUT_MS)\n }\n el.addEventListener(\"wheel\", onWheel, { passive: true })\n return () => {\n el.removeEventListener(\"wheel\", onWheel)\n if (wheelResetTimerRef.current) clearTimeout(wheelResetTimerRef.current)\n }\n }, [])\n\n const updateItemHeight = useCallback((id: number, height: number) => {\n // 変化が無ければ何もしない (再測定の連打を吸収する)\n if (itemHeightsRef.current.get(id) === height) return\n itemHeightsRef.current.set(id, height)\n if (virtualScrollRef.current) {\n const index = dailyReportItemsRef.current.findIndex((item) => item.reportHubId === id)\n if (index !== -1) {\n // VirtualScroll に個別の高さ変更を伝える (これが再レイアウトの権威シグナル)\n virtualScrollRef.current.updateItemSize(index, height)\n }\n }\n }, [])\n\n const getItemHeight = useCallback(\n (index: number) => {\n const item = getItem(index)\n return itemHeightsRef.current.get(item.reportHubId) || 350\n },\n [getItem],\n )\n\n const getItemKey = useCallback(\n (index: number) => {\n const item = getItem(index)\n return item.reportHubId\n },\n [getItem],\n )\n\n /**\n * Updates viewport height when item count changes.\n * アイテム数変更時にビューポート高さを更新する。\n */\n useEffect(() => {\n if (itemCount >= 0) updateViewportHeight()\n }, [itemCount, updateViewportHeight])\n\n /**\n * Schedules initial prefetch when items are loaded.\n * アイテム読み込み時に初期プリフェッチをスケジュールする。\n */\n useEffect(() => {\n if (itemCount === 0) return\n schedulePrefetchForRange(0, Math.min(itemCount - 1, PREFETCH_LOOKAHEAD))\n }, [itemCount, schedulePrefetchForRange])\n\n const handleCommentAdded = useCallback((itemId: number) => {\n const index = dailyReportItemsRef.current.findIndex((item) => item.reportHubId === itemId)\n if (index >= 0) {\n setTimeout(() => virtualScrollRef.current?.scrollToIndex(index, { align: \"bottom\" }), 300)\n }\n }, [])\n\n const renderItem = useCallback(\n (item: DailyReportItem, index: number) => <DailyReportItemContainer key={item.reportHubId} item={item} index={index} setSize={itemCount} onHeightMeasured={updateItemHeight} onCommentAdded={handleCommentAdded} userId={userId} />,\n [updateItemHeight, handleCommentAdded, userId, itemCount],\n )\n\n return (\n <>\n <div ref={scrollContainerRef} className=\"flex min-h-0 flex-1 flex-col\">\n <VirtualScroll\n ref={virtualScrollRef}\n itemCount={itemCount}\n getItem={getItem}\n getItemKey={getItemKey}\n getItemHeight={getItemHeight}\n viewportSize={viewportHeight}\n initialScrollAnchor={initialScrollAnchor}\n overscanCount={VIRTUAL_SCROLL_OVERSCAN_COUNT}\n scrollBarOptions={{\n width: SCROLL_BAR_WIDTH,\n tapScrollCircleOptions: TAP_SCROLL_CIRCLE_OPTIONS,\n renderThumbOverlay: renderThumbOverlay,\n }}\n behaviorOptions={{\n // ドラッグスクロールは無効化せず、**ポインタ種別で絞る**。\n // 丸ごと切るとタッチ端末からスクロール手段が消える (定数の注記参照)\n pointerDragInputs: CONTENT_DRAG_POINTER_INPUTS,\n wheelSpeedMultiplier: FAST_SCROLL_WHEEL_MULTIPLIER,\n inertiaOptions: FAST_SCROLL_INERTIA_OPTIONS,\n // 行ラッパーへの tabIndex 汚染を止め、list/listitem のロール階層を有効にする\n enableKeyboardNavigation: false,\n }}\n onRangeChange={handleRangeChange}\n onScroll={handleScroll}\n testId=\"daily-report-detail-list\"\n className=\"relative w-full\"\n contentProps={{\n // 行 (role=\"listitem\") だけを所有する list をコンテンツ要素へ置く\n role: \"list\",\n \"aria-label\": title,\n }}>\n {renderItem}\n </VirtualScroll>\n </div>\n {itemCount === 0 && (\n <div className=\"py-8 text-center\">\n <p className=\"text-slate-500\">データがありません</p>\n </div>\n )}\n </>\n )\n}\n\n/**\n * Loading skeleton for the virtual scroll detail entry.\n * 仮想スクロール詳細行の読み込み中スケルトン表示。\n */\nconst SkeletonReportItem = () => (\n <div className=\"rounded-2xl border border-slate-200 bg-white p-4 shadow-sm dark:border-slate-700 dark:bg-slate-900/70\">\n <div className=\"mb-4 flex flex-wrap gap-2\">\n <div className=\"h-6 w-20 rounded-full bg-slate-200\"></div>\n <div className=\"h-6 w-24 rounded-full bg-slate-200\"></div>\n <div className=\"h-6 w-24 rounded-full bg-slate-200\"></div>\n <div className=\"h-6 w-28 rounded-full bg-slate-200\"></div>\n </div>\n <div className=\"mb-3 grid gap-3 md:grid-cols-[minmax(0,1fr)_220px]\">\n <div className=\"space-y-3\">\n <div className=\"h-5 w-40 rounded bg-slate-200\"></div>\n <div className=\"h-5 w-56 rounded bg-slate-200\"></div>\n <div className=\"h-24 rounded-lg bg-slate-200\"></div>\n </div>\n <div className=\"rounded-xl bg-slate-100 p-3\">\n <div className=\"mb-2 h-4 w-24 rounded bg-slate-200\"></div>\n <div className=\"mb-2 h-4 w-28 rounded bg-slate-200\"></div>\n <div className=\"h-4 w-32 rounded bg-slate-200\"></div>\n </div>\n </div>\n <div className=\"space-y-3\">\n <div className=\"h-4 w-24 rounded bg-slate-200\"></div>\n <div className=\"flex flex-wrap gap-2\">\n <div className=\"h-6 w-16 rounded-full bg-slate-200\"></div>\n <div className=\"h-6 w-16 rounded-full bg-slate-200\"></div>\n <div className=\"h-6 w-16 rounded-full bg-slate-200\"></div>\n </div>\n <div className=\"space-y-2\">\n <div className=\"h-16 rounded-lg bg-slate-200\"></div>\n <div className=\"h-16 rounded-lg bg-slate-200\"></div>\n </div>\n </div>\n </div>\n)\n\n/**\n * Chooses the appropriate daily report item representation based on fetch state.\n * フェッチ状態に応じて日報アイテムの表示コンポーネントを選択する関数。\n */\nconst DailyReportItemContainer = ({\n item,\n index,\n setSize,\n onHeightMeasured,\n onCommentAdded,\n userId,\n}: {\n item: DailyReportItem\n index: number\n setSize: number\n onHeightMeasured: (id: number, height: number) => void\n onCommentAdded?: (itemId: number) => void\n userId?: string | null\n}) => {\n const { report, error, isLoading, isRefetching } = useDailyReportDetail(item.reportHubId, item.businessDate)\n const { removeStaleItem } = useDailyReportActionContext()\n const { draftLabelName } = useDailyReportConfig()\n const [isEditing, setIsEditing] = useState(false)\n const [hasBypassedAutoEdit, setHasBypassedAutoEdit] = useState(false)\n // 自動編集モード (ドラフト検出による自動遷移) かどうかを追跡\n const autoEditingRef = useRef(false)\n\n // フェッチが一度でも試行されたかを追跡する ref\n const hasEverFetched = useRef(false)\n if (isRefetching) hasEverFetched.current = true\n\n // フェッチ完了後に report が null → stale アイテムとしてリストから除去する安全弁\n // 外部リソース同期: useDailyReportDetail のフェッチ結果に反応する\n useEffect(() => {\n if (!(report || isLoading || isRefetching || error) && hasEverFetched.current && item.reportHubId > 0) {\n removeStaleItem(item.reportHubId)\n }\n }, [report, isLoading, isRefetching, error, item.reportHubId, removeStaleItem])\n\n const isDraft = report?.labels.some((l) => l.name === draftLabelName)\n const isMine = userId && report?.userId === userId\n\n // ドラフトレポートの自動編集モード制御\n // - ドラフト状態かつ自分のレポート: 自動的にエディタモード\n // - SSE でドラフト→公開に遷移: 自動編集モードの場合のみビューモードへ解除\n useEffect(() => {\n if (report && isMine && isDraft && !isEditing && !hasBypassedAutoEdit) {\n setIsEditing(true)\n autoEditingRef.current = true\n } else if (report && !isDraft && isEditing && autoEditingRef.current) {\n // SSE で下書き→公開に遷移: 自動編集モードの場合のみビューモードへ\n setIsEditing(false)\n autoEditingRef.current = false\n }\n }, [report, isMine, isDraft, isEditing, hasBypassedAutoEdit])\n\n return (\n <DailyReportDetailRow itemId={item.reportHubId} index={index} setSize={setSize} onHeightMeasured={onHeightMeasured}>\n {error ? (\n <DailyReportError itemId={item.reportHubId} />\n ) : !report ? (\n <SkeletonReportItem />\n ) : isEditing ? (\n <DailyReportEditForm\n report={report}\n onCancel={() => {\n setIsEditing(false)\n setHasBypassedAutoEdit(true)\n }}\n onPublishSuccess={() => {\n setIsEditing(false)\n }}\n />\n ) : (\n <DailyReportResolvedItem item={item} report={report} onCommentAdded={onCommentAdded} userId={userId} onDoubleClick={isMine ? () => setIsEditing(true) : undefined} />\n )}\n </DailyReportDetailRow>\n )\n}\n\n/**\n * Renders the header section of a daily report.\n * 日報のヘッダー部分を描画するコンポーネント。\n */\nconst DailyReportHeader = ({\n report,\n isRead,\n isStarred,\n isMine,\n onToggleRead,\n onToggleStar,\n onDelete,\n itemSourceType,\n visitTimeDisplay,\n categoryLabel,\n fieldLabels,\n}: {\n report: DailyReportDetail\n itemSourceType?: string\n isRead: boolean\n isStarred: boolean\n isMine: boolean\n onToggleRead: () => void\n onToggleStar: () => void\n onDelete: () => void\n visitTimeDisplay: string\n categoryLabel: string\n fieldLabels: DailyReportFieldLabels\n}) => {\n const { sourceTypeConfigs } = useDailyReportConfig()\n return (\n <div className=\"flex flex-wrap items-center gap-2 font-medium text-slate-600 text-xs\">\n {/* 添付の有無を一目判別できるようにする。仕様書が定める\n [sourceType] [Star] [Read] の相対順は崩さず、その手前に置く */}\n <DailyReportAttachmentIndicator count={report.attachments.length} label={fieldLabels.attachments} />\n {(() => {\n const sourceType = itemSourceType ?? report?.sourceType\n const sourceConfig = resolveSourceTypeConfig(sourceType, sourceTypeConfigs)\n 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\n })()}\n <button\n type=\"button\"\n onClick={onToggleStar}\n 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\"\n title={isStarred ? \"スターを外す\" : \"スターを付ける\"}\n data-detail-star-button={report.reportHubId}>\n {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\" />}\n </button>\n <button\n type=\"button\"\n onClick={onToggleRead}\n 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\"\n title={isRead ? \"未読にする\" : \"既読にする\"}\n data-detail-read-button={report.reportHubId}>\n {isRead ? <Check className=\"h-3.5 w-3.5 text-blue-500\" /> : <UnreadIndicator className=\"text-xs\" />}\n </button>\n {isMine && (\n <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=\"削除する\">\n <Trash2 className=\"h-4 w-4 text-slate-300 hover:text-red-500\" />\n </button>\n )}\n <span className=\"inline-flex items-center gap-1 whitespace-nowrap rounded-full bg-blue-50 px-3 py-1 font-semibold text-blue-700 text-xs\">\n <span className=\"text-[10px] text-blue-500 uppercase tracking-wide\">{fieldLabels.id}</span>\n {report.reportHubId}\n </span>\n <span className=\"inline-flex items-center gap-1 whitespace-nowrap rounded-full bg-slate-100 px-3 py-1\">\n <span className=\"text-[10px] text-slate-400 uppercase tracking-wide\">{fieldLabels.businessDate}</span>\n {formatToIsoDate(report.date)}\n </span>\n <span className=\"inline-flex items-center gap-1 whitespace-nowrap rounded-full bg-slate-100 px-3 py-1\">\n <span className=\"text-[10px] text-slate-400 uppercase tracking-wide\">{fieldLabels.author}</span>\n {fallbackText(report.employeeName)}\n </span>\n <span className=\"inline-flex items-center gap-1 whitespace-nowrap rounded-full bg-slate-100 px-3 py-1\">\n <span className=\"text-[10px] text-slate-400 uppercase tracking-wide\">{fieldLabels.visitTime}</span>\n {visitTimeDisplay || \"-\"}\n </span>\n <span className=\"inline-flex items-center gap-1 whitespace-nowrap rounded-full bg-slate-100 px-3 py-1\">\n <span className=\"text-[10px] text-slate-400 uppercase tracking-wide\">{fieldLabels.updatedAt}</span>\n {formatToIsoDate(report.updatedAt)}\n </span>\n {categoryLabel && (\n <span className=\"inline-flex items-center gap-1 whitespace-nowrap rounded-full bg-amber-100 px-3 py-1 text-amber-700\">\n <span className=\"text-[10px] text-amber-500 uppercase tracking-wide\">{fieldLabels.category}</span>\n {categoryLabel}\n </span>\n )}\n </div>\n )\n}\n\n/**\n * Renders the side panel with detailed metadata.\n * 詳細なメタデータを表示するサイドパネルを描画するコンポーネント。\n */\nconst MetadataPanel = ({ report, visitTimeDisplay, categoryLabel, fieldLabels }: { report: DailyReportDetail; visitTimeDisplay: string; categoryLabel: string; fieldLabels: DailyReportFieldLabels }) => (\n <div className=\"flex flex-col gap-3 rounded-xl bg-slate-100 p-4 text-slate-600 text-xs dark:bg-slate-800/60 dark:text-slate-300\">\n <div className=\"flex flex-col gap-1\">\n <span className=\"font-semibold text-[10px] text-slate-500 uppercase tracking-wide dark:text-slate-400\">{fieldLabels.author}</span>\n <span className=\"text-slate-700 text-sm dark:text-slate-200\">{fallbackText(report.author)}</span>\n </div>\n <div className=\"flex flex-col gap-1\">\n <span className=\"font-semibold text-[10px] text-slate-500 uppercase tracking-wide dark:text-slate-400\">{fieldLabels.createdAt}</span>\n <span className=\"text-slate-700 text-sm dark:text-slate-200\">{formatToIsoDate(report.createdAt)}</span>\n </div>\n <div className=\"flex flex-col gap-1\">\n <span className=\"font-semibold text-[10px] text-slate-500 uppercase tracking-wide dark:text-slate-400\">{fieldLabels.updatedBy}</span>\n <span className=\"text-slate-700 text-sm dark:text-slate-200\">{fallbackText(report.updatedBy)}</span>\n </div>\n <div className=\"flex flex-col gap-1\">\n <span className=\"font-semibold text-[10px] text-slate-500 uppercase tracking-wide dark:text-slate-400\">{fieldLabels.updatedAt}</span>\n <span className=\"text-slate-700 text-sm dark:text-slate-200\">{formatToIsoDate(report.updatedAt)}</span>\n </div>\n <div className=\"flex flex-col gap-1\">\n <span className=\"font-semibold text-[10px] text-slate-500 uppercase tracking-wide dark:text-slate-400\">{fieldLabels.businessDate}</span>\n <span className=\"text-slate-700 text-sm dark:text-slate-200\">{formatToIsoDate(report.date)}</span>\n </div>\n {visitTimeDisplay && (\n <div className=\"flex flex-col gap-1\">\n <span className=\"font-semibold text-[10px] text-slate-500 uppercase tracking-wide dark:text-slate-400\">{fieldLabels.visitTime}</span>\n <span className=\"text-slate-700 text-sm dark:text-slate-200\">{visitTimeDisplay}</span>\n </div>\n )}\n {categoryLabel && (\n <div className=\"flex flex-col gap-1\">\n <span className=\"font-semibold text-[10px] text-slate-500 uppercase tracking-wide dark:text-slate-400\">{fieldLabels.categoryInfo}</span>\n <span className=\"text-slate-700 text-sm dark:text-slate-200\">{categoryLabel}</span>\n </div>\n )}\n </div>\n)\n\n/**\n * Renders the resolved daily report detail card with metadata and content.\n * 解決済みの日報詳細カードをメタデータと内容込みで描画する関数。\n */\nconst DailyReportResolvedItem = ({ item, report, onCommentAdded, userId, onDoubleClick }: { item: DailyReportItem; report: DailyReportDetail; onCommentAdded?: (itemId: number) => void; userId?: string | null; onDoubleClick?: () => void }) => {\n const formContainerRef = useRef<HTMLDivElement>(null)\n // 表示中ユーザーはアクションコンテキストから取得する (root loader 非依存)\n const { toggleStar, toggleRead, pendingStarUpdates, pendingReadUpdates, deleteReport, user } = useDailyReportActionContext()\n const { uiComments, handleAddComment, handleDeleteComment, pendingDeleteId, setPendingDeleteId, resolvedIdMap } = useDailyReportComments(report, user, userId)\n const { fieldLabels, sourceTypeConfigs } = useDailyReportConfig()\n\n const sourceType = item.sourceType ?? report?.sourceType\n const sourceConfig = resolveSourceTypeConfig(sourceType, sourceTypeConfigs)\n const isExternalSource = sourceType ? sourceType.toLowerCase() !== \"internal\" : false\n const canComment = sourceConfig?.allowComment ?? !isExternalSource\n\n const handleAddCommentWrapper = async (content: string) => {\n await handleAddComment(content)\n onCommentAdded?.(item.reportHubId)\n }\n\n const isStarred = pendingStarUpdates.current.has(report.reportHubId) ? (pendingStarUpdates.current.get(report.reportHubId) ?? false) : report.isStarred\n const isRead = pendingReadUpdates.current.has(report.reportHubId) ? (pendingReadUpdates.current.get(report.reportHubId) ?? false) : report.isRead\n const isMine = !!(userId && report.userId === userId)\n\n const handleToggleRead = async () => {\n try {\n await toggleRead(report.reportHubId, !isRead, report.date ?? \"\")\n } catch {}\n }\n\n const handleToggleStar = async () => {\n try {\n await toggleStar(report.reportHubId, !isStarred, report.date ?? \"\")\n } catch {}\n }\n\n const handleDelete = async () => {\n if (!confirm(\"本当に削除しますか?\")) return\n await deleteReport(report.reportHubId, report.date ?? \"\")\n }\n\n const normalizedContent = useMemo(() => (report.content ? report.content.replace(/<br\\s*\\/??\\s*>/gi, \"\\n\").trim() : \"\"), [report.content])\n const visitTimeDisplay = useMemo(() => {\n const from = report.visitTimeFrom?.trim()\n const to = report.visitTimeTo?.trim()\n return from && to ? `${from} - ${to}` : from || to || \"\"\n }, [report.visitTimeFrom, report.visitTimeTo])\n const categoryLabel = useMemo(() => {\n const labels: string[] = []\n if (report.category?.trim()) labels.push(`${fieldLabels.category}: ${report.category}`)\n if (report.creationCategory?.trim()) labels.push(`${fieldLabels.creationCategory}: ${report.creationCategory}`)\n return labels.join(\" / \")\n }, [report.category, report.creationCategory, fieldLabels.category, fieldLabels.creationCategory])\n\n return (\n // biome-ignore lint/a11y/noStaticElementInteractions: Double click to edit feature\n <div\n onDoubleClick={onDoubleClick}\n className={`flex h-full w-full flex-col rounded-2xl border bg-white p-4 shadow-sm transition-all duration-200 hover:-translate-y-0.5 hover:border-blue-300 hover:shadow-lg dark:border-slate-700 dark:bg-slate-900/70 ${!isRead ? \"border-l-4 border-l-blue-500\" : \"\"}`}>\n <div className=\"flex flex-col gap-4\">\n <DailyReportHeader\n report={report}\n itemSourceType={item.sourceType}\n isRead={isRead}\n isStarred={isStarred}\n isMine={isMine}\n onToggleRead={handleToggleRead}\n onToggleStar={handleToggleStar}\n onDelete={handleDelete}\n visitTimeDisplay={visitTimeDisplay}\n categoryLabel={categoryLabel}\n fieldLabels={fieldLabels}\n />\n <div className=\"grid gap-4 md:grid-cols-[minmax(0,1fr)_240px]\">\n <div className=\"flex flex-col gap-4\">\n <div className=\"grid gap-3 sm:grid-cols-2\">\n <div className=\"flex flex-col gap-1\">\n <span className=\"font-semibold text-slate-500 text-xs uppercase tracking-wide\">{fieldLabels.customer}</span>\n <span className=\"font-semibold text-base text-slate-900 dark:text-slate-100\">{fallbackText(report.customerName)}</span>\n </div>\n <div className=\"flex flex-col gap-1\">\n <span className=\"font-semibold text-slate-500 text-xs uppercase tracking-wide\">{fieldLabels.subject}</span>\n <span className=\"text-slate-700 text-sm dark:text-slate-200\">{fallbackText(report.subject)}</span>\n </div>\n </div>\n {normalizedContent && (\n <div className=\"flex flex-col gap-2\">\n <span className=\"font-semibold text-slate-500 text-xs uppercase tracking-wide\">{fieldLabels.content}</span>\n <div className=\"whitespace-pre-wrap rounded-xl bg-slate-50 p-3 text-slate-700 text-sm leading-relaxed dark:bg-slate-800/60 dark:text-slate-200\">{normalizedContent}</div>\n </div>\n )}\n {report.interviewers.length > 0 && (\n <div className=\"flex flex-col gap-2\">\n <span className=\"font-semibold text-slate-500 text-xs uppercase tracking-wide\">{fieldLabels.interviewers}</span>\n <ul className=\"flex flex-wrap gap-2\">\n {report.interviewers.map((interviewer) => (\n <li key={`${interviewer.name}-${interviewer.affiliation || \"\"}`} className=\"rounded-full bg-slate-100 px-3 py-1 text-slate-600 text-xs\">\n {formatInterviewerLabel(interviewer)}\n </li>\n ))}\n </ul>\n </div>\n )}\n <DailyReportAttachmentList attachments={report.attachments} />\n {uiComments.length > 0 && (\n <div className=\"flex flex-col gap-2\">\n <span className=\"font-semibold text-slate-500 text-xs uppercase tracking-wide\">{fieldLabels.comments}</span>\n <DailyReportCommentList\n comments={uiComments}\n reportHubId={report.reportHubId}\n businessDate={report.date ?? \"\"}\n onDeleteComment={handleDeleteComment}\n pendingDeleteId={pendingDeleteId}\n onPendingDeleteIdChange={setPendingDeleteId}\n resolvedIdMap={resolvedIdMap}\n />\n </div>\n )}\n {canComment && (\n <div className=\"mt-auto\" ref={formContainerRef}>\n <DailyReportCommentForm reportHubId={report.reportHubId} businessDate={report.date ?? \"\"} onAddComment={handleAddCommentWrapper} />\n </div>\n )}\n </div>\n <MetadataPanel report={report} visitTimeDisplay={visitTimeDisplay} categoryLabel={categoryLabel} fieldLabels={fieldLabels} />\n </div>\n </div>\n </div>\n )\n}\n\n/**\n * Displays an error banner when daily report detail retrieval fails.\n * 日報詳細の取得に失敗した際のエラーバナーを表示する関数。\n */\nconst DailyReportError = ({ itemId }: { itemId: number }) => (\n <div className=\"rounded-2xl border border-red-200 bg-red-50 p-4 text-red-700 text-sm\">\n <p>日報 {itemId.toLocaleString()} の読み込みに失敗しました。</p>\n </div>\n)\n","/**\n * Action context providing optimistic CRUD + SSE synchronization for daily reports.\n * 日報の楽観的 CRUD と SSE 同期を提供するアクションコンテキスト。\n */\nimport { type Context, createContext, type ReactNode, useCallback, useContext, useEffect, useRef, useState } from \"react\"\nimport { compareIdsStreamOrder, isIdsStreamOrdered, upsertInIdsStreamOrder } from \"../../shared/ids-stream\"\nimport { createLogger, LogLevel } from \"../../shared/logger\"\nimport type { DailyReportSseMessage } from \"../../shared/sse-schema\"\nimport type { DailyReportCommentItem, DailyReportDetail, DailyReportItem, DailyReportUser } from \"../../shared/types\"\nimport { useDailyReportConfig } from \"../config-context\"\nimport {\n acquireMutationLock,\n applyBroadcastReportUpdate,\n applyDailyReportServerUpdates,\n clearDailyReportCache,\n DailyReportCache,\n deleteDailyReportCache,\n getCachedReport,\n registerRecentDeletion,\n releaseMutationLock,\n subscribeCacheReady,\n updateDailyReportCache,\n writeCache,\n} from \"../hooks/use-daily-report\"\nimport { useDailyReportSseConnection } from \"../hooks/use-daily-report-sse-connection\"\nimport { getDailyReportIdsStreamSessionUserKey, refreshDailyReportIdsStream, removeDailyReportIdsStreamItem, upsertDailyReportIdsStreamItem, useDailyReportIdsStream } from \"../streaming/daily-report-ids-stream-session\"\nimport { SSE_INITIAL_CONNECT_DEFER_MS } from \"../utils/constants\"\nimport { useDailyReportErrorSurface } from \"./daily-report-error-context\"\n\nconst logger = createLogger(LogLevel.INFO, \"daily-report-action-context\")\n\n/**\n * Operation intent -> default banner label (Japanese).\n * 操作 intent -> 既定バナー用の日本語ラベル。ホストは config.onError で構造化情報 (operation)\n * を受け取り独自メッセージへ差し替えできる。\n */\nconst OPERATION_LABELS: Record<string, string> = {\n create: \"日報の作成\",\n update: \"日報の保存\",\n publish: \"日報の公開\",\n delete: \"日報の削除\",\n addComment: \"コメントの投稿\",\n deleteComment: \"コメントの削除\",\n toggleStar: \"スターの更新\",\n toggleRead: \"既読状態の更新\",\n}\n\n/** intent から利用者向けの失敗メッセージを組み立てる。 */\nconst describeOperationFailure = (intent: string): string => {\n const label = OPERATION_LABELS[intent] ?? \"操作\"\n return `${label}に失敗しました。時間をおいて再度お試しください。`\n}\n\n/**\n * Stable empty default for the optional `initialItems` prop.\n * 省略可能な `initialItems` の安定した空既定値。\n *\n * 既定を `= []` にすると毎レンダーで新しい配列参照になり、render-phase の\n * `initialItems !== prevInitialItems` 判定が常に真になって setState が無限ループし、\n * React が描画を中断する。単一の凍結参照を共有して同一性を保つ。\n */\nconst EMPTY_INITIAL_ITEMS: DailyReportItem[] = Object.freeze([]) as readonly DailyReportItem[] as DailyReportItem[]\n\ntype ActionResponse = {\n status: string\n reportHubId: number\n newComment?: DailyReportCommentItem\n updatedStatus?: { isStarred?: boolean; isRead?: boolean }\n deletedCommentId?: string\n updatedAt?: string // ISO string from server\n report?: DailyReportDetail // Full report for creation/updates\n clientTempId?: string // Validation token for optimistic updates\n [key: string]: unknown\n}\n\nexport type DailyReportActionContextType = {\n /** 表示中ユーザー (楽観的コメントの作者名等に使用)。 */\n user?: DailyReportUser\n /** reportHubId -> Map<tempCommentId, content> (追加コメントの一時IDと内容) */\n pendingAddCommentIds: React.MutableRefObject<Map<number, Map<number, string>>>\n /** reportHubId -> Set<tempCommentId> (削除された一時ID) */\n pendingDeleteCommentIds: React.MutableRefObject<Map<number, Set<number>>>\n /** reportHubId -> boolean (スターの保留状態) */\n pendingStarUpdates: React.MutableRefObject<Map<number, boolean>>\n /** reportHubId -> boolean (既読の保留状態) */\n pendingReadUpdates: React.MutableRefObject<Map<number, boolean>>\n /** tempId -> realId (解決されたID) */\n resolvedIdMap: React.MutableRefObject<Map<number, number>>\n /** 状態変更通知用バージョン */\n version: number\n /** 日報リスト */\n items: DailyReportItem[]\n /** SSE購読が有効かどうか */\n isSseEnabled: boolean\n /** SSE購読の有効/無効を切り替える */\n toggleSse: () => void\n\n /**\n * コメントを追加します。\n * @param reportHubId 日報HubID\n * @param content コメント内容\n * @param businessDate 営業日\n */\n addComment: (reportHubId: number, content: string, businessDate: string) => Promise<void>\n /**\n * コメントを削除します。\n * @param reportHubId 日報HubID\n * @param commentId コメントID (一時IDまたは実ID)\n * @param businessDate 営業日\n */\n deleteComment: (reportHubId: number, commentId: number, businessDate: string) => Promise<void>\n /**\n * スターの状態を切り替えます。\n * @param reportHubId 日報HubID\n * @param isStarred 新しいスター状態\n * @param businessDate 営業日\n */\n toggleStar: (reportHubId: number, isStarred: boolean, businessDate: string) => Promise<void>\n /**\n * 既読の状態を切り替えます。\n * @param reportHubId 日報HubID\n * @param isRead 新しい既読状態\n * @param businessDate 営業日\n */\n toggleRead: (reportHubId: number, isRead: boolean, businessDate: string) => Promise<void>\n createReport: (businessDate: string) => Promise<number>\n updateReport: (reportHubId: number, data: { title?: string; content?: string }, businessDate: string) => Promise<void>\n publishReport: (reportHubId: number, businessDate: string) => Promise<void>\n deleteReport: (reportHubId: number, businessDate: string) => Promise<void>\n /** stale アイテムをリストから除去する安全弁 */\n removeStaleItem: (reportHubId: number) => void\n /** データのみリロード (ページリロードなし) */\n refetchData: () => Promise<void>\n /** キャッシュを破棄してデータのみ再読み込み (ページリロードなし) */\n clearCacheAndRefetch: () => Promise<void>\n}\n\n// HMR / dual-bundle (ESM+CJS) でもコンテキスト identity を一意に保つため、\n// グローバルへ登録した単一インスタンスを常に使用する。\ndeclare global {\n var __DailyReportActionContext: Context<DailyReportActionContextType | null> | undefined\n}\n\nconst DailyReportActionContext: Context<DailyReportActionContextType | null> = globalThis.__DailyReportActionContext ?? createContext<DailyReportActionContextType | null>(null)\nglobalThis.__DailyReportActionContext = DailyReportActionContext\n\nexport const useDailyReportActionContext = () => {\n const context = useContext(DailyReportActionContext)\n if (!context) {\n throw new Error(\"useDailyReportActionContext must be used within a DailyReportActionProvider\")\n }\n return context\n}\n\nconst getUserDisplayName = (user: DailyReportUser | undefined): string => {\n if (!user) return \"Unknown\"\n if (user.displayName?.trim()) return user.displayName\n if (user.name) {\n const fullName = `${user.name.familyName ?? \"\"} ${user.name.givenName ?? \"\"}`.trim()\n if (fullName) return fullName\n }\n return user.emails?.[0]?.value ?? \"Unknown\"\n}\n\n/**\n * Provides a context for daily report actions including comments, stars, leads, and basic CRUD operations.\n * Also manages optimistic UI updates and real-time synchronization via SSE.\n * 日報のアクション(コメント追加・削除、スター、既読)を管理するプロバイダー。\n * 楽観的UI更新とAPI通信の抽象化を提供します。\n */\nexport const DailyReportActionProvider = ({ children, user, initialItems = EMPTY_INITIAL_ITEMS, userId }: { children: ReactNode; user?: DailyReportUser; initialItems?: DailyReportItem[]; userId?: string | null }) => {\n const { apiBasePath, draftLabelName } = useDailyReportConfig()\n // ミューテーション失敗の通知シーム。executeAction の catch から呼び、失敗を可視化する。\n // ref 経由で読み、executeAction の依存に載せない (通知参照は安定しているが防御的に)\n const { notifyError } = useDailyReportErrorSurface()\n const notifyErrorRef = useRef(notifyError)\n notifyErrorRef.current = notifyError\n const pendingAddCommentIds = useRef<Map<number, Map<number, string>>>(new Map())\n const pendingDeleteCommentIds = useRef<Map<number, Set<number>>>(new Map())\n const pendingStarUpdates = useRef<Map<number, boolean>>(new Map())\n const pendingReadUpdates = useRef<Map<number, boolean>>(new Map())\n const resolvedIdMap = useRef<Map<number, number>>(new Map())\n const [version, setVersion] = useState(0)\n const [items, setItems] = useState<DailyReportItem[]>(initialItems)\n // SSE は初回描画とその最初の中身フェッチに接続枠を譲るため、少し遅らせて自動接続する\n // (HTTP/1.1 の 6 接続のうち 1 枠を初回描画へ回す)。手動トグル済みなら自動接続しない\n const [isSseEnabled, setIsSseEnabled] = useState(false)\n const hasManualSseToggleRef = useRef(false)\n const pendingAddPromises = useRef<Map<number, Promise<number>>>(new Map())\n const manuallyTrackingIds = useRef<Set<number>>(new Set())\n /** 削除済み ID を記録し、マージロジックでの復活を防止する */\n const removedIds = useRef<Set<number>>(new Set())\n /** 自分が発行したアクションのID(SSEエコー無視用) */\n const processedClientTempIds = useRef<Set<string>>(new Set())\n\n // トゥームストーン掃除のゲート参照。ストリーミング途中の部分配列で掃除すると、\n // 後続チャンク到着前に削除された ID のトゥームストーンが失われ、遅れて届いた\n // 削除前スナップショットのチャンクで日報が復活する (effect の依存を増やさないため ref 経由)\n const streamState = useDailyReportIdsStream()\n const isStreamSettledRef = useRef(false)\n isStreamSettledRef.current = streamState.phase === \"complete\" && !streamState.isRevalidating\n\n /**\n * 遅延解決するミューテーション継続の門番。POST /action の待機中にリロード無しの\n * ユーザー切替が起きた場合、この判定が偽になり、継続はモジュール常駐状態\n * (常駐ストリームセッション・DailyReportCache・ミューテーションロック) への\n * 書き込みを放棄する。放棄しないと、旧ユーザーの日報行が新ユーザーの一覧へ\n * 注入され (観測集合に刻まれて完走剪定も免れる)、切替時に破棄したはずの\n * 詳細キャッシュへ旧ユーザーの下書き本文が再シードされる (実測再現済み)。\n * 同一ユーザーの通常ナビゲーション (アンマウント) ではセッションが生き続けるため\n * 真のままであり、作成直後の日報が常駐データから欠ける round-3 の退行は起こさない。\n */\n const isSessionStillOurs = useCallback(() => getDailyReportIdsStreamSessionUserKey() === (userId ?? null), [userId])\n\n const refetchData = useCallback(async () => {\n logger.info(\"[DailyReportActionProvider] Refetching data without page reload...\")\n // ID 一覧はストリームセッションのバックグラウンド再検証で更新する\n // (ルーター再検証は dailyReportShouldRevalidate に抑止されるため使わない)\n refreshDailyReportIdsStream()\n setVersion((v) => v + 1)\n DailyReportCache.listeners.forEach((listener: (id: number) => void) => {\n listener(-1)\n })\n }, [])\n\n const clearCacheAndRefetch = useCallback(async () => {\n logger.info(\"[DailyReportActionProvider] Clearing client & server/DB cache and refetching data without page reload...\")\n // 1. クライアント側キャッシュ破棄\n clearDailyReportCache()\n removedIds.current.clear()\n try {\n sessionStorage.clear()\n } catch {}\n\n // 2. サーバー/DB側キャッシュ破棄 API 呼び出し\n try {\n const formData = new FormData()\n formData.append(\"intent\", \"clearCache\")\n const res = await fetch(`${apiBasePath}/action`, {\n method: \"POST\",\n body: formData,\n })\n if (!res.ok) {\n const errData = await res.json().catch(() => null)\n logger.warn(\"[DailyReportActionProvider] Failed to clear server-side cache:\", res.status, errData)\n }\n } catch (e) {\n logger.warn(\"[DailyReportActionProvider] Failed to clear server-side cache:\", e)\n }\n\n // 3. ID 一覧ストリームをサーバーキャッシュバイパスで再検証 & UI 再描画。\n // POST 待機中にユーザー切替が起きた場合は放棄する (新ユーザーのストリームを\n // 冗長に強制再ダウンロードさせない)\n if (!isSessionStillOurs()) return\n refreshDailyReportIdsStream({ forceRefresh: true })\n setVersion((v) => v + 1)\n DailyReportCache.listeners.forEach((listener: (id: number) => void) => {\n listener(-1)\n })\n }, [apiBasePath, isSessionStillOurs])\n\n /**\n * Queue for SSE messages that arrived before the target report was cached.\n * キャッシュ未取得時に到着した SSE メッセージの待機キュー。\n * key: reportHubId, value: { messages, unsubscribe, timer }\n */\n const pendingSseQueue = useRef<\n Map<\n number,\n {\n messages: Exclude<DailyReportSseMessage, { type: \"connected\" }>[]\n unsubscribe: () => void\n timer: ReturnType<typeof setTimeout>\n }\n >\n >(new Map())\n\n /**\n * Ref to always access the latest handleSseMessage for queue replay.\n * キューの再処理時に常に最新の handleSseMessage を参照するための ref。\n */\n const handleSseMessageRef = useRef<(payload: Exclude<DailyReportSseMessage, { type: \"connected\" }>) => void>(() => {})\n\n /**\n * Enqueue an SSE message for a report that isn't cached yet.\n * Subscribes to cache-ready events and replays queued messages when the cache becomes available.\n *\n * キャッシュ未取得のレポートへの SSE メッセージをキューに追加する。\n * キャッシュ書き込みを監視し、書き込まれたらキューのメッセージを再処理する。\n */\n const enqueuePendingSseMessage = useCallback((reportHubId: number, message: Exclude<DailyReportSseMessage, { type: \"connected\" }>) => {\n const queue = pendingSseQueue.current\n const existing = queue.get(reportHubId)\n\n if (existing) {\n // 既存キューにメッセージを追加するだけ\n existing.messages.push(message)\n return\n }\n\n // 新規キューを作成\n const entry = {\n messages: [message],\n unsubscribe: () => {},\n timer: setTimeout(() => {\n // 30秒経過してもキャッシュが来なければキューを破棄\n const e = queue.get(reportHubId)\n if (e) {\n e.unsubscribe()\n queue.delete(reportHubId)\n logger.warn(`[SSE Queue] Expired: reportHubId=${reportHubId}, dropped=${e.messages.length} messages`)\n }\n }, 30_000),\n }\n\n // キャッシュ書き込みを監視\n entry.unsubscribe = subscribeCacheReady(reportHubId, () => {\n const e = queue.get(reportHubId)\n if (!e) return\n\n // キャッシュが書き込まれたか確認\n if (!getCachedReport(reportHubId, { ignoreExpiry: true })) return\n\n // キューをクリーンアップ\n e.unsubscribe()\n clearTimeout(e.timer)\n queue.delete(reportHubId)\n\n // キューのメッセージを最新の handleSseMessage で再処理\n logger.info(`[SSE Queue] Replaying ${e.messages.length} messages for reportHubId=${reportHubId}`)\n for (const msg of e.messages) {\n handleSseMessageRef.current(msg)\n }\n })\n\n queue.set(reportHubId, entry)\n }, [])\n\n const addProcessedId = (id: string) => {\n processedClientTempIds.current.add(id)\n // Auto-remove after 30 seconds to prevent memory leaks\n // 30秒後に自動削除(メモリリーク防止)\n setTimeout(() => {\n processedClientTempIds.current.delete(id)\n }, 30000)\n }\n\n const isProcessedId = useCallback((id: string | undefined): boolean => {\n return !!id && processedClientTempIds.current.has(id)\n }, [])\n\n /**\n * Upserts a report item into the list while keeping businessDate-desc order.\n *\n * レポートアイテムをリストへ upsert し、businessDate 降順を維持します。\n */\n const upsertItem = useCallback((reportHubId: number, businessDate: string | null | undefined) => {\n // モジュール常駐ストリームセッションへも反映する (鮮度窓内の再訪で作成直後の日報が\n // 消えないように)。businessDate 未指定は「既存値を保持」の意図なので中継しない\n if (businessDate !== undefined) {\n upsertDailyReportIdsStreamItem({ reportHubId, businessDate: businessDate ?? null })\n }\n setItems((prev) => {\n const existing = prev.find((item) => item.reportHubId === reportHubId)\n const nextBusinessDate = businessDate ?? existing?.businessDate ?? null\n // 正準比較器 (営業日降順 → ID 降順) の位置へ二分探索で upsert する。\n // 以前は SSE 1 件ごとに全件を localeCompare でソートし直していた (O(n log n)\n // かつ ID 降順の副キー無しで、同日内の新規作成が誤った位置に描画されていた)\n return upsertInIdsStreamOrder(prev, { reportHubId, businessDate: nextBusinessDate, sourceType: existing?.sourceType })\n })\n }, [])\n\n // 未登録ユーザーは SSE を有効化できない\n const toggleSse = () => {\n if (!userId) return\n // 手動操作後は自動接続タイマーに上書きさせない\n hasManualSseToggleRef.current = true\n setIsSseEnabled((prev) => !prev)\n }\n\n /**\n * Defers the initial SSE auto-connect until just after first paint.\n * 初回 SSE 自動接続を初回描画直後まで遅らせる副作用。\n *\n * 依存配列: userId (ユーザーごとに provider が key リマウントされるため、切替で張り直す)。\n * クリーンアップ: タイマー解除 (アンマウント/再マウントでのリークと二重発火を防ぐ)。\n */\n useEffect(() => {\n if (!userId) return\n const timer = setTimeout(() => {\n // 手動でトグルされていなければ自動接続する\n if (!hasManualSseToggleRef.current) setIsSseEnabled(true)\n }, SSE_INITIAL_CONNECT_DEFER_MS)\n return () => clearTimeout(timer)\n }, [userId])\n\n /**\n * Remove a stale item from the list as a safety net.\n * フェッチ後にレポートが見つからなかった場合の安全弁としてアイテムを除去する。\n */\n const removeStaleItem = useCallback(\n (reportHubId: number) => {\n // ユーザー切替後の遅延発火は新ユーザーのセッションへトゥームストーンを刻んでしまう\n if (!isSessionStillOurs()) return\n removedIds.current.add(reportHubId)\n // 常駐ストリームデータからも除去する (再訪時に安全弁が再発動し続けるのを防ぐ)\n removeDailyReportIdsStreamItem(reportHubId)\n setItems((prev) => prev.filter((item) => item.reportHubId !== reportHubId))\n },\n [isSessionStillOurs],\n )\n\n /**\n * Merge initialItems with prevItems instead of replacing (render-phase derived state).\n * Items already in the list are kept even if temporarily absent from the API response.\n * Removal is handled exclusively by the report-delete SSE handler.\n *\n * initialItems と prevItems をマージする (全置換ではない)。render-phase の\n * 派生状態パターン (prev 比較 + render 中の setState) で行うことが重要 —\n * effect で行うと「最初のチャンクのコミットは items が空 (VirtualScroll が\n * 一瞬 No items を描画) → effect 後の 2 コミット目で行が出る」という\n * 1 コミット分の遅延と空表示フラッシュが生まれる。render 中の setState は\n * React が子のコミット前に同一コンポーネントを再レンダーする公式パターン。\n * マージは純粋 (ref は読み取りのみ)。StrictMode の二重レンダーにも冪等。\n */\n const [prevInitialItems, setPrevInitialItems] = useState(initialItems)\n if (initialItems !== prevInitialItems) {\n setPrevInitialItems(initialItems)\n // initialItems をベースにマップを構築 (removedIds を除外)\n const mergedMap = new Map<number, DailyReportItem>()\n for (const item of initialItems) {\n if (!removedIds.current.has(item.reportHubId)) {\n mergedMap.set(item.reportHubId, item)\n }\n }\n // 現在の items にのみ存在する ID を保持 (削除は report-delete SSE で行う)\n for (const item of items) {\n if (!(mergedMap.has(item.reportHubId) || removedIds.current.has(item.reportHubId))) {\n mergedMap.set(item.reportHubId, item)\n }\n }\n // 並び順は正準比較器 (compareIdsStreamOrder: 営業日降順 → ID 降順、null 営業日は\n // 末尾) に一本化する。ストリームはソート順どおりに届き末尾へ追記されるため通常は\n // 整列済み — O(n) の検査で確認できたらソートを丸ごと省略する (20 万件級で毎 publish\n // の O(n log n) を O(n) へ落とす)。以前は営業日のみの比較で ID 降順の副キーが無く、\n // 前回全量走査以降に作成された日報が同日グループ内の誤った位置に描画されていた\n const merged = Array.from(mergedMap.values())\n if (!isIdsStreamOrdered(merged)) {\n merged.sort(compareIdsStreamOrder)\n }\n setItems(merged)\n }\n\n /**\n * トゥームストーン (removedIds) のクリーンアップ副作用。\n * ストリームの全量が出揃っている (complete かつ再検証中でない) ときにのみ行う。\n * 部分配列からの欠落は「サーバーに存在しない」証拠にならない。\n * 依存配列: initialItems (チャンク到着・再検証ごと)。クリーンアップ: なし。\n * ref の変異を伴うため render-phase ではなく effect に置く。\n */\n useEffect(() => {\n if (isStreamSettledRef.current) {\n const initialIds = new Set(initialItems.map((item) => item.reportHubId))\n // Set のイテレーション中の delete は JS 仕様で安全 (visited は再訪問しない)\n for (const id of removedIds.current) {\n if (!initialIds.has(id)) {\n removedIds.current.delete(id)\n }\n }\n }\n }, [initialItems])\n\n const handleSseMessage = useCallback(\n (payload: Exclude<DailyReportSseMessage, { type: \"connected\" }>) => {\n // セッション切替〜プロバイダーアンマウントの隙間で届いた旧ユーザーの SSE は破棄する。\n // clientLoader の bootstrap は React のコミット前にモジュールセッションを新ユーザーへ\n // 差し替えるため、旧プロバイダーの EventSource が閉じるまでの間に届いたメッセージ\n // (下書きの recipient 宛配信を含む) が、破棄済みキャッシュと新セッションへ\n // 旧ユーザーのデータを書き込んでしまう (POST 継続と同じ汚染シグネチャ・実測再現済み)\n if (!isSessionStillOurs()) {\n return\n }\n // Ignore echo of own actions\n // 自分が発行したアクションのエコーは無視する\n // clientTempId は connected 以外すべてのメッセージに存在する(型で保証済み)\n if (isProcessedId(payload.clientTempId)) {\n return\n }\n\n switch (payload.type) {\n case \"comment-add\": {\n const { comment, reportHubId } = payload\n const cached = getCachedReport(reportHubId, { ignoreExpiry: true })\n if (cached) {\n // Check for duplicates\n // 重複チェック\n if (cached.commentItems.some((c) => c.id === comment.id)) return\n\n // Check if this comment matches a pending optimistic comment\n let isMyComment = false\n const pendingMap = pendingAddCommentIds.current.get(reportHubId)\n if (pendingMap) {\n for (const [tempId, content] of pendingMap.entries()) {\n if (content === comment.content) {\n pendingMap.delete(tempId)\n resolvedIdMap.current.set(tempId, comment.id)\n isMyComment = true\n break\n }\n }\n }\n\n const newComment: DailyReportCommentItem = {\n ...comment,\n userName: comment.userName || (isMyComment ? getUserDisplayName(user) : \"Unknown\"),\n createdAt: comment.createdAt || new Date().toISOString(),\n isMine: isMyComment, // Use the detected ownership\n }\n\n const updatedReport = {\n ...cached,\n commentItems: [...cached.commentItems, newComment].sort((a, b) => new Date(a.createdAt || 0).getTime() - new Date(b.createdAt || 0).getTime()),\n }\n applyDailyReportServerUpdates(reportHubId, updatedReport)\n // Update mutation time to prevent stale fetch overwrite\n // 古いフェッチによる上書きを防ぐために変更時刻を更新\n acquireMutationLock(reportHubId)\n releaseMutationLock(reportHubId)\n setVersion((v) => v + 1) // UI更新トリガー\n } else {\n // キャッシュ未取得 → フェッチ完了後にリプレイ\n enqueuePendingSseMessage(reportHubId, payload)\n }\n break\n }\n case \"comment-delete\": {\n const { commentId, reportHubId } = payload\n const cached = getCachedReport(reportHubId, { ignoreExpiry: true })\n if (cached) {\n const updatedReport = {\n ...cached,\n commentItems: cached.commentItems.filter((c) => c.id !== commentId),\n }\n applyDailyReportServerUpdates(reportHubId, updatedReport)\n // Update mutation time to prevent stale fetch overwrite\n // 古いフェッチによる上書きを防ぐために変更時刻を更新\n acquireMutationLock(reportHubId)\n releaseMutationLock(reportHubId)\n setVersion((v) => v + 1)\n } else {\n enqueuePendingSseMessage(reportHubId, payload)\n }\n break\n }\n case \"status-update\": {\n const { statusType, value, reportHubId } = payload\n const cached = getCachedReport(reportHubId, { ignoreExpiry: true })\n\n if (statusType === \"star\") {\n if (pendingStarUpdates.current.get(reportHubId) === value) {\n pendingStarUpdates.current.delete(reportHubId)\n }\n if (cached) {\n const updatedReport = { ...cached, isStarred: value }\n applyDailyReportServerUpdates(reportHubId, updatedReport)\n setVersion((v) => v + 1)\n } else {\n enqueuePendingSseMessage(reportHubId, payload)\n }\n } else if (statusType === \"read\") {\n if (pendingReadUpdates.current.get(reportHubId) === value) {\n pendingReadUpdates.current.delete(reportHubId)\n }\n if (cached) {\n const updatedReport = { ...cached, isRead: value }\n applyDailyReportServerUpdates(reportHubId, updatedReport)\n setVersion((v) => v + 1)\n } else {\n enqueuePendingSseMessage(reportHubId, payload)\n }\n }\n break\n }\n case \"report-delete\": {\n const { reportHubId } = payload\n manuallyTrackingIds.current.delete(reportHubId)\n // マージロジックでの復活を防止するために削除済み ID を記録\n removedIds.current.add(reportHubId)\n // 常駐ストリームデータからも除去 (鮮度窓内の再訪で削除済み日報が蘇らないように)\n removeDailyReportIdsStreamItem(reportHubId)\n\n // 削除されたレポートへの待機中 SSE メッセージを破棄\n const pendingEntry = pendingSseQueue.current.get(reportHubId)\n if (pendingEntry) {\n pendingEntry.unsubscribe()\n clearTimeout(pendingEntry.timer)\n pendingSseQueue.current.delete(reportHubId)\n }\n\n const cached = getCachedReport(reportHubId, { ignoreExpiry: true })\n if (cached?.date) {\n deleteDailyReportCache(reportHubId, cached.date)\n setVersion((v) => v + 1)\n }\n setItems((prev) => prev.filter((item) => item.reportHubId !== reportHubId))\n break\n }\n case \"report-create\":\n case \"report-update\":\n case \"report-publish\": {\n // report はスキーマで必須なのでバリデーション通過後は必ず存在する(if ガード不要)\n const { report } = payload\n // 全ユーザー放送: report の isRead/isStarred は操作者の状態なので、\n // 受信者自身の per-viewer 状態を保持するマージ経路を使う\n applyBroadcastReportUpdate(report)\n if (payload.type === \"report-create\" || payload.type === \"report-publish\") {\n manuallyTrackingIds.current.add(report.reportHubId)\n upsertItem(report.reportHubId, report.date)\n }\n setVersion((v) => v + 1)\n break\n }\n }\n },\n [isProcessedId, isSessionStillOurs, upsertItem, user, enqueuePendingSseMessage],\n )\n\n // 常に最新の handleSseMessage をキューのリプレイから参照できるようにする\n handleSseMessageRef.current = handleSseMessage\n\n /**\n * Cleanup all pending SSE queues on unmount.\n * アンマウント時に全キューをクリーンアップする。\n */\n useEffect(() => {\n return () => {\n for (const entry of pendingSseQueue.current.values()) {\n entry.unsubscribe()\n clearTimeout(entry.timer)\n }\n pendingSseQueue.current.clear()\n }\n }, [])\n\n useDailyReportSseConnection({ isSseEnabled, onMessage: handleSseMessage })\n\n /**\n * Common helper function to execute API actions.\n * APIアクションを実行する共通ヘルパー関数。\n */\n const executeAction = async <T,>(\n reportHubId: number,\n intent: string,\n businessDate: string,\n params: Record<string, string>,\n callbacks: {\n onOptimistic?: () => void\n onSuccess?: (data: ActionResponse) => Promise<T> | T\n onFailure?: () => void\n },\n options?: {\n keepLock?: boolean\n skipLock?: boolean\n },\n ): Promise<T> => {\n if (!options?.skipLock) acquireMutationLock(reportHubId)\n callbacks.onOptimistic?.()\n\n // Generate or use provided clientTempId\n const clientTempId = params.clientTempId || crypto.randomUUID()\n addProcessedId(clientTempId)\n\n const formData = new FormData()\n formData.append(\"intent\", intent)\n formData.append(\"reportHubId\", String(reportHubId))\n formData.append(\"businessDate\", businessDate)\n formData.append(\"operationTimestamp\", String(Date.now()))\n formData.append(\"clientTempId\", clientTempId)\n\n for (const [k, v] of Object.entries(params)) {\n if (k !== \"clientTempId\") {\n formData.append(k, v)\n }\n }\n\n try {\n const res = await fetch(`${apiBasePath}/action`, {\n method: \"POST\",\n body: formData,\n headers: { Accept: \"application/json\" },\n })\n if (!res.ok) {\n const error = new Error(`Failed to ${intent}`)\n Object.assign(error, { status: res.status })\n throw error\n }\n const data = await res.json()\n if (data.status !== \"OK\" || (intent !== \"create\" && String(data.reportHubId) !== String(reportHubId))) {\n throw new Error(`Invalid response for ${intent}`)\n }\n if (typeof data.clientTempId !== \"string\" || data.clientTempId !== clientTempId) {\n throw new Error(`Invalid clientTempId for ${intent}`)\n }\n // ユーザー切替後に解決した継続は、モジュール常駐状態 (常駐セッション・詳細キャッシュ)\n // を旧ユーザーのデータで汚染しないよう onSuccess を放棄する\n if (!isSessionStillOurs()) {\n logger.warn(`[executeAction] ${intent} resolved after a user switch; discarding continuation`)\n throw new Error(`Stale continuation for ${intent}`)\n }\n return (await callbacks.onSuccess?.(data)) as T\n } catch (e) {\n logger.error(`${intent} failed:`, e)\n // 切替後の失敗も同様: onFailure のロールバックが旧値をモジュールキャッシュへ\n // 書き戻すため放棄する (プロバイダー自身の state は key リマウントで破棄済み)。\n // 通知も同じ門で守る: ユーザー切替で捨てた継続の失敗は当人に見せない\n if (isSessionStillOurs()) {\n callbacks.onFailure?.()\n notifyErrorRef.current({ operation: intent, message: describeOperationFailure(intent), cause: e })\n }\n throw e\n } finally {\n if (!(options?.keepLock || options?.skipLock)) {\n releaseMutationLock(reportHubId)\n }\n }\n }\n\n const addComment = async (reportHubId: number, content: string, businessDate: string) => {\n const id = Number(reportHubId)\n const tempId = -1 * Date.now()\n const promise = executeAction(\n id,\n \"addComment\",\n businessDate,\n { content, clientTempId: String(tempId) },\n {\n onOptimistic: () => {\n if (!pendingAddCommentIds.current.has(id)) pendingAddCommentIds.current.set(id, new Map())\n pendingAddCommentIds.current.get(id)?.set(tempId, content)\n setVersion((v) => v + 1)\n },\n onSuccess: (data) => {\n if (!data.newComment) throw new Error(\"No new comment returned\")\n const realId = data.newComment.id\n resolvedIdMap.current.set(tempId, realId)\n pendingAddCommentIds.current.get(id)?.delete(tempId)\n\n const cachedReport = getCachedReport(id, { ignoreExpiry: true })\n if (cachedReport && !cachedReport.commentItems.some((c) => c.id === realId)) {\n updateDailyReportCache(id, {\n commentItems: [...cachedReport.commentItems, { ...data.newComment, id: realId, isMine: true, userName: getUserDisplayName(user) }],\n updatedAt: data.updatedAt,\n })\n }\n return realId\n },\n onFailure: () => {\n pendingAddCommentIds.current.get(id)?.delete(tempId)\n },\n },\n { skipLock: true },\n ).finally(() => pendingAddPromises.current.delete(tempId))\n\n pendingAddPromises.current.set(tempId, promise)\n await promise\n }\n\n const deleteComment = async (reportHubId: number, commentId: number, businessDate: string) => {\n const id = Number(reportHubId)\n // 1. Optimistic UI Update\n if (!pendingDeleteCommentIds.current.has(id)) pendingDeleteCommentIds.current.set(id, new Set())\n pendingDeleteCommentIds.current.get(id)?.add(commentId)\n setVersion((v) => v + 1)\n\n // 2. Resolve ID\n let realId = commentId\n if (commentId < 0) {\n const resolved = resolvedIdMap.current.get(commentId)\n if (resolved !== undefined) {\n realId = resolved\n } else {\n const promise = pendingAddPromises.current.get(commentId)\n if (!promise) return // 既に削除済みかエラー\n try {\n realId = await promise\n } catch {\n return // 追加失敗時は削除不要\n }\n }\n }\n\n // 3. Server Request\n await executeAction(\n id,\n \"deleteComment\",\n businessDate,\n { commentId: String(realId) },\n {\n onSuccess: (data) => {\n const deletedId = typeof data.deletedCommentId === \"string\" ? Number(data.deletedCommentId) : realId\n const cachedReport = getCachedReport(id, { ignoreExpiry: true })\n if (cachedReport) {\n updateDailyReportCache(id, {\n commentItems: cachedReport.commentItems.filter((c) => c.id !== deletedId),\n updatedAt: data.updatedAt,\n })\n registerRecentDeletion(deletedId)\n }\n pendingDeleteCommentIds.current.get(id)?.delete(commentId)\n },\n onFailure: () => {\n pendingDeleteCommentIds.current.get(id)?.delete(commentId)\n },\n },\n { skipLock: true },\n )\n }\n\n const toggleStar = async (reportHubId: number, isStarred: boolean, businessDate: string) => {\n const id = Number(reportHubId)\n await executeAction(\n id,\n \"toggleStar\",\n businessDate,\n { isStarred: String(isStarred) },\n {\n onOptimistic: () => {\n updateDailyReportCache(id, { isStarred })\n pendingStarUpdates.current.set(id, isStarred)\n setVersion((v) => v + 1)\n },\n onSuccess: (data) => {\n if (data.updatedStatus && typeof data.updatedStatus.isStarred === \"boolean\") {\n const cached = getCachedReport(id, { ignoreExpiry: true })\n if (cached) {\n updateDailyReportCache(id, { isStarred: data.updatedStatus.isStarred, updatedAt: data.updatedAt })\n setVersion((v) => v + 1)\n }\n }\n pendingStarUpdates.current.delete(id)\n },\n onFailure: () => {\n updateDailyReportCache(id, { isStarred: !isStarred })\n pendingStarUpdates.current.delete(id)\n },\n },\n { skipLock: true },\n )\n }\n\n const toggleReadImpl = async (reportHubId: number, isRead: boolean, businessDate: string) => {\n const id = Number(reportHubId)\n await executeAction(\n id,\n \"toggleRead\",\n businessDate,\n { isRead: String(isRead) },\n {\n onOptimistic: () => {\n updateDailyReportCache(id, { isRead })\n pendingReadUpdates.current.set(id, isRead)\n setVersion((v) => v + 1)\n },\n onSuccess: (data) => {\n if (data.updatedStatus && typeof data.updatedStatus.isRead === \"boolean\") {\n const cached = getCachedReport(id, { ignoreExpiry: true })\n if (cached) {\n updateDailyReportCache(id, { isRead: data.updatedStatus.isRead, updatedAt: data.updatedAt })\n setVersion((v) => v + 1)\n }\n }\n pendingReadUpdates.current.delete(id)\n },\n onFailure: () => {\n updateDailyReportCache(id, { isRead: !isRead })\n pendingReadUpdates.current.delete(id)\n },\n },\n { skipLock: true },\n )\n }\n // ref 経由で最新実装を参照し、安定した関数 identity を提供する\n // (SSE イベントによるコンテキスト再レンダリングで auto-read タイマーがリセットされるのを防止)\n const toggleReadRef = useRef(toggleReadImpl)\n toggleReadRef.current = toggleReadImpl\n const toggleRead = useCallback((reportHubId: number, isRead: boolean, businessDate: string) => toggleReadRef.current(reportHubId, isRead, businessDate), [])\n\n const createReport = async (businessDate: string) => {\n const tempId = -1 * Date.now()\n // Optimistic UI update\n const now = new Date().toISOString()\n const dummyReport: DailyReportDetail = {\n reportHubId: tempId,\n date: businessDate,\n createdAt: now,\n author: getUserDisplayName(user),\n userId: user?.id ?? \"unknown\",\n sourceType: \"Internal\",\n employeeName: getUserDisplayName(user),\n updatedBy: getUserDisplayName(user),\n updatedAt: now,\n category: null,\n creationCategory: null,\n visitTimeFrom: null,\n visitTimeTo: null,\n customerName: null,\n interviewers: [],\n subject: \"\",\n content: \"\",\n comments: [],\n isRead: true,\n isStarred: false,\n labels: [{ id: 0, name: draftLabelName, color: \"#9ca3af\" }],\n commentItems: [],\n // 楽観更新の仮レポート。作成直後に添付は存在しない\n attachments: [],\n }\n writeCache(dummyReport, Date.now() + 300000, false)\n\n setItems((prev) => [\n {\n reportHubId: tempId,\n businessDate: businessDate,\n },\n ...prev,\n ])\n\n try {\n const res = await executeAction<ActionResponse>(\n 0,\n \"create\",\n businessDate,\n { clientTempId: String(tempId) },\n {\n onSuccess: (data) => data,\n },\n )\n // Validate response mapping\n if (res.clientTempId && res.clientTempId !== String(tempId)) {\n logger.warn(`[createReport] ID mismatch. req=${tempId}, res=${res.clientTempId}`)\n // Mismatch implies this response is not for this specific request. Throwing here triggers revert.\n throw new Error(\"Optimistic ID mismatch\")\n }\n\n const newId = Number(res.reportHubId)\n\n // 常駐ストリームセッションへも反映する。自分の SSE エコーは clientTempId で\n // 抑止されるため upsertItem 経由の中継は届かず、ここで直接中継しないと\n // 鮮度窓内の再訪で作成直後の日報が一覧から消える\n upsertDailyReportIdsStreamItem({ reportHubId: newId, businessDate })\n\n // newId を一時的にロックし、createReport 以前に開始された背景 prefetch の\n // cacheMany による stale データ上書きを防止する\n acquireMutationLock(newId)\n\n // Seed cache with server response (checking staleness)\n if (res.report) {\n applyDailyReportServerUpdates(newId, res.report)\n setVersion((v) => v + 1)\n }\n\n setItems((prev) => {\n // If newId is already present (e.g. from SSE), just remove tempId to avoid duplication\n if (prev.some((item) => item.reportHubId === newId)) {\n return prev.filter((item) => item.reportHubId !== tempId)\n }\n return prev.map((item) => (item.reportHubId === tempId ? { ...item, reportHubId: newId } : item))\n })\n\n // ロック解放: lastMutations に newId が記録され、以降の cacheMany でも保護される\n setTimeout(() => releaseMutationLock(newId), 1000)\n\n return newId\n } catch (e) {\n logger.error(\"[createReport] Failed or mismatched\", e)\n // Revert optimistic update\n setItems((prev) => prev.filter((item) => item.reportHubId !== tempId))\n throw e\n }\n }\n\n const updateReport = async (reportHubId: number, data: { title?: string; content?: string }, businessDate: string) => {\n const id = Number(reportHubId)\n await executeAction(\n id,\n \"update\",\n businessDate,\n { ...(data.title && { title: data.title }), ...(data.content && { content: data.content }) },\n {\n onSuccess: () => {\n const cached = getCachedReport(id, { ignoreExpiry: true })\n if (cached) {\n updateDailyReportCache(id, {\n subject: data.title ?? cached.subject,\n content: data.content ?? cached.content,\n })\n setVersion((v) => v + 1)\n }\n },\n },\n )\n }\n\n const publishReport = async (reportHubId: number, businessDate: string) => {\n const id = Number(reportHubId)\n const cached = getCachedReport(id, { ignoreExpiry: true })\n const oldLabels = cached?.labels || []\n\n await executeAction(\n id,\n \"publish\",\n businessDate,\n {},\n {\n onOptimistic: () => {\n if (cached) {\n const newLabels = cached.labels.filter((l) => l.name !== draftLabelName)\n updateDailyReportCache(id, {\n labels: newLabels,\n })\n setVersion((v) => v + 1)\n }\n },\n onSuccess: (data) => {\n // サーバーからのレスポンスを正とする\n if (data.report) {\n applyDailyReportServerUpdates(id, data.report)\n setVersion((v) => v + 1)\n }\n // 成功確定後に常駐ストリームセッションへも反映する (自分の SSE エコーは\n // 抑止されるため)。失敗時に走らせると、並行削除済みの日報を誤って\n // 蘇らせトゥームストーンまで解除してしまう\n upsertDailyReportIdsStreamItem({ reportHubId: id, businessDate })\n },\n onFailure: () => {\n // ロールバック\n updateDailyReportCache(id, {\n labels: oldLabels,\n })\n setVersion((v) => v + 1)\n },\n },\n { skipLock: true },\n )\n }\n\n const deleteReport = async (reportHubId: number, businessDate: string) => {\n const id = Number(reportHubId)\n const targetItem = items.find((i) => i.reportHubId === id)\n\n // マージロジックでの復活を防止するために削除済み ID を記録\n removedIds.current.add(id)\n // 常駐ストリームデータからも除去 (鮮度窓内の再訪で削除済み日報が蘇らないように)\n removeDailyReportIdsStreamItem(id)\n // Optimistic UI update\n setItems((prev) => prev.filter((item) => item.reportHubId !== id))\n\n try {\n await executeAction(\n id,\n \"delete\",\n businessDate,\n {},\n {\n onSuccess: (_data) => {\n deleteDailyReportCache(id, businessDate)\n // Note: Item already removed from list optimistically\n },\n },\n )\n } catch (e: unknown) {\n const error = e as { status?: number }\n // 既に削除済み(404)の場合は、成功とみなしてロールバックしない(キャッシュはクリアする)\n if (error.status === 404) {\n deleteDailyReportCache(id, businessDate)\n return\n }\n\n // それ以外のエラーの場合は、楽観的更新をロールバックする。\n // 常駐ストリームデータの除去とトゥームストーンも upsert で巻き戻す\n // (巻き戻さないと、実在する日報が次の全量走査まで再訪時の一覧から消える)。\n // ユーザー切替後に解決した継続は巻き戻さない — 旧ユーザーの行を新ユーザーの\n // セッションへ upsert しトゥームストーンまで解除してしまうため\n // (元の catch と同様に握り潰す。呼び出し元 UI は既に破棄済み)\n if (!isSessionStillOurs()) {\n return\n }\n removedIds.current.delete(id)\n upsertDailyReportIdsStreamItem({ reportHubId: id, businessDate: targetItem?.businessDate ?? businessDate })\n if (targetItem) {\n setItems((prev) => {\n if (prev.some((i) => i.reportHubId === id)) return prev\n // ロールバックも正準比較器の位置へ戻す (第 3 の比較器を持たない)\n return upsertInIdsStreamOrder(prev, targetItem)\n })\n }\n }\n }\n\n return (\n <DailyReportActionContext.Provider\n value={{\n user,\n pendingAddCommentIds,\n pendingDeleteCommentIds,\n pendingStarUpdates,\n pendingReadUpdates,\n resolvedIdMap,\n version,\n items,\n isSseEnabled,\n toggleSse,\n addComment,\n deleteComment,\n toggleStar,\n toggleRead,\n createReport,\n updateReport,\n publishReport,\n deleteReport,\n removeStaleItem,\n refetchData,\n clearCacheAndRefetch,\n }}>\n {children}\n </DailyReportActionContext.Provider>\n )\n}\n","/**\n * Wire contract and ordering helpers for the daily-report ids NDJSON stream.\n * 日報 ID 一覧 NDJSON ストリームのワイヤ契約と並び順ヘルパー。\n *\n * サーバーは `GET {apiBasePath}/ids-stream` で 1 行 1 JSON の NDJSON を返す。\n * 行は原則 {@link DailyReportIdsStreamChunkLine} で、送出中に予期しない失敗が\n * 起きた場合のみ {@link DailyReportIdsStreamErrorLine} を 1 行流して正常クローズする\n * (クライアントは「isLast 無しで終端」を再開シグナルとして扱う)。\n *\n * 並び順は SQL の `ORDER BY business_date DESC, id DESC` (MSSQL の DESC は NULL を\n * 末尾に置く) と完全一致させる。カーソル再開はこの全順序に対する「厳密に後」の\n * 位置からのスライスとして定義する。\n */\nimport type { DailyReportItem } from \"./types\"\n\n/** ストリーム再開カーソル。直前チャンク末尾アイテムの並び順キー。 */\nexport type DailyReportIdsStreamCursor = {\n /** 営業日 (YYYY-MM-DD)。null は「営業日なし領域」(並び順の末尾) を表す。 */\n businessDate: string | null\n /** 日報 Hub ID。 */\n reportHubId: number\n}\n\n/** データを運ぶ NDJSON 行。 */\nexport type DailyReportIdsStreamChunkLine = {\n /** このチャンクのアイテム群 (並び順どおり)。 */\n items: DailyReportItem[]\n /** 次チャンクの再開カーソル。最終チャンクでは null。 */\n nextCursor: DailyReportIdsStreamCursor | null\n /** これが最終チャンクかどうか。 */\n isLast: boolean\n /** 全体件数。接続ごとの先頭行にのみ載る。 */\n total?: number\n}\n\n/** サーバー側の予期しない失敗を伝える NDJSON 行 (内部情報は含めない)。 */\nexport type DailyReportIdsStreamErrorLine = {\n error: string\n}\n\n/** NDJSON 1 行のユニオン。 */\nexport type DailyReportIdsStreamLine = DailyReportIdsStreamChunkLine | DailyReportIdsStreamErrorLine\n\n/**\n * Type guard for chunk lines (anything else is treated as an error line).\n * チャンク行の型ガード (それ以外はエラー行として扱う)。\n *\n * @param line Parsed NDJSON line. パース済みの NDJSON 行。\n * @returns True when the line carries items. アイテムを運ぶ行なら true。\n */\nexport const isIdsStreamChunkLine = (line: unknown): line is DailyReportIdsStreamChunkLine => {\n return typeof line === \"object\" && line !== null && Array.isArray((line as { items?: unknown }).items)\n}\n\n/**\n * Total order of the ids stream: businessDate DESC (NULL last), then id DESC.\n * ids ストリームの全順序。営業日降順 (NULL は末尾)、同日内は ID 降順。\n *\n * MSSQL の `ORDER BY business_date DESC, id DESC` と一致させることが唯一の存在理由。\n * 営業日は正規化済みの `YYYY-MM-DD` 文字列なので辞書順比較 = 時系列比較になる。\n *\n * @param a Left key. 左辺キー。\n * @param b Right key. 右辺キー。\n * @returns Negative when `a` streams before `b`. `a` が先に流れるなら負値。\n */\nexport const compareIdsStreamOrder = (a: DailyReportIdsStreamCursor, b: DailyReportIdsStreamCursor): number => {\n if (a.businessDate !== b.businessDate) {\n // null 営業日は並び順の末尾 (MSSQL DESC の NULL 末尾挙動へ揃える)\n if (a.businessDate === null) return 1\n if (b.businessDate === null) return -1\n return a.businessDate > b.businessDate ? -1 : 1\n }\n return b.reportHubId - a.reportHubId\n}\n\n/**\n * Checks whether items already follow the ids-stream total order.\n * アイテム配列が ids ストリームの全順序に既に従っているかを検査する処理。\n *\n * ストリームはソート順どおりに届いて末尾へ追記されるため通常は整列済みで、\n * この O(n) 検査で全件ソート (O(n log n)) を省略できる。\n *\n * @param items Items to probe. 検査対象のアイテム配列。\n * @returns True when already ordered. 整列済みなら true。\n */\nexport const isIdsStreamOrdered = (items: readonly DailyReportItem[]): boolean => {\n for (let i = 1; i < items.length; i++) {\n if (compareIdsStreamOrder(items[i - 1], items[i]) > 0) return false\n }\n return true\n}\n\n/**\n * Returns a copy with the item upserted at its ids-stream-ordered position.\n * アイテムを ids ストリーム順の正しい位置へ upsert した複製配列を返す処理。\n *\n * 前提: `items` は ids ストリーム順に整列済み (このモジュールの比較器を使う全書き込み元が\n * 維持する不変条件)。既存の同 ID は除去してから二分探索で挿入位置を求めるため、\n * 全件ソート無しの O(n) で並び順を保てる (以前は SSE 1 件ごとに 20 万件超を\n * localeCompare で全ソートしていた)。\n *\n * @param items Ordered items. 整列済みアイテム配列。\n * @param item Item to upsert. upsert するアイテム。\n * @returns New ordered array. 新しい整列済み配列。\n */\nexport const upsertInIdsStreamOrder = (items: readonly DailyReportItem[], item: DailyReportItem): DailyReportItem[] => {\n // 既存の同 ID を除いた複製 (位置移動を伴う更新もこれで単一経路になる)\n const next = items.filter((existing) => existing.reportHubId !== item.reportHubId)\n let low = 0\n let high = next.length\n while (low < high) {\n const mid = (low + high) >> 1\n if (compareIdsStreamOrder(next[mid], item) <= 0) {\n low = mid + 1\n } else {\n high = mid\n }\n }\n next.splice(low, 0, item)\n return next\n}\n\n/**\n * Finds the first index that streams strictly after the cursor.\n * カーソルより厳密に後ろへ並ぶ最初のインデックスを求める処理。\n *\n * 配列は必ずストリーム順 (SQL の ORDER BY) で渡されるが、比較器とのズレが\n * あっても壊れない線形走査で決定する (件数は高々数万・再開時のみの実行)。\n * 該当なし (カーソルが末尾以降) は `items.length` を返し、空チャンク +\n * `isLast: true` の正常終端に落ちる。\n *\n * @param items Stream-ordered items. ストリーム順のアイテム配列。\n * @param cursor Resume cursor. 再開カーソル。\n * @returns Index to resume emission from. 送出を再開するインデックス。\n */\nexport const findIdsStreamResumeIndex = (items: readonly DailyReportItem[], cursor: DailyReportIdsStreamCursor): number => {\n const index = items.findIndex((item) => compareIdsStreamOrder({ businessDate: item.businessDate, reportHubId: item.reportHubId }, cursor) > 0)\n return index === -1 ? items.length : index\n}\n","/**\n * Minimal level-filtered logger used internally by the daily-report package.\n * daily-report パッケージ内部で使う最小のレベルフィルタ付きロガー。\n *\n * 消費アプリのロガー実装 (console 互換の debug/info/warn/error) を注入でき、\n * 未注入時は console にフォールバックする。\n */\n\n/** 出力フィルタリング用のログレベル。 */\nexport enum LogLevel {\n DEBUG = 0,\n INFO = 1,\n WARN = 2,\n ERROR = 3,\n NONE = 4,\n}\n\n/** Console 互換のロガーインターフェース。 */\nexport interface DailyReportLogger {\n debug(message?: unknown, ...optionalParams: unknown[]): void\n info(message?: unknown, ...optionalParams: unknown[]): void\n warn(message?: unknown, ...optionalParams: unknown[]): void\n error(message?: unknown, ...optionalParams: unknown[]): void\n}\n\n/**\n * Creates a prefixed, level-filtered logger delegating to the given implementation.\n * 指定実装へ委譲するプレフィックス付き・レベルフィルタ付きロガーを生成する処理。\n */\nexport const createLogger = (level: LogLevel, prefix: string, impl: DailyReportLogger = console): DailyReportLogger => {\n // 文字列メッセージはプレフィックスを連結、それ以外は先頭引数として付加する\n const format = (message: unknown): unknown[] => (typeof message === \"string\" ? [`${prefix} ${message}`] : [prefix, message])\n return {\n debug: (message?: unknown, ...rest: unknown[]) => {\n if (level <= LogLevel.DEBUG) impl.debug(...format(message), ...rest)\n },\n info: (message?: unknown, ...rest: unknown[]) => {\n if (level <= LogLevel.INFO) impl.info(...format(message), ...rest)\n },\n warn: (message?: unknown, ...rest: unknown[]) => {\n if (level <= LogLevel.WARN) impl.warn(...format(message), ...rest)\n },\n error: (message?: unknown, ...rest: unknown[]) => {\n if (level <= LogLevel.ERROR) impl.error(...format(message), ...rest)\n },\n }\n}\n","/**\n * Suspense-friendly daily report detail loader (fetch-driven).\n * fetch ベースの日報詳細ローダー。\n */\nimport { useCallback, useEffect, useRef, useState } from \"react\"\nimport { normalizeBusinessDateKey } from \"../../shared/business-date\"\nimport { createLogger, LogLevel } from \"../../shared/logger\"\nimport type { DailyReportDetail } from \"../../shared/types\"\nimport { useDailyReportConfig } from \"../config-context\"\nimport { DAILY_REPORT_FETCH_DEBOUNCE_MS } from \"../utils/constants\"\n\nconst logger = createLogger(LogLevel.INFO, \"use-daily-report\")\n\n// --- Configuration & Constants ---\n/**\n * Retrieve global configuration or default.\n * グローバル設定またはデフォルト値を取得する。\n */\nconst getGlobal = (k: string, d: number): number => {\n if (typeof window === \"undefined\") return d\n const v = (window as unknown as Record<string, unknown>)[k]\n return typeof v === \"number\" && !Number.isNaN(v) ? v : d\n}\nconst TTL = getGlobal(\"__DAILY_REPORT_CACHE_TTL__\", 300_000)\nconst REVIVE_MS = getGlobal(\"__DAILY_REPORT_CACHE_REVIVE_MS__\", 60_000)\n\n// --- Cache Store (Centralized State) ---\ntype CacheEntry<T> = { data: T; expiresAt: number }\n\nexport const DailyReportCache = {\n reports: new Map<number, CacheEntry<DailyReportDetail>>(),\n lists: new Map<string, CacheEntry<DailyReportDetail[]>>(),\n pendingLists: new Map<string, Promise<DailyReportDetail[]>>(),\n pendingReports: new Map<number, Promise<DailyReportDetail>>(),\n listeners: new Set<(id: number) => void>(),\n locks: new Set<number>(),\n lastMutations: new Map<number, number>(),\n recentDeletes: new Map<number, number>(),\n lastPrune: 0,\n\n /**\n * Prune expired entries from the cache.\n * キャッシュから期限切れのエントリを削除する。\n */\n prune() {\n const now = Date.now()\n // Prune at most once per minute\n if (now - this.lastPrune < 60_000) return\n this.lastPrune = now\n\n // SWR のために古いデータを保持する。TTL 超過後すぐには削除せず、十分に古い(例えば 1時間経過した)データのみを破棄する\n const GC_THRESHOLD = 60 * 60 * 1000 // 1 hour\n for (const [id, entry] of this.reports) {\n if (now - entry.expiresAt > GC_THRESHOLD) this.reports.delete(id)\n }\n for (const [key, entry] of this.lists) {\n if (now - entry.expiresAt > GC_THRESHOLD) this.lists.delete(key)\n }\n },\n\n /**\n * Clear all cached entries and pending requests.\n * すべてのキャッシュエントリおよび待機中リクエストをクリアする。\n */\n clear() {\n this.reports.clear()\n this.lists.clear()\n this.pendingLists.clear()\n this.pendingReports.clear()\n this.locks.clear()\n this.lastMutations.clear()\n this.recentDeletes.clear()\n this.listeners.forEach((listener) => {\n listener(-1)\n })\n },\n\n /**\n * Check if the cache entry is still valid.\n * キャッシュエントリがまだ有効かを確認する。\n */\n isValid: (ex: number) => ex > Date.now(),\n /**\n * Notify all listeners of a report update.\n * レポートの更新をすべてのリスナーに通知する。\n */\n notify: (id: number) => {\n DailyReportCache.listeners.forEach((l) => {\n l(id)\n })\n },\n\n /**\n * Retrieve a cached report by ID.\n * ID でキャッシュされたレポートを取得する。\n */\n getReport(id: number, ignoreExpiry = false) {\n const e = this.reports.get(id)\n return e && (ignoreExpiry || this.isValid(e.expiresAt)) ? e.data : null\n },\n\n /**\n * Retrieve a cached list of reports by business date key.\n * 営業日キーでレポートのキャッシュリストを取得する。\n */\n getList(dateKey: string | null, ignoreExpiry = false) {\n const e = dateKey && this.lists.get(dateKey)\n return e && (ignoreExpiry || this.isValid(e.expiresAt)) ? e.data : null\n },\n\n /**\n * Set a report in the cache and optionally sync with the list cache.\n * キャッシュにレポートを設定し、必要に応じてリストキャッシュと同期する。\n */\n set(report: DailyReportDetail, expiresAt: number, syncList = true) {\n this.reports.set(report.reportHubId, { data: report, expiresAt })\n if (syncList) {\n const dateKey = normalizeBusinessDateKey(report.date)\n const listEntry = dateKey ? this.lists.get(dateKey) : null\n if (listEntry && dateKey) {\n const exists = listEntry.data.some((r) => r.reportHubId === report.reportHubId)\n const data = exists ? listEntry.data.map((r) => (r.reportHubId === report.reportHubId ? report : r)) : [report, ...listEntry.data]\n this.lists.set(dateKey, { ...listEntry, data })\n }\n }\n this.notify(report.reportHubId)\n },\n\n /**\n * Update a cached report with partial data.\n * 部分的なデータでキャッシュされたレポートを更新する。\n */\n update(id: number, updates: Partial<DailyReportDetail>, strict = true) {\n const entry = this.reports.get(id)\n if (!entry?.data) return\n\n if (strict && updates.updatedAt && entry.data.updatedAt) {\n if (new Date(updates.updatedAt).getTime() < new Date(entry.data.updatedAt).getTime()) {\n logger.warn(`[DailyReportCache] Stale update skipped id=${id}`)\n return\n }\n }\n this.set({ ...entry.data, ...updates } as DailyReportDetail, Math.max(entry.expiresAt, Date.now() + REVIVE_MS))\n },\n\n /**\n * Delete a report from the cache and update the corresponding list.\n * キャッシュからレポートを削除し、対応するリストを更新する。\n */\n delete(id: number, date: string) {\n this.reports.delete(id)\n const dateKey = normalizeBusinessDateKey(date)\n const list = dateKey && this.lists.get(dateKey)\n if (list && dateKey) {\n this.lists.set(dateKey, { ...list, data: list.data.filter((r) => r.reportHubId !== id) })\n }\n this.notify(id)\n },\n\n /**\n * Merge server report with cached report to handle optimistic updates and zombies.\n * サーバーレポートとキャッシュレポートをマージして、楽観的更新とゾンビコメントを処理する。\n */\n merge(serverReport: DailyReportDetail, cachedReport?: DailyReportDetail | null) {\n let report = serverReport\n // Filter zombies\n if (report.commentItems.some((c) => this.recentDeletes.has(c.id))) {\n report = { ...report, commentItems: report.commentItems.filter((c) => !this.recentDeletes.has(c.id)) }\n }\n // Restore missing \"my comments\"\n if (cachedReport) {\n const serverIds = new Set(report.commentItems.map((c) => c.id))\n const missing = cachedReport.commentItems.filter((c) => !serverIds.has(c.id) && c.isMine && Date.now() - new Date(c.createdAt || 0).getTime() < 10000)\n if (missing.length > 0) {\n report = {\n ...report,\n commentItems: [...report.commentItems, ...missing].sort((a, b) => new Date(a.createdAt || 0).getTime() - new Date(b.createdAt || 0).getTime()),\n }\n }\n }\n return report\n },\n\n /**\n * Hydrate the cache with fetched reports, handling comment merges and locks.\n * フェッチされたレポートでキャッシュをハイドレートし、コメントのマージとロックを処理する。\n */\n cacheMany(reports: DailyReportDetail[], dateKey: string | null, fetchStart = 0) {\n if (!reports.length) return []\n this.prune()\n const now = Date.now(),\n expiresAt = now + TTL\n for (const [id, t] of this.recentDeletes) if (now - t > 10000) this.recentDeletes.delete(id)\n\n const processed = reports.map((r) => {\n // Original logic: If locked, return server data directly (don't update cache, don't merge)\n // ロックされている場合はサーバーデータをそのまま返す(キャッシュ更新せず、マージもしない)\n if (this.locks.has(r.reportHubId)) return r\n\n // Original logic: If recently mutated locally, prefer cached data\n // 最近ローカルで変更された場合は、キャッシュデータを優先する\n const lastMut = this.lastMutations.get(r.reportHubId)\n if (lastMut && fetchStart > 0 && lastMut > fetchStart) {\n return this.reports.get(r.reportHubId)?.data ?? r\n }\n\n const cached = this.reports.get(r.reportHubId)?.data\n const merged = this.merge(r, cached)\n this.set(merged, expiresAt, false)\n return merged\n })\n if (dateKey) this.lists.set(dateKey, { data: processed, expiresAt })\n return processed\n },\n}\n\n// --- Public API Exports ---\n/**\n * Update the cache for a single report.\n * 単一レポートのキャッシュを更新する。\n */\nexport const writeCache = (r: DailyReportDetail, exp: number, sync = true) => DailyReportCache.set(r, exp, sync)\n/**\n * Get a cached report by ID.\n * ID でキャッシュされたレポートを取得する。\n */\nexport const getCachedReport = (id: number, opts?: { ignoreExpiry?: boolean }) => DailyReportCache.getReport(id, opts?.ignoreExpiry)\n/**\n * Update specific fields of a cached report.\n * キャッシュされたレポートの特定のフィールドを更新する。\n */\nexport const updateDailyReportCache = (id: number, u: Partial<DailyReportDetail>) => DailyReportCache.update(id, u)\n/**\n * Delete a report from the cache.\n * キャッシュからレポートを削除する。\n */\nexport const deleteDailyReportCache = (id: number, d: string) => DailyReportCache.delete(id, d)\n/**\n * Clear all daily report cache store entries.\n * 日報の全キャッシュストアを消去する。\n */\nexport const clearDailyReportCache = () => DailyReportCache.clear()\n\nexport const acquireMutationLock = (id: number) => {\n DailyReportCache.locks.add(id)\n DailyReportCache.lastMutations.set(id, Date.now())\n}\nexport const releaseMutationLock = (id: number) => {\n DailyReportCache.locks.delete(id)\n DailyReportCache.lastMutations.set(id, Date.now())\n}\nexport const registerRecentDeletion = (cId: number) => DailyReportCache.recentDeletes.set(cId, Date.now())\n\n/**\n * Subscribe to cache-ready events for a specific report.\n * Returns an unsubscribe function.\n *\n * 特定レポートのキャッシュ書き込みを監視する。\n * 解除関数を返す。\n */\nexport const subscribeCacheReady = (reportHubId: number, callback: () => void): (() => void) => {\n const listener = (id: number) => {\n if (id === reportHubId) {\n callback()\n }\n }\n DailyReportCache.listeners.add(listener)\n return () => {\n DailyReportCache.listeners.delete(listener)\n }\n}\n\nexport const applyDailyReportServerUpdates = (_reportHubId: number, report: DailyReportDetail) => {\n DailyReportCache.prune()\n DailyReportCache.set(report, Date.now() + TTL)\n}\n\n/**\n * Applies a broadcast (cross-user SSE) report while preserving the viewer's own per-viewer state.\n * 全ユーザー放送 (SSE) の report を、受信者自身の per-viewer 状態を保持したまま反映する処理。\n *\n * SSE の report-create/update/publish が運ぶ isRead/isStarred は**操作した本人の状態**。\n * そのまま書くと受信者の既読/スター表示が他人の状態で上書きされる。共有フィールドだけを\n * 取り込み、per-viewer フィールドは受信者のキャッシュ値を保持する。ローカルに無い日報は\n * 種まきしない (送信者の per-viewer 値で汚すより、次のフェッチが受信者視点の正しい値を\n * 取るのに任せるほうが安全)。\n *\n * ❗ 信頼できる経路 (自分の HTTP 応答・recipient フィルタ済みの自分宛 status-update) は\n * {@link applyDailyReportServerUpdates} を使うこと — そちらは値をそのまま書く。\n */\nexport const applyBroadcastReportUpdate = (report: DailyReportDetail) => {\n DailyReportCache.prune()\n const cached = DailyReportCache.getReport(report.reportHubId, true)\n // per-viewer フィールド (isRead / isStarred / commentItems[].isMine) は送信者視点なので\n // 受信者のキャッシュ値を保持し、未キャッシュなら中立値 (false) で置く。共有フィールドは配信値。\n // ❗ 未キャッシュでも必ず書く: 書かないと鮮度窓内のリストキャッシュに新 id が載らず、\n // 作成/公開された他ユーザーの日報が stale 安全弁でトゥームストーン化され、閲覧中の\n // 受信者の一覧から恒久的に消える (set は既定で syncList=true = リストにも反映する)\n const cachedMine = new Map<number | string, boolean>()\n for (const c of cached?.commentItems ?? []) {\n cachedMine.set(c.id, c.isMine ?? false)\n }\n const commentItems = (report.commentItems ?? []).map((c) => ({ ...c, isMine: cachedMine.get(c.id) ?? false }))\n DailyReportCache.set({ ...report, isRead: cached?.isRead ?? false, isStarred: cached?.isStarred ?? false, commentItems }, Date.now() + TTL)\n}\n\n// --- Data Loading ---\n/**\n * Fetch a single report by ID.\n * ID で単一のレポートを取得する。\n */\nconst fetchReportById = async (apiBasePath: string, id: number) => {\n // 1. Cache Check\n const cached = DailyReportCache.getReport(id)\n if (cached) return cached\n\n // 2. Prevent Duplicate Requests\n const pending = DailyReportCache.pendingReports.get(id)\n if (pending) return pending\n\n // 3. Fetch\n const start = Date.now()\n const p = fetch(`${apiBasePath}/report?reportHubId=${id}`, { cache: \"no-cache\" })\n .then((res) => (res.ok ? res.json() : Promise.reject(`HTTP ${res.status}`)))\n .then((data) => {\n if (data.error) throw new Error(data.error.message)\n const report = data.report as DailyReportDetail\n // 単一レポートの取得時にキャッシュを一括更新する\n return DailyReportCache.cacheMany([report], null, start)[0]\n })\n .finally(() => DailyReportCache.pendingReports.delete(id))\n DailyReportCache.pendingReports.set(id, p)\n return p\n}\n\n/**\n * Fetch reports by date key.\n * 日付キーでレポートを取得する。\n */\nconst fetchReportsByDate = async (apiBasePath: string, dateKey: string) => {\n const list = DailyReportCache.getList(dateKey)\n if (list) return list\n\n const pending = DailyReportCache.pendingLists.get(dateKey)\n if (pending) return pending\n\n const start = Date.now()\n const p = fetch(`${apiBasePath}/business-date?businessDate=${encodeURIComponent(dateKey)}`, { cache: \"no-cache\" })\n .then((res) => (res.ok ? res.json() : Promise.reject(`HTTP ${res.status}`)))\n .then((data) => {\n if (data.error) throw new Error(data.error.message)\n return DailyReportCache.cacheMany(data.reports ?? [], dateKey, start)\n })\n .finally(() => DailyReportCache.pendingLists.delete(dateKey))\n DailyReportCache.pendingLists.set(dateKey, p)\n return p\n}\n\n// --- Hooks ---\nexport type DailyReportDetailResource = { report: DailyReportDetail | null; error: Error | null; isLoading: boolean; isRefetching: boolean }\n\n/**\n * Provides a fetcher-driven daily report detail reader with business-date aware caching.\n * 営業日キャッシュ対応の日報詳細取得フック。\n */\nexport const useDailyReportDetail = (reportHubId: number, businessDate: string | null): DailyReportDetailResource => {\n const { apiBasePath } = useDailyReportConfig()\n const [report, setReport] = useState<DailyReportDetail | null>(() => DailyReportCache.getReport(reportHubId))\n const [error, setError] = useState<Error | null>(null)\n const [isLoading, setIsLoading] = useState(false)\n const [isRefetching, setIsRefetching] = useState(false)\n const reqRef = useRef({ id: reportHubId, token: 0 })\n\n // 1. Cache Subscription\n // キャッシュの購読: グローバルな DailyReportCache が更新されたときにローカルの状態を反応的に更新する。\n // 依存関係: [reportHubId] - 対象のレポート ID が変更されたときのみ再購読する。\n // クリーンアップ: メモリリークを防ぐためにリスナーを解除する。\n useEffect(() => {\n const onUpdate = (uid: number) => {\n if (uid === reportHubId) {\n const r = DailyReportCache.getReport(reportHubId, true)\n setReport(r)\n }\n }\n DailyReportCache.listeners.add(onUpdate)\n return () => {\n DailyReportCache.listeners.delete(onUpdate)\n }\n }, [reportHubId])\n\n // 2. Data Loading & Stale Check\n // データのロードと有効期限切れチェック: データのフェッチサイクル、 stale-while-revalidate ロジック、リクエストのデバウンスを管理する。\n // 依存関係: [reportHubId, businessDate, apiBasePath]\n useEffect(() => {\n const dateKey = normalizeBusinessDateKey(businessDate)\n const token = (reqRef.current.token || 0) + 1\n reqRef.current = { id: reportHubId, token }\n\n if (reportHubId <= 0) {\n setReport(null)\n setError(reportHubId === 0 ? new Error(\"Invalid ID\") : null)\n setIsLoading(false)\n setIsRefetching(false)\n return\n }\n\n // Cache Check\n let cached = DailyReportCache.getReport(reportHubId, true)\n let isStale = false\n if (cached) {\n isStale = !DailyReportCache.isValid(DailyReportCache.reports.get(reportHubId)?.expiresAt ?? 0)\n } else if (dateKey) {\n const list = DailyReportCache.getList(dateKey, true)\n const found = list?.find((r) => r.reportHubId === reportHubId)\n if (found) {\n cached = found\n isStale = !DailyReportCache.isValid(DailyReportCache.lists.get(dateKey)?.expiresAt ?? 0)\n }\n }\n setReport(cached)\n\n // If cached and valid, we are done\n if (cached && !isStale) {\n setError(null)\n setIsLoading(false)\n setIsRefetching(false)\n return\n }\n\n setIsLoading(!cached)\n setIsRefetching(true)\n\n // 未キャッシュ (骨組み表示中) の初回取得は即時に走らせる。デバウンスはスクロール連打の\n // 無駄取りを抑える目的なので、スクロール churn の無い初回に効かせると純粋な遅延になる。\n // キャッシュ済み (内容表示中) の stale 再取得だけデバウンスして churn を吸収する\n const debounceMs = cached ? DAILY_REPORT_FETCH_DEBOUNCE_MS : 0\n\n const timer = setTimeout(async () => {\n if (reqRef.current.token !== token) return\n try {\n let r: DailyReportDetail | null = null\n if (dateKey) {\n const res = await fetchReportsByDate(apiBasePath, dateKey)\n r = res.find((item) => item.reportHubId === reportHubId) ?? null\n } else {\n r = await fetchReportById(apiBasePath, reportHubId)\n }\n\n if (reqRef.current.token === token) {\n setReport(r)\n setError(null)\n }\n } catch (e) {\n if (reqRef.current.token === token) setError(e instanceof Error ? e : new Error(\"Load failed\"))\n } finally {\n if (reqRef.current.token === token) {\n setIsLoading(false)\n setIsRefetching(false)\n }\n }\n }, debounceMs)\n\n return () => clearTimeout(timer)\n }, [reportHubId, businessDate, apiBasePath])\n\n return { report, error, isLoading, isRefetching }\n}\n\n/**\n * Provides a business-date aware prefetch helper.\n * 営業日キャッシュを活用した日報プリフェッチ補助フック。\n */\nexport const useDailyReportPrefetch = () => {\n const { apiBasePath } = useDailyReportConfig()\n return useCallback(\n (date: string | null) => {\n const k = normalizeBusinessDateKey(date)\n if (k && !DailyReportCache.getList(k)) fetchReportsByDate(apiBasePath, k).catch((e) => logger.warn(\"Prefetch error\", k, e))\n },\n [apiBasePath],\n )\n}\n","/**\n * Business date normalization and formatting helper functions.\n * 営業日の正規化とフォーマットを支援するユーティリティ群。\n */\n\ntype BusinessDateInput = string | Date | null | undefined\n\nconst ISO_DATE_PATTERN = /^\\d{4}-\\d{2}-\\d{2}$/\nconst SLASH_DATE_PATTERN = /^\\d{4}\\/\\d{2}\\/\\d{2}$/\n\nconst zeroPad = (value: number): string => {\n return value < 10 ? `0${value}` : `${value}`\n}\n\nconst normalizeFromDate = (value: Date): string | null => {\n if (Number.isNaN(value.getTime())) {\n return null\n }\n\n const year = value.getFullYear()\n const month = zeroPad(value.getMonth() + 1)\n const day = zeroPad(value.getDate())\n return `${year}-${month}-${day}`\n}\n\n/**\n * Returns today's business-date key (YYYY-MM-DD) in the local calendar.\n * ローカルカレンダー上の「本日」の営業日キー (YYYY-MM-DD) を返す。\n *\n * `new Date().toISOString()` の日付部は UTC 日付になり、UTC より東のタイムゾーン\n * (JST など) では深夜〜午前帯に前日へずれる。営業日はブラウザのローカル暦日で判定する\n * ため、ローカル getter から直接組む。タイムゾーンはパッケージに固定しない (消費側の\n * 実行環境が決める) 方針に沿う。\n *\n * @param now Clock injection for tests (defaults to the current time). テスト用の時刻注入 (既定は現在時刻)。\n * @returns Local calendar day as YYYY-MM-DD. ローカル暦日の YYYY-MM-DD。\n */\nexport const resolveTodayBusinessDateKey = (now: Date = new Date()): string => {\n if (Number.isNaN(now.getTime())) {\n // 不正な Date が注入されたら現在時刻で解決する (空の営業日キーを返さない)\n return resolveTodayBusinessDateKey(new Date())\n }\n return `${now.getFullYear()}-${zeroPad(now.getMonth() + 1)}-${zeroPad(now.getDate())}`\n}\n\n/**\n * Normalizes business date input into YYYY-MM-DD format string.\n * 営業日を YYYY-MM-DD 形式の文字列へ正規化する。\n */\nexport const normalizeBusinessDateKey = (value: BusinessDateInput): string | null => {\n if (value === null || value === undefined) {\n return null\n }\n\n if (value instanceof Date) {\n return normalizeFromDate(value)\n }\n\n const trimmed = value.trim()\n if (trimmed === \"\") {\n return null\n }\n\n if (ISO_DATE_PATTERN.test(trimmed)) {\n return trimmed\n }\n\n if (SLASH_DATE_PATTERN.test(trimmed)) {\n return trimmed.replaceAll(\"/\", \"-\")\n }\n\n const parsed = new Date(trimmed)\n return normalizeFromDate(parsed)\n}\n\n/**\n * Converts normalized business date into display format YYYY-MM-DD.\n * 正規化済みの営業日を YYYY-MM-DD 形式の表示文字列へ変換する。\n */\nexport const formatBusinessDateDisplay = (value: BusinessDateInput): string | null => {\n const normalized = normalizeBusinessDateKey(value)\n if (!normalized) {\n return null\n }\n return normalized\n}\n\n/**\n * Parses normalized business date into Date object.\n * 正規化した営業日を Date オブジェクトへ変換する。\n */\nexport const parseBusinessDateKeyToDate = (value: BusinessDateInput): Date | null => {\n const normalized = normalizeBusinessDateKey(value)\n if (!normalized) {\n return null\n }\n\n const [year, month, day] = normalized.split(\"-\").map((part) => Number.parseInt(part, 10))\n if ([year, month, day].some((component) => Number.isNaN(component))) {\n return null\n }\n\n return new Date(year, month - 1, day)\n}\n","import { type ScrollBarTapCircleOptions, type ScrollPaneInertiaOptions, tapScrollCircleSampleVisual } from \"@aiquants/virtualscroll\"\nimport type { DailyReportItem } from \"../../shared/types\"\nimport type { DailyReportDetailResource } from \"../hooks/use-daily-report\"\n\n/**\n * Shared constants for Daily Report data fetching parameters.\n * 日報データ取得で利用する共通パラメーター。\n */\nexport const DAILY_REPORT_FETCH_DEBOUNCE_MS = 120\n\n/**\n * Delay before the provider auto-connects SSE, freeing a browser connection slot for first paint.\n * SSE を自動接続するまでの遅延。初回描画とその中身フェッチへ接続枠を譲るための猶予。\n *\n * HTTP/1.1 のオリジン当たり 6 接続のうち、ids ストリームと SSE で 2 枠を常時占有する。\n * SSE 接続を初回描画直後まで遅らせ、その 1 枠を初回の中身フェッチ (`/business-date`) に回す。\n * リアルタイム更新の初動がこの分だけ遅れるが、その間の状態はストリーム / SWR が担保する。\n */\nexport const SSE_INITIAL_CONNECT_DEFER_MS = 1000\n\nexport const DEFAULT_ITEM_HEIGHT = 160\nexport const ITEM_VERTICAL_PADDING = 6\nexport const ITEM_CONTAINER_HEIGHT = DEFAULT_ITEM_HEIGHT - ITEM_VERTICAL_PADDING\nexport const FAST_SCROLL_WHEEL_MULTIPLIER = 0.8\nexport const FAST_SCROLL_INERTIA_OPTIONS: ScrollPaneInertiaOptions = { maxVelocity: 18, deceleration: 0.0018, startVelocityThreshold: 0.03 }\nexport const PREFETCH_LOOKAHEAD = 20\nexport const PREFETCH_MAX_DATES = 6\nexport const EMPTY_ITEM: DailyReportItem = { reportHubId: -1, businessDate: null }\nexport const EMPTY_DETAIL: DailyReportDetailResource = { report: null, error: null, isLoading: false, isRefetching: false }\nexport const WHEEL_RESET_TIMEOUT_MS = 240\nexport const VIRTUAL_SCROLL_OVERSCAN_COUNT = 15\nexport const SCROLL_BAR_WIDTH = 8\nexport const MAX_PREVIEW_LINES = 3\n\n/**\n * Tap-scroll circle tuning shared by every daily-report list.\n * 日報のすべての一覧が共有するタップスクロールサークルの調整値。\n *\n * ❗ **List タブと DetailList タブで同じ値を使うこと。** 片方だけ独自定義にすると、\n * 同じ指の位置でタブごとに速度が何十倍も違う (実測で最大 77 倍) という事故になる。\n *\n * 値の根拠 (ビューポート 800px で実測):\n *\n * | 引き距離 | 速度 | List (行 160px) |\n * | --- | --- | --- |\n * | 0 | 160 px/s | 1 行/秒 |\n * | 1/4 | 525 px/s | 3.3 行/秒 |\n * | 1/2 | 1,298 px/s | 8 行/秒 |\n * | 最大 | 6,400 px/s | 40 行/秒 (0.67 行/フレーム) |\n *\n * - `exponentialSteepness: 3` — 引き幅の前半をゆっくりにして微速の分解能を確保する。\n * 大きくしすぎると低速域が最小速度へ張り付いて**引いても動かない死角**になる\n * (旧値 7.5 では引き幅の 43.85% が死角だった)。\n * - `exponentialScale: 8` — 最高速の上限 (ビューポート比)。`maxSpeedMultiplier` と揃えて\n * 上限がどちらで決まるかを一意にする。1 フレームあたりの移動をオーバースキャン\n * (`VIRTUAL_SCROLL_OVERSCAN_COUNT`) の行数より十分小さく保つ値にすること。\n * - `easedOffset` は使わない。最小速度 160 px/s で十分に遅く、下駄を履かせると\n * 低速域の分解能がその分だけ失われる。\n *\n * 長距離の移動はタップスクロールではなくスクロールバーのつまみ (と\n * `BusinessDayThumbOverlay` の日付表示) が担う。ここを速くして解こうとしない。\n */\nexport const TAP_SCROLL_CIRCLE_OPTIONS: ScrollBarTapCircleOptions = {\n enabled: true,\n renderVisual: tapScrollCircleSampleVisual,\n maxSpeedCurve: { exponentialSteepness: 3, exponentialScale: 8 },\n maxSpeedMultiplier: 8,\n}\n\n/**\n * Pointer types allowed to drag-scroll the list content.\n * 一覧のコンテンツ領域をドラッグしてスクロールできるポインタ種別。\n *\n * ❗ **`enablePointerDrag` を丸ごと切ってはならない。** `ScrollPane` はコンテンツを\n * transform で動かしておりネイティブのスクロール領域を持たない\n * (`.aqvs-scroll-pane-content` は `overflow: hidden`) ため、切るとタッチ端末から\n * スクロール手段が消える (一度そうしてしまい、モバイルで一切スクロールできなくなった)。\n *\n * マウスのドラッグだけを外す。行のタップ判定 (`useTapClick`) ・テキスト選択と競合するため。\n * 画面幅のメディアクエリで切り替える案は採らなかった。画面幅は入力手段を表さないので\n * 「主入力がマウスのタッチ対応ノート」のような端末を取りこぼす。判定は**イベントの\n * ポインタ種別**で行うため、同一端末でマウスと指を使い分けても常に正しく振る舞う。\n */\nexport const CONTENT_DRAG_POINTER_INPUTS = [\"pen\", \"touch\"] as const\n","/**\n * SSE connection lifecycle hook for daily-report real-time updates.\n * 日報リアルタイム更新の SSE 接続ライフサイクルフック。\n */\nimport { useEffect } from \"react\"\nimport { createLogger, LogLevel } from \"../../shared/logger\"\nimport { type DailyReportSseMessage, dailyReportSseMessageSchema } from \"../../shared/sse-schema\"\nimport { useDailyReportConfig } from \"../config-context\"\n\nconst logger = createLogger(LogLevel.INFO, \"daily-report-sse-connection\")\n\ntype UseDailyReportSseConnectionOptions = {\n isSseEnabled: boolean\n onMessage: (payload: Exclude<DailyReportSseMessage, { type: \"connected\" }>) => void\n}\n\n/**\n * Manages Daily Report SSE connection lifecycle with reconnect handling.\n * Handles open, message parse/dispatch, error logging, retry, and cleanup.\n *\n * 日報 SSE 接続のライフサイクルを管理する Hook。\n * 接続開始、メッセージ解析と配送、エラー時再接続、クリーンアップを管理します。\n */\nexport const useDailyReportSseConnection = ({ isSseEnabled, onMessage }: UseDailyReportSseConnectionOptions) => {\n const { ssePath } = useDailyReportConfig()\n\n /**\n * Maintains SSE connection while enabled and retries on error.\n * isSseEnabled が true の間は接続を維持し、エラー時は再接続します。\n */\n useEffect(() => {\n if (!isSseEnabled) return\n\n let eventSource: EventSource | null = null\n let reconnectTimer: ReturnType<typeof setTimeout> | null = null\n let reconnectAttempt = 0\n let lastReceivedEventId: string | undefined\n\n const connect = () => {\n eventSource?.close()\n eventSource = new EventSource(`${ssePath}${lastReceivedEventId ? `?lastEventId=${lastReceivedEventId}` : \"\"}`)\n\n eventSource.onopen = () => {\n reconnectAttempt = 0\n logger.info(\"SSE Connected\")\n }\n\n eventSource.onmessage = (event) => {\n if (event.lastEventId) {\n lastReceivedEventId = event.lastEventId\n }\n try {\n const raw = JSON.parse(event.data)\n const result = dailyReportSseMessageSchema.safeParse(raw)\n if (!result.success) {\n logger.error(\"SSE Validation Error:\", result.error.flatten(), \"raw message:\", raw)\n return\n }\n if (result.data.type === \"connected\") {\n logger.info(\"SSE Subscribe Ready\")\n return\n }\n onMessage(result.data)\n } catch (e) {\n logger.error(\"SSE Parse Error\", e)\n }\n }\n\n eventSource.onerror = (e) => {\n eventSource?.close()\n eventSource = null\n reconnectAttempt++\n const delayMs = Math.min(2000 * 2 ** (reconnectAttempt - 1), 30000)\n logger.error(`SSE Error. Retrying in ${delayMs}ms (attempt ${reconnectAttempt})...`, e)\n reconnectTimer = setTimeout(connect, delayMs)\n }\n }\n\n connect()\n\n return () => {\n if (eventSource) eventSource.close()\n if (reconnectTimer) clearTimeout(reconnectTimer)\n }\n }, [isSseEnabled, onMessage, ssePath])\n}\n","/**\n * Zod schemas for daily-report SSE messages (client/server shared contract).\n * 日報 SSE メッセージの zod スキーマ (client / server 共有契約)。\n */\nimport { z } from \"zod\"\nimport type { DailyReportCommentItem as DeclaredComment, DailyReportDetail as DeclaredDetail } from \"./types\"\n\n// --- サブスキーマ(types.ts の型と構造的に一致させる) ---\n\nexport const dailyReportInterviewerSchema = z.object({\n name: z.string().nullish(),\n affiliation: z.string().nullish(),\n})\n\nexport const dailyReportCommentSchema = z.object({\n name: z.string().nullish(),\n text: z.string().nullish(),\n color: z.string().nullish(),\n})\n\nexport const dailyReportLabelDefSchema = z.object({\n id: z.number(),\n name: z.string().nullish(),\n color: z.string().nullish(),\n})\n\nexport const dailyReportCommentItemSchema = z.object({\n id: z.number(),\n userId: z.string().nullish(),\n userName: z.string().nullish(),\n content: z.string().nullish(),\n createdAt: z.string().nullish(),\n isMine: z.boolean(),\n})\n\n/**\n * 添付ファイル 1 件のスキーマ。\n *\n * 周囲のフィールドは `.nullish()` だが、ここは意図して `.nullable()` を使う。\n * `.nullish()` は推論キーを任意 (`?`) にするため、`types.ts` 側の必須 `fileType: string | null`\n * へ代入できず、ファイル末尾の `_AssertDetailCompat` が `never` に落ちてコンパイルエラーになる。\n * 既存の `.nullish()` フィールドはいずれも `types.ts` 側が `?` 付きの任意キーとペアになっており、\n * 「`.nullish()` ↔ 任意キー」「`.nullable()` ↔ 必須 `| null`」の対応で揃っている。\n * `| undefined` を混ぜないことは、詳細型を消費する DuckDB ステージ行の要請でもある。\n */\nexport const dailyReportAttachmentItemSchema = z.object({\n id: z.string(),\n fileName: z.string(),\n fileType: z.string().nullable(),\n fileSize: z.number().nullable(),\n createdAt: z.string().nullable(),\n state: z.enum([\"present\", \"absent\", \"unknown\"]),\n})\n\nexport const dailyReportDetailSchema = z.object({\n reportHubId: z.number(),\n date: z.string().nullish(),\n createdAt: z.string().nullish(),\n author: z.string().nullish(),\n userId: z.string().nullish(),\n sourceType: z.string().nullish(),\n employeeName: z.string().nullish(),\n updatedBy: z.string().nullish(),\n updatedAt: z.string().nullish(),\n category: z.string().nullish(),\n creationCategory: z.string().nullish(),\n visitTimeFrom: z.string().nullish(),\n visitTimeTo: z.string().nullish(),\n customerName: z.string().nullish(),\n interviewers: z.array(dailyReportInterviewerSchema),\n subject: z.string().nullish(),\n content: z.string().nullish(),\n comments: z.array(dailyReportCommentSchema),\n isRead: z.boolean(),\n isStarred: z.boolean(),\n labels: z.array(dailyReportLabelDefSchema),\n commentItems: z.array(dailyReportCommentItemSchema),\n // ❗ `.default([])` は必須。SSE の実体は永続 Redis Stream (MAXLEN 保持) であり、\n // 配備をまたいで**旧形式のエントリが残る**。共有リーダーはプロセス起動時に先頭から\n // 再生し、再接続クライアントの catch-up も同じエントリを読み直すため、\n // このキーを必須にすると添付機能を入れる前に publish された全エントリが\n // fail-closed で捨てられ、再接続タブへ更新が永久に届かなくなる。\n // 送信側の詰め忘れ検知は types.ts の必須フィールドと `_AssertDetailCompat`\n // (出力型は既定値適用後なので必須のまま) が担っており、ここを緩めても失われない。\n attachments: z.array(dailyReportAttachmentItemSchema).default([]),\n})\n\n// --- SSE メッセージスキーマ(8種) ---\n\nexport const connectedMessageSchema = z.object({\n type: z.literal(\"connected\"),\n})\n\nexport const statusUpdateMessageSchema = z.object({\n type: z.literal(\"status-update\"),\n reportHubId: z.number(),\n statusType: z.enum([\"star\", \"read\"]),\n value: z.boolean(),\n clientTempId: z.string(),\n // recipientRawUserId はサーバーサイドフィルタリング専用。\n // SSE ルートが raw JSON から取得してフィルタ後に除去する。\n // クライアントには到達しないが、サーバー側の publish で .parse() を通すため定義が必要。\n recipientRawUserId: z.number().optional(),\n})\n\nexport const commentAddMessageSchema = z.object({\n type: z.literal(\"comment-add\"),\n reportHubId: z.number(),\n comment: dailyReportCommentItemSchema,\n clientTempId: z.string(),\n // 下書き日報のコメントは所有者以外に見えてはならない (本文の全員配信を防ぐ)。\n // サーバーサイドフィルタ専用で、SSE ルートがフィルタ後に除去する\n recipientRawUserId: z.number().optional(),\n})\n\nexport const commentDeleteMessageSchema = z.object({\n type: z.literal(\"comment-delete\"),\n reportHubId: z.number(),\n commentId: z.number(),\n clientTempId: z.string(),\n // comment-add と同じ理由の宛先フィルタ\n recipientRawUserId: z.number().optional(),\n})\n\nexport const reportCreateMessageSchema = z.object({\n type: z.literal(\"report-create\"),\n reportHubId: z.number(),\n report: dailyReportDetailSchema,\n clientTempId: z.string(),\n recipientRawUserId: z.number().optional(),\n})\n\nexport const reportUpdateMessageSchema = z.object({\n type: z.literal(\"report-update\"),\n reportHubId: z.number(),\n report: dailyReportDetailSchema,\n clientTempId: z.string(),\n recipientRawUserId: z.number().optional(),\n})\n\nexport const reportPublishMessageSchema = z.object({\n type: z.literal(\"report-publish\"),\n reportHubId: z.number(),\n report: dailyReportDetailSchema,\n clientTempId: z.string(),\n recipientRawUserId: z.number().optional(),\n})\n\nexport const reportDeleteMessageSchema = z.object({\n type: z.literal(\"report-delete\"),\n reportHubId: z.number(),\n clientTempId: z.string(),\n})\n\n// --- Discriminated Union ---\n\nexport const dailyReportSseMessageSchema = z.discriminatedUnion(\"type\", [\n connectedMessageSchema,\n statusUpdateMessageSchema,\n commentAddMessageSchema,\n commentDeleteMessageSchema,\n reportCreateMessageSchema,\n reportUpdateMessageSchema,\n reportPublishMessageSchema,\n reportDeleteMessageSchema,\n])\n\n// --- 型エクスポート ---\n\nexport type DailyReportSseMessage = z.infer<typeof dailyReportSseMessageSchema>\n\n// --- コンパイル時の型互換チェック ---\n// Zod推論型が既存の型定義に代入可能であることを保証する。\n// ここでエラーが出たらスキーマと types.ts がずれている。\n\ntype _AssertDetailCompat = z.infer<typeof dailyReportDetailSchema> extends DeclaredDetail ? true : never\ntype _AssertCommentCompat = z.infer<typeof dailyReportCommentItemSchema> extends DeclaredComment ? true : never\nconst _detailCheck: _AssertDetailCompat = true\nconst _commentCheck: _AssertCommentCompat = true\nvoid _detailCheck\nvoid _commentCheck\n","/**\n * Module-resident session and React hook for the daily-report ids stream.\n * 日報 ID ストリームのモジュール常駐セッションと React フック。\n *\n * セッション (クライアント実体 + 完走時刻) をモジュールスコープに置くことで、\n * ページのアンマウント/再マウントをまたいで受信済みデータが生き残り、\n * 再訪時はローディング画面なしで即座に一覧を復元できる。鮮度は\n * 「完走から一定時間経過後の再訪でバックグラウンド再検証」(SWR) で担保する。\n * マウント中のリアルタイム性は SSE が担うため、これで欠落窓は実質「離脱中」のみになる。\n */\nimport { useSyncExternalStore } from \"react\"\nimport type { DailyReportItem } from \"../../shared/types\"\nimport { defaultDailyReportClientConfig } from \"../config-context\"\nimport { clearDailyReportCache } from \"../hooks/use-daily-report\"\nimport { DailyReportIdsStreamClient, type DailyReportIdsStreamState } from \"./daily-report-ids-stream-client\"\n\n/** 完走からこの時間を過ぎた再訪で、バックグラウンド再検証を走らせる。 */\nconst REVALIDATE_AFTER_MS = 60_000\n\n/** セッション 1 世代分の記録。 */\ntype SessionRecord = {\n client: DailyReportIdsStreamClient\n /** セッションの持ち主 (難読化ユーザー ID。未登録ユーザーは null)。 */\n userKey: string | null\n /** 正規化済み API ベースパス (既定値解決済み)。 */\n apiBasePath: string\n unsubscribe: () => void\n}\n\nlet session: SessionRecord | null = null\n\n/** セッションの生成/破棄とクライアント publish の両方を伝えるリスナー群。 */\nconst sessionListeners = new Set<() => void>()\n\n/** セッション未生成時に返す不変スナップショット (SSR の getServerSnapshot も兼ねる)。 */\nconst IDLE_STATE: DailyReportIdsStreamState = {\n items: [],\n loadedCount: 0,\n totalCount: null,\n phase: \"idle\",\n isRevalidating: false,\n hasCompletedOnce: false,\n completedAt: null,\n error: null,\n}\n\n/**\n * Full-document reload indirection (mockable seam for auth-expiry tests).\n * ドキュメント全体を再読込する間接層 (認証切れテストで差し替え可能にする継ぎ目)。\n */\nexport const reloadDocument = (): void => {\n if (typeof window !== \"undefined\") {\n window.location.reload()\n }\n}\n\n/** 全セッションリスナーへ変更を通知する。 */\nconst notifySessionListeners = (): void => {\n for (const listener of sessionListeners) {\n listener()\n }\n}\n\n/** 現行セッションを破棄する (ユーザー切替・HMR・テストリセット用)。 */\nconst destroySession = (): void => {\n if (!session) return\n session.unsubscribe()\n session.client.dispose()\n session = null\n notifySessionListeners()\n}\n\n/**\n * Ensures a stream session for the given user, creating/refreshing as needed.\n * 指定ユーザーのストリームセッションを確立する処理 (必要に応じ生成/再検証)。\n *\n * - ユーザーまたは API パスが変わっていれば作り直す (別ユーザーのデータを見せない)。\n * - failed なら自動 retry (再訪 = ユーザーの再試行意思とみなす)。\n * - 完走から {@link REVALIDATE_AFTER_MS} 経過していればバックグラウンド再検証。\n *\n * SSR では何もしない。clientLoader とページマウントの両方から呼ばれ、冪等。\n *\n * @param options.apiBasePath API ベースパス (config の apiBasePath)。\n * @param options.userKey 難読化ユーザー ID (未登録ユーザーは null)。\n */\nexport const ensureDailyReportIdsStreamSession = (options: { apiBasePath?: string; userKey: string | null }): void => {\n if (typeof window === \"undefined\") return\n\n // 呼び出し元によって undefined / 既定文字列が混在するため、比較前に必ず正規化する。\n // 正規化を欠くと clientLoader (undefined) とページ effect (既定文字列) の往復で\n // セッションが毎回破棄され、モジュール常駐による即時復元が丸ごと無効化される\n const apiBasePath = options.apiBasePath ?? defaultDailyReportClientConfig.apiBasePath\n\n if (session && (session.userKey !== options.userKey || session.apiBasePath !== apiBasePath)) {\n // ユーザーが変わる場合はモジュール常駐の詳細キャッシュも破棄する\n // (旧ユーザーが閲覧した下書き詳細等が TTL 内の新ユーザーへ供給されるのを防ぐ)\n if (session.userKey !== options.userKey) {\n clearDailyReportCache()\n }\n destroySession()\n }\n\n if (!session) {\n const client = new DailyReportIdsStreamClient({ apiBasePath })\n session = {\n client,\n userKey: options.userKey,\n apiBasePath,\n // 通知はそのままセッションリスナーへ中継する (完走時刻はクライアントが state に刻む)\n unsubscribe: client.subscribe(notifySessionListeners),\n }\n client.start()\n notifySessionListeners()\n return\n }\n\n runFreshnessCheck(session)\n}\n\n/**\n * Retries a failed session or starts a background revalidation when stale.\n * failed セッションの retry、または鮮度切れ時のバックグラウンド再検証を起動する処理。\n *\n * 再検証が既に走行中 (`isRevalidating`) の間は何もしない。ガードを欠くと、1 回の\n * 再訪で走る複数の確立呼び出し (clientLoader の prime / ensure / ページ effect) が\n * それぞれ走行中の再検証を中断して先頭からやり直し、NDJSON を多重ダウンロードする。\n */\nconst runFreshnessCheck = (record: SessionRecord): void => {\n const state = record.client.getState()\n if (state.phase === \"failed\") {\n record.client.retry()\n return\n }\n if (state.phase === \"complete\" && !state.isRevalidating && state.completedAt !== null && Date.now() - state.completedAt > REVALIDATE_AFTER_MS) {\n record.client.refresh()\n }\n}\n\n/**\n * Bootstraps the session from the clientLoader: create when absent, else reconcile.\n * clientLoader からのセッション起動処理。不在なら生成、既存なら照合と鮮度確認。\n *\n * コールドロードの最重要ゲート対策: 生成をページマウントの effect まで遅らせると、\n * ストリームの最初のバイト要求が「serverLoader 完了 → ハイドレーション → effect flush」\n * の後ろに直列化される。ローダー時点で生成すればハイドレーションと並行してフェッチが走る。\n *\n * パス契約: 生成時のパスは factory オプション (未指定は既定値へ正規化)。\n * `config.apiBasePath` だけを上書きした利用者では初回のみ誤パスで生成されるが、\n * ページマウント側の ensure がパス不一致を検知して作り直す (自己修復・初回 1 回きり)。\n * **既存セッションのパスはここでは照合しない** — 照合すると、factory 未指定 ×\n * config 上書きの組み合わせでナビゲーションごとにセッションが破棄されるスラッシングになる。\n *\n * @param options.apiBasePath 生成時に使う API ベースパス (未指定は既定値)。\n * @param options.userKey 難読化ユーザー ID (未登録ユーザーは null)。\n */\nexport const bootstrapDailyReportIdsStreamSession = (options: { apiBasePath?: string; userKey: string | null }): void => {\n if (typeof window === \"undefined\") return\n if (!session) {\n ensureDailyReportIdsStreamSession(options)\n return\n }\n if (session.userKey !== options.userKey) {\n // 別ユーザーのデータは描画前に破棄し (詳細キャッシュ含む)、新ユーザーのストリームを即時開始する\n clearDailyReportCache()\n destroySession()\n ensureDailyReportIdsStreamSession(options)\n return\n }\n runFreshnessCheck(session)\n}\n\n/**\n * Kicks a freshness check for an existing session before the user is known.\n * ユーザー確定前に既存セッションの鮮度確認だけを先行させる処理。\n *\n * clientLoader が serverLoader を await する前に呼び、再訪 (支配的経路) の\n * 再検証開始を数十〜数百 ms 前倒しする。セッションが無ければ何もしない\n * (誰のデータを読むべきか分からないため)。直後の reconcile が userKey を照合し、\n * ユーザーが変わっていればセッションごと破棄されるので先行は安全。\n * 走行中の再検証は中断しない (runFreshnessCheck の isRevalidating ガード)。\n */\nexport const primeDailyReportIdsStreamSession = (): void => {\n if (typeof window === \"undefined\" || !session) return\n runFreshnessCheck(session)\n}\n\n/**\n * Relays an externally-known item upsert into the resident stream data.\n * 外部由来のアイテム upsert を常駐ストリームデータへ中継する処理。\n *\n * プロバイダーの作成/公開系ミューテーション (ローカル操作と SSE) から呼ばれる。\n * これを欠くと、鮮度窓内の再訪で「作成したばかりの日報が消える」。\n */\nexport const upsertDailyReportIdsStreamItem = (item: DailyReportItem): void => {\n session?.client.applyExternalUpsert(item)\n}\n\n/**\n * Relays an externally-known item removal into the resident stream data.\n * 外部由来のアイテム削除を常駐ストリームデータへ中継する処理。\n *\n * report-delete SSE・ローカル削除・stale 安全弁から呼ばれる。\n * これを欠くと、鮮度窓内の再訪で「削除したはずの日報が蘇る」。\n */\nexport const removeDailyReportIdsStreamItem = (reportHubId: number): void => {\n session?.client.applyExternalRemoval(reportHubId)\n}\n\n/**\n * Restarts a failed stream (UI retry button entry point).\n * failed ストリームを再開する処理 (UI の再試行ボタン入口)。\n */\nexport const retryDailyReportIdsStream = (): void => {\n session?.client.retry()\n}\n\n/**\n * Re-validates the stream, optionally bypassing the server-side cache.\n * ストリームを再検証する処理 (サーバーキャッシュのバイパス指定可)。\n *\n * 開発向けリロード/キャッシュクリアボタンから呼ばれる。\n *\n * @param options.forceRefresh サーバー側 SqlResultCache をバイパスするか。\n */\nexport const refreshDailyReportIdsStream = (options: { forceRefresh?: boolean } = {}): void => {\n session?.client.refresh(options)\n}\n\n/**\n * Returns the current session's owner key (undefined when no session exists).\n * 現行セッションの持ち主 (userKey) を返す処理 (セッション不在時は undefined)。\n *\n * 遅延解決するミューテーション継続の門番に使う。POST /action の待機中にリロード無しの\n * ユーザー切替が起きると、切替時の三重遮断 (セッション破棄 + 詳細キャッシュ破棄 +\n * プロバイダーリマウント) の**後**に旧ユーザーの継続が解決し、モジュール常駐状態\n * (新ユーザーのセッション・詳細キャッシュ) へ旧ユーザーのデータを注入してしまう。\n * 継続側は開始時点の userKey とこの値を比較し、不一致 (undefined 含む) なら\n * モジュール常駐状態への書き込みを放棄する (fail-closed)。\n */\nexport const getDailyReportIdsStreamSessionUserKey = (): string | null | undefined => session?.userKey\n\n/** セッション購読 (useSyncExternalStore 用)。 */\nconst subscribeSession = (listener: () => void): (() => void) => {\n sessionListeners.add(listener)\n return () => sessionListeners.delete(listener)\n}\n\n/** 現在のスナップショットを返す (セッション未生成時は不変の IDLE)。 */\nconst getSessionSnapshot = (): DailyReportIdsStreamState => session?.client.getState() ?? IDLE_STATE\n\n/** SSR スナップショット (常に IDLE)。 */\nconst getServerSnapshot = (): DailyReportIdsStreamState => IDLE_STATE\n\n/**\n * React binding for the module-resident ids stream session.\n * モジュール常駐 ids ストリームセッションの React バインディング。\n *\n * セッションが後から生成されても再購読なしで追従する (リスナーはセッションではなく\n * モジュールに属する)。スナップショット参照は publish 単位で安定している。\n *\n * @returns Current stream state. 現在のストリーム状態。\n */\nexport const useDailyReportIdsStream = (): DailyReportIdsStreamState => {\n return useSyncExternalStore(subscribeSession, getSessionSnapshot, getServerSnapshot)\n}\n\n/**\n * Destroys the session (test isolation helper).\n * セッションを破棄するテスト用ヘルパー。\n */\nexport const resetDailyReportIdsStreamSessionForTesting = (): void => {\n destroySession()\n}\n\n// HMR: モジュール差し替え時に旧セッションの接続とリスナーを確実に始末する\nif (typeof import.meta !== \"undefined\" && (import.meta as { hot?: { dispose: (cb: () => void) => void } }).hot) {\n ;(import.meta as unknown as { hot: { dispose: (cb: () => void) => void } }).hot.dispose(() => {\n destroySession()\n })\n}\n","/**\n * Resilient NDJSON stream client for the daily-report ids feed (React-free).\n * 日報 ID 一覧 NDJSON フィードの耐障害ストリームクライアント (React 非依存)。\n *\n * fetch + ReadableStream で `{apiBasePath}/ids-stream` を読み、チャンク到着ごとに\n * 蓄積スナップショットを publish する。切断・サーバーエラー行・未完終端は\n * カーソル再開付きの指数バックオフで自己回復し、受信済みデータは常に保持する。\n * React へは `subscribe` / `getState` (安定参照スナップショット) だけを公開し、\n * `useSyncExternalStore` でそのまま束縛できる形にする。\n */\nimport { type DailyReportIdsStreamCursor, type DailyReportIdsStreamLine, isIdsStreamChunkLine } from \"../../shared/ids-stream\"\nimport type { DailyReportItem } from \"../../shared/types\"\n\n/** ストリームのライフサイクル局面。 */\nexport type DailyReportIdsStreamPhase = \"idle\" | \"streaming\" | \"retrying\" | \"complete\" | \"failed\" | \"auth-required\"\n\n/** publish されるスナップショット。publish 間で参照が安定している。 */\nexport type DailyReportIdsStreamState = {\n /** 受信済みアイテム (reportHubId で重複排除済み)。 */\n items: DailyReportItem[]\n /** 一意アイテム件数 (`items.length` と同値。表示用の別名)。 */\n loadedCount: number\n /** サーバー申告の全体件数。先頭行未着なら null。 */\n totalCount: number | null\n /** ライフサイクル局面。 */\n phase: DailyReportIdsStreamPhase\n /** complete 後のバックグラウンド再検証が走っているか。 */\n isRevalidating: boolean\n /** 一度でも完走したか (再検証中の UI 判定に使う)。 */\n hasCompletedOnce: boolean\n /**\n * 直近の「実際の完走」時刻 (未完走は null)。鮮度判定 (SWR 窓) の唯一の根拠。\n * 完走の publish でのみ更新される — 再検証の諦めなど phase が complete のままの\n * publish で更新すると、古いデータが新鮮扱いされ再検証が永久に走らなくなる。\n */\n completedAt: number | null\n /** 直近の失敗 (成功で null に戻る)。 */\n error: Error | null\n}\n\n/** 1 接続試行の結末。 */\ntype StreamAttemptOutcome = { kind: \"complete\" } | { kind: \"aborted\" } | { kind: \"auth-required\" } | { kind: \"retryable\"; error: Error }\n\n/** テスト注入用の依存一式。 */\nexport type DailyReportIdsStreamClientOptions = {\n /** API ベースパス (既定 `/daily_report/api`)。 */\n apiBasePath?: string\n /** fetch 実装 (既定 globalThis.fetch)。 */\n fetchFn?: (input: string, init: { signal: AbortSignal; headers: Record<string, string>; cache: RequestCache }) => Promise<Response>\n /** タイマー実装 (既定 setTimeout / clearTimeout)。 */\n setTimeoutFn?: (handler: () => void, ms: number) => ReturnType<typeof setTimeout>\n clearTimeoutFn?: (id: ReturnType<typeof setTimeout>) => void\n /**\n * 1 回の read が無音でいられる上限 (ms)。0 以下で無効。\n * これを超えたら接続を half-open とみなして中断し、retryable として\n * カーソル再開へ倒す。サーバーのハートビート間隔より必ず長く取る\n * (生きているが遅いだけの全量クエリ待ちを誤って切らないため)。\n */\n readStallTimeoutMs?: number\n /** 警告ロガー (既定 console.warn)。 */\n warn?: (...args: unknown[]) => void\n /**\n * チャンク由来 publish の合体窓 (ms)。0 で無効 (毎チャンク即時 publish)。\n * 高速回線ではネットワーク read が密集し、publish ごとに全件ソート + 全行再レンダーが\n * 走るため、先頭は即時 (初回描画優先)・後続は窓内で合体するスロットルを掛ける。\n * 完走 publish と外部ミューテーション (ユーザー操作の即時フィードバック) は対象外。\n */\n publishCoalesceMs?: number\n}\n\n/** チャンク由来 publish の既定合体窓 (ms)。 */\nconst DEFAULT_PUBLISH_COALESCE_MS = 50\n\n/** 初回リトライ遅延。 */\nconst INITIAL_RETRY_DELAY_MS = 1_000\n\n/** リトライ遅延の上限。 */\nconst MAX_RETRY_DELAY_MS = 30_000\n\n/** 連続失敗の上限。到達で failed へ落ち、手動 retry 待ちになる。 */\nconst MAX_CONSECUTIVE_FAILURES = 6\n\n/**\n * 1 回の read が無音でいられる既定の上限 (ms)。\n * サーバーのハートビート間隔 (15s) の 2 倍を取り、生きた接続を誤って切らない。\n * 超過した read は half-open 接続とみなし retryable に倒す。\n */\nconst DEFAULT_READ_STALL_TIMEOUT_MS = 30_000\n\n/** read が stall ウォッチドッグに捕まったことを表す番兵。 */\nconst READ_STALL = Symbol(\"read-stall\")\n\n/** 既定の API ベースパス (config-context の既定と一致させる)。 */\nconst DEFAULT_API_BASE_PATH = \"/daily_report/api\"\n\n/** 再開カーソルとして採用する営業日の唯一の形式 (サーバー側の検証契約と一致)。 */\nconst CURSOR_DATE_PATTERN = /^\\d{4}-\\d{2}-\\d{2}$/\n\n/**\n * NDJSON ids stream client with dedupe, cursor resume, and exponential backoff.\n * 重複排除・カーソル再開・指数バックオフを備えた NDJSON ids ストリームクライアント。\n */\nexport class DailyReportIdsStreamClient {\n private readonly apiBasePath: string\n private readonly fetchFn: NonNullable<DailyReportIdsStreamClientOptions[\"fetchFn\"]>\n private readonly setTimeoutFn: NonNullable<DailyReportIdsStreamClientOptions[\"setTimeoutFn\"]>\n private readonly clearTimeoutFn: NonNullable<DailyReportIdsStreamClientOptions[\"clearTimeoutFn\"]>\n private readonly warn: NonNullable<DailyReportIdsStreamClientOptions[\"warn\"]>\n /** 1 回の read が無音でいられる上限 (ms)。0 以下で無効。 */\n private readonly readStallTimeoutMs: number\n\n /** reportHubId → アイテム。挿入順を保ち、再開の重複到着を吸収する。 */\n private readonly itemMap = new Map<number, DailyReportItem>()\n private readonly listeners = new Set<() => void>()\n\n private state: DailyReportIdsStreamState = {\n items: [],\n loadedCount: 0,\n totalCount: null,\n phase: \"idle\",\n isRevalidating: false,\n hasCompletedOnce: false,\n completedAt: null,\n error: null,\n }\n\n private lastCursor: DailyReportIdsStreamCursor | null = null\n private consecutiveFailures = 0\n private started = false\n private disposed = false\n /** 実行中ループの世代。refresh/dispose で進め、旧ループの続行を無効化する。 */\n private loopGeneration = 0\n private activeAbortController: AbortController | null = null\n /**\n * 先頭 (cursor 無し) から始まった走査で観測した ID の集合。\n * 完走時にこの集合へ含まれない Map エントリを剪定し、サーバー側で削除された\n * 日報がモジュール常駐データに永久残留するのを防ぐ。途中失敗のカーソル再開は\n * 同じ走査の続きなので集合を引き継ぐ (全区間の和 = サーバーの全量)。\n */\n private activeSeenIds: Set<number> | null = null\n /**\n * 外部削除のトゥームストーン。サーバーは配列全体を先に確定させてから送出するため、\n * 走査中に届いた削除の後、同じ削除前スナップショットの後続チャンクが同 ID を\n * 再配送してくる。これを吸収せずに素通しすると、削除済み日報が Map へ戻り\n * 完走剪定も免れてしまう (観測集合に再記録されるため)。新しい走査の開始\n * (削除コミット後のスナップショット) でクリアする。\n */\n private readonly externalRemovalTombstones = new Set<number>()\n /**\n * 走行中の再検証中に届いた明示的キャッシュバイパス要求の持ち越しフラグ。\n * 走行中の再検証は中断しない方針のため、要求を黙って捨てず完走後に連結実行する。\n */\n private pendingForceRefresh = false\n private wakeRetryWait: (() => void) | null = null\n private readonly handleOnline = () => this.wakeRetryWait?.()\n\n /** チャンク由来 publish の合体窓 (ms)。 */\n private readonly publishCoalesceMs: number\n /** 合体窓のトレーリングタイマー。 */\n private publishCoalesceTimer: ReturnType<typeof setTimeout> | null = null\n /** 直近のアイテム publish 時刻 (leading edge 判定用)。 */\n private lastItemsPublishAt = 0\n\n constructor(options: DailyReportIdsStreamClientOptions = {}) {\n this.apiBasePath = options.apiBasePath ?? DEFAULT_API_BASE_PATH\n this.fetchFn = options.fetchFn ?? ((input, init) => fetch(input, init))\n this.setTimeoutFn = options.setTimeoutFn ?? ((handler, ms) => setTimeout(handler, ms))\n this.clearTimeoutFn = options.clearTimeoutFn ?? ((id) => clearTimeout(id))\n this.warn = options.warn ?? ((...args) => console.warn(...args))\n this.readStallTimeoutMs = options.readStallTimeoutMs ?? DEFAULT_READ_STALL_TIMEOUT_MS\n this.publishCoalesceMs = options.publishCoalesceMs ?? DEFAULT_PUBLISH_COALESCE_MS\n if (typeof window !== \"undefined\") {\n // オフライン復帰でバックオフ待機を即時解除する\n window.addEventListener(\"online\", this.handleOnline)\n }\n }\n\n /**\n * Returns the current snapshot (stable reference between publishes).\n * 現在のスナップショットを返す処理 (publish 間で参照が安定)。\n */\n getState(): DailyReportIdsStreamState {\n return this.state\n }\n\n /**\n * Subscribes to snapshot updates.\n * スナップショット更新の購読を登録する処理。\n *\n * @param listener Change callback. 変更通知コールバック。\n * @returns Unsubscribe function. 購読解除関数。\n */\n subscribe(listener: () => void): () => void {\n this.listeners.add(listener)\n return () => this.listeners.delete(listener)\n }\n\n /**\n * Starts the initial stream (idempotent).\n * 初回ストリーミングを開始する処理 (冪等)。\n */\n start(): void {\n if (this.started || this.disposed) return\n this.started = true\n void this.runStreamLoop({ revalidate: false, forceRefresh: false })\n }\n\n /**\n * Restarts after a terminal failure (no-op unless phase is \"failed\").\n * failed からの手動再開処理 (failed 以外では何もしない)。\n *\n * 受信済みデータと再開カーソルは保持したまま続きから読む。\n */\n retry(): void {\n if (this.disposed || this.state.phase !== \"failed\") return\n this.consecutiveFailures = 0\n void this.runStreamLoop({ revalidate: this.state.hasCompletedOnce, forceRefresh: false })\n }\n\n /**\n * Revalidates a completed stream in the background (SWR-style).\n * 完走済みストリームをバックグラウンドで再検証する処理 (SWR)。\n *\n * カーソルを捨てて先頭から読み直し、Map の重複排除で差分を上書き吸収し、\n * 完走時に「今回の走査で観測されなかった行」を剪定する (サーバー側削除の反映)。\n * failed のときはカーソルを保持した再開に切り替える (forceRefresh 指定は失わない)。\n * streaming/retrying 中は何もしない。\n *\n * @param options.forceRefresh サーバー側 SqlResultCache をバイパスするか。\n */\n refresh(options: { forceRefresh?: boolean } = {}): void {\n if (this.disposed) return\n if (this.state.phase === \"failed\") {\n // retry() 相当だが forceRefresh の指定を落とさない (キャッシュクリア意図の保存)\n this.consecutiveFailures = 0\n void this.runStreamLoop({ revalidate: this.state.hasCompletedOnce, forceRefresh: options.forceRefresh ?? false })\n return\n }\n if (this.state.phase !== \"complete\") return\n // 走行中の再検証を別の refresh が中断して先頭からやり直すと、ensure の多重呼び出し\n // (clientLoader の prime / ensure / ページ effect) だけで再訪ごとに 3 連ダウンロードになる\n if (this.state.isRevalidating) {\n // ただし明示的なキャッシュバイパス要求 (キャッシュクリアボタン) は黙って捨てず、\n // 走行中の再検証の完走後に連結実行する\n if (options.forceRefresh) {\n this.pendingForceRefresh = true\n }\n return\n }\n this.lastCursor = null\n this.consecutiveFailures = 0\n void this.runStreamLoop({ revalidate: true, forceRefresh: options.forceRefresh ?? false })\n }\n\n /**\n * Applies an externally-known upsert (SSE / local mutation) to the resident data.\n * 外部由来 (SSE / ローカル操作) の upsert を常駐データへ反映する処理。\n *\n * これを欠くと、マウント中に作成された日報がストリームセッションに存在せず、\n * 鮮度窓内の再訪 (再検証なし) で一覧から消える。アクティブな走査中は観測集合にも\n * 刻み、完走時の剪定で「生きていると分かっている行」を誤って落とさないようにする。\n */\n applyExternalUpsert(item: DailyReportItem): void {\n if (this.disposed || !item || typeof item.reportHubId !== \"number\") return\n // 明示的な upsert は「生きている」の権威シグナルなので削除トゥームストーンを解除する\n this.externalRemovalTombstones.delete(item.reportHubId)\n this.activeSeenIds?.add(item.reportHubId)\n const existing = this.itemMap.get(item.reportHubId)\n // sourceType 未指定の upsert (プロバイダーは知らない) は既存値を保持する\n const next: DailyReportItem = { reportHubId: item.reportHubId, businessDate: item.businessDate, sourceType: item.sourceType ?? existing?.sourceType }\n if (!existing || existing.businessDate !== next.businessDate || existing.sourceType !== next.sourceType) {\n this.itemMap.set(item.reportHubId, next)\n this.publishItems()\n }\n }\n\n /**\n * Applies an externally-known removal (SSE report-delete / stale valve) to the resident data.\n * 外部由来 (SSE report-delete / stale 安全弁) の削除を常駐データへ反映する処理。\n *\n * 走査中の後続チャンク (削除前スナップショット) による同一行の再配送は、\n * クライアント側トゥームストーンが次の新しい走査までブロックする。\n */\n applyExternalRemoval(reportHubId: number): void {\n if (this.disposed) return\n this.activeSeenIds?.delete(reportHubId)\n // 削除前スナップショットの後続チャンクによる再配送を、次の新しい走査までブロックする\n this.externalRemovalTombstones.add(reportHubId)\n if (this.itemMap.delete(reportHubId)) {\n this.publishItems()\n }\n }\n\n /**\n * Aborts everything and detaches listeners (terminal).\n * すべてを中断しリスナーを外す終了処理 (以後再利用不可)。\n */\n dispose(): void {\n this.disposed = true\n this.loopGeneration += 1\n this.activeAbortController?.abort()\n this.cancelScheduledPublishItems()\n // 保留中のバックオフ待機を起こす (finish が自分のタイマーを片付ける)\n this.wakeRetryWait?.()\n if (typeof window !== \"undefined\") {\n window.removeEventListener(\"online\", this.handleOnline)\n }\n this.listeners.clear()\n }\n\n /**\n * Connection-retry loop: stream → classify → backoff → resume.\n * 接続リトライループ。ストリーム → 結果分類 → バックオフ → カーソル再開。\n */\n private async runStreamLoop(mode: { revalidate: boolean; forceRefresh: boolean }): Promise<void> {\n const generation = ++this.loopGeneration\n // 進行中の旧接続 (再検証開始時など) を中断し、バックオフ待機中の旧ループも\n // 即時に起こして退場させる (放置すると旧タイマーが残り続ける)\n this.activeAbortController?.abort()\n this.wakeRetryWait?.()\n\n // 先頭からの走査 (初回/再検証) は削除剪定用の観測集合を新調し、外部削除の\n // トゥームストーンをクリアする (これから受けるスナップショットは削除コミット後)。\n // カーソル再開 (途中失敗からの retry) は同じ走査の続きなので両方引き継ぐ\n if (this.lastCursor === null) {\n this.activeSeenIds = new Set()\n this.externalRemovalTombstones.clear()\n }\n\n // このループ自体が forced run なら、持ち越し要求はここで満たされたことにする\n // (満たされないまま残ると、無関係な後続の再検証に古い強制走査が連鎖する)\n if (mode.forceRefresh) {\n this.pendingForceRefresh = false\n }\n\n this.publish({\n phase: mode.revalidate ? this.state.phase : \"streaming\",\n isRevalidating: mode.revalidate,\n error: null,\n })\n\n while (!this.disposed && generation === this.loopGeneration) {\n // forceRefresh はループ全体で維持する。試行がサーバーへ届く前に失敗した場合、\n // 1 回で降ろすと後続リトライが古いサーバーキャッシュを掴む\n const outcome = await this.streamOnce(mode.forceRefresh)\n if (this.disposed || generation !== this.loopGeneration) return\n\n if (outcome.kind === \"complete\") {\n this.pruneUnseenItems()\n // 完走 = 成功。失敗予算をリセットする (前進ゼロの完走もあり得るため、\n // 進行時リセットだけに頼ると連結実行される次のループへ古い予算が漏れる)\n this.consecutiveFailures = 0\n // 完走 publish が全量を運ぶため、保留中のトレーリング publish は不要\n this.cancelScheduledPublishItems()\n this.lastItemsPublishAt = Date.now()\n this.publish({\n phase: \"complete\",\n isRevalidating: false,\n hasCompletedOnce: true,\n completedAt: Date.now(),\n error: null,\n items: Array.from(this.itemMap.values()),\n loadedCount: this.itemMap.size,\n })\n // 走行中に持ち越された明示的キャッシュバイパス要求を連結実行する\n if (this.pendingForceRefresh && !this.disposed) {\n this.pendingForceRefresh = false\n this.lastCursor = null\n void this.runStreamLoop({ revalidate: true, forceRefresh: true })\n }\n return\n }\n if (outcome.kind === \"aborted\") return\n if (outcome.kind === \"auth-required\") {\n this.publish({ phase: \"auth-required\", isRevalidating: false })\n return\n }\n\n this.consecutiveFailures += 1\n if (this.consecutiveFailures >= MAX_CONSECUTIVE_FAILURES) {\n // 再検証中の力尽きは既存データを守って complete のまま静かに諦める\n // (completedAt は更新しない — 次の再訪で再検証が確実に走るように)\n if (mode.revalidate && this.state.hasCompletedOnce) {\n this.warn(\"[DailyReportIdsStream] revalidation gave up:\", outcome.error.message)\n // 持ち越し要求も破棄する (死んだ再検証の後に古い強制走査を連鎖させない)\n this.pendingForceRefresh = false\n this.publish({ isRevalidating: false, error: outcome.error })\n } else {\n this.publish({ phase: \"failed\", isRevalidating: false, error: outcome.error })\n }\n return\n }\n\n this.publish({ phase: mode.revalidate ? this.state.phase : \"retrying\", error: outcome.error })\n await this.waitBeforeRetry(Math.min(INITIAL_RETRY_DELAY_MS * 2 ** (this.consecutiveFailures - 1), MAX_RETRY_DELAY_MS))\n }\n }\n\n /**\n * Prunes map entries not observed by the completed full scan.\n * 完走した全量走査で観測されなかった Map エントリを剪定する処理。\n *\n * サーバー側で削除された日報の残留 (再訪時の亡霊復活・loadedCount > totalCount) を防ぐ。\n * 観測集合が無い場合 (理論上到達しない防御) は何も消さない。\n */\n private pruneUnseenItems(): void {\n const seen = this.activeSeenIds\n this.activeSeenIds = null\n if (!seen) return\n for (const id of Array.from(this.itemMap.keys())) {\n if (!seen.has(id)) {\n this.itemMap.delete(id)\n }\n }\n }\n\n /**\n * Runs one streaming attempt and classifies its outcome.\n * 1 回のストリーミング試行を実行し、結末を分類する処理。\n */\n private async streamOnce(forceRefresh: boolean): Promise<StreamAttemptOutcome> {\n const abortController = new AbortController()\n this.activeAbortController = abortController\n try {\n // ヘッダー到着前 (再開経路は全量クエリ確定までヘッダーを返さない) にはハートビートが\n // 流れないため、read より緩い上限 (3 倍 = 既定 90s。サーバーの requestTimeout 60s より\n // 長い) で fetch 自体も見張る。ここで切れた場合も retryable としてカーソル再開する\n const fetchPromise = this.fetchFn(this.buildStreamUrl(forceRefresh), {\n signal: abortController.signal,\n headers: { Accept: \"application/x-ndjson\" },\n cache: \"no-store\",\n })\n let headerTimer: ReturnType<typeof setTimeout> | null = null\n const response =\n this.readStallTimeoutMs <= 0\n ? await fetchPromise\n : await Promise.race([\n fetchPromise,\n new Promise<typeof READ_STALL>((resolve) => {\n headerTimer = this.setTimeoutFn(() => resolve(READ_STALL), this.readStallTimeoutMs * 3)\n }),\n ]).finally(() => {\n if (headerTimer !== null) this.clearTimeoutFn(headerTimer)\n })\n if (response === READ_STALL) {\n abortController.abort()\n // 破棄した接続の解決/拒否を未処理のまま残さない\n fetchPromise.catch(() => {})\n return { kind: \"retryable\", error: new Error(\"Stream stalled: no response headers within timeout\") }\n }\n if (response.status === 401 || response.status === 403) {\n return { kind: \"auth-required\" }\n }\n if (!(response.ok && response.body)) {\n return { kind: \"retryable\", error: new Error(`ids stream HTTP ${response.status}`) }\n }\n\n const reader = response.body.getReader()\n const decoder = new TextDecoder()\n let partialLine = \"\"\n let sawErrorLine = false\n let sawIsLast = false\n\n while (true) {\n const chunk = await this.readWithStallGuard(reader)\n if (chunk === READ_STALL) {\n // 無音が続いた half-open 接続。中断してカーソル再開へ倒す\n // (finally が activeAbortController を掃除する)\n abortController.abort()\n void reader.cancel().catch(() => {})\n return { kind: \"retryable\", error: new Error(\"Stream stalled: no data within timeout\") }\n }\n const { done, value } = chunk\n if (done) break\n // ネットワークチャンクは行境界と無関係に割れるため、末尾の欠け行を持ち越す\n const lines = (partialLine + decoder.decode(value, { stream: true })).split(\"\\n\")\n partialLine = lines.pop() ?? \"\"\n\n let mutated = false\n for (const rawLine of lines) {\n if (rawLine.trim() === \"\") continue\n let parsed: DailyReportIdsStreamLine\n try {\n parsed = JSON.parse(rawLine) as DailyReportIdsStreamLine\n } catch {\n // 単一行の破損はスキップし、接続レベルの再開に委ねる\n this.warn(\"[DailyReportIdsStream] skipped malformed line\")\n continue\n }\n if (!isIdsStreamChunkLine(parsed)) {\n sawErrorLine = true\n continue\n }\n mutated = this.absorbChunkItems(parsed.items) || mutated\n if (typeof parsed.total === \"number\" && Number.isFinite(parsed.total) && parsed.total !== this.state.totalCount) {\n // total 単独の変化でも必ず購読者へ通知する (無通知のスナップショット\n // 置換は useSyncExternalStore のスナップショット安定契約を破る)\n this.publish({ totalCount: parsed.total })\n }\n if (parsed.nextCursor && (parsed.nextCursor.businessDate === null || CURSOR_DATE_PATTERN.test(parsed.nextCursor.businessDate))) {\n // 契約外の日付 (混入データ等) を持つカーソルは採用しない。\n // 日付を欠いた id だけの再開は NULL 営業日領域へスキップして欠落を生むため、\n // 直前の正当なカーソル (無ければ先頭) から読み直す方が安全\n this.lastCursor = parsed.nextCursor\n }\n if (parsed.isLast) {\n sawIsLast = true\n }\n }\n if (mutated) {\n // 前進があった時点で失敗カウンタを戻す (断続的な不調で failed へ落とさない)\n this.consecutiveFailures = 0\n this.schedulePublishItems()\n }\n }\n\n if (sawErrorLine) {\n return { kind: \"retryable\", error: new Error(\"Stream interrupted by server error\") }\n }\n if (!sawIsLast) {\n // サーバー再起動や中間切断。カーソルから再開する\n return { kind: \"retryable\", error: new Error(\"Stream ended before completion\") }\n }\n return { kind: \"complete\" }\n } catch (error) {\n // DOMException (ブラウザー/undici) と Error 派生 (テスト DI) の両形を受ける\n if (error instanceof Error && error.name === \"AbortError\") {\n return { kind: \"aborted\" }\n }\n return { kind: \"retryable\", error: error instanceof Error ? error : new Error(String(error)) }\n } finally {\n if (this.activeAbortController === abortController) {\n this.activeAbortController = null\n }\n }\n }\n\n /**\n * Reads one chunk, racing the read against a stall watchdog.\n * 1 チャンクを読む処理。読み取りを stall ウォッチドッグと競争させる。\n *\n * サーバーが無音のまま接続を half-open で放置しても (NAT/プロキシのアイドル切断・\n * VPN 断・スリープ)、`reader.read()` は永久に保留したままになる。これを検知するため\n * read とタイマーを競争させ、無音が上限を超えたら番兵を返す。生きているが遅いだけの\n * 全量クエリ待ちはサーバーのハートビート (空行) が read を解決するので誤検知しない。\n * `readStallTimeoutMs <= 0` のときは素の read (タイマーを張らない・テスト用)。\n *\n * @returns 読み取り結果、または stall を表す番兵。read の reject はそのまま伝播する。\n */\n private async readWithStallGuard(reader: ReadableStreamDefaultReader<Uint8Array>): Promise<ReadableStreamReadResult<Uint8Array> | typeof READ_STALL> {\n if (this.readStallTimeoutMs <= 0) {\n return reader.read()\n }\n let stallTimer: ReturnType<typeof setTimeout> | null = null\n const stallPromise = new Promise<typeof READ_STALL>((resolve) => {\n stallTimer = this.setTimeoutFn(() => resolve(READ_STALL), this.readStallTimeoutMs)\n })\n try {\n return await Promise.race([reader.read(), stallPromise])\n } finally {\n if (stallTimer !== null) {\n this.clearTimeoutFn(stallTimer)\n }\n }\n }\n\n /**\n * Merges chunk items into the dedupe map.\n * チャンクのアイテム群を重複排除 Map へ取り込む処理。\n *\n * @returns True when anything was added or changed. 追加または内容変化があれば true。\n */\n private absorbChunkItems(items: DailyReportItem[]): boolean {\n let mutated = false\n for (const item of items) {\n if (!item || typeof item.reportHubId !== \"number\") continue\n // 走査中に外部削除された ID は、同じ削除前スナップショットの後続チャンクが\n // 再配送してきても受け入れない (受けると剪定も免れて再訪時に蘇る)\n if (this.externalRemovalTombstones.has(item.reportHubId)) continue\n // 内容が既知でも「観測した」事実は剪定用集合へ必ず刻む\n this.activeSeenIds?.add(item.reportHubId)\n const existing = this.itemMap.get(item.reportHubId)\n if (!existing || existing.businessDate !== item.businessDate || existing.sourceType !== item.sourceType) {\n this.itemMap.set(item.reportHubId, item)\n mutated = true\n }\n }\n return mutated\n }\n\n /** アイテム配列を再スナップショットして即時 publish する。 */\n private publishItems(): void {\n this.lastItemsPublishAt = Date.now()\n this.publish({ items: Array.from(this.itemMap.values()), loadedCount: this.itemMap.size })\n }\n\n /**\n * チャンク由来のアイテム publish をスロットルする (leading edge 即時 + trailing 合体)。\n * 最初のチャンクは即時に描画へ届け、窓内に密集した後続チャンクは 1 回の publish\n * (= 1 回の全件ソートと再レンダー) へ合体する。合体窓 0 は無効 (常に即時)。\n */\n private schedulePublishItems(): void {\n if (this.publishCoalesceMs <= 0) {\n this.publishItems()\n return\n }\n const elapsed = Date.now() - this.lastItemsPublishAt\n if (elapsed >= this.publishCoalesceMs) {\n this.publishItems()\n return\n }\n if (this.publishCoalesceTimer !== null) return\n this.publishCoalesceTimer = this.setTimeoutFn(() => {\n this.publishCoalesceTimer = null\n this.publishItems()\n }, this.publishCoalesceMs - elapsed)\n }\n\n /** 保留中のトレーリング publish を破棄する (完走 publish が全量を運ぶ場合など)。 */\n private cancelScheduledPublishItems(): void {\n if (this.publishCoalesceTimer !== null) {\n this.clearTimeoutFn(this.publishCoalesceTimer)\n this.publishCoalesceTimer = null\n }\n }\n\n /** 部分状態をマージした新スナップショットを作り、購読者へ通知する。 */\n private publish(partial: Partial<DailyReportIdsStreamState>): void {\n this.state = { ...this.state, ...partial }\n for (const listener of this.listeners) {\n listener()\n }\n }\n\n /** 再開カーソル付きのストリーム URL を構築する。 */\n private buildStreamUrl(forceRefresh: boolean): string {\n const params = new URLSearchParams()\n if (this.lastCursor) {\n params.set(\"cursor_report_hub_id\", String(this.lastCursor.reportHubId))\n // null 営業日領域の再開は日付パラメータを省略する (サーバー側の契約)\n if (this.lastCursor.businessDate !== null) {\n params.set(\"cursor_business_date\", this.lastCursor.businessDate)\n }\n }\n if (forceRefresh) {\n params.set(\"forceRefresh\", \"true\")\n }\n const query = params.toString()\n return `${this.apiBasePath}/ids-stream${query ? `?${query}` : \"\"}`\n }\n\n /**\n * バックオフ待機。online イベント・dispose・後続ループの開始で早期解除される。\n *\n * タイマー ID は待機ごとのローカルに閉じ、共有スロット (wakeRetryWait) は\n * 「まだ自分を指しているときだけ」外す。単一スロットへ素朴に書くと、\n * 追い越された旧待機の finish が新しいループのタイマーを消してしまい、\n * 再検証が永久に固まる (実測済みの競合)。\n */\n private waitBeforeRetry(delayMs: number): Promise<void> {\n return new Promise((resolve) => {\n let timerId: ReturnType<typeof setTimeout> | null = null\n const finish = () => {\n if (timerId !== null) {\n this.clearTimeoutFn(timerId)\n timerId = null\n }\n if (this.wakeRetryWait === finish) {\n this.wakeRetryWait = null\n }\n resolve()\n }\n this.wakeRetryWait = finish\n timerId = this.setTimeoutFn(finish, delayMs)\n })\n }\n}\n","/**\n * Error-surface seam for daily-report mutations.\n * 日報ミューテーション失敗を利用者へ可視化するための通知シーム。\n *\n * 保存・公開・削除・コメント等の失敗は、これまで握り潰されてボタンが成功したかのように\n * 復帰していた。ここでミューテーションの失敗を一元的に受け取り、`config.onError` が\n * 注入されていればホストへ委譲し、無ければパッケージ内蔵の `role=\"alert\"` バナーで表示する。\n * `notifyError` は Provider 外でも安全に呼べる (既定は no-op) ため、アクションコンテキストは\n * 存在を前提にせず呼び出せる。\n */\nimport { createContext, type ReactNode, useCallback, useContext, useEffect, useMemo, useRef, useState } from \"react\"\nimport { type DailyReportErrorInfo, useDailyReportConfig } from \"../config-context\"\n\n/** バナーの自動消滅までの既定時間 (ms)。手動クローズでも消せる。 */\nconst ERROR_BANNER_AUTO_DISMISS_MS = 8_000\n\ntype DailyReportErrorSurface = {\n /** ミューテーション失敗を通知する。 */\n notifyError: (info: DailyReportErrorInfo) => void\n}\n\n/** 既定は no-op (Provider 外で呼んでも壊れない)。 */\nconst DailyReportErrorContext = createContext<DailyReportErrorSurface>({ notifyError: () => {} })\n\n/**\n * Returns the error-surface API (safe no-op outside the provider).\n * 通知 API を返す処理 (Provider 外では no-op)。\n */\nexport const useDailyReportErrorSurface = (): DailyReportErrorSurface => useContext(DailyReportErrorContext)\n\n/**\n * Provides the mutation error surface and renders the default alert banner.\n * ミューテーション通知シームを供給し、既定のアラートバナーを描画するプロバイダー。\n *\n * `config.onError` があればホストへ委譲し、内蔵バナーは描画しない (二重表示防止)。\n */\nexport const DailyReportErrorProvider = ({ children }: { children: ReactNode }) => {\n const { onError } = useDailyReportConfig()\n const [banner, setBanner] = useState<DailyReportErrorInfo | null>(null)\n // onError を ref 経由で読み、notifyError の参照を安定させる (毎回新関数だと購読側が再レンダー)\n const onErrorRef = useRef(onError)\n onErrorRef.current = onError\n const dismissTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)\n\n const notifyError = useCallback((info: DailyReportErrorInfo) => {\n const host = onErrorRef.current\n if (host) {\n // ホストが通知 UI を持つ場合は委譲し、内蔵バナーは出さない\n host(info)\n return\n }\n setBanner(info)\n }, [])\n\n const dismiss = useCallback(() => {\n if (dismissTimerRef.current !== null) {\n clearTimeout(dismissTimerRef.current)\n dismissTimerRef.current = null\n }\n setBanner(null)\n }, [])\n\n /**\n * バナー表示中は一定時間で自動的に消す副作用。\n * 依存配列: banner (新しい失敗が来るたびにタイマーを張り直す)。\n * クリーンアップ: タイマー解除 (アンマウント/再通知でのリークと二重発火を防ぐ)。\n */\n useEffect(() => {\n if (!banner) return\n dismissTimerRef.current = setTimeout(() => setBanner(null), ERROR_BANNER_AUTO_DISMISS_MS)\n return () => {\n if (dismissTimerRef.current !== null) {\n clearTimeout(dismissTimerRef.current)\n dismissTimerRef.current = null\n }\n }\n }, [banner])\n\n const value = useMemo(() => ({ notifyError }), [notifyError])\n\n return (\n <DailyReportErrorContext.Provider value={value}>\n {children}\n {banner && (\n <div\n // role=\"alert\" で SR へ即時読み上げさせる。fixed トーストで操作を妨げない\n role=\"alert\"\n data-testid=\"daily-report-error-banner\"\n className=\"fade-in slide-in-from-bottom-2 fixed inset-x-0 bottom-4 z-[100] mx-auto flex w-fit max-w-[92vw] animate-in items-center gap-3 rounded-lg border border-red-300 bg-red-600 px-4 py-2.5 text-sm text-white shadow-lg duration-200 dark:border-red-500/60 dark:bg-red-700\">\n <span className=\"font-medium\">{banner.message}</span>\n <button type=\"button\" aria-label=\"閉じる\" onClick={dismiss} className=\"shrink-0 rounded px-1.5 py-0.5 font-bold text-white/90 transition-colors hover:bg-red-700 hover:text-white dark:hover:bg-red-800\">\n ✕\n </button>\n </div>\n )}\n </DailyReportErrorContext.Provider>\n )\n}\n","/**\n * Comment management hook merging server data with optimistic local updates.\n * サーバーデータとローカルの楽観的更新を統合して日報コメントを管理するフック。\n */\nimport { useCallback, useMemo, useRef, useState } from \"react\"\nimport { mergeComments, type UIComment } from \"../../shared/comment-adapter\"\nimport type { DailyReportDetail } from \"../../shared/types\"\nimport { useDailyReportActionContext } from \"../contexts/daily-report-action-context\"\n\ntype User = { displayName?: string; name?: { familyName?: string; givenName?: string }; emails?: { value: string }[] }\n\n/**\n * Manages daily report comments, integrating server data with optimistic local updates.\n * サーバーデータとローカルの楽観的更新を統合して日報コメントを管理するフック。\n */\nexport const useDailyReportComments = (report: DailyReportDetail | null, user: User | null | undefined, userId?: string | null) => {\n const { pendingAddCommentIds, pendingDeleteCommentIds, resolvedIdMap, version, addComment, deleteComment } = useDailyReportActionContext()\n const [pendingDeleteId, setPendingDeleteId] = useState<number | string | null>(null)\n\n const handleDeleteComment = useCallback((cId: number) => report && deleteComment(report.reportHubId, cId, report.date ?? \"\"), [report, deleteComment])\n const handleAddComment = useCallback((content: string) => (report && user ? addComment(report.reportHubId, content, report.date ?? \"\") : Promise.resolve()), [report, user, addComment])\n\n const prevUiCommentsRef = useRef<UIComment[]>([])\n\n // UI用コメントリストの生成: サーバーデータ、レガシーコメント、楽観的更新(追加・削除)をマージして表示用データを構築する。\n // 依存関係: [report, user, userId, pendingDeleteCommentIds, pendingAddCommentIds, resolvedIdMap, version]\n // クリーンアップ: なし\n // biome-ignore lint/correctness/useExhaustiveDependencies: version は ref (pending*) 変更時の再計算トリガーとして意図的に依存させる\n const uiComments = useMemo(() => {\n if (!report) return []\n\n // 1. Base: Merge legacy and new comments\n // 基本データ: レガシーコメントと新しい形式のコメントをマージ。\n let comments = mergeComments(report.comments, report.commentItems, userId)\n\n // 2. Filter: Remove pending deletions\n // フィルタリング: 削除保留中のコメントを除外。\n const deletedIds = pendingDeleteCommentIds.current.get(report.reportHubId)\n if (deletedIds?.size) comments = comments.filter((c) => !deletedIds.has(c.id as number))\n\n // 3. Add: Append pending optimistic additions\n // 追加: 楽観的に追加されたコメントを末尾に追加。\n const addedComments = pendingAddCommentIds.current.get(report.reportHubId)\n if (addedComments?.size && user) {\n const existingIds = new Set(comments.map((c) => c.id))\n const authorName = user.displayName?.trim() || (user.name ? `${user.name.familyName ?? \"\"} ${user.name.givenName ?? \"\"}`.trim() : \"\") || user.emails?.[0]?.value || \"Unknown\"\n\n for (const [tempId, content] of addedComments) {\n if (deletedIds?.has(tempId)) continue\n if (existingIds.has(tempId)) continue\n\n // Skip if real ID resolved and already present\n // 実IDが解決済みで既にリストに存在する場合はスキップ。\n const realId = resolvedIdMap.current.get(tempId)\n if (realId && existingIds.has(realId)) continue\n\n // Skip if content duplicated (server data arrived before ID resolution)\n // 内容が重複している場合(ID解決前にサーバーデータが到達した場合)はスキップ。\n if (comments.some((c) => c.isMine && c.content === content && typeof c.id === \"number\" && c.id > 0)) continue\n\n comments.push({ id: tempId, authorName, content, createdAt: new Date().toISOString(), isMine: true, isLegacy: false })\n }\n }\n\n // Stable reference check\n // 参照の安定性チェック: 内容に変更がなければ前回の参照を返すことで不要な再レンダリングを防ぐ。\n if (JSON.stringify(comments) === JSON.stringify(prevUiCommentsRef.current)) return prevUiCommentsRef.current\n prevUiCommentsRef.current = comments\n return comments\n }, [report, user, userId, pendingDeleteCommentIds, pendingAddCommentIds, resolvedIdMap, version])\n\n return { uiComments, handleAddComment, handleDeleteComment, pendingDeleteId, setPendingDeleteId, resolvedIdMap: resolvedIdMap.current }\n}\n","/**\n * Shared hook for calculating viewport height of virtual scroll containers.\n * 仮想スクロールコンテナのビューポート高さを計測する共通フック。\n */\nimport { type RefObject, useCallback, useEffect, useState } from \"react\"\n\ntype UseDynamicViewportHeightOptions = {\n enableMutationObserver?: boolean\n bottomGap?: number\n minFallbackHeight?: number\n}\n\ntype UseDynamicViewportHeightResult = {\n viewportHeight: number\n updateViewportHeight: () => void\n}\n\n/**\n * Computes and updates the viewport height for scroll containers while reacting to layout changes.\n * レイアウト変化に追従しながらスクロールコンテナのビューポート高さを算出して更新する関数。\n */\nexport const useDynamicViewportHeight = (containerRef: RefObject<HTMLDivElement | null>, options?: UseDynamicViewportHeightOptions): UseDynamicViewportHeightResult => {\n const { enableMutationObserver = false, bottomGap = 4, minFallbackHeight = 240 } = options ?? {}\n const [viewportHeight, setViewportHeight] = useState(600)\n\n const updateViewportHeight = useCallback(() => {\n if (typeof window === \"undefined\") {\n return\n }\n\n const target = containerRef.current\n if (!target) {\n return\n }\n\n // 要素が非表示の場合は計測しない (display: none など)\n if (target.offsetParent === null && window.getComputedStyle(target).position !== \"fixed\") {\n return\n }\n\n const toNumber = (value: string) => Number.parseFloat(value) || 0\n const { top: containerTop } = target.getBoundingClientRect()\n const parentRect = target.parentElement?.getBoundingClientRect()\n const rootStyle = window.getComputedStyle(document.documentElement)\n const footerHeight = toNumber(rootStyle.getPropertyValue(\"--footer-height\"))\n const headerHeight = toNumber(rootStyle.getPropertyValue(\"--header-height\"))\n const scrollbarHeight = toNumber(rootStyle.getPropertyValue(\"--scrollbar-height\"))\n const containerStyle = window.getComputedStyle(target)\n const marginBottom = toNumber(containerStyle.marginBottom)\n const paddingBottom = toNumber(containerStyle.paddingBottom)\n const viewportBottom = window.innerHeight - footerHeight - scrollbarHeight - bottomGap\n const effectiveBottom = parentRect ? Math.min(parentRect.bottom, viewportBottom) : viewportBottom\n const layoutTop = parentRect ? Math.max(parentRect.top, headerHeight) : headerHeight\n const effectiveTop = Math.max(containerTop, layoutTop)\n const availableHeight = effectiveBottom - effectiveTop - marginBottom - paddingBottom\n const safeAvailableHeight = Number.isFinite(availableHeight) ? Math.floor(availableHeight) : 0\n const clampedHeight = safeAvailableHeight > 0 ? safeAvailableHeight : minFallbackHeight\n\n if (clampedHeight > 0 && Number.isFinite(clampedHeight)) {\n setViewportHeight((previous) => (Math.abs(previous - clampedHeight) > 1 ? clampedHeight : previous))\n }\n }, [bottomGap, containerRef, minFallbackHeight])\n\n useEffect(() => {\n if (typeof window === \"undefined\") {\n return\n }\n\n let frameId: number | null = null\n let timeoutId: number | null = null\n let resizeObserver: ResizeObserver | null = null\n let mutationObserver: MutationObserver | null = null\n const scheduleUpdate = () => {\n if (frameId !== null) {\n window.cancelAnimationFrame(frameId)\n }\n frameId = window.requestAnimationFrame(() => {\n updateViewportHeight()\n })\n }\n\n // 初期表示直後とわずかな遅延後に計測して安定させる\n scheduleUpdate()\n timeoutId = window.setTimeout(() => {\n scheduleUpdate()\n }, 150)\n\n const containerElement = containerRef.current\n if (typeof ResizeObserver !== \"undefined\" && containerElement) {\n resizeObserver = new ResizeObserver(() => {\n // コンテナと親要素のリサイズ変化に追従する\n scheduleUpdate()\n })\n resizeObserver.observe(containerElement)\n if (containerElement.parentElement) {\n resizeObserver.observe(containerElement.parentElement)\n }\n }\n\n if (enableMutationObserver && typeof MutationObserver !== \"undefined\") {\n mutationObserver = new MutationObserver(() => {\n // ルートのスタイル変更にも追従する\n scheduleUpdate()\n })\n mutationObserver.observe(document.documentElement, { attributes: true, attributeFilter: [\"style\"] })\n }\n\n window.addEventListener(\"resize\", scheduleUpdate)\n\n return () => {\n if (frameId !== null) {\n window.cancelAnimationFrame(frameId)\n }\n if (timeoutId !== null) {\n window.clearTimeout(timeoutId)\n }\n window.removeEventListener(\"resize\", scheduleUpdate)\n resizeObserver?.disconnect()\n mutationObserver?.disconnect()\n }\n }, [enableMutationObserver, updateViewportHeight, containerRef])\n\n return {\n viewportHeight,\n updateViewportHeight,\n }\n}\n","import { useEffect, useRef, useState } from \"react\"\nimport type { DailyReportDetail } from \"../../shared/types\"\nimport { useDailyReportActionContext } from \"../contexts/daily-report-action-context\"\nimport { Button } from \"../ui/button\"\nimport { Input } from \"../ui/input\"\nimport { Label } from \"../ui/label\"\nimport { Textarea } from \"../ui/textarea\"\n\ntype DailyReportEditFormProps = {\n report: DailyReportDetail\n onCancel?: () => void\n onPublishSuccess?: () => void\n}\n\nexport const DailyReportEditForm = ({ report, onCancel, onPublishSuccess }: DailyReportEditFormProps) => {\n const { updateReport, publishReport } = useDailyReportActionContext()\n const [title, setTitle] = useState(report.subject ?? \"\")\n const [content, setContent] = useState(report.content ?? \"\")\n const [isSaving, setIsSaving] = useState(false)\n\n // 同一レポート編集中に SSE/キャッシュ更新でフォームがリセットされるのを防止するための ID 追跡\n const syncedReportIdRef = useRef(report.reportHubId)\n\n // レポート切り替え時のみフォーム状態を同期する (同一レポートのキャッシュ更新ではリセットしない)\n useEffect(() => {\n if (report.reportHubId !== syncedReportIdRef.current) {\n setTitle(report.subject ?? \"\")\n setContent(report.content ?? \"\")\n syncedReportIdRef.current = report.reportHubId\n }\n }, [report.reportHubId, report.subject, report.content])\n\n const handleSave = async () => {\n setIsSaving(true)\n try {\n await updateReport(report.reportHubId, { title, content }, report.date ?? \"\")\n } catch {\n // 失敗はアクションコンテキストが role=\"alert\" バナー / onError で可視化する。\n // ここで握るのは未処理 Promise 拒否を防ぐためだけ (成功時のみ後続へ進む)\n } finally {\n setIsSaving(false)\n }\n }\n\n const handlePublish = async () => {\n setIsSaving(true)\n try {\n // Ensure latest changes are saved before publishing\n await updateReport(report.reportHubId, { title, content }, report.date ?? \"\")\n await publishReport(report.reportHubId, report.date ?? \"\")\n // 保存・公開の両方が成功したときだけ成功コールバックを呼ぶ\n onPublishSuccess?.()\n } catch {\n // 失敗はアクションコンテキストが可視化する。onPublishSuccess は呼ばない\n } finally {\n setIsSaving(false)\n }\n }\n\n return (\n <div className=\"space-y-6 p-1\">\n <div className=\"space-y-2\">\n <Label htmlFor={`report-title-${report.reportHubId}`}>タイトル</Label>\n <Input id={`report-title-${report.reportHubId}`} value={title} onChange={(e) => setTitle(e.target.value)} placeholder=\"日報のタイトルを入力\" disabled={isSaving} />\n </div>\n\n <div className=\"space-y-2\">\n <Label htmlFor={`report-content-${report.reportHubId}`}>内容</Label>\n <Textarea id={`report-content-${report.reportHubId}`} value={content} onChange={(e) => setContent(e.target.value)} placeholder=\"日報の内容を入力\" className=\"min-h-50\" disabled={isSaving} />\n </div>\n\n <div className=\"flex justify-end gap-2\">\n {onCancel && (\n <Button variant=\"outline\" onClick={onCancel} disabled={isSaving}>\n キャンセル\n </Button>\n )}\n <Button variant=\"secondary\" onClick={handleSave} disabled={isSaving}>\n 保存\n </Button>\n <Button onClick={handlePublish} disabled={isSaving}>\n 公開\n </Button>\n </div>\n </div>\n )\n}\n","import { Slot } from \"@radix-ui/react-slot\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport * as React from \"react\"\nimport { cn } from \"./cn\"\n\nconst buttonVariants = cva(\n \"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0\",\n {\n variants: {\n variant: {\n default: \"bg-primary text-primary-foreground hover:bg-primary/90\",\n destructive: \"bg-destructive text-destructive-foreground hover:bg-destructive/90\",\n outline: \"border border-input bg-background hover:bg-accent hover:text-accent-foreground\",\n secondary: \"bg-secondary text-secondary-foreground hover:bg-secondary/80\",\n ghost: \"hover:bg-accent hover:text-accent-foreground\",\n link: \"text-primary underline-offset-4 hover:underline\",\n },\n size: {\n default: \"h-10 px-4 py-2\",\n sm: \"h-9 rounded-md px-3\",\n lg: \"h-11 rounded-md px-8\",\n icon: \"h-10 w-10\",\n },\n },\n defaultVariants: {\n variant: \"default\",\n size: \"default\",\n },\n },\n)\n\nexport interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> {\n asChild?: boolean\n}\n\nconst Button = React.forwardRef<HTMLButtonElement, ButtonProps>(({ className, variant, size, asChild = false, ...props }, ref) => {\n const Comp = asChild ? Slot : \"button\"\n return <Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />\n})\nButton.displayName = \"Button\"\n\nexport { Button, buttonVariants }\n","import type * as React from \"react\"\n\nimport { cn } from \"./cn\"\n\nfunction Input({ className, type, ...props }: React.ComponentProps<\"input\">) {\n return (\n <input\n type={type}\n data-slot=\"input\"\n className={cn(\n \"flex h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-xs outline-none transition-[color,box-shadow] selection:bg-primary selection:text-primary-foreground file:inline-flex file:h-7 file:border-0 file:bg-transparent file:font-medium file:text-foreground file:text-sm placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30\",\n \"focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50\",\n \"aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40\",\n className,\n )}\n {...props}\n />\n )\n}\n\nexport { Input }\n","import * as LabelPrimitive from \"@radix-ui/react-label\"\nimport type * as React from \"react\"\n\nimport { cn } from \"./cn\"\n\nfunction Label({ className, ...props }: React.ComponentProps<typeof LabelPrimitive.Root>) {\n return (\n <LabelPrimitive.Root\n data-slot=\"label\"\n className={cn(\"flex select-none items-center gap-2 font-medium text-sm leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-50 group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50\", className)}\n {...props}\n />\n )\n}\n\nexport { Label }\n","import type * as React from \"react\"\n\nimport { cn } from \"./cn\"\n\nfunction Textarea({ className, ...props }: React.ComponentProps<\"textarea\">) {\n return (\n <textarea\n data-slot=\"textarea\"\n className={cn(\n \"flex min-h-15 w-full rounded-md border border-input bg-transparent px-3 py-2 text-base shadow-xs placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30\",\n \"aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40\",\n className,\n )}\n {...props}\n />\n )\n}\n\nexport { Textarea }\n","import { cn } from \"../ui/cn\"\n\ntype UnreadIndicatorProps = {\n className?: string\n}\n\n/**\n * A simple blue dot indicator used to represent unread status.\n * 未読状態を表すためのシンプルな青いドットインジケーター。\n */\nexport const UnreadIndicator = ({ className }: UnreadIndicatorProps) => {\n return <span className={cn(\"text-blue-500\", className)}>●</span>\n}\n","/**\n * Header badge that visualizes the ids stream lifecycle.\n * ID ストリームのライフサイクルをヘッダーへ可視化するバッジコンポーネント。\n *\n * 局面ごとの表示:\n * - streaming: 進捗ピル「読み込み中 n / total (p%)」(total 未着なら件数のみ)\n * - retrying: 「再接続中... (n 件受信済)」\n * - failed: 失敗表示 + 再試行ボタン (受信済みデータは保持されている)\n * - complete + 再検証中: 控えめな「最新化中」インジケーター\n * - それ以外 (idle / complete): 何も描画しない\n */\nimport { RefreshCw } from \"lucide-react\"\nimport { useAnimatedNumber } from \"../hooks/use-animated-number\"\nimport { retryDailyReportIdsStream, useDailyReportIdsStream } from \"../streaming/daily-report-ids-stream-session\"\n\n/**\n * Renders the streaming progress / recovery badge for the ids stream.\n * ids ストリームの進捗・復旧バッジを描画するコンポーネント。\n */\nexport const DailyReportIdsStreamStatus = () => {\n const stream = useDailyReportIdsStream()\n const animatedLoaded = useAnimatedNumber(stream.loadedCount)\n const animatedTotal = useAnimatedNumber(stream.totalCount ?? 0)\n\n if (stream.phase === \"streaming\") {\n // total 到着前は件数のみ、到着後は分母と進捗率を添える\n const percent = stream.totalCount && stream.totalCount > 0 ? Math.min(100, Math.round((stream.loadedCount / stream.totalCount) * 100)) : null\n return (\n <span data-testid=\"daily-report-ids-stream-status\" data-phase=\"streaming\" className=\"inline-flex items-center gap-1.5 rounded-full bg-blue-100 px-2 py-0.5 font-medium text-blue-800 text-xs tabular-nums dark:bg-blue-900/40 dark:text-blue-300\">\n <span aria-hidden=\"true\" className=\"h-1.5 w-1.5 animate-pulse rounded-full bg-blue-500 dark:bg-blue-400\" />\n 読み込み中 {animatedLoaded.toLocaleString()}\n {percent !== null ? ` / ${animatedTotal.toLocaleString()} (${percent}%)` : \" 件\"}\n </span>\n )\n }\n\n if (stream.phase === \"retrying\") {\n return (\n <span role=\"status\" data-testid=\"daily-report-ids-stream-status\" data-phase=\"retrying\" className=\"inline-flex items-center gap-1.5 rounded-full bg-amber-100 px-2 py-0.5 font-medium text-amber-800 text-xs tabular-nums dark:bg-amber-900/40 dark:text-amber-300\">\n <RefreshCw aria-hidden=\"true\" className=\"h-3 w-3 animate-spin\" />\n 再接続中... ({stream.loadedCount.toLocaleString()} 件受信済)\n </span>\n )\n }\n\n if (stream.phase === \"failed\") {\n return (\n <span role=\"status\" data-testid=\"daily-report-ids-stream-status\" data-phase=\"failed\" className=\"inline-flex items-center gap-1.5 rounded-full bg-rose-100 px-2 py-0.5 font-medium text-rose-800 text-xs dark:bg-rose-900/40 dark:text-rose-300\">\n 読み込みに失敗しました\n <button type=\"button\" data-testid=\"daily-report-ids-stream-retry\" onClick={() => retryDailyReportIdsStream()} className=\"rounded bg-rose-200 px-1.5 py-0.5 font-semibold text-rose-900 hover:bg-rose-300 dark:bg-rose-800 dark:text-rose-100 dark:hover:bg-rose-700\">\n 再試行\n </button>\n </span>\n )\n }\n\n if (stream.isRevalidating) {\n return (\n <span data-testid=\"daily-report-ids-stream-status\" data-phase=\"revalidating\" className=\"inline-flex items-center gap-1 text-slate-400 text-xs dark:text-slate-500\">\n <RefreshCw aria-hidden=\"true\" className=\"h-3 w-3 animate-spin\" />\n 最新化中\n </span>\n )\n }\n\n return null\n}\n","/**\n * Ease-out animated number for streaming counters.\n * ストリーミングカウンター向けのイーズアウト数値アニメーションフック。\n *\n * 経過時間の基準は「最初の rAF フレームのタイムスタンプ」にする。effect 内の\n * `performance.now()` を基準にすると、メインスレッド混雑時に保留中フレームの\n * vsync 時刻が基準より過去になり、負の進捗が 3 乗されて 0 に張り付く\n * (contact-map で実測された既知の不具合)。進捗は念のため 0 でもクランプする。\n */\nimport { useEffect, useRef, useState } from \"react\"\n\n/**\n * Animates numeric changes with ease-out cubic.\n * 数値変化をイーズアウト 3 次で補間するフック。\n *\n * @param value Target value. 目標値。\n * @param durationMs Animation duration. アニメーション時間 (既定 250ms)。\n * @returns Displayed (interpolated) value. 表示用の補間値。\n */\nexport const useAnimatedNumber = (value: number, durationMs = 250): number => {\n const [display, setDisplay] = useState(value)\n // 表示中の値の同期ミラー (アニメーション中断時の次回開始点に使う)\n const displayRef = useRef(value)\n const isFirstRef = useRef(true)\n\n /**\n * Animation effect.\n * アニメーション副作用。\n *\n * 依存配列:\n * - value: 目標値の変化でアニメーションを開始する\n * - durationMs: 時間変更で再スケジュール\n * クリーンアップ:\n * - 実行中の rAF をキャンセルする (中断時は displayRef が現在値を保持)\n */\n useEffect(() => {\n // 初回は補間せず即時表示する (マウント時のカウントアップ演出は不要)\n if (isFirstRef.current) {\n isFirstRef.current = false\n displayRef.current = value\n setDisplay(value)\n return\n }\n const from = displayRef.current\n if (from === value) return\n\n let rafId: number | null = null\n let startTimestamp: number | null = null\n const step = (timestamp: number) => {\n if (startTimestamp === null) {\n startTimestamp = timestamp\n }\n const progress = Math.min(1, Math.max(0, (timestamp - startTimestamp) / durationMs))\n const eased = 1 - (1 - progress) ** 3\n const next = Math.round(from + (value - from) * eased)\n displayRef.current = next\n setDisplay(next)\n if (progress < 1) {\n rafId = requestAnimationFrame(step)\n }\n }\n rafId = requestAnimationFrame(step)\n return () => {\n if (rafId !== null) cancelAnimationFrame(rafId)\n }\n }, [value, durationMs])\n\n return display\n}\n","/**\n * Virtual-scrolling summary list for daily reports driven by fetcher-based detail hooks.\n * useFetcher ベースの詳細取得フックで日報サマリーを仮想スクロール表示。\n */\nimport { SwipeCloseGuardContext, useSwipeGuardState, useSwipeToDismissOverlay, useTapClick, useTransitionDisplay } from \"@aiquants/swipe-overlay\"\nimport { type ScrollBarThumbOverlayRenderProps, VirtualScroll, type VirtualScrollHandle } from \"@aiquants/virtualscroll\"\nimport { Check, Star, Trash2 } from \"lucide-react\"\nimport { type CSSProperties, memo, type RefObject, useCallback, useEffect, useMemo, useRef, useState } from \"react\"\nimport { fallbackText, formatToIsoDate } from \"../../shared/text-utils\"\nimport type { DailyReportDetail, DailyReportItem } from \"../../shared/types\"\nimport { resolveSourceTypeConfig, useDailyReportConfig } from \"../config-context\"\nimport { useDailyReportActionContext } from \"../contexts/daily-report-action-context\"\nimport { useDailyReportDetail } from \"../hooks/use-daily-report\"\nimport { useDailyReportComments } from \"../hooks/use-daily-report-comments\"\nimport { useDynamicViewportHeight } from \"../hooks/use-dynamic-viewport-height\"\nimport { useIsMobileLayout } from \"../hooks/use-responsive-layout\"\nimport { cn } from \"../ui/cn\"\nimport { Tabs, TabsContent, TabsList, TabsTrigger } from \"../ui/tabs\"\nimport {\n CONTENT_DRAG_POINTER_INPUTS,\n DEFAULT_ITEM_HEIGHT,\n EMPTY_DETAIL,\n EMPTY_ITEM,\n FAST_SCROLL_INERTIA_OPTIONS,\n FAST_SCROLL_WHEEL_MULTIPLIER,\n ITEM_CONTAINER_HEIGHT,\n ITEM_VERTICAL_PADDING,\n MAX_PREVIEW_LINES,\n SCROLL_BAR_WIDTH,\n TAP_SCROLL_CIRCLE_OPTIONS,\n VIRTUAL_SCROLL_OVERSCAN_COUNT,\n WHEEL_RESET_TIMEOUT_MS,\n} from \"../utils/constants\"\nimport { BusinessDayThumbOverlay } from \"./business-day-thumb-overlay\"\nimport { DailyReportAttachmentIndicator } from \"./daily-report-attachment-indicator\"\nimport { DailyReportAttachmentList } from \"./daily-report-attachment-list\"\nimport { DailyReportCommentForm, DailyReportCommentList } from \"./daily-report-comment-section\"\nimport { DailyReportEditForm } from \"./daily-report-edit-form\"\nimport { UnreadIndicator } from \"./unread-indicator\"\n\ntype TestableVirtualScrollHandle = VirtualScrollHandle & {\n findReportIndex: (id: number) => number\n getReportIds: (limit?: number) => number[]\n}\n\ntype DailyReportListProps = {\n dailyReportItems: DailyReportItem[]\n autoMarkRead?: boolean\n selectedReportHubId: number | null\n onSelectItem: (reportHubId: number | null) => void\n userId?: string | null\n /**\n * タブ切替 (アンマウント) をまたいでスクロール位置を持ち回る共有スロット。\n * 値は VirtualScroll の**論理座標** (inset 非依存。onScroll が配信する座標系)。\n * マウント時に一度だけ読み、スクロールのたびに書き戻す。\n * ❗ 値をレンダー時に読んで props へ焼き込む方式は禁止: 非制御タブでは親が\n * 再レンダーされず、復帰マウントに古い値が渡る (実測で再現した実害バグ)\n */\n scrollOffsetRef?: RefObject<number>\n}\n\n/**\n * Manages mobile overlay state, history navigation, and scroll locking.\n * モバイルオーバーレイの状態、履歴ナビゲーション、スクロールロックを管理するフック。\n */\nconst useMobileOverlayManager = (isMobileLayout: boolean, selectedReportHubId: number | null, dailyReportItems: DailyReportItem[], onSelectItem: (reportHubId: number | null) => void) => {\n const [isMobileDetailOpen, setIsMobileDetailOpen] = useState(false)\n const [isOverlayMounted, setIsOverlayMounted] = useState(false)\n const overlayHistoryKeyRef = useRef<string | null>(null)\n const lastClosedItemIdRef = useRef<number | null>(null)\n const bodyOverflowRef = useRef<string | null>(null)\n\n const selectedItem = useMemo(() => (selectedReportHubId === null ? null : (dailyReportItems.find((e) => e.reportHubId === selectedReportHubId) ?? null)), [dailyReportItems, selectedReportHubId])\n const shouldShowOverlay = isMobileLayout && isMobileDetailOpen && selectedItem !== null\n\n /**\n * Manages the mounting state of the mobile overlay for animation purposes.\n * アニメーション用にモバイルオーバーレイのマウント状態を管理する副作用。\n *\n * Purpose: To delay unmounting until the close animation completes.\n * Dependencies: [shouldShowOverlay]\n * Cleanup: Clears the timeout.\n */\n useEffect(() => {\n if (shouldShowOverlay) {\n setIsOverlayMounted(true)\n } else {\n const timer = setTimeout(() => setIsOverlayMounted(false), 300)\n return () => clearTimeout(timer)\n }\n }, [shouldShowOverlay])\n\n /**\n * Manages mobile history state and body scroll locking.\n * モバイル履歴状態と Body スクロールロックを管理する副作用。\n *\n * Purpose: To support browser back button for closing overlay and prevent background scrolling.\n * Dependencies: [isMobileLayout, isMobileDetailOpen, selectedReportHubId]\n * Cleanup: Restores body overflow style.\n */\n useEffect(() => {\n if (typeof window === \"undefined\") return\n\n // History Management\n if (isMobileLayout && isMobileDetailOpen && selectedReportHubId !== null && !overlayHistoryKeyRef.current) {\n const key = `overlay-${Date.now()}`\n window.history.pushState({ ...window.history.state, dailyReportOverlay: key }, \"\", window.location.href)\n overlayHistoryKeyRef.current = key\n } else if (!(isMobileLayout && isMobileDetailOpen) && overlayHistoryKeyRef.current) {\n overlayHistoryKeyRef.current = null\n }\n\n // Scroll Lock\n if (isMobileDetailOpen) {\n bodyOverflowRef.current = document.body.style.overflow\n document.body.style.overflow = \"hidden\"\n } else if (bodyOverflowRef.current !== null) {\n document.body.style.overflow = bodyOverflowRef.current\n bodyOverflowRef.current = null\n }\n\n return () => {\n if (bodyOverflowRef.current !== null) document.body.style.overflow = bodyOverflowRef.current\n }\n }, [isMobileLayout, isMobileDetailOpen, selectedReportHubId])\n\n /**\n * Handles browser back navigation (popstate).\n * ブラウザの戻る操作 (popstate) を処理する副作用。\n *\n * Purpose: To close the mobile overlay when the user presses the back button.\n * Dependencies: [selectedReportHubId]\n * Cleanup: Removes popstate listener.\n */\n useEffect(() => {\n if (typeof window === \"undefined\") return\n const onPopState = () => {\n if (overlayHistoryKeyRef.current) {\n lastClosedItemIdRef.current = selectedReportHubId\n overlayHistoryKeyRef.current = null\n setIsMobileDetailOpen(false)\n onSelectItem(null)\n }\n }\n window.addEventListener(\"popstate\", onPopState)\n return () => window.removeEventListener(\"popstate\", onPopState)\n }, [selectedReportHubId, onSelectItem])\n\n const handleOverlayClose = useCallback(() => {\n if (!isMobileLayout) return\n lastClosedItemIdRef.current = selectedReportHubId\n setIsMobileDetailOpen(false)\n onSelectItem(null)\n if (overlayHistoryKeyRef.current && typeof window !== \"undefined\") {\n overlayHistoryKeyRef.current = null\n window.history.back()\n }\n }, [isMobileLayout, selectedReportHubId, onSelectItem])\n\n return {\n isMobileDetailOpen,\n setIsMobileDetailOpen,\n isOverlayMounted,\n shouldShowOverlay,\n handleOverlayClose,\n lastClosedItemIdRef,\n selectedItem,\n }\n}\n\n/**\n * Manages auto-read logic and read status toggling.\n * 自動既読ロジックと既読状態の切り替えを管理するフック。\n */\nconst useAutoRead = (report: DailyReportDetail | null, isLoading: boolean, autoMarkRead: boolean, isActive: boolean, toggleRead: (reportHubId: number, isRead: boolean, date: string) => Promise<void>) => {\n const ignoreAutoReadForReportHubId = useRef<number | null>(null)\n const lastReportHubIdRef = useRef<number | undefined>(undefined)\n\n if (lastReportHubIdRef.current !== report?.reportHubId) {\n lastReportHubIdRef.current = report?.reportHubId\n ignoreAutoReadForReportHubId.current = null\n }\n\n /**\n * Resets suppression flag when inactive.\n * 非アクティブ時に抑制フラグをリセットする副作用。\n *\n * Purpose: To re-enable auto-read when the report is reopened.\n * Dependencies: [isActive]\n * Cleanup: None.\n */\n useEffect(() => {\n if (!isActive) ignoreAutoReadForReportHubId.current = null\n }, [isActive])\n\n /**\n * Handles the read status toggle logic.\n * 既読状態の切り替え処理を行うコールバック。\n *\n * Purpose: To toggle read status and manage auto-read suppression.\n * Dependencies: [report, toggleRead]\n */\n const handleToggleRead = useCallback(\n async (isAuto: boolean = false) => {\n if (!report) return\n if (!isAuto) ignoreAutoReadForReportHubId.current = report.reportHubId\n\n const nextValue = !report.isRead\n try {\n await toggleRead(report.reportHubId, nextValue, report.date ?? \"\")\n } catch (_e) {\n // Error handled in context\n }\n },\n [report, toggleRead],\n )\n\n /**\n * Automatically marks the report as read after a delay.\n * 一定時間後にレポートを自動的に既読にする副作用。\n *\n * Purpose: To mark reports as read when viewed for a sufficient duration.\n * Dependencies: [report, isLoading, autoMarkRead, handleToggleRead]\n * Cleanup: Clears the timeout.\n */\n useEffect(() => {\n if (autoMarkRead && report && !report.isRead && !isLoading) {\n if (ignoreAutoReadForReportHubId.current === report.reportHubId) return\n const timer = setTimeout(() => handleToggleRead(true), 500)\n return () => clearTimeout(timer)\n }\n }, [report, isLoading, autoMarkRead, handleToggleRead])\n\n return { handleToggleRead }\n}\n\n/**\n * Daily report list cards with key metadata and preview content via virtual scrolling.\n * 主なメタデータと内容プレビューを仮想スクロール表示する日報リスト。\n */\nexport const DailyReportList = ({ dailyReportItems, autoMarkRead = false, selectedReportHubId, onSelectItem, userId, scrollOffsetRef }: DailyReportListProps) => {\n // 復元位置はマウント時に一度だけ共有スロットから読む (レンダー時読取の焼き込みを構造的に排除)\n const [initialScrollOffset] = useState(() => scrollOffsetRef?.current ?? 0)\n const scrollContainerRef = useRef<HTMLDivElement | null>(null)\n const virtualScrollRef = useRef<VirtualScrollHandle>(null)\n // list ロールの aria-label に使う (消費側が注入する業務ドメイン名。既定 \"Daily Reports\")\n const { title } = useDailyReportConfig()\n const { viewportHeight, updateViewportHeight } = useDynamicViewportHeight(scrollContainerRef)\n const [overlayLabel, setOverlayLabel] = useState<string>(\"\")\n const [isWheelScrollActive, setIsWheelScrollActive] = useState(false)\n const isMobileLayout = useIsMobileLayout()\n const wheelResetTimerRef = useRef<number | null>(null)\n const itemCount = dailyReportItems.length\n const scrollEndTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)\n\n const { isMobileDetailOpen, setIsMobileDetailOpen, isOverlayMounted, shouldShowOverlay, handleOverlayClose, lastClosedItemIdRef, selectedItem } = useMobileOverlayManager(isMobileLayout, selectedReportHubId, dailyReportItems, onSelectItem)\n\n const columnHeightStyle = useMemo<CSSProperties | undefined>(() => (Number.isFinite(viewportHeight) && viewportHeight > 0 ? { height: viewportHeight } : undefined), [viewportHeight])\n\n /**\n * Manages global event listeners (Wheel, Resize).\n * ホイール・リサイズのグローバルイベントリスナーを管理する副作用。\n *\n * Purpose: To keep the viewport height in sync and to detect wheel scrolling.\n * (レスポンシブ判定は use-responsive-layout のフックが担当する。)\n * Dependencies: [updateViewportHeight]\n * Cleanup: Removes all event listeners and clears timers.\n */\n useEffect(() => {\n if (typeof window === \"undefined\") return\n\n const onResize = () => requestAnimationFrame(updateViewportHeight)\n window.addEventListener(\"resize\", onResize)\n updateViewportHeight()\n\n const el = scrollContainerRef.current\n const onWheel = () => {\n setIsWheelScrollActive(true)\n if (wheelResetTimerRef.current) clearTimeout(wheelResetTimerRef.current)\n wheelResetTimerRef.current = window.setTimeout(() => {\n setIsWheelScrollActive(false)\n wheelResetTimerRef.current = null\n }, WHEEL_RESET_TIMEOUT_MS)\n }\n el?.addEventListener(\"wheel\", onWheel, { passive: true })\n\n return () => {\n window.removeEventListener(\"resize\", onResize)\n el?.removeEventListener(\"wheel\", onWheel)\n if (wheelResetTimerRef.current) clearTimeout(wheelResetTimerRef.current)\n }\n }, [updateViewportHeight])\n\n /**\n * Clears the selection when the list becomes empty.\n * 一覧が空になったとき選択を解除する副作用。\n *\n * 依存配列: [itemCount, selectedReportHubId, onSelectItem]。クリーンアップ: なし。\n */\n useEffect(() => {\n if (!itemCount && selectedReportHubId !== null) {\n onSelectItem(null)\n }\n }, [itemCount, selectedReportHubId, onSelectItem])\n\n /**\n * Restores focus after closing overlay (Desktop only).\n * オーバーレイを閉じた後のフォーカス復帰 (デスクトップのみ) を行う副作用。\n *\n * Purpose: To maintain keyboard focus context for accessibility.\n * Dependencies: [isMobileDetailOpen, isMobileLayout, dailyReportItems]\n * Cleanup: None.\n */\n useEffect(() => {\n if (isMobileLayout || isMobileDetailOpen || lastClosedItemIdRef.current === null) return\n const targetId = lastClosedItemIdRef.current\n lastClosedItemIdRef.current = null\n\n const index = dailyReportItems.findIndex((item) => item.reportHubId === targetId)\n if (index >= 0) {\n // スクロールで行を DOM に入れてから、カード本体 (tabIndex=0) へフォーカスを戻す。\n // focusItemAtIndex は enableKeyboardNavigation:false では no-op のため使えない\n // (行ラッパーではなくカードへ直接フォーカスするのが本来の意図でもある)\n virtualScrollRef.current?.scrollToIndex(index)\n requestAnimationFrame(() => {\n // detail-list の行も同じ data 属性を持つため、一覧側スコープで限定する\n const card = document.querySelector<HTMLElement>(`[data-testid=\"daily-report-list\"] [data-daily-report-button=\"${targetId}\"]`)\n card?.focus()\n })\n }\n }, [isMobileDetailOpen, isMobileLayout, dailyReportItems, lastClosedItemIdRef])\n\n /**\n * Syncs overlay label with the first item's date.\n * オーバーレイラベルを先頭アイテムの日付と同期する副作用。\n *\n * Purpose: To provide a default label for the scroll thumb overlay.\n * Dependencies: [dailyReportItems, itemCount]\n * Cleanup: None.\n */\n useEffect(() => {\n if (itemCount > 0)\n setOverlayLabel((prev) => {\n const next = formatToIsoDate(dailyReportItems[0]?.businessDate)\n return prev === next ? prev : next\n })\n }, [dailyReportItems, itemCount])\n\n const dailyReportItemsRef = useRef(dailyReportItems)\n dailyReportItemsRef.current = dailyReportItems\n\n useEffect(() => {\n if (virtualScrollRef.current) {\n const el = document.querySelector('[data-testid=\"daily-report-list\"]') as (Element & { __virtualScroll?: TestableVirtualScrollHandle }) | null\n if (el) {\n el.__virtualScroll = {\n ...virtualScrollRef.current,\n findReportIndex: (id: number) => dailyReportItemsRef.current.findIndex((item) => item.reportHubId === id),\n getReportIds: (limit = 20) => dailyReportItemsRef.current.slice(0, limit).map((item) => item.reportHubId),\n }\n }\n }\n })\n\n const handleItemSelect = useCallback(\n (id: number) => {\n if (id <= 0) return\n onSelectItem(id)\n if (isMobileLayout) setIsMobileDetailOpen(true)\n },\n [isMobileLayout, onSelectItem, setIsMobileDetailOpen],\n )\n\n const getItem = useCallback((index: number) => (index < itemCount ? dailyReportItems[index] : EMPTY_ITEM), [dailyReportItems, itemCount])\n const getItemHeight = useCallback(() => DEFAULT_ITEM_HEIGHT, [])\n\n // 行レンダラーを安定参照にする。インライン矢印だと毎レンダー新関数になり、\n // React.memo の ListReportItem が全行で再レンダーへ落ちる (選択変更で全行が再描画)\n const renderListRow = useCallback(\n (item: DailyReportItem, index: number) => <ListReportItem key={item.reportHubId} item={item} index={index} setSize={itemCount} isActive={item.reportHubId === selectedReportHubId} onSelect={handleItemSelect} />,\n [itemCount, selectedReportHubId, handleItemSelect],\n )\n\n const handleRangeChange = useCallback(\n (range: { visibleStartIndex: number }) => {\n const item = getItem(range.visibleStartIndex)\n if (item?.reportHubId > 0)\n setOverlayLabel((prev) => {\n const next = formatToIsoDate(item.businessDate)\n return prev === next ? prev : next\n })\n },\n [getItem],\n )\n\n const handleScroll = useCallback(\n (position: number) => {\n // onScroll が配信する論理座標をそのまま共有スロットへ書く (保存と復元で座標系を一致させる)。\n // ❗ getScrollPosition() の読み戻しへ戻さないこと: あれはペイン座標 (inset 込み) で、\n // 論理座標を受け取る initialScrollOffset との往復ごとに inset 分ずれる\n if (scrollOffsetRef) {\n scrollOffsetRef.current = position\n }\n\n const container = scrollContainerRef.current\n if (!container) return\n\n if (container.dataset.scrolling !== \"true\") {\n container.dataset.scrolling = \"true\"\n }\n\n if (scrollEndTimerRef.current) {\n clearTimeout(scrollEndTimerRef.current)\n }\n\n scrollEndTimerRef.current = setTimeout(() => {\n if (container) {\n delete container.dataset.scrolling\n }\n scrollEndTimerRef.current = null\n }, 150)\n },\n [scrollOffsetRef],\n )\n\n const renderThumbOverlay = useCallback((props: ScrollBarThumbOverlayRenderProps) => <BusinessDayThumbOverlay {...props} label={overlayLabel} isWheelScrollActive={isWheelScrollActive} />, [overlayLabel, isWheelScrollActive])\n\n return (\n <>\n <div className=\"flex h-full min-h-0 gap-4\">\n <div ref={scrollContainerRef} className=\"flex min-h-0 w-full sm:w-90\" style={columnHeightStyle}>\n <VirtualScroll\n ref={virtualScrollRef}\n itemCount={itemCount}\n getItem={getItem}\n getItemHeight={getItemHeight}\n viewportSize={viewportHeight}\n initialScrollOffset={initialScrollOffset}\n overscanCount={VIRTUAL_SCROLL_OVERSCAN_COUNT}\n scrollBarOptions={{\n width: SCROLL_BAR_WIDTH,\n tapScrollCircleOptions: TAP_SCROLL_CIRCLE_OPTIONS,\n renderThumbOverlay: renderThumbOverlay,\n }}\n behaviorOptions={{\n // ドラッグスクロールは無効化せず、**ポインタ種別で絞る**。\n // 丸ごと切るとタッチ端末からスクロール手段が消える (定数の注記参照)\n pointerDragInputs: CONTENT_DRAG_POINTER_INPUTS,\n inertiaOptions: FAST_SCROLL_INERTIA_OPTIONS,\n wheelSpeedMultiplier: FAST_SCROLL_WHEEL_MULTIPLIER,\n // 行ラッパーへの tabIndex 付与 (タブ順汚染) と pointerdown フォーカス移動を止める。\n // 「★/既読タップで詳細ペインが切り替わる」不具合は 2 経路あり、これはその 1 つ目\n // (focus 経由)。2 つ目 (useTapClick の pointerup バブリング) はボタン側の\n // stopPointerPropagation が塞ぐ。onItemFocus は発火しなくなる\n enableKeyboardNavigation: false,\n }}\n onRangeChange={handleRangeChange}\n onScroll={handleScroll}\n contentInsets={{ top: ITEM_VERTICAL_PADDING, bottom: ITEM_VERTICAL_PADDING }}\n testId=\"daily-report-list\"\n className=\"relative w-full\"\n contentProps={{\n // 行 (role=\"option\" ではなく role=\"listitem\") だけを所有する list を、\n // スクロールバーと兄弟のコンテンツ要素に置く (virtualscroll 1.23.0 の口)。\n // 各行が aria-posinset/aria-setsize を持つので SR は「全 N 件中 M 番目」を読める\n role: \"list\",\n \"aria-label\": title,\n }}>\n {renderListRow}\n </VirtualScroll>\n </div>\n <DailyReportSidePane dailyReportItems={dailyReportItems} selectedItem={selectedItem} heightStyle={columnHeightStyle} autoMarkRead={!isMobileLayout && autoMarkRead} isActive={selectedReportHubId !== null} userId={userId} />\n </div>\n {itemCount === 0 && (\n <div className=\"py-8 text-center\">\n <p className=\"text-slate-500\">データがありません</p>\n </div>\n )}\n {isOverlayMounted && <DailyReportMobileOverlay isOpen={shouldShowOverlay} selectedItem={selectedItem} onClose={handleOverlayClose} autoMarkRead={isMobileLayout && autoMarkRead} userId={userId} />}\n </>\n )\n}\n\n/**\n * Loading skeleton representation for daily report list items.\n * 日報リストアイテム用読み込み中スケルトン表示。\n *\n * `wrapperProps` は仮想リストの位置属性 (role=\"listitem\" / aria-posinset / aria-setsize) を\n * 外側ラッパーへ透過するための口。読み込み中の行も一覧内の位置を SR へ公開する。\n */\nconst ListSkeletonItem = (wrapperProps: React.HTMLAttributes<HTMLDivElement> & { \"aria-posinset\"?: number; \"aria-setsize\"?: number }) => (\n <div {...wrapperProps} className=\"px-2 pb-3\" style={{ height: ITEM_CONTAINER_HEIGHT }}>\n <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\">\n <div className=\"mb-3 flex flex-wrap items-center gap-3\">\n <div className=\"h-4 w-24 rounded bg-slate-200\"></div>\n <div className=\"h-4 w-24 rounded bg-slate-200\"></div>\n <div className=\"h-4 w-24 rounded bg-slate-200\"></div>\n </div>\n <div className=\"space-y-2\">\n <div className=\"h-4 w-full rounded bg-slate-200\"></div>\n <div className=\"h-4 w-11/12 rounded bg-slate-200\"></div>\n <div className=\"h-4 w-5/6 rounded bg-slate-200\"></div>\n </div>\n </div>\n </div>\n)\n\ntype DailyReportSidePaneTab = \"article\" | \"relations\"\n\n/**\n * Side pane that exposes quick tabs for the selected report summary, editing, and correlation views.\n * 選択した日報の概要表示・編集・相関ビューを切り替え表示するサイドペイン。\n */\nconst DailyReportSidePane = ({\n dailyReportItems,\n selectedItem,\n heightStyle,\n autoMarkRead = false,\n isActive = true,\n userId,\n}: {\n dailyReportItems?: DailyReportItem[] // Optional for mobile overlay usage\n selectedItem: DailyReportItem | null\n heightStyle?: CSSProperties\n autoMarkRead?: boolean\n isActive?: boolean\n userId?: string | null\n}) => {\n const [activeTab, setActiveTab] = useState<DailyReportSidePaneTab>(\"article\")\n const [isEditing, setIsEditing] = useState(false)\n const { draftLabelName, fieldLabels, sourceTypeConfigs } = useDailyReportConfig()\n const { report, error, isLoading } = useDailyReportDetail(selectedItem?.reportHubId ?? -1, selectedItem?.businessDate ?? null)\n\n const scrollContainerRef = useRef<HTMLDivElement>(null)\n const lastReportIdRef = useRef<number | null>(null)\n // 自動編集モード (ドラフト検出による自動遷移) かどうかを追跡\n const autoEditRef = useRef(false)\n\n // レポート選択時のエディタモード自動制御\n // - 新しいレポート選択: ドラフトならエディタモード、公開済みならビューモード\n // - 同一レポートの SSE 更新: ドラフト→公開遷移時に自動編集のみ解除\n useEffect(() => {\n if (!report) return\n\n const isDraft = report.labels.some((l) => l.name === draftLabelName)\n\n if (report.reportHubId !== lastReportIdRef.current) {\n // 選択レポートが変更された\n lastReportIdRef.current = report.reportHubId\n if (isDraft) {\n setIsEditing(true)\n autoEditRef.current = true\n } else {\n setIsEditing(false)\n autoEditRef.current = false\n }\n } else if (!isDraft && isEditing && autoEditRef.current) {\n // SSE で同一レポートが下書き→公開に遷移: 自動編集モードのみ解除\n // ユーザーがダブルクリックで手動編集中の場合は解除しない\n setIsEditing(false)\n autoEditRef.current = false\n }\n }, [report, isEditing, draftLabelName])\n\n const isMine = !!(userId && report?.userId === userId)\n\n const scrollToBottom = useCallback(() => {\n setTimeout(() => {\n if (scrollContainerRef.current) {\n scrollContainerRef.current.scrollTo({ top: scrollContainerRef.current.scrollHeight, behavior: \"smooth\" })\n }\n }, 100)\n }, [])\n\n if (dailyReportItems && (dailyReportItems.length === 0 || !selectedItem)) {\n return (\n <aside className=\"hidden min-h-0 min-w-0 flex-1 gap-3 rounded-2xl border border-slate-200 bg-white p-4 shadow-sm sm:block dark:border-slate-700 dark:bg-slate-900/70\" style={heightStyle}>\n <div className=\"flex flex-1 items-center justify-center rounded-xl border border-slate-300 border-dashed bg-slate-50 p-4 text-center text-slate-500 text-sm dark:border-slate-600 dark:bg-slate-900/40 dark:text-slate-300\">\n <p>左側の一覧から日報を選択してください。</p>\n </div>\n </aside>\n )\n }\n\n const headerDate = formatToIsoDate(report?.date ?? selectedItem?.businessDate)\n const headerCreator = report?.employeeName || report?.author ? fallbackText(report.employeeName ?? report.author) : \"\"\n const showHeaderSkeleton = isLoading || !report\n\n const content = (\n <>\n <div className=\"space-y-1\">\n <div className=\"font-semibold text-slate-500 text-xs uppercase tracking-wide dark:text-slate-300\">{fieldLabels.businessDate}</div>\n <div className=\"text-slate-900 text-sm dark:text-slate-100\">{headerDate}</div>\n <div className=\"flex min-h-4 items-center gap-1 text-slate-500 text-xs dark:text-slate-300\">\n <span>{fieldLabels.author}:</span>\n {showHeaderSkeleton ? <div data-skeleton className=\"h-3 w-24 animate-pulse rounded bg-slate-200 dark:bg-slate-700\" /> : <span>{headerCreator}</span>}\n {(() => {\n const sourceType = report?.sourceType\n const sourceConfig = sourceType ? sourceTypeConfigs?.[sourceType] : undefined\n 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\n })()}\n </div>\n </div>\n <Tabs value={activeTab} onValueChange={(v) => setActiveTab(v as DailyReportSidePaneTab)} className=\"flex min-h-0 flex-1 flex-col\">\n <TabsList className=\"grid grid-cols-2 gap-1 rounded-md bg-slate-100 p-1 text-xs dark:bg-slate-800\">\n {[\"article\", \"relations\"].map((tab) => (\n <TabsTrigger\n key={tab}\n value={tab}\n className=\"rounded-md px-2 py-1 font-medium text-slate-600 transition-colors data-[state=active]:bg-white data-[state=active]:text-slate-900 data-[state=active]:shadow-sm dark:text-slate-200 dark:data-[state=active]:bg-slate-900/70 dark:data-[state=active]:text-slate-100\">\n {tab === \"article\" ? fieldLabels.tabArticle : fieldLabels.tabRelations}\n </TabsTrigger>\n ))}\n </TabsList>\n <TabsContent ref={scrollContainerRef} value=\"article\" className=\"scrollbar-thin flex-1 touch-pan-y overflow-y-auto overflow-x-hidden pt-3 text-sm\">\n {isEditing ? (\n report ? (\n <DailyReportEditForm report={report} onCancel={() => setIsEditing(false)} onPublishSuccess={() => setIsEditing(false)} />\n ) : (\n <p className=\"rounded-lg border border-slate-300 border-dashed bg-slate-50 p-4 text-slate-600 dark:border-slate-600 dark:bg-slate-900/40 dark:text-slate-300\">読み込み中...</p>\n )\n ) : (\n selectedItem && (\n <DailyReportDisplayPane\n report={report}\n isLoading={isLoading}\n error={error}\n selectedItem={selectedItem}\n autoMarkRead={autoMarkRead}\n isActive={isActive}\n onCommentAdded={scrollToBottom}\n userId={userId}\n onDoubleClick={isMine ? () => setIsEditing(true) : undefined}\n />\n )\n )}\n </TabsContent>\n <TabsContent value=\"relations\" className=\"scrollbar-thin flex-1 touch-pan-y overflow-y-auto pt-3 text-sm\">\n <p className=\"rounded-lg border border-slate-300 border-dashed bg-slate-50 p-4 text-slate-600 dark:border-slate-600 dark:bg-slate-900/40 dark:text-slate-300\">{fieldLabels.relationsEmpty}</p>\n </TabsContent>\n </Tabs>\n </>\n )\n\n // If rendered as a standalone pane (desktop)\n if (dailyReportItems) {\n return (\n <aside\n data-testid=\"daily-report-side-pane\"\n data-report-id={selectedItem?.reportHubId}\n 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\"\n style={heightStyle}>\n {content}\n </aside>\n )\n }\n // If rendered inside mobile overlay\n return (\n <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\">\n {content}\n </div>\n )\n}\n\n/**\n * Skeleton line placeholder (module-scope so it is not re-created every render).\n * スケルトンの 1 行プレースホルダー (レンダーごとに再生成しないようモジュールスコープに置く)。\n *\n * 以前は DailyReportDisplayPane の render 本体で宣言していたため、レンダーのたびに\n * 別コンポーネント型となり全スケルトンが再マウントしていた。\n */\nconst SkeletonText = ({ className = \"w-32\" }: { className?: string }) => <div data-skeleton className={`h-4 animate-pulse rounded bg-slate-200 dark:bg-slate-700 ${className}`} />\n\n/**\n * Shows the selected report details in the display tab, including content and metadata.\n * 表示タブ内で選択した日報の詳細とメタデータを表示するコンポーネント。\n */\nconst DailyReportDisplayPane = ({\n report,\n isLoading,\n error,\n selectedItem,\n autoMarkRead = false,\n isActive = true,\n onCommentAdded,\n userId,\n onDoubleClick,\n}: {\n report: DailyReportDetail | null\n isLoading: boolean\n error: Error | null\n selectedItem: DailyReportItem\n autoMarkRead?: boolean\n isActive?: boolean\n onCommentAdded?: () => void\n userId?: string | null\n onDoubleClick?: () => void\n}) => {\n // 表示中ユーザーはアクションコンテキストから取得する (root loader 非依存)\n const { toggleStar, toggleRead, deleteReport, user } = useDailyReportActionContext()\n const { fieldLabels, sourceTypeConfigs } = useDailyReportConfig()\n\n // Use custom hook for comments\n const { uiComments, handleAddComment, handleDeleteComment, pendingDeleteId, setPendingDeleteId, resolvedIdMap } = useDailyReportComments(report, user, userId)\n const { handleToggleRead } = useAutoRead(report, isLoading, autoMarkRead, isActive, toggleRead)\n\n const sourceType = selectedItem.sourceType ?? report?.sourceType\n const sourceConfig = resolveSourceTypeConfig(sourceType, sourceTypeConfigs)\n const isExternalSource = sourceType ? sourceType.toLowerCase() !== \"internal\" : false\n const canComment = sourceConfig?.allowComment ?? !isExternalSource\n\n // Auto-scroll to bottom when comments are added (e.g. via SSE)\n const prevReportHubIdRef = useRef<number | null>(null)\n const prevCommentsLengthRef = useRef(uiComments.length)\n\n useEffect(() => {\n const currentReportHubId = report?.reportHubId ?? null\n\n // Reset tracking when switching reports\n if (currentReportHubId !== prevReportHubIdRef.current) {\n prevReportHubIdRef.current = currentReportHubId\n prevCommentsLengthRef.current = uiComments.length\n return\n }\n\n // Scroll if comments increased\n if (uiComments.length > prevCommentsLengthRef.current) {\n onCommentAdded?.()\n }\n prevCommentsLengthRef.current = uiComments.length\n }, [uiComments.length, report?.reportHubId, onCommentAdded])\n\n const handleAddCommentWrapper = async (content: string) => {\n await handleAddComment(content)\n }\n\n const displayContent = useMemo(() => {\n if (!report?.content) return null\n return report.content\n .replace(/<br\\s*\\/?\\s*>/gi, \"\\n\")\n .replace(/<\\/p>/gi, \"\\n\")\n .replace(/<[^>]+>/g, \"\")\n }, [report])\n\n const handleToggleStar = async () => {\n if (!report) return\n const nextValue = !report.isStarred\n try {\n await toggleStar(report.reportHubId, nextValue, report.date ?? \"\")\n } catch (_e) {\n // Error handled in context\n }\n }\n\n const handleDelete = async () => {\n if (!report) return\n if (!confirm(\"本当に削除しますか?\")) return\n try {\n await deleteReport(report.reportHubId, report.date ?? \"\")\n } catch (_e) {\n alert(\"削除に失敗しました\")\n }\n }\n\n if (error) return <p className=\"text-red-500\">{error.message}</p>\n\n const showSkeleton = !report\n const isStarred = report?.isStarred ?? false\n const isRead = report?.isRead ?? false\n const isMine = !!(userId && report?.userId === userId)\n const hasComments = uiComments.length > 0\n\n return (\n // biome-ignore lint/a11y/noStaticElementInteractions: Double click to edit feature\n <div className=\"relative space-y-4 text-slate-700 dark:text-slate-200\" onDoubleClick={onDoubleClick}>\n {!showSkeleton && report && (\n <div className=\"absolute top-0 right-2 z-10 flex items-center justify-end gap-3\">\n <button type=\"button\" onClick={() => handleToggleRead(false)} className=\"text-lg leading-none transition-transform focus:outline-none active:scale-110\" title={isRead ? \"未読にする\" : \"既読にする\"} data-detail-read-button={report.reportHubId}>\n {isRead ? <span className=\"text-slate-300 hover:text-blue-500\">✓</span> : <UnreadIndicator className=\"hover:text-blue-600\" />}\n </button>\n <button type=\"button\" onClick={handleToggleStar} className=\"text-lg leading-none transition-transform focus:outline-none active:scale-110\" title={isStarred ? \"スターを外す\" : \"スターを付ける\"} data-detail-star-button={report.reportHubId}>\n {isStarred ? <span className=\"text-yellow-400\">★</span> : <span className=\"text-slate-300 hover:text-yellow-400\">☆</span>}\n </button>\n {isMine && (\n <button type=\"button\" onClick={handleDelete} data-detail-delete-button={report.reportHubId} className=\"text-lg leading-none transition-transform focus:outline-none active:scale-110\" title=\"削除する\">\n <Trash2 className=\"h-4 w-4 text-slate-300 hover:text-red-500\" />\n </button>\n )}\n </div>\n )}\n\n <div className=\"flex items-baseline\">\n <span className=\"w-24 shrink-0 font-semibold text-slate-500 text-xs uppercase tracking-wide dark:text-slate-300\">{fieldLabels.subject}</span>\n <div className=\"flex-1 pr-20\">{showSkeleton ? <SkeletonText className=\"w-3/4\" /> : <span className=\"fade-in animate-in text-sm duration-500\">{fallbackText(report?.subject)}</span>}</div>\n </div>\n <div className=\"flex items-baseline\">\n <span className=\"w-24 shrink-0 font-semibold text-slate-500 text-xs uppercase tracking-wide dark:text-slate-300\">{fieldLabels.customer}</span>\n <div className=\"flex-1\">{showSkeleton ? <SkeletonText className=\"w-1/2\" /> : <span className=\"fade-in animate-in text-sm duration-500\">{fallbackText(report?.customerName)}</span>}</div>\n </div>\n <div className=\"space-y-2\">\n <div className=\"font-semibold text-slate-500 text-xs uppercase tracking-wide dark:text-slate-300\">{fieldLabels.content}</div>\n <div className=\"min-h-22 rounded-lg bg-slate-50 p-3 text-sm leading-relaxed dark:bg-slate-800/40\">\n {showSkeleton ? (\n <div className=\"space-y-2\">\n <SkeletonText className=\"w-full\" />\n <SkeletonText className=\"w-11/12\" />\n <SkeletonText className=\"w-4/5\" />\n </div>\n ) : displayContent ? (\n <div className=\"fade-in animate-in whitespace-pre-wrap duration-500\">{displayContent}</div>\n ) : (\n <p className=\"fade-in animate-in text-slate-500 duration-500 dark:text-slate-400\">内容は登録されていません。</p>\n )}\n </div>\n </div>\n <div className=\"flex items-baseline\">\n <span className=\"w-24 shrink-0 font-semibold text-slate-500 text-xs uppercase tracking-wide dark:text-slate-300\">{fieldLabels.visitTime}</span>\n <div className=\"flex-1\">\n {showSkeleton ? (\n <SkeletonText className=\"w-1/3\" />\n ) : (\n <span className=\"fade-in animate-in text-sm duration-500\">\n {fallbackText(report?.visitTimeFrom)} {report?.visitTimeFrom && report?.visitTimeTo ? \"-\" : \"\"} {fallbackText(report?.visitTimeTo)}\n </span>\n )}\n </div>\n </div>\n {/* 添付節。DetailList タブと同じ位置 (面談者・本文の後、コメントの前) に置く。\n 片方のタブにしか出さないと、利用者は「添付が無い日報」と誤認する */}\n {report && <DailyReportAttachmentList attachments={report.attachments} />}\n {report && (\n <div className=\"fade-in animate-in space-y-2 border-slate-100 border-t pt-4 duration-500 dark:border-slate-800\">\n <div className=\"font-semibold text-slate-500 text-xs uppercase tracking-wide dark:text-slate-300\">{fieldLabels.comments}</div>\n {hasComments && (\n <DailyReportCommentList\n comments={uiComments}\n reportHubId={report.reportHubId}\n businessDate={report.date ?? \"\"}\n onDeleteComment={handleDeleteComment}\n pendingDeleteId={pendingDeleteId}\n onPendingDeleteIdChange={setPendingDeleteId}\n resolvedIdMap={resolvedIdMap}\n />\n )}\n {canComment && <DailyReportCommentForm reportHubId={report.reportHubId} businessDate={report.date ?? \"\"} onAddComment={handleAddCommentWrapper} />}\n </div>\n )}\n </div>\n )\n}\n\n/**\n * Mobile-only overlay that slides in with the selected daily report details.\n * 選択された日報の詳細をモバイル向けにスライド表示し選択ガードを切り替え可能なオーバーレイ。\n */\nconst DailyReportMobileOverlay = ({ isOpen, selectedItem, onClose, autoMarkRead = false, userId }: { isOpen: boolean; selectedItem: DailyReportItem | null; onClose: () => void; autoMarkRead?: boolean; userId?: string | null }) => {\n const containerRef = useRef<HTMLDivElement>(null)\n const closeButtonRef = useRef<HTMLButtonElement | null>(null)\n\n const { isBlocked: isGuardBlocked, requestBlock, reset: resetGuard } = useSwipeGuardState()\n\n const { report, error, isLoading } = useDailyReportDetail(selectedItem?.reportHubId ?? -1, selectedItem?.businessDate ?? null)\n const displayData = useTransitionDisplay(isOpen, selectedItem ? { item: selectedItem, detail: { report, error, isLoading } } : null)\n const displayItem = displayData?.item ?? null\n const _displayDetail = displayData?.detail ?? EMPTY_DETAIL\n _displayDetail\n const { overlayRef, overlayStyle, attemptClose, isSwiping } = useSwipeToDismissOverlay({\n isOpen,\n onClose,\n isCloseBlocked: useCallback(() => isGuardBlocked, [isGuardBlocked]),\n skipWhenTextSelected: true,\n })\n\n const tapHandlers = useTapClick(() => attemptClose())\n\n /**\n * Manages focus and guard state when overlay opens/closes.\n * オーバーレイの開閉時にフォーカスとガード状態を管理する副作用。\n *\n * Purpose: To set focus on close button for accessibility and reset swipe guard.\n * Dependencies: [isOpen, resetGuard]\n * Cleanup: None.\n */\n useEffect(() => {\n if (isOpen) {\n requestAnimationFrame(() => closeButtonRef.current?.focus())\n } else {\n if (containerRef.current?.contains(document.activeElement)) {\n ;(document.activeElement as HTMLElement).blur()\n }\n resetGuard()\n }\n }, [isOpen, resetGuard])\n\n if (!displayItem) return null\n\n return (\n <div ref={containerRef} className={`fixed inset-0 z-50 flex touch-manipulation transition-[visibility] duration-200 sm:hidden ${isOpen ? \"visible\" : \"invisible\"} ${isOpen && !isSwiping ? \"pointer-events-auto\" : \"pointer-events-none\"}`} aria-hidden={!isOpen}>\n <button type=\"button\" aria-label=\"背景をタップして閉じる\" className={`absolute inset-0 touch-manipulation bg-slate-900/50 transition-opacity duration-200 focus:outline-none ${isOpen ? \"opacity-100\" : \"opacity-0\"}`} {...tapHandlers}></button>\n <aside\n role=\"dialog\"\n aria-modal=\"true\"\n aria-label=\"日報詳細\"\n className={`relative ml-auto flex h-full w-full max-w-full transform touch-none select-none bg-white shadow-xl transition-transform duration-200 ease-out dark:bg-slate-900/95 ${isOpen ? \"pointer-events-auto\" : \"pointer-events-none\"}`}\n ref={overlayRef}\n onKeyDown={(e) => {\n if (e.key === \"Escape\") {\n e.preventDefault()\n attemptClose()\n }\n }}\n style={overlayStyle}>\n <div className=\"flex h-full w-full flex-col gap-3 p-4\">\n <SwipeCloseGuardContext.Provider value={{ isBlocked: isGuardBlocked, requestBlock }}>\n <DailyReportSidePane selectedItem={displayItem} autoMarkRead={autoMarkRead} isActive={isOpen} userId={userId} />\n </SwipeCloseGuardContext.Provider>\n <div className=\"mt-2 flex justify-start\">\n <button\n type=\"button\"\n ref={closeButtonRef}\n data-overlay-interactive=\"true\"\n className=\"touch-manipulation rounded-full border border-slate-300 px-4 py-2 font-medium text-slate-600 text-sm shadow-sm transition-colors hover:bg-slate-100 dark:border-slate-600 dark:text-slate-200 dark:hover:bg-slate-800\"\n onClick={() => attemptClose()}>\n 閉じる\n </button>\n </div>\n </div>\n </aside>\n </div>\n )\n}\n\n/**\n * Up to three non-empty plain-text lines extracted from HTML content.\n * HTML コンテンツから抽出する最大三つの非空プレーンテキスト行。\n */\nconst extractPreviewLines = (content: string | null): string[] => {\n if (!content) return []\n return content\n .replace(/<br\\s*\\/?\\s*>/gi, \"\\n\")\n .replace(/<\\/p>/gi, \"\\n\")\n .replace(/<[^>]+>/g, \"\")\n .split(/\\r?\\n/)\n .map((l) => l.replace(/\\u3000/g, \" \").trim())\n .filter((l) => l.length > 0)\n .slice(0, MAX_PREVIEW_LINES)\n}\n\n/**\n * Daily report summary card rendered inside the list tab.\n * List タブ内で表示する日報サマリーカード。\n */\nconst ListReportItem = memo(({ item, index, setSize, isActive, onSelect }: { item: DailyReportItem; index: number; setSize: number; isActive: boolean; onSelect: (id: number) => void }) => {\n const { fieldLabels, sourceTypeConfigs } = useDailyReportConfig()\n const { toggleStar, toggleRead, removeStaleItem } = useDailyReportActionContext()\n const { report, error, isLoading, isRefetching } = useDailyReportDetail(item.reportHubId, item.businessDate)\n // list 内の絶対位置を SR へ公開する (仮想化で描画されるのは窓分だけなので必須)。\n // どの表示状態 (読み込み中・エラー・本体) でも同じ位置属性を載せる\n const listItemProps = { role: \"listitem\", \"aria-posinset\": index + 1, \"aria-setsize\": setSize } as const\n\n // フェッチが一度でも試行されたかを追跡する ref\n const hasEverFetched = useRef(false)\n if (isRefetching) hasEverFetched.current = true\n\n // フェッチ完了後に report が null → stale アイテムとしてリストから除去する安全弁\n // 外部リソース同期: useDailyReportDetail のフェッチ結果に反応する\n useEffect(() => {\n if (!(report || isLoading || isRefetching || error) && hasEverFetched.current && item.reportHubId > 0) {\n removeStaleItem(item.reportHubId)\n }\n }, [report, isLoading, isRefetching, error, item.reportHubId, removeStaleItem])\n\n const tapHandlers = useTapClick(() => onSelect(item.reportHubId))\n\n if (error) {\n return (\n <div {...listItemProps} className=\"px-2 pb-3\" style={{ height: ITEM_CONTAINER_HEIGHT }}>\n <div className=\"flex h-full items-center justify-center rounded-2xl border border-red-200 bg-red-50 p-4 text-red-700 text-sm dark:border-red-900/50 dark:bg-red-950/40 dark:text-red-300\">\n <p>日報 {item.reportHubId.toLocaleString()} の概要読み込みに失敗しました。</p>\n </div>\n </div>\n )\n }\n\n if (isLoading) return <ListSkeletonItem {...listItemProps} />\n\n if (!report) {\n return (\n <div {...listItemProps} className=\"px-2 pb-3\" style={{ height: ITEM_CONTAINER_HEIGHT }}>\n <div className=\"flex h-full items-center justify-center rounded-2xl border border-slate-200 bg-slate-50 p-4 text-slate-500 text-sm dark:border-slate-700 dark:bg-slate-900/40 dark:text-slate-400\">\n <p>日報が見つかりません</p>\n </div>\n </div>\n )\n }\n\n const previewLines = extractPreviewLines(report.content ?? null).map((line, i) => ({\n line,\n key: `${item.reportHubId}-line-${i}`,\n }))\n const creator = fallbackText(report.employeeName ?? report.author)\n const date = formatToIsoDate(report.date)\n\n const handleToggleStar = async (e: React.MouseEvent) => {\n e.stopPropagation()\n const nextValue = !report.isStarred\n try {\n await toggleStar(report.reportHubId, nextValue, report.date ?? \"\")\n } catch (_e) {\n // Error handled in context\n }\n }\n\n const handleToggleRead = async (e: React.MouseEvent) => {\n e.stopPropagation()\n const nextValue = !report.isRead\n try {\n await toggleRead(report.reportHubId, nextValue, report.date ?? \"\")\n } catch (_e) {\n // Error handled in context\n }\n }\n\n /**\n * Keeps card selection from firing when an inner action button is tapped.\n * カード内のアクションボタン操作でカード選択を発火させないための伝播停止。\n *\n * カードの選択は useTapClick (pointerdown/pointerup 駆動) なので、click の\n * stopPropagation だけでは止まらない。pointer イベント自体を止める必要がある。\n */\n const stopPointerPropagation = (e: React.PointerEvent) => {\n e.stopPropagation()\n }\n\n return (\n <div {...listItemProps} className=\"px-2 pb-3\" style={{ height: ITEM_CONTAINER_HEIGHT }}>\n {/* biome-ignore lint/a11y/useSemanticElements: Card contains nested interactive elements (Star/Read buttons) */}\n <div\n data-daily-report-button={item.reportHubId}\n 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 ${\n 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\"\n }`}\n style={{ touchAction: \"manipulation\" }}\n role=\"button\"\n tabIndex={0}\n // aria-current が「一覧の現在項目」の意味論を担う。aria-pressed は\n // 消費アプリの E2E ヘルパーが選択判定に使う既存契約のため併記を維持する\n aria-current={isActive ? \"true\" : undefined}\n aria-pressed={isActive}\n aria-label={`${date} ${creator}`}\n {...tapHandlers}>\n <div className=\"mb-3 flex items-center gap-3 text-slate-600 text-sm\">\n <span className=\"inline-flex shrink-0 items-center rounded-full bg-slate-100 px-3 py-1 font-semibold text-xs dark:bg-slate-800 dark:text-slate-200\">\n <span className=\"whitespace-nowrap font-medium text-slate-900 dark:text-slate-100\">{date}</span>\n </span>\n <span className=\"inline-flex min-w-0 max-w-full items-center rounded-full bg-slate-100 px-3 py-1 font-semibold text-xs dark:bg-slate-800 dark:text-slate-200\">\n <span className=\"truncate font-medium text-slate-900 dark:text-slate-100\">{creator}</span>\n </span>\n <div className=\"ml-auto flex items-center gap-1.5\">\n {/* 添付の有無を一覧で一目判別できるようにする。\n 仕様書が定める [sourceType] [Star] [Read] の相対順は崩さず、その手前に置く */}\n <DailyReportAttachmentIndicator count={report.attachments.length} label={fieldLabels.attachments} />\n {(() => {\n const sourceType = item.sourceType ?? report?.sourceType\n const sourceConfig = resolveSourceTypeConfig(sourceType, sourceTypeConfigs)\n 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\n })()}\n <button\n type=\"button\"\n title={report.isStarred ? \"スターを外す\" : \"スターを付ける\"}\n aria-label={report.isStarred ? \"スターを外す\" : \"スターを付ける\"}\n aria-pressed={report.isStarred}\n data-list-star-button={item.reportHubId}\n onClick={handleToggleStar}\n onPointerDown={stopPointerPropagation}\n onPointerUp={stopPointerPropagation}\n className=\"flex h-6 w-6 items-center justify-center rounded-full transition-colors hover:bg-slate-200 dark:hover:bg-slate-700\">\n {report.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\" />}\n </button>\n <button\n type=\"button\"\n title={report.isRead ? \"未読にする\" : \"既読にする\"}\n aria-label={report.isRead ? \"未読にする\" : \"既読にする\"}\n aria-pressed={report.isRead}\n data-list-read-button={item.reportHubId}\n onClick={handleToggleRead}\n onPointerDown={stopPointerPropagation}\n onPointerUp={stopPointerPropagation}\n className=\"flex h-6 w-6 items-center justify-center rounded-full transition-colors hover:bg-slate-200 dark:hover:bg-slate-700\">\n {report.isRead ? <Check className=\"h-3.5 w-3.5 text-blue-500\" /> : <UnreadIndicator className=\"text-xs\" />}\n </button>\n </div>\n </div>\n <div className=\"space-y-1 text-slate-700 text-sm leading-relaxed dark:text-slate-200\">\n {previewLines.length > 0 ? (\n previewLines.map(({ line, key }) => (\n <p key={key} className=\"wrap-break-word line-clamp-1\">\n {line}\n </p>\n ))\n ) : (\n <p>内容は登録されていません</p>\n )}\n </div>\n </div>\n </div>\n )\n})\nListReportItem.displayName = \"ListReportItem\"\n","/**\n * Responsive layout detection shared by the daily-report views.\n * 日報ビューが共有するレスポンシブ判定。\n *\n * 閾値を 1 箇所に集約する。各ビューが独自に `matchMedia` を持つと、片方だけ変更されて\n * レイアウト分岐が食い違う。\n *\n * ❗ **スクロール操作の可否をここで決めてはならない。** 画面幅で判定すると\n * 「主入力がマウスのタッチ対応ノート」のような端末を取りこぼす。ドラッグスクロールは\n * `ScrollPane` の `pointerDragInputs` で**イベントのポインタ種別ごとに**制御する\n * (`CONTENT_DRAG_POINTER_INPUTS` 参照)。\n */\nimport { useEffect, useState } from \"react\"\n\n/**\n * Viewport width at which the daily-report views switch to the mobile layout.\n * 日報ビューがモバイルレイアウトへ切り替わるビューポート幅。\n *\n * Tailwind の `sm` (640px) の直下に合わせてある。リスト + サイドペインの 2 カラムを\n * 畳んでオーバーレイ表示にする閾値であり、**この値は 1 箇所でだけ定義する**。\n */\nexport const MOBILE_LAYOUT_MEDIA_QUERY = \"(max-width: 639px)\"\n\n/**\n * Reports whether the viewport is narrow enough for the mobile layout.\n * ビューポートがモバイルレイアウトの幅かどうかを返す処理。\n *\n * SSR と初回描画では `false` を返し、マウント後に実環境で更新する\n * (サーバーとクライアントで初期値を揃えてハイドレーション不一致を避ける)。\n *\n * @returns True on the mobile layout. モバイルレイアウトなら true。\n */\nexport const useIsMobileLayout = (): boolean => {\n const [isMobileLayout, setIsMobileLayout] = useState(false)\n\n /**\n * Tracks the viewport width.\n * ビューポート幅を追跡する副作用。\n *\n * 目的: 画面回転・ウィンドウリサイズに追随する。\n * 依存関係: [] - 監視対象は固定の定数なので購読は 1 度だけ張る。\n * クリーンアップ: リスナーを解除する。\n */\n useEffect(() => {\n if (typeof window === \"undefined\" || typeof window.matchMedia !== \"function\") return\n\n const query = window.matchMedia(MOBILE_LAYOUT_MEDIA_QUERY)\n const sync = () => setIsMobileLayout(query.matches)\n\n sync()\n query.addEventListener(\"change\", sync)\n return () => query.removeEventListener(\"change\", sync)\n }, [])\n\n return isMobileLayout\n}\n","import * as TabsPrimitive from \"@radix-ui/react-tabs\"\nimport * as React from \"react\"\n\nimport { cn } from \"./cn\"\n\nconst Tabs = TabsPrimitive.Root\n\nconst TabsList = React.forwardRef<React.ElementRef<typeof TabsPrimitive.List>, React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>>(({ className, ...props }, ref) => (\n <TabsPrimitive.List ref={ref} className={cn(\"inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground\", className)} {...props} />\n))\nTabsList.displayName = TabsPrimitive.List.displayName\n\nconst TabsTrigger = React.forwardRef<React.ElementRef<typeof TabsPrimitive.Trigger>, React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>>(({ className, ...props }, ref) => (\n <TabsPrimitive.Trigger\n ref={ref}\n className={cn(\n \"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 font-medium text-sm ring-offset-background transition-all focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-2xs\",\n className,\n )}\n {...props}\n />\n))\nTabsTrigger.displayName = TabsPrimitive.Trigger.displayName\n\n// https://github.com/radix-ui/primitives/issues/2359\nconst TabsContent = React.forwardRef<React.ElementRef<typeof TabsPrimitive.Content>, React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>>(({ className, ...props }, ref) => (\n <TabsPrimitive.Content\n ref={ref}\n // forceMount={true} //<=======Add this line\n className={cn(\n \"data-[state=inactive]:hidden\", //<=========Add this line\n \"mt-2 ring-offset-background focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2\",\n className,\n )}\n {...props}\n />\n))\nTabsContent.displayName = TabsPrimitive.Content.displayName\n\nexport { Tabs, TabsContent, TabsList, TabsTrigger }\n","/**\n * Top-level daily-report page content (chrome-agnostic, stream-driven).\n * アプリのレイアウトに依存しない日報ページ本体コンポーネント (ストリーム駆動)。\n *\n * ID 一覧はモジュール常駐の NDJSON ストリームセッションから購読し、最初の\n * チャンク到着と同時に一覧を描画する (全件解決を待つ Await ゲートは廃止)。\n * 再訪時はセッションに残る受信済みデータで即座に復元し、鮮度はバックグラウンド\n * 再検証 (SWR) と SSE が担う。アプリ側のルートは本コンポーネントを自前の\n * レイアウトで包み、`config` でヘッダー描画・パス・ラベル名を注入する。\n */\nimport { useEffect } from \"react\"\nimport type { DailyReportUser } from \"../../shared/types\"\nimport { type DailyReportClientConfigInput, DailyReportConfigProvider, useDailyReportConfig } from \"../config-context\"\nimport { DailyReportActionProvider } from \"../contexts/daily-report-action-context\"\nimport { DailyReportErrorProvider } from \"../contexts/daily-report-error-context\"\nimport { ensureDailyReportIdsStreamSession, reloadDocument, retryDailyReportIdsStream, useDailyReportIdsStream } from \"../streaming/daily-report-ids-stream-session\"\nimport { DAILY_REPORT_FETCH_DEBOUNCE_MS } from \"../utils/constants\"\nimport { DailyReportIdsStreamStatus } from \"./daily-report-ids-stream-status\"\nimport { DailyReportResolvedContent } from \"./daily-report-resolved-content\"\n\n/**\n * Error display when the ids stream terminally failed before any data arrived.\n * データ未受信のまま ID ストリームが力尽きたときのエラー表示コンポーネント。\n *\n * 再試行はストリームセッションの retry へ直結する (受信済みカーソルから再開)。\n */\nexport const DailyReportLoadError = () => {\n const { title, renderHeader } = useDailyReportConfig()\n return (\n <>\n {renderHeader({ title, annotation: <span className=\"font-bold text-red-500\">Error</span> })}\n <div className=\"h-full bg-slate-50 p-2\">\n <div className=\"mx-auto flex h-full w-full max-w-6xl flex-1 flex-col gap-6\">\n <div className=\"flex h-full flex-1 flex-col items-center justify-center gap-4 rounded-lg border border-red-200 bg-white p-4 text-slate-500 shadow-sm dark:border-red-900 dark:bg-slate-900/70 dark:text-slate-400\">\n <p className=\"font-medium text-red-600\">日報データの読み込みに失敗しました。</p>\n <button\n type=\"button\"\n data-testid=\"daily-report-load-error-retry\"\n onClick={() => retryDailyReportIdsStream()}\n className=\"rounded bg-slate-100 px-4 py-2 text-slate-700 text-sm hover:bg-slate-200 dark:bg-slate-800 dark:text-slate-300 dark:hover:bg-slate-700\">\n 再読み込み\n </button>\n </div>\n </div>\n </div>\n </>\n )\n}\n\n/**\n * Loading screen shown only until the first stream chunk arrives.\n * 最初のストリームチャンクが届くまでの間だけ表示するローディング画面。\n */\nexport const DailyReportLoading = () => {\n const { title, renderHeader } = useDailyReportConfig()\n return (\n <>\n {renderHeader({\n title,\n annotation: (\n <div className=\"flex flex-wrap items-center gap-2 leading-none\">\n <DailyReportIdsStreamStatus />\n <span>総件数: 読み込み中...</span>\n <span>デバウンス: {DAILY_REPORT_FETCH_DEBOUNCE_MS} ms</span>\n </div>\n ),\n })}\n <div className=\"h-full bg-slate-50 p-2\">\n <div className=\"mx-auto flex h-full w-full max-w-6xl flex-1 flex-col gap-6\">\n <div className=\"flex h-full flex-1 items-center justify-center rounded-lg border border-slate-200 bg-white text-slate-500 shadow-sm dark:border-slate-700 dark:bg-slate-900/70 dark:text-slate-300\">日報 ID を読み込み中です...</div>\n </div>\n </div>\n </>\n )\n}\n\nexport type DailyReportPageProps = {\n /** 表示中ユーザー (楽観的コメントの作者名等に使用)。 */\n user?: DailyReportUser\n /** 難読化済みの内部ユーザー ID (未登録ユーザーは null)。 */\n userId?: string | null\n /** クライアント設定の部分上書き (ヘッダー描画・パス・ラベル名・フィールドラベルなど)。 */\n config?: DailyReportClientConfigInput\n}\n\n/**\n * Stream-driven page body: session bootstrap, phase gating, and provider wiring.\n * ストリーム駆動のページ本体。セッション確立・局面ゲーティング・プロバイダー結線を担う。\n */\nconst DailyReportPageInner = ({ user, userId }: Omit<DailyReportPageProps, \"config\">) => {\n const { apiBasePath } = useDailyReportConfig()\n const stream = useDailyReportIdsStream()\n\n /**\n * ストリームセッション確立の保険。\n * 通常は clientLoader が先に確立するが、clientLoader を経ないマウントや\n * ユーザー切替 (userKey 不一致でセッション再生成) をここで拾う。\n * 依存配列: apiBasePath / userId (いずれかの変化で再確認)。\n * クリーンアップ: なし (セッションはモジュール常駐で、アンマウント後も生存させる)。\n */\n useEffect(() => {\n ensureDailyReportIdsStreamSession({ apiBasePath, userKey: userId ?? null })\n }, [apiBasePath, userId])\n\n /**\n * 認証切れ (ストリームが 401/403) はドキュメントのフルリロードへ倒し、\n * サーバー loader のログインリダイレクトに委譲する。\n * 依存配列: stream.phase。クリーンアップ: なし。\n */\n useEffect(() => {\n if (stream.phase === \"auth-required\") {\n reloadDocument()\n }\n }, [stream.phase])\n\n // 1 件でも届いたら (または空リストで完走したら) 本体を描画する\n const showContent = stream.items.length > 0 || stream.phase === \"complete\"\n return (\n // key でユーザーごとにプロバイダーをリマウントする。リロード無しのユーザー切替\n // (再ログイン・成り代わり) で items / removedIds / 保留キュー等の状態が旧ユーザーの\n // まま残ると、render-phase マージが旧ユーザーの行を新ユーザーの一覧へ持ち越す\n // エラーサーフェスはユーザー切替のリマウント境界より外側 (バナーは切替を跨いで消えてよい)。\n // アクションプロバイダーはこの notifyError を購読して失敗を可視化する\n <DailyReportErrorProvider>\n <DailyReportActionProvider key={userId ?? \"anonymous\"} user={user} initialItems={stream.items} userId={userId}>\n {showContent ? <DailyReportResolvedContent userId={userId} /> : stream.phase === \"failed\" ? <DailyReportLoadError /> : <DailyReportLoading />}\n </DailyReportActionProvider>\n </DailyReportErrorProvider>\n )\n}\n\n/**\n * Chrome-agnostic daily report page with client configuration provider.\n * 設定プロバイダー込みの日報ページコンポーネント。\n */\nexport const DailyReportPage = ({ config, ...rest }: DailyReportPageProps) => (\n <DailyReportConfigProvider config={config}>\n <DailyReportPageInner {...rest} />\n </DailyReportConfigProvider>\n)\n","import { Plus, RefreshCw, RotateCw } from \"lucide-react\"\nimport type { ReactNode } from \"react\"\nimport { useEffect, useMemo, useRef, useState } from \"react\"\nimport { useSearchParams } from \"react-router\"\nimport { resolveTodayBusinessDateKey } from \"../../shared/business-date\"\nimport { useDailyReportConfig } from \"../config-context\"\nimport { useDailyReportActionContext } from \"../contexts/daily-report-action-context\"\nimport { useAnimatedNumber } from \"../hooks/use-animated-number\"\nimport { Button } from \"../ui/button\"\nimport { Label } from \"../ui/label\"\nimport { Switch } from \"../ui/switch\"\nimport { Tabs, TabsContent, TabsList, TabsTrigger } from \"../ui/tabs\"\nimport { DAILY_REPORT_FETCH_DEBOUNCE_MS } from \"../utils/constants\"\nimport { DailyReportDetailList, type DailyReportDetailListScrollRestore } from \"./daily-report-detail-list\"\nimport { DailyReportIdsStreamStatus } from \"./daily-report-ids-stream-status\"\nimport { DailyReportList } from \"./daily-report-list\"\n\nconst tabTriggerClass = \"min-w-24 p-2 font-medium text-slate-600 text-xs transition-colors data-[state=active]:bg-white data-[state=active]:text-slate-900 sm:min-w-36 sm:text-sm dark:text-slate-300 dark:data-[state=active]:bg-slate-800 dark:data-[state=active]:text-slate-100\"\n\ntype DailyReportResolvedContentProps = {\n userId?: string | null\n}\n\n/**\n * Displays streamed daily reports with layout and tabbed virtual scroll views.\n * レイアウトとタブ付き仮想スクロールでストリーム受信済み日報を表示するコンポーネント。\n *\n * アイテムは Action Context (ストリームチャンクをマージ済み) から読む。\n */\nexport const DailyReportResolvedContent = ({ userId }: DailyReportResolvedContentProps) => {\n // 自動既読設定\n const [autoMarkRead, setAutoMarkRead] = useState(false)\n const { title, renderHeader, showDevControls } = useDailyReportConfig()\n const { isSseEnabled, toggleSse, createReport, items: dailyReportItems, refetchData, clearCacheAndRefetch } = useDailyReportActionContext()\n const [searchParams] = useSearchParams()\n\n // 総件数の表示値 (チャンク到着ごとの跳びをイーズアウトで滑らかにする)\n const animatedItemCount = useAnimatedNumber(dailyReportItems.length)\n\n // 各タブのスクロール位置をアンマウント後も保持する共有スロット (子がマウント時に読み、\n // スクロールのたびに書き戻す)。❗ ref の「値」をここで読んで props へ渡してはならない:\n // タブは非制御でこのコンポーネントはタブ切替時に再レンダーされないため、レンダー時読取は\n // 古い値の焼き込みになる (タブ復帰でスクロール位置がリセットされた実害バグの根本原因)\n const listScrollOffsetRef = useRef<number>(0)\n const detailListScrollRestoreRef = useRef<DailyReportDetailListScrollRestore>({ topItem: null, anchorReportHubId: null, measuredHeights: new Map() })\n\n // 選択状態の管理 (タブ間で共有)\n const [selectedItemId, setSelectedItemId] = useState<number | null>(() => {\n if (dailyReportItems.length === 0) return null\n return dailyReportItems[0].reportHubId\n })\n\n // 初回の自動選択を一度きりにするフラグ (ユーザーの明示的な選択解除を上書きしない)\n const hasAutoSelectedRef = useRef(false)\n\n /**\n * 最初のアイテム到着時に先頭日報を一度だけ自動選択する副作用。\n * useState 初期化子はマウント時点で context の items がまだ空のため実質機能せず、\n * ストリーミングでは最初のチャンク到着後に選択を確定させる必要がある。\n * モバイルのオーバーレイは isMobileDetailOpen (明示タップ) でのみ開くため、\n * プログラム選択でオーバーレイが開くことはない。\n * 依存配列: dailyReportItems (チャンク到着ごと)。クリーンアップ: なし。\n */\n useEffect(() => {\n if (!hasAutoSelectedRef.current && dailyReportItems.length > 0) {\n hasAutoSelectedRef.current = true\n setSelectedItemId((prev) => prev ?? dailyReportItems[0].reportHubId)\n }\n }, [dailyReportItems])\n\n const handleReload = async () => {\n await refetchData()\n }\n\n const handleReloadClearCache = async () => {\n await clearCacheAndRefetch()\n }\n\n const handleCreateReport = async () => {\n const businessDateParam = searchParams.get(\"businessDate\")\n // 「本日」はローカル暦日で決める。toISOString() の UTC 日付だと JST 深夜〜午前帯に\n // 前日の営業日へ新規日報が登録されてしまう\n const dateToUse = businessDateParam || resolveTodayBusinessDateKey()\n const newId = await createReport(dateToUse)\n setSelectedItemId(newId)\n }\n\n /**\n * Effect to load the auto-read setting from local storage on mount.\n * マウント時にローカルストレージから自動既読設定を読み込む副作用。\n *\n * Purpose: To persist user preference for auto-read behavior.\n * Dependencies: [] - Runs only on mount.\n * Cleanup: None.\n */\n useEffect(() => {\n const stored = localStorage.getItem(\"daily-report-auto-read\")\n if (stored === \"true\") {\n setAutoMarkRead(true)\n }\n }, [])\n\n const handleAutoMarkReadChange = (checked: boolean) => {\n setAutoMarkRead(checked)\n localStorage.setItem(\"daily-report-auto-read\", String(checked))\n }\n\n const tabItems = useMemo<{ value: string; label: string; content: ReactNode }[]>(\n () => [\n {\n value: \"list\",\n label: \"📄 List\",\n content: <DailyReportList dailyReportItems={dailyReportItems} autoMarkRead={autoMarkRead} selectedReportHubId={selectedItemId} onSelectItem={setSelectedItemId} userId={userId} scrollOffsetRef={listScrollOffsetRef} />,\n },\n {\n value: \"tree\",\n label: \"🌳 Tree\",\n content: <div className=\"flex flex-1 items-center justify-center rounded-lg border border-slate-300 border-dashed bg-white p-6 text-slate-500 transition-colors dark:border-slate-700 dark:bg-slate-900/70 dark:text-slate-300\">Tree ビューは現在準備中です</div>,\n },\n {\n value: \"detailList\",\n label: \"📋 DetailList\",\n content: <DailyReportDetailList dailyReportItems={dailyReportItems} userId={userId} selectedItemId={selectedItemId} onSelectItem={setSelectedItemId} scrollRestoreRef={detailListScrollRestoreRef} />,\n },\n ],\n [dailyReportItems, autoMarkRead, selectedItemId, userId],\n )\n\n return (\n <>\n {renderHeader({\n title,\n annotation: (\n <div className=\"flex flex-wrap items-center gap-2 leading-1\">\n <DailyReportIdsStreamStatus />\n <span className=\"tabular-nums\">総件数: {animatedItemCount.toLocaleString()} 件</span>\n <span>デバウンス: {DAILY_REPORT_FETCH_DEBOUNCE_MS} ms</span>\n </div>\n ),\n })}\n <div className=\"h-full bg-slate-50 dark:bg-slate-950\">\n <div className=\"flex h-full w-full max-w-6xl flex-1 flex-col p-1\">\n <Tabs defaultValue=\"list\" className=\"flex h-full flex-1 flex-col\">\n <div className=\"flex items-center justify-between gap-2 rounded-md bg-slate-50 p-1 transition-colors dark:bg-slate-900/70 dark:text-slate-200\">\n <TabsList className=\"scrollbar-none flex w-full touch-pan-x flex-nowrap overflow-x-auto overflow-y-hidden bg-transparent p-0\">\n {tabItems.map(({ value, label }) => (\n <TabsTrigger key={value} value={value} className={tabTriggerClass}>\n {label}\n </TabsTrigger>\n ))}\n </TabsList>\n <div className=\"flex shrink-0 items-center space-x-1 px-2\">\n <Button variant=\"ghost\" size=\"icon\" onClick={handleCreateReport} title=\"日報を作成\">\n <Plus className=\"h-4 w-4\" />\n </Button>\n {showDevControls && (\n <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\">\n <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=\"開発環境限定のコントロールです\">\n DEV\n </span>\n <div className=\"flex items-center space-x-1\">\n <Switch id=\"sse-mode\" checked={isSseEnabled} onCheckedChange={toggleSse} className=\"scale-75\" />\n <Label htmlFor=\"sse-mode\" className=\"cursor-pointer text-slate-600 text-xs dark:text-slate-300\" title=\"開発環境のみ: Real-time SSE 購読切り替え\">\n 購読\n </Label>\n </div>\n <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=\"開発環境のみ: ページリロードなしでデータのみ再取得\">\n <RotateCw className=\"mr-1 h-3 w-3\" />\n リロード\n </Button>\n <Button\n variant=\"ghost\"\n size=\"sm\"\n onClick={handleReloadClearCache}\n className=\"h-6 px-1.5 text-amber-800 text-xs hover:bg-amber-200 dark:text-amber-300 dark:hover:bg-amber-800\"\n title=\"開発環境のみ: サーバー/DB & クライアントのキャッシュを破棄してデータ再取得\">\n <RefreshCw className=\"mr-1 h-3 w-3\" />\n キャッシュクリア\n </Button>\n </div>\n )}\n <Switch id=\"auto-read-mode\" checked={autoMarkRead} onCheckedChange={handleAutoMarkReadChange} className=\"scale-75\" />\n <Label htmlFor=\"auto-read-mode\" className=\"cursor-pointer text-slate-500 text-xs\">\n 自動既読\n </Label>\n </div>\n </div>\n {tabItems.map(({ value, content }) => (\n <TabsContent key={value} value={value} className=\"mt-0 flex h-full min-h-0 flex-1 flex-col\">\n {content}\n </TabsContent>\n ))}\n </Tabs>\n </div>\n </div>\n </>\n )\n}\n","import * as SwitchPrimitives from \"@radix-ui/react-switch\"\nimport * as React from \"react\"\n\nimport { cn } from \"./cn\"\n\nconst Switch = React.forwardRef<React.ElementRef<typeof SwitchPrimitives.Root>, React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>>(({ className, ...props }, ref) => (\n <SwitchPrimitives.Root\n className={cn(\n \"peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input\",\n className,\n )}\n {...props}\n ref={ref}>\n <SwitchPrimitives.Thumb className={cn(\"pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0\")} />\n </SwitchPrimitives.Root>\n))\nSwitch.displayName = SwitchPrimitives.Root.displayName\n\nexport { Switch }\n","/**\n * React Router route helpers for the daily-report page (revalidation policy / client loader).\n * 日報ページ向け React Router ルートヘルパー (再検証ポリシー / クライアントローダー)。\n */\nimport type { ShouldRevalidateFunction, ShouldRevalidateFunctionArgs } from \"react-router\"\nimport { bootstrapDailyReportIdsStreamSession, primeDailyReportIdsStreamSession } from \"./streaming/daily-report-ids-stream-session\"\n\n/**\n * Prevents loader revalidation when POP navigations keep identical URL state.\n * URL 状態が同一の POP ナビゲーション時にローダー再検証を抑止。\n */\nexport const dailyReportShouldRevalidate: ShouldRevalidateFunction = ({ actionResult, actionStatus, currentUrl, defaultShouldRevalidate, formAction, formData, formEncType, formMethod, json, nextUrl, text }: ShouldRevalidateFunctionArgs) => {\n if (!defaultShouldRevalidate) {\n return false\n }\n\n const isSameUrl = currentUrl.pathname === nextUrl.pathname && currentUrl.search === nextUrl.search && currentUrl.hash === nextUrl.hash\n if (!isSameUrl) {\n return defaultShouldRevalidate\n }\n\n if (formData) {\n const intent = formData.get(\"intent\")\n if (intent === \"toggleRead\" || intent === \"toggleStar\" || intent === \"addComment\" || intent === \"deleteComment\" || intent === \"create\" || intent === \"update\" || intent === \"publish\" || intent === \"delete\") {\n return false\n }\n }\n\n const hasSubmission = actionResult !== undefined || actionStatus !== undefined || formAction !== undefined || formData !== undefined || formEncType !== undefined || formMethod !== undefined || json !== undefined || text !== undefined\n return hasSubmission ? defaultShouldRevalidate : false\n}\n\n/**\n * Creates a React Router clientLoader that bootstraps the ids stream session.\n * ID ストリームセッションを起動する React Router clientLoader を生成する処理。\n *\n * ID 一覧は Promise で返さず、モジュール常駐のストリームセッションが供給する\n * (ページ側は `useDailyReportIdsStream` で購読する)。ローダー時点でセッションを\n * 生成することが**コールドロードの最重要最適化** — ページマウントの effect まで\n * 遅らせると、ストリームの開始が「ハイドレーション完了 + effect flush」の後ろに\n * 直列化される。再訪 (セッション既存) は serverLoader を待たずに鮮度確認を先行させる。\n *\n * `config.apiBasePath` を上書きするアプリは同じ値を `options.apiBasePath` にも渡すこと\n * (渡し忘れは初回 1 回だけ誤パス生成 → ページマウントの ensure が自己修復する)。\n *\n * 返す関数へ `hydrate = true as const` を付与済み (初期ハイドレーションでも実行される)。\n */\nexport const createDailyReportClientLoader = <TServerData extends Record<string, unknown>>(options?: { apiBasePath?: string }) => {\n const clientLoader = async (args: { serverLoader: () => Promise<unknown> }): Promise<TServerData> => {\n // 再訪の主経路: 既存セッションの鮮度確認を serverLoader より先に始める\n primeDailyReportIdsStreamSession()\n const serverData = (await args.serverLoader()) as TServerData\n // 不在なら生成 (コールドロードでハイドレーションと並行してフェッチを開始)、\n // 既存なら userKey 照合 (別ユーザーは描画前に破棄・再生成) + 鮮度確認\n const rawUserId = (serverData as { userId?: unknown }).userId\n bootstrapDailyReportIdsStreamSession({ apiBasePath: options?.apiBasePath, userKey: typeof rawUserId === \"string\" ? rawUserId : null })\n return serverData\n }\n clientLoader.hydrate = true as const\n return clientLoader\n}\n"],"mappings":"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","names":["client_exports","__export","BusinessDayThumbOverlay","CONTENT_DRAG_POINTER_INPUTS","DAILY_REPORT_FETCH_DEBOUNCE_MS","DEFAULT_ITEM_HEIGHT","DailyReportActionProvider","DailyReportAttachmentIndicator","DailyReportAttachmentList","DailyReportCache","DailyReportCommentForm","DailyReportCommentItem","DailyReportCommentList","DailyReportConfigProvider","DailyReportDetailList","DailyReportEditForm","DailyReportErrorProvider","DailyReportIdsStreamClient","DailyReportIdsStreamStatus","DailyReportList","DailyReportLoadError","DailyReportLoading","DailyReportPage","DailyReportResolvedContent","EMPTY_DETAIL","EMPTY_ITEM","FAST_SCROLL_INERTIA_OPTIONS","FAST_SCROLL_WHEEL_MULTIPLIER","ITEM_CONTAINER_HEIGHT","ITEM_VERTICAL_PADDING","MAX_PREVIEW_LINES","PREFETCH_LOOKAHEAD","PREFETCH_MAX_DATES","SCROLL_BAR_WIDTH","SSE_INITIAL_CONNECT_DEFER_MS","TAP_SCROLL_CIRCLE_OPTIONS","UnreadIndicator","VIRTUAL_SCROLL_OVERSCAN_COUNT","WHEEL_RESET_TIMEOUT_MS","acquireMutationLock","applyBroadcastReportUpdate","applyDailyReportServerUpdates","bootstrapDailyReportIdsStreamSession","clearDailyReportCache","createDailyReportClientLoader","dailyReportShouldRevalidate","defaultDailyReportClientConfig","defaultDailyReportFieldLabels","deleteDailyReportCache","ensureDailyReportIdsStreamSession","getCachedReport","getDailyReportIdsStreamSessionUserKey","primeDailyReportIdsStreamSession","refreshDailyReportIdsStream","registerRecentDeletion","releaseMutationLock","reloadDocument","removeDailyReportIdsStreamItem","resetDailyReportIdsStreamSessionForTesting","resolveSourceTypeConfig","retryDailyReportIdsStream","subscribeCacheReady","updateDailyReportCache","upsertDailyReportIdsStreamItem","useAnimatedNumber","useDailyReportActionContext","useDailyReportComments","useDailyReportConfig","useDailyReportDetail","useDailyReportErrorSurface","useDailyReportIdsStream","useDailyReportPrefetch","useDailyReportSseConnection","useDynamicViewportHeight","writeCache","__toCommonJS","import_jsx_runtime","BusinessDayThumbOverlay","orientation","thumbCenter","label","isDragging","isTapScrollActive","isWheelScrollActive","import_lucide_react","import_jsx_runtime","COUNT_VISIBLE_FROM","DailyReportAttachmentIndicator","count","label","import_react","import_jsx_runtime","defaultDailyReportFieldLabels","defaultDailyReportClientConfig","title","annotation","DailyReportConfigContext","DailyReportConfigProvider","config","children","merged","useDailyReportConfig","resolveSourceTypeConfig","sourceType","sourceTypeConfigs","lower","matchedKey","k","import_jsx_runtime","BYTES_PER_UNIT","SIZE_UNITS","formatFileSize","bytes","value","unitIndex","DailyReportAttachmentList","attachments","apiBasePath","fieldLabels","useDailyReportConfig","attachment","href","size","isMissing","import_outline","import_react","mergeComments","legacyComments","modernComments","currentUserId","legacyUIComments","c","index","modernUIComments","resolveCommentColorClass","color","normalized","fallbackText","value","trimmed","formatToIsoDate","dateOnly","date","year","month","day","hours","minutes","seconds","import_clsx","import_tailwind_merge","cn","inputs","import_jsx_runtime","checkIsPendingDelete","commentId","pendingDeleteId","resolvedIdMap","DailyReportCommentItem","comment","_reportHubId","_businessDate","onOptimisticDelete","onPendingDeleteIdChange","onTrashClick","isDeleted","setIsDeleted","isPendingDelete","handleConfirmDelete","e","cn","resolveCommentColorClass","fallbackText","formatToIsoDate","import_react","import_jsx_runtime","useAutoResetPendingDelete","pendingDeleteId","onPendingDeleteIdChange","timerRef","handleGlobalClick","e","commentId","DailyReportCommentList","comments","reportHubId","businessDate","onDeleteComment","resolvedIdMap","handleTrashClick","comment","DailyReportCommentItem","DailyReportCommentForm","onAddComment","isSubmitting","setIsSubmitting","formRef","content","input","import_virtualscroll","import_lucide_react","import_react","import_react","isIdsStreamChunkLine","line","compareIdsStreamOrder","a","b","isIdsStreamOrdered","items","i","upsertInIdsStreamOrder","item","next","existing","low","high","mid","createLogger","level","prefix","impl","format","message","rest","import_react","ISO_DATE_PATTERN","SLASH_DATE_PATTERN","zeroPad","value","normalizeFromDate","year","month","day","resolveTodayBusinessDateKey","now","normalizeBusinessDateKey","trimmed","parsed","import_virtualscroll","DAILY_REPORT_FETCH_DEBOUNCE_MS","SSE_INITIAL_CONNECT_DEFER_MS","DEFAULT_ITEM_HEIGHT","ITEM_VERTICAL_PADDING","ITEM_CONTAINER_HEIGHT","FAST_SCROLL_WHEEL_MULTIPLIER","FAST_SCROLL_INERTIA_OPTIONS","PREFETCH_LOOKAHEAD","PREFETCH_MAX_DATES","EMPTY_ITEM","EMPTY_DETAIL","WHEEL_RESET_TIMEOUT_MS","VIRTUAL_SCROLL_OVERSCAN_COUNT","SCROLL_BAR_WIDTH","MAX_PREVIEW_LINES","TAP_SCROLL_CIRCLE_OPTIONS","CONTENT_DRAG_POINTER_INPUTS","logger","createLogger","getGlobal","k","d","v","TTL","REVIVE_MS","DailyReportCache","now","GC_THRESHOLD","id","entry","key","listener","ex","l","ignoreExpiry","e","dateKey","report","expiresAt","syncList","normalizeBusinessDateKey","listEntry","data","r","updates","strict","date","list","serverReport","cachedReport","c","serverIds","missing","b","reports","fetchStart","t","processed","lastMut","cached","merged","writeCache","exp","sync","getCachedReport","opts","updateDailyReportCache","u","deleteDailyReportCache","clearDailyReportCache","acquireMutationLock","releaseMutationLock","registerRecentDeletion","cId","subscribeCacheReady","reportHubId","callback","applyDailyReportServerUpdates","_reportHubId","applyBroadcastReportUpdate","cachedMine","commentItems","fetchReportById","apiBasePath","pending","start","p","res","fetchReportsByDate","useDailyReportDetail","businessDate","useDailyReportConfig","setReport","error","setError","isLoading","setIsLoading","isRefetching","setIsRefetching","reqRef","onUpdate","uid","token","isStale","found","timer","item","DAILY_REPORT_FETCH_DEBOUNCE_MS","useDailyReportPrefetch","import_react","import_zod","dailyReportInterviewerSchema","dailyReportCommentSchema","dailyReportLabelDefSchema","dailyReportCommentItemSchema","dailyReportAttachmentItemSchema","dailyReportDetailSchema","connectedMessageSchema","statusUpdateMessageSchema","commentAddMessageSchema","commentDeleteMessageSchema","reportCreateMessageSchema","reportUpdateMessageSchema","reportPublishMessageSchema","reportDeleteMessageSchema","dailyReportSseMessageSchema","logger","createLogger","useDailyReportSseConnection","isSseEnabled","onMessage","ssePath","useDailyReportConfig","eventSource","reconnectTimer","reconnectAttempt","lastReceivedEventId","connect","event","raw","result","dailyReportSseMessageSchema","e","delayMs","import_react","DEFAULT_PUBLISH_COALESCE_MS","INITIAL_RETRY_DELAY_MS","MAX_RETRY_DELAY_MS","MAX_CONSECUTIVE_FAILURES","DEFAULT_READ_STALL_TIMEOUT_MS","READ_STALL","DEFAULT_API_BASE_PATH","CURSOR_DATE_PATTERN","DailyReportIdsStreamClient","options","input","init","handler","ms","id","args","listener","item","existing","next","reportHubId","mode","generation","outcome","seen","forceRefresh","abortController","fetchPromise","headerTimer","response","resolve","reader","decoder","partialLine","sawErrorLine","sawIsLast","chunk","done","value","lines","mutated","rawLine","parsed","isIdsStreamChunkLine","error","stallTimer","stallPromise","items","elapsed","partial","params","query","delayMs","timerId","finish","import_meta","REVALIDATE_AFTER_MS","session","sessionListeners","IDLE_STATE","reloadDocument","notifySessionListeners","listener","destroySession","ensureDailyReportIdsStreamSession","options","apiBasePath","defaultDailyReportClientConfig","clearDailyReportCache","client","DailyReportIdsStreamClient","runFreshnessCheck","record","state","bootstrapDailyReportIdsStreamSession","primeDailyReportIdsStreamSession","upsertDailyReportIdsStreamItem","item","removeDailyReportIdsStreamItem","reportHubId","retryDailyReportIdsStream","refreshDailyReportIdsStream","getDailyReportIdsStreamSessionUserKey","subscribeSession","getSessionSnapshot","getServerSnapshot","useDailyReportIdsStream","resetDailyReportIdsStreamSessionForTesting","import_react","import_jsx_runtime","ERROR_BANNER_AUTO_DISMISS_MS","DailyReportErrorContext","useDailyReportErrorSurface","DailyReportErrorProvider","children","onError","useDailyReportConfig","banner","setBanner","onErrorRef","dismissTimerRef","notifyError","info","host","dismiss","value","import_jsx_runtime","logger","createLogger","OPERATION_LABELS","describeOperationFailure","intent","EMPTY_INITIAL_ITEMS","DailyReportActionContext","useDailyReportActionContext","context","getUserDisplayName","user","fullName","DailyReportActionProvider","children","initialItems","userId","apiBasePath","draftLabelName","useDailyReportConfig","notifyError","useDailyReportErrorSurface","notifyErrorRef","pendingAddCommentIds","pendingDeleteCommentIds","pendingStarUpdates","pendingReadUpdates","resolvedIdMap","version","setVersion","items","setItems","isSseEnabled","setIsSseEnabled","hasManualSseToggleRef","pendingAddPromises","manuallyTrackingIds","removedIds","processedClientTempIds","streamState","useDailyReportIdsStream","isStreamSettledRef","isSessionStillOurs","getDailyReportIdsStreamSessionUserKey","refetchData","refreshDailyReportIdsStream","v","DailyReportCache","listener","clearCacheAndRefetch","clearDailyReportCache","formData","res","errData","e","pendingSseQueue","handleSseMessageRef","enqueuePendingSseMessage","reportHubId","message","queue","existing","entry","subscribeCacheReady","getCachedReport","msg","addProcessedId","id","isProcessedId","upsertItem","businessDate","upsertDailyReportIdsStreamItem","prev","item","nextBusinessDate","upsertInIdsStreamOrder","toggleSse","timer","SSE_INITIAL_CONNECT_DEFER_MS","removeStaleItem","removeDailyReportIdsStreamItem","prevInitialItems","setPrevInitialItems","mergedMap","merged","isIdsStreamOrdered","compareIdsStreamOrder","initialIds","handleSseMessage","payload","comment","cached","c","isMyComment","pendingMap","tempId","content","newComment","updatedReport","a","b","applyDailyReportServerUpdates","acquireMutationLock","releaseMutationLock","commentId","statusType","value","pendingEntry","deleteDailyReportCache","report","applyBroadcastReportUpdate","useDailyReportSseConnection","executeAction","params","callbacks","options","clientTempId","k","error","data","addComment","promise","realId","cachedReport","updateDailyReportCache","deleteComment","resolved","deletedId","registerRecentDeletion","toggleStar","isStarred","toggleReadImpl","isRead","toggleReadRef","toggleRead","createReport","now","dummyReport","writeCache","newId","updateReport","publishReport","oldLabels","newLabels","l","deleteReport","targetItem","i","_data","import_react","useDailyReportComments","report","user","userId","pendingAddCommentIds","pendingDeleteCommentIds","resolvedIdMap","version","addComment","deleteComment","useDailyReportActionContext","pendingDeleteId","setPendingDeleteId","handleDeleteComment","cId","handleAddComment","content","prevUiCommentsRef","comments","mergeComments","deletedIds","c","addedComments","existingIds","authorName","tempId","realId","import_react","useDynamicViewportHeight","containerRef","options","enableMutationObserver","bottomGap","minFallbackHeight","viewportHeight","setViewportHeight","updateViewportHeight","target","toNumber","value","containerTop","parentRect","rootStyle","footerHeight","headerHeight","scrollbarHeight","containerStyle","marginBottom","paddingBottom","viewportBottom","effectiveBottom","layoutTop","effectiveTop","availableHeight","safeAvailableHeight","clampedHeight","previous","frameId","timeoutId","resizeObserver","mutationObserver","scheduleUpdate","containerElement","import_react","import_react_slot","import_class_variance_authority","React","import_jsx_runtime","buttonVariants","Button","className","variant","size","asChild","props","ref","cn","import_jsx_runtime","Input","className","type","props","cn","LabelPrimitive","import_jsx_runtime","Label","className","props","cn","import_jsx_runtime","Textarea","className","props","cn","import_jsx_runtime","DailyReportEditForm","report","onCancel","onPublishSuccess","updateReport","publishReport","useDailyReportActionContext","title","setTitle","content","setContent","isSaving","setIsSaving","syncedReportIdRef","handleSave","handlePublish","Label","Input","e","Textarea","Button","import_jsx_runtime","UnreadIndicator","className","cn","import_jsx_runtime","useReportItemHeightObserver","ref","itemId","onHeightMeasured","element","resizeObserver","DailyReportDetailRow","index","setSize","children","elementRef","formatInterviewerLabel","interviewer","DailyReportDetailList","dailyReportItems","userId","selectedItemId","onSelectItem","scrollRestoreRef","initialScrollAnchor","isRestoredMount","restore","foundIndex","item","skipInitialScrollToSelectionRef","mountSelectionIdRef","itemHeightsRef","scrollContainerRef","virtualScrollRef","title","useDailyReportConfig","viewportHeight","updateViewportHeight","useDynamicViewportHeight","prefetchDailyReportByDate","useDailyReportPrefetch","overlayLabel","setOverlayLabel","isWheelScrollActive","setIsWheelScrollActive","wheelResetTimerRef","scrollEndTimerRef","dailyReportItemsRef","lastScrolledRef","onSelectItemRef","el","handle","extendedHandle","id","itemCount","getItem","EMPTY_ITEM","schedulePrefetchForRange","fromIndex","toIndex","boundedFrom","boundedTo","businessDates","candidate","PREFETCH_MAX_DATES","date","handleRangeChange","renderingStartIndex","renderingEndIndex","visibleStartIndex","firstVisibleItem","formatToIsoDate","PREFETCH_LOOKAHEAD","lastVirtualScrollHandleRef","handleScroll","anchor","container","renderThumbOverlay","props","BusinessDayThumbOverlay","onWheel","WHEEL_RESET_TIMEOUT_MS","updateItemHeight","height","getItemHeight","getItemKey","handleCommentAdded","renderItem","DailyReportItemContainer","VIRTUAL_SCROLL_OVERSCAN_COUNT","SCROLL_BAR_WIDTH","TAP_SCROLL_CIRCLE_OPTIONS","CONTENT_DRAG_POINTER_INPUTS","FAST_SCROLL_WHEEL_MULTIPLIER","FAST_SCROLL_INERTIA_OPTIONS","SkeletonReportItem","onCommentAdded","report","error","isLoading","isRefetching","useDailyReportDetail","removeStaleItem","useDailyReportActionContext","draftLabelName","isEditing","setIsEditing","hasBypassedAutoEdit","setHasBypassedAutoEdit","autoEditingRef","hasEverFetched","isDraft","l","isMine","DailyReportError","DailyReportEditForm","DailyReportResolvedItem","DailyReportHeader","isRead","isStarred","onToggleRead","onToggleStar","onDelete","itemSourceType","visitTimeDisplay","categoryLabel","fieldLabels","sourceTypeConfigs","DailyReportAttachmentIndicator","sourceType","sourceConfig","resolveSourceTypeConfig","cn","UnreadIndicator","fallbackText","MetadataPanel","onDoubleClick","formContainerRef","toggleStar","toggleRead","pendingStarUpdates","pendingReadUpdates","deleteReport","user","uiComments","handleAddComment","handleDeleteComment","pendingDeleteId","setPendingDeleteId","resolvedIdMap","useDailyReportComments","isExternalSource","canComment","handleAddCommentWrapper","content","handleToggleRead","handleToggleStar","handleDelete","normalizedContent","from","to","labels","DailyReportAttachmentList","DailyReportCommentList","DailyReportCommentForm","import_lucide_react","import_react","useAnimatedNumber","value","durationMs","display","setDisplay","displayRef","isFirstRef","from","rafId","startTimestamp","step","timestamp","progress","eased","next","import_jsx_runtime","DailyReportIdsStreamStatus","stream","useDailyReportIdsStream","animatedLoaded","useAnimatedNumber","animatedTotal","percent","retryDailyReportIdsStream","import_swipe_overlay","import_virtualscroll","import_lucide_react","import_react","import_react","MOBILE_LAYOUT_MEDIA_QUERY","useIsMobileLayout","isMobileLayout","setIsMobileLayout","query","sync","TabsPrimitive","React","import_jsx_runtime","Tabs","TabsList","className","props","ref","cn","TabsTrigger","TabsContent","import_jsx_runtime","useMobileOverlayManager","isMobileLayout","selectedReportHubId","dailyReportItems","onSelectItem","isMobileDetailOpen","setIsMobileDetailOpen","isOverlayMounted","setIsOverlayMounted","overlayHistoryKeyRef","lastClosedItemIdRef","bodyOverflowRef","selectedItem","e","shouldShowOverlay","timer","key","onPopState","handleOverlayClose","useAutoRead","report","isLoading","autoMarkRead","isActive","toggleRead","ignoreAutoReadForReportHubId","lastReportHubIdRef","handleToggleRead","isAuto","nextValue","DailyReportList","userId","scrollOffsetRef","initialScrollOffset","scrollContainerRef","virtualScrollRef","title","useDailyReportConfig","viewportHeight","updateViewportHeight","useDynamicViewportHeight","overlayLabel","setOverlayLabel","isWheelScrollActive","setIsWheelScrollActive","useIsMobileLayout","wheelResetTimerRef","itemCount","scrollEndTimerRef","columnHeightStyle","onResize","el","onWheel","WHEEL_RESET_TIMEOUT_MS","targetId","index","item","prev","next","formatToIsoDate","dailyReportItemsRef","id","limit","handleItemSelect","getItem","EMPTY_ITEM","getItemHeight","DEFAULT_ITEM_HEIGHT","renderListRow","ListReportItem","handleRangeChange","range","handleScroll","position","container","renderThumbOverlay","props","BusinessDayThumbOverlay","VIRTUAL_SCROLL_OVERSCAN_COUNT","SCROLL_BAR_WIDTH","TAP_SCROLL_CIRCLE_OPTIONS","CONTENT_DRAG_POINTER_INPUTS","FAST_SCROLL_INERTIA_OPTIONS","FAST_SCROLL_WHEEL_MULTIPLIER","ITEM_VERTICAL_PADDING","DailyReportSidePane","DailyReportMobileOverlay","ListSkeletonItem","wrapperProps","ITEM_CONTAINER_HEIGHT","heightStyle","activeTab","setActiveTab","isEditing","setIsEditing","draftLabelName","fieldLabels","sourceTypeConfigs","error","useDailyReportDetail","lastReportIdRef","autoEditRef","isDraft","l","isMine","scrollToBottom","headerDate","headerCreator","fallbackText","showHeaderSkeleton","content","sourceType","sourceConfig","cn","Tabs","v","TabsList","tab","TabsTrigger","TabsContent","DailyReportEditForm","DailyReportDisplayPane","SkeletonText","className","onCommentAdded","onDoubleClick","toggleStar","deleteReport","user","useDailyReportActionContext","uiComments","handleAddComment","handleDeleteComment","pendingDeleteId","setPendingDeleteId","resolvedIdMap","useDailyReportComments","resolveSourceTypeConfig","isExternalSource","canComment","prevReportHubIdRef","prevCommentsLengthRef","currentReportHubId","handleAddCommentWrapper","displayContent","handleToggleStar","handleDelete","showSkeleton","isStarred","isRead","hasComments","UnreadIndicator","DailyReportAttachmentList","DailyReportCommentList","DailyReportCommentForm","isOpen","onClose","containerRef","closeButtonRef","isGuardBlocked","requestBlock","resetGuard","displayData","displayItem","EMPTY_DETAIL","overlayRef","overlayStyle","attemptClose","isSwiping","tapHandlers","extractPreviewLines","MAX_PREVIEW_LINES","setSize","onSelect","removeStaleItem","isRefetching","listItemProps","hasEverFetched","previewLines","line","i","creator","date","stopPointerPropagation","DailyReportAttachmentIndicator","import_react","import_lucide_react","import_react","import_react_router","SwitchPrimitives","React","import_jsx_runtime","Switch","className","props","ref","cn","import_jsx_runtime","tabTriggerClass","DailyReportResolvedContent","userId","autoMarkRead","setAutoMarkRead","title","renderHeader","showDevControls","useDailyReportConfig","isSseEnabled","toggleSse","createReport","dailyReportItems","refetchData","clearCacheAndRefetch","useDailyReportActionContext","searchParams","animatedItemCount","useAnimatedNumber","listScrollOffsetRef","detailListScrollRestoreRef","selectedItemId","setSelectedItemId","hasAutoSelectedRef","prev","handleReload","handleReloadClearCache","handleCreateReport","dateToUse","resolveTodayBusinessDateKey","newId","handleAutoMarkReadChange","checked","tabItems","DailyReportList","DailyReportDetailList","DailyReportIdsStreamStatus","DAILY_REPORT_FETCH_DEBOUNCE_MS","Tabs","TabsList","value","label","TabsTrigger","Button","Switch","Label","content","TabsContent","import_jsx_runtime","DailyReportLoadError","title","renderHeader","useDailyReportConfig","retryDailyReportIdsStream","DailyReportLoading","DailyReportIdsStreamStatus","DAILY_REPORT_FETCH_DEBOUNCE_MS","DailyReportPageInner","user","userId","apiBasePath","stream","useDailyReportIdsStream","ensureDailyReportIdsStreamSession","reloadDocument","showContent","DailyReportErrorProvider","DailyReportActionProvider","DailyReportResolvedContent","DailyReportPage","config","rest","DailyReportConfigProvider","dailyReportShouldRevalidate","actionResult","actionStatus","currentUrl","defaultShouldRevalidate","formAction","formData","formEncType","formMethod","json","nextUrl","text","intent","createDailyReportClientLoader","options","clientLoader","args","primeDailyReportIdsStreamSession","serverData","rawUserId","bootstrapDailyReportIdsStreamSession"]}
|
|
1
|
+
{"version":3,"sources":["../src/client.ts","../src/client/components/business-day-thumb-overlay.tsx","../src/client/components/daily-report-attachment-indicator.tsx","../src/client/config-context.tsx","../src/client/components/daily-report-attachment-list.tsx","../src/client/components/daily-report-comment-item.tsx","../src/shared/comment-adapter.ts","../src/shared/text-utils.ts","../src/client/ui/cn.ts","../src/client/components/daily-report-comment-section.tsx","../src/client/components/daily-report-detail-list.tsx","../src/client/contexts/daily-report-action-context.tsx","../src/shared/ids-stream.ts","../src/shared/logger.ts","../src/client/hooks/use-daily-report.ts","../src/shared/business-date.ts","../src/client/utils/constants.ts","../src/client/hooks/use-daily-report-sse-connection.ts","../src/shared/sse-schema.ts","../src/client/streaming/daily-report-ids-stream-session.ts","../src/client/streaming/daily-report-ids-stream-client.ts","../src/client/contexts/daily-report-error-context.tsx","../src/client/hooks/use-daily-report-comments.ts","../src/client/hooks/use-dynamic-viewport-height.ts","../src/client/components/daily-report-edit-form.tsx","../src/client/ui/button.tsx","../src/client/ui/input.tsx","../src/client/ui/label.tsx","../src/client/ui/textarea.tsx","../src/client/components/unread-indicator.tsx","../src/client/components/daily-report-ids-stream-status.tsx","../src/client/hooks/use-animated-number.ts","../src/client/components/daily-report-list.tsx","../src/client/hooks/use-responsive-layout.ts","../src/client/ui/tabs.tsx","../src/client/components/daily-report-page.tsx","../src/client/components/daily-report-resolved-content.tsx","../src/client/ui/switch.tsx","../src/client/route-helpers.ts"],"sourcesContent":["/**\n * Client entry of @aiquants/daily-report: React components, hooks, and route helpers.\n * @aiquants/daily-report の client エントリ。React コンポーネント・フック・ルートヘルパーを公開。\n */\nexport * from \"./client/components/business-day-thumb-overlay\"\nexport * from \"./client/components/daily-report-attachment-indicator\"\nexport * from \"./client/components/daily-report-attachment-list\"\nexport * from \"./client/components/daily-report-comment-item\"\nexport * from \"./client/components/daily-report-comment-section\"\nexport * from \"./client/components/daily-report-detail-list\"\nexport * from \"./client/components/daily-report-edit-form\"\nexport * from \"./client/components/daily-report-ids-stream-status\"\nexport * from \"./client/components/daily-report-list\"\nexport * from \"./client/components/daily-report-page\"\nexport * from \"./client/components/daily-report-resolved-content\"\nexport * from \"./client/components/unread-indicator\"\nexport * from \"./client/config-context\"\nexport * from \"./client/contexts/daily-report-action-context\"\nexport * from \"./client/contexts/daily-report-error-context\"\nexport * from \"./client/hooks/use-animated-number\"\nexport * from \"./client/hooks/use-daily-report\"\nexport * from \"./client/hooks/use-daily-report-comments\"\nexport * from \"./client/hooks/use-daily-report-sse-connection\"\nexport * from \"./client/hooks/use-dynamic-viewport-height\"\nexport * from \"./client/route-helpers\"\nexport * from \"./client/streaming/daily-report-ids-stream-client\"\nexport * from \"./client/streaming/daily-report-ids-stream-session\"\nexport * from \"./client/utils/constants\"\n","import type { ScrollBarThumbOverlayRenderProps } from \"@aiquants/virtualscroll\"\nimport type { CSSProperties } from \"react\"\n\n/**\n * Tooltip bubble that anchors to the scrollbar thumb while interacting.\n * スクロール操作中にスクロールバーのサムへ連動表示するツールチップのコンポーネント。\n */\nexport const BusinessDayThumbOverlay = ({ orientation, thumbCenter, label, isDragging, isTapScrollActive, isWheelScrollActive }: ScrollBarThumbOverlayRenderProps & { label: string; isWheelScrollActive: boolean }) => {\n if (!(label.trim() && (isDragging || isTapScrollActive || isWheelScrollActive))) return null\n const style: CSSProperties = orientation === \"vertical\" ? { top: thumbCenter, left: -16, transform: \"translate(-100%, -50%)\" } : { left: thumbCenter, top: -16, transform: \"translate(-50%, -100%)\" }\n\n return (\n <div className=\"pointer-events-none absolute flex items-center\" style={style}>\n <div className=\"rounded-full border border-slate-200 bg-white px-3 py-1 text-slate-700 text-xs shadow-md shadow-slate-400/30 dark:border-slate-700 dark:bg-slate-900/90 dark:text-slate-100\">\n <span className=\"whitespace-nowrap font-semibold text-slate-800 dark:text-slate-100\">{label.trim()}</span>\n </div>\n </div>\n )\n}\n","/**\n * Compact \"has attachments\" marker for summary rows.\n * 一覧の要約行に出す「添付あり」の小さな目印。\n *\n * 一覧をスクロールしながら添付の有無を一目で判断できるようにするためのもの。\n * 詳細カードの添付節 (`DailyReportAttachmentList`) とは役割が異なり、\n * **ファイル名も配信 URL も出さない**。行に出すのは有無と件数だけであり、\n * リンクを置くと一覧のタップ領域と競合するうえ、配信エンドポイントを\n * 一覧から誘発しやすくなる (原寸をサーバーのヒープへ載せる経路である)。\n *\n * DOM フックは `data-*` 属性のみを使う (AGENTS.md)。\n */\nimport { Paperclip } from \"lucide-react\"\n\n/** 件数を併記し始める閾値。1 件のときは記号だけで足りる。 */\nconst COUNT_VISIBLE_FROM = 2\n\ntype DailyReportAttachmentIndicatorProps = {\n /** 添付の件数。0 なら何も描画しない。 */\n count: number\n /** 読み上げ・ツールチップ用の見出し。消費アプリの `fieldLabels.attachments` を渡す。 */\n label: string\n}\n\n/**\n * Renders the attachment marker, or nothing when the report has no attachment.\n * 添付の目印を描画する (添付が無ければ何も描画しない)。\n *\n * @param props Attachment count and the label. 添付件数と見出し。\n * @returns The marker element, or null. 目印の要素 (添付が無ければ null)。\n */\nexport const DailyReportAttachmentIndicator = ({ count, label }: DailyReportAttachmentIndicatorProps) => {\n if (count <= 0) {\n return null\n }\n\n // role=\"img\" を付けるのは aria-label を有効にするため (role の無い span では無効になる)。\n // 中身は記号 1 つで、読み上げ上も 1 つの図像として扱うのが適切。\n return (\n <span className=\"inline-flex shrink-0 items-center gap-0.5 rounded px-1 py-0.5 text-slate-500 dark:text-slate-400\" title={`${label}: ${count}`} role=\"img\" aria-label={`${label}: ${count}`} data-testid=\"daily-report-attachment-indicator\" data-attachment-count={count}>\n <Paperclip className=\"h-3.5 w-3.5\" aria-hidden=\"true\" />\n {count >= COUNT_VISIBLE_FROM && <span className=\"font-semibold text-[10px] leading-none\">{count}</span>}\n </span>\n )\n}\n","/**\n * Client-side DI configuration for the daily-report UI (paths, chrome slots, labels).\n * 日報 UI のクライアント側 DI 設定 (パス・クローム差し込み・ラベル名) を提供するコンテキスト。\n */\nimport { createContext, type ReactNode, useContext, useMemo } from \"react\"\n\n/** ヘッダー描画スロットに渡される引数。 */\nexport type DailyReportHeaderProps = {\n title: string\n annotation?: ReactNode\n}\n\n/**\n * Display labels for the report detail/list fields, sections, and side-pane tabs.\n * 日報の詳細/一覧カードのフィールド・セクション・タブ見出しの表示ラベル群。\n *\n * パッケージは中立な英語ラベルのみを既定として同梱し、ドメイン固有の表現 (言語・業務語彙) は\n * 消費アプリが `fieldLabels` で注入する。これによりパッケージ本体が業務ドメイン知識を持たない。\n */\nexport type DailyReportFieldLabels = {\n id: string\n businessDate: string\n author: string\n visitTime: string\n createdAt: string\n updatedAt: string\n updatedBy: string\n category: string\n categoryInfo: string\n creationCategory: string\n customer: string\n subject: string\n content: string\n interviewers: string\n comments: string\n attachments: string\n /** 実体の消失が確認された添付に添える注記。 */\n attachmentMissing: string\n /** 添付をダウンロードするリンクのラベル。 */\n attachmentDownload: string\n tabArticle: string\n tabRelations: string\n relationsEmpty: string\n}\n\n/**\n * Mutation-failure notification payload (for a host-supplied error surface).\n * ミューテーション失敗の通知情報 (ホストが独自の通知 UI を出すための口)。\n */\nexport type DailyReportErrorInfo = {\n /** 失敗した操作の種別 (create / update / publish / delete / addComment / deleteComment / toggleStar / toggleRead)。 */\n operation: string\n /** 表示用のメッセージ (パッケージ内蔵バナー用)。 */\n message: string\n /** 元例外 (診断用)。 */\n cause?: unknown\n}\n\n/** データソースごとのバッジ表示設定。 */\nexport type SourceTypeConfig = {\n label: string\n badgeClassName?: string\n /** コメント投稿を許可するかどうか (既定 true)。false の場合、該当ソースの日報でコメント投稿フォームを非表示にする。 */\n allowComment?: boolean\n}\n\n/** クライアント層の設定一式。 */\nexport type DailyReportClientConfig = {\n /** 画面タイトル (ヘッダーへ渡す)。 */\n title: string\n /** API ベースパス (例 `/daily_report/api`)。末尾スラッシュ無し。 */\n apiBasePath: string\n /** SSE エンドポイントパス (例 `/sse/daily_report/updates`)。 */\n ssePath: string\n /** 下書き状態を表すラベル名。UI の編集モード判定に使う。既定はプレースホルダーなので消費側で自アプリのラベル名へ上書きする。 */\n draftLabelName: string\n /** 詳細/一覧カード of field 見出しラベル。既定は中立英語。アプリが自ドメインの表記を注入する。 */\n fieldLabels: DailyReportFieldLabels\n /** 外部ソース区分ごとのバッジ表示設定。 */\n sourceTypeConfigs?: Record<string, SourceTypeConfig>\n /** ヘッダー描画スロット。アプリのヘッダーコンポーネントを差し込む。 */\n renderHeader: (props: DailyReportHeaderProps) => ReactNode\n /** 開発向けコントロール (SSE 購読トグル等) を表示するか。 */\n showDevControls: boolean\n /**\n * ミューテーション失敗時の通知ポート。未指定ならパッケージ内蔵の role=\"alert\"\n * バナーで表示する。ホストが独自トースト等を持つ場合はここへ委譲する。\n */\n onError?: (info: DailyReportErrorInfo) => void\n}\n\n/**\n * Partial client-config override. `fieldLabels` may itself be partially overridden.\n * クライアント設定の部分上書き型。`fieldLabels` も部分上書き可 (プロバイダーが深くマージする)。\n */\nexport type DailyReportClientConfigInput = Partial<Omit<DailyReportClientConfig, \"fieldLabels\">> & {\n fieldLabels?: Partial<DailyReportFieldLabels>\n sourceTypeConfigs?: Record<string, SourceTypeConfig>\n}\n\n/** 中立英語のフィールドラベル既定 (ドメイン非依存)。 */\nexport const defaultDailyReportFieldLabels: DailyReportFieldLabels = {\n id: \"ID\",\n businessDate: \"Business date\",\n author: \"Author\",\n visitTime: \"Visit time\",\n createdAt: \"Created at\",\n updatedAt: \"Updated at\",\n updatedBy: \"Updated by\",\n category: \"Category\",\n categoryInfo: \"Category\",\n creationCategory: \"Creation category\",\n customer: \"Customer\",\n subject: \"Subject\",\n content: \"Content\",\n interviewers: \"Attendees\",\n comments: \"Comments\",\n attachments: \"Attachments\",\n attachmentMissing: \"File is missing\",\n attachmentDownload: \"Download\",\n tabArticle: \"Overview\",\n tabRelations: \"Related\",\n relationsEmpty: \"No related view configured yet.\",\n}\n\n/** 既定設定 (消費側は必要なフィールドのみ部分上書きする)。 */\nexport const defaultDailyReportClientConfig: DailyReportClientConfig = {\n title: \"Daily Reports\",\n apiBasePath: \"/daily_report/api\",\n ssePath: \"/sse/daily_report/updates\",\n // 中立プレースホルダー。実運用のラベル名 (アプリ固有) は Provider / config prop で上書きする。\n draftLabelName: \"draft\",\n fieldLabels: defaultDailyReportFieldLabels,\n sourceTypeConfigs: {\n Internal: {\n label: \"オリジナル\",\n badgeClassName: \"bg-emerald-100 text-emerald-800 dark:bg-emerald-900/30 dark:text-emerald-400\",\n },\n },\n renderHeader: ({ title, annotation }) => (\n <header className=\"flex flex-wrap items-baseline justify-between gap-2 px-2 py-1\">\n <span className=\"text-nowrap text-slate-700 text-sm dark:text-slate-200\">{title}</span>\n {annotation ? <span className=\"text-slate-600 text-xs dark:text-slate-300\">{annotation}</span> : null}\n </header>\n ),\n showDevControls: false,\n}\n\nconst DailyReportConfigContext = createContext<DailyReportClientConfig>(defaultDailyReportClientConfig)\n\n/**\n * Provides a partial override of the daily-report client configuration.\n * 日報クライアント設定の部分上書きを提供するプロバイダー。\n */\nexport const DailyReportConfigProvider = ({ config, children }: { config?: DailyReportClientConfigInput; children: ReactNode }) => {\n // 部分設定を既定値へ浅くマージする (fieldLabels のみ 1 段深くマージして部分上書きを許す)\n const merged = useMemo<DailyReportClientConfig>(\n () => ({\n ...defaultDailyReportClientConfig,\n ...config,\n fieldLabels: { ...defaultDailyReportFieldLabels, ...(config?.fieldLabels ?? {}) },\n }),\n [config],\n )\n return <DailyReportConfigContext.Provider value={merged}>{children}</DailyReportConfigContext.Provider>\n}\n\n/**\n * Reads the daily-report client configuration (defaults when no provider).\n * 日報クライアント設定を読む (プロバイダー未設置時は既定値)。\n */\nexport const useDailyReportConfig = (): DailyReportClientConfig => useContext(DailyReportConfigContext)\n\n/**\n * Resolves source type badge configuration with case-insensitive fallback.\n * ケースインセンシティブなフォールバック付きでデータソースバッジ設定を解決する。\n */\nexport const resolveSourceTypeConfig = (sourceType?: string | null, sourceTypeConfigs?: Record<string, SourceTypeConfig>): SourceTypeConfig | undefined => {\n if (!(sourceType && sourceTypeConfigs)) return undefined\n if (sourceTypeConfigs[sourceType]) return sourceTypeConfigs[sourceType]\n const lower = sourceType.toLowerCase()\n const matchedKey = Object.keys(sourceTypeConfigs).find((k) => k.toLowerCase() === lower)\n return matchedKey ? sourceTypeConfigs[matchedKey] : undefined\n}\n","/**\n * Attachment list rendered inside a daily report detail card.\n * 日報詳細カード内に描画する添付ファイル一覧。\n *\n * このコンポーネントは詳細カードの本体 (`DailyReportDetailRow` の children) の内側に置くこと。\n * 行の高さは行ごとの `ResizeObserver` が測って仮想スクロールへ通知するため、\n * portal や absolute 配置で測定コンテナの外へ出すと行高が壊れ、\n * タブ切替時のスクロール位置保存 E2E も落ちる。\n *\n * **ネットワーク要求を自動発火させないこと。** 配信エンドポイントは 1 リクエストごとに\n * 実体全体をサーバーのヒープへ載せる経路で、レート上限と同時実行ゲートを持つ。\n * ここで `<img src={配信URL}>` のような自動取得を置くと、仮想スクロールの再マウントの\n * たびに実体 (数十 MiB) を取り直し、利用者自身のダウンロードを 429 / 503 で潰す。\n * サムネイルを出すなら縮小画像を返す専用経路を用意してからにする。\n *\n * DOM フックは `data-*` 属性のみを使う。クラス名はスタイリング契約であり\n * 選択子として使ってはならない (AGENTS.md)。\n */\nimport type { DailyReportAttachmentSummary } from \"../../shared/types\"\nimport { useDailyReportConfig } from \"../config-context\"\n\n/** 1 KiB。サイズ表記の基数。 */\nconst BYTES_PER_UNIT = 1024\n\n/** サイズ表記の単位列。 */\nconst SIZE_UNITS = [\"B\", \"KB\", \"MB\", \"GB\"] as const\n\n/**\n * Formats a byte count for display, or returns null when unknown.\n * バイト数を表示用に整形する。不明なら null を返す。\n *\n * サイズを報告しない取込元の添付は null になるため、0 と null を区別する\n * (0 は「空ファイル」、null は「サイズ不明」)。\n *\n * @param bytes Size in bytes, or null when unknown. バイト数 (不明なら null)。\n * @returns Human-readable size, or null. 可読なサイズ表記 (不明なら null)。\n */\nconst formatFileSize = (bytes: number | null): string | null => {\n if (bytes === null || !Number.isFinite(bytes) || bytes < 0) return null\n let value = bytes\n let unitIndex = 0\n while (value >= BYTES_PER_UNIT && unitIndex < SIZE_UNITS.length - 1) {\n value /= BYTES_PER_UNIT\n unitIndex += 1\n }\n // 1 KB 未満は小数を出さない\n return unitIndex === 0 ? `${value} ${SIZE_UNITS[unitIndex]}` : `${value.toFixed(1)} ${SIZE_UNITS[unitIndex]}`\n}\n\ntype DailyReportAttachmentListProps = {\n /** 表示する添付一覧。空配列ならこのコンポーネントは何も描画しない。 */\n attachments: DailyReportAttachmentSummary[]\n}\n\n/**\n * Renders the attachment section of a daily report detail card.\n * 日報詳細カードの添付ファイル節を描画するコンポーネント。\n *\n * 表示文言はすべて `fieldLabels` から取る (パッケージは業務ドメインの語彙も言語も持たない)。\n *\n * @param props Attachments to render. 描画する添付一覧。\n * @returns The section element, or null when there is nothing to show. 節の要素 (表示対象が無ければ null)。\n */\nexport const DailyReportAttachmentList = ({ attachments }: DailyReportAttachmentListProps) => {\n const { apiBasePath, fieldLabels } = useDailyReportConfig()\n\n if (attachments.length === 0) {\n return null\n }\n\n return (\n <div className=\"flex flex-col gap-2\" data-testid=\"daily-report-attachment-section\">\n <span className=\"font-semibold text-slate-500 text-xs uppercase tracking-wide dark:text-slate-300\">{fieldLabels.attachments}</span>\n <ul className=\"flex flex-col gap-1.5\">\n {attachments.map((attachment) => {\n // URL は必ず apiBasePath から組む。パッケージ内にルートを焼き込まない\n const href = `${apiBasePath}/attachment/${attachment.id}`\n const size = formatFileSize(attachment.fileSize)\n const isMissing = attachment.state === \"absent\"\n\n return (\n <li key={attachment.id} className=\"flex items-center gap-2 rounded-lg bg-slate-50 px-3 py-2 dark:bg-slate-800/40\" data-testid=\"daily-report-attachment-item\" data-attachment-state={attachment.state}>\n <div className=\"flex min-w-0 flex-col\">\n {isMissing ? (\n // 消失が確認された添付はリンクにしない (押しても 404 になるだけ)\n <span className=\"truncate text-slate-400 text-sm line-through dark:text-slate-500\" data-testid=\"daily-report-attachment-name\">\n {attachment.fileName}\n </span>\n ) : (\n <a href={href} className=\"truncate text-blue-600 text-sm hover:underline dark:text-blue-400\" data-testid=\"daily-report-attachment-link\">\n {attachment.fileName}\n </a>\n )}\n <span className=\"text-[10px] text-slate-400 dark:text-slate-500\">\n {size ?? \"—\"}\n {/* 実体の消失を確認できた場合のみ注記する。unknown は「未検証」であって\n 「消失」ではないため、通常表示のままにする (判定不能を偽の真偽で埋めない) */}\n {isMissing && ` · ${fieldLabels.attachmentMissing}`}\n </span>\n </div>\n {!isMissing && (\n <a href={`${href}?download=1`} className=\"ml-auto shrink-0 text-[10px] text-slate-500 hover:text-slate-700 hover:underline dark:text-slate-400 dark:hover:text-slate-200\" data-testid=\"daily-report-attachment-download\">\n {fieldLabels.attachmentDownload}\n </a>\n )}\n </li>\n )\n })}\n </ul>\n </div>\n )\n}\n","import { TrashIcon } from \"@heroicons/react/24/outline\"\nimport { useState } from \"react\"\nimport { resolveCommentColorClass, type UIComment } from \"../../shared/comment-adapter\"\nimport { fallbackText, formatToIsoDate } from \"../../shared/text-utils\"\nimport { cn } from \"../ui/cn\"\n\ntype DailyReportCommentItemProps = {\n comment: UIComment\n reportHubId: number\n businessDate: string\n onOptimisticDelete: (commentId: number) => void\n pendingDeleteId: number | string | null\n onPendingDeleteIdChange: (id: number | string | null) => void\n onTrashClick: (e: React.MouseEvent | React.TouchEvent, commentId: number | string) => void\n resolvedIdMap?: Map<number, number>\n}\n\n/**\n * Checks if the current comment is in a pending delete state, considering temporary ID resolution.\n * 現在のコメントが削除保留状態かどうかを判定する。一時IDの解決も考慮する。\n */\nconst checkIsPendingDelete = (commentId: number | string, pendingDeleteId: number | string | null, resolvedIdMap?: Map<number, number>): boolean => {\n if (pendingDeleteId === commentId) return true\n // Case: commentId is temp, pendingDeleteId is resolved real ID\n if (typeof commentId === \"number\" && commentId < 0 && resolvedIdMap?.get(commentId) === pendingDeleteId) return true\n // Case: pendingDeleteId is temp, commentId is resolved real ID\n if (typeof pendingDeleteId === \"number\" && pendingDeleteId < 0 && resolvedIdMap?.get(pendingDeleteId) === commentId) return true\n return false\n}\n\n/**\n * Renders a single comment item with delete functionality.\n * 削除機能付きの単一コメントアイテムを描画するコンポーネント。\n */\nexport const DailyReportCommentItem = ({ comment, reportHubId: _reportHubId, businessDate: _businessDate, onOptimisticDelete, pendingDeleteId, onPendingDeleteIdChange, onTrashClick, resolvedIdMap }: DailyReportCommentItemProps) => {\n const [isDeleted, setIsDeleted] = useState(false)\n\n const isPendingDelete = checkIsPendingDelete(comment.id, pendingDeleteId, resolvedIdMap)\n\n const handleConfirmDelete = (e: React.MouseEvent | React.TouchEvent) => {\n e.stopPropagation()\n e.preventDefault()\n\n onPendingDeleteIdChange(null)\n setIsDeleted(true)\n\n if (typeof comment.id === \"number\") {\n // Optimistic UI Update (API call is handled by parent hook)\n onOptimisticDelete(comment.id)\n }\n }\n\n return (\n <li\n data-comment-id={comment.id}\n data-deleted={isDeleted ? \"true\" : undefined}\n className={cn(\n \"group relative rounded-xl border border-slate-200 bg-white p-3 dark:border-slate-700 dark:bg-slate-900/70\",\n comment.isMine && \"border-blue-200 bg-blue-50 dark:border-blue-800/70 dark:bg-blue-950/40\",\n isDeleted && \"hidden\",\n isPendingDelete ? \"z-50\" : \"z-0 hover:z-10\",\n )}>\n <div className=\"mb-1 flex items-center justify-between\">\n <div className={cn(\"font-semibold text-xs\", resolveCommentColorClass(comment.color))}>{fallbackText(comment.authorName)}</div>\n <div className=\"flex items-center gap-2\">\n {comment.createdAt && <div className=\"text-[10px] text-slate-400\">{formatToIsoDate(comment.createdAt)}</div>}\n {comment.isMine && !comment.isLegacy && (\n <div className=\"relative\">\n <button type=\"button\" className={cn(\"transition-colors\", isPendingDelete ? \"text-red-500\" : \"text-slate-400 hover:text-red-500\")} title=\"削除\" aria-label=\"削除\" onClick={(e) => onTrashClick(e, comment.id)}>\n <TrashIcon className=\"h-4 w-4\" />\n </button>\n\n {isPendingDelete && (\n <button\n type=\"button\"\n data-delete-confirmation\n className=\"fade-in zoom-in-95 absolute top-full right-0 z-20 mt-1 animate-in whitespace-nowrap rounded-md bg-red-600 px-3 py-1.5 font-bold text-white text-xs shadow-lg duration-200 hover:bg-red-700 active:scale-95\"\n onClick={handleConfirmDelete}>\n 削除する\n </button>\n )}\n </div>\n )}\n </div>\n </div>\n <div className=\"whitespace-pre-wrap text-slate-700 text-sm leading-relaxed dark:text-slate-200\">{comment.content}</div>\n </li>\n )\n}\n","/**\n * Adapters merging legacy JSON comments and relational comments for the UI.\n * レガシー JSON コメントとリレーショナルコメントを UI 向けに統合するアダプタ群。\n */\nimport type { DailyReportComment, DailyReportCommentItem } from \"./types\"\n\nexport type UIComment = {\n id: string | number\n authorName: string\n content: string\n createdAt?: string\n isMine: boolean\n color?: string\n isLegacy: boolean\n}\n\n/**\n * Merges legacy JSON comments and modern relational comments into a unified UI format.\n * レガシーな JSON コメントとモダンなリレーショナルコメントを統合された UI 形式にマージする。\n */\nexport const mergeComments = (legacyComments: DailyReportComment[], modernComments: DailyReportCommentItem[], currentUserId?: string | null): UIComment[] => {\n // 1. Legacy コメントの変換\n const legacyUIComments: UIComment[] = legacyComments.map((c, index) => ({\n id: `legacy-${index}`,\n authorName: c.name || \"Unknown\",\n content: c.text || \"\",\n isMine: false,\n color: c.color || undefined,\n isLegacy: true,\n }))\n\n // 2. Modern コメントの変換\n const modernUIComments: UIComment[] = modernComments.map((c) => ({\n id: c.id,\n authorName: c.userName ?? \"\",\n content: c.content ?? \"\",\n createdAt: c.createdAt ?? \"\",\n isMine: currentUserId ? c.userId === currentUserId : c.isMine,\n isLegacy: false,\n }))\n\n // 3. 結合 (Legacy を先に表示)\n return [...legacyUIComments, ...modernUIComments]\n}\n\n/**\n * Resolves comment color tokens into Tailwind text classes.\n * コメント色のキーワードを Tailwind のテキストクラスに変換する関数。\n */\nexport const resolveCommentColorClass = (color: string | null | undefined): string => {\n // 各色にダークモードの明色対応を必ず添える。ダークカード上で暗色トークン\n // (text-gray-700 など) 単体だと文字が背景に沈んで読めない\n if (!color) {\n return \"text-gray-700 dark:text-gray-300\"\n }\n\n const normalized = color.toLowerCase()\n\n if (normalized.includes(\"red\") || normalized.includes(\"赤\")) {\n return \"text-red-600 dark:text-red-400\"\n }\n\n if (normalized.includes(\"green\") || normalized.includes(\"緑\")) {\n return \"text-green-600 dark:text-green-400\"\n }\n\n if (normalized.includes(\"blue\") || normalized.includes(\"青\")) {\n return \"text-blue-600 dark:text-blue-400\"\n }\n\n if (normalized.includes(\"yellow\") || normalized.includes(\"黄\")) {\n return \"text-yellow-600 dark:text-yellow-400\"\n }\n\n if (normalized.includes(\"orange\") || normalized.includes(\"橙\")) {\n return \"text-orange-500 dark:text-orange-400\"\n }\n\n return \"text-gray-700 dark:text-gray-300\"\n}\n","/**\n * Shared text helpers for daily report view components.\n * 日報ビューコンポーネント向けの共通テキストヘルパー。\n */\n\n/**\n * Normalizes nullable text by trimming whitespace and applying fallback.\n * null 許容の文字列をトリムしつつフォールバック値を適用する関数。\n */\nexport const fallbackText = (value: string | null | undefined): string => {\n if (!value) {\n return \"-\"\n }\n const trimmed = value.trim()\n return trimmed === \"\" ? \"-\" : trimmed\n}\n\n/**\n * Formats a date string to ISO 8601 format (YYYY-MM-DD or YYYY-MM-DDTHH:mm:ss).\n * 日付文字列を ISO 8601 形式 (YYYY-MM-DD または YYYY-MM-DDTHH:mm:ss) に整形する関数。\n */\nexport const formatToIsoDate = (value: unknown): string => {\n if (!value) {\n return \"-\"\n }\n if (typeof value !== \"string\") {\n // If value is not a string (e.g. Date object), try to convert it to string or handle it\n if (value instanceof Date) {\n return formatToIsoDate(value.toISOString())\n }\n return String(value)\n }\n const trimmed = value.trim()\n if (trimmed === \"\") {\n return \"-\"\n }\n\n // 日付のみ (YYYY-MM-DD / YYYY/MM/DD) は Date を経由せず整形する。\n // `new Date(\"2026-08-08\")` は UTC 深夜と解釈され、ローカル getter で負オフセット圏\n // (米国など) では前日へずれるため。\n const dateOnly = trimmed.match(/^(\\d{4})[-/](\\d{2})[-/](\\d{2})$/)\n if (dateOnly) {\n return `${dateOnly[1]}-${dateOnly[2]}-${dateOnly[3]}`\n }\n\n const date = new Date(trimmed)\n if (Number.isNaN(date.getTime())) {\n return trimmed\n }\n\n const year = date.getFullYear()\n const month = String(date.getMonth() + 1).padStart(2, \"0\")\n const day = String(date.getDate()).padStart(2, \"0\")\n\n const hasTime = trimmed.includes(\":\") || trimmed.includes(\"T\")\n\n if (hasTime) {\n const hours = String(date.getHours()).padStart(2, \"0\")\n const minutes = String(date.getMinutes()).padStart(2, \"0\")\n const seconds = String(date.getSeconds()).padStart(2, \"0\")\n return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}`\n }\n\n return `${year}-${month}-${day}`\n}\n","/**\n * Tailwind class combiner (clsx + tailwind-merge).\n * Tailwind クラスを結合・重複解決するユーティリティ。\n */\nimport { type ClassValue, clsx } from \"clsx\"\nimport { twMerge } from \"tailwind-merge\"\n\n/**\n * Combines class values and resolves Tailwind conflicts.\n * クラス値を結合し Tailwind の衝突を解決する処理。\n */\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs))\n}\n","import { useCallback, useEffect, useRef, useState } from \"react\"\nimport type { UIComment } from \"../../shared/comment-adapter\"\nimport { DailyReportCommentItem } from \"./daily-report-comment-item\"\n\n/**\n * Custom hook to manage the auto-reset behavior of the pending delete state.\n * 削除保留状態の自動リセット(タイマーおよび外部クリック)を管理するカスタムフック。\n */\nconst useAutoResetPendingDelete = (pendingDeleteId: number | string | null, onPendingDeleteIdChange: (id: number | string | null) => void) => {\n const timerRef = useRef<number | null>(null)\n\n /**\n * Effect to handle global clicks and reset pending delete state if clicked outside.\n * 外部クリックを監視し、削除確認ボタン以外がクリックされた場合に削除保留状態をリセットする副作用。\n *\n * Purpose: To cancel the delete confirmation mode when the user interacts with other parts of the UI.\n * Dependencies: [pendingDeleteId, onPendingDeleteIdChange] - Re-runs when the pending ID changes.\n * Cleanup: Removes event listeners for mousedown and touchstart.\n */\n useEffect(() => {\n if (pendingDeleteId === null) return\n\n const handleGlobalClick = (e: Event) => {\n const target = e.target as HTMLElement\n // Ignore clicks inside the delete confirmation button\n if (target.closest(\"[data-delete-confirmation]\")) return\n\n onPendingDeleteIdChange(null)\n }\n\n document.addEventListener(\"mousedown\", handleGlobalClick)\n document.addEventListener(\"touchstart\", handleGlobalClick)\n\n return () => {\n document.removeEventListener(\"mousedown\", handleGlobalClick)\n document.removeEventListener(\"touchstart\", handleGlobalClick)\n }\n }, [pendingDeleteId, onPendingDeleteIdChange])\n\n /**\n * Effect to clean up the timer on unmount.\n * アンマウント時にタイマーをクリーンアップする副作用。\n *\n * Purpose: To prevent memory leaks and state updates on unmounted components.\n * Dependencies: [] - Runs only on mount/unmount.\n * Cleanup: Clears the timeout if it exists.\n */\n useEffect(() => {\n return () => {\n if (timerRef.current) clearTimeout(timerRef.current)\n }\n }, [])\n\n const handleTrashClick = useCallback(\n (e: React.MouseEvent | React.TouchEvent, commentId: number | string) => {\n e.stopPropagation()\n e.preventDefault()\n\n if (pendingDeleteId === commentId) return\n\n onPendingDeleteIdChange(commentId)\n\n if (timerRef.current) clearTimeout(timerRef.current)\n // Auto-reset after 5 seconds\n timerRef.current = window.setTimeout(() => {\n onPendingDeleteIdChange(null)\n timerRef.current = null\n }, 5000)\n },\n [pendingDeleteId, onPendingDeleteIdChange],\n )\n\n return { handleTrashClick }\n}\n\ntype DailyReportCommentListProps = {\n comments: UIComment[]\n reportHubId: number\n businessDate: string\n onDeleteComment?: (commentId: number) => void\n pendingDeleteId: number | string | null\n onPendingDeleteIdChange: (id: number | string | null) => void\n resolvedIdMap?: Map<number, number>\n}\n\n/**\n * Renders a list of daily report comments.\n * 日報コメントのリストを描画するコンポーネント。\n */\nexport const DailyReportCommentList = ({ comments, reportHubId, businessDate, onDeleteComment, pendingDeleteId, onPendingDeleteIdChange, resolvedIdMap }: DailyReportCommentListProps) => {\n const { handleTrashClick } = useAutoResetPendingDelete(pendingDeleteId, onPendingDeleteIdChange)\n\n if (comments.length === 0) return null\n\n return (\n <ul className=\"mb-3 space-y-2\">\n {comments.map((comment) => (\n <DailyReportCommentItem\n key={comment.id}\n comment={comment}\n reportHubId={reportHubId}\n businessDate={businessDate}\n onOptimisticDelete={onDeleteComment || (() => {})}\n pendingDeleteId={pendingDeleteId}\n onPendingDeleteIdChange={onPendingDeleteIdChange}\n onTrashClick={handleTrashClick}\n resolvedIdMap={resolvedIdMap}\n />\n ))}\n </ul>\n )\n}\n\ntype DailyReportCommentFormProps = {\n reportHubId: number\n businessDate: string\n onAddComment?: (content: string) => Promise<void> | void\n}\n\n/**\n * Renders a form to add a new comment to a daily report.\n * 日報に新しいコメントを追加するためのフォームを描画するコンポーネント。\n */\nexport const DailyReportCommentForm = ({ reportHubId, businessDate, onAddComment }: DailyReportCommentFormProps) => {\n const [isSubmitting, setIsSubmitting] = useState(false)\n const formRef = useRef<HTMLFormElement>(null)\n\n const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {\n e.preventDefault()\n const formData = new FormData(e.currentTarget)\n const content = (formData.get(\"content\") as string)?.trim()\n\n if (!(content && onAddComment)) return\n\n setIsSubmitting(true)\n formRef.current?.reset()\n try {\n await onAddComment(content)\n } catch {\n if (formRef.current) {\n const input = formRef.current.elements.namedItem(\"content\") as HTMLInputElement\n if (input) input.value = content\n }\n } finally {\n setIsSubmitting(false)\n }\n }\n\n return (\n <form className=\"flex gap-2\" ref={formRef} onSubmit={handleSubmit}>\n <input type=\"hidden\" name=\"intent\" value=\"addComment\" />\n <input type=\"hidden\" name=\"reportHubId\" value={reportHubId} />\n <input type=\"hidden\" name=\"businessDate\" value={businessDate} />\n <input\n type=\"text\"\n name=\"content\"\n placeholder=\"コメントを追加...\"\n className=\"flex-1 rounded-lg border border-slate-300 bg-slate-50 px-3 py-2 text-sm focus:border-blue-500 focus:bg-white focus:outline-none dark:border-slate-700 dark:bg-slate-800 dark:text-slate-200\"\n autoComplete=\"off\"\n />\n <button type=\"submit\" className=\"rounded-lg bg-blue-600 px-4 py-2 font-medium text-sm text-white hover:bg-blue-700 disabled:opacity-50\" disabled={isSubmitting}>\n 送信\n </button>\n </form>\n )\n}\n","/**\n * Virtual-scroll based detailed daily report view backed by fetcher-driven data hooks.\n * useFetcher ベースのデータフックで日報詳細を提供する仮想スクロールビュー。\n */\n\nimport { type ScrollBarThumbOverlayRenderProps, VirtualScroll, type VirtualScrollHandle } from \"@aiquants/virtualscroll\"\n\n/**\n * Handle type augmented with E2E test helper methods.\n * E2E テスト用ヘルパーメソッドで拡張されたハンドル型。\n */\ntype DailyReportVirtualScrollHandle = VirtualScrollHandle & {\n findReportIndex: (id: number) => number\n getReportItem: (index: number) => DailyReportItem | undefined\n selectItem: (id: number) => void\n}\n\nimport { Check, Star, Trash2 } from \"lucide-react\"\nimport { type RefObject, useCallback, useEffect, useMemo, useRef, useState } from \"react\"\nimport { fallbackText, formatToIsoDate } from \"../../shared/text-utils\"\nimport type { DailyReportDetail, DailyReportItem } from \"../../shared/types\"\nimport { type DailyReportFieldLabels, resolveSourceTypeConfig, useDailyReportConfig } from \"../config-context\"\nimport { useDailyReportActionContext } from \"../contexts/daily-report-action-context\"\nimport { useDailyReportDetail, useDailyReportPrefetch } from \"../hooks/use-daily-report\"\nimport { useDailyReportComments } from \"../hooks/use-daily-report-comments\"\nimport { useDynamicViewportHeight } from \"../hooks/use-dynamic-viewport-height\"\nimport { cn } from \"../ui/cn\"\nimport { CONTENT_DRAG_POINTER_INPUTS, EMPTY_ITEM, FAST_SCROLL_INERTIA_OPTIONS, FAST_SCROLL_WHEEL_MULTIPLIER, PREFETCH_LOOKAHEAD, PREFETCH_MAX_DATES, SCROLL_BAR_WIDTH, TAP_SCROLL_CIRCLE_OPTIONS, VIRTUAL_SCROLL_OVERSCAN_COUNT, WHEEL_RESET_TIMEOUT_MS } from \"../utils/constants\"\nimport { BusinessDayThumbOverlay } from \"./business-day-thumb-overlay\"\nimport { DailyReportAttachmentIndicator } from \"./daily-report-attachment-indicator\"\nimport { DailyReportAttachmentList } from \"./daily-report-attachment-list\"\nimport { DailyReportCommentForm, DailyReportCommentList } from \"./daily-report-comment-section\"\nimport { DailyReportEditForm } from \"./daily-report-edit-form\"\nimport { UnreadIndicator } from \"./unread-indicator\"\n\n/**\n * Observes container height changes and reports measured values upstream.\n * コンテナの高さ変化を監視し測定結果を親へ通知するフック。\n */\nconst useReportItemHeightObserver = (ref: RefObject<HTMLDivElement | null>, itemId: number, onHeightMeasured: (id: number, height: number) => void) => {\n useEffect(() => {\n const element = ref.current\n if (!element) return\n\n const resizeObserver = new ResizeObserver(() => {\n onHeightMeasured(itemId, element.offsetHeight)\n })\n resizeObserver.observe(element)\n return () => resizeObserver.disconnect()\n }, [itemId, onHeightMeasured, ref])\n}\n\n/**\n * Wraps a daily report item row, observing its height and providing a stable test identifier.\n * 日報アイテム行をラップし、高さを監視するとともに安定したテスト識別子を提供するコンポーネント。\n */\nconst DailyReportDetailRow = ({ itemId, index, setSize, onHeightMeasured, children }: { itemId: number; index: number; setSize: number; onHeightMeasured: (id: number, height: number) => void; children: React.ReactNode }) => {\n const elementRef = useRef<HTMLDivElement | null>(null)\n useReportItemHeightObserver(elementRef, itemId, onHeightMeasured)\n return (\n // 仮想化リスト内の絶対位置を SR へ公開する (描画は窓分だけなので必須)。\n // 親は VirtualScroll のコンテンツ要素 (role=\"list\" の div) であり実 <ul> ではないため、\n // <li> にすると list 要素外の <li> になって不正。role=\"listitem\" を div に付けるのが正しい\n // biome-ignore lint/a11y/useSemanticElements: parent is a role=\"list\" div (virtualized), not a real <ul>; <li> would be invalid here\n <div ref={elementRef} className=\"px-2 pb-3\" data-daily-report-button={itemId} role=\"listitem\" aria-posinset={index + 1} aria-setsize={setSize}>\n {children}\n </div>\n )\n}\n\n/**\n * Formats interviewer labels with optional affiliation.\n * 面談者情報を所属付きのラベル文字列に整形する関数。\n */\nconst formatInterviewerLabel = (interviewer: DailyReportDetail[\"interviewers\"][number]): string => {\n if (interviewer.affiliation?.trim()) {\n return `${interviewer.name}(${interviewer.affiliation})`\n }\n return interviewer.name ?? \"\"\n}\n\n/**\n * Scroll restore slot for DailyReportDetailList that survives tab unmounting.\n * タブアンマウントをまたいで DailyReportDetailList のスクロール位置を持ち回る共有スロット。\n *\n * topItem は先頭可視カードの**同一性アンカー** (reportHubId + 行内オフセット px + 保存時 index)。\n * ❗ 生の論理 px を保存してはならない: 可変高さでは px→行変換が再マウントの推定空間で行われ、\n * 深い位置で別のカードに着地する (実機で確定した実害バグ)。同一性アンカーは一覧が留守中に\n * 変わっても findIndex で正しいカードへ再解決される。index は該当カードが消えていたときの\n * 近似フォールバック。offsetPx はカード上端がビューポート上端より上に隠れている px (論理座標)。\n * anchorReportHubId は「記録時点の選択日報」。復元はこれが現在の選択と一致するときだけ行う:\n * 選択が他ビューで変わっていたら、復元よりも「選択日報へのスクロール」(既存 UX) を優先する。\n * measuredHeights は実測カード高さ (reportHubId → px)。着地窓の行内オフセットの厳密さと\n * 総高さ推定の質を保つため、実測をアンマウント越しに持ち回る。\n */\nexport type DailyReportDetailListScrollRestore = {\n topItem: { reportHubId: number; index: number; offsetPx: number } | null\n anchorReportHubId: number | null\n measuredHeights: Map<number, number>\n}\n\ntype DailyReportDetailListProps = {\n dailyReportItems: DailyReportItem[]\n userId?: string | null\n selectedItemId?: number | null\n onSelectItem?: (id: number | null) => void\n /**\n * スクロール位置の共有スロット。マウント時に一度だけ読み、スクロールのたびに書き戻す。\n * ❗ 値をレンダー時に読んで props へ焼き込む方式は禁止 (daily-report-list.tsx 参照)\n */\n scrollRestoreRef?: RefObject<DailyReportDetailListScrollRestore>\n}\n\n/**\n * Daily report detail cards rendered with dynamic height virtual scroll.\n * 動的高さの仮想スクロールで日報詳細カードを描画するコンポーネント。\n */\nexport const DailyReportDetailList = ({ dailyReportItems, userId, selectedItemId, onSelectItem, scrollRestoreRef }: DailyReportDetailListProps) => {\n // 復元アンカーと「復元マウントかどうか」はマウント時に一度だけ共有スロットから決める。\n // 復元判定は選択アンカー (記録時の選択日報) の一致のみで行う。❗ 位置の有無や大小を復元\n // 判定に流用してはならない: 先頭まで戻してからタブを離れた復元マウントが「非復元」に\n // 誤分類され、初回の選択スクロールが復元位置 (先頭) を上書きしてしまう。\n // topItem は reportHubId の同一性で現在の一覧へ再解決する (留守中の並び替え・挿入に不変)。\n // 消えていたら保存時 index への近似フォールバック (行内オフセットは破棄 = カード上端合わせ)\n const [{ initialScrollAnchor, restored: isRestoredMount }] = useState(() => {\n const restore = scrollRestoreRef?.current\n if (!restore || restore.anchorReportHubId !== (selectedItemId ?? null)) {\n return { initialScrollAnchor: undefined, restored: false }\n }\n if (!restore.topItem) {\n return { initialScrollAnchor: undefined, restored: true }\n }\n const foundIndex = dailyReportItems.findIndex((item) => item.reportHubId === restore.topItem?.reportHubId)\n if (foundIndex !== -1) {\n return { initialScrollAnchor: { index: foundIndex, offsetPx: restore.topItem.offsetPx }, restored: true }\n }\n return { initialScrollAnchor: { index: restore.topItem.index, offsetPx: 0 }, restored: true }\n })\n // 復元マウントの初回だけ「選択日報へのスクロール」をスキップするための武装フラグと、\n // 消費条件 (マウント時と同じ選択であること) の基準値。\n // ❗ 消費条件なしで武装したままにしてはならない: アンカー日報がアンマウント中に削除されて\n // いた場合など、マウント効果が index を解決できないとフラグが残留し、その後の正当な\n // 選択変更 (新規作成ボタン等) のスクロールを 1 回無言で飲み込む\n const skipInitialScrollToSelectionRef = useRef(isRestoredMount)\n const mountSelectionIdRef = useRef(selectedItemId ?? null)\n // 実測高さは ref にインプレースで持つ。以前は測定行ごとに `new Map(prev)` を作っており\n // 行数に対して O(n^2) だった。VirtualScroll へは updateItemSize で個別に伝えるため、\n // ここで state を持って再レンダーを起こす必要はない (getItemHeight は ref を直接読む)。\n // ❗ 共有スロットがあればその Map を使う (アンマウントで実測空間を捨てない): 同一性アンカー\n // 復元の下で、実測の持ち回りは (a) アンカーカードの行内オフセットの厳密さ (そのカードの\n // 実測高さが行内 px の座標系そのもの) と (b) 総高さ推定・周辺カードの初期配置の質を保つ。\n // カードの同一性は topItem (reportHubId) が保証するため、深い復元でも別カードへは漂着しない\n const itemHeightsRef = useRef<Map<number, number>>(scrollRestoreRef?.current.measuredHeights ?? new Map())\n const scrollContainerRef = useRef<HTMLDivElement | null>(null)\n const virtualScrollRef = useRef<VirtualScrollHandle>(null)\n // list ロールの aria-label に使う (消費側注入の業務ドメイン名)\n const { title } = useDailyReportConfig()\n const { viewportHeight, updateViewportHeight } = useDynamicViewportHeight(scrollContainerRef, { enableMutationObserver: true })\n const prefetchDailyReportByDate = useDailyReportPrefetch()\n const [overlayLabel, setOverlayLabel] = useState<string>(\"\")\n const [isWheelScrollActive, setIsWheelScrollActive] = useState(false)\n const wheelResetTimerRef = useRef<number | null>(null)\n const scrollEndTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)\n const dailyReportItemsRef = useRef(dailyReportItems)\n dailyReportItemsRef.current = dailyReportItems\n\n // 同一 selectedItemId かつ同一 index への重複スクロールを防止する ref\n const lastScrolledRef = useRef<{ id: number; index: number } | null>(null)\n\n useEffect(() => {\n if (selectedItemId != null && virtualScrollRef.current) {\n const index = dailyReportItems.findIndex((item) => item.reportHubId === selectedItemId)\n if (index !== -1) {\n // ID もインデックスも同じなら再スクロール不要\n if (lastScrolledRef.current?.id === selectedItemId && lastScrolledRef.current?.index === index) return\n // 復元マウントの初回だけはスクロールせず「スクロール済み」として記録する\n // (以後の選択変更・並び替え追従は従来どおり動く)。\n // 消費できるのは「マウント時と同じ選択」のときだけ: 選択が変わっていたら\n // それは正当な選択変更なので、武装解除して通常のスクロールへ落とす\n if (skipInitialScrollToSelectionRef.current) {\n skipInitialScrollToSelectionRef.current = false\n if ((selectedItemId ?? null) === mountSelectionIdRef.current) {\n lastScrolledRef.current = { id: selectedItemId, index }\n return\n }\n }\n virtualScrollRef.current.scrollToIndex(index, { align: \"top\" })\n lastScrolledRef.current = { id: selectedItemId, index }\n }\n } else {\n lastScrolledRef.current = null\n }\n }, [selectedItemId, dailyReportItems])\n\n /**\n * Keeps the restore slot's anchor aligned with the current selection.\n * 復元スロットのアンカーを現在の選択日報と同期し続ける副作用。\n *\n * スクロールイベントに依存せず選択変更を即座に反映する (選択日報が既に上端にあり\n * scrollToIndex が onScroll を発火しないケースでもアンカーが古くならない)。\n * 依存配列: [selectedItemId, scrollRestoreRef]。クリーンアップ: なし。\n */\n useEffect(() => {\n const restore = scrollRestoreRef?.current\n if (restore) {\n restore.anchorReportHubId = selectedItemId ?? null\n }\n }, [selectedItemId, scrollRestoreRef])\n\n // onSelectItem の最新参照を保持する ref (useEffect 依存配列を不要にする)\n const onSelectItemRef = useRef(onSelectItem)\n onSelectItemRef.current = onSelectItem\n\n useEffect(() => {\n if (virtualScrollRef.current) {\n const el = document.querySelector('[data-testid=\"daily-report-detail-list\"]')\n if (el) {\n const handle = virtualScrollRef.current\n const extendedHandle: DailyReportVirtualScrollHandle = Object.assign({}, handle, {\n findReportIndex: (id: number) => dailyReportItemsRef.current.findIndex((item) => item.reportHubId === id),\n getReportItem: (index: number) => dailyReportItemsRef.current[index],\n selectItem: (id: number) => onSelectItemRef.current?.(id),\n })\n ;(el as Element & { __virtualScroll: DailyReportVirtualScrollHandle }).__virtualScroll = extendedHandle\n }\n }\n })\n\n const itemCount = dailyReportItems.length\n\n const getItem = useCallback(\n (index: number): DailyReportItem => {\n if (index >= itemCount) return EMPTY_ITEM\n return dailyReportItems[index]\n },\n [dailyReportItems, itemCount],\n )\n\n const schedulePrefetchForRange = useCallback(\n (fromIndex: number, toIndex: number) => {\n if (itemCount === 0) return\n const boundedFrom = Math.max(0, fromIndex)\n const boundedTo = Math.min(itemCount - 1, toIndex)\n if (boundedFrom > boundedTo) return\n\n const businessDates = new Set<string>()\n for (let index = boundedFrom; index <= boundedTo; index++) {\n const candidate = getItem(index)\n if (candidate.businessDate) businessDates.add(candidate.businessDate)\n if (businessDates.size >= PREFETCH_MAX_DATES) break\n }\n businessDates.forEach((date) => {\n prefetchDailyReportByDate(date)\n })\n },\n [getItem, itemCount, prefetchDailyReportByDate],\n )\n\n /**\n * Handles visible range changes to update overlay label and schedule prefetching.\n * 表示範囲の変更を処理し、オーバーレイラベルの更新とプリフェッチのスケジュールを行う。\n */\n const handleRangeChange = useCallback(\n ({ renderingStartIndex, renderingEndIndex, visibleStartIndex }: { renderingStartIndex: number; renderingEndIndex: number; visibleStartIndex: number }) => {\n if (itemCount === 0) return\n const firstVisibleItem = getItem(visibleStartIndex)\n if (firstVisibleItem?.businessDate) {\n setOverlayLabel(formatToIsoDate(firstVisibleItem.businessDate))\n }\n schedulePrefetchForRange(renderingStartIndex, renderingEndIndex)\n schedulePrefetchForRange(renderingEndIndex + 1, renderingEndIndex + PREFETCH_LOOKAHEAD)\n schedulePrefetchForRange(renderingStartIndex - PREFETCH_LOOKAHEAD, renderingStartIndex - 1)\n },\n [getItem, itemCount, schedulePrefetchForRange],\n )\n\n // ❗ teardown 用ハンドルミラー: React は削除コミットの mutation phase で virtualScrollRef を\n // null 化するため、アンマウント時の RAF flush (最終 onScroll 配信) の時点では素の ref が\n // 読めない。最後に観測した非 null ハンドルを保持し、フリング中のタブ切替でも最終アンカーを\n // 取りこぼさない (closure 経由の読取は detach 後も直近コミットの値で安全)\n const lastVirtualScrollHandleRef = useRef<VirtualScrollHandle | null>(null)\n\n const handleScroll = useCallback(() => {\n // スクロールのたびに先頭可視カードの同一性アンカーを共有スロットへ書き戻す。\n // ❗ 生の論理 px (onScroll 引数や getScrollPosition) を保存してはならない:\n // 可変高さでは再マウントの推定空間で px→行変換が狂い、別のカードに着地する。\n // getScrollAnchor は可視ウィンドウの実測が照合済みのツリーで計算されるため常に正確\n if (virtualScrollRef.current) {\n lastVirtualScrollHandleRef.current = virtualScrollRef.current\n }\n const restore = scrollRestoreRef?.current\n const handle = virtualScrollRef.current ?? lastVirtualScrollHandleRef.current\n if (restore && handle) {\n const anchor = handle.getScrollAnchor()\n const item = anchor ? dailyReportItemsRef.current[anchor.index] : undefined\n if (anchor && item) {\n restore.topItem = { reportHubId: item.reportHubId, index: anchor.index, offsetPx: anchor.offsetPx }\n }\n }\n\n const container = scrollContainerRef.current\n if (!container) return\n\n if (container.dataset.scrolling !== \"true\") {\n container.dataset.scrolling = \"true\"\n }\n\n if (scrollEndTimerRef.current) {\n clearTimeout(scrollEndTimerRef.current)\n }\n\n scrollEndTimerRef.current = setTimeout(() => {\n if (container) {\n delete container.dataset.scrolling\n }\n scrollEndTimerRef.current = null\n }, 150)\n }, [scrollRestoreRef])\n\n const renderThumbOverlay = useCallback((props: ScrollBarThumbOverlayRenderProps) => <BusinessDayThumbOverlay {...props} label={overlayLabel} isWheelScrollActive={isWheelScrollActive} />, [overlayLabel, isWheelScrollActive])\n\n /**\n * Sets up wheel event listener to toggle overlay visibility.\n * ホイールイベントリスナーを設定し、オーバーレイの表示状態を切り替える。\n */\n useEffect(() => {\n const el = scrollContainerRef.current\n if (!el) return\n const onWheel = () => {\n setIsWheelScrollActive(true)\n if (wheelResetTimerRef.current) clearTimeout(wheelResetTimerRef.current)\n wheelResetTimerRef.current = window.setTimeout(() => {\n setIsWheelScrollActive(false)\n wheelResetTimerRef.current = null\n }, WHEEL_RESET_TIMEOUT_MS)\n }\n el.addEventListener(\"wheel\", onWheel, { passive: true })\n return () => {\n el.removeEventListener(\"wheel\", onWheel)\n if (wheelResetTimerRef.current) clearTimeout(wheelResetTimerRef.current)\n }\n }, [])\n\n const updateItemHeight = useCallback((id: number, height: number) => {\n // 変化が無ければ何もしない (再測定の連打を吸収する)\n if (itemHeightsRef.current.get(id) === height) return\n itemHeightsRef.current.set(id, height)\n if (virtualScrollRef.current) {\n const index = dailyReportItemsRef.current.findIndex((item) => item.reportHubId === id)\n if (index !== -1) {\n // VirtualScroll に個別の高さ変更を伝える (これが再レイアウトの権威シグナル)\n virtualScrollRef.current.updateItemSize(index, height)\n }\n }\n }, [])\n\n const getItemHeight = useCallback(\n (index: number) => {\n const item = getItem(index)\n return itemHeightsRef.current.get(item.reportHubId) || 350\n },\n [getItem],\n )\n\n const getItemKey = useCallback(\n (index: number) => {\n const item = getItem(index)\n return item.reportHubId\n },\n [getItem],\n )\n\n /**\n * Updates viewport height when item count changes.\n * アイテム数変更時にビューポート高さを更新する。\n */\n useEffect(() => {\n if (itemCount >= 0) updateViewportHeight()\n }, [itemCount, updateViewportHeight])\n\n /**\n * Schedules initial prefetch when items are loaded.\n * アイテム読み込み時に初期プリフェッチをスケジュールする。\n */\n useEffect(() => {\n if (itemCount === 0) return\n schedulePrefetchForRange(0, Math.min(itemCount - 1, PREFETCH_LOOKAHEAD))\n }, [itemCount, schedulePrefetchForRange])\n\n const handleCommentAdded = useCallback((itemId: number) => {\n const index = dailyReportItemsRef.current.findIndex((item) => item.reportHubId === itemId)\n if (index >= 0) {\n setTimeout(() => virtualScrollRef.current?.scrollToIndex(index, { align: \"bottom\" }), 300)\n }\n }, [])\n\n const renderItem = useCallback(\n (item: DailyReportItem, index: number) => <DailyReportItemContainer key={item.reportHubId} item={item} index={index} setSize={itemCount} onHeightMeasured={updateItemHeight} onCommentAdded={handleCommentAdded} userId={userId} />,\n [updateItemHeight, handleCommentAdded, userId, itemCount],\n )\n\n return (\n <>\n <div ref={scrollContainerRef} className=\"flex min-h-0 flex-1 flex-col\">\n <VirtualScroll\n ref={virtualScrollRef}\n itemCount={itemCount}\n getItem={getItem}\n getItemKey={getItemKey}\n getItemHeight={getItemHeight}\n viewportSize={viewportHeight}\n initialScrollAnchor={initialScrollAnchor}\n overscanCount={VIRTUAL_SCROLL_OVERSCAN_COUNT}\n scrollBarOptions={{\n width: SCROLL_BAR_WIDTH,\n tapScrollCircleOptions: TAP_SCROLL_CIRCLE_OPTIONS,\n renderThumbOverlay: renderThumbOverlay,\n }}\n behaviorOptions={{\n // ドラッグスクロールは無効化せず、**ポインタ種別で絞る**。\n // 丸ごと切るとタッチ端末からスクロール手段が消える (定数の注記参照)\n pointerDragInputs: CONTENT_DRAG_POINTER_INPUTS,\n wheelSpeedMultiplier: FAST_SCROLL_WHEEL_MULTIPLIER,\n inertiaOptions: FAST_SCROLL_INERTIA_OPTIONS,\n // 行ラッパーへの tabIndex 汚染を止め、list/listitem のロール階層を有効にする\n enableKeyboardNavigation: false,\n }}\n onRangeChange={handleRangeChange}\n onScroll={handleScroll}\n testId=\"daily-report-detail-list\"\n className=\"relative w-full\"\n contentProps={{\n // 行 (role=\"listitem\") だけを所有する list をコンテンツ要素へ置く\n role: \"list\",\n \"aria-label\": title,\n }}>\n {renderItem}\n </VirtualScroll>\n </div>\n {itemCount === 0 && (\n <div className=\"py-8 text-center\">\n <p className=\"text-slate-500\">データがありません</p>\n </div>\n )}\n </>\n )\n}\n\n/**\n * Loading skeleton for the virtual scroll detail entry.\n * 仮想スクロール詳細行の読み込み中スケルトン表示。\n */\nconst SkeletonReportItem = () => (\n <div className=\"rounded-2xl border border-slate-200 bg-white p-4 shadow-sm dark:border-slate-700 dark:bg-slate-900/70\">\n <div className=\"mb-4 flex flex-wrap gap-2\">\n <div className=\"h-6 w-20 rounded-full bg-slate-200\"></div>\n <div className=\"h-6 w-24 rounded-full bg-slate-200\"></div>\n <div className=\"h-6 w-24 rounded-full bg-slate-200\"></div>\n <div className=\"h-6 w-28 rounded-full bg-slate-200\"></div>\n </div>\n <div className=\"mb-3 grid gap-3 md:grid-cols-[minmax(0,1fr)_220px]\">\n <div className=\"space-y-3\">\n <div className=\"h-5 w-40 rounded bg-slate-200\"></div>\n <div className=\"h-5 w-56 rounded bg-slate-200\"></div>\n <div className=\"h-24 rounded-lg bg-slate-200\"></div>\n </div>\n <div className=\"rounded-xl bg-slate-100 p-3\">\n <div className=\"mb-2 h-4 w-24 rounded bg-slate-200\"></div>\n <div className=\"mb-2 h-4 w-28 rounded bg-slate-200\"></div>\n <div className=\"h-4 w-32 rounded bg-slate-200\"></div>\n </div>\n </div>\n <div className=\"space-y-3\">\n <div className=\"h-4 w-24 rounded bg-slate-200\"></div>\n <div className=\"flex flex-wrap gap-2\">\n <div className=\"h-6 w-16 rounded-full bg-slate-200\"></div>\n <div className=\"h-6 w-16 rounded-full bg-slate-200\"></div>\n <div className=\"h-6 w-16 rounded-full bg-slate-200\"></div>\n </div>\n <div className=\"space-y-2\">\n <div className=\"h-16 rounded-lg bg-slate-200\"></div>\n <div className=\"h-16 rounded-lg bg-slate-200\"></div>\n </div>\n </div>\n </div>\n)\n\n/**\n * Chooses the appropriate daily report item representation based on fetch state.\n * フェッチ状態に応じて日報アイテムの表示コンポーネントを選択する関数。\n */\nconst DailyReportItemContainer = ({\n item,\n index,\n setSize,\n onHeightMeasured,\n onCommentAdded,\n userId,\n}: {\n item: DailyReportItem\n index: number\n setSize: number\n onHeightMeasured: (id: number, height: number) => void\n onCommentAdded?: (itemId: number) => void\n userId?: string | null\n}) => {\n const { report, error, isLoading, isRefetching } = useDailyReportDetail(item.reportHubId, item.businessDate)\n const { removeStaleItem } = useDailyReportActionContext()\n const { draftLabelName } = useDailyReportConfig()\n const [isEditing, setIsEditing] = useState(false)\n const [hasBypassedAutoEdit, setHasBypassedAutoEdit] = useState(false)\n // 自動編集モード (ドラフト検出による自動遷移) かどうかを追跡\n const autoEditingRef = useRef(false)\n\n // フェッチが一度でも試行されたかを追跡する ref\n const hasEverFetched = useRef(false)\n if (isRefetching) hasEverFetched.current = true\n\n // フェッチ完了後に report が null → stale アイテムとしてリストから除去する安全弁\n // 外部リソース同期: useDailyReportDetail のフェッチ結果に反応する\n useEffect(() => {\n if (!(report || isLoading || isRefetching || error) && hasEverFetched.current && item.reportHubId > 0) {\n removeStaleItem(item.reportHubId)\n }\n }, [report, isLoading, isRefetching, error, item.reportHubId, removeStaleItem])\n\n const isDraft = report?.labels.some((l) => l.name === draftLabelName)\n const isMine = userId && report?.userId === userId\n\n // ドラフトレポートの自動編集モード制御\n // - ドラフト状態かつ自分のレポート: 自動的にエディタモード\n // - SSE でドラフト→公開に遷移: 自動編集モードの場合のみビューモードへ解除\n useEffect(() => {\n if (report && isMine && isDraft && !isEditing && !hasBypassedAutoEdit) {\n setIsEditing(true)\n autoEditingRef.current = true\n } else if (report && !isDraft && isEditing && autoEditingRef.current) {\n // SSE で下書き→公開に遷移: 自動編集モードの場合のみビューモードへ\n setIsEditing(false)\n autoEditingRef.current = false\n }\n }, [report, isMine, isDraft, isEditing, hasBypassedAutoEdit])\n\n return (\n <DailyReportDetailRow itemId={item.reportHubId} index={index} setSize={setSize} onHeightMeasured={onHeightMeasured}>\n {error ? (\n <DailyReportError itemId={item.reportHubId} />\n ) : !report ? (\n <SkeletonReportItem />\n ) : isEditing ? (\n <DailyReportEditForm\n report={report}\n onCancel={() => {\n setIsEditing(false)\n setHasBypassedAutoEdit(true)\n }}\n onPublishSuccess={() => {\n setIsEditing(false)\n }}\n />\n ) : (\n <DailyReportResolvedItem item={item} report={report} onCommentAdded={onCommentAdded} userId={userId} onDoubleClick={isMine ? () => setIsEditing(true) : undefined} />\n )}\n </DailyReportDetailRow>\n )\n}\n\n/**\n * Renders the header section of a daily report.\n * 日報のヘッダー部分を描画するコンポーネント。\n */\nconst DailyReportHeader = ({\n report,\n isRead,\n isStarred,\n isMine,\n onToggleRead,\n onToggleStar,\n onDelete,\n itemSourceType,\n visitTimeDisplay,\n categoryLabel,\n fieldLabels,\n}: {\n report: DailyReportDetail\n itemSourceType?: string\n isRead: boolean\n isStarred: boolean\n isMine: boolean\n onToggleRead: () => void\n onToggleStar: () => void\n onDelete: () => void\n visitTimeDisplay: string\n categoryLabel: string\n fieldLabels: DailyReportFieldLabels\n}) => {\n const { sourceTypeConfigs } = useDailyReportConfig()\n return (\n <div className=\"flex flex-wrap items-center gap-2 font-medium text-slate-600 text-xs\">\n {/* 添付の有無を一目判別できるようにする。仕様書が定める\n [sourceType] [Star] [Read] の相対順は崩さず、その手前に置く */}\n <DailyReportAttachmentIndicator count={report.attachments.length} label={fieldLabels.attachments} />\n {(() => {\n const sourceType = itemSourceType ?? report?.sourceType\n const sourceConfig = resolveSourceTypeConfig(sourceType, sourceTypeConfigs)\n 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\n })()}\n <button\n type=\"button\"\n onClick={onToggleStar}\n 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\"\n title={isStarred ? \"スターを外す\" : \"スターを付ける\"}\n data-detail-star-button={report.reportHubId}>\n {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\" />}\n </button>\n <button\n type=\"button\"\n onClick={onToggleRead}\n 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\"\n title={isRead ? \"未読にする\" : \"既読にする\"}\n data-detail-read-button={report.reportHubId}>\n {isRead ? <Check className=\"h-3.5 w-3.5 text-blue-500\" /> : <UnreadIndicator className=\"text-xs\" />}\n </button>\n {isMine && (\n <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=\"削除する\">\n <Trash2 className=\"h-4 w-4 text-slate-300 hover:text-red-500\" />\n </button>\n )}\n <span className=\"inline-flex items-center gap-1 whitespace-nowrap rounded-full bg-blue-50 px-3 py-1 font-semibold text-blue-700 text-xs\">\n <span className=\"text-[10px] text-blue-500 uppercase tracking-wide\">{fieldLabels.id}</span>\n {report.reportHubId}\n </span>\n <span className=\"inline-flex items-center gap-1 whitespace-nowrap rounded-full bg-slate-100 px-3 py-1\">\n <span className=\"text-[10px] text-slate-400 uppercase tracking-wide\">{fieldLabels.businessDate}</span>\n {formatToIsoDate(report.date)}\n </span>\n <span className=\"inline-flex items-center gap-1 whitespace-nowrap rounded-full bg-slate-100 px-3 py-1\">\n <span className=\"text-[10px] text-slate-400 uppercase tracking-wide\">{fieldLabels.author}</span>\n {fallbackText(report.employeeName)}\n </span>\n <span className=\"inline-flex items-center gap-1 whitespace-nowrap rounded-full bg-slate-100 px-3 py-1\">\n <span className=\"text-[10px] text-slate-400 uppercase tracking-wide\">{fieldLabels.visitTime}</span>\n {visitTimeDisplay || \"-\"}\n </span>\n <span className=\"inline-flex items-center gap-1 whitespace-nowrap rounded-full bg-slate-100 px-3 py-1\">\n <span className=\"text-[10px] text-slate-400 uppercase tracking-wide\">{fieldLabels.updatedAt}</span>\n {formatToIsoDate(report.updatedAt)}\n </span>\n {categoryLabel && (\n <span className=\"inline-flex items-center gap-1 whitespace-nowrap rounded-full bg-amber-100 px-3 py-1 text-amber-700\">\n <span className=\"text-[10px] text-amber-500 uppercase tracking-wide\">{fieldLabels.category}</span>\n {categoryLabel}\n </span>\n )}\n </div>\n )\n}\n\n/**\n * Renders the side panel with detailed metadata.\n * 詳細なメタデータを表示するサイドパネルを描画するコンポーネント。\n */\nconst MetadataPanel = ({ report, visitTimeDisplay, categoryLabel, fieldLabels }: { report: DailyReportDetail; visitTimeDisplay: string; categoryLabel: string; fieldLabels: DailyReportFieldLabels }) => (\n <div className=\"flex flex-col gap-3 rounded-xl bg-slate-100 p-4 text-slate-600 text-xs dark:bg-slate-800/60 dark:text-slate-300\">\n <div className=\"flex flex-col gap-1\">\n <span className=\"font-semibold text-[10px] text-slate-500 uppercase tracking-wide dark:text-slate-400\">{fieldLabels.author}</span>\n <span className=\"text-slate-700 text-sm dark:text-slate-200\">{fallbackText(report.author)}</span>\n </div>\n <div className=\"flex flex-col gap-1\">\n <span className=\"font-semibold text-[10px] text-slate-500 uppercase tracking-wide dark:text-slate-400\">{fieldLabels.createdAt}</span>\n <span className=\"text-slate-700 text-sm dark:text-slate-200\">{formatToIsoDate(report.createdAt)}</span>\n </div>\n <div className=\"flex flex-col gap-1\">\n <span className=\"font-semibold text-[10px] text-slate-500 uppercase tracking-wide dark:text-slate-400\">{fieldLabels.updatedBy}</span>\n <span className=\"text-slate-700 text-sm dark:text-slate-200\">{fallbackText(report.updatedBy)}</span>\n </div>\n <div className=\"flex flex-col gap-1\">\n <span className=\"font-semibold text-[10px] text-slate-500 uppercase tracking-wide dark:text-slate-400\">{fieldLabels.updatedAt}</span>\n <span className=\"text-slate-700 text-sm dark:text-slate-200\">{formatToIsoDate(report.updatedAt)}</span>\n </div>\n <div className=\"flex flex-col gap-1\">\n <span className=\"font-semibold text-[10px] text-slate-500 uppercase tracking-wide dark:text-slate-400\">{fieldLabels.businessDate}</span>\n <span className=\"text-slate-700 text-sm dark:text-slate-200\">{formatToIsoDate(report.date)}</span>\n </div>\n {visitTimeDisplay && (\n <div className=\"flex flex-col gap-1\">\n <span className=\"font-semibold text-[10px] text-slate-500 uppercase tracking-wide dark:text-slate-400\">{fieldLabels.visitTime}</span>\n <span className=\"text-slate-700 text-sm dark:text-slate-200\">{visitTimeDisplay}</span>\n </div>\n )}\n {categoryLabel && (\n <div className=\"flex flex-col gap-1\">\n <span className=\"font-semibold text-[10px] text-slate-500 uppercase tracking-wide dark:text-slate-400\">{fieldLabels.categoryInfo}</span>\n <span className=\"text-slate-700 text-sm dark:text-slate-200\">{categoryLabel}</span>\n </div>\n )}\n </div>\n)\n\n/**\n * Renders the resolved daily report detail card with metadata and content.\n * 解決済みの日報詳細カードをメタデータと内容込みで描画する関数。\n */\nconst DailyReportResolvedItem = ({ item, report, onCommentAdded, userId, onDoubleClick }: { item: DailyReportItem; report: DailyReportDetail; onCommentAdded?: (itemId: number) => void; userId?: string | null; onDoubleClick?: () => void }) => {\n const formContainerRef = useRef<HTMLDivElement>(null)\n // 表示中ユーザーはアクションコンテキストから取得する (root loader 非依存)\n const { toggleStar, toggleRead, pendingStarUpdates, pendingReadUpdates, deleteReport, user } = useDailyReportActionContext()\n const { uiComments, handleAddComment, handleDeleteComment, pendingDeleteId, setPendingDeleteId, resolvedIdMap } = useDailyReportComments(report, user, userId)\n const { fieldLabels, sourceTypeConfigs } = useDailyReportConfig()\n\n const sourceType = item.sourceType ?? report?.sourceType\n const sourceConfig = resolveSourceTypeConfig(sourceType, sourceTypeConfigs)\n const isExternalSource = sourceType ? sourceType.toLowerCase() !== \"internal\" : false\n const canComment = sourceConfig?.allowComment ?? !isExternalSource\n\n const handleAddCommentWrapper = async (content: string) => {\n await handleAddComment(content)\n onCommentAdded?.(item.reportHubId)\n }\n\n const isStarred = pendingStarUpdates.current.has(report.reportHubId) ? (pendingStarUpdates.current.get(report.reportHubId) ?? false) : report.isStarred\n const isRead = pendingReadUpdates.current.has(report.reportHubId) ? (pendingReadUpdates.current.get(report.reportHubId) ?? false) : report.isRead\n const isMine = !!(userId && report.userId === userId)\n\n const handleToggleRead = async () => {\n try {\n await toggleRead(report.reportHubId, !isRead, report.date ?? \"\")\n } catch {}\n }\n\n const handleToggleStar = async () => {\n try {\n await toggleStar(report.reportHubId, !isStarred, report.date ?? \"\")\n } catch {}\n }\n\n const handleDelete = async () => {\n if (!confirm(\"本当に削除しますか?\")) return\n await deleteReport(report.reportHubId, report.date ?? \"\")\n }\n\n const normalizedContent = useMemo(() => (report.content ? report.content.replace(/<br\\s*\\/??\\s*>/gi, \"\\n\").trim() : \"\"), [report.content])\n const visitTimeDisplay = useMemo(() => {\n const from = report.visitTimeFrom?.trim()\n const to = report.visitTimeTo?.trim()\n return from && to ? `${from} - ${to}` : from || to || \"\"\n }, [report.visitTimeFrom, report.visitTimeTo])\n const categoryLabel = useMemo(() => {\n const labels: string[] = []\n if (report.category?.trim()) labels.push(`${fieldLabels.category}: ${report.category}`)\n if (report.creationCategory?.trim()) labels.push(`${fieldLabels.creationCategory}: ${report.creationCategory}`)\n return labels.join(\" / \")\n }, [report.category, report.creationCategory, fieldLabels.category, fieldLabels.creationCategory])\n\n return (\n // biome-ignore lint/a11y/noStaticElementInteractions: Double click to edit feature\n <div\n onDoubleClick={onDoubleClick}\n className={`flex h-full w-full flex-col rounded-2xl border bg-white p-4 shadow-sm transition-all duration-200 hover:-translate-y-0.5 hover:border-blue-300 hover:shadow-lg dark:border-slate-700 dark:bg-slate-900/70 ${!isRead ? \"border-l-4 border-l-blue-500\" : \"\"}`}>\n <div className=\"flex flex-col gap-4\">\n <DailyReportHeader\n report={report}\n itemSourceType={item.sourceType}\n isRead={isRead}\n isStarred={isStarred}\n isMine={isMine}\n onToggleRead={handleToggleRead}\n onToggleStar={handleToggleStar}\n onDelete={handleDelete}\n visitTimeDisplay={visitTimeDisplay}\n categoryLabel={categoryLabel}\n fieldLabels={fieldLabels}\n />\n <div className=\"grid gap-4 md:grid-cols-[minmax(0,1fr)_240px]\">\n <div className=\"flex flex-col gap-4\">\n <div className=\"grid gap-3 sm:grid-cols-2\">\n <div className=\"flex flex-col gap-1\">\n <span className=\"font-semibold text-slate-500 text-xs uppercase tracking-wide\">{fieldLabels.customer}</span>\n <span className=\"font-semibold text-base text-slate-900 dark:text-slate-100\">{fallbackText(report.customerName)}</span>\n </div>\n <div className=\"flex flex-col gap-1\">\n <span className=\"font-semibold text-slate-500 text-xs uppercase tracking-wide\">{fieldLabels.subject}</span>\n <span className=\"text-slate-700 text-sm dark:text-slate-200\">{fallbackText(report.subject)}</span>\n </div>\n </div>\n {normalizedContent && (\n <div className=\"flex flex-col gap-2\">\n <span className=\"font-semibold text-slate-500 text-xs uppercase tracking-wide\">{fieldLabels.content}</span>\n <div className=\"whitespace-pre-wrap rounded-xl bg-slate-50 p-3 text-slate-700 text-sm leading-relaxed dark:bg-slate-800/60 dark:text-slate-200\">{normalizedContent}</div>\n </div>\n )}\n {report.interviewers.length > 0 && (\n <div className=\"flex flex-col gap-2\">\n <span className=\"font-semibold text-slate-500 text-xs uppercase tracking-wide\">{fieldLabels.interviewers}</span>\n <ul className=\"flex flex-wrap gap-2\">\n {report.interviewers.map((interviewer) => (\n <li key={`${interviewer.name}-${interviewer.affiliation || \"\"}`} className=\"rounded-full bg-slate-100 px-3 py-1 text-slate-600 text-xs\">\n {formatInterviewerLabel(interviewer)}\n </li>\n ))}\n </ul>\n </div>\n )}\n <DailyReportAttachmentList attachments={report.attachments} />\n {uiComments.length > 0 && (\n <div className=\"flex flex-col gap-2\">\n <span className=\"font-semibold text-slate-500 text-xs uppercase tracking-wide\">{fieldLabels.comments}</span>\n <DailyReportCommentList\n comments={uiComments}\n reportHubId={report.reportHubId}\n businessDate={report.date ?? \"\"}\n onDeleteComment={handleDeleteComment}\n pendingDeleteId={pendingDeleteId}\n onPendingDeleteIdChange={setPendingDeleteId}\n resolvedIdMap={resolvedIdMap}\n />\n </div>\n )}\n {canComment && (\n <div className=\"mt-auto\" ref={formContainerRef}>\n <DailyReportCommentForm reportHubId={report.reportHubId} businessDate={report.date ?? \"\"} onAddComment={handleAddCommentWrapper} />\n </div>\n )}\n </div>\n <MetadataPanel report={report} visitTimeDisplay={visitTimeDisplay} categoryLabel={categoryLabel} fieldLabels={fieldLabels} />\n </div>\n </div>\n </div>\n )\n}\n\n/**\n * Displays an error banner when daily report detail retrieval fails.\n * 日報詳細の取得に失敗した際のエラーバナーを表示する関数。\n */\nconst DailyReportError = ({ itemId }: { itemId: number }) => (\n <div className=\"rounded-2xl border border-red-200 bg-red-50 p-4 text-red-700 text-sm\">\n <p>日報 {itemId.toLocaleString()} の読み込みに失敗しました。</p>\n </div>\n)\n","/**\n * Action context providing optimistic CRUD + SSE synchronization for daily reports.\n * 日報の楽観的 CRUD と SSE 同期を提供するアクションコンテキスト。\n */\nimport { type Context, createContext, type ReactNode, useCallback, useContext, useEffect, useRef, useState } from \"react\"\nimport { compareIdsStreamOrder, isIdsStreamOrdered, upsertInIdsStreamOrder } from \"../../shared/ids-stream\"\nimport { createLogger, LogLevel } from \"../../shared/logger\"\nimport type { DailyReportSseMessage } from \"../../shared/sse-schema\"\nimport type { DailyReportCommentItem, DailyReportDetail, DailyReportItem, DailyReportUser } from \"../../shared/types\"\nimport { useDailyReportConfig } from \"../config-context\"\nimport {\n acquireMutationLock,\n applyBroadcastReportUpdate,\n applyDailyReportServerUpdates,\n clearDailyReportCache,\n DailyReportCache,\n deleteDailyReportCache,\n getCachedReport,\n registerRecentDeletion,\n releaseMutationLock,\n subscribeCacheReady,\n updateDailyReportCache,\n writeCache,\n} from \"../hooks/use-daily-report\"\nimport { useDailyReportSseConnection } from \"../hooks/use-daily-report-sse-connection\"\nimport { getDailyReportIdsStreamSessionUserKey, refreshDailyReportIdsStream, removeDailyReportIdsStreamItem, upsertDailyReportIdsStreamItem, useDailyReportIdsStream } from \"../streaming/daily-report-ids-stream-session\"\nimport { SSE_INITIAL_CONNECT_DEFER_MS } from \"../utils/constants\"\nimport { useDailyReportErrorSurface } from \"./daily-report-error-context\"\n\nconst logger = createLogger(LogLevel.INFO, \"daily-report-action-context\")\n\n/**\n * Operation intent -> default banner label (Japanese).\n * 操作 intent -> 既定バナー用の日本語ラベル。ホストは config.onError で構造化情報 (operation)\n * を受け取り独自メッセージへ差し替えできる。\n */\nconst OPERATION_LABELS: Record<string, string> = {\n create: \"日報の作成\",\n update: \"日報の保存\",\n publish: \"日報の公開\",\n delete: \"日報の削除\",\n addComment: \"コメントの投稿\",\n deleteComment: \"コメントの削除\",\n toggleStar: \"スターの更新\",\n toggleRead: \"既読状態の更新\",\n}\n\n/** intent から利用者向けの失敗メッセージを組み立てる。 */\nconst describeOperationFailure = (intent: string): string => {\n const label = OPERATION_LABELS[intent] ?? \"操作\"\n return `${label}に失敗しました。時間をおいて再度お試しください。`\n}\n\n/**\n * Stable empty default for the optional `initialItems` prop.\n * 省略可能な `initialItems` の安定した空既定値。\n *\n * 既定を `= []` にすると毎レンダーで新しい配列参照になり、render-phase の\n * `initialItems !== prevInitialItems` 判定が常に真になって setState が無限ループし、\n * React が描画を中断する。単一の凍結参照を共有して同一性を保つ。\n */\nconst EMPTY_INITIAL_ITEMS: DailyReportItem[] = Object.freeze([]) as readonly DailyReportItem[] as DailyReportItem[]\n\ntype ActionResponse = {\n status: string\n reportHubId: number\n newComment?: DailyReportCommentItem\n updatedStatus?: { isStarred?: boolean; isRead?: boolean }\n deletedCommentId?: string\n updatedAt?: string // ISO string from server\n report?: DailyReportDetail // Full report for creation/updates\n clientTempId?: string // Validation token for optimistic updates\n [key: string]: unknown\n}\n\nexport type DailyReportActionContextType = {\n /** 表示中ユーザー (楽観的コメントの作者名等に使用)。 */\n user?: DailyReportUser\n /** reportHubId -> Map<tempCommentId, content> (追加コメントの一時IDと内容) */\n pendingAddCommentIds: React.MutableRefObject<Map<number, Map<number, string>>>\n /** reportHubId -> Set<tempCommentId> (削除された一時ID) */\n pendingDeleteCommentIds: React.MutableRefObject<Map<number, Set<number>>>\n /** reportHubId -> boolean (スターの保留状態) */\n pendingStarUpdates: React.MutableRefObject<Map<number, boolean>>\n /** reportHubId -> boolean (既読の保留状態) */\n pendingReadUpdates: React.MutableRefObject<Map<number, boolean>>\n /** tempId -> realId (解決されたID) */\n resolvedIdMap: React.MutableRefObject<Map<number, number>>\n /** 状態変更通知用バージョン */\n version: number\n /** 日報リスト */\n items: DailyReportItem[]\n /** SSE購読が有効かどうか */\n isSseEnabled: boolean\n /** SSE購読の有効/無効を切り替える */\n toggleSse: () => void\n\n /**\n * コメントを追加します。\n * @param reportHubId 日報HubID\n * @param content コメント内容\n * @param businessDate 営業日\n */\n addComment: (reportHubId: number, content: string, businessDate: string) => Promise<void>\n /**\n * コメントを削除します。\n * @param reportHubId 日報HubID\n * @param commentId コメントID (一時IDまたは実ID)\n * @param businessDate 営業日\n */\n deleteComment: (reportHubId: number, commentId: number, businessDate: string) => Promise<void>\n /**\n * スターの状態を切り替えます。\n * @param reportHubId 日報HubID\n * @param isStarred 新しいスター状態\n * @param businessDate 営業日\n */\n toggleStar: (reportHubId: number, isStarred: boolean, businessDate: string) => Promise<void>\n /**\n * 既読の状態を切り替えます。\n * @param reportHubId 日報HubID\n * @param isRead 新しい既読状態\n * @param businessDate 営業日\n */\n toggleRead: (reportHubId: number, isRead: boolean, businessDate: string) => Promise<void>\n createReport: (businessDate: string) => Promise<number>\n updateReport: (reportHubId: number, data: { title?: string; content?: string }, businessDate: string) => Promise<void>\n publishReport: (reportHubId: number, businessDate: string) => Promise<void>\n deleteReport: (reportHubId: number, businessDate: string) => Promise<void>\n /** stale アイテムをリストから除去する安全弁 */\n removeStaleItem: (reportHubId: number) => void\n /** データのみリロード (ページリロードなし) */\n refetchData: () => Promise<void>\n /** キャッシュを破棄してデータのみ再読み込み (ページリロードなし) */\n clearCacheAndRefetch: () => Promise<void>\n}\n\n// HMR / dual-bundle (ESM+CJS) でもコンテキスト identity を一意に保つため、\n// グローバルへ登録した単一インスタンスを常に使用する。\ndeclare global {\n var __DailyReportActionContext: Context<DailyReportActionContextType | null> | undefined\n}\n\nconst DailyReportActionContext: Context<DailyReportActionContextType | null> = globalThis.__DailyReportActionContext ?? createContext<DailyReportActionContextType | null>(null)\nglobalThis.__DailyReportActionContext = DailyReportActionContext\n\nexport const useDailyReportActionContext = () => {\n const context = useContext(DailyReportActionContext)\n if (!context) {\n throw new Error(\"useDailyReportActionContext must be used within a DailyReportActionProvider\")\n }\n return context\n}\n\nconst getUserDisplayName = (user: DailyReportUser | undefined): string => {\n if (!user) return \"Unknown\"\n if (user.displayName?.trim()) return user.displayName\n if (user.name) {\n const fullName = `${user.name.familyName ?? \"\"} ${user.name.givenName ?? \"\"}`.trim()\n if (fullName) return fullName\n }\n return user.emails?.[0]?.value ?? \"Unknown\"\n}\n\n/**\n * Provides a context for daily report actions including comments, stars, leads, and basic CRUD operations.\n * Also manages optimistic UI updates and real-time synchronization via SSE.\n * 日報のアクション(コメント追加・削除、スター、既読)を管理するプロバイダー。\n * 楽観的UI更新とAPI通信の抽象化を提供します。\n */\nexport const DailyReportActionProvider = ({ children, user, initialItems = EMPTY_INITIAL_ITEMS, userId }: { children: ReactNode; user?: DailyReportUser; initialItems?: DailyReportItem[]; userId?: string | null }) => {\n const { apiBasePath, draftLabelName } = useDailyReportConfig()\n // ミューテーション失敗の通知シーム。executeAction の catch から呼び、失敗を可視化する。\n // ref 経由で読み、executeAction の依存に載せない (通知参照は安定しているが防御的に)\n const { notifyError } = useDailyReportErrorSurface()\n const notifyErrorRef = useRef(notifyError)\n notifyErrorRef.current = notifyError\n const pendingAddCommentIds = useRef<Map<number, Map<number, string>>>(new Map())\n const pendingDeleteCommentIds = useRef<Map<number, Set<number>>>(new Map())\n const pendingStarUpdates = useRef<Map<number, boolean>>(new Map())\n const pendingReadUpdates = useRef<Map<number, boolean>>(new Map())\n const resolvedIdMap = useRef<Map<number, number>>(new Map())\n const [version, setVersion] = useState(0)\n const [items, setItems] = useState<DailyReportItem[]>(initialItems)\n // SSE は初回描画とその最初の中身フェッチに接続枠を譲るため、少し遅らせて自動接続する\n // (HTTP/1.1 の 6 接続のうち 1 枠を初回描画へ回す)。手動トグル済みなら自動接続しない\n const [isSseEnabled, setIsSseEnabled] = useState(false)\n const hasManualSseToggleRef = useRef(false)\n const pendingAddPromises = useRef<Map<number, Promise<number>>>(new Map())\n const manuallyTrackingIds = useRef<Set<number>>(new Set())\n /** 削除済み ID を記録し、マージロジックでの復活を防止する */\n const removedIds = useRef<Set<number>>(new Set())\n /** 自分が発行したアクションのID(SSEエコー無視用) */\n const processedClientTempIds = useRef<Set<string>>(new Set())\n\n // トゥームストーン掃除のゲート参照。ストリーミング途中の部分配列で掃除すると、\n // 後続チャンク到着前に削除された ID のトゥームストーンが失われ、遅れて届いた\n // 削除前スナップショットのチャンクで日報が復活する (effect の依存を増やさないため ref 経由)\n const streamState = useDailyReportIdsStream()\n const isStreamSettledRef = useRef(false)\n isStreamSettledRef.current = streamState.phase === \"complete\" && !streamState.isRevalidating\n\n /**\n * 遅延解決するミューテーション継続の門番。POST /action の待機中にリロード無しの\n * ユーザー切替が起きた場合、この判定が偽になり、継続はモジュール常駐状態\n * (常駐ストリームセッション・DailyReportCache・ミューテーションロック) への\n * 書き込みを放棄する。放棄しないと、旧ユーザーの日報行が新ユーザーの一覧へ\n * 注入され (観測集合に刻まれて完走剪定も免れる)、切替時に破棄したはずの\n * 詳細キャッシュへ旧ユーザーの下書き本文が再シードされる (実測再現済み)。\n * 同一ユーザーの通常ナビゲーション (アンマウント) ではセッションが生き続けるため\n * 真のままであり、作成直後の日報が常駐データから欠ける round-3 の退行は起こさない。\n */\n const isSessionStillOurs = useCallback(() => getDailyReportIdsStreamSessionUserKey() === (userId ?? null), [userId])\n\n const refetchData = useCallback(async () => {\n logger.info(\"[DailyReportActionProvider] Refetching data without page reload...\")\n // ID 一覧はストリームセッションのバックグラウンド再検証で更新する\n // (ルーター再検証は dailyReportShouldRevalidate に抑止されるため使わない)\n refreshDailyReportIdsStream()\n setVersion((v) => v + 1)\n DailyReportCache.listeners.forEach((listener: (id: number) => void) => {\n listener(-1)\n })\n }, [])\n\n const clearCacheAndRefetch = useCallback(async () => {\n logger.info(\"[DailyReportActionProvider] Clearing client & server/DB cache and refetching data without page reload...\")\n // 1. クライアント側キャッシュ破棄\n clearDailyReportCache()\n removedIds.current.clear()\n try {\n sessionStorage.clear()\n } catch {}\n\n // 2. サーバー/DB側キャッシュ破棄 API 呼び出し\n try {\n const formData = new FormData()\n formData.append(\"intent\", \"clearCache\")\n const res = await fetch(`${apiBasePath}/action`, {\n method: \"POST\",\n body: formData,\n })\n if (!res.ok) {\n const errData = await res.json().catch(() => null)\n logger.warn(\"[DailyReportActionProvider] Failed to clear server-side cache:\", res.status, errData)\n }\n } catch (e) {\n logger.warn(\"[DailyReportActionProvider] Failed to clear server-side cache:\", e)\n }\n\n // 3. ID 一覧ストリームをサーバーキャッシュバイパスで再検証 & UI 再描画。\n // POST 待機中にユーザー切替が起きた場合は放棄する (新ユーザーのストリームを\n // 冗長に強制再ダウンロードさせない)\n if (!isSessionStillOurs()) return\n refreshDailyReportIdsStream({ forceRefresh: true })\n setVersion((v) => v + 1)\n DailyReportCache.listeners.forEach((listener: (id: number) => void) => {\n listener(-1)\n })\n }, [apiBasePath, isSessionStillOurs])\n\n /**\n * Queue for SSE messages that arrived before the target report was cached.\n * キャッシュ未取得時に到着した SSE メッセージの待機キュー。\n * key: reportHubId, value: { messages, unsubscribe, timer }\n */\n const pendingSseQueue = useRef<\n Map<\n number,\n {\n messages: Exclude<DailyReportSseMessage, { type: \"connected\" }>[]\n unsubscribe: () => void\n timer: ReturnType<typeof setTimeout>\n }\n >\n >(new Map())\n\n /**\n * Ref to always access the latest handleSseMessage for queue replay.\n * キューの再処理時に常に最新の handleSseMessage を参照するための ref。\n */\n const handleSseMessageRef = useRef<(payload: Exclude<DailyReportSseMessage, { type: \"connected\" }>) => void>(() => {})\n\n /**\n * Enqueue an SSE message for a report that isn't cached yet.\n * Subscribes to cache-ready events and replays queued messages when the cache becomes available.\n *\n * キャッシュ未取得のレポートへの SSE メッセージをキューに追加する。\n * キャッシュ書き込みを監視し、書き込まれたらキューのメッセージを再処理する。\n */\n const enqueuePendingSseMessage = useCallback((reportHubId: number, message: Exclude<DailyReportSseMessage, { type: \"connected\" }>) => {\n const queue = pendingSseQueue.current\n const existing = queue.get(reportHubId)\n\n if (existing) {\n // 既存キューにメッセージを追加するだけ\n existing.messages.push(message)\n return\n }\n\n // 新規キューを作成\n const entry = {\n messages: [message],\n unsubscribe: () => {},\n timer: setTimeout(() => {\n // 30秒経過してもキャッシュが来なければキューを破棄\n const e = queue.get(reportHubId)\n if (e) {\n e.unsubscribe()\n queue.delete(reportHubId)\n logger.warn(`[SSE Queue] Expired: reportHubId=${reportHubId}, dropped=${e.messages.length} messages`)\n }\n }, 30_000),\n }\n\n // キャッシュ書き込みを監視\n entry.unsubscribe = subscribeCacheReady(reportHubId, () => {\n const e = queue.get(reportHubId)\n if (!e) return\n\n // キャッシュが書き込まれたか確認\n if (!getCachedReport(reportHubId, { ignoreExpiry: true })) return\n\n // キューをクリーンアップ\n e.unsubscribe()\n clearTimeout(e.timer)\n queue.delete(reportHubId)\n\n // キューのメッセージを最新の handleSseMessage で再処理\n logger.info(`[SSE Queue] Replaying ${e.messages.length} messages for reportHubId=${reportHubId}`)\n for (const msg of e.messages) {\n handleSseMessageRef.current(msg)\n }\n })\n\n queue.set(reportHubId, entry)\n }, [])\n\n const addProcessedId = (id: string) => {\n processedClientTempIds.current.add(id)\n // Auto-remove after 30 seconds to prevent memory leaks\n // 30秒後に自動削除(メモリリーク防止)\n setTimeout(() => {\n processedClientTempIds.current.delete(id)\n }, 30000)\n }\n\n const isProcessedId = useCallback((id: string | undefined): boolean => {\n return !!id && processedClientTempIds.current.has(id)\n }, [])\n\n /**\n * Upserts a report item into the list while keeping businessDate-desc order.\n *\n * レポートアイテムをリストへ upsert し、businessDate 降順を維持します。\n */\n const upsertItem = useCallback((reportHubId: number, businessDate: string | null | undefined) => {\n // モジュール常駐ストリームセッションへも反映する (鮮度窓内の再訪で作成直後の日報が\n // 消えないように)。businessDate 未指定は「既存値を保持」の意図なので中継しない\n if (businessDate !== undefined) {\n upsertDailyReportIdsStreamItem({ reportHubId, businessDate: businessDate ?? null })\n }\n setItems((prev) => {\n const existing = prev.find((item) => item.reportHubId === reportHubId)\n const nextBusinessDate = businessDate ?? existing?.businessDate ?? null\n // 正準比較器 (営業日降順 → ID 降順) の位置へ二分探索で upsert する。\n // 以前は SSE 1 件ごとに全件を localeCompare でソートし直していた (O(n log n)\n // かつ ID 降順の副キー無しで、同日内の新規作成が誤った位置に描画されていた)\n return upsertInIdsStreamOrder(prev, { reportHubId, businessDate: nextBusinessDate, sourceType: existing?.sourceType })\n })\n }, [])\n\n // 未登録ユーザーは SSE を有効化できない\n const toggleSse = () => {\n if (!userId) return\n // 手動操作後は自動接続タイマーに上書きさせない\n hasManualSseToggleRef.current = true\n setIsSseEnabled((prev) => !prev)\n }\n\n /**\n * Defers the initial SSE auto-connect until just after first paint.\n * 初回 SSE 自動接続を初回描画直後まで遅らせる副作用。\n *\n * 依存配列: userId (ユーザーごとに provider が key リマウントされるため、切替で張り直す)。\n * クリーンアップ: タイマー解除 (アンマウント/再マウントでのリークと二重発火を防ぐ)。\n */\n useEffect(() => {\n if (!userId) return\n const timer = setTimeout(() => {\n // 手動でトグルされていなければ自動接続する\n if (!hasManualSseToggleRef.current) setIsSseEnabled(true)\n }, SSE_INITIAL_CONNECT_DEFER_MS)\n return () => clearTimeout(timer)\n }, [userId])\n\n /**\n * Remove a stale item from the list as a safety net.\n * フェッチ後にレポートが見つからなかった場合の安全弁としてアイテムを除去する。\n */\n const removeStaleItem = useCallback(\n (reportHubId: number) => {\n // ユーザー切替後の遅延発火は新ユーザーのセッションへトゥームストーンを刻んでしまう\n if (!isSessionStillOurs()) return\n removedIds.current.add(reportHubId)\n // 常駐ストリームデータからも除去する (再訪時に安全弁が再発動し続けるのを防ぐ)\n removeDailyReportIdsStreamItem(reportHubId)\n setItems((prev) => prev.filter((item) => item.reportHubId !== reportHubId))\n },\n [isSessionStillOurs],\n )\n\n /**\n * Merge initialItems with prevItems instead of replacing (render-phase derived state).\n * Items already in the list are kept even if temporarily absent from the API response.\n * Removal is handled exclusively by the report-delete SSE handler.\n *\n * initialItems と prevItems をマージする (全置換ではない)。render-phase の\n * 派生状態パターン (prev 比較 + render 中の setState) で行うことが重要 —\n * effect で行うと「最初のチャンクのコミットは items が空 (VirtualScroll が\n * 一瞬 No items を描画) → effect 後の 2 コミット目で行が出る」という\n * 1 コミット分の遅延と空表示フラッシュが生まれる。render 中の setState は\n * React が子のコミット前に同一コンポーネントを再レンダーする公式パターン。\n * マージは純粋 (ref は読み取りのみ)。StrictMode の二重レンダーにも冪等。\n */\n const [prevInitialItems, setPrevInitialItems] = useState(initialItems)\n if (initialItems !== prevInitialItems) {\n setPrevInitialItems(initialItems)\n // initialItems をベースにマップを構築 (removedIds を除外)\n const mergedMap = new Map<number, DailyReportItem>()\n for (const item of initialItems) {\n if (!removedIds.current.has(item.reportHubId)) {\n mergedMap.set(item.reportHubId, item)\n }\n }\n // 現在の items にのみ存在する ID を保持 (削除は report-delete SSE で行う)\n for (const item of items) {\n if (!(mergedMap.has(item.reportHubId) || removedIds.current.has(item.reportHubId))) {\n mergedMap.set(item.reportHubId, item)\n }\n }\n // 並び順は正準比較器 (compareIdsStreamOrder: 営業日降順 → ID 降順、null 営業日は\n // 末尾) に一本化する。ストリームはソート順どおりに届き末尾へ追記されるため通常は\n // 整列済み — O(n) の検査で確認できたらソートを丸ごと省略する (20 万件級で毎 publish\n // の O(n log n) を O(n) へ落とす)。以前は営業日のみの比較で ID 降順の副キーが無く、\n // 前回全量走査以降に作成された日報が同日グループ内の誤った位置に描画されていた\n const merged = Array.from(mergedMap.values())\n if (!isIdsStreamOrdered(merged)) {\n merged.sort(compareIdsStreamOrder)\n }\n setItems(merged)\n }\n\n /**\n * トゥームストーン (removedIds) のクリーンアップ副作用。\n * ストリームの全量が出揃っている (complete かつ再検証中でない) ときにのみ行う。\n * 部分配列からの欠落は「サーバーに存在しない」証拠にならない。\n * 依存配列: initialItems (チャンク到着・再検証ごと)。クリーンアップ: なし。\n * ref の変異を伴うため render-phase ではなく effect に置く。\n */\n useEffect(() => {\n if (isStreamSettledRef.current) {\n const initialIds = new Set(initialItems.map((item) => item.reportHubId))\n // Set のイテレーション中の delete は JS 仕様で安全 (visited は再訪問しない)\n for (const id of removedIds.current) {\n if (!initialIds.has(id)) {\n removedIds.current.delete(id)\n }\n }\n }\n }, [initialItems])\n\n const handleSseMessage = useCallback(\n (payload: Exclude<DailyReportSseMessage, { type: \"connected\" }>) => {\n // セッション切替〜プロバイダーアンマウントの隙間で届いた旧ユーザーの SSE は破棄する。\n // clientLoader の bootstrap は React のコミット前にモジュールセッションを新ユーザーへ\n // 差し替えるため、旧プロバイダーの EventSource が閉じるまでの間に届いたメッセージ\n // (下書きの recipient 宛配信を含む) が、破棄済みキャッシュと新セッションへ\n // 旧ユーザーのデータを書き込んでしまう (POST 継続と同じ汚染シグネチャ・実測再現済み)\n if (!isSessionStillOurs()) {\n return\n }\n // Ignore echo of own actions\n // 自分が発行したアクションのエコーは無視する\n // clientTempId は connected 以外すべてのメッセージに存在する(型で保証済み)\n if (isProcessedId(payload.clientTempId)) {\n return\n }\n\n switch (payload.type) {\n case \"comment-add\": {\n const { comment, reportHubId } = payload\n const cached = getCachedReport(reportHubId, { ignoreExpiry: true })\n if (cached) {\n // Check for duplicates\n // 重複チェック\n if (cached.commentItems.some((c) => c.id === comment.id)) return\n\n // Check if this comment matches a pending optimistic comment\n let isMyComment = false\n const pendingMap = pendingAddCommentIds.current.get(reportHubId)\n if (pendingMap) {\n for (const [tempId, content] of pendingMap.entries()) {\n if (content === comment.content) {\n pendingMap.delete(tempId)\n resolvedIdMap.current.set(tempId, comment.id)\n isMyComment = true\n break\n }\n }\n }\n\n const newComment: DailyReportCommentItem = {\n ...comment,\n userName: comment.userName || (isMyComment ? getUserDisplayName(user) : \"Unknown\"),\n createdAt: comment.createdAt || new Date().toISOString(),\n isMine: isMyComment, // Use the detected ownership\n }\n\n const updatedReport = {\n ...cached,\n commentItems: [...cached.commentItems, newComment].sort((a, b) => new Date(a.createdAt || 0).getTime() - new Date(b.createdAt || 0).getTime()),\n }\n applyDailyReportServerUpdates(reportHubId, updatedReport)\n // Update mutation time to prevent stale fetch overwrite\n // 古いフェッチによる上書きを防ぐために変更時刻を更新\n acquireMutationLock(reportHubId)\n releaseMutationLock(reportHubId)\n setVersion((v) => v + 1) // UI更新トリガー\n } else {\n // キャッシュ未取得 → フェッチ完了後にリプレイ\n enqueuePendingSseMessage(reportHubId, payload)\n }\n break\n }\n case \"comment-delete\": {\n const { commentId, reportHubId } = payload\n const cached = getCachedReport(reportHubId, { ignoreExpiry: true })\n if (cached) {\n const updatedReport = {\n ...cached,\n commentItems: cached.commentItems.filter((c) => c.id !== commentId),\n }\n applyDailyReportServerUpdates(reportHubId, updatedReport)\n // Update mutation time to prevent stale fetch overwrite\n // 古いフェッチによる上書きを防ぐために変更時刻を更新\n acquireMutationLock(reportHubId)\n releaseMutationLock(reportHubId)\n setVersion((v) => v + 1)\n } else {\n enqueuePendingSseMessage(reportHubId, payload)\n }\n break\n }\n case \"status-update\": {\n const { statusType, value, reportHubId } = payload\n const cached = getCachedReport(reportHubId, { ignoreExpiry: true })\n\n if (statusType === \"star\") {\n if (pendingStarUpdates.current.get(reportHubId) === value) {\n pendingStarUpdates.current.delete(reportHubId)\n }\n if (cached) {\n const updatedReport = { ...cached, isStarred: value }\n applyDailyReportServerUpdates(reportHubId, updatedReport)\n setVersion((v) => v + 1)\n } else {\n enqueuePendingSseMessage(reportHubId, payload)\n }\n } else if (statusType === \"read\") {\n if (pendingReadUpdates.current.get(reportHubId) === value) {\n pendingReadUpdates.current.delete(reportHubId)\n }\n if (cached) {\n const updatedReport = { ...cached, isRead: value }\n applyDailyReportServerUpdates(reportHubId, updatedReport)\n setVersion((v) => v + 1)\n } else {\n enqueuePendingSseMessage(reportHubId, payload)\n }\n }\n break\n }\n case \"report-delete\": {\n const { reportHubId } = payload\n manuallyTrackingIds.current.delete(reportHubId)\n // マージロジックでの復活を防止するために削除済み ID を記録\n removedIds.current.add(reportHubId)\n // 常駐ストリームデータからも除去 (鮮度窓内の再訪で削除済み日報が蘇らないように)\n removeDailyReportIdsStreamItem(reportHubId)\n\n // 削除されたレポートへの待機中 SSE メッセージを破棄\n const pendingEntry = pendingSseQueue.current.get(reportHubId)\n if (pendingEntry) {\n pendingEntry.unsubscribe()\n clearTimeout(pendingEntry.timer)\n pendingSseQueue.current.delete(reportHubId)\n }\n\n const cached = getCachedReport(reportHubId, { ignoreExpiry: true })\n if (cached?.date) {\n deleteDailyReportCache(reportHubId, cached.date)\n setVersion((v) => v + 1)\n }\n setItems((prev) => prev.filter((item) => item.reportHubId !== reportHubId))\n break\n }\n case \"report-create\":\n case \"report-update\":\n case \"report-publish\": {\n // report はスキーマで必須なのでバリデーション通過後は必ず存在する(if ガード不要)\n const { report } = payload\n // 全ユーザー放送: report の isRead/isStarred は操作者の状態なので、\n // 受信者自身の per-viewer 状態を保持するマージ経路を使う\n applyBroadcastReportUpdate(report)\n if (payload.type === \"report-create\" || payload.type === \"report-publish\") {\n manuallyTrackingIds.current.add(report.reportHubId)\n upsertItem(report.reportHubId, report.date)\n }\n setVersion((v) => v + 1)\n break\n }\n }\n },\n [isProcessedId, isSessionStillOurs, upsertItem, user, enqueuePendingSseMessage],\n )\n\n // 常に最新の handleSseMessage をキューのリプレイから参照できるようにする\n handleSseMessageRef.current = handleSseMessage\n\n /**\n * Cleanup all pending SSE queues on unmount.\n * アンマウント時に全キューをクリーンアップする。\n */\n useEffect(() => {\n return () => {\n for (const entry of pendingSseQueue.current.values()) {\n entry.unsubscribe()\n clearTimeout(entry.timer)\n }\n pendingSseQueue.current.clear()\n }\n }, [])\n\n useDailyReportSseConnection({ isSseEnabled, onMessage: handleSseMessage })\n\n /**\n * Common helper function to execute API actions.\n * APIアクションを実行する共通ヘルパー関数。\n */\n const executeAction = async <T,>(\n reportHubId: number,\n intent: string,\n businessDate: string,\n params: Record<string, string>,\n callbacks: {\n onOptimistic?: () => void\n onSuccess?: (data: ActionResponse) => Promise<T> | T\n onFailure?: () => void\n },\n options?: {\n keepLock?: boolean\n skipLock?: boolean\n },\n ): Promise<T> => {\n if (!options?.skipLock) acquireMutationLock(reportHubId)\n callbacks.onOptimistic?.()\n\n // Generate or use provided clientTempId\n const clientTempId = params.clientTempId || crypto.randomUUID()\n addProcessedId(clientTempId)\n\n const formData = new FormData()\n formData.append(\"intent\", intent)\n formData.append(\"reportHubId\", String(reportHubId))\n formData.append(\"businessDate\", businessDate)\n formData.append(\"operationTimestamp\", String(Date.now()))\n formData.append(\"clientTempId\", clientTempId)\n\n for (const [k, v] of Object.entries(params)) {\n if (k !== \"clientTempId\") {\n formData.append(k, v)\n }\n }\n\n try {\n const res = await fetch(`${apiBasePath}/action`, {\n method: \"POST\",\n body: formData,\n headers: { Accept: \"application/json\" },\n })\n if (!res.ok) {\n const error = new Error(`Failed to ${intent}`)\n Object.assign(error, { status: res.status })\n throw error\n }\n const data = await res.json()\n if (data.status !== \"OK\" || (intent !== \"create\" && String(data.reportHubId) !== String(reportHubId))) {\n throw new Error(`Invalid response for ${intent}`)\n }\n if (typeof data.clientTempId !== \"string\" || data.clientTempId !== clientTempId) {\n throw new Error(`Invalid clientTempId for ${intent}`)\n }\n // ユーザー切替後に解決した継続は、モジュール常駐状態 (常駐セッション・詳細キャッシュ)\n // を旧ユーザーのデータで汚染しないよう onSuccess を放棄する\n if (!isSessionStillOurs()) {\n logger.warn(`[executeAction] ${intent} resolved after a user switch; discarding continuation`)\n throw new Error(`Stale continuation for ${intent}`)\n }\n return (await callbacks.onSuccess?.(data)) as T\n } catch (e) {\n logger.error(`${intent} failed:`, e)\n // 切替後の失敗も同様: onFailure のロールバックが旧値をモジュールキャッシュへ\n // 書き戻すため放棄する (プロバイダー自身の state は key リマウントで破棄済み)。\n // 通知も同じ門で守る: ユーザー切替で捨てた継続の失敗は当人に見せない\n if (isSessionStillOurs()) {\n callbacks.onFailure?.()\n notifyErrorRef.current({ operation: intent, message: describeOperationFailure(intent), cause: e })\n }\n throw e\n } finally {\n if (!(options?.keepLock || options?.skipLock)) {\n releaseMutationLock(reportHubId)\n }\n }\n }\n\n const addComment = async (reportHubId: number, content: string, businessDate: string) => {\n const id = Number(reportHubId)\n const tempId = -1 * Date.now()\n const promise = executeAction(\n id,\n \"addComment\",\n businessDate,\n { content, clientTempId: String(tempId) },\n {\n onOptimistic: () => {\n if (!pendingAddCommentIds.current.has(id)) pendingAddCommentIds.current.set(id, new Map())\n pendingAddCommentIds.current.get(id)?.set(tempId, content)\n setVersion((v) => v + 1)\n },\n onSuccess: (data) => {\n if (!data.newComment) throw new Error(\"No new comment returned\")\n const realId = data.newComment.id\n resolvedIdMap.current.set(tempId, realId)\n pendingAddCommentIds.current.get(id)?.delete(tempId)\n\n const cachedReport = getCachedReport(id, { ignoreExpiry: true })\n if (cachedReport && !cachedReport.commentItems.some((c) => c.id === realId)) {\n updateDailyReportCache(id, {\n commentItems: [...cachedReport.commentItems, { ...data.newComment, id: realId, isMine: true, userName: getUserDisplayName(user) }],\n updatedAt: data.updatedAt,\n })\n }\n return realId\n },\n onFailure: () => {\n pendingAddCommentIds.current.get(id)?.delete(tempId)\n },\n },\n { skipLock: true },\n ).finally(() => pendingAddPromises.current.delete(tempId))\n\n pendingAddPromises.current.set(tempId, promise)\n await promise\n }\n\n const deleteComment = async (reportHubId: number, commentId: number, businessDate: string) => {\n const id = Number(reportHubId)\n // 1. Optimistic UI Update\n if (!pendingDeleteCommentIds.current.has(id)) pendingDeleteCommentIds.current.set(id, new Set())\n pendingDeleteCommentIds.current.get(id)?.add(commentId)\n setVersion((v) => v + 1)\n\n // 2. Resolve ID\n let realId = commentId\n if (commentId < 0) {\n const resolved = resolvedIdMap.current.get(commentId)\n if (resolved !== undefined) {\n realId = resolved\n } else {\n const promise = pendingAddPromises.current.get(commentId)\n if (!promise) return // 既に削除済みかエラー\n try {\n realId = await promise\n } catch {\n return // 追加失敗時は削除不要\n }\n }\n }\n\n // 3. Server Request\n await executeAction(\n id,\n \"deleteComment\",\n businessDate,\n { commentId: String(realId) },\n {\n onSuccess: (data) => {\n const deletedId = typeof data.deletedCommentId === \"string\" ? Number(data.deletedCommentId) : realId\n const cachedReport = getCachedReport(id, { ignoreExpiry: true })\n if (cachedReport) {\n updateDailyReportCache(id, {\n commentItems: cachedReport.commentItems.filter((c) => c.id !== deletedId),\n updatedAt: data.updatedAt,\n })\n registerRecentDeletion(deletedId)\n }\n pendingDeleteCommentIds.current.get(id)?.delete(commentId)\n },\n onFailure: () => {\n pendingDeleteCommentIds.current.get(id)?.delete(commentId)\n },\n },\n { skipLock: true },\n )\n }\n\n const toggleStar = async (reportHubId: number, isStarred: boolean, businessDate: string) => {\n const id = Number(reportHubId)\n await executeAction(\n id,\n \"toggleStar\",\n businessDate,\n { isStarred: String(isStarred) },\n {\n onOptimistic: () => {\n updateDailyReportCache(id, { isStarred })\n pendingStarUpdates.current.set(id, isStarred)\n setVersion((v) => v + 1)\n },\n onSuccess: (data) => {\n if (data.updatedStatus && typeof data.updatedStatus.isStarred === \"boolean\") {\n const cached = getCachedReport(id, { ignoreExpiry: true })\n if (cached) {\n updateDailyReportCache(id, { isStarred: data.updatedStatus.isStarred, updatedAt: data.updatedAt })\n setVersion((v) => v + 1)\n }\n }\n pendingStarUpdates.current.delete(id)\n },\n onFailure: () => {\n updateDailyReportCache(id, { isStarred: !isStarred })\n pendingStarUpdates.current.delete(id)\n },\n },\n { skipLock: true },\n )\n }\n\n const toggleReadImpl = async (reportHubId: number, isRead: boolean, businessDate: string) => {\n const id = Number(reportHubId)\n await executeAction(\n id,\n \"toggleRead\",\n businessDate,\n { isRead: String(isRead) },\n {\n onOptimistic: () => {\n updateDailyReportCache(id, { isRead })\n pendingReadUpdates.current.set(id, isRead)\n setVersion((v) => v + 1)\n },\n onSuccess: (data) => {\n if (data.updatedStatus && typeof data.updatedStatus.isRead === \"boolean\") {\n const cached = getCachedReport(id, { ignoreExpiry: true })\n if (cached) {\n updateDailyReportCache(id, { isRead: data.updatedStatus.isRead, updatedAt: data.updatedAt })\n setVersion((v) => v + 1)\n }\n }\n pendingReadUpdates.current.delete(id)\n },\n onFailure: () => {\n updateDailyReportCache(id, { isRead: !isRead })\n pendingReadUpdates.current.delete(id)\n },\n },\n { skipLock: true },\n )\n }\n // ref 経由で最新実装を参照し、安定した関数 identity を提供する\n // (SSE イベントによるコンテキスト再レンダリングで auto-read タイマーがリセットされるのを防止)\n const toggleReadRef = useRef(toggleReadImpl)\n toggleReadRef.current = toggleReadImpl\n const toggleRead = useCallback((reportHubId: number, isRead: boolean, businessDate: string) => toggleReadRef.current(reportHubId, isRead, businessDate), [])\n\n const createReport = async (businessDate: string) => {\n const tempId = -1 * Date.now()\n // Optimistic UI update\n const now = new Date().toISOString()\n const dummyReport: DailyReportDetail = {\n reportHubId: tempId,\n date: businessDate,\n createdAt: now,\n author: getUserDisplayName(user),\n userId: user?.id ?? \"unknown\",\n sourceType: \"Internal\",\n employeeName: getUserDisplayName(user),\n updatedBy: getUserDisplayName(user),\n updatedAt: now,\n category: null,\n creationCategory: null,\n visitTimeFrom: null,\n visitTimeTo: null,\n customerName: null,\n interviewers: [],\n subject: \"\",\n content: \"\",\n comments: [],\n isRead: true,\n isStarred: false,\n labels: [{ id: 0, name: draftLabelName, color: \"#9ca3af\" }],\n commentItems: [],\n // 楽観更新の仮レポート。作成直後に添付は存在しない\n attachments: [],\n }\n writeCache(dummyReport, Date.now() + 300000, false)\n\n setItems((prev) => [\n {\n reportHubId: tempId,\n businessDate: businessDate,\n },\n ...prev,\n ])\n\n try {\n const res = await executeAction<ActionResponse>(\n 0,\n \"create\",\n businessDate,\n { clientTempId: String(tempId) },\n {\n onSuccess: (data) => data,\n },\n )\n // Validate response mapping\n if (res.clientTempId && res.clientTempId !== String(tempId)) {\n logger.warn(`[createReport] ID mismatch. req=${tempId}, res=${res.clientTempId}`)\n // Mismatch implies this response is not for this specific request. Throwing here triggers revert.\n throw new Error(\"Optimistic ID mismatch\")\n }\n\n const newId = Number(res.reportHubId)\n\n // 常駐ストリームセッションへも反映する。自分の SSE エコーは clientTempId で\n // 抑止されるため upsertItem 経由の中継は届かず、ここで直接中継しないと\n // 鮮度窓内の再訪で作成直後の日報が一覧から消える\n upsertDailyReportIdsStreamItem({ reportHubId: newId, businessDate })\n\n // newId を一時的にロックし、createReport 以前に開始された背景 prefetch の\n // cacheMany による stale データ上書きを防止する\n acquireMutationLock(newId)\n\n // Seed cache with server response (checking staleness)\n if (res.report) {\n applyDailyReportServerUpdates(newId, res.report)\n setVersion((v) => v + 1)\n }\n\n setItems((prev) => {\n // If newId is already present (e.g. from SSE), just remove tempId to avoid duplication\n if (prev.some((item) => item.reportHubId === newId)) {\n return prev.filter((item) => item.reportHubId !== tempId)\n }\n return prev.map((item) => (item.reportHubId === tempId ? { ...item, reportHubId: newId } : item))\n })\n\n // ロック解放: lastMutations に newId が記録され、以降の cacheMany でも保護される\n setTimeout(() => releaseMutationLock(newId), 1000)\n\n return newId\n } catch (e) {\n logger.error(\"[createReport] Failed or mismatched\", e)\n // Revert optimistic update\n setItems((prev) => prev.filter((item) => item.reportHubId !== tempId))\n throw e\n }\n }\n\n const updateReport = async (reportHubId: number, data: { title?: string; content?: string }, businessDate: string) => {\n const id = Number(reportHubId)\n await executeAction(\n id,\n \"update\",\n businessDate,\n { ...(data.title && { title: data.title }), ...(data.content && { content: data.content }) },\n {\n onSuccess: () => {\n const cached = getCachedReport(id, { ignoreExpiry: true })\n if (cached) {\n updateDailyReportCache(id, {\n subject: data.title ?? cached.subject,\n content: data.content ?? cached.content,\n })\n setVersion((v) => v + 1)\n }\n },\n },\n )\n }\n\n const publishReport = async (reportHubId: number, businessDate: string) => {\n const id = Number(reportHubId)\n const cached = getCachedReport(id, { ignoreExpiry: true })\n const oldLabels = cached?.labels || []\n\n await executeAction(\n id,\n \"publish\",\n businessDate,\n {},\n {\n onOptimistic: () => {\n if (cached) {\n const newLabels = cached.labels.filter((l) => l.name !== draftLabelName)\n updateDailyReportCache(id, {\n labels: newLabels,\n })\n setVersion((v) => v + 1)\n }\n },\n onSuccess: (data) => {\n // サーバーからのレスポンスを正とする\n if (data.report) {\n applyDailyReportServerUpdates(id, data.report)\n setVersion((v) => v + 1)\n }\n // 成功確定後に常駐ストリームセッションへも反映する (自分の SSE エコーは\n // 抑止されるため)。失敗時に走らせると、並行削除済みの日報を誤って\n // 蘇らせトゥームストーンまで解除してしまう\n upsertDailyReportIdsStreamItem({ reportHubId: id, businessDate })\n },\n onFailure: () => {\n // ロールバック\n updateDailyReportCache(id, {\n labels: oldLabels,\n })\n setVersion((v) => v + 1)\n },\n },\n { skipLock: true },\n )\n }\n\n const deleteReport = async (reportHubId: number, businessDate: string) => {\n const id = Number(reportHubId)\n const targetItem = items.find((i) => i.reportHubId === id)\n\n // マージロジックでの復活を防止するために削除済み ID を記録\n removedIds.current.add(id)\n // 常駐ストリームデータからも除去 (鮮度窓内の再訪で削除済み日報が蘇らないように)\n removeDailyReportIdsStreamItem(id)\n // Optimistic UI update\n setItems((prev) => prev.filter((item) => item.reportHubId !== id))\n\n try {\n await executeAction(\n id,\n \"delete\",\n businessDate,\n {},\n {\n onSuccess: (_data) => {\n deleteDailyReportCache(id, businessDate)\n // Note: Item already removed from list optimistically\n },\n },\n )\n } catch (e: unknown) {\n const error = e as { status?: number }\n // 既に削除済み(404)の場合は、成功とみなしてロールバックしない(キャッシュはクリアする)\n if (error.status === 404) {\n deleteDailyReportCache(id, businessDate)\n return\n }\n\n // それ以外のエラーの場合は、楽観的更新をロールバックする。\n // 常駐ストリームデータの除去とトゥームストーンも upsert で巻き戻す\n // (巻き戻さないと、実在する日報が次の全量走査まで再訪時の一覧から消える)。\n // ユーザー切替後に解決した継続は巻き戻さない — 旧ユーザーの行を新ユーザーの\n // セッションへ upsert しトゥームストーンまで解除してしまうため\n // (元の catch と同様に握り潰す。呼び出し元 UI は既に破棄済み)\n if (!isSessionStillOurs()) {\n return\n }\n removedIds.current.delete(id)\n upsertDailyReportIdsStreamItem({ reportHubId: id, businessDate: targetItem?.businessDate ?? businessDate })\n if (targetItem) {\n setItems((prev) => {\n if (prev.some((i) => i.reportHubId === id)) return prev\n // ロールバックも正準比較器の位置へ戻す (第 3 の比較器を持たない)\n return upsertInIdsStreamOrder(prev, targetItem)\n })\n }\n }\n }\n\n return (\n <DailyReportActionContext.Provider\n value={{\n user,\n pendingAddCommentIds,\n pendingDeleteCommentIds,\n pendingStarUpdates,\n pendingReadUpdates,\n resolvedIdMap,\n version,\n items,\n isSseEnabled,\n toggleSse,\n addComment,\n deleteComment,\n toggleStar,\n toggleRead,\n createReport,\n updateReport,\n publishReport,\n deleteReport,\n removeStaleItem,\n refetchData,\n clearCacheAndRefetch,\n }}>\n {children}\n </DailyReportActionContext.Provider>\n )\n}\n","/**\n * Wire contract and ordering helpers for the daily-report ids NDJSON stream.\n * 日報 ID 一覧 NDJSON ストリームのワイヤ契約と並び順ヘルパー。\n *\n * サーバーは `GET {apiBasePath}/ids-stream` で 1 行 1 JSON の NDJSON を返す。\n * 行は原則 {@link DailyReportIdsStreamChunkLine} で、送出中に予期しない失敗が\n * 起きた場合のみ {@link DailyReportIdsStreamErrorLine} を 1 行流して正常クローズする\n * (クライアントは「isLast 無しで終端」を再開シグナルとして扱う)。\n *\n * 並び順は SQL の `ORDER BY business_date DESC, id DESC` (MSSQL の DESC は NULL を\n * 末尾に置く) と完全一致させる。カーソル再開はこの全順序に対する「厳密に後」の\n * 位置からのスライスとして定義する。\n */\nimport type { DailyReportItem } from \"./types\"\n\n/** ストリーム再開カーソル。直前チャンク末尾アイテムの並び順キー。 */\nexport type DailyReportIdsStreamCursor = {\n /** 営業日 (YYYY-MM-DD)。null は「営業日なし領域」(並び順の末尾) を表す。 */\n businessDate: string | null\n /** 日報 Hub ID。 */\n reportHubId: number\n}\n\n/** データを運ぶ NDJSON 行。 */\nexport type DailyReportIdsStreamChunkLine = {\n /** このチャンクのアイテム群 (並び順どおり)。 */\n items: DailyReportItem[]\n /** 次チャンクの再開カーソル。最終チャンクでは null。 */\n nextCursor: DailyReportIdsStreamCursor | null\n /** これが最終チャンクかどうか。 */\n isLast: boolean\n /** 全体件数。接続ごとの先頭行にのみ載る。 */\n total?: number\n}\n\n/** サーバー側の予期しない失敗を伝える NDJSON 行 (内部情報は含めない)。 */\nexport type DailyReportIdsStreamErrorLine = {\n error: string\n}\n\n/** NDJSON 1 行のユニオン。 */\nexport type DailyReportIdsStreamLine = DailyReportIdsStreamChunkLine | DailyReportIdsStreamErrorLine\n\n/**\n * Type guard for chunk lines (anything else is treated as an error line).\n * チャンク行の型ガード (それ以外はエラー行として扱う)。\n *\n * @param line Parsed NDJSON line. パース済みの NDJSON 行。\n * @returns True when the line carries items. アイテムを運ぶ行なら true。\n */\nexport const isIdsStreamChunkLine = (line: unknown): line is DailyReportIdsStreamChunkLine => {\n return typeof line === \"object\" && line !== null && Array.isArray((line as { items?: unknown }).items)\n}\n\n/**\n * Total order of the ids stream: businessDate DESC (NULL last), then id DESC.\n * ids ストリームの全順序。営業日降順 (NULL は末尾)、同日内は ID 降順。\n *\n * MSSQL の `ORDER BY business_date DESC, id DESC` と一致させることが唯一の存在理由。\n * 営業日は正規化済みの `YYYY-MM-DD` 文字列なので辞書順比較 = 時系列比較になる。\n *\n * @param a Left key. 左辺キー。\n * @param b Right key. 右辺キー。\n * @returns Negative when `a` streams before `b`. `a` が先に流れるなら負値。\n */\nexport const compareIdsStreamOrder = (a: DailyReportIdsStreamCursor, b: DailyReportIdsStreamCursor): number => {\n if (a.businessDate !== b.businessDate) {\n // null 営業日は並び順の末尾 (MSSQL DESC の NULL 末尾挙動へ揃える)\n if (a.businessDate === null) return 1\n if (b.businessDate === null) return -1\n return a.businessDate > b.businessDate ? -1 : 1\n }\n return b.reportHubId - a.reportHubId\n}\n\n/**\n * Checks whether items already follow the ids-stream total order.\n * アイテム配列が ids ストリームの全順序に既に従っているかを検査する処理。\n *\n * ストリームはソート順どおりに届いて末尾へ追記されるため通常は整列済みで、\n * この O(n) 検査で全件ソート (O(n log n)) を省略できる。\n *\n * @param items Items to probe. 検査対象のアイテム配列。\n * @returns True when already ordered. 整列済みなら true。\n */\nexport const isIdsStreamOrdered = (items: readonly DailyReportItem[]): boolean => {\n for (let i = 1; i < items.length; i++) {\n if (compareIdsStreamOrder(items[i - 1], items[i]) > 0) return false\n }\n return true\n}\n\n/**\n * Returns a copy with the item upserted at its ids-stream-ordered position.\n * アイテムを ids ストリーム順の正しい位置へ upsert した複製配列を返す処理。\n *\n * 前提: `items` は ids ストリーム順に整列済み (このモジュールの比較器を使う全書き込み元が\n * 維持する不変条件)。既存の同 ID は除去してから二分探索で挿入位置を求めるため、\n * 全件ソート無しの O(n) で並び順を保てる (以前は SSE 1 件ごとに 20 万件超を\n * localeCompare で全ソートしていた)。\n *\n * @param items Ordered items. 整列済みアイテム配列。\n * @param item Item to upsert. upsert するアイテム。\n * @returns New ordered array. 新しい整列済み配列。\n */\nexport const upsertInIdsStreamOrder = (items: readonly DailyReportItem[], item: DailyReportItem): DailyReportItem[] => {\n // 既存の同 ID を除いた複製 (位置移動を伴う更新もこれで単一経路になる)\n const next = items.filter((existing) => existing.reportHubId !== item.reportHubId)\n let low = 0\n let high = next.length\n while (low < high) {\n const mid = (low + high) >> 1\n if (compareIdsStreamOrder(next[mid], item) <= 0) {\n low = mid + 1\n } else {\n high = mid\n }\n }\n next.splice(low, 0, item)\n return next\n}\n\n/**\n * Finds the first index that streams strictly after the cursor.\n * カーソルより厳密に後ろへ並ぶ最初のインデックスを求める処理。\n *\n * 配列は必ずストリーム順 (SQL の ORDER BY) で渡されるが、比較器とのズレが\n * あっても壊れない線形走査で決定する (件数は高々数万・再開時のみの実行)。\n * 該当なし (カーソルが末尾以降) は `items.length` を返し、空チャンク +\n * `isLast: true` の正常終端に落ちる。\n *\n * @param items Stream-ordered items. ストリーム順のアイテム配列。\n * @param cursor Resume cursor. 再開カーソル。\n * @returns Index to resume emission from. 送出を再開するインデックス。\n */\nexport const findIdsStreamResumeIndex = (items: readonly DailyReportItem[], cursor: DailyReportIdsStreamCursor): number => {\n const index = items.findIndex((item) => compareIdsStreamOrder({ businessDate: item.businessDate, reportHubId: item.reportHubId }, cursor) > 0)\n return index === -1 ? items.length : index\n}\n","/**\n * Minimal level-filtered logger used internally by the daily-report package.\n * daily-report パッケージ内部で使う最小のレベルフィルタ付きロガー。\n *\n * 消費アプリのロガー実装 (console 互換の debug/info/warn/error) を注入でき、\n * 未注入時は console にフォールバックする。\n */\n\n/** 出力フィルタリング用のログレベル。 */\nexport enum LogLevel {\n DEBUG = 0,\n INFO = 1,\n WARN = 2,\n ERROR = 3,\n NONE = 4,\n}\n\n/** Console 互換のロガーインターフェース。 */\nexport interface DailyReportLogger {\n debug(message?: unknown, ...optionalParams: unknown[]): void\n info(message?: unknown, ...optionalParams: unknown[]): void\n warn(message?: unknown, ...optionalParams: unknown[]): void\n error(message?: unknown, ...optionalParams: unknown[]): void\n}\n\n/**\n * Creates a prefixed, level-filtered logger delegating to the given implementation.\n * 指定実装へ委譲するプレフィックス付き・レベルフィルタ付きロガーを生成する処理。\n */\nexport const createLogger = (level: LogLevel, prefix: string, impl: DailyReportLogger = console): DailyReportLogger => {\n // 文字列メッセージはプレフィックスを連結、それ以外は先頭引数として付加する\n const format = (message: unknown): unknown[] => (typeof message === \"string\" ? [`${prefix} ${message}`] : [prefix, message])\n return {\n debug: (message?: unknown, ...rest: unknown[]) => {\n if (level <= LogLevel.DEBUG) impl.debug(...format(message), ...rest)\n },\n info: (message?: unknown, ...rest: unknown[]) => {\n if (level <= LogLevel.INFO) impl.info(...format(message), ...rest)\n },\n warn: (message?: unknown, ...rest: unknown[]) => {\n if (level <= LogLevel.WARN) impl.warn(...format(message), ...rest)\n },\n error: (message?: unknown, ...rest: unknown[]) => {\n if (level <= LogLevel.ERROR) impl.error(...format(message), ...rest)\n },\n }\n}\n","/**\n * Suspense-friendly daily report detail loader (fetch-driven).\n * fetch ベースの日報詳細ローダー。\n */\nimport { useCallback, useEffect, useRef, useState } from \"react\"\nimport { normalizeBusinessDateKey } from \"../../shared/business-date\"\nimport { createLogger, LogLevel } from \"../../shared/logger\"\nimport type { DailyReportDetail } from \"../../shared/types\"\nimport { useDailyReportConfig } from \"../config-context\"\nimport { DAILY_REPORT_FETCH_DEBOUNCE_MS } from \"../utils/constants\"\n\nconst logger = createLogger(LogLevel.INFO, \"use-daily-report\")\n\n// --- Configuration & Constants ---\n/**\n * Retrieve global configuration or default.\n * グローバル設定またはデフォルト値を取得する。\n */\nconst getGlobal = (k: string, d: number): number => {\n if (typeof window === \"undefined\") return d\n const v = (window as unknown as Record<string, unknown>)[k]\n return typeof v === \"number\" && !Number.isNaN(v) ? v : d\n}\nconst TTL = getGlobal(\"__DAILY_REPORT_CACHE_TTL__\", 300_000)\nconst REVIVE_MS = getGlobal(\"__DAILY_REPORT_CACHE_REVIVE_MS__\", 60_000)\n\n// --- Cache Store (Centralized State) ---\ntype CacheEntry<T> = { data: T; expiresAt: number }\n\nexport const DailyReportCache = {\n reports: new Map<number, CacheEntry<DailyReportDetail>>(),\n lists: new Map<string, CacheEntry<DailyReportDetail[]>>(),\n pendingLists: new Map<string, Promise<DailyReportDetail[]>>(),\n pendingReports: new Map<number, Promise<DailyReportDetail>>(),\n listeners: new Set<(id: number) => void>(),\n locks: new Set<number>(),\n lastMutations: new Map<number, number>(),\n recentDeletes: new Map<number, number>(),\n lastPrune: 0,\n\n /**\n * Prune expired entries from the cache.\n * キャッシュから期限切れのエントリを削除する。\n */\n prune() {\n const now = Date.now()\n // Prune at most once per minute\n if (now - this.lastPrune < 60_000) return\n this.lastPrune = now\n\n // SWR のために古いデータを保持する。TTL 超過後すぐには削除せず、十分に古い(例えば 1時間経過した)データのみを破棄する\n const GC_THRESHOLD = 60 * 60 * 1000 // 1 hour\n for (const [id, entry] of this.reports) {\n if (now - entry.expiresAt > GC_THRESHOLD) this.reports.delete(id)\n }\n for (const [key, entry] of this.lists) {\n if (now - entry.expiresAt > GC_THRESHOLD) this.lists.delete(key)\n }\n },\n\n /**\n * Clear all cached entries and pending requests.\n * すべてのキャッシュエントリおよび待機中リクエストをクリアする。\n */\n clear() {\n this.reports.clear()\n this.lists.clear()\n this.pendingLists.clear()\n this.pendingReports.clear()\n this.locks.clear()\n this.lastMutations.clear()\n this.recentDeletes.clear()\n this.listeners.forEach((listener) => {\n listener(-1)\n })\n },\n\n /**\n * Check if the cache entry is still valid.\n * キャッシュエントリがまだ有効かを確認する。\n */\n isValid: (ex: number) => ex > Date.now(),\n /**\n * Notify all listeners of a report update.\n * レポートの更新をすべてのリスナーに通知する。\n */\n notify: (id: number) => {\n DailyReportCache.listeners.forEach((l) => {\n l(id)\n })\n },\n\n /**\n * Retrieve a cached report by ID.\n * ID でキャッシュされたレポートを取得する。\n */\n getReport(id: number, ignoreExpiry = false) {\n const e = this.reports.get(id)\n return e && (ignoreExpiry || this.isValid(e.expiresAt)) ? e.data : null\n },\n\n /**\n * Retrieve a cached list of reports by business date key.\n * 営業日キーでレポートのキャッシュリストを取得する。\n */\n getList(dateKey: string | null, ignoreExpiry = false) {\n const e = dateKey && this.lists.get(dateKey)\n return e && (ignoreExpiry || this.isValid(e.expiresAt)) ? e.data : null\n },\n\n /**\n * Set a report in the cache and optionally sync with the list cache.\n * キャッシュにレポートを設定し、必要に応じてリストキャッシュと同期する。\n */\n set(report: DailyReportDetail, expiresAt: number, syncList = true) {\n this.reports.set(report.reportHubId, { data: report, expiresAt })\n if (syncList) {\n const dateKey = normalizeBusinessDateKey(report.date)\n const listEntry = dateKey ? this.lists.get(dateKey) : null\n if (listEntry && dateKey) {\n const exists = listEntry.data.some((r) => r.reportHubId === report.reportHubId)\n const data = exists ? listEntry.data.map((r) => (r.reportHubId === report.reportHubId ? report : r)) : [report, ...listEntry.data]\n this.lists.set(dateKey, { ...listEntry, data })\n }\n }\n this.notify(report.reportHubId)\n },\n\n /**\n * Update a cached report with partial data.\n * 部分的なデータでキャッシュされたレポートを更新する。\n */\n update(id: number, updates: Partial<DailyReportDetail>, strict = true) {\n const entry = this.reports.get(id)\n if (!entry?.data) return\n\n if (strict && updates.updatedAt && entry.data.updatedAt) {\n if (new Date(updates.updatedAt).getTime() < new Date(entry.data.updatedAt).getTime()) {\n logger.warn(`[DailyReportCache] Stale update skipped id=${id}`)\n return\n }\n }\n this.set({ ...entry.data, ...updates } as DailyReportDetail, Math.max(entry.expiresAt, Date.now() + REVIVE_MS))\n },\n\n /**\n * Delete a report from the cache and update the corresponding list.\n * キャッシュからレポートを削除し、対応するリストを更新する。\n */\n delete(id: number, date: string) {\n this.reports.delete(id)\n const dateKey = normalizeBusinessDateKey(date)\n const list = dateKey && this.lists.get(dateKey)\n if (list && dateKey) {\n this.lists.set(dateKey, { ...list, data: list.data.filter((r) => r.reportHubId !== id) })\n }\n this.notify(id)\n },\n\n /**\n * Merge server report with cached report to handle optimistic updates and zombies.\n * サーバーレポートとキャッシュレポートをマージして、楽観的更新とゾンビコメントを処理する。\n */\n merge(serverReport: DailyReportDetail, cachedReport?: DailyReportDetail | null) {\n let report = serverReport\n // Filter zombies\n if (report.commentItems.some((c) => this.recentDeletes.has(c.id))) {\n report = { ...report, commentItems: report.commentItems.filter((c) => !this.recentDeletes.has(c.id)) }\n }\n // Restore missing \"my comments\"\n if (cachedReport) {\n const serverIds = new Set(report.commentItems.map((c) => c.id))\n const missing = cachedReport.commentItems.filter((c) => !serverIds.has(c.id) && c.isMine && Date.now() - new Date(c.createdAt || 0).getTime() < 10000)\n if (missing.length > 0) {\n report = {\n ...report,\n commentItems: [...report.commentItems, ...missing].sort((a, b) => new Date(a.createdAt || 0).getTime() - new Date(b.createdAt || 0).getTime()),\n }\n }\n }\n return report\n },\n\n /**\n * Hydrate the cache with fetched reports, handling comment merges and locks.\n * フェッチされたレポートでキャッシュをハイドレートし、コメントのマージとロックを処理する。\n */\n cacheMany(reports: DailyReportDetail[], dateKey: string | null, fetchStart = 0) {\n if (!reports.length) return []\n this.prune()\n const now = Date.now(),\n expiresAt = now + TTL\n for (const [id, t] of this.recentDeletes) if (now - t > 10000) this.recentDeletes.delete(id)\n\n const processed = reports.map((r) => {\n // Original logic: If locked, return server data directly (don't update cache, don't merge)\n // ロックされている場合はサーバーデータをそのまま返す(キャッシュ更新せず、マージもしない)\n if (this.locks.has(r.reportHubId)) return r\n\n // Original logic: If recently mutated locally, prefer cached data\n // 最近ローカルで変更された場合は、キャッシュデータを優先する\n const lastMut = this.lastMutations.get(r.reportHubId)\n if (lastMut && fetchStart > 0 && lastMut > fetchStart) {\n return this.reports.get(r.reportHubId)?.data ?? r\n }\n\n const cached = this.reports.get(r.reportHubId)?.data\n const merged = this.merge(r, cached)\n this.set(merged, expiresAt, false)\n return merged\n })\n if (dateKey) this.lists.set(dateKey, { data: processed, expiresAt })\n return processed\n },\n}\n\n// --- Public API Exports ---\n/**\n * Update the cache for a single report.\n * 単一レポートのキャッシュを更新する。\n */\nexport const writeCache = (r: DailyReportDetail, exp: number, sync = true) => DailyReportCache.set(r, exp, sync)\n/**\n * Get a cached report by ID.\n * ID でキャッシュされたレポートを取得する。\n */\nexport const getCachedReport = (id: number, opts?: { ignoreExpiry?: boolean }) => DailyReportCache.getReport(id, opts?.ignoreExpiry)\n/**\n * Update specific fields of a cached report.\n * キャッシュされたレポートの特定のフィールドを更新する。\n */\nexport const updateDailyReportCache = (id: number, u: Partial<DailyReportDetail>) => DailyReportCache.update(id, u)\n/**\n * Delete a report from the cache.\n * キャッシュからレポートを削除する。\n */\nexport const deleteDailyReportCache = (id: number, d: string) => DailyReportCache.delete(id, d)\n/**\n * Clear all daily report cache store entries.\n * 日報の全キャッシュストアを消去する。\n */\nexport const clearDailyReportCache = () => DailyReportCache.clear()\n\nexport const acquireMutationLock = (id: number) => {\n DailyReportCache.locks.add(id)\n DailyReportCache.lastMutations.set(id, Date.now())\n}\nexport const releaseMutationLock = (id: number) => {\n DailyReportCache.locks.delete(id)\n DailyReportCache.lastMutations.set(id, Date.now())\n}\nexport const registerRecentDeletion = (cId: number) => DailyReportCache.recentDeletes.set(cId, Date.now())\n\n/**\n * Subscribe to cache-ready events for a specific report.\n * Returns an unsubscribe function.\n *\n * 特定レポートのキャッシュ書き込みを監視する。\n * 解除関数を返す。\n */\nexport const subscribeCacheReady = (reportHubId: number, callback: () => void): (() => void) => {\n const listener = (id: number) => {\n if (id === reportHubId) {\n callback()\n }\n }\n DailyReportCache.listeners.add(listener)\n return () => {\n DailyReportCache.listeners.delete(listener)\n }\n}\n\nexport const applyDailyReportServerUpdates = (_reportHubId: number, report: DailyReportDetail) => {\n DailyReportCache.prune()\n DailyReportCache.set(report, Date.now() + TTL)\n}\n\n/**\n * Applies a broadcast (cross-user SSE) report while preserving the viewer's own per-viewer state.\n * 全ユーザー放送 (SSE) の report を、受信者自身の per-viewer 状態を保持したまま反映する処理。\n *\n * SSE の report-create/update/publish が運ぶ isRead/isStarred は**操作した本人の状態**。\n * そのまま書くと受信者の既読/スター表示が他人の状態で上書きされる。共有フィールドだけを\n * 取り込み、per-viewer フィールドは受信者のキャッシュ値を保持する。ローカルに無い日報は\n * 種まきしない (送信者の per-viewer 値で汚すより、次のフェッチが受信者視点の正しい値を\n * 取るのに任せるほうが安全)。\n *\n * ❗ 信頼できる経路 (自分の HTTP 応答・recipient フィルタ済みの自分宛 status-update) は\n * {@link applyDailyReportServerUpdates} を使うこと — そちらは値をそのまま書く。\n */\nexport const applyBroadcastReportUpdate = (report: DailyReportDetail) => {\n DailyReportCache.prune()\n const cached = DailyReportCache.getReport(report.reportHubId, true)\n // per-viewer フィールド (isRead / isStarred / commentItems[].isMine) は送信者視点なので\n // 受信者のキャッシュ値を保持し、未キャッシュなら中立値 (false) で置く。共有フィールドは配信値。\n // ❗ 未キャッシュでも必ず書く: 書かないと鮮度窓内のリストキャッシュに新 id が載らず、\n // 作成/公開された他ユーザーの日報が stale 安全弁でトゥームストーン化され、閲覧中の\n // 受信者の一覧から恒久的に消える (set は既定で syncList=true = リストにも反映する)\n const cachedMine = new Map<number | string, boolean>()\n for (const c of cached?.commentItems ?? []) {\n cachedMine.set(c.id, c.isMine ?? false)\n }\n const commentItems = (report.commentItems ?? []).map((c) => ({ ...c, isMine: cachedMine.get(c.id) ?? false }))\n DailyReportCache.set({ ...report, isRead: cached?.isRead ?? false, isStarred: cached?.isStarred ?? false, commentItems }, Date.now() + TTL)\n}\n\n// --- Data Loading ---\n/**\n * Fetch a single report by ID.\n * ID で単一のレポートを取得する。\n */\nconst fetchReportById = async (apiBasePath: string, id: number) => {\n // 1. Cache Check\n const cached = DailyReportCache.getReport(id)\n if (cached) return cached\n\n // 2. Prevent Duplicate Requests\n const pending = DailyReportCache.pendingReports.get(id)\n if (pending) return pending\n\n // 3. Fetch\n const start = Date.now()\n const p = fetch(`${apiBasePath}/report?reportHubId=${id}`, { cache: \"no-cache\" })\n .then((res) => (res.ok ? res.json() : Promise.reject(`HTTP ${res.status}`)))\n .then((data) => {\n if (data.error) throw new Error(data.error.message)\n const report = data.report as DailyReportDetail\n // 単一レポートの取得時にキャッシュを一括更新する\n return DailyReportCache.cacheMany([report], null, start)[0]\n })\n .finally(() => DailyReportCache.pendingReports.delete(id))\n DailyReportCache.pendingReports.set(id, p)\n return p\n}\n\n/**\n * Fetch reports by date key.\n * 日付キーでレポートを取得する。\n */\nconst fetchReportsByDate = async (apiBasePath: string, dateKey: string) => {\n const list = DailyReportCache.getList(dateKey)\n if (list) return list\n\n const pending = DailyReportCache.pendingLists.get(dateKey)\n if (pending) return pending\n\n const start = Date.now()\n const p = fetch(`${apiBasePath}/business-date?businessDate=${encodeURIComponent(dateKey)}`, { cache: \"no-cache\" })\n .then((res) => (res.ok ? res.json() : Promise.reject(`HTTP ${res.status}`)))\n .then((data) => {\n if (data.error) throw new Error(data.error.message)\n return DailyReportCache.cacheMany(data.reports ?? [], dateKey, start)\n })\n .finally(() => DailyReportCache.pendingLists.delete(dateKey))\n DailyReportCache.pendingLists.set(dateKey, p)\n return p\n}\n\n// --- Hooks ---\nexport type DailyReportDetailResource = { report: DailyReportDetail | null; error: Error | null; isLoading: boolean; isRefetching: boolean }\n\n/**\n * Provides a fetcher-driven daily report detail reader with business-date aware caching.\n * 営業日キャッシュ対応の日報詳細取得フック。\n */\nexport const useDailyReportDetail = (reportHubId: number, businessDate: string | null): DailyReportDetailResource => {\n const { apiBasePath } = useDailyReportConfig()\n const [report, setReport] = useState<DailyReportDetail | null>(() => DailyReportCache.getReport(reportHubId))\n const [error, setError] = useState<Error | null>(null)\n const [isLoading, setIsLoading] = useState(false)\n const [isRefetching, setIsRefetching] = useState(false)\n const reqRef = useRef({ id: reportHubId, token: 0 })\n\n // 1. Cache Subscription\n // キャッシュの購読: グローバルな DailyReportCache が更新されたときにローカルの状態を反応的に更新する。\n // 依存関係: [reportHubId] - 対象のレポート ID が変更されたときのみ再購読する。\n // クリーンアップ: メモリリークを防ぐためにリスナーを解除する。\n useEffect(() => {\n const onUpdate = (uid: number) => {\n if (uid === reportHubId) {\n const r = DailyReportCache.getReport(reportHubId, true)\n setReport(r)\n }\n }\n DailyReportCache.listeners.add(onUpdate)\n return () => {\n DailyReportCache.listeners.delete(onUpdate)\n }\n }, [reportHubId])\n\n // 2. Data Loading & Stale Check\n // データのロードと有効期限切れチェック: データのフェッチサイクル、 stale-while-revalidate ロジック、リクエストのデバウンスを管理する。\n // 依存関係: [reportHubId, businessDate, apiBasePath]\n useEffect(() => {\n const dateKey = normalizeBusinessDateKey(businessDate)\n const token = (reqRef.current.token || 0) + 1\n reqRef.current = { id: reportHubId, token }\n\n if (reportHubId <= 0) {\n setReport(null)\n setError(reportHubId === 0 ? new Error(\"Invalid ID\") : null)\n setIsLoading(false)\n setIsRefetching(false)\n return\n }\n\n // Cache Check\n let cached = DailyReportCache.getReport(reportHubId, true)\n let isStale = false\n if (cached) {\n isStale = !DailyReportCache.isValid(DailyReportCache.reports.get(reportHubId)?.expiresAt ?? 0)\n } else if (dateKey) {\n const list = DailyReportCache.getList(dateKey, true)\n const found = list?.find((r) => r.reportHubId === reportHubId)\n if (found) {\n cached = found\n isStale = !DailyReportCache.isValid(DailyReportCache.lists.get(dateKey)?.expiresAt ?? 0)\n }\n }\n setReport(cached)\n\n // If cached and valid, we are done\n if (cached && !isStale) {\n setError(null)\n setIsLoading(false)\n setIsRefetching(false)\n return\n }\n\n setIsLoading(!cached)\n setIsRefetching(true)\n\n // 未キャッシュ (骨組み表示中) の初回取得は即時に走らせる。デバウンスはスクロール連打の\n // 無駄取りを抑える目的なので、スクロール churn の無い初回に効かせると純粋な遅延になる。\n // キャッシュ済み (内容表示中) の stale 再取得だけデバウンスして churn を吸収する\n const debounceMs = cached ? DAILY_REPORT_FETCH_DEBOUNCE_MS : 0\n\n const timer = setTimeout(async () => {\n if (reqRef.current.token !== token) return\n try {\n let r: DailyReportDetail | null = null\n if (dateKey) {\n const res = await fetchReportsByDate(apiBasePath, dateKey)\n r = res.find((item) => item.reportHubId === reportHubId) ?? null\n } else {\n r = await fetchReportById(apiBasePath, reportHubId)\n }\n\n if (reqRef.current.token === token) {\n setReport(r)\n setError(null)\n }\n } catch (e) {\n if (reqRef.current.token === token) setError(e instanceof Error ? e : new Error(\"Load failed\"))\n } finally {\n if (reqRef.current.token === token) {\n setIsLoading(false)\n setIsRefetching(false)\n }\n }\n }, debounceMs)\n\n return () => clearTimeout(timer)\n }, [reportHubId, businessDate, apiBasePath])\n\n return { report, error, isLoading, isRefetching }\n}\n\n/**\n * Provides a business-date aware prefetch helper.\n * 営業日キャッシュを活用した日報プリフェッチ補助フック。\n */\nexport const useDailyReportPrefetch = () => {\n const { apiBasePath } = useDailyReportConfig()\n return useCallback(\n (date: string | null) => {\n const k = normalizeBusinessDateKey(date)\n if (k && !DailyReportCache.getList(k)) fetchReportsByDate(apiBasePath, k).catch((e) => logger.warn(\"Prefetch error\", k, e))\n },\n [apiBasePath],\n )\n}\n","/**\n * Business date normalization and formatting helper functions.\n * 営業日の正規化とフォーマットを支援するユーティリティ群。\n */\n\ntype BusinessDateInput = string | Date | null | undefined\n\nconst ISO_DATE_PATTERN = /^\\d{4}-\\d{2}-\\d{2}$/\nconst SLASH_DATE_PATTERN = /^\\d{4}\\/\\d{2}\\/\\d{2}$/\n\nconst zeroPad = (value: number): string => {\n return value < 10 ? `0${value}` : `${value}`\n}\n\nconst normalizeFromDate = (value: Date): string | null => {\n if (Number.isNaN(value.getTime())) {\n return null\n }\n\n const year = value.getFullYear()\n const month = zeroPad(value.getMonth() + 1)\n const day = zeroPad(value.getDate())\n return `${year}-${month}-${day}`\n}\n\n/**\n * Returns today's business-date key (YYYY-MM-DD) in the local calendar.\n * ローカルカレンダー上の「本日」の営業日キー (YYYY-MM-DD) を返す。\n *\n * `new Date().toISOString()` の日付部は UTC 日付になり、UTC より東のタイムゾーン\n * (JST など) では深夜〜午前帯に前日へずれる。営業日はブラウザのローカル暦日で判定する\n * ため、ローカル getter から直接組む。タイムゾーンはパッケージに固定しない (消費側の\n * 実行環境が決める) 方針に沿う。\n *\n * @param now Clock injection for tests (defaults to the current time). テスト用の時刻注入 (既定は現在時刻)。\n * @returns Local calendar day as YYYY-MM-DD. ローカル暦日の YYYY-MM-DD。\n */\nexport const resolveTodayBusinessDateKey = (now: Date = new Date()): string => {\n if (Number.isNaN(now.getTime())) {\n // 不正な Date が注入されたら現在時刻で解決する (空の営業日キーを返さない)\n return resolveTodayBusinessDateKey(new Date())\n }\n return `${now.getFullYear()}-${zeroPad(now.getMonth() + 1)}-${zeroPad(now.getDate())}`\n}\n\n/**\n * Normalizes business date input into YYYY-MM-DD format string.\n * 営業日を YYYY-MM-DD 形式の文字列へ正規化する。\n */\nexport const normalizeBusinessDateKey = (value: BusinessDateInput): string | null => {\n if (value === null || value === undefined) {\n return null\n }\n\n if (value instanceof Date) {\n return normalizeFromDate(value)\n }\n\n const trimmed = value.trim()\n if (trimmed === \"\") {\n return null\n }\n\n if (ISO_DATE_PATTERN.test(trimmed)) {\n return trimmed\n }\n\n if (SLASH_DATE_PATTERN.test(trimmed)) {\n return trimmed.replaceAll(\"/\", \"-\")\n }\n\n const parsed = new Date(trimmed)\n return normalizeFromDate(parsed)\n}\n\n/**\n * Converts normalized business date into display format YYYY-MM-DD.\n * 正規化済みの営業日を YYYY-MM-DD 形式の表示文字列へ変換する。\n */\nexport const formatBusinessDateDisplay = (value: BusinessDateInput): string | null => {\n const normalized = normalizeBusinessDateKey(value)\n if (!normalized) {\n return null\n }\n return normalized\n}\n\n/**\n * Parses normalized business date into Date object.\n * 正規化した営業日を Date オブジェクトへ変換する。\n */\nexport const parseBusinessDateKeyToDate = (value: BusinessDateInput): Date | null => {\n const normalized = normalizeBusinessDateKey(value)\n if (!normalized) {\n return null\n }\n\n const [year, month, day] = normalized.split(\"-\").map((part) => Number.parseInt(part, 10))\n if ([year, month, day].some((component) => Number.isNaN(component))) {\n return null\n }\n\n return new Date(year, month - 1, day)\n}\n","import { type ScrollBarTapCircleOptions, type ScrollPaneInertiaOptions, tapScrollCircleSampleVisual } from \"@aiquants/virtualscroll\"\nimport type { DailyReportItem } from \"../../shared/types\"\nimport type { DailyReportDetailResource } from \"../hooks/use-daily-report\"\n\n/**\n * Shared constants for Daily Report data fetching parameters.\n * 日報データ取得で利用する共通パラメーター。\n */\nexport const DAILY_REPORT_FETCH_DEBOUNCE_MS = 120\n\n/**\n * Delay before the provider auto-connects SSE, freeing a browser connection slot for first paint.\n * SSE を自動接続するまでの遅延。初回描画とその中身フェッチへ接続枠を譲るための猶予。\n *\n * HTTP/1.1 のオリジン当たり 6 接続のうち、ids ストリームと SSE で 2 枠を常時占有する。\n * SSE 接続を初回描画直後まで遅らせ、その 1 枠を初回の中身フェッチ (`/business-date`) に回す。\n * リアルタイム更新の初動がこの分だけ遅れるが、その間の状態はストリーム / SWR が担保する。\n */\nexport const SSE_INITIAL_CONNECT_DEFER_MS = 1000\n\nexport const DEFAULT_ITEM_HEIGHT = 160\nexport const ITEM_VERTICAL_PADDING = 6\nexport const ITEM_CONTAINER_HEIGHT = DEFAULT_ITEM_HEIGHT - ITEM_VERTICAL_PADDING\nexport const FAST_SCROLL_WHEEL_MULTIPLIER = 0.8\nexport const FAST_SCROLL_INERTIA_OPTIONS: ScrollPaneInertiaOptions = { maxVelocity: 18, deceleration: 0.0018, startVelocityThreshold: 0.03 }\nexport const PREFETCH_LOOKAHEAD = 20\nexport const PREFETCH_MAX_DATES = 6\nexport const EMPTY_ITEM: DailyReportItem = { reportHubId: -1, businessDate: null }\nexport const EMPTY_DETAIL: DailyReportDetailResource = { report: null, error: null, isLoading: false, isRefetching: false }\nexport const WHEEL_RESET_TIMEOUT_MS = 240\nexport const VIRTUAL_SCROLL_OVERSCAN_COUNT = 15\nexport const SCROLL_BAR_WIDTH = 8\nexport const MAX_PREVIEW_LINES = 3\n\n/**\n * Tap-scroll circle tuning shared by every daily-report list.\n * 日報のすべての一覧が共有するタップスクロールサークルの調整値。\n *\n * ❗ **List タブと DetailList タブで同じ値を使うこと。** 片方だけ独自定義にすると、\n * 同じ指の位置でタブごとに速度が何十倍も違う (実測で最大 77 倍) という事故になる。\n *\n * 値の根拠 (ビューポート 800px で実測):\n *\n * | 引き距離 | 速度 | List (行 160px) |\n * | --- | --- | --- |\n * | 0 | 160 px/s | 1 行/秒 |\n * | 1/4 | 525 px/s | 3.3 行/秒 |\n * | 1/2 | 1,298 px/s | 8 行/秒 |\n * | 最大 | 6,400 px/s | 40 行/秒 (0.67 行/フレーム) |\n *\n * - `exponentialSteepness: 3` — 引き幅の前半をゆっくりにして微速の分解能を確保する。\n * 大きくしすぎると低速域が最小速度へ張り付いて**引いても動かない死角**になる\n * (旧値 7.5 では引き幅の 43.85% が死角だった)。\n * - `exponentialScale: 8` — 最高速の上限 (ビューポート比)。`maxSpeedMultiplier` と揃えて\n * 上限がどちらで決まるかを一意にする。1 フレームあたりの移動をオーバースキャン\n * (`VIRTUAL_SCROLL_OVERSCAN_COUNT`) の行数より十分小さく保つ値にすること。\n * - `easedOffset` は使わない。最小速度 160 px/s で十分に遅く、下駄を履かせると\n * 低速域の分解能がその分だけ失われる。\n *\n * 長距離の移動はタップスクロールではなくスクロールバーのつまみ (と\n * `BusinessDayThumbOverlay` の日付表示) が担う。ここを速くして解こうとしない。\n */\nexport const TAP_SCROLL_CIRCLE_OPTIONS: ScrollBarTapCircleOptions = {\n enabled: true,\n renderVisual: tapScrollCircleSampleVisual,\n maxSpeedCurve: { exponentialSteepness: 3, exponentialScale: 8 },\n maxSpeedMultiplier: 8,\n}\n\n/**\n * Pointer types allowed to drag-scroll the list content.\n * 一覧のコンテンツ領域をドラッグしてスクロールできるポインタ種別。\n *\n * ❗ **`enablePointerDrag` を丸ごと切ってはならない。** `ScrollPane` はコンテンツを\n * transform で動かしておりネイティブのスクロール領域を持たない\n * (`.aqvs-scroll-pane-content` は `overflow: hidden`) ため、切るとタッチ端末から\n * スクロール手段が消える (一度そうしてしまい、モバイルで一切スクロールできなくなった)。\n *\n * マウスのドラッグだけを外す。行のタップ判定 (`useTapClick`) ・テキスト選択と競合するため。\n * 画面幅のメディアクエリで切り替える案は採らなかった。画面幅は入力手段を表さないので\n * 「主入力がマウスのタッチ対応ノート」のような端末を取りこぼす。判定は**イベントの\n * ポインタ種別**で行うため、同一端末でマウスと指を使い分けても常に正しく振る舞う。\n */\nexport const CONTENT_DRAG_POINTER_INPUTS = [\"pen\", \"touch\"] as const\n","/**\n * SSE connection lifecycle hook for daily-report real-time updates.\n * 日報リアルタイム更新の SSE 接続ライフサイクルフック。\n */\nimport { useEffect } from \"react\"\nimport { createLogger, LogLevel } from \"../../shared/logger\"\nimport { type DailyReportSseMessage, dailyReportSseMessageSchema } from \"../../shared/sse-schema\"\nimport { useDailyReportConfig } from \"../config-context\"\n\nconst logger = createLogger(LogLevel.INFO, \"daily-report-sse-connection\")\n\ntype UseDailyReportSseConnectionOptions = {\n isSseEnabled: boolean\n onMessage: (payload: Exclude<DailyReportSseMessage, { type: \"connected\" }>) => void\n}\n\n/**\n * Manages Daily Report SSE connection lifecycle with reconnect handling.\n * Handles open, message parse/dispatch, error logging, retry, and cleanup.\n *\n * 日報 SSE 接続のライフサイクルを管理する Hook。\n * 接続開始、メッセージ解析と配送、エラー時再接続、クリーンアップを管理します。\n */\nexport const useDailyReportSseConnection = ({ isSseEnabled, onMessage }: UseDailyReportSseConnectionOptions) => {\n const { ssePath } = useDailyReportConfig()\n\n /**\n * Maintains SSE connection while enabled and retries on error.\n * isSseEnabled が true の間は接続を維持し、エラー時は再接続します。\n */\n useEffect(() => {\n if (!isSseEnabled) return\n\n let eventSource: EventSource | null = null\n let reconnectTimer: ReturnType<typeof setTimeout> | null = null\n let reconnectAttempt = 0\n let lastReceivedEventId: string | undefined\n\n const connect = () => {\n eventSource?.close()\n eventSource = new EventSource(`${ssePath}${lastReceivedEventId ? `?lastEventId=${lastReceivedEventId}` : \"\"}`)\n\n eventSource.onopen = () => {\n reconnectAttempt = 0\n logger.info(\"SSE Connected\")\n }\n\n eventSource.onmessage = (event) => {\n if (event.lastEventId) {\n lastReceivedEventId = event.lastEventId\n }\n try {\n const raw = JSON.parse(event.data)\n const result = dailyReportSseMessageSchema.safeParse(raw)\n if (!result.success) {\n logger.error(\"SSE Validation Error:\", result.error.flatten(), \"raw message:\", raw)\n return\n }\n if (result.data.type === \"connected\") {\n logger.info(\"SSE Subscribe Ready\")\n return\n }\n onMessage(result.data)\n } catch (e) {\n logger.error(\"SSE Parse Error\", e)\n }\n }\n\n eventSource.onerror = (e) => {\n eventSource?.close()\n eventSource = null\n reconnectAttempt++\n const delayMs = Math.min(2000 * 2 ** (reconnectAttempt - 1), 30000)\n logger.error(`SSE Error. Retrying in ${delayMs}ms (attempt ${reconnectAttempt})...`, e)\n reconnectTimer = setTimeout(connect, delayMs)\n }\n }\n\n connect()\n\n return () => {\n if (eventSource) eventSource.close()\n if (reconnectTimer) clearTimeout(reconnectTimer)\n }\n }, [isSseEnabled, onMessage, ssePath])\n}\n","/**\n * Zod schemas for daily-report SSE messages (client/server shared contract).\n * 日報 SSE メッセージの zod スキーマ (client / server 共有契約)。\n */\nimport { z } from \"zod\"\nimport type { DailyReportCommentItem as DeclaredComment, DailyReportDetail as DeclaredDetail } from \"./types\"\n\n// --- サブスキーマ(types.ts の型と構造的に一致させる) ---\n\nexport const dailyReportInterviewerSchema = z.object({\n name: z.string().nullish(),\n affiliation: z.string().nullish(),\n})\n\nexport const dailyReportCommentSchema = z.object({\n name: z.string().nullish(),\n text: z.string().nullish(),\n color: z.string().nullish(),\n})\n\nexport const dailyReportLabelDefSchema = z.object({\n id: z.number(),\n name: z.string().nullish(),\n color: z.string().nullish(),\n})\n\nexport const dailyReportCommentItemSchema = z.object({\n id: z.number(),\n userId: z.string().nullish(),\n userName: z.string().nullish(),\n content: z.string().nullish(),\n createdAt: z.string().nullish(),\n isMine: z.boolean(),\n})\n\n/**\n * 添付ファイル 1 件のスキーマ。\n *\n * 周囲のフィールドは `.nullish()` だが、ここは意図して `.nullable()` を使う。\n * `.nullish()` は推論キーを任意 (`?`) にするため、`types.ts` 側の必須 `fileType: string | null`\n * へ代入できず、ファイル末尾の `_AssertDetailCompat` が `never` に落ちてコンパイルエラーになる。\n * 既存の `.nullish()` フィールドはいずれも `types.ts` 側が `?` 付きの任意キーとペアになっており、\n * 「`.nullish()` ↔ 任意キー」「`.nullable()` ↔ 必須 `| null`」の対応で揃っている。\n * `| undefined` を混ぜないことは、詳細型を消費する DuckDB ステージ行の要請でもある。\n */\nexport const dailyReportAttachmentItemSchema = z.object({\n id: z.string(),\n fileName: z.string(),\n fileType: z.string().nullable(),\n fileSize: z.number().nullable(),\n createdAt: z.string().nullable(),\n state: z.enum([\"present\", \"absent\", \"unknown\"]),\n})\n\nexport const dailyReportDetailSchema = z.object({\n reportHubId: z.number(),\n date: z.string().nullish(),\n createdAt: z.string().nullish(),\n author: z.string().nullish(),\n userId: z.string().nullish(),\n sourceType: z.string().nullish(),\n employeeName: z.string().nullish(),\n updatedBy: z.string().nullish(),\n updatedAt: z.string().nullish(),\n category: z.string().nullish(),\n creationCategory: z.string().nullish(),\n visitTimeFrom: z.string().nullish(),\n visitTimeTo: z.string().nullish(),\n customerName: z.string().nullish(),\n interviewers: z.array(dailyReportInterviewerSchema),\n subject: z.string().nullish(),\n content: z.string().nullish(),\n comments: z.array(dailyReportCommentSchema),\n isRead: z.boolean(),\n isStarred: z.boolean(),\n labels: z.array(dailyReportLabelDefSchema),\n commentItems: z.array(dailyReportCommentItemSchema),\n // ❗ `.default([])` は必須。SSE の実体は永続 Redis Stream (MAXLEN 保持) であり、\n // 配備をまたいで**旧形式のエントリが残る**。共有リーダーはプロセス起動時に先頭から\n // 再生し、再接続クライアントの catch-up も同じエントリを読み直すため、\n // このキーを必須にすると添付機能を入れる前に publish された全エントリが\n // fail-closed で捨てられ、再接続タブへ更新が永久に届かなくなる。\n // 送信側の詰め忘れ検知は types.ts の必須フィールドと `_AssertDetailCompat`\n // (出力型は既定値適用後なので必須のまま) が担っており、ここを緩めても失われない。\n attachments: z.array(dailyReportAttachmentItemSchema).default([]),\n})\n\n// --- SSE メッセージスキーマ(8種) ---\n\nexport const connectedMessageSchema = z.object({\n type: z.literal(\"connected\"),\n})\n\nexport const statusUpdateMessageSchema = z.object({\n type: z.literal(\"status-update\"),\n reportHubId: z.number(),\n statusType: z.enum([\"star\", \"read\"]),\n value: z.boolean(),\n clientTempId: z.string(),\n // recipientRawUserId はサーバーサイドフィルタリング専用。\n // SSE ルートが raw JSON から取得してフィルタ後に除去する。\n // クライアントには到達しないが、サーバー側の publish で .parse() を通すため定義が必要。\n recipientRawUserId: z.number().optional(),\n})\n\nexport const commentAddMessageSchema = z.object({\n type: z.literal(\"comment-add\"),\n reportHubId: z.number(),\n comment: dailyReportCommentItemSchema,\n clientTempId: z.string(),\n // 下書き日報のコメントは所有者以外に見えてはならない (本文の全員配信を防ぐ)。\n // サーバーサイドフィルタ専用で、SSE ルートがフィルタ後に除去する\n recipientRawUserId: z.number().optional(),\n})\n\nexport const commentDeleteMessageSchema = z.object({\n type: z.literal(\"comment-delete\"),\n reportHubId: z.number(),\n commentId: z.number(),\n clientTempId: z.string(),\n // comment-add と同じ理由の宛先フィルタ\n recipientRawUserId: z.number().optional(),\n})\n\nexport const reportCreateMessageSchema = z.object({\n type: z.literal(\"report-create\"),\n reportHubId: z.number(),\n report: dailyReportDetailSchema,\n clientTempId: z.string(),\n recipientRawUserId: z.number().optional(),\n})\n\nexport const reportUpdateMessageSchema = z.object({\n type: z.literal(\"report-update\"),\n reportHubId: z.number(),\n report: dailyReportDetailSchema,\n clientTempId: z.string(),\n recipientRawUserId: z.number().optional(),\n})\n\nexport const reportPublishMessageSchema = z.object({\n type: z.literal(\"report-publish\"),\n reportHubId: z.number(),\n report: dailyReportDetailSchema,\n clientTempId: z.string(),\n recipientRawUserId: z.number().optional(),\n})\n\nexport const reportDeleteMessageSchema = z.object({\n type: z.literal(\"report-delete\"),\n reportHubId: z.number(),\n clientTempId: z.string(),\n})\n\n// --- Discriminated Union ---\n\nexport const dailyReportSseMessageSchema = z.discriminatedUnion(\"type\", [\n connectedMessageSchema,\n statusUpdateMessageSchema,\n commentAddMessageSchema,\n commentDeleteMessageSchema,\n reportCreateMessageSchema,\n reportUpdateMessageSchema,\n reportPublishMessageSchema,\n reportDeleteMessageSchema,\n])\n\n// --- 型エクスポート ---\n\nexport type DailyReportSseMessage = z.infer<typeof dailyReportSseMessageSchema>\n\n// --- コンパイル時の型互換チェック ---\n// Zod推論型が既存の型定義に代入可能であることを保証する。\n// ここでエラーが出たらスキーマと types.ts がずれている。\n\ntype _AssertDetailCompat = z.infer<typeof dailyReportDetailSchema> extends DeclaredDetail ? true : never\ntype _AssertCommentCompat = z.infer<typeof dailyReportCommentItemSchema> extends DeclaredComment ? true : never\nconst _detailCheck: _AssertDetailCompat = true\nconst _commentCheck: _AssertCommentCompat = true\nvoid _detailCheck\nvoid _commentCheck\n","/**\n * Module-resident session and React hook for the daily-report ids stream.\n * 日報 ID ストリームのモジュール常駐セッションと React フック。\n *\n * セッション (クライアント実体 + 完走時刻) をモジュールスコープに置くことで、\n * ページのアンマウント/再マウントをまたいで受信済みデータが生き残り、\n * 再訪時はローディング画面なしで即座に一覧を復元できる。鮮度は\n * 「完走から一定時間経過後の再訪でバックグラウンド再検証」(SWR) で担保する。\n * マウント中のリアルタイム性は SSE が担うため、これで欠落窓は実質「離脱中」のみになる。\n */\nimport { useSyncExternalStore } from \"react\"\nimport type { DailyReportItem } from \"../../shared/types\"\nimport { defaultDailyReportClientConfig } from \"../config-context\"\nimport { clearDailyReportCache } from \"../hooks/use-daily-report\"\nimport { DailyReportIdsStreamClient, type DailyReportIdsStreamState } from \"./daily-report-ids-stream-client\"\n\n/** 完走からこの時間を過ぎた再訪で、バックグラウンド再検証を走らせる。 */\nconst REVALIDATE_AFTER_MS = 60_000\n\n/** セッション 1 世代分の記録。 */\ntype SessionRecord = {\n client: DailyReportIdsStreamClient\n /** セッションの持ち主 (難読化ユーザー ID。未登録ユーザーは null)。 */\n userKey: string | null\n /** 正規化済み API ベースパス (既定値解決済み)。 */\n apiBasePath: string\n unsubscribe: () => void\n}\n\nlet session: SessionRecord | null = null\n\n/** セッションの生成/破棄とクライアント publish の両方を伝えるリスナー群。 */\nconst sessionListeners = new Set<() => void>()\n\n/** セッション未生成時に返す不変スナップショット (SSR の getServerSnapshot も兼ねる)。 */\nconst IDLE_STATE: DailyReportIdsStreamState = {\n items: [],\n loadedCount: 0,\n totalCount: null,\n phase: \"idle\",\n isRevalidating: false,\n hasCompletedOnce: false,\n completedAt: null,\n error: null,\n}\n\n/**\n * Full-document reload indirection (mockable seam for auth-expiry tests).\n * ドキュメント全体を再読込する間接層 (認証切れテストで差し替え可能にする継ぎ目)。\n */\nexport const reloadDocument = (): void => {\n if (typeof window !== \"undefined\") {\n window.location.reload()\n }\n}\n\n/** 全セッションリスナーへ変更を通知する。 */\nconst notifySessionListeners = (): void => {\n for (const listener of sessionListeners) {\n listener()\n }\n}\n\n/** 現行セッションを破棄する (ユーザー切替・HMR・テストリセット用)。 */\nconst destroySession = (): void => {\n if (!session) return\n session.unsubscribe()\n session.client.dispose()\n session = null\n notifySessionListeners()\n}\n\n/**\n * Ensures a stream session for the given user, creating/refreshing as needed.\n * 指定ユーザーのストリームセッションを確立する処理 (必要に応じ生成/再検証)。\n *\n * - ユーザーまたは API パスが変わっていれば作り直す (別ユーザーのデータを見せない)。\n * - failed なら自動 retry (再訪 = ユーザーの再試行意思とみなす)。\n * - 完走から {@link REVALIDATE_AFTER_MS} 経過していればバックグラウンド再検証。\n *\n * SSR では何もしない。clientLoader とページマウントの両方から呼ばれ、冪等。\n *\n * @param options.apiBasePath API ベースパス (config の apiBasePath)。\n * @param options.userKey 難読化ユーザー ID (未登録ユーザーは null)。\n */\nexport const ensureDailyReportIdsStreamSession = (options: { apiBasePath?: string; userKey: string | null }): void => {\n if (typeof window === \"undefined\") return\n\n // 呼び出し元によって undefined / 既定文字列が混在するため、比較前に必ず正規化する。\n // 正規化を欠くと clientLoader (undefined) とページ effect (既定文字列) の往復で\n // セッションが毎回破棄され、モジュール常駐による即時復元が丸ごと無効化される\n const apiBasePath = options.apiBasePath ?? defaultDailyReportClientConfig.apiBasePath\n\n if (session && (session.userKey !== options.userKey || session.apiBasePath !== apiBasePath)) {\n // ユーザーが変わる場合はモジュール常駐の詳細キャッシュも破棄する\n // (旧ユーザーが閲覧した下書き詳細等が TTL 内の新ユーザーへ供給されるのを防ぐ)\n if (session.userKey !== options.userKey) {\n clearDailyReportCache()\n }\n destroySession()\n }\n\n if (!session) {\n const client = new DailyReportIdsStreamClient({ apiBasePath })\n session = {\n client,\n userKey: options.userKey,\n apiBasePath,\n // 通知はそのままセッションリスナーへ中継する (完走時刻はクライアントが state に刻む)\n unsubscribe: client.subscribe(notifySessionListeners),\n }\n client.start()\n notifySessionListeners()\n return\n }\n\n runFreshnessCheck(session)\n}\n\n/**\n * Retries a failed session or starts a background revalidation when stale.\n * failed セッションの retry、または鮮度切れ時のバックグラウンド再検証を起動する処理。\n *\n * 再検証が既に走行中 (`isRevalidating`) の間は何もしない。ガードを欠くと、1 回の\n * 再訪で走る複数の確立呼び出し (clientLoader の prime / ensure / ページ effect) が\n * それぞれ走行中の再検証を中断して先頭からやり直し、NDJSON を多重ダウンロードする。\n */\nconst runFreshnessCheck = (record: SessionRecord): void => {\n const state = record.client.getState()\n if (state.phase === \"failed\") {\n record.client.retry()\n return\n }\n if (state.phase === \"complete\" && !state.isRevalidating && state.completedAt !== null && Date.now() - state.completedAt > REVALIDATE_AFTER_MS) {\n record.client.refresh()\n }\n}\n\n/**\n * Bootstraps the session from the clientLoader: create when absent, else reconcile.\n * clientLoader からのセッション起動処理。不在なら生成、既存なら照合と鮮度確認。\n *\n * コールドロードの最重要ゲート対策: 生成をページマウントの effect まで遅らせると、\n * ストリームの最初のバイト要求が「serverLoader 完了 → ハイドレーション → effect flush」\n * の後ろに直列化される。ローダー時点で生成すればハイドレーションと並行してフェッチが走る。\n *\n * パス契約: 生成時のパスは factory オプション (未指定は既定値へ正規化)。\n * `config.apiBasePath` だけを上書きした利用者では初回のみ誤パスで生成されるが、\n * ページマウント側の ensure がパス不一致を検知して作り直す (自己修復・初回 1 回きり)。\n * **既存セッションのパスはここでは照合しない** — 照合すると、factory 未指定 ×\n * config 上書きの組み合わせでナビゲーションごとにセッションが破棄されるスラッシングになる。\n *\n * @param options.apiBasePath 生成時に使う API ベースパス (未指定は既定値)。\n * @param options.userKey 難読化ユーザー ID (未登録ユーザーは null)。\n */\nexport const bootstrapDailyReportIdsStreamSession = (options: { apiBasePath?: string; userKey: string | null }): void => {\n if (typeof window === \"undefined\") return\n if (!session) {\n ensureDailyReportIdsStreamSession(options)\n return\n }\n if (session.userKey !== options.userKey) {\n // 別ユーザーのデータは描画前に破棄し (詳細キャッシュ含む)、新ユーザーのストリームを即時開始する\n clearDailyReportCache()\n destroySession()\n ensureDailyReportIdsStreamSession(options)\n return\n }\n runFreshnessCheck(session)\n}\n\n/**\n * Kicks a freshness check for an existing session before the user is known.\n * ユーザー確定前に既存セッションの鮮度確認だけを先行させる処理。\n *\n * clientLoader が serverLoader を await する前に呼び、再訪 (支配的経路) の\n * 再検証開始を数十〜数百 ms 前倒しする。セッションが無ければ何もしない\n * (誰のデータを読むべきか分からないため)。直後の reconcile が userKey を照合し、\n * ユーザーが変わっていればセッションごと破棄されるので先行は安全。\n * 走行中の再検証は中断しない (runFreshnessCheck の isRevalidating ガード)。\n */\nexport const primeDailyReportIdsStreamSession = (): void => {\n if (typeof window === \"undefined\" || !session) return\n runFreshnessCheck(session)\n}\n\n/**\n * Relays an externally-known item upsert into the resident stream data.\n * 外部由来のアイテム upsert を常駐ストリームデータへ中継する処理。\n *\n * プロバイダーの作成/公開系ミューテーション (ローカル操作と SSE) から呼ばれる。\n * これを欠くと、鮮度窓内の再訪で「作成したばかりの日報が消える」。\n */\nexport const upsertDailyReportIdsStreamItem = (item: DailyReportItem): void => {\n session?.client.applyExternalUpsert(item)\n}\n\n/**\n * Relays an externally-known item removal into the resident stream data.\n * 外部由来のアイテム削除を常駐ストリームデータへ中継する処理。\n *\n * report-delete SSE・ローカル削除・stale 安全弁から呼ばれる。\n * これを欠くと、鮮度窓内の再訪で「削除したはずの日報が蘇る」。\n */\nexport const removeDailyReportIdsStreamItem = (reportHubId: number): void => {\n session?.client.applyExternalRemoval(reportHubId)\n}\n\n/**\n * Restarts a failed stream (UI retry button entry point).\n * failed ストリームを再開する処理 (UI の再試行ボタン入口)。\n */\nexport const retryDailyReportIdsStream = (): void => {\n session?.client.retry()\n}\n\n/**\n * Re-validates the stream, optionally bypassing the server-side cache.\n * ストリームを再検証する処理 (サーバーキャッシュのバイパス指定可)。\n *\n * 開発向けリロード/キャッシュクリアボタンから呼ばれる。\n *\n * @param options.forceRefresh サーバー側 SqlResultCache をバイパスするか。\n */\nexport const refreshDailyReportIdsStream = (options: { forceRefresh?: boolean } = {}): void => {\n session?.client.refresh(options)\n}\n\n/**\n * Returns the current session's owner key (undefined when no session exists).\n * 現行セッションの持ち主 (userKey) を返す処理 (セッション不在時は undefined)。\n *\n * 遅延解決するミューテーション継続の門番に使う。POST /action の待機中にリロード無しの\n * ユーザー切替が起きると、切替時の三重遮断 (セッション破棄 + 詳細キャッシュ破棄 +\n * プロバイダーリマウント) の**後**に旧ユーザーの継続が解決し、モジュール常駐状態\n * (新ユーザーのセッション・詳細キャッシュ) へ旧ユーザーのデータを注入してしまう。\n * 継続側は開始時点の userKey とこの値を比較し、不一致 (undefined 含む) なら\n * モジュール常駐状態への書き込みを放棄する (fail-closed)。\n */\nexport const getDailyReportIdsStreamSessionUserKey = (): string | null | undefined => session?.userKey\n\n/** セッション購読 (useSyncExternalStore 用)。 */\nconst subscribeSession = (listener: () => void): (() => void) => {\n sessionListeners.add(listener)\n return () => sessionListeners.delete(listener)\n}\n\n/** 現在のスナップショットを返す (セッション未生成時は不変の IDLE)。 */\nconst getSessionSnapshot = (): DailyReportIdsStreamState => session?.client.getState() ?? IDLE_STATE\n\n/** SSR スナップショット (常に IDLE)。 */\nconst getServerSnapshot = (): DailyReportIdsStreamState => IDLE_STATE\n\n/**\n * React binding for the module-resident ids stream session.\n * モジュール常駐 ids ストリームセッションの React バインディング。\n *\n * セッションが後から生成されても再購読なしで追従する (リスナーはセッションではなく\n * モジュールに属する)。スナップショット参照は publish 単位で安定している。\n *\n * @returns Current stream state. 現在のストリーム状態。\n */\nexport const useDailyReportIdsStream = (): DailyReportIdsStreamState => {\n return useSyncExternalStore(subscribeSession, getSessionSnapshot, getServerSnapshot)\n}\n\n/**\n * Destroys the session (test isolation helper).\n * セッションを破棄するテスト用ヘルパー。\n */\nexport const resetDailyReportIdsStreamSessionForTesting = (): void => {\n destroySession()\n}\n\n// HMR: モジュール差し替え時に旧セッションの接続とリスナーを確実に始末する\nif (typeof import.meta !== \"undefined\" && (import.meta as { hot?: { dispose: (cb: () => void) => void } }).hot) {\n ;(import.meta as unknown as { hot: { dispose: (cb: () => void) => void } }).hot.dispose(() => {\n destroySession()\n })\n}\n","/**\n * Resilient NDJSON stream client for the daily-report ids feed (React-free).\n * 日報 ID 一覧 NDJSON フィードの耐障害ストリームクライアント (React 非依存)。\n *\n * fetch + ReadableStream で `{apiBasePath}/ids-stream` を読み、チャンク到着ごとに\n * 蓄積スナップショットを publish する。切断・サーバーエラー行・未完終端は\n * カーソル再開付きの指数バックオフで自己回復し、受信済みデータは常に保持する。\n * React へは `subscribe` / `getState` (安定参照スナップショット) だけを公開し、\n * `useSyncExternalStore` でそのまま束縛できる形にする。\n */\nimport { type DailyReportIdsStreamCursor, type DailyReportIdsStreamLine, isIdsStreamChunkLine } from \"../../shared/ids-stream\"\nimport type { DailyReportItem } from \"../../shared/types\"\n\n/** ストリームのライフサイクル局面。 */\nexport type DailyReportIdsStreamPhase = \"idle\" | \"streaming\" | \"retrying\" | \"complete\" | \"failed\" | \"auth-required\"\n\n/** publish されるスナップショット。publish 間で参照が安定している。 */\nexport type DailyReportIdsStreamState = {\n /** 受信済みアイテム (reportHubId で重複排除済み)。 */\n items: DailyReportItem[]\n /** 一意アイテム件数 (`items.length` と同値。表示用の別名)。 */\n loadedCount: number\n /** サーバー申告の全体件数。先頭行未着なら null。 */\n totalCount: number | null\n /** ライフサイクル局面。 */\n phase: DailyReportIdsStreamPhase\n /** complete 後のバックグラウンド再検証が走っているか。 */\n isRevalidating: boolean\n /** 一度でも完走したか (再検証中の UI 判定に使う)。 */\n hasCompletedOnce: boolean\n /**\n * 直近の「実際の完走」時刻 (未完走は null)。鮮度判定 (SWR 窓) の唯一の根拠。\n * 完走の publish でのみ更新される — 再検証の諦めなど phase が complete のままの\n * publish で更新すると、古いデータが新鮮扱いされ再検証が永久に走らなくなる。\n */\n completedAt: number | null\n /** 直近の失敗 (成功で null に戻る)。 */\n error: Error | null\n}\n\n/** 1 接続試行の結末。 */\ntype StreamAttemptOutcome = { kind: \"complete\" } | { kind: \"aborted\" } | { kind: \"auth-required\" } | { kind: \"retryable\"; error: Error }\n\n/** テスト注入用の依存一式。 */\nexport type DailyReportIdsStreamClientOptions = {\n /** API ベースパス (既定 `/daily_report/api`)。 */\n apiBasePath?: string\n /** fetch 実装 (既定 globalThis.fetch)。 */\n fetchFn?: (input: string, init: { signal: AbortSignal; headers: Record<string, string>; cache: RequestCache }) => Promise<Response>\n /** タイマー実装 (既定 setTimeout / clearTimeout)。 */\n setTimeoutFn?: (handler: () => void, ms: number) => ReturnType<typeof setTimeout>\n clearTimeoutFn?: (id: ReturnType<typeof setTimeout>) => void\n /**\n * 1 回の read が無音でいられる上限 (ms)。0 以下で無効。\n * これを超えたら接続を half-open とみなして中断し、retryable として\n * カーソル再開へ倒す。サーバーのハートビート間隔より必ず長く取る\n * (生きているが遅いだけの全量クエリ待ちを誤って切らないため)。\n */\n readStallTimeoutMs?: number\n /** 警告ロガー (既定 console.warn)。 */\n warn?: (...args: unknown[]) => void\n /**\n * チャンク由来 publish の合体窓 (ms)。0 で無効 (毎チャンク即時 publish)。\n * 高速回線ではネットワーク read が密集し、publish ごとに全件ソート + 全行再レンダーが\n * 走るため、先頭は即時 (初回描画優先)・後続は窓内で合体するスロットルを掛ける。\n * 完走 publish と外部ミューテーション (ユーザー操作の即時フィードバック) は対象外。\n */\n publishCoalesceMs?: number\n}\n\n/** チャンク由来 publish の既定合体窓 (ms)。 */\nconst DEFAULT_PUBLISH_COALESCE_MS = 50\n\n/** 初回リトライ遅延。 */\nconst INITIAL_RETRY_DELAY_MS = 1_000\n\n/** リトライ遅延の上限。 */\nconst MAX_RETRY_DELAY_MS = 30_000\n\n/** 連続失敗の上限。到達で failed へ落ち、手動 retry 待ちになる。 */\nconst MAX_CONSECUTIVE_FAILURES = 6\n\n/**\n * 1 回の read が無音でいられる既定の上限 (ms)。\n * サーバーのハートビート間隔 (15s) の 2 倍を取り、生きた接続を誤って切らない。\n * 超過した read は half-open 接続とみなし retryable に倒す。\n */\nconst DEFAULT_READ_STALL_TIMEOUT_MS = 30_000\n\n/** read が stall ウォッチドッグに捕まったことを表す番兵。 */\nconst READ_STALL = Symbol(\"read-stall\")\n\n/** 既定の API ベースパス (config-context の既定と一致させる)。 */\nconst DEFAULT_API_BASE_PATH = \"/daily_report/api\"\n\n/** 再開カーソルとして採用する営業日の唯一の形式 (サーバー側の検証契約と一致)。 */\nconst CURSOR_DATE_PATTERN = /^\\d{4}-\\d{2}-\\d{2}$/\n\n/**\n * NDJSON ids stream client with dedupe, cursor resume, and exponential backoff.\n * 重複排除・カーソル再開・指数バックオフを備えた NDJSON ids ストリームクライアント。\n */\nexport class DailyReportIdsStreamClient {\n private readonly apiBasePath: string\n private readonly fetchFn: NonNullable<DailyReportIdsStreamClientOptions[\"fetchFn\"]>\n private readonly setTimeoutFn: NonNullable<DailyReportIdsStreamClientOptions[\"setTimeoutFn\"]>\n private readonly clearTimeoutFn: NonNullable<DailyReportIdsStreamClientOptions[\"clearTimeoutFn\"]>\n private readonly warn: NonNullable<DailyReportIdsStreamClientOptions[\"warn\"]>\n /** 1 回の read が無音でいられる上限 (ms)。0 以下で無効。 */\n private readonly readStallTimeoutMs: number\n\n /** reportHubId → アイテム。挿入順を保ち、再開の重複到着を吸収する。 */\n private readonly itemMap = new Map<number, DailyReportItem>()\n private readonly listeners = new Set<() => void>()\n\n private state: DailyReportIdsStreamState = {\n items: [],\n loadedCount: 0,\n totalCount: null,\n phase: \"idle\",\n isRevalidating: false,\n hasCompletedOnce: false,\n completedAt: null,\n error: null,\n }\n\n private lastCursor: DailyReportIdsStreamCursor | null = null\n private consecutiveFailures = 0\n private started = false\n private disposed = false\n /** 実行中ループの世代。refresh/dispose で進め、旧ループの続行を無効化する。 */\n private loopGeneration = 0\n private activeAbortController: AbortController | null = null\n /**\n * 先頭 (cursor 無し) から始まった走査で観測した ID の集合。\n * 完走時にこの集合へ含まれない Map エントリを剪定し、サーバー側で削除された\n * 日報がモジュール常駐データに永久残留するのを防ぐ。途中失敗のカーソル再開は\n * 同じ走査の続きなので集合を引き継ぐ (全区間の和 = サーバーの全量)。\n */\n private activeSeenIds: Set<number> | null = null\n /**\n * 外部削除のトゥームストーン。サーバーは配列全体を先に確定させてから送出するため、\n * 走査中に届いた削除の後、同じ削除前スナップショットの後続チャンクが同 ID を\n * 再配送してくる。これを吸収せずに素通しすると、削除済み日報が Map へ戻り\n * 完走剪定も免れてしまう (観測集合に再記録されるため)。新しい走査の開始\n * (削除コミット後のスナップショット) でクリアする。\n */\n private readonly externalRemovalTombstones = new Set<number>()\n /**\n * 走行中の再検証中に届いた明示的キャッシュバイパス要求の持ち越しフラグ。\n * 走行中の再検証は中断しない方針のため、要求を黙って捨てず完走後に連結実行する。\n */\n private pendingForceRefresh = false\n private wakeRetryWait: (() => void) | null = null\n private readonly handleOnline = () => this.wakeRetryWait?.()\n\n /** チャンク由来 publish の合体窓 (ms)。 */\n private readonly publishCoalesceMs: number\n /** 合体窓のトレーリングタイマー。 */\n private publishCoalesceTimer: ReturnType<typeof setTimeout> | null = null\n /** 直近のアイテム publish 時刻 (leading edge 判定用)。 */\n private lastItemsPublishAt = 0\n\n constructor(options: DailyReportIdsStreamClientOptions = {}) {\n this.apiBasePath = options.apiBasePath ?? DEFAULT_API_BASE_PATH\n this.fetchFn = options.fetchFn ?? ((input, init) => fetch(input, init))\n this.setTimeoutFn = options.setTimeoutFn ?? ((handler, ms) => setTimeout(handler, ms))\n this.clearTimeoutFn = options.clearTimeoutFn ?? ((id) => clearTimeout(id))\n this.warn = options.warn ?? ((...args) => console.warn(...args))\n this.readStallTimeoutMs = options.readStallTimeoutMs ?? DEFAULT_READ_STALL_TIMEOUT_MS\n this.publishCoalesceMs = options.publishCoalesceMs ?? DEFAULT_PUBLISH_COALESCE_MS\n if (typeof window !== \"undefined\") {\n // オフライン復帰でバックオフ待機を即時解除する\n window.addEventListener(\"online\", this.handleOnline)\n }\n }\n\n /**\n * Returns the current snapshot (stable reference between publishes).\n * 現在のスナップショットを返す処理 (publish 間で参照が安定)。\n */\n getState(): DailyReportIdsStreamState {\n return this.state\n }\n\n /**\n * Subscribes to snapshot updates.\n * スナップショット更新の購読を登録する処理。\n *\n * @param listener Change callback. 変更通知コールバック。\n * @returns Unsubscribe function. 購読解除関数。\n */\n subscribe(listener: () => void): () => void {\n this.listeners.add(listener)\n return () => this.listeners.delete(listener)\n }\n\n /**\n * Starts the initial stream (idempotent).\n * 初回ストリーミングを開始する処理 (冪等)。\n */\n start(): void {\n if (this.started || this.disposed) return\n this.started = true\n void this.runStreamLoop({ revalidate: false, forceRefresh: false })\n }\n\n /**\n * Restarts after a terminal failure (no-op unless phase is \"failed\").\n * failed からの手動再開処理 (failed 以外では何もしない)。\n *\n * 受信済みデータと再開カーソルは保持したまま続きから読む。\n */\n retry(): void {\n if (this.disposed || this.state.phase !== \"failed\") return\n this.consecutiveFailures = 0\n void this.runStreamLoop({ revalidate: this.state.hasCompletedOnce, forceRefresh: false })\n }\n\n /**\n * Revalidates a completed stream in the background (SWR-style).\n * 完走済みストリームをバックグラウンドで再検証する処理 (SWR)。\n *\n * カーソルを捨てて先頭から読み直し、Map の重複排除で差分を上書き吸収し、\n * 完走時に「今回の走査で観測されなかった行」を剪定する (サーバー側削除の反映)。\n * failed のときはカーソルを保持した再開に切り替える (forceRefresh 指定は失わない)。\n * streaming/retrying 中は何もしない。\n *\n * @param options.forceRefresh サーバー側 SqlResultCache をバイパスするか。\n */\n refresh(options: { forceRefresh?: boolean } = {}): void {\n if (this.disposed) return\n if (this.state.phase === \"failed\") {\n // retry() 相当だが forceRefresh の指定を落とさない (キャッシュクリア意図の保存)\n this.consecutiveFailures = 0\n void this.runStreamLoop({ revalidate: this.state.hasCompletedOnce, forceRefresh: options.forceRefresh ?? false })\n return\n }\n if (this.state.phase !== \"complete\") return\n // 走行中の再検証を別の refresh が中断して先頭からやり直すと、ensure の多重呼び出し\n // (clientLoader の prime / ensure / ページ effect) だけで再訪ごとに 3 連ダウンロードになる\n if (this.state.isRevalidating) {\n // ただし明示的なキャッシュバイパス要求 (キャッシュクリアボタン) は黙って捨てず、\n // 走行中の再検証の完走後に連結実行する\n if (options.forceRefresh) {\n this.pendingForceRefresh = true\n }\n return\n }\n this.lastCursor = null\n this.consecutiveFailures = 0\n void this.runStreamLoop({ revalidate: true, forceRefresh: options.forceRefresh ?? false })\n }\n\n /**\n * Applies an externally-known upsert (SSE / local mutation) to the resident data.\n * 外部由来 (SSE / ローカル操作) の upsert を常駐データへ反映する処理。\n *\n * これを欠くと、マウント中に作成された日報がストリームセッションに存在せず、\n * 鮮度窓内の再訪 (再検証なし) で一覧から消える。アクティブな走査中は観測集合にも\n * 刻み、完走時の剪定で「生きていると分かっている行」を誤って落とさないようにする。\n */\n applyExternalUpsert(item: DailyReportItem): void {\n if (this.disposed || !item || typeof item.reportHubId !== \"number\") return\n // 明示的な upsert は「生きている」の権威シグナルなので削除トゥームストーンを解除する\n this.externalRemovalTombstones.delete(item.reportHubId)\n this.activeSeenIds?.add(item.reportHubId)\n const existing = this.itemMap.get(item.reportHubId)\n // sourceType 未指定の upsert (プロバイダーは知らない) は既存値を保持する\n const next: DailyReportItem = { reportHubId: item.reportHubId, businessDate: item.businessDate, sourceType: item.sourceType ?? existing?.sourceType }\n if (!existing || existing.businessDate !== next.businessDate || existing.sourceType !== next.sourceType) {\n this.itemMap.set(item.reportHubId, next)\n this.publishItems()\n }\n }\n\n /**\n * Applies an externally-known removal (SSE report-delete / stale valve) to the resident data.\n * 外部由来 (SSE report-delete / stale 安全弁) の削除を常駐データへ反映する処理。\n *\n * 走査中の後続チャンク (削除前スナップショット) による同一行の再配送は、\n * クライアント側トゥームストーンが次の新しい走査までブロックする。\n */\n applyExternalRemoval(reportHubId: number): void {\n if (this.disposed) return\n this.activeSeenIds?.delete(reportHubId)\n // 削除前スナップショットの後続チャンクによる再配送を、次の新しい走査までブロックする\n this.externalRemovalTombstones.add(reportHubId)\n if (this.itemMap.delete(reportHubId)) {\n this.publishItems()\n }\n }\n\n /**\n * Aborts everything and detaches listeners (terminal).\n * すべてを中断しリスナーを外す終了処理 (以後再利用不可)。\n */\n dispose(): void {\n this.disposed = true\n this.loopGeneration += 1\n this.activeAbortController?.abort()\n this.cancelScheduledPublishItems()\n // 保留中のバックオフ待機を起こす (finish が自分のタイマーを片付ける)\n this.wakeRetryWait?.()\n if (typeof window !== \"undefined\") {\n window.removeEventListener(\"online\", this.handleOnline)\n }\n this.listeners.clear()\n }\n\n /**\n * Connection-retry loop: stream → classify → backoff → resume.\n * 接続リトライループ。ストリーム → 結果分類 → バックオフ → カーソル再開。\n */\n private async runStreamLoop(mode: { revalidate: boolean; forceRefresh: boolean }): Promise<void> {\n const generation = ++this.loopGeneration\n // 進行中の旧接続 (再検証開始時など) を中断し、バックオフ待機中の旧ループも\n // 即時に起こして退場させる (放置すると旧タイマーが残り続ける)\n this.activeAbortController?.abort()\n this.wakeRetryWait?.()\n\n // 先頭からの走査 (初回/再検証) は削除剪定用の観測集合を新調し、外部削除の\n // トゥームストーンをクリアする (これから受けるスナップショットは削除コミット後)。\n // カーソル再開 (途中失敗からの retry) は同じ走査の続きなので両方引き継ぐ\n if (this.lastCursor === null) {\n this.activeSeenIds = new Set()\n this.externalRemovalTombstones.clear()\n }\n\n // このループ自体が forced run なら、持ち越し要求はここで満たされたことにする\n // (満たされないまま残ると、無関係な後続の再検証に古い強制走査が連鎖する)\n if (mode.forceRefresh) {\n this.pendingForceRefresh = false\n }\n\n this.publish({\n phase: mode.revalidate ? this.state.phase : \"streaming\",\n isRevalidating: mode.revalidate,\n error: null,\n })\n\n while (!this.disposed && generation === this.loopGeneration) {\n // forceRefresh はループ全体で維持する。試行がサーバーへ届く前に失敗した場合、\n // 1 回で降ろすと後続リトライが古いサーバーキャッシュを掴む\n const outcome = await this.streamOnce(mode.forceRefresh)\n if (this.disposed || generation !== this.loopGeneration) return\n\n if (outcome.kind === \"complete\") {\n this.pruneUnseenItems()\n // 完走 = 成功。失敗予算をリセットする (前進ゼロの完走もあり得るため、\n // 進行時リセットだけに頼ると連結実行される次のループへ古い予算が漏れる)\n this.consecutiveFailures = 0\n // 完走 publish が全量を運ぶため、保留中のトレーリング publish は不要\n this.cancelScheduledPublishItems()\n this.lastItemsPublishAt = Date.now()\n this.publish({\n phase: \"complete\",\n isRevalidating: false,\n hasCompletedOnce: true,\n completedAt: Date.now(),\n error: null,\n items: Array.from(this.itemMap.values()),\n loadedCount: this.itemMap.size,\n })\n // 走行中に持ち越された明示的キャッシュバイパス要求を連結実行する\n if (this.pendingForceRefresh && !this.disposed) {\n this.pendingForceRefresh = false\n this.lastCursor = null\n void this.runStreamLoop({ revalidate: true, forceRefresh: true })\n }\n return\n }\n if (outcome.kind === \"aborted\") return\n if (outcome.kind === \"auth-required\") {\n this.publish({ phase: \"auth-required\", isRevalidating: false })\n return\n }\n\n this.consecutiveFailures += 1\n if (this.consecutiveFailures >= MAX_CONSECUTIVE_FAILURES) {\n // 再検証中の力尽きは既存データを守って complete のまま静かに諦める\n // (completedAt は更新しない — 次の再訪で再検証が確実に走るように)\n if (mode.revalidate && this.state.hasCompletedOnce) {\n this.warn(\"[DailyReportIdsStream] revalidation gave up:\", outcome.error.message)\n // 持ち越し要求も破棄する (死んだ再検証の後に古い強制走査を連鎖させない)\n this.pendingForceRefresh = false\n this.publish({ isRevalidating: false, error: outcome.error })\n } else {\n this.publish({ phase: \"failed\", isRevalidating: false, error: outcome.error })\n }\n return\n }\n\n this.publish({ phase: mode.revalidate ? this.state.phase : \"retrying\", error: outcome.error })\n await this.waitBeforeRetry(Math.min(INITIAL_RETRY_DELAY_MS * 2 ** (this.consecutiveFailures - 1), MAX_RETRY_DELAY_MS))\n }\n }\n\n /**\n * Prunes map entries not observed by the completed full scan.\n * 完走した全量走査で観測されなかった Map エントリを剪定する処理。\n *\n * サーバー側で削除された日報の残留 (再訪時の亡霊復活・loadedCount > totalCount) を防ぐ。\n * 観測集合が無い場合 (理論上到達しない防御) は何も消さない。\n */\n private pruneUnseenItems(): void {\n const seen = this.activeSeenIds\n this.activeSeenIds = null\n if (!seen) return\n for (const id of Array.from(this.itemMap.keys())) {\n if (!seen.has(id)) {\n this.itemMap.delete(id)\n }\n }\n }\n\n /**\n * Runs one streaming attempt and classifies its outcome.\n * 1 回のストリーミング試行を実行し、結末を分類する処理。\n */\n private async streamOnce(forceRefresh: boolean): Promise<StreamAttemptOutcome> {\n const abortController = new AbortController()\n this.activeAbortController = abortController\n try {\n // ヘッダー到着前 (再開経路は全量クエリ確定までヘッダーを返さない) にはハートビートが\n // 流れないため、read より緩い上限 (3 倍 = 既定 90s。サーバーの requestTimeout 60s より\n // 長い) で fetch 自体も見張る。ここで切れた場合も retryable としてカーソル再開する\n const fetchPromise = this.fetchFn(this.buildStreamUrl(forceRefresh), {\n signal: abortController.signal,\n headers: { Accept: \"application/x-ndjson\" },\n cache: \"no-store\",\n })\n let headerTimer: ReturnType<typeof setTimeout> | null = null\n const response =\n this.readStallTimeoutMs <= 0\n ? await fetchPromise\n : await Promise.race([\n fetchPromise,\n new Promise<typeof READ_STALL>((resolve) => {\n headerTimer = this.setTimeoutFn(() => resolve(READ_STALL), this.readStallTimeoutMs * 3)\n }),\n ]).finally(() => {\n if (headerTimer !== null) this.clearTimeoutFn(headerTimer)\n })\n if (response === READ_STALL) {\n abortController.abort()\n // 破棄した接続の解決/拒否を未処理のまま残さない\n fetchPromise.catch(() => {})\n return { kind: \"retryable\", error: new Error(\"Stream stalled: no response headers within timeout\") }\n }\n if (response.status === 401 || response.status === 403) {\n return { kind: \"auth-required\" }\n }\n if (!(response.ok && response.body)) {\n return { kind: \"retryable\", error: new Error(`ids stream HTTP ${response.status}`) }\n }\n\n const reader = response.body.getReader()\n const decoder = new TextDecoder()\n let partialLine = \"\"\n let sawErrorLine = false\n let sawIsLast = false\n\n while (true) {\n const chunk = await this.readWithStallGuard(reader)\n if (chunk === READ_STALL) {\n // 無音が続いた half-open 接続。中断してカーソル再開へ倒す\n // (finally が activeAbortController を掃除する)\n abortController.abort()\n void reader.cancel().catch(() => {})\n return { kind: \"retryable\", error: new Error(\"Stream stalled: no data within timeout\") }\n }\n const { done, value } = chunk\n if (done) break\n // ネットワークチャンクは行境界と無関係に割れるため、末尾の欠け行を持ち越す\n const lines = (partialLine + decoder.decode(value, { stream: true })).split(\"\\n\")\n partialLine = lines.pop() ?? \"\"\n\n let mutated = false\n for (const rawLine of lines) {\n if (rawLine.trim() === \"\") continue\n let parsed: DailyReportIdsStreamLine\n try {\n parsed = JSON.parse(rawLine) as DailyReportIdsStreamLine\n } catch {\n // 単一行の破損はスキップし、接続レベルの再開に委ねる\n this.warn(\"[DailyReportIdsStream] skipped malformed line\")\n continue\n }\n if (!isIdsStreamChunkLine(parsed)) {\n sawErrorLine = true\n continue\n }\n mutated = this.absorbChunkItems(parsed.items) || mutated\n if (typeof parsed.total === \"number\" && Number.isFinite(parsed.total) && parsed.total !== this.state.totalCount) {\n // total 単独の変化でも必ず購読者へ通知する (無通知のスナップショット\n // 置換は useSyncExternalStore のスナップショット安定契約を破る)\n this.publish({ totalCount: parsed.total })\n }\n if (parsed.nextCursor && (parsed.nextCursor.businessDate === null || CURSOR_DATE_PATTERN.test(parsed.nextCursor.businessDate))) {\n // 契約外の日付 (混入データ等) を持つカーソルは採用しない。\n // 日付を欠いた id だけの再開は NULL 営業日領域へスキップして欠落を生むため、\n // 直前の正当なカーソル (無ければ先頭) から読み直す方が安全\n this.lastCursor = parsed.nextCursor\n }\n if (parsed.isLast) {\n sawIsLast = true\n }\n }\n if (mutated) {\n // 前進があった時点で失敗カウンタを戻す (断続的な不調で failed へ落とさない)\n this.consecutiveFailures = 0\n this.schedulePublishItems()\n }\n }\n\n if (sawErrorLine) {\n return { kind: \"retryable\", error: new Error(\"Stream interrupted by server error\") }\n }\n if (!sawIsLast) {\n // サーバー再起動や中間切断。カーソルから再開する\n return { kind: \"retryable\", error: new Error(\"Stream ended before completion\") }\n }\n return { kind: \"complete\" }\n } catch (error) {\n // DOMException (ブラウザー/undici) と Error 派生 (テスト DI) の両形を受ける\n if (error instanceof Error && error.name === \"AbortError\") {\n return { kind: \"aborted\" }\n }\n return { kind: \"retryable\", error: error instanceof Error ? error : new Error(String(error)) }\n } finally {\n if (this.activeAbortController === abortController) {\n this.activeAbortController = null\n }\n }\n }\n\n /**\n * Reads one chunk, racing the read against a stall watchdog.\n * 1 チャンクを読む処理。読み取りを stall ウォッチドッグと競争させる。\n *\n * サーバーが無音のまま接続を half-open で放置しても (NAT/プロキシのアイドル切断・\n * VPN 断・スリープ)、`reader.read()` は永久に保留したままになる。これを検知するため\n * read とタイマーを競争させ、無音が上限を超えたら番兵を返す。生きているが遅いだけの\n * 全量クエリ待ちはサーバーのハートビート (空行) が read を解決するので誤検知しない。\n * `readStallTimeoutMs <= 0` のときは素の read (タイマーを張らない・テスト用)。\n *\n * @returns 読み取り結果、または stall を表す番兵。read の reject はそのまま伝播する。\n */\n private async readWithStallGuard(reader: ReadableStreamDefaultReader<Uint8Array>): Promise<ReadableStreamReadResult<Uint8Array> | typeof READ_STALL> {\n if (this.readStallTimeoutMs <= 0) {\n return reader.read()\n }\n let stallTimer: ReturnType<typeof setTimeout> | null = null\n const stallPromise = new Promise<typeof READ_STALL>((resolve) => {\n stallTimer = this.setTimeoutFn(() => resolve(READ_STALL), this.readStallTimeoutMs)\n })\n try {\n return await Promise.race([reader.read(), stallPromise])\n } finally {\n if (stallTimer !== null) {\n this.clearTimeoutFn(stallTimer)\n }\n }\n }\n\n /**\n * Merges chunk items into the dedupe map.\n * チャンクのアイテム群を重複排除 Map へ取り込む処理。\n *\n * @returns True when anything was added or changed. 追加または内容変化があれば true。\n */\n private absorbChunkItems(items: DailyReportItem[]): boolean {\n let mutated = false\n for (const item of items) {\n if (!item || typeof item.reportHubId !== \"number\") continue\n // 走査中に外部削除された ID は、同じ削除前スナップショットの後続チャンクが\n // 再配送してきても受け入れない (受けると剪定も免れて再訪時に蘇る)\n if (this.externalRemovalTombstones.has(item.reportHubId)) continue\n // 内容が既知でも「観測した」事実は剪定用集合へ必ず刻む\n this.activeSeenIds?.add(item.reportHubId)\n const existing = this.itemMap.get(item.reportHubId)\n if (!existing || existing.businessDate !== item.businessDate || existing.sourceType !== item.sourceType) {\n this.itemMap.set(item.reportHubId, item)\n mutated = true\n }\n }\n return mutated\n }\n\n /** アイテム配列を再スナップショットして即時 publish する。 */\n private publishItems(): void {\n this.lastItemsPublishAt = Date.now()\n this.publish({ items: Array.from(this.itemMap.values()), loadedCount: this.itemMap.size })\n }\n\n /**\n * チャンク由来のアイテム publish をスロットルする (leading edge 即時 + trailing 合体)。\n * 最初のチャンクは即時に描画へ届け、窓内に密集した後続チャンクは 1 回の publish\n * (= 1 回の全件ソートと再レンダー) へ合体する。合体窓 0 は無効 (常に即時)。\n */\n private schedulePublishItems(): void {\n if (this.publishCoalesceMs <= 0) {\n this.publishItems()\n return\n }\n const elapsed = Date.now() - this.lastItemsPublishAt\n if (elapsed >= this.publishCoalesceMs) {\n this.publishItems()\n return\n }\n if (this.publishCoalesceTimer !== null) return\n this.publishCoalesceTimer = this.setTimeoutFn(() => {\n this.publishCoalesceTimer = null\n this.publishItems()\n }, this.publishCoalesceMs - elapsed)\n }\n\n /** 保留中のトレーリング publish を破棄する (完走 publish が全量を運ぶ場合など)。 */\n private cancelScheduledPublishItems(): void {\n if (this.publishCoalesceTimer !== null) {\n this.clearTimeoutFn(this.publishCoalesceTimer)\n this.publishCoalesceTimer = null\n }\n }\n\n /** 部分状態をマージした新スナップショットを作り、購読者へ通知する。 */\n private publish(partial: Partial<DailyReportIdsStreamState>): void {\n this.state = { ...this.state, ...partial }\n for (const listener of this.listeners) {\n listener()\n }\n }\n\n /** 再開カーソル付きのストリーム URL を構築する。 */\n private buildStreamUrl(forceRefresh: boolean): string {\n const params = new URLSearchParams()\n if (this.lastCursor) {\n params.set(\"cursor_report_hub_id\", String(this.lastCursor.reportHubId))\n // null 営業日領域の再開は日付パラメータを省略する (サーバー側の契約)\n if (this.lastCursor.businessDate !== null) {\n params.set(\"cursor_business_date\", this.lastCursor.businessDate)\n }\n }\n if (forceRefresh) {\n params.set(\"forceRefresh\", \"true\")\n }\n const query = params.toString()\n return `${this.apiBasePath}/ids-stream${query ? `?${query}` : \"\"}`\n }\n\n /**\n * バックオフ待機。online イベント・dispose・後続ループの開始で早期解除される。\n *\n * タイマー ID は待機ごとのローカルに閉じ、共有スロット (wakeRetryWait) は\n * 「まだ自分を指しているときだけ」外す。単一スロットへ素朴に書くと、\n * 追い越された旧待機の finish が新しいループのタイマーを消してしまい、\n * 再検証が永久に固まる (実測済みの競合)。\n */\n private waitBeforeRetry(delayMs: number): Promise<void> {\n return new Promise((resolve) => {\n let timerId: ReturnType<typeof setTimeout> | null = null\n const finish = () => {\n if (timerId !== null) {\n this.clearTimeoutFn(timerId)\n timerId = null\n }\n if (this.wakeRetryWait === finish) {\n this.wakeRetryWait = null\n }\n resolve()\n }\n this.wakeRetryWait = finish\n timerId = this.setTimeoutFn(finish, delayMs)\n })\n }\n}\n","/**\n * Error-surface seam for daily-report mutations.\n * 日報ミューテーション失敗を利用者へ可視化するための通知シーム。\n *\n * 保存・公開・削除・コメント等の失敗は、これまで握り潰されてボタンが成功したかのように\n * 復帰していた。ここでミューテーションの失敗を一元的に受け取り、`config.onError` が\n * 注入されていればホストへ委譲し、無ければパッケージ内蔵の `role=\"alert\"` バナーで表示する。\n * `notifyError` は Provider 外でも安全に呼べる (既定は no-op) ため、アクションコンテキストは\n * 存在を前提にせず呼び出せる。\n */\nimport { createContext, type ReactNode, useCallback, useContext, useEffect, useMemo, useRef, useState } from \"react\"\nimport { type DailyReportErrorInfo, useDailyReportConfig } from \"../config-context\"\n\n/** バナーの自動消滅までの既定時間 (ms)。手動クローズでも消せる。 */\nconst ERROR_BANNER_AUTO_DISMISS_MS = 8_000\n\ntype DailyReportErrorSurface = {\n /** ミューテーション失敗を通知する。 */\n notifyError: (info: DailyReportErrorInfo) => void\n}\n\n/** 既定は no-op (Provider 外で呼んでも壊れない)。 */\nconst DailyReportErrorContext = createContext<DailyReportErrorSurface>({ notifyError: () => {} })\n\n/**\n * Returns the error-surface API (safe no-op outside the provider).\n * 通知 API を返す処理 (Provider 外では no-op)。\n */\nexport const useDailyReportErrorSurface = (): DailyReportErrorSurface => useContext(DailyReportErrorContext)\n\n/**\n * Provides the mutation error surface and renders the default alert banner.\n * ミューテーション通知シームを供給し、既定のアラートバナーを描画するプロバイダー。\n *\n * `config.onError` があればホストへ委譲し、内蔵バナーは描画しない (二重表示防止)。\n */\nexport const DailyReportErrorProvider = ({ children }: { children: ReactNode }) => {\n const { onError } = useDailyReportConfig()\n const [banner, setBanner] = useState<DailyReportErrorInfo | null>(null)\n // onError を ref 経由で読み、notifyError の参照を安定させる (毎回新関数だと購読側が再レンダー)\n const onErrorRef = useRef(onError)\n onErrorRef.current = onError\n const dismissTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)\n\n const notifyError = useCallback((info: DailyReportErrorInfo) => {\n const host = onErrorRef.current\n if (host) {\n // ホストが通知 UI を持つ場合は委譲し、内蔵バナーは出さない\n host(info)\n return\n }\n setBanner(info)\n }, [])\n\n const dismiss = useCallback(() => {\n if (dismissTimerRef.current !== null) {\n clearTimeout(dismissTimerRef.current)\n dismissTimerRef.current = null\n }\n setBanner(null)\n }, [])\n\n /**\n * バナー表示中は一定時間で自動的に消す副作用。\n * 依存配列: banner (新しい失敗が来るたびにタイマーを張り直す)。\n * クリーンアップ: タイマー解除 (アンマウント/再通知でのリークと二重発火を防ぐ)。\n */\n useEffect(() => {\n if (!banner) return\n dismissTimerRef.current = setTimeout(() => setBanner(null), ERROR_BANNER_AUTO_DISMISS_MS)\n return () => {\n if (dismissTimerRef.current !== null) {\n clearTimeout(dismissTimerRef.current)\n dismissTimerRef.current = null\n }\n }\n }, [banner])\n\n const value = useMemo(() => ({ notifyError }), [notifyError])\n\n return (\n <DailyReportErrorContext.Provider value={value}>\n {children}\n {banner && (\n <div\n // role=\"alert\" で SR へ即時読み上げさせる。fixed トーストで操作を妨げない\n role=\"alert\"\n data-testid=\"daily-report-error-banner\"\n className=\"fade-in slide-in-from-bottom-2 fixed inset-x-0 bottom-4 z-[100] mx-auto flex w-fit max-w-[92vw] animate-in items-center gap-3 rounded-lg border border-red-300 bg-red-600 px-4 py-2.5 text-sm text-white shadow-lg duration-200 dark:border-red-500/60 dark:bg-red-700\">\n <span className=\"font-medium\">{banner.message}</span>\n <button type=\"button\" aria-label=\"閉じる\" onClick={dismiss} className=\"shrink-0 rounded px-1.5 py-0.5 font-bold text-white/90 transition-colors hover:bg-red-700 hover:text-white dark:hover:bg-red-800\">\n ✕\n </button>\n </div>\n )}\n </DailyReportErrorContext.Provider>\n )\n}\n","/**\n * Comment management hook merging server data with optimistic local updates.\n * サーバーデータとローカルの楽観的更新を統合して日報コメントを管理するフック。\n */\nimport { useCallback, useMemo, useRef, useState } from \"react\"\nimport { mergeComments, type UIComment } from \"../../shared/comment-adapter\"\nimport type { DailyReportDetail } from \"../../shared/types\"\nimport { useDailyReportActionContext } from \"../contexts/daily-report-action-context\"\n\ntype User = { displayName?: string; name?: { familyName?: string; givenName?: string }; emails?: { value: string }[] }\n\n/**\n * Manages daily report comments, integrating server data with optimistic local updates.\n * サーバーデータとローカルの楽観的更新を統合して日報コメントを管理するフック。\n */\nexport const useDailyReportComments = (report: DailyReportDetail | null, user: User | null | undefined, userId?: string | null) => {\n const { pendingAddCommentIds, pendingDeleteCommentIds, resolvedIdMap, version, addComment, deleteComment } = useDailyReportActionContext()\n const [pendingDeleteId, setPendingDeleteId] = useState<number | string | null>(null)\n\n const handleDeleteComment = useCallback((cId: number) => report && deleteComment(report.reportHubId, cId, report.date ?? \"\"), [report, deleteComment])\n const handleAddComment = useCallback((content: string) => (report && user ? addComment(report.reportHubId, content, report.date ?? \"\") : Promise.resolve()), [report, user, addComment])\n\n const prevUiCommentsRef = useRef<UIComment[]>([])\n\n // UI用コメントリストの生成: サーバーデータ、レガシーコメント、楽観的更新(追加・削除)をマージして表示用データを構築する。\n // 依存関係: [report, user, userId, pendingDeleteCommentIds, pendingAddCommentIds, resolvedIdMap, version]\n // クリーンアップ: なし\n // biome-ignore lint/correctness/useExhaustiveDependencies: version は ref (pending*) 変更時の再計算トリガーとして意図的に依存させる\n const uiComments = useMemo(() => {\n if (!report) return []\n\n // 1. Base: Merge legacy and new comments\n // 基本データ: レガシーコメントと新しい形式のコメントをマージ。\n let comments = mergeComments(report.comments, report.commentItems, userId)\n\n // 2. Filter: Remove pending deletions\n // フィルタリング: 削除保留中のコメントを除外。\n const deletedIds = pendingDeleteCommentIds.current.get(report.reportHubId)\n if (deletedIds?.size) comments = comments.filter((c) => !deletedIds.has(c.id as number))\n\n // 3. Add: Append pending optimistic additions\n // 追加: 楽観的に追加されたコメントを末尾に追加。\n const addedComments = pendingAddCommentIds.current.get(report.reportHubId)\n if (addedComments?.size && user) {\n const existingIds = new Set(comments.map((c) => c.id))\n const authorName = user.displayName?.trim() || (user.name ? `${user.name.familyName ?? \"\"} ${user.name.givenName ?? \"\"}`.trim() : \"\") || user.emails?.[0]?.value || \"Unknown\"\n\n for (const [tempId, content] of addedComments) {\n if (deletedIds?.has(tempId)) continue\n if (existingIds.has(tempId)) continue\n\n // Skip if real ID resolved and already present\n // 実IDが解決済みで既にリストに存在する場合はスキップ。\n const realId = resolvedIdMap.current.get(tempId)\n if (realId && existingIds.has(realId)) continue\n\n // Skip if content duplicated (server data arrived before ID resolution)\n // 内容が重複している場合(ID解決前にサーバーデータが到達した場合)はスキップ。\n if (comments.some((c) => c.isMine && c.content === content && typeof c.id === \"number\" && c.id > 0)) continue\n\n comments.push({ id: tempId, authorName, content, createdAt: new Date().toISOString(), isMine: true, isLegacy: false })\n }\n }\n\n // Stable reference check\n // 参照の安定性チェック: 内容に変更がなければ前回の参照を返すことで不要な再レンダリングを防ぐ。\n if (JSON.stringify(comments) === JSON.stringify(prevUiCommentsRef.current)) return prevUiCommentsRef.current\n prevUiCommentsRef.current = comments\n return comments\n }, [report, user, userId, pendingDeleteCommentIds, pendingAddCommentIds, resolvedIdMap, version])\n\n return { uiComments, handleAddComment, handleDeleteComment, pendingDeleteId, setPendingDeleteId, resolvedIdMap: resolvedIdMap.current }\n}\n","/**\n * Shared hook for calculating viewport height of virtual scroll containers.\n * 仮想スクロールコンテナのビューポート高さを計測する共通フック。\n */\nimport { type RefObject, useCallback, useEffect, useState } from \"react\"\n\ntype UseDynamicViewportHeightOptions = {\n enableMutationObserver?: boolean\n bottomGap?: number\n minFallbackHeight?: number\n}\n\ntype UseDynamicViewportHeightResult = {\n viewportHeight: number\n updateViewportHeight: () => void\n}\n\n/**\n * Computes and updates the viewport height for scroll containers while reacting to layout changes.\n * レイアウト変化に追従しながらスクロールコンテナのビューポート高さを算出して更新する関数。\n */\nexport const useDynamicViewportHeight = (containerRef: RefObject<HTMLDivElement | null>, options?: UseDynamicViewportHeightOptions): UseDynamicViewportHeightResult => {\n const { enableMutationObserver = false, bottomGap = 4, minFallbackHeight = 240 } = options ?? {}\n const [viewportHeight, setViewportHeight] = useState(600)\n\n const updateViewportHeight = useCallback(() => {\n if (typeof window === \"undefined\") {\n return\n }\n\n const target = containerRef.current\n if (!target) {\n return\n }\n\n // 要素が非表示の場合は計測しない (display: none など)\n if (target.offsetParent === null && window.getComputedStyle(target).position !== \"fixed\") {\n return\n }\n\n const toNumber = (value: string) => Number.parseFloat(value) || 0\n const { top: containerTop } = target.getBoundingClientRect()\n const parentRect = target.parentElement?.getBoundingClientRect()\n const rootStyle = window.getComputedStyle(document.documentElement)\n const footerHeight = toNumber(rootStyle.getPropertyValue(\"--footer-height\"))\n const headerHeight = toNumber(rootStyle.getPropertyValue(\"--header-height\"))\n const scrollbarHeight = toNumber(rootStyle.getPropertyValue(\"--scrollbar-height\"))\n const containerStyle = window.getComputedStyle(target)\n const marginBottom = toNumber(containerStyle.marginBottom)\n const paddingBottom = toNumber(containerStyle.paddingBottom)\n const viewportBottom = window.innerHeight - footerHeight - scrollbarHeight - bottomGap\n const effectiveBottom = parentRect ? Math.min(parentRect.bottom, viewportBottom) : viewportBottom\n const layoutTop = parentRect ? Math.max(parentRect.top, headerHeight) : headerHeight\n const effectiveTop = Math.max(containerTop, layoutTop)\n const availableHeight = effectiveBottom - effectiveTop - marginBottom - paddingBottom\n const safeAvailableHeight = Number.isFinite(availableHeight) ? Math.floor(availableHeight) : 0\n const clampedHeight = safeAvailableHeight > 0 ? safeAvailableHeight : minFallbackHeight\n\n if (clampedHeight > 0 && Number.isFinite(clampedHeight)) {\n setViewportHeight((previous) => (Math.abs(previous - clampedHeight) > 1 ? clampedHeight : previous))\n }\n }, [bottomGap, containerRef, minFallbackHeight])\n\n useEffect(() => {\n if (typeof window === \"undefined\") {\n return\n }\n\n let frameId: number | null = null\n let timeoutId: number | null = null\n let resizeObserver: ResizeObserver | null = null\n let mutationObserver: MutationObserver | null = null\n const scheduleUpdate = () => {\n if (frameId !== null) {\n window.cancelAnimationFrame(frameId)\n }\n frameId = window.requestAnimationFrame(() => {\n updateViewportHeight()\n })\n }\n\n // 初期表示直後とわずかな遅延後に計測して安定させる\n scheduleUpdate()\n timeoutId = window.setTimeout(() => {\n scheduleUpdate()\n }, 150)\n\n const containerElement = containerRef.current\n if (typeof ResizeObserver !== \"undefined\" && containerElement) {\n resizeObserver = new ResizeObserver(() => {\n // コンテナと親要素のリサイズ変化に追従する\n scheduleUpdate()\n })\n resizeObserver.observe(containerElement)\n if (containerElement.parentElement) {\n resizeObserver.observe(containerElement.parentElement)\n }\n }\n\n if (enableMutationObserver && typeof MutationObserver !== \"undefined\") {\n mutationObserver = new MutationObserver(() => {\n // ルートのスタイル変更にも追従する\n scheduleUpdate()\n })\n mutationObserver.observe(document.documentElement, { attributes: true, attributeFilter: [\"style\"] })\n }\n\n window.addEventListener(\"resize\", scheduleUpdate)\n\n return () => {\n if (frameId !== null) {\n window.cancelAnimationFrame(frameId)\n }\n if (timeoutId !== null) {\n window.clearTimeout(timeoutId)\n }\n window.removeEventListener(\"resize\", scheduleUpdate)\n resizeObserver?.disconnect()\n mutationObserver?.disconnect()\n }\n }, [enableMutationObserver, updateViewportHeight, containerRef])\n\n return {\n viewportHeight,\n updateViewportHeight,\n }\n}\n","import { useEffect, useRef, useState } from \"react\"\nimport type { DailyReportDetail } from \"../../shared/types\"\nimport { useDailyReportActionContext } from \"../contexts/daily-report-action-context\"\nimport { Button } from \"../ui/button\"\nimport { Input } from \"../ui/input\"\nimport { Label } from \"../ui/label\"\nimport { Textarea } from \"../ui/textarea\"\n\ntype DailyReportEditFormProps = {\n report: DailyReportDetail\n onCancel?: () => void\n onPublishSuccess?: () => void\n}\n\nexport const DailyReportEditForm = ({ report, onCancel, onPublishSuccess }: DailyReportEditFormProps) => {\n const { updateReport, publishReport } = useDailyReportActionContext()\n const [title, setTitle] = useState(report.subject ?? \"\")\n const [content, setContent] = useState(report.content ?? \"\")\n const [isSaving, setIsSaving] = useState(false)\n\n // 同一レポート編集中に SSE/キャッシュ更新でフォームがリセットされるのを防止するための ID 追跡\n const syncedReportIdRef = useRef(report.reportHubId)\n\n // レポート切り替え時のみフォーム状態を同期する (同一レポートのキャッシュ更新ではリセットしない)\n useEffect(() => {\n if (report.reportHubId !== syncedReportIdRef.current) {\n setTitle(report.subject ?? \"\")\n setContent(report.content ?? \"\")\n syncedReportIdRef.current = report.reportHubId\n }\n }, [report.reportHubId, report.subject, report.content])\n\n const handleSave = async () => {\n setIsSaving(true)\n try {\n await updateReport(report.reportHubId, { title, content }, report.date ?? \"\")\n } catch {\n // 失敗はアクションコンテキストが role=\"alert\" バナー / onError で可視化する。\n // ここで握るのは未処理 Promise 拒否を防ぐためだけ (成功時のみ後続へ進む)\n } finally {\n setIsSaving(false)\n }\n }\n\n const handlePublish = async () => {\n setIsSaving(true)\n try {\n // Ensure latest changes are saved before publishing\n await updateReport(report.reportHubId, { title, content }, report.date ?? \"\")\n await publishReport(report.reportHubId, report.date ?? \"\")\n // 保存・公開の両方が成功したときだけ成功コールバックを呼ぶ\n onPublishSuccess?.()\n } catch {\n // 失敗はアクションコンテキストが可視化する。onPublishSuccess は呼ばない\n } finally {\n setIsSaving(false)\n }\n }\n\n return (\n <div className=\"space-y-6 p-1\">\n <div className=\"space-y-2\">\n <Label htmlFor={`report-title-${report.reportHubId}`}>タイトル</Label>\n <Input id={`report-title-${report.reportHubId}`} value={title} onChange={(e) => setTitle(e.target.value)} placeholder=\"日報のタイトルを入力\" disabled={isSaving} />\n </div>\n\n <div className=\"space-y-2\">\n <Label htmlFor={`report-content-${report.reportHubId}`}>内容</Label>\n <Textarea id={`report-content-${report.reportHubId}`} value={content} onChange={(e) => setContent(e.target.value)} placeholder=\"日報の内容を入力\" className=\"min-h-50\" disabled={isSaving} />\n </div>\n\n <div className=\"flex justify-end gap-2\">\n {onCancel && (\n <Button variant=\"outline\" onClick={onCancel} disabled={isSaving}>\n キャンセル\n </Button>\n )}\n <Button variant=\"secondary\" onClick={handleSave} disabled={isSaving}>\n 保存\n </Button>\n <Button onClick={handlePublish} disabled={isSaving}>\n 公開\n </Button>\n </div>\n </div>\n )\n}\n","import { Slot } from \"@radix-ui/react-slot\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport * as React from \"react\"\nimport { cn } from \"./cn\"\n\nconst buttonVariants = cva(\n \"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0\",\n {\n variants: {\n variant: {\n default: \"bg-primary text-primary-foreground hover:bg-primary/90\",\n destructive: \"bg-destructive text-destructive-foreground hover:bg-destructive/90\",\n outline: \"border border-input bg-background hover:bg-accent hover:text-accent-foreground\",\n secondary: \"bg-secondary text-secondary-foreground hover:bg-secondary/80\",\n ghost: \"hover:bg-accent hover:text-accent-foreground\",\n link: \"text-primary underline-offset-4 hover:underline\",\n },\n size: {\n default: \"h-10 px-4 py-2\",\n sm: \"h-9 rounded-md px-3\",\n lg: \"h-11 rounded-md px-8\",\n icon: \"h-10 w-10\",\n },\n },\n defaultVariants: {\n variant: \"default\",\n size: \"default\",\n },\n },\n)\n\nexport interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> {\n asChild?: boolean\n}\n\nconst Button = React.forwardRef<HTMLButtonElement, ButtonProps>(({ className, variant, size, asChild = false, ...props }, ref) => {\n const Comp = asChild ? Slot : \"button\"\n return <Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />\n})\nButton.displayName = \"Button\"\n\nexport { Button, buttonVariants }\n","import type * as React from \"react\"\n\nimport { cn } from \"./cn\"\n\nfunction Input({ className, type, ...props }: React.ComponentProps<\"input\">) {\n return (\n <input\n type={type}\n data-slot=\"input\"\n className={cn(\n \"flex h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-xs outline-none transition-[color,box-shadow] selection:bg-primary selection:text-primary-foreground file:inline-flex file:h-7 file:border-0 file:bg-transparent file:font-medium file:text-foreground file:text-sm placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30\",\n \"focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50\",\n \"aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40\",\n className,\n )}\n {...props}\n />\n )\n}\n\nexport { Input }\n","import * as LabelPrimitive from \"@radix-ui/react-label\"\nimport type * as React from \"react\"\n\nimport { cn } from \"./cn\"\n\nfunction Label({ className, ...props }: React.ComponentProps<typeof LabelPrimitive.Root>) {\n return (\n <LabelPrimitive.Root\n data-slot=\"label\"\n className={cn(\"flex select-none items-center gap-2 font-medium text-sm leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-50 group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50\", className)}\n {...props}\n />\n )\n}\n\nexport { Label }\n","import type * as React from \"react\"\n\nimport { cn } from \"./cn\"\n\nfunction Textarea({ className, ...props }: React.ComponentProps<\"textarea\">) {\n return (\n <textarea\n data-slot=\"textarea\"\n className={cn(\n \"flex min-h-15 w-full rounded-md border border-input bg-transparent px-3 py-2 text-base shadow-xs placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30\",\n \"aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40\",\n className,\n )}\n {...props}\n />\n )\n}\n\nexport { Textarea }\n","import { cn } from \"../ui/cn\"\n\ntype UnreadIndicatorProps = {\n className?: string\n}\n\n/**\n * A simple blue dot indicator used to represent unread status.\n * 未読状態を表すためのシンプルな青いドットインジケーター。\n */\nexport const UnreadIndicator = ({ className }: UnreadIndicatorProps) => {\n return <span className={cn(\"text-blue-500\", className)}>●</span>\n}\n","/**\n * Header badge that visualizes the ids stream lifecycle.\n * ID ストリームのライフサイクルをヘッダーへ可視化するバッジコンポーネント。\n *\n * 局面ごとの表示:\n * - streaming: 進捗ピル「読み込み中 n / total (p%)」(total 未着なら件数のみ)\n * - retrying: 「再接続中... (n 件受信済)」\n * - failed: 失敗表示 + 再試行ボタン (受信済みデータは保持されている)\n * - complete + 再検証中: 控えめな「最新化中」インジケーター\n * - それ以外 (idle / complete): 何も描画しない\n */\nimport { RefreshCw } from \"lucide-react\"\nimport { useAnimatedNumber } from \"../hooks/use-animated-number\"\nimport { retryDailyReportIdsStream, useDailyReportIdsStream } from \"../streaming/daily-report-ids-stream-session\"\n\n/**\n * Renders the streaming progress / recovery badge for the ids stream.\n * ids ストリームの進捗・復旧バッジを描画するコンポーネント。\n */\nexport const DailyReportIdsStreamStatus = () => {\n const stream = useDailyReportIdsStream()\n const animatedLoaded = useAnimatedNumber(stream.loadedCount)\n const animatedTotal = useAnimatedNumber(stream.totalCount ?? 0)\n\n if (stream.phase === \"streaming\") {\n // total 到着前は件数のみ、到着後は分母と進捗率を添える\n const percent = stream.totalCount && stream.totalCount > 0 ? Math.min(100, Math.round((stream.loadedCount / stream.totalCount) * 100)) : null\n return (\n <span data-testid=\"daily-report-ids-stream-status\" data-phase=\"streaming\" className=\"inline-flex items-center gap-1.5 rounded-full bg-blue-100 px-2 py-0.5 font-medium text-blue-800 text-xs tabular-nums dark:bg-blue-900/40 dark:text-blue-300\">\n <span aria-hidden=\"true\" className=\"h-1.5 w-1.5 animate-pulse rounded-full bg-blue-500 dark:bg-blue-400\" />\n 読み込み中 {animatedLoaded.toLocaleString()}\n {percent !== null ? ` / ${animatedTotal.toLocaleString()} (${percent}%)` : \" 件\"}\n </span>\n )\n }\n\n if (stream.phase === \"retrying\") {\n return (\n <span role=\"status\" data-testid=\"daily-report-ids-stream-status\" data-phase=\"retrying\" className=\"inline-flex items-center gap-1.5 rounded-full bg-amber-100 px-2 py-0.5 font-medium text-amber-800 text-xs tabular-nums dark:bg-amber-900/40 dark:text-amber-300\">\n <RefreshCw aria-hidden=\"true\" className=\"h-3 w-3 animate-spin\" />\n 再接続中... ({stream.loadedCount.toLocaleString()} 件受信済)\n </span>\n )\n }\n\n if (stream.phase === \"failed\") {\n return (\n <span role=\"status\" data-testid=\"daily-report-ids-stream-status\" data-phase=\"failed\" className=\"inline-flex items-center gap-1.5 rounded-full bg-rose-100 px-2 py-0.5 font-medium text-rose-800 text-xs dark:bg-rose-900/40 dark:text-rose-300\">\n 読み込みに失敗しました\n <button type=\"button\" data-testid=\"daily-report-ids-stream-retry\" onClick={() => retryDailyReportIdsStream()} className=\"rounded bg-rose-200 px-1.5 py-0.5 font-semibold text-rose-900 hover:bg-rose-300 dark:bg-rose-800 dark:text-rose-100 dark:hover:bg-rose-700\">\n 再試行\n </button>\n </span>\n )\n }\n\n if (stream.isRevalidating) {\n return (\n <span data-testid=\"daily-report-ids-stream-status\" data-phase=\"revalidating\" className=\"inline-flex items-center gap-1 text-slate-400 text-xs dark:text-slate-500\">\n <RefreshCw aria-hidden=\"true\" className=\"h-3 w-3 animate-spin\" />\n 最新化中\n </span>\n )\n }\n\n return null\n}\n","/**\n * Ease-out animated number for streaming counters.\n * ストリーミングカウンター向けのイーズアウト数値アニメーションフック。\n *\n * 経過時間の基準は「最初の rAF フレームのタイムスタンプ」にする。effect 内の\n * `performance.now()` を基準にすると、メインスレッド混雑時に保留中フレームの\n * vsync 時刻が基準より過去になり、負の進捗が 3 乗されて 0 に張り付く\n * (contact-map で実測された既知の不具合)。進捗は念のため 0 でもクランプする。\n */\nimport { useEffect, useRef, useState } from \"react\"\n\n/**\n * Animates numeric changes with ease-out cubic.\n * 数値変化をイーズアウト 3 次で補間するフック。\n *\n * @param value Target value. 目標値。\n * @param durationMs Animation duration. アニメーション時間 (既定 250ms)。\n * @returns Displayed (interpolated) value. 表示用の補間値。\n */\nexport const useAnimatedNumber = (value: number, durationMs = 250): number => {\n const [display, setDisplay] = useState(value)\n // 表示中の値の同期ミラー (アニメーション中断時の次回開始点に使う)\n const displayRef = useRef(value)\n const isFirstRef = useRef(true)\n\n /**\n * Animation effect.\n * アニメーション副作用。\n *\n * 依存配列:\n * - value: 目標値の変化でアニメーションを開始する\n * - durationMs: 時間変更で再スケジュール\n * クリーンアップ:\n * - 実行中の rAF をキャンセルする (中断時は displayRef が現在値を保持)\n */\n useEffect(() => {\n // 初回は補間せず即時表示する (マウント時のカウントアップ演出は不要)\n if (isFirstRef.current) {\n isFirstRef.current = false\n displayRef.current = value\n setDisplay(value)\n return\n }\n const from = displayRef.current\n if (from === value) return\n\n let rafId: number | null = null\n let startTimestamp: number | null = null\n const step = (timestamp: number) => {\n if (startTimestamp === null) {\n startTimestamp = timestamp\n }\n const progress = Math.min(1, Math.max(0, (timestamp - startTimestamp) / durationMs))\n const eased = 1 - (1 - progress) ** 3\n const next = Math.round(from + (value - from) * eased)\n displayRef.current = next\n setDisplay(next)\n if (progress < 1) {\n rafId = requestAnimationFrame(step)\n }\n }\n rafId = requestAnimationFrame(step)\n return () => {\n if (rafId !== null) cancelAnimationFrame(rafId)\n }\n }, [value, durationMs])\n\n return display\n}\n","/**\n * Virtual-scrolling summary list for daily reports driven by fetcher-based detail hooks.\n * useFetcher ベースの詳細取得フックで日報サマリーを仮想スクロール表示。\n */\nimport { SwipeCloseGuardContext, useSwipeGuardState, useSwipeToDismissOverlay, useTapClick, useTransitionDisplay } from \"@aiquants/swipe-overlay\"\nimport { type ScrollBarThumbOverlayRenderProps, VirtualScroll, type VirtualScrollHandle } from \"@aiquants/virtualscroll\"\nimport { Check, Star, Trash2 } from \"lucide-react\"\nimport { type CSSProperties, memo, type RefObject, useCallback, useEffect, useMemo, useRef, useState } from \"react\"\nimport { fallbackText, formatToIsoDate } from \"../../shared/text-utils\"\nimport type { DailyReportDetail, DailyReportItem } from \"../../shared/types\"\nimport { resolveSourceTypeConfig, useDailyReportConfig } from \"../config-context\"\nimport { useDailyReportActionContext } from \"../contexts/daily-report-action-context\"\nimport { useDailyReportDetail } from \"../hooks/use-daily-report\"\nimport { useDailyReportComments } from \"../hooks/use-daily-report-comments\"\nimport { useDynamicViewportHeight } from \"../hooks/use-dynamic-viewport-height\"\nimport { useIsMobileLayout } from \"../hooks/use-responsive-layout\"\nimport { cn } from \"../ui/cn\"\nimport { Tabs, TabsContent, TabsList, TabsTrigger } from \"../ui/tabs\"\nimport {\n CONTENT_DRAG_POINTER_INPUTS,\n DEFAULT_ITEM_HEIGHT,\n EMPTY_DETAIL,\n EMPTY_ITEM,\n FAST_SCROLL_INERTIA_OPTIONS,\n FAST_SCROLL_WHEEL_MULTIPLIER,\n ITEM_CONTAINER_HEIGHT,\n ITEM_VERTICAL_PADDING,\n MAX_PREVIEW_LINES,\n SCROLL_BAR_WIDTH,\n TAP_SCROLL_CIRCLE_OPTIONS,\n VIRTUAL_SCROLL_OVERSCAN_COUNT,\n WHEEL_RESET_TIMEOUT_MS,\n} from \"../utils/constants\"\nimport { BusinessDayThumbOverlay } from \"./business-day-thumb-overlay\"\nimport { DailyReportAttachmentIndicator } from \"./daily-report-attachment-indicator\"\nimport { DailyReportAttachmentList } from \"./daily-report-attachment-list\"\nimport { DailyReportCommentForm, DailyReportCommentList } from \"./daily-report-comment-section\"\nimport { DailyReportEditForm } from \"./daily-report-edit-form\"\nimport { UnreadIndicator } from \"./unread-indicator\"\n\ntype TestableVirtualScrollHandle = VirtualScrollHandle & {\n findReportIndex: (id: number) => number\n getReportIds: (limit?: number) => number[]\n}\n\ntype DailyReportListProps = {\n dailyReportItems: DailyReportItem[]\n autoMarkRead?: boolean\n selectedReportHubId: number | null\n onSelectItem: (reportHubId: number | null) => void\n userId?: string | null\n /**\n * タブ切替 (アンマウント) をまたいでスクロール位置を持ち回る共有スロット。\n * 値は VirtualScroll の**論理座標** (inset 非依存。onScroll が配信する座標系)。\n * マウント時に一度だけ読み、スクロールのたびに書き戻す。\n * ❗ 値をレンダー時に読んで props へ焼き込む方式は禁止: 非制御タブでは親が\n * 再レンダーされず、復帰マウントに古い値が渡る (実測で再現した実害バグ)\n */\n scrollOffsetRef?: RefObject<number>\n}\n\n/**\n * Manages mobile overlay state, history navigation, and scroll locking.\n * モバイルオーバーレイの状態、履歴ナビゲーション、スクロールロックを管理するフック。\n */\nconst useMobileOverlayManager = (isMobileLayout: boolean, selectedReportHubId: number | null, dailyReportItems: DailyReportItem[], onSelectItem: (reportHubId: number | null) => void) => {\n const [isMobileDetailOpen, setIsMobileDetailOpen] = useState(false)\n const [isOverlayMounted, setIsOverlayMounted] = useState(false)\n const overlayHistoryKeyRef = useRef<string | null>(null)\n const lastClosedItemIdRef = useRef<number | null>(null)\n const bodyOverflowRef = useRef<string | null>(null)\n\n const selectedItem = useMemo(() => (selectedReportHubId === null ? null : (dailyReportItems.find((e) => e.reportHubId === selectedReportHubId) ?? null)), [dailyReportItems, selectedReportHubId])\n const shouldShowOverlay = isMobileLayout && isMobileDetailOpen && selectedItem !== null\n\n /**\n * Manages the mounting state of the mobile overlay for animation purposes.\n * アニメーション用にモバイルオーバーレイのマウント状態を管理する副作用。\n *\n * Purpose: To delay unmounting until the close animation completes.\n * Dependencies: [shouldShowOverlay]\n * Cleanup: Clears the timeout.\n */\n useEffect(() => {\n if (shouldShowOverlay) {\n setIsOverlayMounted(true)\n } else {\n const timer = setTimeout(() => setIsOverlayMounted(false), 300)\n return () => clearTimeout(timer)\n }\n }, [shouldShowOverlay])\n\n /**\n * Manages mobile history state and body scroll locking.\n * モバイル履歴状態と Body スクロールロックを管理する副作用。\n *\n * Purpose: To support browser back button for closing overlay and prevent background scrolling.\n * Dependencies: [isMobileLayout, isMobileDetailOpen, selectedReportHubId]\n * Cleanup: Restores body overflow style.\n */\n useEffect(() => {\n if (typeof window === \"undefined\") return\n\n // History Management\n if (isMobileLayout && isMobileDetailOpen && selectedReportHubId !== null && !overlayHistoryKeyRef.current) {\n const key = `overlay-${Date.now()}`\n window.history.pushState({ ...window.history.state, dailyReportOverlay: key }, \"\", window.location.href)\n overlayHistoryKeyRef.current = key\n } else if (!(isMobileLayout && isMobileDetailOpen) && overlayHistoryKeyRef.current) {\n overlayHistoryKeyRef.current = null\n }\n\n // Scroll Lock\n if (isMobileDetailOpen) {\n bodyOverflowRef.current = document.body.style.overflow\n document.body.style.overflow = \"hidden\"\n } else if (bodyOverflowRef.current !== null) {\n document.body.style.overflow = bodyOverflowRef.current\n bodyOverflowRef.current = null\n }\n\n return () => {\n if (bodyOverflowRef.current !== null) document.body.style.overflow = bodyOverflowRef.current\n }\n }, [isMobileLayout, isMobileDetailOpen, selectedReportHubId])\n\n /**\n * Handles browser back navigation (popstate).\n * ブラウザの戻る操作 (popstate) を処理する副作用。\n *\n * Purpose: To close the mobile overlay when the user presses the back button.\n * Dependencies: [selectedReportHubId]\n * Cleanup: Removes popstate listener.\n */\n useEffect(() => {\n if (typeof window === \"undefined\") return\n const onPopState = () => {\n if (overlayHistoryKeyRef.current) {\n lastClosedItemIdRef.current = selectedReportHubId\n overlayHistoryKeyRef.current = null\n setIsMobileDetailOpen(false)\n onSelectItem(null)\n }\n }\n window.addEventListener(\"popstate\", onPopState)\n return () => window.removeEventListener(\"popstate\", onPopState)\n }, [selectedReportHubId, onSelectItem])\n\n const handleOverlayClose = useCallback(() => {\n if (!isMobileLayout) return\n lastClosedItemIdRef.current = selectedReportHubId\n setIsMobileDetailOpen(false)\n onSelectItem(null)\n if (overlayHistoryKeyRef.current && typeof window !== \"undefined\") {\n overlayHistoryKeyRef.current = null\n window.history.back()\n }\n }, [isMobileLayout, selectedReportHubId, onSelectItem])\n\n return {\n isMobileDetailOpen,\n setIsMobileDetailOpen,\n isOverlayMounted,\n shouldShowOverlay,\n handleOverlayClose,\n lastClosedItemIdRef,\n selectedItem,\n }\n}\n\n/**\n * Manages auto-read logic and read status toggling.\n * 自動既読ロジックと既読状態の切り替えを管理するフック。\n */\nconst useAutoRead = (report: DailyReportDetail | null, isLoading: boolean, autoMarkRead: boolean, isActive: boolean, toggleRead: (reportHubId: number, isRead: boolean, date: string) => Promise<void>) => {\n const ignoreAutoReadForReportHubId = useRef<number | null>(null)\n const lastReportHubIdRef = useRef<number | undefined>(undefined)\n\n if (lastReportHubIdRef.current !== report?.reportHubId) {\n lastReportHubIdRef.current = report?.reportHubId\n ignoreAutoReadForReportHubId.current = null\n }\n\n /**\n * Resets suppression flag when inactive.\n * 非アクティブ時に抑制フラグをリセットする副作用。\n *\n * Purpose: To re-enable auto-read when the report is reopened.\n * Dependencies: [isActive]\n * Cleanup: None.\n */\n useEffect(() => {\n if (!isActive) ignoreAutoReadForReportHubId.current = null\n }, [isActive])\n\n /**\n * Handles the read status toggle logic.\n * 既読状態の切り替え処理を行うコールバック。\n *\n * Purpose: To toggle read status and manage auto-read suppression.\n * Dependencies: [report, toggleRead]\n */\n const handleToggleRead = useCallback(\n async (isAuto: boolean = false) => {\n if (!report) return\n if (!isAuto) ignoreAutoReadForReportHubId.current = report.reportHubId\n\n const nextValue = !report.isRead\n try {\n await toggleRead(report.reportHubId, nextValue, report.date ?? \"\")\n } catch (_e) {\n // Error handled in context\n }\n },\n [report, toggleRead],\n )\n\n /**\n * Automatically marks the report as read after a delay.\n * 一定時間後にレポートを自動的に既読にする副作用。\n *\n * Purpose: To mark reports as read when viewed for a sufficient duration.\n * Dependencies: [report, isLoading, autoMarkRead, handleToggleRead]\n * Cleanup: Clears the timeout.\n */\n useEffect(() => {\n if (autoMarkRead && report && !report.isRead && !isLoading) {\n if (ignoreAutoReadForReportHubId.current === report.reportHubId) return\n const timer = setTimeout(() => handleToggleRead(true), 500)\n return () => clearTimeout(timer)\n }\n }, [report, isLoading, autoMarkRead, handleToggleRead])\n\n return { handleToggleRead }\n}\n\n/**\n * Daily report list cards with key metadata and preview content via virtual scrolling.\n * 主なメタデータと内容プレビューを仮想スクロール表示する日報リスト。\n */\nexport const DailyReportList = ({ dailyReportItems, autoMarkRead = false, selectedReportHubId, onSelectItem, userId, scrollOffsetRef }: DailyReportListProps) => {\n // 復元位置はマウント時に一度だけ共有スロットから読む (レンダー時読取の焼き込みを構造的に排除)\n const [initialScrollOffset] = useState(() => scrollOffsetRef?.current ?? 0)\n const scrollContainerRef = useRef<HTMLDivElement | null>(null)\n const virtualScrollRef = useRef<VirtualScrollHandle>(null)\n // list ロールの aria-label に使う (消費側が注入する業務ドメイン名。既定 \"Daily Reports\")\n const { title } = useDailyReportConfig()\n const { viewportHeight, updateViewportHeight } = useDynamicViewportHeight(scrollContainerRef)\n const [overlayLabel, setOverlayLabel] = useState<string>(\"\")\n const [isWheelScrollActive, setIsWheelScrollActive] = useState(false)\n const isMobileLayout = useIsMobileLayout()\n const wheelResetTimerRef = useRef<number | null>(null)\n const itemCount = dailyReportItems.length\n const scrollEndTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)\n\n const { isMobileDetailOpen, setIsMobileDetailOpen, isOverlayMounted, shouldShowOverlay, handleOverlayClose, lastClosedItemIdRef, selectedItem } = useMobileOverlayManager(isMobileLayout, selectedReportHubId, dailyReportItems, onSelectItem)\n\n const columnHeightStyle = useMemo<CSSProperties | undefined>(() => (Number.isFinite(viewportHeight) && viewportHeight > 0 ? { height: viewportHeight } : undefined), [viewportHeight])\n\n /**\n * Manages global event listeners (Wheel, Resize).\n * ホイール・リサイズのグローバルイベントリスナーを管理する副作用。\n *\n * Purpose: To keep the viewport height in sync and to detect wheel scrolling.\n * (レスポンシブ判定は use-responsive-layout のフックが担当する。)\n * Dependencies: [updateViewportHeight]\n * Cleanup: Removes all event listeners and clears timers.\n */\n useEffect(() => {\n if (typeof window === \"undefined\") return\n\n const onResize = () => requestAnimationFrame(updateViewportHeight)\n window.addEventListener(\"resize\", onResize)\n updateViewportHeight()\n\n const el = scrollContainerRef.current\n const onWheel = () => {\n setIsWheelScrollActive(true)\n if (wheelResetTimerRef.current) clearTimeout(wheelResetTimerRef.current)\n wheelResetTimerRef.current = window.setTimeout(() => {\n setIsWheelScrollActive(false)\n wheelResetTimerRef.current = null\n }, WHEEL_RESET_TIMEOUT_MS)\n }\n el?.addEventListener(\"wheel\", onWheel, { passive: true })\n\n return () => {\n window.removeEventListener(\"resize\", onResize)\n el?.removeEventListener(\"wheel\", onWheel)\n if (wheelResetTimerRef.current) clearTimeout(wheelResetTimerRef.current)\n }\n }, [updateViewportHeight])\n\n /**\n * Clears the selection when the list becomes empty.\n * 一覧が空になったとき選択を解除する副作用。\n *\n * 依存配列: [itemCount, selectedReportHubId, onSelectItem]。クリーンアップ: なし。\n */\n useEffect(() => {\n if (!itemCount && selectedReportHubId !== null) {\n onSelectItem(null)\n }\n }, [itemCount, selectedReportHubId, onSelectItem])\n\n /**\n * Restores focus after closing overlay (Desktop only).\n * オーバーレイを閉じた後のフォーカス復帰 (デスクトップのみ) を行う副作用。\n *\n * Purpose: To maintain keyboard focus context for accessibility.\n * Dependencies: [isMobileDetailOpen, isMobileLayout, dailyReportItems]\n * Cleanup: None.\n */\n useEffect(() => {\n if (isMobileLayout || isMobileDetailOpen || lastClosedItemIdRef.current === null) return\n const targetId = lastClosedItemIdRef.current\n lastClosedItemIdRef.current = null\n\n const index = dailyReportItems.findIndex((item) => item.reportHubId === targetId)\n if (index >= 0) {\n // スクロールで行を DOM に入れてから、カード本体 (tabIndex=0) へフォーカスを戻す。\n // focusItemAtIndex は enableKeyboardNavigation:false では no-op のため使えない\n // (行ラッパーではなくカードへ直接フォーカスするのが本来の意図でもある)\n virtualScrollRef.current?.scrollToIndex(index)\n requestAnimationFrame(() => {\n // detail-list の行も同じ data 属性を持つため、一覧側スコープで限定する\n const card = document.querySelector<HTMLElement>(`[data-testid=\"daily-report-list\"] [data-daily-report-button=\"${targetId}\"]`)\n card?.focus()\n })\n }\n }, [isMobileDetailOpen, isMobileLayout, dailyReportItems, lastClosedItemIdRef])\n\n /**\n * Syncs overlay label with the first item's date.\n * オーバーレイラベルを先頭アイテムの日付と同期する副作用。\n *\n * Purpose: To provide a default label for the scroll thumb overlay.\n * Dependencies: [dailyReportItems, itemCount]\n * Cleanup: None.\n */\n useEffect(() => {\n if (itemCount > 0)\n setOverlayLabel((prev) => {\n const next = formatToIsoDate(dailyReportItems[0]?.businessDate)\n return prev === next ? prev : next\n })\n }, [dailyReportItems, itemCount])\n\n const dailyReportItemsRef = useRef(dailyReportItems)\n dailyReportItemsRef.current = dailyReportItems\n\n useEffect(() => {\n if (virtualScrollRef.current) {\n const el = document.querySelector('[data-testid=\"daily-report-list\"]') as (Element & { __virtualScroll?: TestableVirtualScrollHandle }) | null\n if (el) {\n el.__virtualScroll = {\n ...virtualScrollRef.current,\n findReportIndex: (id: number) => dailyReportItemsRef.current.findIndex((item) => item.reportHubId === id),\n getReportIds: (limit = 20) => dailyReportItemsRef.current.slice(0, limit).map((item) => item.reportHubId),\n }\n }\n }\n })\n\n const handleItemSelect = useCallback(\n (id: number) => {\n if (id <= 0) return\n onSelectItem(id)\n if (isMobileLayout) setIsMobileDetailOpen(true)\n },\n [isMobileLayout, onSelectItem, setIsMobileDetailOpen],\n )\n\n const getItem = useCallback((index: number) => (index < itemCount ? dailyReportItems[index] : EMPTY_ITEM), [dailyReportItems, itemCount])\n const getItemHeight = useCallback(() => DEFAULT_ITEM_HEIGHT, [])\n\n // 行レンダラーを安定参照にする。インライン矢印だと毎レンダー新関数になり、\n // React.memo の ListReportItem が全行で再レンダーへ落ちる (選択変更で全行が再描画)\n const renderListRow = useCallback(\n (item: DailyReportItem, index: number) => <ListReportItem key={item.reportHubId} item={item} index={index} setSize={itemCount} isActive={item.reportHubId === selectedReportHubId} onSelect={handleItemSelect} />,\n [itemCount, selectedReportHubId, handleItemSelect],\n )\n\n const handleRangeChange = useCallback(\n (range: { visibleStartIndex: number }) => {\n const item = getItem(range.visibleStartIndex)\n if (item?.reportHubId > 0)\n setOverlayLabel((prev) => {\n const next = formatToIsoDate(item.businessDate)\n return prev === next ? prev : next\n })\n },\n [getItem],\n )\n\n const handleScroll = useCallback(\n (position: number) => {\n // onScroll が配信する論理座標をそのまま共有スロットへ書く (保存と復元で座標系を一致させる)。\n // 補足 (virtualscroll 2.0.0): ハンドル getScrollPosition も論理座標に統一されたため\n // 読み戻しても座標は一致するが、配信引数が正 (追加のハンドル呼び出しは冗長)。\n // 歴史的経緯: 2.0.0 以前はペイン座標を返す非対称があり、読み戻しが inset 分ずれる\n // 実害バグの原因だった\n if (scrollOffsetRef) {\n scrollOffsetRef.current = position\n }\n\n const container = scrollContainerRef.current\n if (!container) return\n\n if (container.dataset.scrolling !== \"true\") {\n container.dataset.scrolling = \"true\"\n }\n\n if (scrollEndTimerRef.current) {\n clearTimeout(scrollEndTimerRef.current)\n }\n\n scrollEndTimerRef.current = setTimeout(() => {\n if (container) {\n delete container.dataset.scrolling\n }\n scrollEndTimerRef.current = null\n }, 150)\n },\n [scrollOffsetRef],\n )\n\n const renderThumbOverlay = useCallback((props: ScrollBarThumbOverlayRenderProps) => <BusinessDayThumbOverlay {...props} label={overlayLabel} isWheelScrollActive={isWheelScrollActive} />, [overlayLabel, isWheelScrollActive])\n\n return (\n <>\n <div className=\"flex h-full min-h-0 gap-4\">\n <div ref={scrollContainerRef} className=\"flex min-h-0 w-full sm:w-90\" style={columnHeightStyle}>\n <VirtualScroll\n ref={virtualScrollRef}\n itemCount={itemCount}\n getItem={getItem}\n getItemHeight={getItemHeight}\n viewportSize={viewportHeight}\n initialScrollOffset={initialScrollOffset}\n overscanCount={VIRTUAL_SCROLL_OVERSCAN_COUNT}\n scrollBarOptions={{\n width: SCROLL_BAR_WIDTH,\n tapScrollCircleOptions: TAP_SCROLL_CIRCLE_OPTIONS,\n renderThumbOverlay: renderThumbOverlay,\n }}\n behaviorOptions={{\n // ドラッグスクロールは無効化せず、**ポインタ種別で絞る**。\n // 丸ごと切るとタッチ端末からスクロール手段が消える (定数の注記参照)\n pointerDragInputs: CONTENT_DRAG_POINTER_INPUTS,\n inertiaOptions: FAST_SCROLL_INERTIA_OPTIONS,\n wheelSpeedMultiplier: FAST_SCROLL_WHEEL_MULTIPLIER,\n // 行ラッパーへの tabIndex 付与 (タブ順汚染) と pointerdown フォーカス移動を止める。\n // 「★/既読タップで詳細ペインが切り替わる」不具合は 2 経路あり、これはその 1 つ目\n // (focus 経由)。2 つ目 (useTapClick の pointerup バブリング) はボタン側の\n // stopPointerPropagation が塞ぐ。onItemFocus は発火しなくなる\n enableKeyboardNavigation: false,\n }}\n onRangeChange={handleRangeChange}\n onScroll={handleScroll}\n contentInsets={{ top: ITEM_VERTICAL_PADDING, bottom: ITEM_VERTICAL_PADDING }}\n testId=\"daily-report-list\"\n className=\"relative w-full\"\n contentProps={{\n // 行 (role=\"option\" ではなく role=\"listitem\") だけを所有する list を、\n // スクロールバーと兄弟のコンテンツ要素に置く (virtualscroll 1.23.0 の口)。\n // 各行が aria-posinset/aria-setsize を持つので SR は「全 N 件中 M 番目」を読める\n role: \"list\",\n \"aria-label\": title,\n }}>\n {renderListRow}\n </VirtualScroll>\n </div>\n <DailyReportSidePane dailyReportItems={dailyReportItems} selectedItem={selectedItem} heightStyle={columnHeightStyle} autoMarkRead={!isMobileLayout && autoMarkRead} isActive={selectedReportHubId !== null} userId={userId} />\n </div>\n {itemCount === 0 && (\n <div className=\"py-8 text-center\">\n <p className=\"text-slate-500\">データがありません</p>\n </div>\n )}\n {isOverlayMounted && <DailyReportMobileOverlay isOpen={shouldShowOverlay} selectedItem={selectedItem} onClose={handleOverlayClose} autoMarkRead={isMobileLayout && autoMarkRead} userId={userId} />}\n </>\n )\n}\n\n/**\n * Loading skeleton representation for daily report list items.\n * 日報リストアイテム用読み込み中スケルトン表示。\n *\n * `wrapperProps` は仮想リストの位置属性 (role=\"listitem\" / aria-posinset / aria-setsize) を\n * 外側ラッパーへ透過するための口。読み込み中の行も一覧内の位置を SR へ公開する。\n */\nconst ListSkeletonItem = (wrapperProps: React.HTMLAttributes<HTMLDivElement> & { \"aria-posinset\"?: number; \"aria-setsize\"?: number }) => (\n <div {...wrapperProps} className=\"px-2 pb-3\" style={{ height: ITEM_CONTAINER_HEIGHT }}>\n <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\">\n <div className=\"mb-3 flex flex-wrap items-center gap-3\">\n <div className=\"h-4 w-24 rounded bg-slate-200\"></div>\n <div className=\"h-4 w-24 rounded bg-slate-200\"></div>\n <div className=\"h-4 w-24 rounded bg-slate-200\"></div>\n </div>\n <div className=\"space-y-2\">\n <div className=\"h-4 w-full rounded bg-slate-200\"></div>\n <div className=\"h-4 w-11/12 rounded bg-slate-200\"></div>\n <div className=\"h-4 w-5/6 rounded bg-slate-200\"></div>\n </div>\n </div>\n </div>\n)\n\ntype DailyReportSidePaneTab = \"article\" | \"relations\"\n\n/**\n * Side pane that exposes quick tabs for the selected report summary, editing, and correlation views.\n * 選択した日報の概要表示・編集・相関ビューを切り替え表示するサイドペイン。\n */\nconst DailyReportSidePane = ({\n dailyReportItems,\n selectedItem,\n heightStyle,\n autoMarkRead = false,\n isActive = true,\n userId,\n}: {\n dailyReportItems?: DailyReportItem[] // Optional for mobile overlay usage\n selectedItem: DailyReportItem | null\n heightStyle?: CSSProperties\n autoMarkRead?: boolean\n isActive?: boolean\n userId?: string | null\n}) => {\n const [activeTab, setActiveTab] = useState<DailyReportSidePaneTab>(\"article\")\n const [isEditing, setIsEditing] = useState(false)\n const { draftLabelName, fieldLabels, sourceTypeConfigs } = useDailyReportConfig()\n const { report, error, isLoading } = useDailyReportDetail(selectedItem?.reportHubId ?? -1, selectedItem?.businessDate ?? null)\n\n const scrollContainerRef = useRef<HTMLDivElement>(null)\n const lastReportIdRef = useRef<number | null>(null)\n // 自動編集モード (ドラフト検出による自動遷移) かどうかを追跡\n const autoEditRef = useRef(false)\n\n // レポート選択時のエディタモード自動制御\n // - 新しいレポート選択: ドラフトならエディタモード、公開済みならビューモード\n // - 同一レポートの SSE 更新: ドラフト→公開遷移時に自動編集のみ解除\n useEffect(() => {\n if (!report) return\n\n const isDraft = report.labels.some((l) => l.name === draftLabelName)\n\n if (report.reportHubId !== lastReportIdRef.current) {\n // 選択レポートが変更された\n lastReportIdRef.current = report.reportHubId\n if (isDraft) {\n setIsEditing(true)\n autoEditRef.current = true\n } else {\n setIsEditing(false)\n autoEditRef.current = false\n }\n } else if (!isDraft && isEditing && autoEditRef.current) {\n // SSE で同一レポートが下書き→公開に遷移: 自動編集モードのみ解除\n // ユーザーがダブルクリックで手動編集中の場合は解除しない\n setIsEditing(false)\n autoEditRef.current = false\n }\n }, [report, isEditing, draftLabelName])\n\n const isMine = !!(userId && report?.userId === userId)\n\n const scrollToBottom = useCallback(() => {\n setTimeout(() => {\n if (scrollContainerRef.current) {\n scrollContainerRef.current.scrollTo({ top: scrollContainerRef.current.scrollHeight, behavior: \"smooth\" })\n }\n }, 100)\n }, [])\n\n if (dailyReportItems && (dailyReportItems.length === 0 || !selectedItem)) {\n return (\n <aside className=\"hidden min-h-0 min-w-0 flex-1 gap-3 rounded-2xl border border-slate-200 bg-white p-4 shadow-sm sm:block dark:border-slate-700 dark:bg-slate-900/70\" style={heightStyle}>\n <div className=\"flex flex-1 items-center justify-center rounded-xl border border-slate-300 border-dashed bg-slate-50 p-4 text-center text-slate-500 text-sm dark:border-slate-600 dark:bg-slate-900/40 dark:text-slate-300\">\n <p>左側の一覧から日報を選択してください。</p>\n </div>\n </aside>\n )\n }\n\n const headerDate = formatToIsoDate(report?.date ?? selectedItem?.businessDate)\n const headerCreator = report?.employeeName || report?.author ? fallbackText(report.employeeName ?? report.author) : \"\"\n const showHeaderSkeleton = isLoading || !report\n\n const content = (\n <>\n <div className=\"space-y-1\">\n <div className=\"font-semibold text-slate-500 text-xs uppercase tracking-wide dark:text-slate-300\">{fieldLabels.businessDate}</div>\n <div className=\"text-slate-900 text-sm dark:text-slate-100\">{headerDate}</div>\n <div className=\"flex min-h-4 items-center gap-1 text-slate-500 text-xs dark:text-slate-300\">\n <span>{fieldLabels.author}:</span>\n {showHeaderSkeleton ? <div data-skeleton className=\"h-3 w-24 animate-pulse rounded bg-slate-200 dark:bg-slate-700\" /> : <span>{headerCreator}</span>}\n {(() => {\n const sourceType = report?.sourceType\n const sourceConfig = sourceType ? sourceTypeConfigs?.[sourceType] : undefined\n 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\n })()}\n </div>\n </div>\n <Tabs value={activeTab} onValueChange={(v) => setActiveTab(v as DailyReportSidePaneTab)} className=\"flex min-h-0 flex-1 flex-col\">\n <TabsList className=\"grid grid-cols-2 gap-1 rounded-md bg-slate-100 p-1 text-xs dark:bg-slate-800\">\n {[\"article\", \"relations\"].map((tab) => (\n <TabsTrigger\n key={tab}\n value={tab}\n className=\"rounded-md px-2 py-1 font-medium text-slate-600 transition-colors data-[state=active]:bg-white data-[state=active]:text-slate-900 data-[state=active]:shadow-sm dark:text-slate-200 dark:data-[state=active]:bg-slate-900/70 dark:data-[state=active]:text-slate-100\">\n {tab === \"article\" ? fieldLabels.tabArticle : fieldLabels.tabRelations}\n </TabsTrigger>\n ))}\n </TabsList>\n <TabsContent ref={scrollContainerRef} value=\"article\" className=\"scrollbar-thin flex-1 touch-pan-y overflow-y-auto overflow-x-hidden pt-3 text-sm\">\n {isEditing ? (\n report ? (\n <DailyReportEditForm report={report} onCancel={() => setIsEditing(false)} onPublishSuccess={() => setIsEditing(false)} />\n ) : (\n <p className=\"rounded-lg border border-slate-300 border-dashed bg-slate-50 p-4 text-slate-600 dark:border-slate-600 dark:bg-slate-900/40 dark:text-slate-300\">読み込み中...</p>\n )\n ) : (\n selectedItem && (\n <DailyReportDisplayPane\n report={report}\n isLoading={isLoading}\n error={error}\n selectedItem={selectedItem}\n autoMarkRead={autoMarkRead}\n isActive={isActive}\n onCommentAdded={scrollToBottom}\n userId={userId}\n onDoubleClick={isMine ? () => setIsEditing(true) : undefined}\n />\n )\n )}\n </TabsContent>\n <TabsContent value=\"relations\" className=\"scrollbar-thin flex-1 touch-pan-y overflow-y-auto pt-3 text-sm\">\n <p className=\"rounded-lg border border-slate-300 border-dashed bg-slate-50 p-4 text-slate-600 dark:border-slate-600 dark:bg-slate-900/40 dark:text-slate-300\">{fieldLabels.relationsEmpty}</p>\n </TabsContent>\n </Tabs>\n </>\n )\n\n // If rendered as a standalone pane (desktop)\n if (dailyReportItems) {\n return (\n <aside\n data-testid=\"daily-report-side-pane\"\n data-report-id={selectedItem?.reportHubId}\n 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\"\n style={heightStyle}>\n {content}\n </aside>\n )\n }\n // If rendered inside mobile overlay\n return (\n <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\">\n {content}\n </div>\n )\n}\n\n/**\n * Skeleton line placeholder (module-scope so it is not re-created every render).\n * スケルトンの 1 行プレースホルダー (レンダーごとに再生成しないようモジュールスコープに置く)。\n *\n * 以前は DailyReportDisplayPane の render 本体で宣言していたため、レンダーのたびに\n * 別コンポーネント型となり全スケルトンが再マウントしていた。\n */\nconst SkeletonText = ({ className = \"w-32\" }: { className?: string }) => <div data-skeleton className={`h-4 animate-pulse rounded bg-slate-200 dark:bg-slate-700 ${className}`} />\n\n/**\n * Shows the selected report details in the display tab, including content and metadata.\n * 表示タブ内で選択した日報の詳細とメタデータを表示するコンポーネント。\n */\nconst DailyReportDisplayPane = ({\n report,\n isLoading,\n error,\n selectedItem,\n autoMarkRead = false,\n isActive = true,\n onCommentAdded,\n userId,\n onDoubleClick,\n}: {\n report: DailyReportDetail | null\n isLoading: boolean\n error: Error | null\n selectedItem: DailyReportItem\n autoMarkRead?: boolean\n isActive?: boolean\n onCommentAdded?: () => void\n userId?: string | null\n onDoubleClick?: () => void\n}) => {\n // 表示中ユーザーはアクションコンテキストから取得する (root loader 非依存)\n const { toggleStar, toggleRead, deleteReport, user } = useDailyReportActionContext()\n const { fieldLabels, sourceTypeConfigs } = useDailyReportConfig()\n\n // Use custom hook for comments\n const { uiComments, handleAddComment, handleDeleteComment, pendingDeleteId, setPendingDeleteId, resolvedIdMap } = useDailyReportComments(report, user, userId)\n const { handleToggleRead } = useAutoRead(report, isLoading, autoMarkRead, isActive, toggleRead)\n\n const sourceType = selectedItem.sourceType ?? report?.sourceType\n const sourceConfig = resolveSourceTypeConfig(sourceType, sourceTypeConfigs)\n const isExternalSource = sourceType ? sourceType.toLowerCase() !== \"internal\" : false\n const canComment = sourceConfig?.allowComment ?? !isExternalSource\n\n // Auto-scroll to bottom when comments are added (e.g. via SSE)\n const prevReportHubIdRef = useRef<number | null>(null)\n const prevCommentsLengthRef = useRef(uiComments.length)\n\n useEffect(() => {\n const currentReportHubId = report?.reportHubId ?? null\n\n // Reset tracking when switching reports\n if (currentReportHubId !== prevReportHubIdRef.current) {\n prevReportHubIdRef.current = currentReportHubId\n prevCommentsLengthRef.current = uiComments.length\n return\n }\n\n // Scroll if comments increased\n if (uiComments.length > prevCommentsLengthRef.current) {\n onCommentAdded?.()\n }\n prevCommentsLengthRef.current = uiComments.length\n }, [uiComments.length, report?.reportHubId, onCommentAdded])\n\n const handleAddCommentWrapper = async (content: string) => {\n await handleAddComment(content)\n }\n\n const displayContent = useMemo(() => {\n if (!report?.content) return null\n return report.content\n .replace(/<br\\s*\\/?\\s*>/gi, \"\\n\")\n .replace(/<\\/p>/gi, \"\\n\")\n .replace(/<[^>]+>/g, \"\")\n }, [report])\n\n const handleToggleStar = async () => {\n if (!report) return\n const nextValue = !report.isStarred\n try {\n await toggleStar(report.reportHubId, nextValue, report.date ?? \"\")\n } catch (_e) {\n // Error handled in context\n }\n }\n\n const handleDelete = async () => {\n if (!report) return\n if (!confirm(\"本当に削除しますか?\")) return\n try {\n await deleteReport(report.reportHubId, report.date ?? \"\")\n } catch (_e) {\n alert(\"削除に失敗しました\")\n }\n }\n\n if (error) return <p className=\"text-red-500\">{error.message}</p>\n\n const showSkeleton = !report\n const isStarred = report?.isStarred ?? false\n const isRead = report?.isRead ?? false\n const isMine = !!(userId && report?.userId === userId)\n const hasComments = uiComments.length > 0\n\n return (\n // biome-ignore lint/a11y/noStaticElementInteractions: Double click to edit feature\n <div className=\"relative space-y-4 text-slate-700 dark:text-slate-200\" onDoubleClick={onDoubleClick}>\n {!showSkeleton && report && (\n <div className=\"absolute top-0 right-2 z-10 flex items-center justify-end gap-3\">\n <button type=\"button\" onClick={() => handleToggleRead(false)} className=\"text-lg leading-none transition-transform focus:outline-none active:scale-110\" title={isRead ? \"未読にする\" : \"既読にする\"} data-detail-read-button={report.reportHubId}>\n {isRead ? <span className=\"text-slate-300 hover:text-blue-500\">✓</span> : <UnreadIndicator className=\"hover:text-blue-600\" />}\n </button>\n <button type=\"button\" onClick={handleToggleStar} className=\"text-lg leading-none transition-transform focus:outline-none active:scale-110\" title={isStarred ? \"スターを外す\" : \"スターを付ける\"} data-detail-star-button={report.reportHubId}>\n {isStarred ? <span className=\"text-yellow-400\">★</span> : <span className=\"text-slate-300 hover:text-yellow-400\">☆</span>}\n </button>\n {isMine && (\n <button type=\"button\" onClick={handleDelete} data-detail-delete-button={report.reportHubId} className=\"text-lg leading-none transition-transform focus:outline-none active:scale-110\" title=\"削除する\">\n <Trash2 className=\"h-4 w-4 text-slate-300 hover:text-red-500\" />\n </button>\n )}\n </div>\n )}\n\n <div className=\"flex items-baseline\">\n <span className=\"w-24 shrink-0 font-semibold text-slate-500 text-xs uppercase tracking-wide dark:text-slate-300\">{fieldLabels.subject}</span>\n <div className=\"flex-1 pr-20\">{showSkeleton ? <SkeletonText className=\"w-3/4\" /> : <span className=\"fade-in animate-in text-sm duration-500\">{fallbackText(report?.subject)}</span>}</div>\n </div>\n <div className=\"flex items-baseline\">\n <span className=\"w-24 shrink-0 font-semibold text-slate-500 text-xs uppercase tracking-wide dark:text-slate-300\">{fieldLabels.customer}</span>\n <div className=\"flex-1\">{showSkeleton ? <SkeletonText className=\"w-1/2\" /> : <span className=\"fade-in animate-in text-sm duration-500\">{fallbackText(report?.customerName)}</span>}</div>\n </div>\n <div className=\"space-y-2\">\n <div className=\"font-semibold text-slate-500 text-xs uppercase tracking-wide dark:text-slate-300\">{fieldLabels.content}</div>\n <div className=\"min-h-22 rounded-lg bg-slate-50 p-3 text-sm leading-relaxed dark:bg-slate-800/40\">\n {showSkeleton ? (\n <div className=\"space-y-2\">\n <SkeletonText className=\"w-full\" />\n <SkeletonText className=\"w-11/12\" />\n <SkeletonText className=\"w-4/5\" />\n </div>\n ) : displayContent ? (\n <div className=\"fade-in animate-in whitespace-pre-wrap duration-500\">{displayContent}</div>\n ) : (\n <p className=\"fade-in animate-in text-slate-500 duration-500 dark:text-slate-400\">内容は登録されていません。</p>\n )}\n </div>\n </div>\n <div className=\"flex items-baseline\">\n <span className=\"w-24 shrink-0 font-semibold text-slate-500 text-xs uppercase tracking-wide dark:text-slate-300\">{fieldLabels.visitTime}</span>\n <div className=\"flex-1\">\n {showSkeleton ? (\n <SkeletonText className=\"w-1/3\" />\n ) : (\n <span className=\"fade-in animate-in text-sm duration-500\">\n {fallbackText(report?.visitTimeFrom)} {report?.visitTimeFrom && report?.visitTimeTo ? \"-\" : \"\"} {fallbackText(report?.visitTimeTo)}\n </span>\n )}\n </div>\n </div>\n {/* 添付節。DetailList タブと同じ位置 (面談者・本文の後、コメントの前) に置く。\n 片方のタブにしか出さないと、利用者は「添付が無い日報」と誤認する */}\n {report && <DailyReportAttachmentList attachments={report.attachments} />}\n {report && (\n <div className=\"fade-in animate-in space-y-2 border-slate-100 border-t pt-4 duration-500 dark:border-slate-800\">\n <div className=\"font-semibold text-slate-500 text-xs uppercase tracking-wide dark:text-slate-300\">{fieldLabels.comments}</div>\n {hasComments && (\n <DailyReportCommentList\n comments={uiComments}\n reportHubId={report.reportHubId}\n businessDate={report.date ?? \"\"}\n onDeleteComment={handleDeleteComment}\n pendingDeleteId={pendingDeleteId}\n onPendingDeleteIdChange={setPendingDeleteId}\n resolvedIdMap={resolvedIdMap}\n />\n )}\n {canComment && <DailyReportCommentForm reportHubId={report.reportHubId} businessDate={report.date ?? \"\"} onAddComment={handleAddCommentWrapper} />}\n </div>\n )}\n </div>\n )\n}\n\n/**\n * Mobile-only overlay that slides in with the selected daily report details.\n * 選択された日報の詳細をモバイル向けにスライド表示し選択ガードを切り替え可能なオーバーレイ。\n */\nconst DailyReportMobileOverlay = ({ isOpen, selectedItem, onClose, autoMarkRead = false, userId }: { isOpen: boolean; selectedItem: DailyReportItem | null; onClose: () => void; autoMarkRead?: boolean; userId?: string | null }) => {\n const containerRef = useRef<HTMLDivElement>(null)\n const closeButtonRef = useRef<HTMLButtonElement | null>(null)\n\n const { isBlocked: isGuardBlocked, requestBlock, reset: resetGuard } = useSwipeGuardState()\n\n const { report, error, isLoading } = useDailyReportDetail(selectedItem?.reportHubId ?? -1, selectedItem?.businessDate ?? null)\n const displayData = useTransitionDisplay(isOpen, selectedItem ? { item: selectedItem, detail: { report, error, isLoading } } : null)\n const displayItem = displayData?.item ?? null\n const _displayDetail = displayData?.detail ?? EMPTY_DETAIL\n _displayDetail\n const { overlayRef, overlayStyle, attemptClose, isSwiping } = useSwipeToDismissOverlay({\n isOpen,\n onClose,\n isCloseBlocked: useCallback(() => isGuardBlocked, [isGuardBlocked]),\n skipWhenTextSelected: true,\n })\n\n const tapHandlers = useTapClick(() => attemptClose())\n\n /**\n * Manages focus and guard state when overlay opens/closes.\n * オーバーレイの開閉時にフォーカスとガード状態を管理する副作用。\n *\n * Purpose: To set focus on close button for accessibility and reset swipe guard.\n * Dependencies: [isOpen, resetGuard]\n * Cleanup: None.\n */\n useEffect(() => {\n if (isOpen) {\n requestAnimationFrame(() => closeButtonRef.current?.focus())\n } else {\n if (containerRef.current?.contains(document.activeElement)) {\n ;(document.activeElement as HTMLElement).blur()\n }\n resetGuard()\n }\n }, [isOpen, resetGuard])\n\n if (!displayItem) return null\n\n return (\n <div ref={containerRef} className={`fixed inset-0 z-50 flex touch-manipulation transition-[visibility] duration-200 sm:hidden ${isOpen ? \"visible\" : \"invisible\"} ${isOpen && !isSwiping ? \"pointer-events-auto\" : \"pointer-events-none\"}`} aria-hidden={!isOpen}>\n <button type=\"button\" aria-label=\"背景をタップして閉じる\" className={`absolute inset-0 touch-manipulation bg-slate-900/50 transition-opacity duration-200 focus:outline-none ${isOpen ? \"opacity-100\" : \"opacity-0\"}`} {...tapHandlers}></button>\n <aside\n role=\"dialog\"\n aria-modal=\"true\"\n aria-label=\"日報詳細\"\n className={`relative ml-auto flex h-full w-full max-w-full transform touch-none select-none bg-white shadow-xl transition-transform duration-200 ease-out dark:bg-slate-900/95 ${isOpen ? \"pointer-events-auto\" : \"pointer-events-none\"}`}\n ref={overlayRef}\n onKeyDown={(e) => {\n if (e.key === \"Escape\") {\n e.preventDefault()\n attemptClose()\n }\n }}\n style={overlayStyle}>\n <div className=\"flex h-full w-full flex-col gap-3 p-4\">\n <SwipeCloseGuardContext.Provider value={{ isBlocked: isGuardBlocked, requestBlock }}>\n <DailyReportSidePane selectedItem={displayItem} autoMarkRead={autoMarkRead} isActive={isOpen} userId={userId} />\n </SwipeCloseGuardContext.Provider>\n <div className=\"mt-2 flex justify-start\">\n <button\n type=\"button\"\n ref={closeButtonRef}\n data-overlay-interactive=\"true\"\n className=\"touch-manipulation rounded-full border border-slate-300 px-4 py-2 font-medium text-slate-600 text-sm shadow-sm transition-colors hover:bg-slate-100 dark:border-slate-600 dark:text-slate-200 dark:hover:bg-slate-800\"\n onClick={() => attemptClose()}>\n 閉じる\n </button>\n </div>\n </div>\n </aside>\n </div>\n )\n}\n\n/**\n * Up to three non-empty plain-text lines extracted from HTML content.\n * HTML コンテンツから抽出する最大三つの非空プレーンテキスト行。\n */\nconst extractPreviewLines = (content: string | null): string[] => {\n if (!content) return []\n return content\n .replace(/<br\\s*\\/?\\s*>/gi, \"\\n\")\n .replace(/<\\/p>/gi, \"\\n\")\n .replace(/<[^>]+>/g, \"\")\n .split(/\\r?\\n/)\n .map((l) => l.replace(/\\u3000/g, \" \").trim())\n .filter((l) => l.length > 0)\n .slice(0, MAX_PREVIEW_LINES)\n}\n\n/**\n * Daily report summary card rendered inside the list tab.\n * List タブ内で表示する日報サマリーカード。\n */\nconst ListReportItem = memo(({ item, index, setSize, isActive, onSelect }: { item: DailyReportItem; index: number; setSize: number; isActive: boolean; onSelect: (id: number) => void }) => {\n const { fieldLabels, sourceTypeConfigs } = useDailyReportConfig()\n const { toggleStar, toggleRead, removeStaleItem } = useDailyReportActionContext()\n const { report, error, isLoading, isRefetching } = useDailyReportDetail(item.reportHubId, item.businessDate)\n // list 内の絶対位置を SR へ公開する (仮想化で描画されるのは窓分だけなので必須)。\n // どの表示状態 (読み込み中・エラー・本体) でも同じ位置属性を載せる\n const listItemProps = { role: \"listitem\", \"aria-posinset\": index + 1, \"aria-setsize\": setSize } as const\n\n // フェッチが一度でも試行されたかを追跡する ref\n const hasEverFetched = useRef(false)\n if (isRefetching) hasEverFetched.current = true\n\n // フェッチ完了後に report が null → stale アイテムとしてリストから除去する安全弁\n // 外部リソース同期: useDailyReportDetail のフェッチ結果に反応する\n useEffect(() => {\n if (!(report || isLoading || isRefetching || error) && hasEverFetched.current && item.reportHubId > 0) {\n removeStaleItem(item.reportHubId)\n }\n }, [report, isLoading, isRefetching, error, item.reportHubId, removeStaleItem])\n\n const tapHandlers = useTapClick(() => onSelect(item.reportHubId))\n\n if (error) {\n return (\n <div {...listItemProps} className=\"px-2 pb-3\" style={{ height: ITEM_CONTAINER_HEIGHT }}>\n <div className=\"flex h-full items-center justify-center rounded-2xl border border-red-200 bg-red-50 p-4 text-red-700 text-sm dark:border-red-900/50 dark:bg-red-950/40 dark:text-red-300\">\n <p>日報 {item.reportHubId.toLocaleString()} の概要読み込みに失敗しました。</p>\n </div>\n </div>\n )\n }\n\n if (isLoading) return <ListSkeletonItem {...listItemProps} />\n\n if (!report) {\n return (\n <div {...listItemProps} className=\"px-2 pb-3\" style={{ height: ITEM_CONTAINER_HEIGHT }}>\n <div className=\"flex h-full items-center justify-center rounded-2xl border border-slate-200 bg-slate-50 p-4 text-slate-500 text-sm dark:border-slate-700 dark:bg-slate-900/40 dark:text-slate-400\">\n <p>日報が見つかりません</p>\n </div>\n </div>\n )\n }\n\n const previewLines = extractPreviewLines(report.content ?? null).map((line, i) => ({\n line,\n key: `${item.reportHubId}-line-${i}`,\n }))\n const creator = fallbackText(report.employeeName ?? report.author)\n const date = formatToIsoDate(report.date)\n\n const handleToggleStar = async (e: React.MouseEvent) => {\n e.stopPropagation()\n const nextValue = !report.isStarred\n try {\n await toggleStar(report.reportHubId, nextValue, report.date ?? \"\")\n } catch (_e) {\n // Error handled in context\n }\n }\n\n const handleToggleRead = async (e: React.MouseEvent) => {\n e.stopPropagation()\n const nextValue = !report.isRead\n try {\n await toggleRead(report.reportHubId, nextValue, report.date ?? \"\")\n } catch (_e) {\n // Error handled in context\n }\n }\n\n /**\n * Keeps card selection from firing when an inner action button is tapped.\n * カード内のアクションボタン操作でカード選択を発火させないための伝播停止。\n *\n * カードの選択は useTapClick (pointerdown/pointerup 駆動) なので、click の\n * stopPropagation だけでは止まらない。pointer イベント自体を止める必要がある。\n */\n const stopPointerPropagation = (e: React.PointerEvent) => {\n e.stopPropagation()\n }\n\n return (\n <div {...listItemProps} className=\"px-2 pb-3\" style={{ height: ITEM_CONTAINER_HEIGHT }}>\n {/* biome-ignore lint/a11y/useSemanticElements: Card contains nested interactive elements (Star/Read buttons) */}\n <div\n data-daily-report-button={item.reportHubId}\n 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 ${\n 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\"\n }`}\n style={{ touchAction: \"manipulation\" }}\n role=\"button\"\n tabIndex={0}\n // aria-current が「一覧の現在項目」の意味論を担う。aria-pressed は\n // 消費アプリの E2E ヘルパーが選択判定に使う既存契約のため併記を維持する\n aria-current={isActive ? \"true\" : undefined}\n aria-pressed={isActive}\n aria-label={`${date} ${creator}`}\n {...tapHandlers}>\n <div className=\"mb-3 flex items-center gap-3 text-slate-600 text-sm\">\n <span className=\"inline-flex shrink-0 items-center rounded-full bg-slate-100 px-3 py-1 font-semibold text-xs dark:bg-slate-800 dark:text-slate-200\">\n <span className=\"whitespace-nowrap font-medium text-slate-900 dark:text-slate-100\">{date}</span>\n </span>\n <span className=\"inline-flex min-w-0 max-w-full items-center rounded-full bg-slate-100 px-3 py-1 font-semibold text-xs dark:bg-slate-800 dark:text-slate-200\">\n <span className=\"truncate font-medium text-slate-900 dark:text-slate-100\">{creator}</span>\n </span>\n <div className=\"ml-auto flex items-center gap-1.5\">\n {/* 添付の有無を一覧で一目判別できるようにする。\n 仕様書が定める [sourceType] [Star] [Read] の相対順は崩さず、その手前に置く */}\n <DailyReportAttachmentIndicator count={report.attachments.length} label={fieldLabels.attachments} />\n {(() => {\n const sourceType = item.sourceType ?? report?.sourceType\n const sourceConfig = resolveSourceTypeConfig(sourceType, sourceTypeConfigs)\n 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\n })()}\n <button\n type=\"button\"\n title={report.isStarred ? \"スターを外す\" : \"スターを付ける\"}\n aria-label={report.isStarred ? \"スターを外す\" : \"スターを付ける\"}\n aria-pressed={report.isStarred}\n data-list-star-button={item.reportHubId}\n onClick={handleToggleStar}\n onPointerDown={stopPointerPropagation}\n onPointerUp={stopPointerPropagation}\n className=\"flex h-6 w-6 items-center justify-center rounded-full transition-colors hover:bg-slate-200 dark:hover:bg-slate-700\">\n {report.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\" />}\n </button>\n <button\n type=\"button\"\n title={report.isRead ? \"未読にする\" : \"既読にする\"}\n aria-label={report.isRead ? \"未読にする\" : \"既読にする\"}\n aria-pressed={report.isRead}\n data-list-read-button={item.reportHubId}\n onClick={handleToggleRead}\n onPointerDown={stopPointerPropagation}\n onPointerUp={stopPointerPropagation}\n className=\"flex h-6 w-6 items-center justify-center rounded-full transition-colors hover:bg-slate-200 dark:hover:bg-slate-700\">\n {report.isRead ? <Check className=\"h-3.5 w-3.5 text-blue-500\" /> : <UnreadIndicator className=\"text-xs\" />}\n </button>\n </div>\n </div>\n <div className=\"space-y-1 text-slate-700 text-sm leading-relaxed dark:text-slate-200\">\n {previewLines.length > 0 ? (\n previewLines.map(({ line, key }) => (\n <p key={key} className=\"wrap-break-word line-clamp-1\">\n {line}\n </p>\n ))\n ) : (\n <p>内容は登録されていません</p>\n )}\n </div>\n </div>\n </div>\n )\n})\nListReportItem.displayName = \"ListReportItem\"\n","/**\n * Responsive layout detection shared by the daily-report views.\n * 日報ビューが共有するレスポンシブ判定。\n *\n * 閾値を 1 箇所に集約する。各ビューが独自に `matchMedia` を持つと、片方だけ変更されて\n * レイアウト分岐が食い違う。\n *\n * ❗ **スクロール操作の可否をここで決めてはならない。** 画面幅で判定すると\n * 「主入力がマウスのタッチ対応ノート」のような端末を取りこぼす。ドラッグスクロールは\n * `ScrollPane` の `pointerDragInputs` で**イベントのポインタ種別ごとに**制御する\n * (`CONTENT_DRAG_POINTER_INPUTS` 参照)。\n */\nimport { useEffect, useState } from \"react\"\n\n/**\n * Viewport width at which the daily-report views switch to the mobile layout.\n * 日報ビューがモバイルレイアウトへ切り替わるビューポート幅。\n *\n * Tailwind の `sm` (640px) の直下に合わせてある。リスト + サイドペインの 2 カラムを\n * 畳んでオーバーレイ表示にする閾値であり、**この値は 1 箇所でだけ定義する**。\n */\nexport const MOBILE_LAYOUT_MEDIA_QUERY = \"(max-width: 639px)\"\n\n/**\n * Reports whether the viewport is narrow enough for the mobile layout.\n * ビューポートがモバイルレイアウトの幅かどうかを返す処理。\n *\n * SSR と初回描画では `false` を返し、マウント後に実環境で更新する\n * (サーバーとクライアントで初期値を揃えてハイドレーション不一致を避ける)。\n *\n * @returns True on the mobile layout. モバイルレイアウトなら true。\n */\nexport const useIsMobileLayout = (): boolean => {\n const [isMobileLayout, setIsMobileLayout] = useState(false)\n\n /**\n * Tracks the viewport width.\n * ビューポート幅を追跡する副作用。\n *\n * 目的: 画面回転・ウィンドウリサイズに追随する。\n * 依存関係: [] - 監視対象は固定の定数なので購読は 1 度だけ張る。\n * クリーンアップ: リスナーを解除する。\n */\n useEffect(() => {\n if (typeof window === \"undefined\" || typeof window.matchMedia !== \"function\") return\n\n const query = window.matchMedia(MOBILE_LAYOUT_MEDIA_QUERY)\n const sync = () => setIsMobileLayout(query.matches)\n\n sync()\n query.addEventListener(\"change\", sync)\n return () => query.removeEventListener(\"change\", sync)\n }, [])\n\n return isMobileLayout\n}\n","import * as TabsPrimitive from \"@radix-ui/react-tabs\"\nimport * as React from \"react\"\n\nimport { cn } from \"./cn\"\n\nconst Tabs = TabsPrimitive.Root\n\nconst TabsList = React.forwardRef<React.ElementRef<typeof TabsPrimitive.List>, React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>>(({ className, ...props }, ref) => (\n <TabsPrimitive.List ref={ref} className={cn(\"inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground\", className)} {...props} />\n))\nTabsList.displayName = TabsPrimitive.List.displayName\n\nconst TabsTrigger = React.forwardRef<React.ElementRef<typeof TabsPrimitive.Trigger>, React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>>(({ className, ...props }, ref) => (\n <TabsPrimitive.Trigger\n ref={ref}\n className={cn(\n \"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 font-medium text-sm ring-offset-background transition-all focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-2xs\",\n className,\n )}\n {...props}\n />\n))\nTabsTrigger.displayName = TabsPrimitive.Trigger.displayName\n\n// https://github.com/radix-ui/primitives/issues/2359\nconst TabsContent = React.forwardRef<React.ElementRef<typeof TabsPrimitive.Content>, React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>>(({ className, ...props }, ref) => (\n <TabsPrimitive.Content\n ref={ref}\n // forceMount={true} //<=======Add this line\n className={cn(\n \"data-[state=inactive]:hidden\", //<=========Add this line\n \"mt-2 ring-offset-background focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2\",\n className,\n )}\n {...props}\n />\n))\nTabsContent.displayName = TabsPrimitive.Content.displayName\n\nexport { Tabs, TabsContent, TabsList, TabsTrigger }\n","/**\n * Top-level daily-report page content (chrome-agnostic, stream-driven).\n * アプリのレイアウトに依存しない日報ページ本体コンポーネント (ストリーム駆動)。\n *\n * ID 一覧はモジュール常駐の NDJSON ストリームセッションから購読し、最初の\n * チャンク到着と同時に一覧を描画する (全件解決を待つ Await ゲートは廃止)。\n * 再訪時はセッションに残る受信済みデータで即座に復元し、鮮度はバックグラウンド\n * 再検証 (SWR) と SSE が担う。アプリ側のルートは本コンポーネントを自前の\n * レイアウトで包み、`config` でヘッダー描画・パス・ラベル名を注入する。\n */\nimport { useEffect } from \"react\"\nimport type { DailyReportUser } from \"../../shared/types\"\nimport { type DailyReportClientConfigInput, DailyReportConfigProvider, useDailyReportConfig } from \"../config-context\"\nimport { DailyReportActionProvider } from \"../contexts/daily-report-action-context\"\nimport { DailyReportErrorProvider } from \"../contexts/daily-report-error-context\"\nimport { ensureDailyReportIdsStreamSession, reloadDocument, retryDailyReportIdsStream, useDailyReportIdsStream } from \"../streaming/daily-report-ids-stream-session\"\nimport { DAILY_REPORT_FETCH_DEBOUNCE_MS } from \"../utils/constants\"\nimport { DailyReportIdsStreamStatus } from \"./daily-report-ids-stream-status\"\nimport { DailyReportResolvedContent } from \"./daily-report-resolved-content\"\n\n/**\n * Error display when the ids stream terminally failed before any data arrived.\n * データ未受信のまま ID ストリームが力尽きたときのエラー表示コンポーネント。\n *\n * 再試行はストリームセッションの retry へ直結する (受信済みカーソルから再開)。\n */\nexport const DailyReportLoadError = () => {\n const { title, renderHeader } = useDailyReportConfig()\n return (\n <>\n {renderHeader({ title, annotation: <span className=\"font-bold text-red-500\">Error</span> })}\n <div className=\"h-full bg-slate-50 p-2\">\n <div className=\"mx-auto flex h-full w-full max-w-6xl flex-1 flex-col gap-6\">\n <div className=\"flex h-full flex-1 flex-col items-center justify-center gap-4 rounded-lg border border-red-200 bg-white p-4 text-slate-500 shadow-sm dark:border-red-900 dark:bg-slate-900/70 dark:text-slate-400\">\n <p className=\"font-medium text-red-600\">日報データの読み込みに失敗しました。</p>\n <button\n type=\"button\"\n data-testid=\"daily-report-load-error-retry\"\n onClick={() => retryDailyReportIdsStream()}\n className=\"rounded bg-slate-100 px-4 py-2 text-slate-700 text-sm hover:bg-slate-200 dark:bg-slate-800 dark:text-slate-300 dark:hover:bg-slate-700\">\n 再読み込み\n </button>\n </div>\n </div>\n </div>\n </>\n )\n}\n\n/**\n * Loading screen shown only until the first stream chunk arrives.\n * 最初のストリームチャンクが届くまでの間だけ表示するローディング画面。\n */\nexport const DailyReportLoading = () => {\n const { title, renderHeader } = useDailyReportConfig()\n return (\n <>\n {renderHeader({\n title,\n annotation: (\n <div className=\"flex flex-wrap items-center gap-2 leading-none\">\n <DailyReportIdsStreamStatus />\n <span>総件数: 読み込み中...</span>\n <span>デバウンス: {DAILY_REPORT_FETCH_DEBOUNCE_MS} ms</span>\n </div>\n ),\n })}\n <div className=\"h-full bg-slate-50 p-2\">\n <div className=\"mx-auto flex h-full w-full max-w-6xl flex-1 flex-col gap-6\">\n <div className=\"flex h-full flex-1 items-center justify-center rounded-lg border border-slate-200 bg-white text-slate-500 shadow-sm dark:border-slate-700 dark:bg-slate-900/70 dark:text-slate-300\">日報 ID を読み込み中です...</div>\n </div>\n </div>\n </>\n )\n}\n\nexport type DailyReportPageProps = {\n /** 表示中ユーザー (楽観的コメントの作者名等に使用)。 */\n user?: DailyReportUser\n /** 難読化済みの内部ユーザー ID (未登録ユーザーは null)。 */\n userId?: string | null\n /** クライアント設定の部分上書き (ヘッダー描画・パス・ラベル名・フィールドラベルなど)。 */\n config?: DailyReportClientConfigInput\n}\n\n/**\n * Stream-driven page body: session bootstrap, phase gating, and provider wiring.\n * ストリーム駆動のページ本体。セッション確立・局面ゲーティング・プロバイダー結線を担う。\n */\nconst DailyReportPageInner = ({ user, userId }: Omit<DailyReportPageProps, \"config\">) => {\n const { apiBasePath } = useDailyReportConfig()\n const stream = useDailyReportIdsStream()\n\n /**\n * ストリームセッション確立の保険。\n * 通常は clientLoader が先に確立するが、clientLoader を経ないマウントや\n * ユーザー切替 (userKey 不一致でセッション再生成) をここで拾う。\n * 依存配列: apiBasePath / userId (いずれかの変化で再確認)。\n * クリーンアップ: なし (セッションはモジュール常駐で、アンマウント後も生存させる)。\n */\n useEffect(() => {\n ensureDailyReportIdsStreamSession({ apiBasePath, userKey: userId ?? null })\n }, [apiBasePath, userId])\n\n /**\n * 認証切れ (ストリームが 401/403) はドキュメントのフルリロードへ倒し、\n * サーバー loader のログインリダイレクトに委譲する。\n * 依存配列: stream.phase。クリーンアップ: なし。\n */\n useEffect(() => {\n if (stream.phase === \"auth-required\") {\n reloadDocument()\n }\n }, [stream.phase])\n\n // 1 件でも届いたら (または空リストで完走したら) 本体を描画する\n const showContent = stream.items.length > 0 || stream.phase === \"complete\"\n return (\n // key でユーザーごとにプロバイダーをリマウントする。リロード無しのユーザー切替\n // (再ログイン・成り代わり) で items / removedIds / 保留キュー等の状態が旧ユーザーの\n // まま残ると、render-phase マージが旧ユーザーの行を新ユーザーの一覧へ持ち越す\n // エラーサーフェスはユーザー切替のリマウント境界より外側 (バナーは切替を跨いで消えてよい)。\n // アクションプロバイダーはこの notifyError を購読して失敗を可視化する\n <DailyReportErrorProvider>\n <DailyReportActionProvider key={userId ?? \"anonymous\"} user={user} initialItems={stream.items} userId={userId}>\n {showContent ? <DailyReportResolvedContent userId={userId} /> : stream.phase === \"failed\" ? <DailyReportLoadError /> : <DailyReportLoading />}\n </DailyReportActionProvider>\n </DailyReportErrorProvider>\n )\n}\n\n/**\n * Chrome-agnostic daily report page with client configuration provider.\n * 設定プロバイダー込みの日報ページコンポーネント。\n */\nexport const DailyReportPage = ({ config, ...rest }: DailyReportPageProps) => (\n <DailyReportConfigProvider config={config}>\n <DailyReportPageInner {...rest} />\n </DailyReportConfigProvider>\n)\n","import { Plus, RefreshCw, RotateCw } from \"lucide-react\"\nimport type { ReactNode } from \"react\"\nimport { useEffect, useMemo, useRef, useState } from \"react\"\nimport { useSearchParams } from \"react-router\"\nimport { resolveTodayBusinessDateKey } from \"../../shared/business-date\"\nimport { useDailyReportConfig } from \"../config-context\"\nimport { useDailyReportActionContext } from \"../contexts/daily-report-action-context\"\nimport { useAnimatedNumber } from \"../hooks/use-animated-number\"\nimport { Button } from \"../ui/button\"\nimport { Label } from \"../ui/label\"\nimport { Switch } from \"../ui/switch\"\nimport { Tabs, TabsContent, TabsList, TabsTrigger } from \"../ui/tabs\"\nimport { DAILY_REPORT_FETCH_DEBOUNCE_MS } from \"../utils/constants\"\nimport { DailyReportDetailList, type DailyReportDetailListScrollRestore } from \"./daily-report-detail-list\"\nimport { DailyReportIdsStreamStatus } from \"./daily-report-ids-stream-status\"\nimport { DailyReportList } from \"./daily-report-list\"\n\nconst tabTriggerClass = \"min-w-24 p-2 font-medium text-slate-600 text-xs transition-colors data-[state=active]:bg-white data-[state=active]:text-slate-900 sm:min-w-36 sm:text-sm dark:text-slate-300 dark:data-[state=active]:bg-slate-800 dark:data-[state=active]:text-slate-100\"\n\ntype DailyReportResolvedContentProps = {\n userId?: string | null\n}\n\n/**\n * Displays streamed daily reports with layout and tabbed virtual scroll views.\n * レイアウトとタブ付き仮想スクロールでストリーム受信済み日報を表示するコンポーネント。\n *\n * アイテムは Action Context (ストリームチャンクをマージ済み) から読む。\n */\nexport const DailyReportResolvedContent = ({ userId }: DailyReportResolvedContentProps) => {\n // 自動既読設定\n const [autoMarkRead, setAutoMarkRead] = useState(false)\n const { title, renderHeader, showDevControls } = useDailyReportConfig()\n const { isSseEnabled, toggleSse, createReport, items: dailyReportItems, refetchData, clearCacheAndRefetch } = useDailyReportActionContext()\n const [searchParams] = useSearchParams()\n\n // 総件数の表示値 (チャンク到着ごとの跳びをイーズアウトで滑らかにする)\n const animatedItemCount = useAnimatedNumber(dailyReportItems.length)\n\n // 各タブのスクロール位置をアンマウント後も保持する共有スロット (子がマウント時に読み、\n // スクロールのたびに書き戻す)。❗ ref の「値」をここで読んで props へ渡してはならない:\n // タブは非制御でこのコンポーネントはタブ切替時に再レンダーされないため、レンダー時読取は\n // 古い値の焼き込みになる (タブ復帰でスクロール位置がリセットされた実害バグの根本原因)\n const listScrollOffsetRef = useRef<number>(0)\n const detailListScrollRestoreRef = useRef<DailyReportDetailListScrollRestore>({ topItem: null, anchorReportHubId: null, measuredHeights: new Map() })\n\n // 選択状態の管理 (タブ間で共有)\n const [selectedItemId, setSelectedItemId] = useState<number | null>(() => {\n if (dailyReportItems.length === 0) return null\n return dailyReportItems[0].reportHubId\n })\n\n // 初回の自動選択を一度きりにするフラグ (ユーザーの明示的な選択解除を上書きしない)\n const hasAutoSelectedRef = useRef(false)\n\n /**\n * 最初のアイテム到着時に先頭日報を一度だけ自動選択する副作用。\n * useState 初期化子はマウント時点で context の items がまだ空のため実質機能せず、\n * ストリーミングでは最初のチャンク到着後に選択を確定させる必要がある。\n * モバイルのオーバーレイは isMobileDetailOpen (明示タップ) でのみ開くため、\n * プログラム選択でオーバーレイが開くことはない。\n * 依存配列: dailyReportItems (チャンク到着ごと)。クリーンアップ: なし。\n */\n useEffect(() => {\n if (!hasAutoSelectedRef.current && dailyReportItems.length > 0) {\n hasAutoSelectedRef.current = true\n setSelectedItemId((prev) => prev ?? dailyReportItems[0].reportHubId)\n }\n }, [dailyReportItems])\n\n const handleReload = async () => {\n await refetchData()\n }\n\n const handleReloadClearCache = async () => {\n await clearCacheAndRefetch()\n }\n\n const handleCreateReport = async () => {\n const businessDateParam = searchParams.get(\"businessDate\")\n // 「本日」はローカル暦日で決める。toISOString() の UTC 日付だと JST 深夜〜午前帯に\n // 前日の営業日へ新規日報が登録されてしまう\n const dateToUse = businessDateParam || resolveTodayBusinessDateKey()\n const newId = await createReport(dateToUse)\n setSelectedItemId(newId)\n }\n\n /**\n * Effect to load the auto-read setting from local storage on mount.\n * マウント時にローカルストレージから自動既読設定を読み込む副作用。\n *\n * Purpose: To persist user preference for auto-read behavior.\n * Dependencies: [] - Runs only on mount.\n * Cleanup: None.\n */\n useEffect(() => {\n const stored = localStorage.getItem(\"daily-report-auto-read\")\n if (stored === \"true\") {\n setAutoMarkRead(true)\n }\n }, [])\n\n const handleAutoMarkReadChange = (checked: boolean) => {\n setAutoMarkRead(checked)\n localStorage.setItem(\"daily-report-auto-read\", String(checked))\n }\n\n const tabItems = useMemo<{ value: string; label: string; content: ReactNode }[]>(\n () => [\n {\n value: \"list\",\n label: \"📄 List\",\n content: <DailyReportList dailyReportItems={dailyReportItems} autoMarkRead={autoMarkRead} selectedReportHubId={selectedItemId} onSelectItem={setSelectedItemId} userId={userId} scrollOffsetRef={listScrollOffsetRef} />,\n },\n {\n value: \"tree\",\n label: \"🌳 Tree\",\n content: <div className=\"flex flex-1 items-center justify-center rounded-lg border border-slate-300 border-dashed bg-white p-6 text-slate-500 transition-colors dark:border-slate-700 dark:bg-slate-900/70 dark:text-slate-300\">Tree ビューは現在準備中です</div>,\n },\n {\n value: \"detailList\",\n label: \"📋 DetailList\",\n content: <DailyReportDetailList dailyReportItems={dailyReportItems} userId={userId} selectedItemId={selectedItemId} onSelectItem={setSelectedItemId} scrollRestoreRef={detailListScrollRestoreRef} />,\n },\n ],\n [dailyReportItems, autoMarkRead, selectedItemId, userId],\n )\n\n return (\n <>\n {renderHeader({\n title,\n annotation: (\n <div className=\"flex flex-wrap items-center gap-2 leading-1\">\n <DailyReportIdsStreamStatus />\n <span className=\"tabular-nums\">総件数: {animatedItemCount.toLocaleString()} 件</span>\n <span>デバウンス: {DAILY_REPORT_FETCH_DEBOUNCE_MS} ms</span>\n </div>\n ),\n })}\n <div className=\"h-full bg-slate-50 dark:bg-slate-950\">\n <div className=\"flex h-full w-full max-w-6xl flex-1 flex-col p-1\">\n <Tabs defaultValue=\"list\" className=\"flex h-full flex-1 flex-col\">\n <div className=\"flex items-center justify-between gap-2 rounded-md bg-slate-50 p-1 transition-colors dark:bg-slate-900/70 dark:text-slate-200\">\n <TabsList className=\"scrollbar-none flex w-full touch-pan-x flex-nowrap overflow-x-auto overflow-y-hidden bg-transparent p-0\">\n {tabItems.map(({ value, label }) => (\n <TabsTrigger key={value} value={value} className={tabTriggerClass}>\n {label}\n </TabsTrigger>\n ))}\n </TabsList>\n <div className=\"flex shrink-0 items-center space-x-1 px-2\">\n <Button variant=\"ghost\" size=\"icon\" onClick={handleCreateReport} title=\"日報を作成\">\n <Plus className=\"h-4 w-4\" />\n </Button>\n {showDevControls && (\n <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\">\n <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=\"開発環境限定のコントロールです\">\n DEV\n </span>\n <div className=\"flex items-center space-x-1\">\n <Switch id=\"sse-mode\" checked={isSseEnabled} onCheckedChange={toggleSse} className=\"scale-75\" />\n <Label htmlFor=\"sse-mode\" className=\"cursor-pointer text-slate-600 text-xs dark:text-slate-300\" title=\"開発環境のみ: Real-time SSE 購読切り替え\">\n 購読\n </Label>\n </div>\n <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=\"開発環境のみ: ページリロードなしでデータのみ再取得\">\n <RotateCw className=\"mr-1 h-3 w-3\" />\n リロード\n </Button>\n <Button\n variant=\"ghost\"\n size=\"sm\"\n onClick={handleReloadClearCache}\n className=\"h-6 px-1.5 text-amber-800 text-xs hover:bg-amber-200 dark:text-amber-300 dark:hover:bg-amber-800\"\n title=\"開発環境のみ: サーバー/DB & クライアントのキャッシュを破棄してデータ再取得\">\n <RefreshCw className=\"mr-1 h-3 w-3\" />\n キャッシュクリア\n </Button>\n </div>\n )}\n <Switch id=\"auto-read-mode\" checked={autoMarkRead} onCheckedChange={handleAutoMarkReadChange} className=\"scale-75\" />\n <Label htmlFor=\"auto-read-mode\" className=\"cursor-pointer text-slate-500 text-xs\">\n 自動既読\n </Label>\n </div>\n </div>\n {tabItems.map(({ value, content }) => (\n <TabsContent key={value} value={value} className=\"mt-0 flex h-full min-h-0 flex-1 flex-col\">\n {content}\n </TabsContent>\n ))}\n </Tabs>\n </div>\n </div>\n </>\n )\n}\n","import * as SwitchPrimitives from \"@radix-ui/react-switch\"\nimport * as React from \"react\"\n\nimport { cn } from \"./cn\"\n\nconst Switch = React.forwardRef<React.ElementRef<typeof SwitchPrimitives.Root>, React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>>(({ className, ...props }, ref) => (\n <SwitchPrimitives.Root\n className={cn(\n \"peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input\",\n className,\n )}\n {...props}\n ref={ref}>\n <SwitchPrimitives.Thumb className={cn(\"pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0\")} />\n </SwitchPrimitives.Root>\n))\nSwitch.displayName = SwitchPrimitives.Root.displayName\n\nexport { Switch }\n","/**\n * React Router route helpers for the daily-report page (revalidation policy / client loader).\n * 日報ページ向け React Router ルートヘルパー (再検証ポリシー / クライアントローダー)。\n */\nimport type { ShouldRevalidateFunction, ShouldRevalidateFunctionArgs } from \"react-router\"\nimport { bootstrapDailyReportIdsStreamSession, primeDailyReportIdsStreamSession } from \"./streaming/daily-report-ids-stream-session\"\n\n/**\n * Prevents loader revalidation when POP navigations keep identical URL state.\n * URL 状態が同一の POP ナビゲーション時にローダー再検証を抑止。\n */\nexport const dailyReportShouldRevalidate: ShouldRevalidateFunction = ({ actionResult, actionStatus, currentUrl, defaultShouldRevalidate, formAction, formData, formEncType, formMethod, json, nextUrl, text }: ShouldRevalidateFunctionArgs) => {\n if (!defaultShouldRevalidate) {\n return false\n }\n\n const isSameUrl = currentUrl.pathname === nextUrl.pathname && currentUrl.search === nextUrl.search && currentUrl.hash === nextUrl.hash\n if (!isSameUrl) {\n return defaultShouldRevalidate\n }\n\n if (formData) {\n const intent = formData.get(\"intent\")\n if (intent === \"toggleRead\" || intent === \"toggleStar\" || intent === \"addComment\" || intent === \"deleteComment\" || intent === \"create\" || intent === \"update\" || intent === \"publish\" || intent === \"delete\") {\n return false\n }\n }\n\n const hasSubmission = actionResult !== undefined || actionStatus !== undefined || formAction !== undefined || formData !== undefined || formEncType !== undefined || formMethod !== undefined || json !== undefined || text !== undefined\n return hasSubmission ? defaultShouldRevalidate : false\n}\n\n/**\n * Creates a React Router clientLoader that bootstraps the ids stream session.\n * ID ストリームセッションを起動する React Router clientLoader を生成する処理。\n *\n * ID 一覧は Promise で返さず、モジュール常駐のストリームセッションが供給する\n * (ページ側は `useDailyReportIdsStream` で購読する)。ローダー時点でセッションを\n * 生成することが**コールドロードの最重要最適化** — ページマウントの effect まで\n * 遅らせると、ストリームの開始が「ハイドレーション完了 + effect flush」の後ろに\n * 直列化される。再訪 (セッション既存) は serverLoader を待たずに鮮度確認を先行させる。\n *\n * `config.apiBasePath` を上書きするアプリは同じ値を `options.apiBasePath` にも渡すこと\n * (渡し忘れは初回 1 回だけ誤パス生成 → ページマウントの ensure が自己修復する)。\n *\n * 返す関数へ `hydrate = true as const` を付与済み (初期ハイドレーションでも実行される)。\n */\nexport const createDailyReportClientLoader = <TServerData extends Record<string, unknown>>(options?: { apiBasePath?: string }) => {\n const clientLoader = async (args: { serverLoader: () => Promise<unknown> }): Promise<TServerData> => {\n // 再訪の主経路: 既存セッションの鮮度確認を serverLoader より先に始める\n primeDailyReportIdsStreamSession()\n const serverData = (await args.serverLoader()) as TServerData\n // 不在なら生成 (コールドロードでハイドレーションと並行してフェッチを開始)、\n // 既存なら userKey 照合 (別ユーザーは描画前に破棄・再生成) + 鮮度確認\n const rawUserId = (serverData as { userId?: unknown }).userId\n bootstrapDailyReportIdsStreamSession({ apiBasePath: options?.apiBasePath, userKey: typeof rawUserId === \"string\" ? rawUserId : null })\n return serverData\n }\n clientLoader.hydrate = true as const\n return clientLoader\n}\n"],"mappings":"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","names":["client_exports","__export","BusinessDayThumbOverlay","CONTENT_DRAG_POINTER_INPUTS","DAILY_REPORT_FETCH_DEBOUNCE_MS","DEFAULT_ITEM_HEIGHT","DailyReportActionProvider","DailyReportAttachmentIndicator","DailyReportAttachmentList","DailyReportCache","DailyReportCommentForm","DailyReportCommentItem","DailyReportCommentList","DailyReportConfigProvider","DailyReportDetailList","DailyReportEditForm","DailyReportErrorProvider","DailyReportIdsStreamClient","DailyReportIdsStreamStatus","DailyReportList","DailyReportLoadError","DailyReportLoading","DailyReportPage","DailyReportResolvedContent","EMPTY_DETAIL","EMPTY_ITEM","FAST_SCROLL_INERTIA_OPTIONS","FAST_SCROLL_WHEEL_MULTIPLIER","ITEM_CONTAINER_HEIGHT","ITEM_VERTICAL_PADDING","MAX_PREVIEW_LINES","PREFETCH_LOOKAHEAD","PREFETCH_MAX_DATES","SCROLL_BAR_WIDTH","SSE_INITIAL_CONNECT_DEFER_MS","TAP_SCROLL_CIRCLE_OPTIONS","UnreadIndicator","VIRTUAL_SCROLL_OVERSCAN_COUNT","WHEEL_RESET_TIMEOUT_MS","acquireMutationLock","applyBroadcastReportUpdate","applyDailyReportServerUpdates","bootstrapDailyReportIdsStreamSession","clearDailyReportCache","createDailyReportClientLoader","dailyReportShouldRevalidate","defaultDailyReportClientConfig","defaultDailyReportFieldLabels","deleteDailyReportCache","ensureDailyReportIdsStreamSession","getCachedReport","getDailyReportIdsStreamSessionUserKey","primeDailyReportIdsStreamSession","refreshDailyReportIdsStream","registerRecentDeletion","releaseMutationLock","reloadDocument","removeDailyReportIdsStreamItem","resetDailyReportIdsStreamSessionForTesting","resolveSourceTypeConfig","retryDailyReportIdsStream","subscribeCacheReady","updateDailyReportCache","upsertDailyReportIdsStreamItem","useAnimatedNumber","useDailyReportActionContext","useDailyReportComments","useDailyReportConfig","useDailyReportDetail","useDailyReportErrorSurface","useDailyReportIdsStream","useDailyReportPrefetch","useDailyReportSseConnection","useDynamicViewportHeight","writeCache","__toCommonJS","import_jsx_runtime","BusinessDayThumbOverlay","orientation","thumbCenter","label","isDragging","isTapScrollActive","isWheelScrollActive","import_lucide_react","import_jsx_runtime","COUNT_VISIBLE_FROM","DailyReportAttachmentIndicator","count","label","import_react","import_jsx_runtime","defaultDailyReportFieldLabels","defaultDailyReportClientConfig","title","annotation","DailyReportConfigContext","DailyReportConfigProvider","config","children","merged","useDailyReportConfig","resolveSourceTypeConfig","sourceType","sourceTypeConfigs","lower","matchedKey","k","import_jsx_runtime","BYTES_PER_UNIT","SIZE_UNITS","formatFileSize","bytes","value","unitIndex","DailyReportAttachmentList","attachments","apiBasePath","fieldLabels","useDailyReportConfig","attachment","href","size","isMissing","import_outline","import_react","mergeComments","legacyComments","modernComments","currentUserId","legacyUIComments","c","index","modernUIComments","resolveCommentColorClass","color","normalized","fallbackText","value","trimmed","formatToIsoDate","dateOnly","date","year","month","day","hours","minutes","seconds","import_clsx","import_tailwind_merge","cn","inputs","import_jsx_runtime","checkIsPendingDelete","commentId","pendingDeleteId","resolvedIdMap","DailyReportCommentItem","comment","_reportHubId","_businessDate","onOptimisticDelete","onPendingDeleteIdChange","onTrashClick","isDeleted","setIsDeleted","isPendingDelete","handleConfirmDelete","e","cn","resolveCommentColorClass","fallbackText","formatToIsoDate","import_react","import_jsx_runtime","useAutoResetPendingDelete","pendingDeleteId","onPendingDeleteIdChange","timerRef","handleGlobalClick","e","commentId","DailyReportCommentList","comments","reportHubId","businessDate","onDeleteComment","resolvedIdMap","handleTrashClick","comment","DailyReportCommentItem","DailyReportCommentForm","onAddComment","isSubmitting","setIsSubmitting","formRef","content","input","import_virtualscroll","import_lucide_react","import_react","import_react","isIdsStreamChunkLine","line","compareIdsStreamOrder","a","b","isIdsStreamOrdered","items","i","upsertInIdsStreamOrder","item","next","existing","low","high","mid","createLogger","level","prefix","impl","format","message","rest","import_react","ISO_DATE_PATTERN","SLASH_DATE_PATTERN","zeroPad","value","normalizeFromDate","year","month","day","resolveTodayBusinessDateKey","now","normalizeBusinessDateKey","trimmed","parsed","import_virtualscroll","DAILY_REPORT_FETCH_DEBOUNCE_MS","SSE_INITIAL_CONNECT_DEFER_MS","DEFAULT_ITEM_HEIGHT","ITEM_VERTICAL_PADDING","ITEM_CONTAINER_HEIGHT","FAST_SCROLL_WHEEL_MULTIPLIER","FAST_SCROLL_INERTIA_OPTIONS","PREFETCH_LOOKAHEAD","PREFETCH_MAX_DATES","EMPTY_ITEM","EMPTY_DETAIL","WHEEL_RESET_TIMEOUT_MS","VIRTUAL_SCROLL_OVERSCAN_COUNT","SCROLL_BAR_WIDTH","MAX_PREVIEW_LINES","TAP_SCROLL_CIRCLE_OPTIONS","CONTENT_DRAG_POINTER_INPUTS","logger","createLogger","getGlobal","k","d","v","TTL","REVIVE_MS","DailyReportCache","now","GC_THRESHOLD","id","entry","key","listener","ex","l","ignoreExpiry","e","dateKey","report","expiresAt","syncList","normalizeBusinessDateKey","listEntry","data","r","updates","strict","date","list","serverReport","cachedReport","c","serverIds","missing","b","reports","fetchStart","t","processed","lastMut","cached","merged","writeCache","exp","sync","getCachedReport","opts","updateDailyReportCache","u","deleteDailyReportCache","clearDailyReportCache","acquireMutationLock","releaseMutationLock","registerRecentDeletion","cId","subscribeCacheReady","reportHubId","callback","applyDailyReportServerUpdates","_reportHubId","applyBroadcastReportUpdate","cachedMine","commentItems","fetchReportById","apiBasePath","pending","start","p","res","fetchReportsByDate","useDailyReportDetail","businessDate","useDailyReportConfig","setReport","error","setError","isLoading","setIsLoading","isRefetching","setIsRefetching","reqRef","onUpdate","uid","token","isStale","found","timer","item","DAILY_REPORT_FETCH_DEBOUNCE_MS","useDailyReportPrefetch","import_react","import_zod","dailyReportInterviewerSchema","dailyReportCommentSchema","dailyReportLabelDefSchema","dailyReportCommentItemSchema","dailyReportAttachmentItemSchema","dailyReportDetailSchema","connectedMessageSchema","statusUpdateMessageSchema","commentAddMessageSchema","commentDeleteMessageSchema","reportCreateMessageSchema","reportUpdateMessageSchema","reportPublishMessageSchema","reportDeleteMessageSchema","dailyReportSseMessageSchema","logger","createLogger","useDailyReportSseConnection","isSseEnabled","onMessage","ssePath","useDailyReportConfig","eventSource","reconnectTimer","reconnectAttempt","lastReceivedEventId","connect","event","raw","result","dailyReportSseMessageSchema","e","delayMs","import_react","DEFAULT_PUBLISH_COALESCE_MS","INITIAL_RETRY_DELAY_MS","MAX_RETRY_DELAY_MS","MAX_CONSECUTIVE_FAILURES","DEFAULT_READ_STALL_TIMEOUT_MS","READ_STALL","DEFAULT_API_BASE_PATH","CURSOR_DATE_PATTERN","DailyReportIdsStreamClient","options","input","init","handler","ms","id","args","listener","item","existing","next","reportHubId","mode","generation","outcome","seen","forceRefresh","abortController","fetchPromise","headerTimer","response","resolve","reader","decoder","partialLine","sawErrorLine","sawIsLast","chunk","done","value","lines","mutated","rawLine","parsed","isIdsStreamChunkLine","error","stallTimer","stallPromise","items","elapsed","partial","params","query","delayMs","timerId","finish","import_meta","REVALIDATE_AFTER_MS","session","sessionListeners","IDLE_STATE","reloadDocument","notifySessionListeners","listener","destroySession","ensureDailyReportIdsStreamSession","options","apiBasePath","defaultDailyReportClientConfig","clearDailyReportCache","client","DailyReportIdsStreamClient","runFreshnessCheck","record","state","bootstrapDailyReportIdsStreamSession","primeDailyReportIdsStreamSession","upsertDailyReportIdsStreamItem","item","removeDailyReportIdsStreamItem","reportHubId","retryDailyReportIdsStream","refreshDailyReportIdsStream","getDailyReportIdsStreamSessionUserKey","subscribeSession","getSessionSnapshot","getServerSnapshot","useDailyReportIdsStream","resetDailyReportIdsStreamSessionForTesting","import_react","import_jsx_runtime","ERROR_BANNER_AUTO_DISMISS_MS","DailyReportErrorContext","useDailyReportErrorSurface","DailyReportErrorProvider","children","onError","useDailyReportConfig","banner","setBanner","onErrorRef","dismissTimerRef","notifyError","info","host","dismiss","value","import_jsx_runtime","logger","createLogger","OPERATION_LABELS","describeOperationFailure","intent","EMPTY_INITIAL_ITEMS","DailyReportActionContext","useDailyReportActionContext","context","getUserDisplayName","user","fullName","DailyReportActionProvider","children","initialItems","userId","apiBasePath","draftLabelName","useDailyReportConfig","notifyError","useDailyReportErrorSurface","notifyErrorRef","pendingAddCommentIds","pendingDeleteCommentIds","pendingStarUpdates","pendingReadUpdates","resolvedIdMap","version","setVersion","items","setItems","isSseEnabled","setIsSseEnabled","hasManualSseToggleRef","pendingAddPromises","manuallyTrackingIds","removedIds","processedClientTempIds","streamState","useDailyReportIdsStream","isStreamSettledRef","isSessionStillOurs","getDailyReportIdsStreamSessionUserKey","refetchData","refreshDailyReportIdsStream","v","DailyReportCache","listener","clearCacheAndRefetch","clearDailyReportCache","formData","res","errData","e","pendingSseQueue","handleSseMessageRef","enqueuePendingSseMessage","reportHubId","message","queue","existing","entry","subscribeCacheReady","getCachedReport","msg","addProcessedId","id","isProcessedId","upsertItem","businessDate","upsertDailyReportIdsStreamItem","prev","item","nextBusinessDate","upsertInIdsStreamOrder","toggleSse","timer","SSE_INITIAL_CONNECT_DEFER_MS","removeStaleItem","removeDailyReportIdsStreamItem","prevInitialItems","setPrevInitialItems","mergedMap","merged","isIdsStreamOrdered","compareIdsStreamOrder","initialIds","handleSseMessage","payload","comment","cached","c","isMyComment","pendingMap","tempId","content","newComment","updatedReport","a","b","applyDailyReportServerUpdates","acquireMutationLock","releaseMutationLock","commentId","statusType","value","pendingEntry","deleteDailyReportCache","report","applyBroadcastReportUpdate","useDailyReportSseConnection","executeAction","params","callbacks","options","clientTempId","k","error","data","addComment","promise","realId","cachedReport","updateDailyReportCache","deleteComment","resolved","deletedId","registerRecentDeletion","toggleStar","isStarred","toggleReadImpl","isRead","toggleReadRef","toggleRead","createReport","now","dummyReport","writeCache","newId","updateReport","publishReport","oldLabels","newLabels","l","deleteReport","targetItem","i","_data","import_react","useDailyReportComments","report","user","userId","pendingAddCommentIds","pendingDeleteCommentIds","resolvedIdMap","version","addComment","deleteComment","useDailyReportActionContext","pendingDeleteId","setPendingDeleteId","handleDeleteComment","cId","handleAddComment","content","prevUiCommentsRef","comments","mergeComments","deletedIds","c","addedComments","existingIds","authorName","tempId","realId","import_react","useDynamicViewportHeight","containerRef","options","enableMutationObserver","bottomGap","minFallbackHeight","viewportHeight","setViewportHeight","updateViewportHeight","target","toNumber","value","containerTop","parentRect","rootStyle","footerHeight","headerHeight","scrollbarHeight","containerStyle","marginBottom","paddingBottom","viewportBottom","effectiveBottom","layoutTop","effectiveTop","availableHeight","safeAvailableHeight","clampedHeight","previous","frameId","timeoutId","resizeObserver","mutationObserver","scheduleUpdate","containerElement","import_react","import_react_slot","import_class_variance_authority","React","import_jsx_runtime","buttonVariants","Button","className","variant","size","asChild","props","ref","cn","import_jsx_runtime","Input","className","type","props","cn","LabelPrimitive","import_jsx_runtime","Label","className","props","cn","import_jsx_runtime","Textarea","className","props","cn","import_jsx_runtime","DailyReportEditForm","report","onCancel","onPublishSuccess","updateReport","publishReport","useDailyReportActionContext","title","setTitle","content","setContent","isSaving","setIsSaving","syncedReportIdRef","handleSave","handlePublish","Label","Input","e","Textarea","Button","import_jsx_runtime","UnreadIndicator","className","cn","import_jsx_runtime","useReportItemHeightObserver","ref","itemId","onHeightMeasured","element","resizeObserver","DailyReportDetailRow","index","setSize","children","elementRef","formatInterviewerLabel","interviewer","DailyReportDetailList","dailyReportItems","userId","selectedItemId","onSelectItem","scrollRestoreRef","initialScrollAnchor","isRestoredMount","restore","foundIndex","item","skipInitialScrollToSelectionRef","mountSelectionIdRef","itemHeightsRef","scrollContainerRef","virtualScrollRef","title","useDailyReportConfig","viewportHeight","updateViewportHeight","useDynamicViewportHeight","prefetchDailyReportByDate","useDailyReportPrefetch","overlayLabel","setOverlayLabel","isWheelScrollActive","setIsWheelScrollActive","wheelResetTimerRef","scrollEndTimerRef","dailyReportItemsRef","lastScrolledRef","onSelectItemRef","el","handle","extendedHandle","id","itemCount","getItem","EMPTY_ITEM","schedulePrefetchForRange","fromIndex","toIndex","boundedFrom","boundedTo","businessDates","candidate","PREFETCH_MAX_DATES","date","handleRangeChange","renderingStartIndex","renderingEndIndex","visibleStartIndex","firstVisibleItem","formatToIsoDate","PREFETCH_LOOKAHEAD","lastVirtualScrollHandleRef","handleScroll","anchor","container","renderThumbOverlay","props","BusinessDayThumbOverlay","onWheel","WHEEL_RESET_TIMEOUT_MS","updateItemHeight","height","getItemHeight","getItemKey","handleCommentAdded","renderItem","DailyReportItemContainer","VIRTUAL_SCROLL_OVERSCAN_COUNT","SCROLL_BAR_WIDTH","TAP_SCROLL_CIRCLE_OPTIONS","CONTENT_DRAG_POINTER_INPUTS","FAST_SCROLL_WHEEL_MULTIPLIER","FAST_SCROLL_INERTIA_OPTIONS","SkeletonReportItem","onCommentAdded","report","error","isLoading","isRefetching","useDailyReportDetail","removeStaleItem","useDailyReportActionContext","draftLabelName","isEditing","setIsEditing","hasBypassedAutoEdit","setHasBypassedAutoEdit","autoEditingRef","hasEverFetched","isDraft","l","isMine","DailyReportError","DailyReportEditForm","DailyReportResolvedItem","DailyReportHeader","isRead","isStarred","onToggleRead","onToggleStar","onDelete","itemSourceType","visitTimeDisplay","categoryLabel","fieldLabels","sourceTypeConfigs","DailyReportAttachmentIndicator","sourceType","sourceConfig","resolveSourceTypeConfig","cn","UnreadIndicator","fallbackText","MetadataPanel","onDoubleClick","formContainerRef","toggleStar","toggleRead","pendingStarUpdates","pendingReadUpdates","deleteReport","user","uiComments","handleAddComment","handleDeleteComment","pendingDeleteId","setPendingDeleteId","resolvedIdMap","useDailyReportComments","isExternalSource","canComment","handleAddCommentWrapper","content","handleToggleRead","handleToggleStar","handleDelete","normalizedContent","from","to","labels","DailyReportAttachmentList","DailyReportCommentList","DailyReportCommentForm","import_lucide_react","import_react","useAnimatedNumber","value","durationMs","display","setDisplay","displayRef","isFirstRef","from","rafId","startTimestamp","step","timestamp","progress","eased","next","import_jsx_runtime","DailyReportIdsStreamStatus","stream","useDailyReportIdsStream","animatedLoaded","useAnimatedNumber","animatedTotal","percent","retryDailyReportIdsStream","import_swipe_overlay","import_virtualscroll","import_lucide_react","import_react","import_react","MOBILE_LAYOUT_MEDIA_QUERY","useIsMobileLayout","isMobileLayout","setIsMobileLayout","query","sync","TabsPrimitive","React","import_jsx_runtime","Tabs","TabsList","className","props","ref","cn","TabsTrigger","TabsContent","import_jsx_runtime","useMobileOverlayManager","isMobileLayout","selectedReportHubId","dailyReportItems","onSelectItem","isMobileDetailOpen","setIsMobileDetailOpen","isOverlayMounted","setIsOverlayMounted","overlayHistoryKeyRef","lastClosedItemIdRef","bodyOverflowRef","selectedItem","e","shouldShowOverlay","timer","key","onPopState","handleOverlayClose","useAutoRead","report","isLoading","autoMarkRead","isActive","toggleRead","ignoreAutoReadForReportHubId","lastReportHubIdRef","handleToggleRead","isAuto","nextValue","DailyReportList","userId","scrollOffsetRef","initialScrollOffset","scrollContainerRef","virtualScrollRef","title","useDailyReportConfig","viewportHeight","updateViewportHeight","useDynamicViewportHeight","overlayLabel","setOverlayLabel","isWheelScrollActive","setIsWheelScrollActive","useIsMobileLayout","wheelResetTimerRef","itemCount","scrollEndTimerRef","columnHeightStyle","onResize","el","onWheel","WHEEL_RESET_TIMEOUT_MS","targetId","index","item","prev","next","formatToIsoDate","dailyReportItemsRef","id","limit","handleItemSelect","getItem","EMPTY_ITEM","getItemHeight","DEFAULT_ITEM_HEIGHT","renderListRow","ListReportItem","handleRangeChange","range","handleScroll","position","container","renderThumbOverlay","props","BusinessDayThumbOverlay","VIRTUAL_SCROLL_OVERSCAN_COUNT","SCROLL_BAR_WIDTH","TAP_SCROLL_CIRCLE_OPTIONS","CONTENT_DRAG_POINTER_INPUTS","FAST_SCROLL_INERTIA_OPTIONS","FAST_SCROLL_WHEEL_MULTIPLIER","ITEM_VERTICAL_PADDING","DailyReportSidePane","DailyReportMobileOverlay","ListSkeletonItem","wrapperProps","ITEM_CONTAINER_HEIGHT","heightStyle","activeTab","setActiveTab","isEditing","setIsEditing","draftLabelName","fieldLabels","sourceTypeConfigs","error","useDailyReportDetail","lastReportIdRef","autoEditRef","isDraft","l","isMine","scrollToBottom","headerDate","headerCreator","fallbackText","showHeaderSkeleton","content","sourceType","sourceConfig","cn","Tabs","v","TabsList","tab","TabsTrigger","TabsContent","DailyReportEditForm","DailyReportDisplayPane","SkeletonText","className","onCommentAdded","onDoubleClick","toggleStar","deleteReport","user","useDailyReportActionContext","uiComments","handleAddComment","handleDeleteComment","pendingDeleteId","setPendingDeleteId","resolvedIdMap","useDailyReportComments","resolveSourceTypeConfig","isExternalSource","canComment","prevReportHubIdRef","prevCommentsLengthRef","currentReportHubId","handleAddCommentWrapper","displayContent","handleToggleStar","handleDelete","showSkeleton","isStarred","isRead","hasComments","UnreadIndicator","DailyReportAttachmentList","DailyReportCommentList","DailyReportCommentForm","isOpen","onClose","containerRef","closeButtonRef","isGuardBlocked","requestBlock","resetGuard","displayData","displayItem","EMPTY_DETAIL","overlayRef","overlayStyle","attemptClose","isSwiping","tapHandlers","extractPreviewLines","MAX_PREVIEW_LINES","setSize","onSelect","removeStaleItem","isRefetching","listItemProps","hasEverFetched","previewLines","line","i","creator","date","stopPointerPropagation","DailyReportAttachmentIndicator","import_react","import_lucide_react","import_react","import_react_router","SwitchPrimitives","React","import_jsx_runtime","Switch","className","props","ref","cn","import_jsx_runtime","tabTriggerClass","DailyReportResolvedContent","userId","autoMarkRead","setAutoMarkRead","title","renderHeader","showDevControls","useDailyReportConfig","isSseEnabled","toggleSse","createReport","dailyReportItems","refetchData","clearCacheAndRefetch","useDailyReportActionContext","searchParams","animatedItemCount","useAnimatedNumber","listScrollOffsetRef","detailListScrollRestoreRef","selectedItemId","setSelectedItemId","hasAutoSelectedRef","prev","handleReload","handleReloadClearCache","handleCreateReport","dateToUse","resolveTodayBusinessDateKey","newId","handleAutoMarkReadChange","checked","tabItems","DailyReportList","DailyReportDetailList","DailyReportIdsStreamStatus","DAILY_REPORT_FETCH_DEBOUNCE_MS","Tabs","TabsList","value","label","TabsTrigger","Button","Switch","Label","content","TabsContent","import_jsx_runtime","DailyReportLoadError","title","renderHeader","useDailyReportConfig","retryDailyReportIdsStream","DailyReportLoading","DailyReportIdsStreamStatus","DAILY_REPORT_FETCH_DEBOUNCE_MS","DailyReportPageInner","user","userId","apiBasePath","stream","useDailyReportIdsStream","ensureDailyReportIdsStreamSession","reloadDocument","showContent","DailyReportErrorProvider","DailyReportActionProvider","DailyReportResolvedContent","DailyReportPage","config","rest","DailyReportConfigProvider","dailyReportShouldRevalidate","actionResult","actionStatus","currentUrl","defaultShouldRevalidate","formAction","formData","formEncType","formMethod","json","nextUrl","text","intent","createDailyReportClientLoader","options","clientLoader","args","primeDailyReportIdsStreamSession","serverData","rawUserId","bootstrapDailyReportIdsStreamSession"]}
|