feat: add edited_at column to notes and implement note update functionality with audit logging

This commit is contained in:
2025-11-18 09:43:39 +01:00
parent acb7117c7d
commit fae7615818
4 changed files with 222 additions and 83 deletions

View File

@@ -28,6 +28,8 @@ export default function ProjectViewPage() {
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) => {
@@ -69,14 +71,14 @@ export default function ProjectViewPage() {
setNotes(prevNotes => [newNote, ...prevNotes]);
};
// Helper function to check if user can delete a note
const canDeleteNote = (note) => {
// Helper function to check if user can modify a note (edit or delete)
const canModifyNote = (note) => {
if (!session?.user) return false;
// Admins can delete any note
// Admins can modify any note
if (session.user.role === 'admin') return true;
// Users can delete their own notes
// Users can modify their own notes
return note.created_by === session.user.id;
};
@@ -113,6 +115,34 @@ export default function ProjectViewPage() {
);
};
// 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;
@@ -839,49 +869,109 @@ export default function ProjectViewPage() {
{n.created_by_name}
</span>
)}
{n.edited_at && (
<span className="text-xs text-gray-400 italic">
edytowane
</span>
)}
</div>
{canDeleteNote(n) && (
<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 {
{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');
}
} 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"
}}
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ę"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
<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>
<div className="text-gray-900 leading-relaxed">
{parseNoteText(n.note)}
</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>