- Implemented ContactForm component for creating and editing contacts. - Added ProjectContactSelector component to manage project-specific contacts. - Updated ProjectForm to include ProjectContactSelector for associating contacts with projects. - Enhanced Card component with a new CardTitle subcomponent for better structure. - Updated Navigation to include a link to the contacts page. - Added translations for contact-related terms in the i18n module. - Initialized contacts database schema and created necessary tables for contact management. - Developed queries for CRUD operations on contacts, including linking and unlinking contacts to projects. - Created a test script to validate contact queries against the database.
486 lines
13 KiB
JavaScript
486 lines
13 KiB
JavaScript
"use client";
|
||
|
||
import { useState, useEffect, forwardRef, useImperativeHandle } from "react";
|
||
import { useRouter } from "next/navigation";
|
||
import { useSession } from "next-auth/react";
|
||
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";
|
||
import ProjectContactSelector from "@/components/ProjectContactSelector";
|
||
|
||
const ProjectForm = forwardRef(function ProjectForm({ initialData = null }, ref) {
|
||
const { t } = useTranslation();
|
||
const { data: session } = useSession();
|
||
const [form, setForm] = useState({
|
||
contract_id: "",
|
||
project_name: "",
|
||
address: "",
|
||
plot: "",
|
||
district: "",
|
||
unit: "",
|
||
city: "",
|
||
investment_number: "",
|
||
finish_date: "",
|
||
completion_date: "",
|
||
wp: "",
|
||
contact: "",
|
||
notes: "",
|
||
coordinates: "",
|
||
wartosc_zlecenia: "",
|
||
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: "",
|
||
completion_date: "",
|
||
wp: "",
|
||
contact: "",
|
||
notes: "",
|
||
coordinates: "",
|
||
wartosc_zlecenia: "",
|
||
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 || "",
|
||
wartosc_zlecenia: initialData.wartosc_zlecenia || "",
|
||
// Format dates for input if they exist
|
||
finish_date: initialData.finish_date
|
||
? formatDateForInput(initialData.finish_date)
|
||
: "",
|
||
completion_date: initialData.completion_date
|
||
? formatDateForInput(initialData.completion_date)
|
||
: "",
|
||
});
|
||
}
|
||
}, [initialData]);
|
||
|
||
function handleChange(e) {
|
||
setForm({ ...form, [e.target.name]: e.target.value });
|
||
}
|
||
|
||
async function saveProject() {
|
||
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);
|
||
}
|
||
}
|
||
|
||
async function handleSubmit(e) {
|
||
e.preventDefault();
|
||
await saveProject();
|
||
}
|
||
|
||
// Expose save function to parent component
|
||
useImperativeHandle(ref, () => ({
|
||
saveProject
|
||
}));
|
||
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>
|
||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||
Data zakończenia projektu
|
||
</label>
|
||
<Input
|
||
type="date"
|
||
name="completion_date"
|
||
value={formatDateForInput(form.completion_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>
|
||
|
||
{session?.user?.role === 'team_lead' && (
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||
Wartość zlecenia
|
||
</label>
|
||
<Input
|
||
type="number"
|
||
name="wartosc_zlecenia"
|
||
value={form.wartosc_zlecenia || ""}
|
||
onChange={handleChange}
|
||
placeholder="0.00"
|
||
step="0.01"
|
||
min="0"
|
||
/>
|
||
</div>
|
||
)}
|
||
|
||
<div className="md:col-span-2">
|
||
<ProjectContactSelector
|
||
projectId={initialData?.project_id}
|
||
/>
|
||
</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>
|
||
);
|
||
});
|
||
|
||
export default ProjectForm;
|