- Added translation support for task-related strings in ProjectTaskForm and ProjectTasksSection components. - Integrated translation for navigation items in the Navigation component. - Created ProjectCalendarWidget component with Polish translations for project statuses and deadlines. - Developed Tooltip component for enhanced user experience with tooltips. - Established a field change history logging system in the database with associated queries. - Enhanced task update logging to include translated status and priority changes. - Introduced server-side translations for system messages to improve localization.
442 lines
12 KiB
JavaScript
442 lines
12 KiB
JavaScript
"use client";
|
||
|
||
import { useState, useEffect } from "react";
|
||
import { useRouter } from "next/navigation";
|
||
import { Card, CardHeader, CardContent } from "@/components/ui/Card";
|
||
import Button from "@/components/ui/Button";
|
||
import { Input } from "@/components/ui/Input";
|
||
import { formatDateForInput } from "@/lib/utils";
|
||
import { useTranslation } from "@/lib/i18n";
|
||
|
||
export default function ProjectForm({ initialData = null }) {
|
||
const { t } = useTranslation();
|
||
const [form, setForm] = useState({
|
||
contract_id: "",
|
||
project_name: "",
|
||
address: "",
|
||
plot: "",
|
||
district: "",
|
||
unit: "",
|
||
city: "",
|
||
investment_number: "",
|
||
finish_date: "",
|
||
wp: "",
|
||
contact: "",
|
||
notes: "",
|
||
coordinates: "",
|
||
project_type: "design",
|
||
assigned_to: "",
|
||
});
|
||
|
||
const [contracts, setContracts] = useState([]);
|
||
const [users, setUsers] = useState([]);
|
||
const [loading, setLoading] = useState(false);
|
||
const router = useRouter();
|
||
const isEdit = !!initialData;
|
||
|
||
useEffect(() => {
|
||
// Fetch contracts
|
||
fetch("/api/contracts")
|
||
.then((res) => res.json())
|
||
.then(setContracts);
|
||
|
||
// Fetch users for assignment
|
||
fetch("/api/projects/users")
|
||
.then((res) => res.json())
|
||
.then(setUsers);
|
||
}, []);
|
||
|
||
// Update form state when initialData changes (for edit mode)
|
||
useEffect(() => {
|
||
if (initialData) {
|
||
setForm({
|
||
contract_id: "",
|
||
project_name: "",
|
||
address: "",
|
||
plot: "",
|
||
district: "",
|
||
unit: "",
|
||
city: "",
|
||
investment_number: "",
|
||
finish_date: "",
|
||
wp: "",
|
||
contact: "",
|
||
notes: "",
|
||
coordinates: "",
|
||
project_type: "design",
|
||
assigned_to: "",
|
||
...initialData,
|
||
// Ensure these defaults are preserved if not in initialData
|
||
project_type: initialData.project_type || "design",
|
||
assigned_to: initialData.assigned_to || "",
|
||
// Format finish_date for input if it exists
|
||
finish_date: initialData.finish_date
|
||
? formatDateForInput(initialData.finish_date)
|
||
: "",
|
||
});
|
||
}
|
||
}, [initialData]);
|
||
|
||
function handleChange(e) {
|
||
setForm({ ...form, [e.target.name]: e.target.value });
|
||
}
|
||
|
||
async function handleSubmit(e) {
|
||
e.preventDefault();
|
||
setLoading(true);
|
||
|
||
try {
|
||
const res = await fetch(
|
||
isEdit ? `/api/projects/${initialData.project_id}` : "/api/projects",
|
||
{
|
||
method: isEdit ? "PUT" : "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify(form),
|
||
}
|
||
);
|
||
|
||
if (res.ok) {
|
||
const project = await res.json();
|
||
if (isEdit) {
|
||
router.push(`/projects/${project.project_id}`);
|
||
} else {
|
||
router.push("/projects");
|
||
}
|
||
} else {
|
||
alert(t('projects.saveError'));
|
||
}
|
||
} catch (error) {
|
||
console.error("Error saving project:", error);
|
||
alert(t('projects.saveError'));
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
}
|
||
return (
|
||
<Card>
|
||
<CardHeader>
|
||
<h2 className="text-xl font-semibold text-gray-900">
|
||
{isEdit ? t('projects.editProjectDetails') : t('projects.projectDetails')}
|
||
</h2>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<form onSubmit={handleSubmit} className="space-y-6">
|
||
{/* Contract and Project Type Section */}
|
||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||
{t('projects.contract')} <span className="text-red-500">*</span>
|
||
</label>
|
||
<select
|
||
name="contract_id"
|
||
value={form.contract_id || ""}
|
||
onChange={handleChange}
|
||
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
|
||
required
|
||
>
|
||
<option value="">{t('projects.selectContract')}</option>
|
||
{contracts.map((contract) => (
|
||
<option
|
||
key={contract.contract_id}
|
||
value={contract.contract_id}
|
||
>
|
||
{contract.contract_number} – {contract.contract_name}
|
||
</option>
|
||
))}
|
||
</select>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||
{t('projects.type')} <span className="text-red-500">*</span>
|
||
</label>
|
||
<select
|
||
name="project_type"
|
||
value={form.project_type}
|
||
onChange={handleChange}
|
||
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
|
||
required
|
||
>
|
||
<option value="design">{t('projectType.design')}</option>
|
||
<option value="construction">{t('projectType.construction')}</option>
|
||
<option value="design+construction">
|
||
{t('projectType.design+construction')}
|
||
</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||
{t('projects.assignedTo')}
|
||
</label>
|
||
<select
|
||
name="assigned_to"
|
||
value={form.assigned_to || ""}
|
||
onChange={handleChange}
|
||
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
|
||
>
|
||
<option value="">{t('projects.unassigned')}</option>
|
||
{users.map((user) => (
|
||
<option key={user.id} value={user.id}>
|
||
{user.name} ({user.username})
|
||
</option>
|
||
))}
|
||
</select>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Basic Information Section */}
|
||
<div className="border-t pt-6">
|
||
<h3 className="text-lg font-medium text-gray-900 mb-4">
|
||
{t('projects.basicInformation')}
|
||
</h3>
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||
<div className="md:col-span-2">
|
||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||
{t('projects.projectName')} <span className="text-red-500">*</span>
|
||
</label>
|
||
<Input
|
||
type="text"
|
||
name="project_name"
|
||
value={form.project_name || ""}
|
||
onChange={handleChange}
|
||
placeholder={t('projects.enterProjectName')}
|
||
required
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||
{t('projects.city')}
|
||
</label>
|
||
<Input
|
||
type="text"
|
||
name="city"
|
||
value={form.city || ""}
|
||
onChange={handleChange}
|
||
placeholder={t('projects.enterCity')}
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||
{t('projects.address')}
|
||
</label>
|
||
<Input
|
||
type="text"
|
||
name="address"
|
||
value={form.address || ""}
|
||
onChange={handleChange}
|
||
placeholder={t('projects.enterAddress')}
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||
{t('projects.plot')}
|
||
</label>
|
||
<Input
|
||
type="text"
|
||
name="plot"
|
||
value={form.plot || ""}
|
||
onChange={handleChange}
|
||
placeholder={t('projects.enterPlotNumber')}
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||
{t('projects.district')}
|
||
</label>
|
||
<Input
|
||
type="text"
|
||
name="district"
|
||
value={form.district || ""}
|
||
onChange={handleChange}
|
||
placeholder={t('projects.enterDistrict')}
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||
{t('projects.unit')}
|
||
</label>
|
||
<Input
|
||
type="text"
|
||
name="unit"
|
||
value={form.unit || ""}
|
||
onChange={handleChange}
|
||
placeholder={t('projects.enterUnit')}
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||
{t('projects.finishDate')}
|
||
</label>
|
||
<Input
|
||
type="date"
|
||
name="finish_date"
|
||
value={formatDateForInput(form.finish_date)}
|
||
onChange={handleChange}
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Additional Information Section */}
|
||
<div className="border-t pt-6">
|
||
<h3 className="text-lg font-medium text-gray-900 mb-4">
|
||
{t('projects.additionalInfo')}
|
||
</h3>
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||
{t('projects.investmentNumber')}
|
||
</label>
|
||
<Input
|
||
type="text"
|
||
name="investment_number"
|
||
value={form.investment_number || ""}
|
||
onChange={handleChange}
|
||
placeholder={t('projects.placeholders.investmentNumber')}
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||
WP
|
||
</label>
|
||
<Input
|
||
type="text"
|
||
name="wp"
|
||
value={form.wp || ""}
|
||
onChange={handleChange}
|
||
placeholder={t('projects.placeholders.wp')}
|
||
/>
|
||
</div>
|
||
|
||
<div className="md:col-span-2">
|
||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||
{t('projects.contact')}
|
||
</label>
|
||
<Input
|
||
type="text"
|
||
name="contact"
|
||
value={form.contact || ""}
|
||
onChange={handleChange}
|
||
placeholder={t('projects.placeholders.contact')}
|
||
/>
|
||
</div>
|
||
|
||
<div className="md:col-span-2">
|
||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||
{t('projects.coordinates')}
|
||
</label>
|
||
<Input
|
||
type="text"
|
||
name="coordinates"
|
||
value={form.coordinates || ""}
|
||
onChange={handleChange}
|
||
placeholder={t('projects.placeholders.coordinates')}
|
||
/>
|
||
</div>
|
||
|
||
<div className="md:col-span-2">
|
||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||
{t('projects.notes')}
|
||
</label>
|
||
<textarea
|
||
name="notes"
|
||
value={form.notes || ""}
|
||
onChange={handleChange}
|
||
rows={4}
|
||
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
|
||
placeholder={t('projects.placeholders.notes')}
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Form Actions */}
|
||
<div className="border-t pt-6 flex items-center justify-end gap-4">
|
||
<Button
|
||
type="button"
|
||
variant="outline"
|
||
onClick={() => router.back()}
|
||
disabled={loading}
|
||
>
|
||
{t('common.cancel')}
|
||
</Button>
|
||
<Button type="submit" variant="primary" disabled={loading}>
|
||
{loading ? (
|
||
<>
|
||
<svg
|
||
className="animate-spin -ml-1 mr-3 h-4 w-4 text-white"
|
||
xmlns="http://www.w3.org/2000/svg"
|
||
fill="none"
|
||
viewBox="0 0 24 24"
|
||
>
|
||
<circle
|
||
className="opacity-25"
|
||
cx="12"
|
||
cy="12"
|
||
r="10"
|
||
stroke="currentColor"
|
||
strokeWidth="4"
|
||
></circle>
|
||
<path
|
||
className="opacity-75"
|
||
fill="currentColor"
|
||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||
></path>
|
||
</svg>
|
||
{isEdit ? t('projects.updating') : t('projects.creating')}
|
||
</>
|
||
) : (
|
||
<>
|
||
{isEdit ? (
|
||
<>
|
||
<svg
|
||
className="w-4 h-4 mr-2"
|
||
fill="none"
|
||
stroke="currentColor"
|
||
viewBox="0 0 24 24"
|
||
>
|
||
<path
|
||
strokeLinecap="round"
|
||
strokeLinejoin="round"
|
||
strokeWidth={2}
|
||
d="M5 13l4 4L19 7"
|
||
/>
|
||
</svg>
|
||
{t('projects.updateProject')}
|
||
</>
|
||
) : (
|
||
<>
|
||
<svg
|
||
className="w-4 h-4 mr-2"
|
||
fill="none"
|
||
stroke="currentColor"
|
||
viewBox="0 0 24 24"
|
||
>
|
||
<path
|
||
strokeLinecap="round"
|
||
strokeLinejoin="round"
|
||
strokeWidth={2}
|
||
d="M12 4v16m8-8H4"
|
||
/>
|
||
</svg>
|
||
{t('projects.createProject')}
|
||
</>
|
||
)}
|
||
</>
|
||
)}
|
||
</Button>
|
||
</div>
|
||
</form>
|
||
</CardContent>
|
||
</Card>
|
||
);
|
||
}
|