195 lines
7.1 KiB
TypeScript
195 lines
7.1 KiB
TypeScript
import { useState } from 'react';
|
|
import { Plus, Edit2, Trash2, FolderOpen } from 'lucide-react';
|
|
import { useProjects } from '@/hooks/useProjects';
|
|
import { useClients } from '@/hooks/useClients';
|
|
import type { Project, CreateProjectInput, UpdateProjectInput } from '@/types';
|
|
|
|
const PRESET_COLORS = [
|
|
'#ef4444', '#f97316', '#f59e0b', '#84cc16', '#22c55e',
|
|
'#10b981', '#14b8a6', '#06b6d4', '#0ea5e9', '#3b82f6',
|
|
'#6366f1', '#8b5cf6', '#a855f7', '#d946ef', '#ec4899',
|
|
'#f43f5e', '#6b7280', '#374151',
|
|
];
|
|
|
|
export function ProjectsPage() {
|
|
const { projects, isLoading: projectsLoading, createProject, updateProject, deleteProject } = useProjects();
|
|
const { clients, isLoading: clientsLoading } = useClients();
|
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
const [editingProject, setEditingProject] = useState<Project | null>(null);
|
|
const [formData, setFormData] = useState<CreateProjectInput>({
|
|
name: '',
|
|
description: '',
|
|
color: '#3b82f6',
|
|
clientId: '',
|
|
});
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
const isLoading = projectsLoading || clientsLoading;
|
|
|
|
const handleOpenModal = (project?: Project) => {
|
|
if (project) {
|
|
setEditingProject(project);
|
|
setFormData({
|
|
name: project.name,
|
|
description: project.description || '',
|
|
color: project.color || '#3b82f6',
|
|
clientId: project.clientId,
|
|
});
|
|
} else {
|
|
setEditingProject(null);
|
|
setFormData({
|
|
name: '',
|
|
description: '',
|
|
color: '#3b82f6',
|
|
clientId: clients?.[0]?.id || '',
|
|
});
|
|
}
|
|
setError(null);
|
|
setIsModalOpen(true);
|
|
};
|
|
|
|
const handleCloseModal = () => {
|
|
setIsModalOpen(false);
|
|
setEditingProject(null);
|
|
setFormData({ name: '', description: '', color: '#3b82f6', clientId: '' });
|
|
setError(null);
|
|
};
|
|
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
setError(null);
|
|
|
|
if (!formData.name.trim()) {
|
|
setError('Project name is required');
|
|
return;
|
|
}
|
|
|
|
if (!formData.clientId) {
|
|
setError('Please select a client');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
if (editingProject) {
|
|
await updateProject.mutateAsync({
|
|
id: editingProject.id,
|
|
input: formData as UpdateProjectInput,
|
|
});
|
|
} else {
|
|
await createProject.mutateAsync(formData);
|
|
}
|
|
handleCloseModal();
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : 'Failed to save project');
|
|
}
|
|
};
|
|
|
|
const handleDelete = async (project: Project) => {
|
|
if (!confirm(`Are you sure you want to delete "${project.name}"?`)) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await deleteProject.mutateAsync(project.id);
|
|
} catch (err) {
|
|
alert(err instanceof Error ? err.message : 'Failed to delete project');
|
|
}
|
|
};
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<div className="flex items-center justify-center h-64">
|
|
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary-600"></div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (!clients?.length) {
|
|
return (
|
|
<div className="space-y-6">
|
|
<div>
|
|
<h1 className="text-2xl font-bold text-gray-900">Projects</h1>
|
|
</div>
|
|
<div className="card text-center py-12">
|
|
<FolderOpen className="h-12 w-12 text-gray-400 mx-auto mb-4" />
|
|
<p className="mt-2 text-gray-600">Please create a client first.</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<div className="flex items-center justify-between">
|
|
<h1 className="text-2xl font-bold text-gray-900">Projects</h1>
|
|
<button onClick={() => handleOpenModal()} className="btn-primary">
|
|
<Plus className="h-5 w-5 mr-2" />
|
|
Add Project
|
|
</button>
|
|
</div>
|
|
|
|
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
|
{projects?.map((project) => (
|
|
<div key={project.id} className="card">
|
|
<div className="flex items-start justify-between">
|
|
<div className="flex-1 min-w-0">
|
|
<div className="flex items-center space-x-2">
|
|
<div className="w-4 h-4 rounded-full" style={{ backgroundColor: project.color || '#6b7280' }} />
|
|
<h3 className="font-medium text-gray-900 truncate">{project.name}</h3>
|
|
</div>
|
|
<p className="mt-1 text-sm text-gray-500">{project.client.name}</p>
|
|
</div>
|
|
<div className="flex space-x-1 ml-2">
|
|
<button onClick={() => handleOpenModal(project)} className="p-1.5 text-gray-400 hover:text-gray-600 rounded">
|
|
<Edit2 className="h-4 w-4" />
|
|
</button>
|
|
<button onClick={() => handleDelete(project)} className="p-1.5 text-gray-400 hover:text-red-600 rounded">
|
|
<Trash2 className="h-4 w-4" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
{isModalOpen && (
|
|
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50">
|
|
<div className="bg-white rounded-lg shadow-xl max-w-md w-full p-6">
|
|
<h2 className="text-lg font-semibold mb-4">{editingProject ? 'Edit Project' : 'Add Project'}</h2>
|
|
{error && <div className="mb-4 p-3 bg-red-50 text-red-700 rounded-lg text-sm">{error}</div>}
|
|
<form onSubmit={handleSubmit} className="space-y-4">
|
|
<div>
|
|
<label className="label">Project Name</label>
|
|
<input type="text" value={formData.name} onChange={(e) => setFormData({ ...formData, name: e.target.value })} className="input" />
|
|
</div>
|
|
<div>
|
|
<label className="label">Client</label>
|
|
<select value={formData.clientId} onChange={(e) => setFormData({ ...formData, clientId: e.target.value })} className="input">
|
|
{clients?.map((client) => (
|
|
<option key={client.id} value={client.id}>{client.name}</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label className="label">Color</label>
|
|
<div className="flex flex-wrap gap-2">
|
|
{PRESET_COLORS.map((color) => (
|
|
<button key={color} type="button" onClick={() => setFormData({ ...formData, color })} className={`w-8 h-8 rounded-full ${formData.color === color ? 'ring-2 ring-offset-2 ring-gray-400' : ''}`} style={{ backgroundColor: color }} />
|
|
))}
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<label className="label">Description</label>
|
|
<textarea value={formData.description} onChange={(e) => setFormData({ ...formData, description: e.target.value })} className="input" rows={2} />
|
|
</div>
|
|
<div className="flex justify-end space-x-3 pt-2">
|
|
<button type="button" onClick={handleCloseModal} className="btn-secondary">Cancel</button>
|
|
<button type="submit" className="btn-primary">{editingProject ? 'Save' : 'Create'}</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
} |