Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions migrations/004-unique-project-prefix.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
-- Up
CREATE UNIQUE INDEX idx_project_prefix ON Project(Prefix COLLATE NOCASE);

-- Down
DROP INDEX idx_project_prefix;
21 changes: 19 additions & 2 deletions src/main/repositories/ProjectRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,36 @@ export class ProjectRepository {
return await db.all('SELECT * FROM Project ORDER BY Title ASC');
}

async getByPrefix(prefix: string): Promise<Project | undefined> {
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]
);
}
}
Expand Down
14 changes: 12 additions & 2 deletions src/renderer/components/shared/ProjectModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,11 @@ const ProjectModal: React.FC<ProjectModalProps> = ({
const [ownerId, setOwnerId] = useState<number | ''>('');
const [taskSourceId, setTaskSourceId] = useState<number | ''>('');
const [isSubmitting, setIsSubmitting] = useState(false);
const [error, setError] = useState<string | null>(null);

useEffect(() => {
if (show) {
setError(null);
if (initialData) {
setTitle(initialData.Title || '');
setPrefix(initialData.Prefix || '');
Expand All @@ -59,6 +61,7 @@ const ProjectModal: React.FC<ProjectModalProps> = ({
if (!title.trim() || !prefix.trim()) return;

setIsSubmitting(true);
setError(null);
try {
await onSave({
title,
Expand All @@ -69,9 +72,10 @@ const ProjectModal: React.FC<ProjectModalProps> = ({
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);
}
Expand All @@ -93,6 +97,12 @@ const ProjectModal: React.FC<ProjectModalProps> = ({
</>
}
>
{error && (
<div className="alert alert-danger py-2 mb-3" role="alert">
<i className="bi bi-exclamation-triangle-fill me-2"></i>
{error}
</div>
)}
<div className="row g-3">
<div className="col-md-9 mb-3">
<label htmlFor="projectTitle" className="form-label">Project Title</label>
Expand Down
Loading