From 8c1312527bc8160c9085e800750f01c36935e340 Mon Sep 17 00:00:00 2001 From: Mark Benson <41453195+thealternator89@users.noreply.github.com> Date: Sun, 19 Apr 2026 12:46:31 +1200 Subject: [PATCH] feat: enforce unique project prefixes in repository, UI, and database --- migrations/004-unique-project-prefix.sql | 5 +++++ src/main/repositories/ProjectRepository.ts | 21 +++++++++++++++++-- .../components/shared/ProjectModal.tsx | 14 +++++++++++-- 3 files changed, 36 insertions(+), 4 deletions(-) create mode 100644 migrations/004-unique-project-prefix.sql diff --git a/migrations/004-unique-project-prefix.sql b/migrations/004-unique-project-prefix.sql new file mode 100644 index 0000000..7e07fab --- /dev/null +++ b/migrations/004-unique-project-prefix.sql @@ -0,0 +1,5 @@ +-- Up +CREATE UNIQUE INDEX idx_project_prefix ON Project(Prefix COLLATE NOCASE); + +-- Down +DROP INDEX idx_project_prefix; diff --git a/src/main/repositories/ProjectRepository.ts b/src/main/repositories/ProjectRepository.ts index 0db897c..adba9f2 100644 --- a/src/main/repositories/ProjectRepository.ts +++ b/src/main/repositories/ProjectRepository.ts @@ -12,19 +12,36 @@ export class ProjectRepository { return await db.all('SELECT * FROM Project ORDER BY Title ASC'); } + async getByPrefix(prefix: string): Promise { + const db = getDatabase(); + return await db.get('SELECT * FROM Project WHERE UPPER(Prefix) = UPPER(?)', [prefix]); + } + async create(project: { title: string; prefix: string; startDate?: string; dueDate?: string; ownerId?: number; taskSourceId?: number }) { + const normalizedPrefix = project.prefix.toUpperCase(); + const existing = await this.getByPrefix(normalizedPrefix); + if (existing) { + throw new Error(`Project with prefix "${normalizedPrefix}" already exists.`); + } + const db = getDatabase(); return await db.run( 'INSERT INTO Project (Title, Prefix, StartDate, DueDate, OwnerId, TaskSourceId) VALUES (?, ?, ?, ?, ?, ?)', - [project.title, project.prefix, project.startDate || null, project.dueDate || null, project.ownerId || null, project.taskSourceId || null] + [project.title, normalizedPrefix, project.startDate || null, project.dueDate || null, project.ownerId || null, project.taskSourceId || null] ); } async update(project: { id: number; title: string; prefix: string; startDate?: string; dueDate?: string; ownerId?: number; taskSourceId?: number }) { + const normalizedPrefix = project.prefix.toUpperCase(); + const existing = await this.getByPrefix(normalizedPrefix); + if (existing && existing.Id !== project.id) { + throw new Error(`Project with prefix "${normalizedPrefix}" already exists.`); + } + const db = getDatabase(); return await db.run( 'UPDATE Project SET Title = ?, Prefix = ?, StartDate = ?, DueDate = ?, OwnerId = ?, TaskSourceId = ? WHERE Id = ?', - [project.title, project.prefix, project.startDate || null, project.dueDate || null, project.ownerId || null, project.taskSourceId || null, project.id] + [project.title, normalizedPrefix, project.startDate || null, project.dueDate || null, project.ownerId || null, project.taskSourceId || null, project.id] ); } } diff --git a/src/renderer/components/shared/ProjectModal.tsx b/src/renderer/components/shared/ProjectModal.tsx index 18758e9..cb056bf 100644 --- a/src/renderer/components/shared/ProjectModal.tsx +++ b/src/renderer/components/shared/ProjectModal.tsx @@ -32,9 +32,11 @@ const ProjectModal: React.FC = ({ const [ownerId, setOwnerId] = useState(''); const [taskSourceId, setTaskSourceId] = useState(''); const [isSubmitting, setIsSubmitting] = useState(false); + const [error, setError] = useState(null); useEffect(() => { if (show) { + setError(null); if (initialData) { setTitle(initialData.Title || ''); setPrefix(initialData.Prefix || ''); @@ -59,6 +61,7 @@ const ProjectModal: React.FC = ({ if (!title.trim() || !prefix.trim()) return; setIsSubmitting(true); + setError(null); try { await onSave({ title, @@ -69,9 +72,10 @@ const ProjectModal: React.FC = ({ taskSourceId: taskSourceId === '' ? undefined : taskSourceId, }); onClose(); - } catch (error) { + } catch (error: any) { console.error('Failed to save project:', error); - alert('Error saving project. Please try again.'); + const message = error.message || 'Error saving project. Please try again.'; + setError(message.replace('Error invoking remote method \'create-project\': Error: ', '').replace('Error invoking remote method \'update-project\': Error: ', '')); } finally { setIsSubmitting(false); } @@ -93,6 +97,12 @@ const ProjectModal: React.FC = ({ } > + {error && ( +
+ + {error} +
+ )}