Skip to content

feat: 頒布履歴ページにCSVエクスポート機能を追加 - #1

Open
syusui-s wants to merge 4 commits into
mainfrom
claude/add-csv-export-BGbvA
Open

feat: 頒布履歴ページにCSVエクスポート機能を追加#1
syusui-s wants to merge 4 commits into
mainfrom
claude/add-csv-export-BGbvA

Conversation

@syusui-s

@syusui-s syusui-s commented Mar 31, 2026

Copy link
Copy Markdown
Owner

売上明細をCSVファイルとしてダウンロードできる機能を追加。
日別表示時は日付ごと、カタログ別表示時はカタログごとにエクスポート可能。
CSVカラム: 日時, カタログ名, 商品名, 単価, 数量, 小計

https://claude.ai/code/session_01FMAzUty2yxNMaj7mE5x62a

Summary by CodeRabbit

  • New Features

    • Added CSV export buttons for daily and catalog sales, producing downloadable UTF-8 CSVs with timestamped, sanitized filenames.
    • Exports filter and format sales rows, include subtotals, and handle catalog-specific exports.
  • Tests

    • Added tests covering CSV generation, field escaping (including protection against formula-injection), filename sanitization, and datetime formatting used in filenames.

売上明細をCSVファイルとしてダウンロードできる機能を追加。
日別表示時は日付ごと、カタログ別表示時はカタログごとにエクスポート可能。
CSVカラム: 日時, カタログ名, 商品名, 単価, 数量, 小計

https://claude.ai/code/session_01FMAzUty2yxNMaj7mE5x62a
Heroicons の arrow-down-tray (mini) アイコンを ExportButton に追加し、
アイコン + "CSV" テキストの構成にした。

https://claude.ai/code/session_01FMAzUty2yxNMaj7mE5x62a
@coderabbitai

coderabbitai Bot commented Mar 31, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

CSV export is added: a new utility module generates and downloads CSV from sales data (escaping fields, formatting datetimes, sanitizing filenames). The SaleList UI includes Export buttons for daily and catalog views that call these utilities and trigger browser downloads.

Changes

Cohort / File(s) Summary
CSV Export Utilities
src/utils/csvExport.ts, src/utils/csvExport.test.ts
Adds CSV generation and download utilities: escapeCsvField, formatDateTime, formatDateTimeForFilename, generateSalesCsv, sanitizeFilename, and downloadCsv. Includes tests covering escaping, formula-injection prevention, datetime formatting, filename sanitization, header/row generation, and CSV escaping for comma-containing fields.
Sales Page Export Integration
SaleList UI
src/pages/SaleList.tsx
Integrates export UI (ExportButton + inline DownloadIcon) and handlers: daily export builds CSV from grouped sales and uses first sale date (or empty) plus timestamp for filename; catalog export filters sales by catalogId, narrows items per sale, resolves catalog name, and downloads CSV. Extends local SalesDisplay props with onExport handler.

Sequence Diagram

sequenceDiagram
    actor User
    participant SaleList as "SaleList Component"
    participant CSVUtil as "CSV Utilities"
    participant Browser as "Browser Download"

    User->>SaleList: Click Export button (daily or catalog)
    activate SaleList
    SaleList->>CSVUtil: generateSalesCsv(sales, catalogNameResolver)
    activate CSVUtil
    CSVUtil->>CSVUtil: Flatten sales → items, calc subtotals
    CSVUtil->>CSVUtil: formatDateTime / escapeCsvField / sanitizeFilename
    CSVUtil-->>SaleList: Return CSV string
    deactivate CSVUtil
    SaleList->>CSVUtil: downloadCsv(csvString, filename)
    activate CSVUtil
    CSVUtil->>Browser: Create Blob & object URL
    CSVUtil->>Browser: Create temporary link, trigger click
    CSVUtil->>Browser: Revoke object URL, remove link
    deactivate CSVUtil
    deactivate SaleList
    Browser-->>User: CSV file downloaded
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 I hopped through rows and fields with glee,
Escaped the commas, kept quotes tidy,
Timestamps stitched each filename bright,
A carrot-click — CSV takes flight! ✨📥

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: 頒布履歴ページにCSVエクスポート機能を追加' (feat: Add CSV export feature to sales history page) directly matches the main objective of adding CSV export functionality to the SaleList page.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/add-csv-export-BGbvA

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/pages/SaleList.tsx`:
- Around line 223-234: The download filename uses raw catalogName which may
contain characters invalid in filenames; update handleExportCatalog to sanitize
catalogName before using it: remove or replace characters like \/:*?"<>| and
control characters (and optionally trim/limit length), then use the sanitized
value in the call to downloadCsv (still format exportTime via
formatDateTimeForFilename). Ensure the change targets the handleExportCatalog
function and the variable passed into downloadCsv so filenames are safe across
environments.

In `@src/utils/csvExport.ts`:
- Around line 57-59: The CSV download currently creates a Blob from csvContent
in downloadCsv without a UTF-8 BOM, which causes Japanese text to garble in
Excel; modify downloadCsv to prepend the UTF-8 BOM character (U+FEFF) to
csvContent before creating the Blob (so the Blob is constructed from "\uFEFF" +
csvContent) and keep the MIME type as 'text/csv;charset=utf-8' and the rest of
the flow (URL.createObjectURL, link download) unchanged.
- Around line 5-10: escapeCsvField currently only handles commas, quotes and
newlines but doesn't protect against spreadsheet formula injection; update the
escapeCsvField function to detect if the field begins with any of the dangerous
characters (=, +, -, @, tab, carriage return) and if so prefix the field with a
single quote (') before performing the normal quote-escaping and
surrounding-quote logic; ensure you still replace internal " with "" and apply
the surrounding quotes when the field contains comma, quote, newline or the
injected leading character so the returned string both prevents formula
execution and remains valid CSV.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d0287154-4e5f-4eeb-848e-f6deda1a66e3

📥 Commits

Reviewing files that changed from the base of the PR and between 05265f2 and b7bfca9.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (3)
  • src/pages/SaleList.tsx
  • src/utils/csvExport.test.ts
  • src/utils/csvExport.ts

Comment thread src/pages/SaleList.tsx
Comment thread src/utils/csvExport.ts
Comment on lines +5 to +10
export const escapeCsvField = (field: string): string => {
if (field.includes(',') || field.includes('"') || field.includes('\n')) {
return `"${field.replace(/"/g, '""')}"`;
}
return field;
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Harden CSV escaping against spreadsheet formula injection.

Fields beginning with =, +, -, @, tab, or carriage return can be interpreted as formulas by spreadsheet tools.

🔒 Proposed fix
 export const escapeCsvField = (field: string): string => {
-  if (field.includes(',') || field.includes('"') || field.includes('\n')) {
-    return `"${field.replace(/"/g, '""')}"`;
+  const neutralized = /^[=+\-@\t\r]/.test(field) ? `'${field}` : field;
+  if (
+    neutralized.includes(',') ||
+    neutralized.includes('"') ||
+    neutralized.includes('\n') ||
+    neutralized.includes('\r')
+  ) {
+    return `"${neutralized.replace(/"/g, '""')}"`;
   }
-  return field;
+  return neutralized;
 };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const escapeCsvField = (field: string): string => {
if (field.includes(',') || field.includes('"') || field.includes('\n')) {
return `"${field.replace(/"/g, '""')}"`;
}
return field;
};
export const escapeCsvField = (field: string): string => {
const neutralized = /^[=+\-@\t\r]/.test(field) ? `'${field}` : field;
if (
neutralized.includes(',') ||
neutralized.includes('"') ||
neutralized.includes('\n') ||
neutralized.includes('\r')
) {
return `"${neutralized.replace(/"/g, '""')}"`;
}
return neutralized;
};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/utils/csvExport.ts` around lines 5 - 10, escapeCsvField currently only
handles commas, quotes and newlines but doesn't protect against spreadsheet
formula injection; update the escapeCsvField function to detect if the field
begins with any of the dangerous characters (=, +, -, @, tab, carriage return)
and if so prefix the field with a single quote (') before performing the normal
quote-escaping and surrounding-quote logic; ensure you still replace internal "
with "" and apply the surrounding quotes when the field contains comma, quote,
newline or the injected leading character so the returned string both prevents
formula execution and remains valid CSV.

Comment thread src/utils/csvExport.ts
Comment on lines +57 to +59
export const downloadCsv = (csvContent: string, filename: string): void => {
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8' });
const url = URL.createObjectURL(blob);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Prepend UTF-8 BOM for Excel compatibility with Japanese CSV.

Without BOM, Japanese headers/values are frequently garbled in Windows Excel.

🧾 Proposed fix
 export const downloadCsv = (csvContent: string, filename: string): void => {
-  const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8' });
+  const blob = new Blob(['\uFEFF', csvContent], { type: 'text/csv;charset=utf-8' });
   const url = URL.createObjectURL(blob);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const downloadCsv = (csvContent: string, filename: string): void => {
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8' });
const url = URL.createObjectURL(blob);
export const downloadCsv = (csvContent: string, filename: string): void => {
const blob = new Blob(['\uFEFF', csvContent], { type: 'text/csv;charset=utf-8' });
const url = URL.createObjectURL(blob);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/utils/csvExport.ts` around lines 57 - 59, The CSV download currently
creates a Blob from csvContent in downloadCsv without a UTF-8 BOM, which causes
Japanese text to garble in Excel; modify downloadCsv to prepend the UTF-8 BOM
character (U+FEFF) to csvContent before creating the Blob (so the Blob is
constructed from "\uFEFF" + csvContent) and keep the MIME type as
'text/csv;charset=utf-8' and the rest of the flow (URL.createObjectURL, link
download) unchanged.

PRレビューの指摘に対応:
- sanitizeFilename: カタログ名に含まれるファイル名不正文字を_に置換
- escapeCsvField: =,+,-,@,\t,\r で始まるフィールドにシングルクォートを
  プレフィックスしてスプレッドシートの数式インジェクションを防止
- 対応するユニットテストを追加

https://claude.ai/code/session_01FMAzUty2yxNMaj7mE5x62a

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (2)
src/utils/csvExport.ts (2)

7-14: ⚠️ Potential issue | 🟠 Major

Quote \r-containing fields to avoid broken CSV rows.

Line 12 does not treat carriage return (\r) as a quoting trigger, so fields with \r can break record boundaries in CSV parsers.

🔧 Proposed fix
 export const escapeCsvField = (field: string): string => {
-  let sanitized = field;
-  if (sanitized.length > 0 && FORMULA_PREFIXES.includes(sanitized[0])) {
+  let sanitized = field;
+  const needsFormulaNeutralization =
+    sanitized.length > 0 && FORMULA_PREFIXES.includes(sanitized[0]);
+  if (needsFormulaNeutralization) {
     sanitized = `'${sanitized}`;
   }
-  if (sanitized.includes(',') || sanitized.includes('"') || sanitized.includes('\n')) {
+  if (
+    needsFormulaNeutralization ||
+    sanitized.includes(',') ||
+    sanitized.includes('"') ||
+    sanitized.includes('\n') ||
+    sanitized.includes('\r')
+  ) {
     return `"${sanitized.replace(/"/g, '""')}"`;
   }
   return sanitized;
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/utils/csvExport.ts` around lines 7 - 14, The escapeCsvField function
fails to treat carriage returns as a quoting trigger, which can break CSV rows;
update the quoting condition in escapeCsvField (and any related checks around
FORMULA_PREFIXES) to include '\r' alongside '\n', ',' and '"' so fields
containing '\r' are wrapped and double-quoted via the existing replace logic;
ensure the function still returns the sanitized value when no quoting is needed.

68-70: ⚠️ Potential issue | 🟠 Major

Prepend UTF-8 BOM for Japanese CSV readability in Excel.

Line 69 builds the Blob without BOM, which often causes mojibake in Windows Excel for Japanese text.

🔧 Proposed fix
 export const downloadCsv = (csvContent: string, filename: string): void => {
-  const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8' });
+  const blob = new Blob(['\uFEFF', csvContent], { type: 'text/csv;charset=utf-8' });
   const url = URL.createObjectURL(blob);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/utils/csvExport.ts` around lines 68 - 70, The CSV Blob is created without
a UTF-8 BOM which causes mojibake in Excel for Japanese text; in downloadCsv,
prepend the UTF-8 BOM (e.g. '\uFEFF' or the byte sequence 0xEF,0xBB,0xBF) to the
csvContent before constructing the Blob (the code creating `const blob = new
Blob([csvContent], ...)`), so replace that creation to include the BOM and then
continue using URL.createObjectURL(blob) and the existing filename/path logic.
🧹 Nitpick comments (1)
src/utils/csvExport.ts (1)

70-77: Harden cleanup with try/finally around temporary DOM/URL resources.

If any step after URL creation throws, cleanup at Lines 76-77 may be skipped.

♻️ Proposed refactor
 export const downloadCsv = (csvContent: string, filename: string): void => {
   const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8' });
   const url = URL.createObjectURL(blob);
   const a = document.createElement('a');
-  a.href = url;
-  a.download = filename;
-  document.body.appendChild(a);
-  a.click();
-  document.body.removeChild(a);
-  URL.revokeObjectURL(url);
+  try {
+    a.href = url;
+    a.download = filename;
+    document.body.appendChild(a);
+    a.click();
+  } finally {
+    if (a.parentNode) document.body.removeChild(a);
+    URL.revokeObjectURL(url);
+  }
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/utils/csvExport.ts` around lines 70 - 77, Wrap the DOM/URL cleanup in a
try/finally: after creating url via URL.createObjectURL(blob) and the anchor
element a (document.createElement('a')), call document.body.appendChild(a) and
perform a.click() inside a try block, and in the finally ensure
document.body.removeChild(a) (only if appended) and URL.revokeObjectURL(url) are
always executed; this guarantees the temporary anchor and object URL (url, a,
URL.createObjectURL, URL.revokeObjectURL, a.click(),
document.body.appendChild/removeChild) are cleaned up even if a.click() or other
steps throw.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src/utils/csvExport.ts`:
- Around line 7-14: The escapeCsvField function fails to treat carriage returns
as a quoting trigger, which can break CSV rows; update the quoting condition in
escapeCsvField (and any related checks around FORMULA_PREFIXES) to include '\r'
alongside '\n', ',' and '"' so fields containing '\r' are wrapped and
double-quoted via the existing replace logic; ensure the function still returns
the sanitized value when no quoting is needed.
- Around line 68-70: The CSV Blob is created without a UTF-8 BOM which causes
mojibake in Excel for Japanese text; in downloadCsv, prepend the UTF-8 BOM (e.g.
'\uFEFF' or the byte sequence 0xEF,0xBB,0xBF) to the csvContent before
constructing the Blob (the code creating `const blob = new Blob([csvContent],
...)`), so replace that creation to include the BOM and then continue using
URL.createObjectURL(blob) and the existing filename/path logic.

---

Nitpick comments:
In `@src/utils/csvExport.ts`:
- Around line 70-77: Wrap the DOM/URL cleanup in a try/finally: after creating
url via URL.createObjectURL(blob) and the anchor element a
(document.createElement('a')), call document.body.appendChild(a) and perform
a.click() inside a try block, and in the finally ensure
document.body.removeChild(a) (only if appended) and URL.revokeObjectURL(url) are
always executed; this guarantees the temporary anchor and object URL (url, a,
URL.createObjectURL, URL.revokeObjectURL, a.click(),
document.body.appendChild/removeChild) are cleaned up even if a.click() or other
steps throw.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c8649632-41ef-4a06-8bc8-ebe66cc3513f

📥 Commits

Reviewing files that changed from the base of the PR and between b7bfca9 and eeaf797.

📒 Files selected for processing (3)
  • src/pages/SaleList.tsx
  • src/utils/csvExport.test.ts
  • src/utils/csvExport.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/pages/SaleList.tsx
  • src/utils/csvExport.test.ts

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants