Skip to content

Commit 2617899

Browse files
committed
カテゴリ名の追加と削除をサポート
1 parent 12bfdf0 commit 2617899

File tree

4 files changed

+202
-38
lines changed

4 files changed

+202
-38
lines changed

README.md

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
- **パーツ検索**: カテゴリ別検索・キーワード検索
1919
- **在庫管理**: 在庫数の表示・編集(ローカルモード)
2020
- **パーツ管理**: 詳細表示・編集・追加・削除
21-
- **カテゴリ編集**: カテゴリ名編集・表示順序変更
21+
- **カテゴリ管理**: カテゴリ名編集・表示順序変更・追加・削除
2222
- **データ同期**: 編集内容のダウンロード・バックアップ
2323

2424
### 🛠 技術スタック
@@ -31,8 +31,8 @@
3131

3232
### 📊 プロジェクト統計
3333

34-
- **総コード行数**: 2,738行(TypeScript/React)
35-
- **プロジェクト全体**: 11,680行(設定・ドキュメント含む)
34+
- **総コード行数**: 2,730行(TypeScript/React)
35+
- **プロジェクト全体**: 8,384行(設定・ドキュメント含む)
3636
- **コンポーネント数**: 9個(React TSX)
3737
- **対応ブラウザ**: Chrome, Safari, Firefox, Edge
3838

@@ -90,7 +90,7 @@ npm run dev
9090
- **パーツ編集**: パーツ行の編集ボタンをクリック→情報を編集→保存
9191
- **パーツ追加**: 「パーツの追加」ボタン→必要な情報を入力→保存
9292
- **パーツ削除**: パーツ行の削除ボタン→確認ダイアログで実行
93-
- **カテゴリ編集**: 「カテゴリ編集」ボタン→名前編集・順序変更→保存
93+
- **カテゴリ管理**: 「カテゴリ編集」ボタン→名前編集・順序変更・追加・削除→保存
9494

9595
## 👤 個人運用
9696

@@ -264,16 +264,13 @@ npm run preview # ビルド結果をプレビュー
264264
- ✅ パーツ新規追加
265265
- ✅ パーツ削除
266266
- ✅ カテゴリ編集(名前・表示順序変更)
267+
- ✅ カテゴリ新規追加
268+
- ✅ カテゴリ削除
267269
- ✅ データベース同期(ダウンロード)
268270
- ✅ 環境自動判別
269271
- ✅ レスポンシブUI
270272
- ✅ ブラウザ別ダウンロード対応
271273

272-
### 🚧 未実装機能
273-
274-
-**カテゴリ追加**: eparts.dbを直接編集すれば可能
275-
-**カテゴリ削除**: 関連のあるパーツを削除する実装が必要
276-
277274
### 🎯 品質指標
278275

279276
- ✅ TypeScript型安全性

src/App.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -181,9 +181,9 @@ function App() {
181181
setIsCategoryEditModalOpen(true);
182182
};
183183

184-
const handleCategorySave = (updatedCategories: Category[]) => {
184+
const handleCategorySave = (updatedCategories: Category[], deletedCategoryIds: number[]) => {
185185
if (environment === 'local') {
186-
const success = dbManager.updateCategories(updatedCategories);
186+
const success = dbManager.updateCategories(updatedCategories, deletedCategoryIds);
187187
if (success) {
188188
setCategories(dbManager.getCategories());
189189
refreshSearchResults();

src/components/CategoryEditModal.tsx

Lines changed: 101 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import React, { useState, useEffect } from 'react';
22
import { Category } from '../types';
3-
import { X, ChevronUp, ChevronDown } from 'lucide-react';
3+
import { X, ChevronUp, ChevronDown, Plus, Trash2 } from 'lucide-react';
44

55
interface CategoryEditModalProps {
66
isOpen: boolean;
77
onClose: () => void;
8-
onSave: (categories: Category[]) => void;
8+
onSave: (categories: Category[], deletedCategoryIds: number[]) => void;
99
categories: Category[];
1010
}
1111

@@ -16,12 +16,19 @@ export const CategoryEditModal: React.FC<CategoryEditModalProps> = ({
1616
categories
1717
}) => {
1818
const [editedCategories, setEditedCategories] = useState<Category[]>([]);
19+
const [newCategoryName, setNewCategoryName] = useState<string>('');
20+
const [nextTempId, setNextTempId] = useState<number>(-1); // 一時的なIDは負の値を使用
21+
const [deletedCategoryIds, setDeletedCategoryIds] = useState<number[]>([]); // 削除対象の既存カテゴリID
1922

2023
useEffect(() => {
2124
if (isOpen) {
2225
// カテゴリをdisplay_orderで昇順ソート
2326
const sortedCategories = [...categories].sort((a, b) => a.display_order - b.display_order);
2427
setEditedCategories(sortedCategories);
28+
setNewCategoryName('');
29+
setDeletedCategoryIds([]); // 削除対象IDをリセット
30+
// 一時的なIDの初期値を設定(既存のIDと重複しないように負の値から開始)
31+
setNextTempId(-1);
2532
}
2633
}, [isOpen, categories]);
2734

@@ -49,6 +56,38 @@ export const CategoryEditModal: React.FC<CategoryEditModalProps> = ({
4956
setEditedCategories(newCategories);
5057
};
5158

59+
const addNewCategory = () => {
60+
if (!newCategoryName.trim()) {
61+
return; // 空欄の場合は何もしない
62+
}
63+
64+
const newCategory: Category = {
65+
id: nextTempId, // 一時的なID(負の値)
66+
name: newCategoryName.trim(),
67+
parent_id: undefined,
68+
display_order: 1 // 先頭に表示されるように小さい値
69+
};
70+
71+
// カテゴリ一覧の先頭に追加
72+
setEditedCategories([newCategory, ...editedCategories]);
73+
setNewCategoryName(''); // 入力欄をクリア
74+
setNextTempId(nextTempId - 1); // 次の一時的IDを準備
75+
};
76+
77+
const deleteCategory = (index: number) => {
78+
const categoryToDelete = editedCategories[index];
79+
80+
if (categoryToDelete.id >= 0) {
81+
// 既存カテゴリの場合は削除対象IDに追加
82+
setDeletedCategoryIds([...deletedCategoryIds, categoryToDelete.id]);
83+
}
84+
// 新規追加カテゴリ(負のID)の場合は単に一覧から削除
85+
86+
// 表示一覧から削除
87+
const newCategories = editedCategories.filter((_, i) => i !== index);
88+
setEditedCategories(newCategories);
89+
};
90+
5291
const handleSave = () => {
5392
// 空欄チェック
5493
const hasEmptyName = editedCategories.some(category => !category.name.trim());
@@ -63,7 +102,14 @@ export const CategoryEditModal: React.FC<CategoryEditModalProps> = ({
63102
display_order: (index + 1) * 100 // 100, 200, 300... のように設定
64103
}));
65104

66-
onSave(updatedCategories);
105+
// デバッグログ
106+
console.log('CategoryEditModal - handleSave:', {
107+
updatedCategories: updatedCategories.length,
108+
deletedCategoryIds,
109+
categories: updatedCategories.map(c => ({ id: c.id, name: c.name }))
110+
});
111+
112+
onSave(updatedCategories, deletedCategoryIds);
67113
onClose();
68114
};
69115

@@ -88,9 +134,45 @@ export const CategoryEditModal: React.FC<CategoryEditModalProps> = ({
88134
</p>
89135
</div>
90136

137+
{/* 新しいカテゴリ追加フォーム */}
138+
<div className="mb-4 p-3 bg-gray-50 rounded-lg border-2 border-dashed border-gray-300">
139+
<div className="flex items-center gap-2">
140+
<div className="flex-1">
141+
<input
142+
type="text"
143+
value={newCategoryName}
144+
onChange={(e) => setNewCategoryName(e.target.value)}
145+
placeholder="新しいカテゴリ名を入力"
146+
className="w-full px-3 py-2 text-sm border border-gray-300 rounded focus:ring-1 focus:ring-blue-500 focus:border-transparent"
147+
/>
148+
</div>
149+
<button
150+
type="button"
151+
onClick={addNewCategory}
152+
disabled={!newCategoryName.trim()}
153+
className={`px-3 py-2 rounded text-sm font-medium transition-colors flex items-center gap-1 ${
154+
newCategoryName.trim()
155+
? 'bg-green-500 hover:bg-green-600 text-white'
156+
: 'bg-gray-300 text-gray-500 cursor-not-allowed'
157+
}`}
158+
title="カテゴリを追加"
159+
>
160+
<Plus className="w-4 h-4" />
161+
追加
162+
</button>
163+
</div>
164+
</div>
165+
91166
<div className="space-y-1">
92167
{editedCategories.map((category, index) => (
93-
<div key={category.id} className="flex items-center gap-2 py-1 px-2 border border-gray-200 rounded">
168+
<div
169+
key={category.id}
170+
className={`flex items-center gap-2 py-1 px-2 border rounded ${
171+
category.id < 0
172+
? 'border-green-200 bg-green-50' // 新規追加カテゴリ
173+
: 'border-gray-200' // 既存カテゴリ
174+
}`}
175+
>
94176
<button
95177
type="button"
96178
onClick={() => moveUp(index)}
@@ -130,9 +212,22 @@ export const CategoryEditModal: React.FC<CategoryEditModalProps> = ({
130212
/>
131213
</div>
132214

133-
<div className="text-xs text-gray-400 w-12 text-right">
134-
{category.id}
215+
<div className={`text-xs w-12 text-right ${
216+
category.id < 0
217+
? 'text-green-600 font-medium' // 新規追加カテゴリ
218+
: 'text-gray-400' // 既存カテゴリ
219+
}`}>
220+
{category.id < 0 ? 'NEW' : category.id}
135221
</div>
222+
223+
<button
224+
type="button"
225+
onClick={() => deleteCategory(index)}
226+
className="p-1 text-red-500 hover:bg-red-50 rounded transition-colors"
227+
title="カテゴリを削除"
228+
>
229+
<Trash2 className="w-4 h-4" />
230+
</button>
136231
</div>
137232
))}
138233
</div>

src/utils/database.ts

Lines changed: 93 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -889,15 +889,39 @@ export class DatabaseManager {
889889
}
890890

891891
/**
892-
* カテゴリ情報を更新する
892+
* カテゴリ情報を更新する(新規追加と削除も含む)
893893
*/
894-
updateCategories(updatedCategories: Category[]): boolean {
894+
updateCategories(updatedCategories: Category[], deletedCategoryIds: number[] = []): boolean {
895895
if (this.useSampleData) {
896896
// サンプルデータモードでもカテゴリ更新をサポート
897897
if (this.sampleData) {
898-
this.sampleData.categories = updatedCategories.map(category => ({ ...category }));
898+
// 削除対象カテゴリに関連するパーツとインベントリを削除
899+
for (const deletedId of deletedCategoryIds) {
900+
// 関連するパーツを削除
901+
this.sampleData.parts = this.sampleData.parts.filter(part => part.category_id !== deletedId);
902+
}
903+
904+
// 削除対象カテゴリを現在のカテゴリリストから除外
905+
const remainingCategories = this.sampleData.categories.filter(
906+
category => !deletedCategoryIds.includes(category.id)
907+
);
908+
909+
// 新規カテゴリ(負のID)に正のIDを割り当て
910+
let maxId = Math.max(...remainingCategories.map(c => c.id), 0);
911+
const processedCategories = updatedCategories.map(category => {
912+
if (category.id < 0) {
913+
// 新規カテゴリの場合、正のIDを割り当て
914+
return { ...category, id: ++maxId };
915+
}
916+
return { ...category };
917+
});
918+
919+
this.sampleData.categories = processedCategories;
899920
this.hasChanges = true;
900-
devLog('サンプルデータのカテゴリを更新');
921+
devLog('サンプルデータのカテゴリを更新(新規追加・削除含む)', {
922+
updated: processedCategories.length,
923+
deleted: deletedCategoryIds.length
924+
});
901925
return true;
902926
}
903927
return false;
@@ -913,30 +937,78 @@ export class DatabaseManager {
913937
// トランザクション開始
914938
db.exec('BEGIN TRANSACTION');
915939

916-
// 各カテゴリを更新
917-
for (const category of updatedCategories) {
918-
const updateStmt = db.prepare(`
919-
UPDATE categories SET
920-
name = ?,
921-
display_order = ?
922-
WHERE id = ?
940+
// 削除対象カテゴリの関連データを削除
941+
for (const deletedId of deletedCategoryIds) {
942+
// 関連するインベントリを削除(parts経由)
943+
const deleteInventoryStmt = db.prepare(`
944+
DELETE FROM inventory
945+
WHERE part_id IN (
946+
SELECT id FROM parts WHERE category_id = ?
947+
)
923948
`);
924-
925-
updateStmt.bind([
926-
category.name,
927-
category.display_order,
928-
category.id
929-
]);
930-
931-
updateStmt.step();
932-
updateStmt.free();
949+
deleteInventoryStmt.bind([deletedId]);
950+
deleteInventoryStmt.step();
951+
deleteInventoryStmt.free();
952+
953+
// 関連するパーツを削除
954+
const deletePartsStmt = db.prepare(`
955+
DELETE FROM parts WHERE category_id = ?
956+
`);
957+
deletePartsStmt.bind([deletedId]);
958+
deletePartsStmt.step();
959+
deletePartsStmt.free();
960+
961+
// カテゴリを削除
962+
const deleteCategoryStmt = db.prepare(`
963+
DELETE FROM categories WHERE id = ?
964+
`);
965+
deleteCategoryStmt.bind([deletedId]);
966+
deleteCategoryStmt.step();
967+
deleteCategoryStmt.free();
968+
}
969+
970+
// 各カテゴリを処理
971+
for (const category of updatedCategories) {
972+
if (category.id < 0) {
973+
// 新規カテゴリの場合はINSERT
974+
const insertStmt = db.prepare(`
975+
INSERT INTO categories (name, parent_id, display_order)
976+
VALUES (?, ?, ?)
977+
`);
978+
979+
insertStmt.bind([
980+
category.name,
981+
category.parent_id || null,
982+
category.display_order
983+
]);
984+
985+
insertStmt.step();
986+
insertStmt.free();
987+
} else {
988+
// 既存カテゴリの場合はUPDATE
989+
const updateStmt = db.prepare(`
990+
UPDATE categories SET
991+
name = ?,
992+
display_order = ?
993+
WHERE id = ?
994+
`);
995+
996+
updateStmt.bind([
997+
category.name,
998+
category.display_order,
999+
category.id
1000+
]);
1001+
1002+
updateStmt.step();
1003+
updateStmt.free();
1004+
}
9331005
}
9341006

9351007
// トランザクション完了
9361008
db.exec('COMMIT');
9371009

9381010
this.hasChanges = true;
939-
devLog('データベースのカテゴリを更新');
1011+
devLog('データベースのカテゴリを更新(新規追加・削除含む)');
9401012
return true;
9411013
} catch (error) {
9421014
// エラー時はロールバック

0 commit comments

Comments
 (0)