Files
panel/src/components/ProjectForm.js
RKWojs 60b79fa360 feat: add contact management functionality
- 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.
2025-12-03 16:23:05 +01:00

486 lines
13 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"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;