Skip to content

Commit 74ec347

Browse files
committed
カテゴリ名と順序の変更をサポート
1 parent 8c719ae commit 74ec347

File tree

4 files changed

+263
-2
lines changed

4 files changed

+263
-2
lines changed

src/App.tsx

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { PartsTable } from './components/PartsTable';
44
import { PartDetailModal } from './components/PartDetailModal';
55
import { PartEditModal } from './components/PartEditModal';
66
import { PartAddModal } from './components/PartAddModal';
7+
import { CategoryEditModal } from './components/CategoryEditModal';
78
import { ActionButtons } from './components/ActionButtons';
89
import { DatabaseManager } from './utils/database';
910
import { detectEnvironment } from './utils/environment';
@@ -24,6 +25,7 @@ function App() {
2425
const [isDetailModalOpen, setIsDetailModalOpen] = useState(false);
2526
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
2627
const [isAddModalOpen, setIsAddModalOpen] = useState(false);
28+
const [isCategoryEditModalOpen, setIsCategoryEditModalOpen] = useState(false);
2729
const [hasChanges, setHasChanges] = useState(false);
2830

2931
useEffect(() => {
@@ -175,6 +177,24 @@ function App() {
175177
setSelectedCategoryId(null);
176178
};
177179

180+
const handleCategoryEdit = () => {
181+
setIsCategoryEditModalOpen(true);
182+
};
183+
184+
const handleCategorySave = (updatedCategories: Category[]) => {
185+
if (environment === 'local') {
186+
const success = dbManager.updateCategories(updatedCategories);
187+
if (success) {
188+
setCategories(dbManager.getCategories());
189+
refreshSearchResults();
190+
setHasChanges(dbManager.getHasChanges());
191+
setIsCategoryEditModalOpen(false);
192+
} else {
193+
alert('カテゴリの更新に失敗しました。');
194+
}
195+
}
196+
};
197+
178198
if (isLoading) {
179199
return (
180200
<div className="min-h-screen bg-gray-100 flex items-center justify-center">
@@ -224,6 +244,7 @@ function App() {
224244
onUndo={handleUndo}
225245
onSync={handleSync}
226246
onAddPart={handleAddPart}
247+
onCategoryEdit={handleCategoryEdit}
227248
/>
228249

229250
{/* Search Section */}
@@ -301,6 +322,14 @@ function App() {
301322
onAdd={handlePartAdd}
302323
categories={categories}
303324
/>
325+
326+
{/* Category Edit Modal */}
327+
<CategoryEditModal
328+
isOpen={isCategoryEditModalOpen}
329+
onClose={() => setIsCategoryEditModalOpen(false)}
330+
onSave={handleCategorySave}
331+
categories={categories}
332+
/>
304333
</div>
305334
</div>
306335
);

src/components/ActionButtons.tsx

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,23 @@
11
import React from 'react';
22
import { Environment } from '../types';
3-
import { Undo, Download, Plus } from 'lucide-react';
3+
import { Undo, Download, Plus, Edit3 } from 'lucide-react';
44

55
interface ActionButtonsProps {
66
environment: Environment;
77
hasChanges: boolean;
88
onUndo: () => void;
99
onSync: () => void;
1010
onAddPart: () => void;
11+
onCategoryEdit: () => void;
1112
}
1213

1314
export const ActionButtons: React.FC<ActionButtonsProps> = ({
1415
environment,
1516
hasChanges,
1617
onUndo,
1718
onSync,
18-
onAddPart
19+
onAddPart,
20+
onCategoryEdit
1921
}) => {
2022
if (environment === 'remote') {
2123
return null;
@@ -32,6 +34,14 @@ export const ActionButtons: React.FC<ActionButtonsProps> = ({
3234
パーツの追加
3335
</button>
3436

37+
<button
38+
onClick={onCategoryEdit}
39+
className="flex items-center gap-2 px-4 py-2 bg-purple-500 hover:bg-purple-600 text-white rounded-lg font-medium transition-colors"
40+
>
41+
<Edit3 className="w-4 h-4" />
42+
カテゴリ編集
43+
</button>
44+
3545
{hasChanges && (
3646
<>
3747
<button

src/components/CategoryEditModal.tsx

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
import React, { useState, useEffect } from 'react';
2+
import { Category } from '../types';
3+
import { X, ChevronUp, ChevronDown } from 'lucide-react';
4+
5+
interface CategoryEditModalProps {
6+
isOpen: boolean;
7+
onClose: () => void;
8+
onSave: (categories: Category[]) => void;
9+
categories: Category[];
10+
}
11+
12+
export const CategoryEditModal: React.FC<CategoryEditModalProps> = ({
13+
isOpen,
14+
onClose,
15+
onSave,
16+
categories
17+
}) => {
18+
const [editedCategories, setEditedCategories] = useState<Category[]>([]);
19+
20+
useEffect(() => {
21+
if (isOpen) {
22+
// カテゴリをdisplay_orderで昇順ソート
23+
const sortedCategories = [...categories].sort((a, b) => a.display_order - b.display_order);
24+
setEditedCategories(sortedCategories);
25+
}
26+
}, [isOpen, categories]);
27+
28+
if (!isOpen) return null;
29+
30+
const moveUp = (index: number) => {
31+
if (index === 0) return; // 一番上の場合は何もしない
32+
33+
const newCategories = [...editedCategories];
34+
[newCategories[index], newCategories[index - 1]] = [newCategories[index - 1], newCategories[index]];
35+
setEditedCategories(newCategories);
36+
};
37+
38+
const moveDown = (index: number) => {
39+
if (index === editedCategories.length - 1) return; // 一番下の場合は何もしない
40+
41+
const newCategories = [...editedCategories];
42+
[newCategories[index], newCategories[index + 1]] = [newCategories[index + 1], newCategories[index]];
43+
setEditedCategories(newCategories);
44+
};
45+
46+
const handleNameChange = (index: number, newName: string) => {
47+
const newCategories = [...editedCategories];
48+
newCategories[index] = { ...newCategories[index], name: newName };
49+
setEditedCategories(newCategories);
50+
};
51+
52+
const handleSave = () => {
53+
// 空欄チェック
54+
const hasEmptyName = editedCategories.some(category => !category.name.trim());
55+
if (hasEmptyName) {
56+
alert('カテゴリ名が空欄のものがあります。すべてのカテゴリ名を入力してください。');
57+
return;
58+
}
59+
60+
// display_orderを新しい順序で更新
61+
const updatedCategories = editedCategories.map((category, index) => ({
62+
...category,
63+
display_order: (index + 1) * 100 // 100, 200, 300... のように設定
64+
}));
65+
66+
onSave(updatedCategories);
67+
onClose();
68+
};
69+
70+
return (
71+
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50">
72+
<div className="bg-white rounded-lg max-w-2xl w-full max-h-[90vh] overflow-y-auto">
73+
<div className="flex items-center justify-between p-4 border-b">
74+
<h2 className="text-lg font-semibold text-gray-900">カテゴリ編集</h2>
75+
<button
76+
type="button"
77+
onClick={onClose}
78+
className="text-gray-400 hover:text-gray-600"
79+
>
80+
<X className="w-5 h-5" />
81+
</button>
82+
</div>
83+
84+
<div className="p-4">
85+
<div className="mb-3">
86+
<p className="text-sm text-gray-600">
87+
カテゴリの名前を編集し、上下ボタンで表示順序を変更できます。
88+
</p>
89+
</div>
90+
91+
<div className="space-y-1">
92+
{editedCategories.map((category, index) => (
93+
<div key={category.id} className="flex items-center gap-2 py-1 px-2 border border-gray-200 rounded">
94+
<button
95+
type="button"
96+
onClick={() => moveUp(index)}
97+
disabled={index === 0}
98+
className={`p-1 rounded ${
99+
index === 0
100+
? 'text-gray-300 cursor-not-allowed'
101+
: 'text-gray-600 hover:bg-gray-100'
102+
}`}
103+
title="上に移動"
104+
>
105+
<ChevronUp className="w-3 h-3" />
106+
</button>
107+
108+
<button
109+
type="button"
110+
onClick={() => moveDown(index)}
111+
disabled={index === editedCategories.length - 1}
112+
className={`p-1 rounded ${
113+
index === editedCategories.length - 1
114+
? 'text-gray-300 cursor-not-allowed'
115+
: 'text-gray-600 hover:bg-gray-100'
116+
}`}
117+
title="下に移動"
118+
>
119+
<ChevronDown className="w-3 h-3" />
120+
</button>
121+
122+
<div className="flex-1">
123+
<input
124+
type="text"
125+
value={category.name}
126+
onChange={(e) => handleNameChange(index, e.target.value)}
127+
required
128+
className="w-full px-2 py-1 text-sm border border-gray-300 rounded focus:ring-1 focus:ring-blue-500 focus:border-transparent"
129+
placeholder="カテゴリ名"
130+
/>
131+
</div>
132+
133+
<div className="text-xs text-gray-400 w-12 text-right">
134+
{category.id}
135+
</div>
136+
</div>
137+
))}
138+
</div>
139+
</div>
140+
141+
<div className="flex justify-end gap-3 p-4 border-t bg-gray-50">
142+
<button
143+
type="button"
144+
onClick={onClose}
145+
className="px-3 py-1.5 bg-gray-300 hover:bg-gray-400 text-gray-700 rounded text-sm font-medium transition-colors"
146+
>
147+
キャンセル
148+
</button>
149+
<button
150+
type="button"
151+
onClick={handleSave}
152+
className="px-3 py-1.5 bg-blue-500 hover:bg-blue-600 text-white rounded text-sm font-medium transition-colors"
153+
>
154+
変更
155+
</button>
156+
</div>
157+
</div>
158+
</div>
159+
);
160+
};

src/utils/database.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -888,6 +888,68 @@ export class DatabaseManager {
888888
}
889889
}
890890

891+
/**
892+
* カテゴリ情報を更新する
893+
*/
894+
updateCategories(updatedCategories: Category[]): boolean {
895+
if (this.useSampleData) {
896+
// サンプルデータモードでもカテゴリ更新をサポート
897+
if (this.sampleData) {
898+
this.sampleData.categories = updatedCategories.map(category => ({ ...category }));
899+
this.hasChanges = true;
900+
devLog('サンプルデータのカテゴリを更新');
901+
return true;
902+
}
903+
return false;
904+
}
905+
906+
const db = this.db;
907+
if (!db) {
908+
console.warn('データベースが初期化されていません');
909+
return false;
910+
}
911+
912+
try {
913+
// トランザクション開始
914+
db.exec('BEGIN TRANSACTION');
915+
916+
// 各カテゴリを更新
917+
for (const category of updatedCategories) {
918+
const updateStmt = db.prepare(`
919+
UPDATE categories SET
920+
name = ?,
921+
display_order = ?
922+
WHERE id = ?
923+
`);
924+
925+
updateStmt.bind([
926+
category.name,
927+
category.display_order,
928+
category.id
929+
]);
930+
931+
updateStmt.step();
932+
updateStmt.free();
933+
}
934+
935+
// トランザクション完了
936+
db.exec('COMMIT');
937+
938+
this.hasChanges = true;
939+
devLog('データベースのカテゴリを更新');
940+
return true;
941+
} catch (error) {
942+
// エラー時はロールバック
943+
try {
944+
db.exec('ROLLBACK');
945+
} catch (rollbackError) {
946+
console.error('ロールバックエラー:', rollbackError);
947+
}
948+
console.error('カテゴリ更新エラー:', error);
949+
return false;
950+
}
951+
}
952+
891953
/**
892954
* サンプルデータモードかどうかを取得
893955
*/

0 commit comments

Comments
 (0)