993 lines
30 KiB
JavaScript
993 lines
30 KiB
JavaScript
"use client";
|
|
|
|
import { useState, useEffect } from "react";
|
|
import { useParams } from "next/navigation";
|
|
import { useSession } from "next-auth/react";
|
|
import NoteForm from "@/components/NoteForm";
|
|
import ProjectTasksSection from "@/components/ProjectTasksSection";
|
|
import FieldWithHistory from "@/components/FieldWithHistory";
|
|
import { Card, CardHeader, CardContent } from "@/components/ui/Card";
|
|
import Button from "@/components/ui/Button";
|
|
import Badge from "@/components/ui/Badge";
|
|
import Link from "next/link";
|
|
import { differenceInCalendarDays, parseISO } from "date-fns";
|
|
import { formatDate, formatCoordinates } from "@/lib/utils";
|
|
import PageContainer from "@/components/ui/PageContainer";
|
|
import PageHeader from "@/components/ui/PageHeader";
|
|
import ProjectStatusDropdown from "@/components/ProjectStatusDropdown";
|
|
import ProjectAssigneeDropdown from "@/components/ProjectAssigneeDropdown";
|
|
import ClientProjectMap from "@/components/ui/ClientProjectMap";
|
|
import FileUploadBox from "@/components/FileUploadBox";
|
|
import FileItem from "@/components/FileItem";
|
|
import proj4 from "proj4";
|
|
|
|
export default function ProjectViewPage() {
|
|
const params = useParams();
|
|
const { data: session } = useSession();
|
|
const [project, setProject] = useState(null);
|
|
const [notes, setNotes] = useState([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [uploadedFiles, setUploadedFiles] = useState([]);
|
|
const [editingNoteId, setEditingNoteId] = useState(null);
|
|
const [editText, setEditText] = useState('');
|
|
|
|
// Helper function to parse note text with links
|
|
const parseNoteText = (text) => {
|
|
const linkRegex = /\[([^\]]+)\]\(([^)]+)\)/g;
|
|
const parts = [];
|
|
let lastIndex = 0;
|
|
let match;
|
|
|
|
while ((match = linkRegex.exec(text)) !== null) {
|
|
// Add text before the link
|
|
if (match.index > lastIndex) {
|
|
parts.push(text.slice(lastIndex, match.index));
|
|
}
|
|
// Add the link
|
|
parts.push(
|
|
<a
|
|
key={match.index}
|
|
href={match[2]}
|
|
className="text-blue-600 hover:text-blue-800 underline"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
>
|
|
{match[1]}
|
|
</a>
|
|
);
|
|
lastIndex = match.index + match[0].length;
|
|
}
|
|
|
|
// Add remaining text
|
|
if (lastIndex < text.length) {
|
|
parts.push(text.slice(lastIndex));
|
|
}
|
|
|
|
return parts.length > 0 ? parts : text;
|
|
};
|
|
|
|
// Helper function to add a new note to the list
|
|
const addNote = (newNote) => {
|
|
setNotes(prevNotes => [newNote, ...prevNotes]);
|
|
};
|
|
|
|
// Helper function to check if user can modify a note (edit or delete)
|
|
const canModifyNote = (note) => {
|
|
if (!session?.user) return false;
|
|
|
|
// Admins can modify any note
|
|
if (session.user.role === 'admin') return true;
|
|
|
|
// Users can modify their own notes
|
|
return note.created_by === session.user.id;
|
|
};
|
|
|
|
// Helper function to handle file upload
|
|
const handleFileUploaded = (newFile) => {
|
|
setUploadedFiles(prevFiles => [newFile, ...prevFiles]);
|
|
};
|
|
|
|
// Helper function to handle file deletion
|
|
const handleFileDelete = async (fileId) => {
|
|
if (confirm('Czy na pewno chcesz usunąć ten plik?')) {
|
|
try {
|
|
const res = await fetch(`/api/files/${fileId}`, {
|
|
method: 'DELETE',
|
|
});
|
|
if (res.ok) {
|
|
setUploadedFiles(prevFiles => prevFiles.filter(file => file.file_id !== fileId));
|
|
} else {
|
|
alert('Błąd podczas usuwania pliku');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error deleting file:', error);
|
|
alert('Błąd podczas usuwania pliku');
|
|
}
|
|
}
|
|
};
|
|
|
|
// Helper function to handle file update (edit)
|
|
const handleFileUpdate = async (updatedFile) => {
|
|
setUploadedFiles(prevFiles =>
|
|
prevFiles.map(file =>
|
|
file.file_id === updatedFile.file_id ? updatedFile : file
|
|
)
|
|
);
|
|
};
|
|
|
|
// Helper function to save edited note
|
|
const handleSaveNote = async (noteId) => {
|
|
try {
|
|
const res = await fetch(`/api/notes/${noteId}`, {
|
|
method: 'PUT',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({ note: editText }),
|
|
});
|
|
if (res.ok) {
|
|
// Update the note in local state
|
|
setNotes(prevNotes =>
|
|
prevNotes.map(note =>
|
|
note.note_id === noteId ? { ...note, note: editText, edited_at: new Date().toISOString() } : note
|
|
)
|
|
);
|
|
setEditingNoteId(null);
|
|
setEditText('');
|
|
} else {
|
|
alert('Błąd podczas aktualizacji notatki');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error updating note:', error);
|
|
alert('Błąd podczas aktualizacji notatki');
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
const fetchData = async () => {
|
|
if (!params.id) return;
|
|
|
|
try {
|
|
// Fetch project data
|
|
const projectRes = await fetch(`/api/projects/${params.id}`);
|
|
if (!projectRes.ok) {
|
|
throw new Error('Project not found');
|
|
}
|
|
const projectData = await projectRes.json();
|
|
|
|
// Fetch notes data
|
|
const notesRes = await fetch(`/api/notes?project_id=${params.id}`);
|
|
const notesData = notesRes.ok ? await notesRes.json() : [];
|
|
|
|
// Fetch files data
|
|
const filesRes = await fetch(`/api/files?entityType=project&entityId=${params.id}`);
|
|
const filesData = filesRes.ok ? await filesRes.json() : [];
|
|
|
|
setProject(projectData);
|
|
setNotes(notesData);
|
|
setUploadedFiles(filesData);
|
|
} catch (error) {
|
|
console.error('Error fetching data:', error);
|
|
setProject(null);
|
|
setNotes([]);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
fetchData();
|
|
}, [params.id]);
|
|
|
|
useEffect(() => {
|
|
if (project?.project_name) {
|
|
document.title = `${project.project_name} - Panel`;
|
|
} else {
|
|
document.title = 'Panel';
|
|
}
|
|
}, [project]);
|
|
|
|
if (loading) {
|
|
return (
|
|
<PageContainer>
|
|
<div className="flex items-center justify-center py-12">
|
|
<div className="text-gray-500">Loading...</div>
|
|
</div>
|
|
</PageContainer>
|
|
);
|
|
}
|
|
|
|
if (!project) {
|
|
return (
|
|
<PageContainer>
|
|
<Card>
|
|
<CardContent className="text-center py-8">
|
|
<p className="text-red-600 text-lg">Projekt nie został znaleziony.</p>
|
|
<Link href="/projects" className="mt-4 inline-block">
|
|
<Button variant="primary">Powrót do projektów</Button>
|
|
</Link>
|
|
</CardContent>
|
|
</Card>
|
|
</PageContainer>
|
|
);
|
|
}
|
|
|
|
const daysRemaining = project.finish_date
|
|
? differenceInCalendarDays(parseISO(project.finish_date), new Date())
|
|
: null;
|
|
|
|
const getDeadlineVariant = (days) => {
|
|
if (days < 0) return "danger";
|
|
if (days <= 7) return "warning";
|
|
return "success";
|
|
};
|
|
|
|
return (
|
|
<PageContainer>
|
|
{/* Mobile: Full-width title, Desktop: Standard PageHeader */}
|
|
<div className="block sm:hidden mb-6">
|
|
{/* Mobile Layout */}
|
|
<div className="space-y-4">
|
|
{/* Full-width title */}
|
|
<div className="w-full">
|
|
<h1 className="text-2xl font-bold text-gray-900 break-words">
|
|
{project.project_name}
|
|
</h1>
|
|
<p className="text-sm text-gray-600 mt-1">
|
|
{project.city} • {project.address} • {project.project_number}
|
|
</p>
|
|
</div>
|
|
|
|
{/* Mobile action bar */}
|
|
<div className="flex flex-col space-y-3">
|
|
{/* Status and deadline badges */}
|
|
<div className="flex items-center gap-2 flex-wrap">
|
|
<ProjectStatusDropdown project={project} size="sm" />
|
|
{daysRemaining !== null && (
|
|
<Badge variant={getDeadlineVariant(daysRemaining)} size="sm" className="text-xs">
|
|
{daysRemaining === 0
|
|
? "Termin dzisiaj"
|
|
: daysRemaining > 0
|
|
? `${daysRemaining} dni pozostało`
|
|
: `${Math.abs(daysRemaining)} dni po terminie`}
|
|
</Badge>
|
|
)}
|
|
</div>
|
|
|
|
{/* Action buttons - full width */}
|
|
<div className="flex gap-2 w-full">
|
|
<Link href="/projects" className="flex-1">
|
|
<Button variant="outline" size="sm" className="w-full text-xs">
|
|
<svg
|
|
className="w-4 h-4 mr-1"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth={2}
|
|
d="M15 19l-7-7 7-7"
|
|
/>
|
|
</svg>
|
|
Powrót
|
|
</Button>
|
|
</Link>
|
|
<Link href={`/projects/${params.id}/edit`} className="flex-1">
|
|
<Button variant="primary" size="sm" className="w-full text-xs">
|
|
<svg
|
|
className="w-4 h-4 mr-1"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth={2}
|
|
d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"
|
|
/>
|
|
</svg>
|
|
Edytuj
|
|
</Button>
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Desktop: Standard PageHeader */}
|
|
<div className="hidden sm:block">
|
|
<PageHeader
|
|
title={project.project_name}
|
|
description={`${project.city} • ${project.address} • ${project.project_number}`}
|
|
action={
|
|
<div className="flex items-center gap-3">
|
|
<ProjectStatusDropdown project={project} size="sm" />
|
|
{daysRemaining !== null && (
|
|
<Badge variant={getDeadlineVariant(daysRemaining)} size="md">
|
|
{daysRemaining === 0
|
|
? "Termin dzisiaj"
|
|
: daysRemaining > 0
|
|
? `${daysRemaining} dni pozostało`
|
|
: `${Math.abs(daysRemaining)} dni po terminie`}
|
|
</Badge>
|
|
)}
|
|
<Link href="/projects">
|
|
<Button variant="outline" size="sm">
|
|
<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="M15 19l-7-7 7-7"
|
|
/>
|
|
</svg>
|
|
Powrót do projektów
|
|
</Button>
|
|
</Link>
|
|
<Link href={`/projects/${params.id}/edit`}>
|
|
<Button variant="primary">
|
|
<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="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"
|
|
/>
|
|
</svg>
|
|
Edytuj projekt
|
|
</Button>
|
|
</Link>
|
|
</div>
|
|
}
|
|
/>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-8">
|
|
{/* Main Project Information */}
|
|
<div className="lg:col-span-2 space-y-6">
|
|
<Card>
|
|
<CardHeader>
|
|
{" "}
|
|
<div className="flex items-center justify-between">
|
|
<h2 className="text-xl font-semibold text-gray-900">
|
|
Informacje o projekcie
|
|
</h2>
|
|
<Badge
|
|
variant={
|
|
project.project_type === "design"
|
|
? "secondary"
|
|
: project.project_type === "construction"
|
|
? "primary"
|
|
: project.project_type === "design+construction"
|
|
? "success"
|
|
: "default"
|
|
}
|
|
size="sm"
|
|
>
|
|
{project.project_type === "design"
|
|
? "Projektowanie (P)"
|
|
: project.project_type === "construction"
|
|
? "Budowa (B)"
|
|
: project.project_type === "design+construction"
|
|
? "Projektowanie + Budowa (P+B)"
|
|
: "Nieznany"}
|
|
</Badge>
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent className="space-y-6">
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
<div>
|
|
<span className="text-sm font-medium text-gray-500 block mb-1">
|
|
Lokalizacja
|
|
</span>
|
|
<p className="text-gray-900 font-medium">
|
|
{project.city || "N/A"}
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<span className="text-sm font-medium text-gray-500 block mb-1">
|
|
Adres
|
|
</span>
|
|
<p className="text-gray-900 font-medium">
|
|
{project.address || "N/A"}
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<span className="text-sm font-medium text-gray-500 block mb-1">
|
|
Działka
|
|
</span>
|
|
<p className="text-gray-900 font-medium">
|
|
{project.plot || "N/A"}
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<span className="text-sm font-medium text-gray-500 block mb-1">
|
|
Jednostka ewidencyjna
|
|
</span>
|
|
<p className="text-gray-900 font-medium">
|
|
{project.district || "N/A"}
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<span className="text-sm font-medium text-gray-500 block mb-1">
|
|
Obręb
|
|
</span>
|
|
<p className="text-gray-900 font-medium">
|
|
{project.unit || "N/A"}
|
|
</p>
|
|
</div>{" "}
|
|
<FieldWithHistory
|
|
tableName="projects"
|
|
recordId={project.project_id}
|
|
fieldName="finish_date"
|
|
currentValue={project.finish_date}
|
|
label="Termin zakończenia"
|
|
/>
|
|
{project.completion_date && (
|
|
<div>
|
|
<span className="text-sm font-medium text-gray-500 block mb-1">
|
|
Data zakończenia projektu
|
|
</span>
|
|
<p className="text-gray-900 font-medium">
|
|
{formatDate(project.completion_date)}
|
|
</p>
|
|
</div>
|
|
)}
|
|
<div>
|
|
<span className="text-sm font-medium text-gray-500 block mb-1">
|
|
WP
|
|
</span>
|
|
<p className="text-gray-900 font-medium">
|
|
{project.wp || "N/A"}
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<span className="text-sm font-medium text-gray-500 block mb-1">
|
|
Numer inwestycji
|
|
</span>
|
|
<p className="text-gray-900 font-medium">
|
|
{project.investment_number || "N/A"}
|
|
</p>
|
|
</div>
|
|
{session?.user?.role === 'team_lead' && project.wartosc_zlecenia && (
|
|
<FieldWithHistory
|
|
tableName="projects"
|
|
recordId={project.project_id}
|
|
fieldName="wartosc_zlecenia"
|
|
currentValue={project.wartosc_zlecenia}
|
|
displayValue={parseFloat(project.wartosc_zlecenia).toLocaleString('pl-PL', {
|
|
style: 'currency',
|
|
currency: 'PLN'
|
|
})}
|
|
label="Wartość zlecenia"
|
|
/>
|
|
)}
|
|
</div>
|
|
|
|
{project.contact && (
|
|
<div className="border-t pt-4">
|
|
<span className="text-sm font-medium text-gray-500 block mb-1">
|
|
Kontakt
|
|
</span>
|
|
<p className="text-gray-900 font-medium">{project.contact}</p>
|
|
</div>
|
|
)}
|
|
|
|
{project.coordinates && (
|
|
<div className="border-t pt-4">
|
|
<span className="text-sm font-medium text-gray-500 block mb-1">
|
|
Współrzędne
|
|
</span>
|
|
<div className="flex items-center gap-2">
|
|
<p className="text-gray-900 font-medium font-mono text-sm">
|
|
{formatCoordinates(project.coordinates)}
|
|
</p>
|
|
<a
|
|
href={`https://www.google.com/maps/place/${project.coordinates}`}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="text-blue-600 hover:text-blue-800 transition-colors"
|
|
title="Otwórz w Google Maps"
|
|
>
|
|
<svg
|
|
className="w-5 h-5"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth={2}
|
|
d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"
|
|
/>
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth={2}
|
|
d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"
|
|
/>
|
|
</svg>
|
|
</a>
|
|
<a
|
|
href={(() => {
|
|
// Define EPSG:2180 projection (Poland CS92)
|
|
proj4.defs("EPSG:2180", "+proj=tmerc +lat_0=0 +lon_0=19 +k=0.9993 +x_0=500000 +y_0=-5300000 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs");
|
|
|
|
const [lat, lng] = project.coordinates.split(',').map(c => parseFloat(c.trim()));
|
|
|
|
// Convert WGS84 to EPSG:2180
|
|
const [x, y] = proj4('EPSG:4326', 'EPSG:2180', [lng, lat]);
|
|
|
|
// Create bbox with ~100m offset in each direction
|
|
const offset = 100;
|
|
const bbox = `${x - offset},${y - offset},${x + offset},${y + offset}`;
|
|
|
|
return `https://mapy.geoportal.gov.pl/imap/Imgp_2.html?gpmap=gp0&bbox=${bbox}&variant=KATASTER`;
|
|
})()}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="text-green-600 hover:text-green-800 transition-colors"
|
|
title="Otwórz w Geoportal"
|
|
>
|
|
<svg
|
|
className="w-5 h-5"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth={2}
|
|
d="M9 20l-5.447-2.724A1 1 0 013 16.382V5.618a1 1 0 011.447-.894L9 7m0 13l6-3m-6 3V7m6 10l4.553 2.276A1 1 0 0021 18.382V7.618a1 1 0 00-1.447-.894L15 4m0 13V4m0 0L9 7"
|
|
/>
|
|
</svg>
|
|
</a>
|
|
</div>
|
|
</div>
|
|
)} {project.notes && (
|
|
<div className="border-t pt-4">
|
|
<span className="text-sm font-medium text-gray-500 block mb-1">
|
|
Notes
|
|
</span>
|
|
<p className="text-gray-900">{project.notes}</p>
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Contract Details */}
|
|
<Card>
|
|
<CardHeader>
|
|
<h2 className="text-xl font-semibold text-gray-900">
|
|
Szczegóły umowy
|
|
</h2>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
<div>
|
|
<span className="text-sm font-medium text-gray-500 block mb-1">
|
|
Numer umowy
|
|
</span>
|
|
<p className="text-gray-900 font-medium">
|
|
{project.contract_number || "N/A"}
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<span className="text-sm font-medium text-gray-500 block mb-1">
|
|
Numer umowy klienta
|
|
</span>
|
|
<p className="text-gray-900 font-medium">
|
|
{project.customer_contract_number ? (
|
|
<Link
|
|
href={`/contracts/${project.contract_id}`}
|
|
className="text-inherit hover:text-inherit no-underline"
|
|
>
|
|
{project.customer_contract_number}
|
|
</Link>
|
|
) : (
|
|
"N/A"
|
|
)}
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<span className="text-sm font-medium text-gray-500 block mb-1">
|
|
Klient
|
|
</span>
|
|
<p className="text-gray-900 font-medium">
|
|
{project.customer || "N/A"}
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<span className="text-sm font-medium text-gray-500 block mb-1">
|
|
Inwestor
|
|
</span>
|
|
<p className="text-gray-900 font-medium">
|
|
{project.investor || "N/A"}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* Status Sidebar */}
|
|
<div className="space-y-6">
|
|
<Card>
|
|
<CardHeader>
|
|
<h2 className="text-lg font-semibold text-gray-900">
|
|
Status projektu
|
|
</h2>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
{" "}
|
|
<div>
|
|
<span className="text-sm font-medium text-gray-500 block mb-2">
|
|
Aktualny status
|
|
</span>
|
|
<ProjectStatusDropdown project={project} size="md" />
|
|
</div>
|
|
<div className="border-t pt-4">
|
|
<span className="text-sm font-medium text-gray-500 block mb-2">
|
|
Przypisany do
|
|
</span>
|
|
<ProjectAssigneeDropdown project={project} size="md" />
|
|
</div>
|
|
{daysRemaining !== null && (
|
|
<div className="border-t pt-4">
|
|
<span className="text-sm font-medium text-gray-500 block mb-2">
|
|
Harmonogram
|
|
</span>
|
|
<div className="text-center">
|
|
<Badge
|
|
variant={getDeadlineVariant(daysRemaining)}
|
|
size="lg"
|
|
>
|
|
{daysRemaining === 0
|
|
? "Termin dzisiaj"
|
|
: daysRemaining > 0
|
|
? `${daysRemaining} dni pozostało`
|
|
: `${Math.abs(daysRemaining)} dni po terminie`}
|
|
</Badge>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Quick Actions */}
|
|
<Card>
|
|
<CardHeader>
|
|
<h2 className="text-lg font-semibold text-gray-900">
|
|
Szybkie akcje
|
|
</h2>
|
|
</CardHeader>
|
|
<CardContent className="space-y-3">
|
|
<Link href={`/projects/${params.id}/edit`} className="block">
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
className="w-full justify-start"
|
|
>
|
|
<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="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"
|
|
/>
|
|
</svg>
|
|
Edytuj projekt
|
|
</Button>
|
|
</Link>{" "}
|
|
<Link href="/projects" className="block">
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
className="w-full justify-start"
|
|
>
|
|
<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="M15 19l-7-7 7-7"
|
|
/>
|
|
</svg>
|
|
Powrót do projektów
|
|
</Button>
|
|
</Link>
|
|
<Link href="/projects/map" className="block">
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
className="w-full justify-start"
|
|
>
|
|
<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="M9 20l-5.447-2.724A1 1 0 013 16.382V5.618a1 1 0 011.447-.894L9 7m0 13l6-3m-6 3V7m6 10l4.553 2.276A1 1 0 0021 18.382V7.618a1 1 0 00-1.447-.894L15 4m0 13V4m0 0L9 7"
|
|
/>
|
|
</svg>
|
|
Zobacz wszystkie na mapie
|
|
</Button>
|
|
</Link>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* File Upload */}
|
|
<Card>
|
|
<CardHeader>
|
|
<h2 className="text-lg font-semibold text-gray-900">
|
|
Załączniki
|
|
</h2>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
<FileUploadBox projectId={params.id} onFileUploaded={handleFileUploaded} />
|
|
{uploadedFiles.length > 0 && (
|
|
<div className="space-y-2">
|
|
<h3 className="text-sm font-medium text-gray-700">Przesłane pliki:</h3>
|
|
{uploadedFiles.map((file) => (
|
|
<FileItem
|
|
key={file.file_id}
|
|
file={file}
|
|
onDelete={handleFileDelete}
|
|
onUpdate={handleFileUpdate}
|
|
/>
|
|
))}
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
{/* Project Location Map */}
|
|
{project.coordinates && (
|
|
<div className="mb-8">
|
|
{" "}
|
|
<Card>
|
|
<CardHeader>
|
|
{" "}
|
|
<div className="flex items-center justify-between">
|
|
<h2 className="text-xl font-semibold text-gray-900">
|
|
Lokalizacja projektu
|
|
</h2>
|
|
{project.coordinates && (
|
|
<Link
|
|
href={`/projects/map?lat=${project.coordinates
|
|
.split(",")[0]
|
|
.trim()}&lng=${project.coordinates
|
|
.split(",")[1]
|
|
.trim()}&zoom=16`}
|
|
>
|
|
<Button variant="outline" size="sm">
|
|
<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="M9 20l-5.447-2.724A1 1 0 013 16.382V5.618a1 1 0 011.447-.894L9 7m0 13l6-3m-6 3V7m6 10l4.553 2.276A1 1 0 0021 18.382V7.618a1 1 0 00-1.447-.894L15 4m0 13V4m0 0L9 7"
|
|
/>
|
|
</svg>
|
|
Zobacz na pełnej mapie
|
|
</Button>
|
|
</Link>
|
|
)}
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<ClientProjectMap
|
|
coordinates={project.coordinates}
|
|
projectName={project.project_name}
|
|
projectStatus={project.project_status}
|
|
showLayerControl={true}
|
|
mapHeight="h-80"
|
|
defaultLayer="Polish Geoportal Orthophoto"
|
|
showOverlays={false}
|
|
/>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
)}
|
|
{/* Project Tasks Section */}
|
|
<div className="mb-8">
|
|
<ProjectTasksSection projectId={params.id} />
|
|
</div>
|
|
{/* Notes Section */}
|
|
<Card>
|
|
<CardHeader>
|
|
<h2 className="text-xl font-semibold text-gray-900">Notatki</h2>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="mb-6">
|
|
<NoteForm projectId={params.id} onNoteAdded={addNote} />
|
|
</div>
|
|
{notes.length === 0 ? (
|
|
<div className="text-center py-12">
|
|
<div className="text-gray-400 mb-4">
|
|
<svg
|
|
className="w-12 h-12 mx-auto"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth={1.5}
|
|
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
|
|
/>
|
|
</svg>
|
|
</div>
|
|
<h3 className="text-lg font-medium text-gray-900 mb-2">
|
|
Brak notatek
|
|
</h3>
|
|
<p className="text-gray-500">
|
|
Dodaj swoją pierwszą notatkę używając formularza powyżej.
|
|
</p>
|
|
</div>
|
|
) : (
|
|
<div className="space-y-4">
|
|
{notes.map((n) => (
|
|
<div
|
|
key={n.note_id}
|
|
className="border border-gray-200 p-4 rounded-lg bg-gray-50 hover:bg-gray-100 transition-colors group"
|
|
>
|
|
<div className="flex items-center justify-between mb-2">
|
|
<div className="flex items-center gap-2">
|
|
<span className="text-sm font-medium text-gray-500">
|
|
{formatDate(n.note_date, { includeTime: true })}
|
|
</span>
|
|
{n.created_by_name && (
|
|
<span className="px-2 py-1 text-xs bg-blue-100 text-blue-700 rounded-full font-medium">
|
|
{n.created_by_name}
|
|
</span>
|
|
)}
|
|
{n.edited_at && (
|
|
<span className="text-xs text-gray-400 italic">
|
|
• edytowane
|
|
</span>
|
|
)}
|
|
</div>
|
|
{canModifyNote(n) && (
|
|
<div className="flex gap-1">
|
|
<button
|
|
onClick={() => {
|
|
setEditingNoteId(n.note_id);
|
|
setEditText(n.note);
|
|
}}
|
|
className="opacity-0 group-hover:opacity-100 transition-opacity p-1 text-gray-400 hover:text-blue-500 hover:bg-blue-50 rounded"
|
|
title="Edytuj notatkę"
|
|
>
|
|
<svg
|
|
className="w-4 h-4"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth={2}
|
|
d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"
|
|
/>
|
|
</svg>
|
|
</button>
|
|
<button
|
|
onClick={async () => {
|
|
if (confirm('Czy na pewno chcesz usunąć tę notatkę?')) {
|
|
try {
|
|
const res = await fetch(`/api/notes/${n.note_id}`, {
|
|
method: 'DELETE',
|
|
});
|
|
if (res.ok) {
|
|
// Remove the note from local state instead of full page reload
|
|
setNotes(prevNotes => prevNotes.filter(note => note.note_id !== n.note_id));
|
|
} else {
|
|
alert('Błąd podczas usuwania notatki');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error deleting note:', error);
|
|
alert('Błąd podczas usuwania notatki');
|
|
}
|
|
}
|
|
}}
|
|
className="opacity-0 group-hover:opacity-100 transition-opacity p-1 text-gray-400 hover:text-red-500 hover:bg-red-50 rounded"
|
|
title="Usuń notatkę"
|
|
>
|
|
<svg
|
|
className="w-4 h-4"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth={2}
|
|
d="M6 18L18 6M6 6l12 12"
|
|
/>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
{editingNoteId === n.note_id ? (
|
|
<div className="space-y-2">
|
|
<textarea
|
|
value={editText}
|
|
onChange={(e) => setEditText(e.target.value)}
|
|
className="w-full p-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
rows={3}
|
|
placeholder="Wpisz notatkę..."
|
|
/>
|
|
<div className="flex gap-2">
|
|
<Button
|
|
onClick={() => handleSaveNote(n.note_id)}
|
|
variant="primary"
|
|
size="sm"
|
|
>
|
|
Zapisz
|
|
</Button>
|
|
<Button
|
|
onClick={() => {
|
|
setEditingNoteId(null);
|
|
setEditText('');
|
|
}}
|
|
variant="outline"
|
|
size="sm"
|
|
>
|
|
Anuluj
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<div className="text-gray-900 leading-relaxed">
|
|
{parseNoteText(n.note)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
</PageContainer>
|
|
);
|
|
}
|