tuanOts d9320683dd 🚀 Enhanced Create Project & Removed /react URL Prefix
📊 Create Project Enhancements:
- Added progressPercentage field with validation (0-100 range)
- Updated API data structure to match backend requirements
- Simplified form validation (removed required managers)
- Made team assignment optional with clear UX indicators
- Enhanced form layout with 4-column responsive design

🔧 API Data Structure:
- Streamlined to 10 essential fields: projectName, description, clientName, categoryId, priority, status, startDate, endDate, budget, progressPercentage
- Removed complex fields: managerIds, teamMemberIds, tags, createdDate, isActive
- Proper data type conversion (parseInt, parseFloat)
- Clean JSON structure for backend integration

🌐 URL Structure Improvement:
- Removed /react prefix from URLs by updating package.json homepage
- Clean URLs: localhost:3001/ instead of localhost:3001/react/
- Better SEO and user experience with standard URL patterns
- Professional URL structure following best practices

🎨 UI/UX Improvements:
- Progress percentage field with number input and % suffix
- Beautiful gradient styling for input group elements
- Enhanced form section headers with icons
- Optional field indicators with helpful placeholder text
- Responsive 3-column layout for timeline/budget/progress section

 Form Features:
- Real-time validation with clear error messages
- Loading states for API calls
- Success notifications with auto-redirect
- Fallback data handling for offline scenarios
- Professional form styling with hover effects
2025-06-01 23:30:55 +07:00

611 lines
22 KiB
JavaScript

import React, { useState, useEffect } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import { DatePicker, Select, Input, message } from 'antd';
import {
ArrowLeft,
Calendar,
Users,
DollarSign,
Target,
FileText,
CheckCircle,
TrendingUp
} from 'feather-icons-react';
import { LoadingButton } from '../../components/Loading';
import dayjs from 'dayjs';
const { Option } = Select;
const { TextArea } = Input;
const CreateProject = () => {
const navigate = useNavigate();
const [loading, setLoading] = useState(false);
const [categoriesLoading, setCategoriesLoading] = useState(true);
const [usersLoading, setUsersLoading] = useState(true);
const [formData, setFormData] = useState({
projectName: '',
description: '',
clientName: '',
categoryId: '',
priority: 'medium',
status: 'planning',
startDate: dayjs(),
endDate: dayjs().add(1, 'month'),
budget: '',
progressPercentage: 0,
managers: [],
teamMembers: [],
tags: []
});
const [errors, setErrors] = useState({});
const [categories, setCategories] = useState([]);
const [users, setUsers] = useState([]);
// Load categories from API
const loadCategories = async () => {
try {
const apiBaseUrl = process.env.REACT_APP_API_BASE_URL || '';
const response = await fetch(`${apiBaseUrl}ProjectCategories`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
}
});
if (response.ok) {
const result = await response.json();
setCategories(result.data || []);
} else {
console.error('Failed to load categories');
// Fallback to sample data
setCategories([
{ id: 1, name: 'Web Development', color: 'blue' },
{ id: 2, name: 'Mobile App', color: 'green' },
{ id: 3, name: 'Design', color: 'purple' },
{ id: 4, name: 'Marketing', color: 'orange' }
]);
}
} catch (error) {
console.error('Error loading categories:', error);
// Fallback to sample data
setCategories([
{ id: 1, name: 'Web Development', color: 'blue' },
{ id: 2, name: 'Mobile App', color: 'green' },
{ id: 3, name: 'Design', color: 'purple' },
{ id: 4, name: 'Marketing', color: 'orange' }
]);
} finally {
setCategoriesLoading(false);
}
};
// Load users from API
const loadUsers = async () => {
try {
const apiBaseUrl = process.env.REACT_APP_API_BASE_URL || '';
const response = await fetch(`${apiBaseUrl}Users`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
}
});
if (response.ok) {
const result = await response.json();
setUsers(result.data || []);
} else {
console.error('Failed to load users');
// Fallback to sample data
setUsers([
{ id: 1, fullName: 'John Smith', email: 'john@example.com' },
{ id: 2, fullName: 'Sarah Johnson', email: 'sarah@example.com' },
{ id: 3, fullName: 'Mike Wilson', email: 'mike@example.com' },
{ id: 4, fullName: 'Lisa Chen', email: 'lisa@example.com' }
]);
}
} catch (error) {
console.error('Error loading users:', error);
// Fallback to sample data
setUsers([
{ id: 1, fullName: 'John Smith', email: 'john@example.com' },
{ id: 2, fullName: 'Sarah Johnson', email: 'sarah@example.com' },
{ id: 3, fullName: 'Mike Wilson', email: 'mike@example.com' },
{ id: 4, fullName: 'Lisa Chen', email: 'lisa@example.com' }
]);
} finally {
setUsersLoading(false);
}
};
// Load data on component mount
useEffect(() => {
loadCategories();
loadUsers();
}, []);
// Avatar component with initials fallback
const UserAvatar = ({ initials, name }) => (
<div
style={{
width: '24px',
height: '24px',
borderRadius: '50%',
background: 'linear-gradient(135deg, #ff9f43, #e8890a)',
color: 'white',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '10px',
fontWeight: '600',
marginRight: '8px',
flexShrink: 0
}}
title={name}
>
{initials}
</div>
);
// Generate initials from name
const getInitials = (name) => {
return name
.split(' ')
.map(word => word.charAt(0))
.join('')
.toUpperCase()
.substring(0, 2);
};
const handleInputChange = (field, value) => {
setFormData(prev => ({
...prev,
[field]: value
}));
// Clear error when user starts typing
if (errors[field]) {
setErrors(prev => ({
...prev,
[field]: ''
}));
}
};
const validateForm = () => {
const newErrors = {};
if (!formData.projectName.trim()) {
newErrors.projectName = 'Project name is required';
}
if (!formData.description.trim()) {
newErrors.description = 'Project description is required';
}
if (!formData.categoryId) {
newErrors.categoryId = 'Please select a category';
}
if (!formData.clientName.trim()) {
newErrors.clientName = 'Client name is required';
}
if (!formData.budget.trim()) {
newErrors.budget = 'Budget is required';
} else if (isNaN(parseFloat(formData.budget))) {
newErrors.budget = 'Budget must be a valid number';
}
if (formData.progressPercentage < 0 || formData.progressPercentage > 100) {
newErrors.progressPercentage = 'Progress must be between 0 and 100';
}
if (dayjs(formData.endDate).isBefore(dayjs(formData.startDate))) {
newErrors.endDate = 'End date must be after start date';
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleSubmit = async (e) => {
e.preventDefault();
if (!validateForm()) {
return;
}
setLoading(true);
try {
const apiBaseUrl = process.env.REACT_APP_API_BASE_URL || '';
// Prepare data for API - Simple format
const projectData = {
projectName: formData.projectName,
description: formData.description,
clientName: formData.clientName,
categoryId: parseInt(formData.categoryId),
priority: formData.priority,
status: formData.status,
startDate: formData.startDate.format('YYYY-MM-DD'),
endDate: formData.endDate.format('YYYY-MM-DD'),
budget: parseFloat(formData.budget),
progressPercentage: parseInt(formData.progressPercentage)
};
console.log('Sending project data:', projectData);
const response = await fetch(`${apiBaseUrl}Projects`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify(projectData)
});
if (response.ok) {
const result = await response.json();
console.log('Project created successfully:', result);
message.success('Project created successfully!');
// Redirect to project list
setTimeout(() => {
navigate('/project-tracker');
}, 1500);
} else {
const errorData = await response.json();
console.error('API Error:', errorData);
message.error(errorData.message || 'Failed to create project. Please try again.');
}
} catch (error) {
console.error('Error creating project:', error);
message.error('Network error. Please check your connection and try again.');
} finally {
setLoading(false);
}
};
return (
<div className="page-wrapper">
<div className="content">
{/* Header */}
<div className="page-header">
<div className="add-item d-flex">
<div className="page-title">
<h4>Create New Project</h4>
<h6>Add a new project to your workspace</h6>
</div>
</div>
<div className="page-btn">
<Link to="/project-tracker" className="btn btn-secondary">
<ArrowLeft size={16} className="me-2" />
Back to Projects
</Link>
</div>
</div>
{/* Form */}
<div className="card">
<div className="card-body">
<form onSubmit={handleSubmit}>
<div className="row">
{/* Project Basic Info */}
<div className="col-lg-12">
<div className="form-group-header">
<div className="form-group-icon">
<FileText size={20} />
</div>
<h5>Project Information</h5>
</div>
</div>
<div className="col-lg-6">
<div className="mb-3">
<label className="form-label">Project Name <span className="text-danger">*</span></label>
<input
type="text"
className={`form-control ${errors.projectName ? 'is-invalid' : ''}`}
value={formData.projectName}
onChange={(e) => handleInputChange('projectName', e.target.value)}
placeholder="Enter project name"
/>
{errors.projectName && <div className="invalid-feedback">{errors.projectName}</div>}
</div>
</div>
<div className="col-lg-6">
<div className="mb-3">
<label className="form-label">Client Name <span className="text-danger">*</span></label>
<input
type="text"
className={`form-control ${errors.clientName ? 'is-invalid' : ''}`}
value={formData.clientName}
onChange={(e) => handleInputChange('clientName', e.target.value)}
placeholder="Enter client name"
/>
{errors.clientName && <div className="invalid-feedback">{errors.clientName}</div>}
</div>
</div>
<div className="col-lg-12">
<div className="mb-3">
<label className="form-label">Project Description <span className="text-danger">*</span></label>
<TextArea
rows={4}
className={`form-control ${errors.description ? 'is-invalid' : ''}`}
value={formData.description}
onChange={(e) => handleInputChange('description', e.target.value)}
placeholder="Describe your project goals, requirements, and deliverables..."
/>
{errors.description && <div className="invalid-feedback">{errors.description}</div>}
</div>
</div>
</div>
<div className="row">
{/* Project Settings */}
<div className="col-lg-12">
<div className="form-group-header">
<div className="form-group-icon">
<Target size={20} />
</div>
<h5>Project Settings</h5>
</div>
</div>
<div className="col-lg-4">
<div className="mb-3">
<label className="form-label">Category <span className="text-danger">*</span></label>
<Select
value={formData.categoryId}
onChange={(value) => handleInputChange('categoryId', value)}
className={`project-select ${errors.categoryId ? 'is-invalid' : ''}`}
placeholder="Select category"
loading={categoriesLoading}
>
{categories.map(cat => (
<Option key={cat.id} value={cat.id}>
<span className={`badge badge-${cat.color || 'primary'} me-2`}></span>
{cat.name}
</Option>
))}
</Select>
{errors.categoryId && <div className="invalid-feedback d-block">{errors.categoryId}</div>}
</div>
</div>
<div className="col-lg-4">
<div className="mb-3">
<label className="form-label">Priority</label>
<Select
value={formData.priority}
onChange={(value) => handleInputChange('priority', value)}
className="project-select"
>
<Option value="low">
<span className="badge badge-success me-2"></span>
Low Priority
</Option>
<Option value="medium">
<span className="badge badge-warning me-2"></span>
Medium Priority
</Option>
<Option value="high">
<span className="badge badge-danger me-2"></span>
High Priority
</Option>
</Select>
</div>
</div>
<div className="col-lg-4">
<div className="mb-3">
<label className="form-label">Status</label>
<Select
value={formData.status}
onChange={(value) => handleInputChange('status', value)}
className="project-select"
>
<Option value="planning">Planning</Option>
<Option value="in-progress">In Progress</Option>
<Option value="review">Review</Option>
<Option value="completed">Completed</Option>
<Option value="on-hold">On Hold</Option>
</Select>
</div>
</div>
</div>
<div className="row">
{/* Timeline, Budget & Progress */}
<div className="col-lg-12">
<div className="form-group-header">
<div className="form-group-icon">
<TrendingUp size={20} />
</div>
<h5>Timeline, Budget & Progress</h5>
</div>
</div>
<div className="col-lg-3">
<div className="mb-3">
<label className="form-label">Start Date</label>
<DatePicker
value={formData.startDate}
onChange={(date) => handleInputChange('startDate', date)}
className="form-control project-date-picker"
format="DD/MM/YYYY"
suffixIcon={<Calendar size={16} />}
/>
</div>
</div>
<div className="col-lg-3">
<div className="mb-3">
<label className="form-label">End Date</label>
<DatePicker
value={formData.endDate}
onChange={(date) => handleInputChange('endDate', date)}
className={`form-control project-date-picker ${errors.endDate ? 'is-invalid' : ''}`}
format="DD/MM/YYYY"
suffixIcon={<Calendar size={16} />}
/>
{errors.endDate && <div className="invalid-feedback d-block">{errors.endDate}</div>}
</div>
</div>
<div className="col-lg-3">
<div className="mb-3">
<label className="form-label">Budget <span className="text-danger">*</span></label>
<div className="input-group">
<span className="input-group-text">
<DollarSign size={16} />
</span>
<input
type="text"
className={`form-control ${errors.budget ? 'is-invalid' : ''}`}
value={formData.budget}
onChange={(e) => handleInputChange('budget', e.target.value)}
placeholder="0.00"
/>
</div>
{errors.budget && <div className="invalid-feedback">{errors.budget}</div>}
</div>
</div>
<div className="col-lg-3">
<div className="mb-3">
<label className="form-label">Progress Percentage</label>
<div className="input-group">
<input
type="number"
className={`form-control ${errors.progressPercentage ? 'is-invalid' : ''}`}
value={formData.progressPercentage}
onChange={(e) => handleInputChange('progressPercentage', parseInt(e.target.value) || 0)}
placeholder="0"
min="0"
max="100"
/>
<span className="input-group-text">%</span>
</div>
{errors.progressPercentage && <div className="invalid-feedback">{errors.progressPercentage}</div>}
<small className="form-text text-muted">Enter progress from 0 to 100</small>
</div>
</div>
</div>
<div className="row">
{/* Team Assignment */}
<div className="col-lg-12">
<div className="form-group-header">
<div className="form-group-icon">
<Users size={20} />
</div>
<h5>Team Assignment</h5>
</div>
</div>
<div className="col-lg-6">
<div className="mb-3">
<label className="form-label">Project Manager</label>
<Select
mode="multiple"
value={formData.managers}
onChange={(value) => handleInputChange('managers', value)}
className="project-select"
placeholder="Select project manager(s) (Optional)"
optionLabelProp="label"
loading={usersLoading}
allowClear
>
{users.map(user => (
<Option key={user.id} value={user.id} label={user.fullName}>
<div style={{ display: 'flex', alignItems: 'center' }}>
<UserAvatar initials={getInitials(user.fullName)} name={user.fullName} />
<div>
<div>{user.fullName}</div>
<small style={{ color: '#666' }}>{user.email}</small>
</div>
</div>
</Option>
))}
</Select>
<small className="form-text text-muted">You can assign managers later</small>
</div>
</div>
<div className="col-lg-6">
<div className="mb-3">
<label className="form-label">Team Members</label>
<Select
mode="multiple"
value={formData.teamMembers}
onChange={(value) => handleInputChange('teamMembers', value)}
className="project-select"
placeholder="Select team members (Optional)"
optionLabelProp="label"
loading={usersLoading}
allowClear
>
{users.map(user => (
<Option key={user.id} value={user.id} label={user.fullName}>
<div style={{ display: 'flex', alignItems: 'center' }}>
<UserAvatar initials={getInitials(user.fullName)} name={user.fullName} />
<div>
<div>{user.fullName}</div>
<small style={{ color: '#666' }}>{user.email}</small>
</div>
</div>
</Option>
))}
</Select>
<small className="form-text text-muted">You can assign team members later</small>
</div>
</div>
</div>
{/* Submit Buttons */}
<div className="row">
<div className="col-lg-12">
<div className="btn-addproduct mb-4 d-flex align-items-center gap-3">
<LoadingButton
type="submit"
variant="primary"
size="medium"
loading={loading}
loadingText="Creating Project..."
className="create-project-btn"
icon={<CheckCircle size={16} />}
>
Create Project
</LoadingButton>
<Link to="/project-tracker" className="btn btn-cancel btn-cancel-project">
Cancel
</Link>
</div>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
);
};
export default CreateProject;