Przejdź do głównej zawartości

Services Layer

Na tej stronie

Vista zawiera 50 serwisów zorganizowanych domenowo:

src/services/
├── ai/ # 8 serwisów AI
├── audio/ # 6 serwisów audio
├── patients/ # 5 serwisów pacjentów
├── visits/ # 7 serwisów wizyt
├── auth/ # 4 serwisy auth
├── calendar/ # 5 serwisów kalendarza
├── settings/ # 3 serwisy ustawień
├── utils/ # 12 utility services
└── index.ts # Re-exports

Centralny klient AI - abstrakcja nad wszystkimi providerami AI.

src/services/ai/UnifiedAIClient.ts
export class UnifiedAIClient {
private serviceResolver: ServiceResolver;
private retryConfig: RetryConfig;
private cacheManager: ResponseCache;
/**
* Chat completion - główna metoda LLM
*/
async chat(request: ChatRequest): Promise<ChatResponse> {
// 1. Request validation
const validatedRequest = this.validateChatRequest(request);
// 2. Provider selection with health check
const provider = await this.serviceResolver.selectProvider('llm');
// 3. Request execution with retry logic
const response = await this.executeWithRetry(
() => provider.chat(validatedRequest),
this.retryConfig
);
// 4. Response caching and metrics
await this.cacheManager.store(request, response);
this.metrics.recordRequest(provider.name, response.latencyMs);
return response;
}
/**
* Speech-to-Text - transkrypcja audio
*/
async transcribe(audioFile: File): Promise<TranscriptionResponse> {
const provider = await this.serviceResolver.selectProvider('stt');
return provider.transcribe(audioFile);
}
/**
* Text-to-Speech - synteza mowy
*/
async synthesize(text: string, voice?: string): Promise<AudioBuffer> {
const provider = await this.serviceResolver.selectProvider('tts');
return provider.synthesize(text, voice);
}
}
// Singleton instance
export const aiClient = new UnifiedAIClient();
sequenceDiagram
participant UI as Frontend UI
participant UAI as Unified AI Client
participant SR as Service Resolver
participant LIB as LibraxisAI Cloud
participant MLX as Local MLX
participant OAI as OpenAI API
UI->>UAI: AI Request
UAI->>SR: Resolve Provider Chain
Note over SR: Provider Priority:<br/>1. LibraxisAI<br/>2. Local MLX<br/>3. OpenAI
SR->>LIB: Try Primary
alt LibraxisAI Success
LIB->>SR: Response
SR->>UAI: Success
else LibraxisAI Failure
SR->>MLX: Try Secondary
alt MLX Success
MLX->>SR: Response
SR->>UAI: Success
else MLX Failure
SR->>OAI: Try Tertiary
OAI->>SR: Response
SR->>UAI: Success
end
end
UAI->>UI: Result

Zarządzanie pacjentami - CRUD + wyszukiwanie.

src/services/patients/patientService.ts
export const patientService = {
/**
* Pobierz pacjenta po ID
*/
async getById(patientId: string): Promise<Patient> {
return safeInvoke('get_patient', { patientId });
},
/**
* Lista pacjentów z paginacją
*/
async list(options: ListOptions): Promise<PaginatedResult<Patient>> {
return safeInvoke('list_patients', {
page: options.page,
limit: options.limit,
sortBy: options.sortBy,
sortOrder: options.sortOrder,
});
},
/**
* Wyszukiwanie pacjentów
*/
async search(query: string): Promise<Patient[]> {
if (query.length < 2) return [];
return safeInvoke('search_patients', { query });
},
/**
* Utwórz nowego pacjenta
*/
async create(data: CreatePatientData): Promise<Patient> {
const validated = PatientSchema.parse(data);
return safeInvoke('create_patient', validated);
},
/**
* Aktualizuj pacjenta
*/
async update(patientId: string, updates: Partial<Patient>): Promise<Patient> {
return safeInvoke('update_patient', { patientId, updates });
},
/**
* Usuń pacjenta
*/
async delete(patientId: string): Promise<void> {
return safeInvoke('delete_patient', { patientId });
},
/**
* Historia wizyt pacjenta
*/
async getVisitHistory(patientId: string): Promise<Visit[]> {
return safeInvoke('get_patient_visits', { patientId });
},
/**
* Sprawdź duplikaty (identity matching)
*/
async checkDuplicates(data: PatientIdentity): Promise<Patient[]> {
return safeInvoke('find_duplicate_patients', data);
},
};

Zarządzanie wizytami - główny workflow medyczny.

src/services/visits/visitService.ts
export const visitService = {
/**
* Pobierz wizytę po ID
*/
async getById(visitId: string): Promise<Visit> {
return safeInvoke('get_visit', { visitId });
},
/**
* Lista wizyt użytkownika
*/
async listForUser(userId: string, filters?: VisitFilters): Promise<Visit[]> {
return safeInvoke('list_user_visits', { userId, filters });
},
/**
* Utwórz nową wizytę
*/
async create(data: CreateVisitData): Promise<Visit> {
const validated = CreateVisitSchema.parse(data);
return safeInvoke('create_visit', validated);
},
/**
* Aktualizuj SOAP z optimistic locking
*/
async updateSOAP(
visitId: string,
soapData: SOAPData,
expectedVersion: number
): Promise<Visit> {
return safeInvoke('update_visit_soap', {
visitId,
soapData,
expectedVersion,
});
},
/**
* Finalizuj wizytę
*/
async finalize(visitId: string): Promise<Visit> {
return safeInvoke('finalize_visit', { visitId });
},
/**
* Eksportuj wizytę do PDF
*/
async exportToPDF(visitId: string): Promise<string> {
return safeInvoke('export_visit_pdf', { visitId });
},
/**
* Wyślij wizytę emailem
*/
async sendEmail(visitId: string, recipientEmail: string): Promise<void> {
return safeInvoke('send_visit_email', { visitId, recipientEmail });
},
};

Obsługa audio - nagrywanie, transkrypcja, storage.

src/services/audio/audioService.ts
export const audioService = {
/**
* Zapisz nagranie audio
*/
async saveRecording(
visitId: string,
audioBlob: Blob
): Promise<RecordingMetadata> {
const base64 = await blobToBase64(audioBlob);
return safeInvoke('save_audio_file', {
visitId,
audioData: base64,
format: audioBlob.type,
});
},
/**
* Rozpocznij transkrypcję
*/
async startTranscription(
recordingId: string,
options?: TranscriptionOptions
): Promise<TranscriptionJob> {
return safeInvoke('start_transcription', {
recordingId,
provider: options?.provider ?? 'auto',
language: options?.language ?? 'pl',
});
},
/**
* Pobierz status transkrypcji
*/
async getTranscriptionStatus(jobId: string): Promise<TranscriptionStatus> {
return safeInvoke('get_transcription_status', { jobId });
},
/**
* Pobierz plik audio
*/
async getAudioFile(recordingId: string): Promise<ArrayBuffer> {
return safeInvoke('get_audio_file', { recordingId });
},
/**
* Usuń nagranie
*/
async deleteRecording(recordingId: string): Promise<void> {
return safeInvoke('delete_recording', { recordingId });
},
};

Autoryzacja i sesje.

src/services/auth/authService.ts
export const authService = {
/**
* Logowanie hasłem
*/
async login(email: string, password: string): Promise<LoginResponse> {
return safeInvoke('login', { email, password });
},
/**
* Logowanie biometrią
*/
async loginWithBiometric(): Promise<LoginResponse> {
return safeInvoke('login_biometric', {});
},
/**
* Wylogowanie
*/
async logout(): Promise<void> {
return safeInvoke('logout', {});
},
/**
* Odśwież sesję
*/
async refreshSession(): Promise<Session> {
return safeInvoke('refresh_session', {});
},
/**
* Zmiana hasła
*/
async changePassword(
currentPassword: string,
newPassword: string
): Promise<void> {
return safeInvoke('change_password', { currentPassword, newPassword });
},
/**
* Sprawdź aktywną sesję
*/
async getActiveSession(): Promise<Session | null> {
try {
return await safeInvoke('get_active_session', {});
} catch {
return null;
}
},
};

Zarządzanie wizytami w kalendarzu.

src/services/calendar/appointmentService.ts
export const appointmentService = {
/**
* Lista wizyt dla weterynarza
*/
async listForVet(
vetId: string,
dateRange: DateRange
): Promise<Appointment[]> {
return safeInvoke('list_vet_appointments', {
vetId,
startDate: dateRange.start.toISOString(),
endDate: dateRange.end.toISOString(),
});
},
/**
* Utwórz wizytę
*/
async create(data: CreateAppointmentData): Promise<Appointment> {
// Sprawdź konflikty przed utworzeniem
const conflicts = await this.checkConflicts(
data.veterinarianId,
data.scheduledDate,
data.scheduledTime,
data.durationMinutes
);
if (conflicts.hasConflicts) {
throw new ConflictError(conflicts);
}
return safeInvoke('create_appointment', data);
},
/**
* Sprawdź konflikty czasowe
*/
async checkConflicts(
vetId: string,
date: string,
time: string,
duration: number
): Promise<ConflictResult> {
return safeInvoke('check_appointment_conflicts', {
veterinarianId: vetId,
date,
startTime: time,
durationMinutes: duration,
});
},
/**
* Zmień status wizyty
*/
async updateStatus(
appointmentId: string,
status: AppointmentStatus
): Promise<Appointment> {
return safeInvoke('update_appointment_status', {
appointmentId,
status,
});
},
/**
* Konwertuj wizytę na konsultację
*/
async convertToVisit(appointmentId: string): Promise<Visit> {
return safeInvoke('convert_appointment_to_visit', { appointmentId });
},
};

Centralne zarządzanie błędami.

src/services/errorHandlingService.ts
interface ErrorContext {
category: 'auth' | 'database' | 'ai' | 'audio' | 'network';
severity: 'low' | 'medium' | 'high' | 'critical';
context: Record<string, any>;
showToast?: boolean;
logToFile?: boolean;
}
export const handleError = (error: Error, context: ErrorContext) => {
// 1. Error classification
const classification = classifyError(error, context);
// 2. Logging strategy
if (classification.severity >= 'medium') {
secureLogger.logError(error, context);
}
// 3. User notification
if (context.showToast !== false) {
notificationService.showError(
getLocalizedErrorMessage(error, context)
);
}
// 4. Recovery suggestions
if (classification.canRecover) {
return classification.recoveryActions;
}
return null;
};

Bezpieczne logowanie (bez danych wrażliwych).

src/services/secureLogger.ts
export const secureLogger = {
/**
* Loguj info (bez danych wrażliwych)
*/
info(message: string, context?: object): void {
const sanitized = sanitizeLogData(context);
console.info(`[INFO] ${message}`, sanitized);
},
/**
* Loguj błąd
*/
error(error: Error, context?: ErrorContext): void {
const sanitized = sanitizeLogData(context);
console.error(`[ERROR] ${error.message}`, sanitized);
// Opcjonalnie: zapisz do pliku
if (context?.logToFile) {
safeInvoke('log_error_to_file', {
message: error.message,
stack: error.stack,
context: sanitized,
});
}
},
/**
* Sanityzuj dane przed logowaniem
*/
sanitize<T extends object>(data: T): T {
return sanitizeLogData(data);
},
};
function sanitizeLogData<T extends object>(data?: T): T | undefined {
if (!data) return data;
const sensitiveKeys = [
'password', 'token', 'api_key', 'apiKey',
'secret', 'authorization', 'cookie',
];
const sanitized = { ...data };
for (const key of Object.keys(sanitized)) {
if (sensitiveKeys.some(sk => key.toLowerCase().includes(sk))) {
(sanitized as any)[key] = '[REDACTED]';
}
}
return sanitized;
}

src/services/tauriInvoke.ts
export async function safeInvoke<T>(
command: string,
args?: Record<string, unknown>
): Promise<T> {
try {
return await invoke<T>(command, args);
} catch (error) {
// Log error
secureLogger.error(error as Error, {
category: 'network',
severity: 'medium',
context: { command },
});
// Transform to user-friendly message
throw new Error(getLocalizedErrorMessage(error));
}
}
src/services/utils/retry.ts
interface RetryConfig {
maxAttempts: number;
delayMs: number;
backoffMultiplier: number;
retryOn: (error: Error) => boolean;
}
export async function withRetry<T>(
fn: () => Promise<T>,
config: RetryConfig
): Promise<T> {
let lastError: Error;
let delay = config.delayMs;
for (let attempt = 1; attempt <= config.maxAttempts; attempt++) {
try {
return await fn();
} catch (error) {
lastError = error as Error;
if (!config.retryOn(lastError) || attempt === config.maxAttempts) {
throw lastError;
}
await sleep(delay);
delay *= config.backoffMultiplier;
}
}
throw lastError!;
}
src/services/utils/cache.ts
interface CacheEntry<T> {
data: T;
timestamp: number;
ttl: number;
}
export class ServiceCache<T> {
private cache = new Map<string, CacheEntry<T>>();
get(key: string): T | null {
const entry = this.cache.get(key);
if (!entry) return null;
if (Date.now() - entry.timestamp > entry.ttl) {
this.cache.delete(key);
return null;
}
return entry.data;
}
set(key: string, data: T, ttlMs: number): void {
this.cache.set(key, {
data,
timestamp: Date.now(),
ttl: ttlMs,
});
}
invalidate(key: string): void {
this.cache.delete(key);
}
clear(): void {
this.cache.clear();
}
}

src/services/__tests__/patientService.test.ts
import { patientService } from '../patients/patientService';
import { vi } from 'vitest';
vi.mock('@tauri-apps/api/core', () => ({
invoke: vi.fn(),
}));
describe('patientService', () => {
describe('search', () => {
it('returns empty array for short queries', async () => {
const result = await patientService.search('B');
expect(result).toEqual([]);
});
it('calls backend for valid queries', async () => {
const mockPatients = [{ patient_id: '1', name: 'Burek' }];
vi.mocked(invoke).mockResolvedValue(mockPatients);
const result = await patientService.search('Burek');
expect(invoke).toHaveBeenCalledWith('search_patients', {
query: 'Burek',
});
expect(result).toEqual(mockPatients);
});
});
});