Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions frontend/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,7 @@ function App() {
onBack={goBackToDashboard}
onRegisterCase={(pet) => { setSelectedPet(pet); setCurrentView('create-case'); }}
onCaseClick={handleCaseClick}
onDelete={fetchData}
/>
);

Expand Down
118 changes: 86 additions & 32 deletions frontend/src/pages/PetDetail.jsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import React, { useState, useEffect } from 'react';
import { STATUS_MAP } from '../utils/statusHelper';
import { medicalRecordService } from '../services/api';
import { medicalRecordService, petService } from '../services/api';
import { Trash2, Info, AlertCircle } from 'lucide-react';

const PetDetail = ({ pet, onBack, onRegisterCase, onCaseClick }) => {
const PetDetail = ({ pet, onBack, onRegisterCase, onCaseClick, onDelete }) => {
const [petRecords, setPetRecords] = useState([]);
const [loading, setLoading] = useState(true);
const [isDeleting, setIsDeleting] = useState(false);

useEffect(() => {
if (!pet?.id) {
Expand All @@ -20,6 +22,30 @@ const PetDetail = ({ pet, onBack, onRegisterCase, onCaseClick }) => {
return () => { cancelled = true; };
}, [pet?.id]);

// Strikt kontroll: Vi tillåter bara borttagning om noll journaler finns (matchar backend)
const hasHistory = Array.isArray(petRecords) && petRecords.length > 0;

const handleDeletePet = async () => {
if (hasHistory) return;

const confirmMessage = `Är du helt säker på att du vill ta bort ${pet.name}? Detta går inte att ångra.`;
if (window.confirm(confirmMessage)) {
setIsDeleting(true);
try {
await petService.deletePet(pet.id);
if (onDelete) {
await onDelete();
}
onBack(); // Navigera tillbaka till listan
} catch (err) {
console.error("Fel vid borttagning:", err);
alert(err.response?.data?.message || "Kunde inte ta bort djurprofilen.");
} finally {
setIsDeleting(false);
}
}
};

if (!pet) {
return <div className="p-10 text-center italic text-slate-400">Ingen djurdata hittades.</div>;
}
Expand All @@ -29,64 +55,88 @@ const PetDetail = ({ pet, onBack, onRegisterCase, onCaseClick }) => {
{/* Navigering tillbaka */}
<button
onClick={onBack}
className="flex items-center gap-2 text-slate-400 hover:text-slate-900 font-bold text-[10px] uppercase tracking-[0.2em] mb-8 transition group"
className="flex items-center gap-2 text-slate-400 hover:text-slate-900 font-black text-[10px] uppercase tracking-[0.2em] mb-8 transition group"
>
<span className="group-hover:-translate-x-1 transition-transform">← Tillbaka till mina djur</span>
</button>

<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8 text-left">
{/* VÄNSTERKOLUMN: DJURPROFIL */}
<div className="lg:col-span-1">
<div className="bg-white rounded-2xl border border-slate-200 overflow-hidden shadow-sm">
<div className="bg-white rounded-[2.5rem] border border-slate-200 overflow-hidden shadow-sm">
<div className="bg-slate-900 h-32 relative">
<div className="absolute -bottom-10 left-8 w-20 h-20 bg-white rounded-2xl shadow-lg border-4 border-white flex items-center justify-center text-3xl font-bold text-slate-900 uppercase italic">
<div className="absolute -bottom-10 left-8 w-20 h-20 bg-white rounded-3xl shadow-lg border-4 border-white flex items-center justify-center text-3xl font-black text-slate-900 uppercase italic">
{pet?.name?.charAt(0) || '?'}
</div>
</div>

<div className="pt-14 p-8 pb-8">
<h1 className="text-3xl font-black text-slate-900 italic tracking-tight uppercase">
<h1 className="text-3xl font-black text-slate-900 italic tracking-tight uppercase leading-none">
{pet?.name}
</h1>
<p className="text-slate-400 text-xs font-bold uppercase tracking-widest mt-1">
<p className="text-slate-400 text-[10px] font-black uppercase tracking-[0.2em] mt-2">
{pet?.species} • {pet?.breed || 'Okänd ras'}
</p>

<div className="mt-8 space-y-4 border-t border-slate-100 pt-6">
<div className="flex justify-between items-center">
<span className="text-[10px] font-bold text-slate-400 uppercase tracking-widest">Född</span>
<span className="text-[10px] font-black text-slate-400 uppercase tracking-widest italic">Född</span>
<span className="font-bold text-slate-700">{pet?.dateOfBirth || 'Ej angivet'}</span>
</div>
<div className="flex justify-between items-center">
<span className="text-[10px] font-bold text-slate-400 uppercase tracking-widest">Vikt</span>
<span className="text-[10px] font-black text-slate-400 uppercase tracking-widest italic">Vikt</span>
<span className="font-bold text-slate-700">
{pet?.weightKg ? `${pet.weightKg} kg` : 'Ej angivet'}
</span>
</div>
</div>

<button
onClick={() => onRegisterCase(pet)}
className="w-full mt-8 bg-slate-900 text-white font-bold py-4 rounded-xl shadow-lg hover:bg-blue-900 transition transform active:scale-95 italic text-sm uppercase tracking-widest"
>
Sök vård för {pet?.name}
</button>
<div className="mt-8 space-y-3">
<button
onClick={() => onRegisterCase(pet)}
className="w-full bg-slate-900 text-white font-black py-4 rounded-2xl shadow-lg hover:bg-blue-600 transition transform active:scale-95 italic text-[11px] uppercase tracking-[0.2em]"
>
Sök vård för {pet?.name}
</button>

{/* TA BORT-KNAPP */}
<button
onClick={handleDeletePet}
disabled={hasHistory || isDeleting}
className={`w-full flex items-center justify-center gap-2 py-3 rounded-xl font-black text-[10px] uppercase tracking-widest transition-all italic
${hasHistory
? 'bg-slate-50 text-slate-300 cursor-not-allowed'
: 'text-red-500 hover:bg-red-50'}`}
>
<Trash2 size={14} />
{isDeleting ? 'Tar bort...' : 'Ta bort djurprofil'}
</button>

{hasHistory && (
<div className="flex items-start gap-3 p-4 bg-slate-50 rounded-2xl border border-slate-100 mt-2">
<Info size={16} className="text-slate-400 shrink-0 mt-0.5" />
<p className="text-[9px] font-black text-slate-500 uppercase leading-relaxed italic tracking-wide">
Profilen kan ej raderas då den är kopplad till medicinsk historik i systemet.
</p>
</div>
)}
Comment on lines +103 to +122

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Minor a11y: the disabled delete button has no programmatic explanation.

When hasHistory is true the button is disabled but assistive tech only reads "Ta bort djurprofil, disabled"; the explanatory notice at lines 115–122 is visually adjacent but not associated. Consider adding a title and aria-describedby so screen-reader users learn why the action is unavailable.

♻️ Suggested tweak
                                 <button
                                     onClick={handleDeletePet}
                                     disabled={hasHistory || isDeleting}
+                                    title={hasHistory ? 'Profilen kan ej raderas då den är kopplad till medicinsk historik.' : undefined}
+                                    aria-describedby={hasHistory ? 'delete-disabled-reason' : undefined}
                                     className={`w-full flex items-center justify-center gap-2 py-3 rounded-xl font-black text-[10px] uppercase tracking-widest transition-all italic
                                         ${hasHistory
                                         ? 'bg-slate-50 text-slate-300 cursor-not-allowed'
                                         : 'text-red-500 hover:bg-red-50'}`}
                                 >
                                     <Trash2 size={14} />
                                     {isDeleting ? 'Tar bort...' : 'Ta bort djurprofil'}
                                 </button>

                                 {hasHistory && (
-                                    <div className="flex items-start gap-3 p-4 bg-slate-50 rounded-2xl border border-slate-100 mt-2">
+                                    <div id="delete-disabled-reason" className="flex items-start gap-3 p-4 bg-slate-50 rounded-2xl border border-slate-100 mt-2">
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/pages/PetDetail.jsx` around lines 103 - 122, The delete button
is disabled when hasHistory is true but lacks programmatic explanation; update
the button rendered in the block using handleDeletePet / hasHistory / isDeleting
so that when hasHistory is true it includes a descriptive title and an
aria-describedby pointing to the explanatory message element, and give that
explanatory <div> a stable id (e.g., "delete-disabled-reason") so screen readers
announce why the button is disabled; ensure the aria-describedby is only present
when hasHistory is true and removed otherwise.

</div>
</div>
</div>
</div>

{/* HÖGERKOLUMN: JOURNALHISTORIK */}
<div className="lg:col-span-2 space-y-6">
<h2 className="text-xl font-bold text-slate-900 flex items-center">
<span className="w-2 h-6 bg-slate-300 mr-3 rounded-full"></span>
<h2 className="text-xl font-black text-slate-900 flex items-center italic uppercase tracking-tight">
<span className="w-2 h-6 bg-slate-900 mr-3 rounded-full"></span>
Journalhistorik för {pet?.name}
</h2>

<div className="space-y-4">
{loading ? (
<div className="flex items-center gap-2 text-slate-400 py-4">
<div className="w-4 h-4 border-2 border-slate-200 border-t-blue-400 rounded-full animate-spin"></div>
<span className="text-[10px] font-bold uppercase tracking-widest italic">Laddar...</span>
<div className="flex items-center gap-3 text-slate-400 py-8">
<div className="w-5 h-5 border-2 border-slate-200 border-t-blue-500 rounded-full animate-spin"></div>
<span className="text-[10px] font-black uppercase tracking-widest italic">Hämtar journaler...</span>
</div>
) : Array.isArray(petRecords) && petRecords.length > 0 ? (
petRecords.map(record => {
Expand All @@ -101,31 +151,35 @@ const PetDetail = ({ pet, onBack, onRegisterCase, onCaseClick }) => {
type="button"
key={record.id}
onClick={() => onCaseClick && onCaseClick(record)}
aria-label={`Visa detaljer för journal: ${record.title || 'Journalanteckning'}`}
className="w-full text-left p-5 bg-white border border-slate-200 rounded-xl hover:shadow-md transition group focus:ring-2 focus:ring-blue-500 outline-none"
className="w-full text-left p-6 bg-white border border-slate-200 rounded-[2rem] hover:shadow-xl hover:border-blue-100 transition-all group outline-none"
>
<div className="flex justify-between items-start">
<div>
<span className={`text-[10px] font-bold px-2 py-0.5 rounded uppercase border italic ${statusConfig.color}`}>
<div className="flex-1">
<span className={`text-[9px] font-black px-3 py-1 rounded-full border uppercase italic ${statusConfig.color}`}>
{statusConfig.label}
</span>
<h3 className="text-lg font-bold text-slate-900 mt-2 group-hover:text-blue-700">
<h3 className="text-xl font-black text-slate-900 mt-3 group-hover:text-blue-600 transition-colors italic tracking-tight">
{record.reasonForVisit || record.title || 'Journalanteckning'}
</h3>
<p className="text-sm text-slate-500 italic mt-1">
{record.diagnosis || 'Ingen diagnos ställd än.'}
<p className="text-sm text-slate-500 font-medium italic mt-1 line-clamp-1">
{record.diagnosis || 'Väntar på medicinsk bedömning...'}
</p>
</div>
<div className="text-right text-xs text-slate-400 italic">
{record.createdAt ? new Date(record.createdAt).toLocaleDateString('sv-SE') : ''}
<div className="text-right flex flex-col items-end gap-2">
<div className="text-[10px] font-black text-slate-300 uppercase italic tracking-widest">
{record.createdAt ? new Date(record.createdAt).toLocaleDateString('sv-SE') : ''}
</div>
</div>
</div>
</button>
);
})
) : (
<div className="bg-slate-50 border border-dashed border-slate-200 rounded-2xl p-12 text-center">
<h3 className="text-slate-900 font-bold italic uppercase tracking-tight text-sm">Ingen historik hittades</h3>
<div className="bg-white border-2 border-dashed border-slate-200 rounded-[2.5rem] p-16 text-center">
<div className="w-16 h-16 bg-slate-50 rounded-2xl flex items-center justify-center mx-auto mb-4 text-slate-200">
<AlertCircle size={32} />
</div>
<h3 className="text-slate-400 font-black italic uppercase tracking-widest text-xs">Ingen historik hittades</h3>
</div>
)}
</div>
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/services/api.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ export const petService = {
getPetsByOwner: (ownerId) => api.get(`/pets/owner/${ownerId}`),
getPetById: (id) => api.get(`/pets/${id}`),
createPet: (data) => api.post('/pets', data),
updatePet: (id, data) => api.put(`/pets/${id}`, data),
deletePet: (id) => api.delete(`/pets/${id}`),
};

export const medicalRecordService = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,5 @@ public interface MedicalRecordRepository extends JpaRepository<MedicalRecord, UU
boolean existsByAssignedVetId(UUID vetId);
boolean existsByCreatedById(UUID userId);
boolean existsByUpdatedById(UUID userId);
boolean existsByPetId(UUID petId);
}
6 changes: 6 additions & 0 deletions src/main/java/org/example/vet1177/services/PetService.java
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,12 @@ public void deletePet(UUID petId, User currentUser) {
if (!petPolicy.canDelete(currentUser, pet)) {
throw new ForbiddenException("Du har inte behörighet att radera detta djur");
}

boolean hasRecords = medicalRecordRepository.existsByPetId(petId);
if (hasRecords) {
throw new BusinessRuleException("Djuret kan inte raderas eftersom det finns registrerade journaler/ärenden.");
}

try {
petRepository.delete(pet);
petRepository.flush();
Expand Down