Przejdź do głównej zawartości

Hooks & Contexts

Na tej stronie

Vista zawiera 58 custom hooks zorganizowanych domenowo:

src/hooks/
├── audio/ # 8 hooków audio
├── visits/ # 6 hooków wizyt
├── patients/ # 4 hooki pacjentów
├── auth/ # 3 hooki auth
├── calendar/ # 4 hooki kalendarza
├── ai/ # 5 hooków AI
├── settings/ # 3 hooki ustawień
├── ui/ # 6 hooków UI
└── utils/ # 19 utility hooks

Główny hook do nagrywania audio wizyt.

interface UseRecordingReturn {
// State
isRecording: boolean;
isPaused: boolean;
duration: number; // seconds
audioLevel: number; // 0-100 (for visualisation)
// Actions
startRecording: () => Promise<void>;
stopRecording: () => Promise<Blob>;
pauseRecording: () => void;
resumeRecording: () => void;
// Errors
error: Error | null;
permissionDenied: boolean;
}
// Użycie
const {
isRecording,
duration,
startRecording,
stopRecording,
} = useRecording({
onRecordingComplete: (audioBlob) => handleAudio(audioBlob),
maxDuration: 3600, // 1 hour max
});

Automatyczne wykrywanie mowy dla smart recording.

interface VADConfig {
threshold: number; // -50 to 0 dB (default: -45)
minSilenceDuration: number; // ms before auto-stop (default: 1500)
bufferSize: number; // Audio buffer size (default: 4096)
smoothingFrames: number; // Noise reduction (default: 3)
}
const { isVoiceActive, currentLevel } = useVoiceActivityDetection({
threshold: -45,
minSilenceDuration: 1500,
onSilenceDetected: () => {
// Auto-pause or notify user
},
});

Odtwarzanie nagrań z wizyt.

const {
isPlaying,
currentTime,
duration,
play,
pause,
seek,
setPlaybackRate,
} = useAudioPlayer(audioUrl);

Główny hook do zarządzania wizytą.

interface UseVisitReturn {
visit: Visit | null;
isLoading: boolean;
error: Error | null;
// CRUD
updateVisit: (updates: Partial<Visit>) => Promise<void>;
finalizeVisit: () => Promise<void>;
deleteVisit: () => Promise<void>;
// SOAP
updateSOAP: (field: SOAPField, value: string) => Promise<void>;
// Optimistic locking
version: number;
hasConflict: boolean;
}
const {
visit,
updateSOAP,
finalizeVisit,
} = useVisit(visitId);

Edycja notatek SOAP z auto-save.

const {
subjective,
objective,
assessment,
plan,
setSubjective,
setObjective,
setAssessment,
setPlan,
isDirty,
isSaving,
save,
} = useSOAPEditor(visitId, {
autoSaveDelay: 2000, // 2 seconds debounce
onAutoSave: (data) => console.log('Auto-saved:', data),
});

Transkrypcja audio wizyty.

const {
transcript,
isTranscribing,
progress, // 0-100
speakerSegments, // Diarization results
startTranscription,
retryTranscription,
} = useVisitTranscription(visitId, recordingId);

Zarządzanie pacjentem.

const {
patient,
isLoading,
updatePatient,
deletePatient,
visitHistory,
upcomingAppointments,
} = usePatient(patientId);

Wyszukiwanie pacjentów z debounce.

const {
results,
isSearching,
search,
clearResults,
} = usePatientSearch({
debounceMs: 300,
minChars: 2,
});
// W komponencie
<input
onChange={(e) => search(e.target.value)}
placeholder="Szukaj pacjenta..."
/>

Główny hook autoryzacji.

const {
user,
isAuthenticated,
isLoading,
// Actions
login,
logout,
loginWithBiometric,
// Session
sessionExpiresAt,
refreshSession,
} = useAuth();

Obsługa Touch ID / Face ID.

const {
isAvailable, // czy biometria dostępna
isEnabled, // czy włączona dla usera
authenticate, // uruchom auth
enable, // włącz biometrię
disable, // wyłącz biometrię
} = useBiometric();
// Użycie
const handleBiometricLogin = async () => {
const result = await authenticate('Zaloguj się do Vista');
if (result.success) {
// User authenticated
}
};

Czat z AI assistentem.

const {
messages,
isTyping,
sendMessage,
clearHistory,
currentSessionId,
} = useAIChat({
visitContext: visitId,
onMessageReceived: (msg) => playNotificationSound(),
});
// Wysłanie wiadomości
await sendMessage('Co sugerujesz dla tego pacjenta?');

Sugestie AI dla wizyty.

const {
suggestions,
isGenerating,
acceptSuggestion,
dismissSuggestion,
regenerate,
} = useAISuggestions(visitId, {
autoGenerate: true, // Generuj automatycznie po transkrypcji
});

const debouncedValue = useDebounce(searchQuery, 300);
useEffect(() => {
if (debouncedValue) {
performSearch(debouncedValue);
}
}, [debouncedValue]);
const [theme, setTheme] = useLocalStorage('vista-theme', 'light');
// Automatycznie sync z localStorage
setTheme('dark');
const isMobile = useMediaQuery('(max-width: 768px)');
const prefersReducedMotion = useMediaQuery('(prefers-reduced-motion: reduce)');
return isMobile ? <MobileLayout /> : <DesktopLayout />;
const dropdownRef = useRef<HTMLDivElement>(null);
useClickOutside(dropdownRef, () => {
setIsOpen(false);
});
return (
<div ref={dropdownRef}>
{isOpen && <DropdownContent />}
</div>
);
// Ctrl+N → nowa wizyta
useKeyboardShortcut(['ctrl', 'n'], () => {
navigateTo('new-visit');
});
// Escape → zamknij modal
useKeyboardShortcut(['escape'], () => {
closeModal();
}, { enabled: isModalOpen });

interface AuthContextValue {
user: User | null;
isAuthenticated: boolean;
isLoading: boolean;
login: (credentials: LoginCredentials) => Promise<void>;
logout: () => Promise<void>;
loginWithBiometric: () => Promise<void>;
updateUser: (updates: Partial<User>) => Promise<void>;
refreshSession: () => Promise<void>;
}
// Provider w App.tsx
<AuthProvider
onSessionExpired={() => navigateTo('login')}
sessionTimeout={30 * 60 * 1000} // 30 minutes
>
<App />
</AuthProvider>
// Użycie w komponencie
const { user, logout } = useAuth();
interface ThemeContextValue {
theme: 'light' | 'dark' | 'system';
resolvedTheme: 'light' | 'dark';
setTheme: (theme: 'light' | 'dark' | 'system') => void;
// Galaxy theme
galaxyEnabled: boolean;
setGalaxyEnabled: (enabled: boolean) => void;
galaxyConfig: GalaxyConfig;
updateGalaxyConfig: (config: Partial<GalaxyConfig>) => void;
}
const { theme, setTheme, galaxyEnabled } = useTheme();
interface SidebarContextValue {
// Navigation
currentView: MenuItem;
setCurrentView: (view: MenuItem) => void;
// Sidebar state
isCollapsed: boolean;
toggleSidebar: () => void;
isMobileOpen: boolean;
setMobileOpen: (open: boolean) => void;
// Deep linking
navigationParams: Record<string, string>;
navigateWithParams: (view: MenuItem, params: Record<string, string>) => void;
}
const { currentView, setCurrentView, navigateWithParams } = useSidebar();
// Navigate to patient details
navigateWithParams('patients', { patientId: '123' });
interface NotificationContextValue {
// Toast notifications
showSuccess: (message: string) => void;
showError: (message: string) => void;
showWarning: (message: string) => void;
showInfo: (message: string) => void;
// Custom notifications
notify: (notification: Notification) => void;
dismiss: (id: string) => void;
dismissAll: () => void;
}
const { showSuccess, showError } = useNotification();
// Po zapisaniu
showSuccess('Wizyta została zapisana');
// Przy błędzie
showError('Nie udało się zapisać wizyty');

function useDataFetch<T>(
fetcher: () => Promise<T>,
deps: DependencyList = []
) {
const [data, setData] = useState<T | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
let isMounted = true;
const fetchData = async () => {
setIsLoading(true);
setError(null);
try {
const result = await fetcher();
if (isMounted) {
setData(result);
}
} catch (e) {
if (isMounted) {
setError(e as Error);
}
} finally {
if (isMounted) {
setIsLoading(false);
}
}
};
fetchData();
return () => {
isMounted = false;
};
}, deps);
return { data, isLoading, error };
}
function useOptimisticUpdate<T>(
initialData: T,
updateFn: (data: T) => Promise<T>
) {
const [data, setData] = useState(initialData);
const [isUpdating, setIsUpdating] = useState(false);
const update = async (newData: T) => {
const previousData = data;
// Optimistic update
setData(newData);
setIsUpdating(true);
try {
const result = await updateFn(newData);
setData(result);
} catch (error) {
// Rollback on error
setData(previousData);
throw error;
} finally {
setIsUpdating(false);
}
};
return { data, update, isUpdating };
}

src/hooks/__tests__/usePatientSearch.test.ts
import { renderHook, act, waitFor } from '@testing-library/react';
import { usePatientSearch } from '../usePatientSearch';
describe('usePatientSearch', () => {
it('debounces search queries', async () => {
const { result } = renderHook(() =>
usePatientSearch({ debounceMs: 100 })
);
act(() => {
result.current.search('Bu');
result.current.search('Bur');
result.current.search('Bure');
result.current.search('Burek');
});
// Only last search should execute
await waitFor(() => {
expect(mockSearchFn).toHaveBeenCalledTimes(1);
expect(mockSearchFn).toHaveBeenCalledWith('Burek');
});
});
});