Przejdź do głównej zawartości

Patients

KomendaOpisTyp
get_all_patientsPobiera listę wszystkich pacjentówquery
get_patientPobiera pojedynczego pacjenta po IDquery
create_patientTworzy nowego pacjentawrite
update_patientAktualizuje dane pacjentawrite
delete_patientUsuwa pacjentawrite
search_patientsWyszukuje pacjentów po queryquery
get_patient_visitsPobiera wizyty pacjentaquery
export_patientsEksportuje pacjentów (JSON/CSV)query
import_patientsImportuje pacjentów z plikuwrite

erDiagram
PATIENT {
string patient_id PK
string name "Imię zwierzęcia"
string species "pies, kot, królik..."
string breed "Rasa"
date date_of_birth
string sex "male, female, unknown"
float weight_kg
string chip_number "Numer chipa"
string coat_color "Umaszczenie"
boolean is_neutered
string owner_id FK
string clinic_id FK
text special_notes
text allergies
string status "active, deceased, transferred"
timestamp created_at
timestamp updated_at
}
OWNER {
string owner_id PK
string name
string email
string phone
string address
}
PATIENT }o--|| OWNER : "belongs to"

flowchart TB
subgraph PatientsView["PatientsView"]
LIST[Lista pacjentów]
SEARCH[Wyszukiwarka]
FILTERS[Filtry]
end
subgraph Actions["Akcje"]
NEW[➕ Nowy pacjent]
EDIT[✏️ Edycja]
DELETE[🗑️ Usuń]
VIEW[👁️ Szczegóły]
end
subgraph Backend["Tauri Backend"]
GET[get_all_patients]
CREATE[create_patient]
UPDATE[update_patient]
DEL[delete_patient]
SRCH[search_patients]
end
LIST --> GET
SEARCH --> SRCH
NEW --> CREATE
EDIT --> UPDATE
DELETE --> DEL
CREATE -.->|Event| LIST
UPDATE -.->|Event| LIST
DEL -.->|Event| LIST

Pobiera listę wszystkich pacjentów z joinowanymi danymi właściciela:

// Frontend (src/services/patientService.ts)
const patients = await safeInvoke<Patient[]>(
'get_all_patients',
withSessionPayload({})
);
// Response zawiera:
interface Patient {
patient_id: string;
name: string;
species: string;
breed?: string;
date_of_birth?: string;
sex?: 'male' | 'female' | 'unknown';
weight_kg?: number;
chip_number?: string;
coat_color?: string;
is_neutered?: boolean;
special_notes?: string;
allergies?: string;
status: 'active' | 'deceased' | 'transferred';
created_at: string;
updated_at: string;
// Joined fields
owner_id?: string;
owner_name?: string;
owner_phone?: string;
owner_email?: string;
clinic_id?: string;
}

Pobiera pojedynczego pacjenta z pełnymi danymi:

const patient = await safeInvoke<Patient | null>(
'get_patient',
withSessionPayload({ patient_id })
);

Tworzy nowego pacjenta:

sequenceDiagram
participant UI as NewPatientView
participant Service as patientService
participant Tauri as Tauri Backend
participant DB as SQLite
UI->>Service: createPatient(data)
Service->>Service: validatePatientData(data)
Service->>Tauri: create_patient(patient_data)
Tauri->>Tauri: Generate UUID
Tauri->>DB: INSERT INTO patients
alt Owner nie istnieje
Tauri->>DB: INSERT INTO owners
end
DB-->>Tauri: OK
Tauri-->>Service: Patient
Tauri--)UI: Event: patient-created
Service-->>UI: Patient
UI->>UI: Navigate → Patient Profile
// Frontend
interface CreatePatientRequest {
name: string;
species: string;
breed?: string;
date_of_birth?: string;
sex?: 'male' | 'female' | 'unknown';
weight_kg?: number;
chip_number?: string;
coat_color?: string;
is_neutered?: boolean;
special_notes?: string;
allergies?: string;
// Owner data (can be new or existing)
owner_id?: string; // Existing owner
owner_name?: string; // New owner
owner_phone?: string;
owner_email?: string;
owner_address?: string;
}
const newPatient = await safeInvoke<Patient>(
'create_patient',
withSessionPayload({ patient_data: request })
);

Aktualizuje dane pacjenta:

interface UpdatePatientRequest {
patient_id: string; // Required
name?: string;
species?: string;
breed?: string;
date_of_birth?: string;
sex?: string;
weight_kg?: number;
chip_number?: string;
coat_color?: string;
is_neutered?: boolean;
special_notes?: string;
allergies?: string;
status?: 'active' | 'deceased' | 'transferred';
owner_id?: string;
}
const updated = await safeInvoke<Patient>(
'update_patient',
withSessionPayload({ patient_data: request })
);
// Emituje event: patient-updated

Usuwa pacjenta (soft delete przez zmianę statusu lub hard delete):

// Frontend (src/services/patientService.ts)
await safeInvoke(
'delete_patient',
withSessionPayload({ patient_id })
);
// Emituje event: patient-deleted

Wyszukuje pacjentów po nazwie, gatunku, rasie lub danych właściciela:

// Frontend
const results = await safeInvoke<Patient[]>(
'search_patients',
withSessionPayload({ query: 'Burek' })
);
// Backend szuka w:
// - patients.name
// - patients.species
// - patients.breed
// - patients.chip_number
// - owners.name
// - owners.phone

Pobiera wszystkie wizyty pacjenta:

const visits = await safeInvoke<Visit[]>(
'get_patient_visits',
withSessionPayload({ patient_id })
);

Eksportuje wszystkich pacjentów do pliku:

// Frontend (src/components/settings/PrivacySettings.tsx)
const data = await safeInvoke<string>(
'export_patients',
withSessionPayload({ format: 'json' }) // lub 'csv'
);
// Następnie zapis do pliku przez Tauri dialog
await saveFile(data, 'patients.json');

Importuje pacjentów z pliku:

const result = await safeInvoke<ImportResult>(
'import_patients',
withSessionPayload({
format: 'json',
data: fileContent
})
);
interface ImportResult {
imported: number;
skipped: number;
errors: string[];
}

Backend emituje eventy przy zmianach, pozwalając UI na reaktywne odświeżanie:

EventPayloadKiedy
patient-created{ patient_id }Po utworzeniu pacjenta
patient-updated{ patient_id }Po aktualizacji pacjenta
patient-deleted{ patient_id }Po usunięciu pacjenta
// Frontend - nasłuchiwanie eventów
import { listen } from '@tauri-apps/api/event';
useEffect(() => {
const unlisten = listen<{ patient_id: string }>('patient-created', (event) => {
console.log('New patient:', event.payload.patient_id);
refreshPatients();
});
return () => {
unlisten.then(fn => fn());
};
}, []);

Frontend używa dedupeGet do cachowania i deduplikacji requestów:

src/services/patientService.ts
export const patientService = {
getAllPatients: async () => {
const sessionId = requireSessionId();
const cacheKey = `patients:${sessionId}:all`;
return dedupeGet(cacheKey, async () => {
return safeInvoke<Patient[]>(
'get_all_patients',
withSessionPayload({})
);
});
},
// Po mutacji - inwalidacja cache
createPatient: async (data: CreatePatientRequest) => {
const result = await safeInvoke<Patient>(
'create_patient',
withSessionPayload({ patient_data: data })
);
invalidateDedupePrefix('patients:'); // Czyści cache
return result;
}
};

Główny widok listy pacjentów:

src/features/patients/PatientsView.tsx
const PatientsView: React.FC = () => {
const { patients, loading, error, refresh } = usePatients();
const [searchQuery, setSearchQuery] = useState('');
const [filters, setFilters] = useState<PatientFilters>({});
// Filtrowanie po stronie FE
const filteredPatients = useMemo(() => {
return patients.filter(p => {
if (searchQuery && !matchesSearch(p, searchQuery)) return false;
if (filters.species && p.species !== filters.species) return false;
if (filters.status && p.status !== filters.status) return false;
return true;
});
}, [patients, searchQuery, filters]);
return (
<div>
<PatientSearchBar value={searchQuery} onChange={setSearchQuery} />
<PatientFilters value={filters} onChange={setFilters} />
<PatientsList patients={filteredPatients} />
</div>
);
};

Karta pojedynczego pacjenta:

interface PatientCardProps {
patient: Patient;
onEdit: () => void;
onDelete: () => void;
onViewDetails: () => void;
}

KodOpisRozwiązanie
patient_not_foundPacjent nie istniejeSprawdź ID
duplicate_chip_numberNumer chipa już istniejeUżyj innego numeru
invalid_speciesNieznany gatunekSprawdź dozwolone wartości
owner_not_foundWłaściciel nie istniejeUtwórz właściciela
import_validation_errorBłąd walidacji importuSprawdź format pliku