840 lines
26 KiB
Dart
840 lines
26 KiB
Dart
import 'dart:convert';
|
|
import 'dart:io';
|
|
import 'dart:typed_data';
|
|
|
|
import 'package:http/http.dart' as http;
|
|
import 'package:path_provider/path_provider.dart';
|
|
|
|
import 'config.dart';
|
|
|
|
class Patient {
|
|
final int id;
|
|
final String firstName;
|
|
final String lastName;
|
|
final String? birthDate;
|
|
final String? phone;
|
|
final String? email;
|
|
final String? procedure;
|
|
final String? status;
|
|
final String? consentSigned;
|
|
final String? notes;
|
|
final String? immichAlbumId;
|
|
|
|
Patient({
|
|
required this.id,
|
|
required this.firstName,
|
|
required this.lastName,
|
|
this.birthDate,
|
|
this.phone,
|
|
this.email,
|
|
this.procedure,
|
|
this.status,
|
|
this.consentSigned,
|
|
this.notes,
|
|
this.immichAlbumId,
|
|
});
|
|
|
|
factory Patient.fromJson(Map<String, dynamic> json) {
|
|
return Patient(
|
|
id: json['id'] as int,
|
|
firstName: (json['first_name'] ?? '') as String,
|
|
lastName: (json['last_name'] ?? '') as String,
|
|
birthDate: json['birth_date'] as String?,
|
|
phone: json['phone'] as String?,
|
|
email: json['email'] as String?,
|
|
procedure: json['procedure'] as String?,
|
|
status: json['status'] as String?,
|
|
consentSigned: json['consent_signed'] as String?,
|
|
notes: json['notes'] as String?,
|
|
immichAlbumId: json['immich_album_id'] as String?,
|
|
);
|
|
}
|
|
|
|
String get displayName => '$lastName $firstName'.trim();
|
|
}
|
|
|
|
class PatientPhoto {
|
|
final int id;
|
|
final int patientId;
|
|
final String kind;
|
|
final String filename;
|
|
final String immichAssetId;
|
|
final String? notes;
|
|
final DateTime? createdAt;
|
|
|
|
PatientPhoto({
|
|
required this.id,
|
|
required this.patientId,
|
|
required this.kind,
|
|
required this.filename,
|
|
required this.immichAssetId,
|
|
this.notes,
|
|
this.createdAt,
|
|
});
|
|
|
|
factory PatientPhoto.fromJson(Map<String, dynamic> json) {
|
|
return PatientPhoto(
|
|
id: json['id'] as int,
|
|
patientId: json['patient_id'] as int,
|
|
kind: json['kind'] as String,
|
|
filename: json['filename'] as String,
|
|
immichAssetId: json['immich_asset_id'] as String,
|
|
notes: json['notes'] as String?,
|
|
createdAt: DateTime.tryParse((json['created_at'] ?? '').toString()),
|
|
);
|
|
}
|
|
}
|
|
|
|
|
|
class ConsultationRecord {
|
|
final int id;
|
|
final int patientId;
|
|
final String? consultationDate;
|
|
final String? procedure;
|
|
final String? reason;
|
|
final String? plan;
|
|
final String? followUp;
|
|
final String? status;
|
|
final String? notes;
|
|
final DateTime? createdAt;
|
|
|
|
ConsultationRecord({
|
|
required this.id,
|
|
required this.patientId,
|
|
this.consultationDate,
|
|
this.procedure,
|
|
this.reason,
|
|
this.plan,
|
|
this.followUp,
|
|
this.status,
|
|
this.notes,
|
|
this.createdAt,
|
|
});
|
|
|
|
factory ConsultationRecord.fromJson(Map<String, dynamic> json) {
|
|
return ConsultationRecord(
|
|
id: json['id'] as int,
|
|
patientId: json['patient_id'] as int,
|
|
consultationDate: json['consultation_date'] as String?,
|
|
procedure: json['procedure'] as String?,
|
|
reason: json['reason'] as String?,
|
|
plan: json['plan'] as String?,
|
|
followUp: json['follow_up'] as String?,
|
|
status: json['status'] as String?,
|
|
notes: json['notes'] as String?,
|
|
createdAt: DateTime.tryParse((json['created_at'] ?? '').toString()),
|
|
);
|
|
}
|
|
}
|
|
|
|
|
|
class DocumentRecord {
|
|
final int id;
|
|
final int patientId;
|
|
final int? consultationId;
|
|
final String documentType;
|
|
final String title;
|
|
final String status;
|
|
final String? content;
|
|
final String? signedBy;
|
|
final String? signedAt;
|
|
final DateTime? createdAt;
|
|
|
|
DocumentRecord({
|
|
required this.id,
|
|
required this.patientId,
|
|
this.consultationId,
|
|
required this.documentType,
|
|
required this.title,
|
|
required this.status,
|
|
this.content,
|
|
this.signedBy,
|
|
this.signedAt,
|
|
this.createdAt,
|
|
});
|
|
|
|
factory DocumentRecord.fromJson(Map<String, dynamic> json) {
|
|
return DocumentRecord(
|
|
id: json['id'] as int,
|
|
patientId: json['patient_id'] as int,
|
|
consultationId: json['consultation_id'] as int?,
|
|
documentType: (json['document_type'] ?? 'autre') as String,
|
|
title: (json['title'] ?? 'Document') as String,
|
|
status: (json['status'] ?? 'brouillon') as String,
|
|
content: json['content'] as String?,
|
|
signedBy: json['signed_by'] as String?,
|
|
signedAt: json['signed_at'] as String?,
|
|
createdAt: DateTime.tryParse((json['created_at'] ?? '').toString()),
|
|
);
|
|
}
|
|
}
|
|
|
|
class ImmichPerson {
|
|
final String id;
|
|
final String? name;
|
|
final String thumbnailUrl;
|
|
final bool? isHidden;
|
|
|
|
ImmichPerson({
|
|
required this.id,
|
|
this.name,
|
|
required this.thumbnailUrl,
|
|
this.isHidden,
|
|
});
|
|
|
|
factory ImmichPerson.fromJson(Map<String, dynamic> json, String baseUrl) {
|
|
final rawThumbnail = (json['thumbnail_url'] ?? '') as String;
|
|
final resolvedThumbnail = rawThumbnail.startsWith('http') ? rawThumbnail : '$baseUrl$rawThumbnail';
|
|
return ImmichPerson(
|
|
id: json['id'] as String,
|
|
name: json['name'] as String?,
|
|
thumbnailUrl: resolvedThumbnail,
|
|
isHidden: json['is_hidden'] as bool?,
|
|
);
|
|
}
|
|
|
|
String get displayName {
|
|
final cleaned = (name ?? '').trim();
|
|
return cleaned.isEmpty ? 'Sans nom' : cleaned;
|
|
}
|
|
}
|
|
|
|
|
|
class ImmichAsset {
|
|
final String id;
|
|
final String? filename;
|
|
final String thumbnailUrl;
|
|
final String fileUrl;
|
|
final String? takenAt;
|
|
final String? type;
|
|
|
|
ImmichAsset({
|
|
required this.id,
|
|
this.filename,
|
|
required this.thumbnailUrl,
|
|
required this.fileUrl,
|
|
this.takenAt,
|
|
this.type,
|
|
});
|
|
|
|
factory ImmichAsset.fromJson(Map<String, dynamic> json, String baseUrl) {
|
|
String resolve(String raw) => raw.startsWith('http') ? raw : '$baseUrl$raw';
|
|
final rawThumbnail = (json['thumbnail_url'] ?? '') as String;
|
|
final rawFile = (json['file_url'] ?? '') as String;
|
|
return ImmichAsset(
|
|
id: json['id'] as String,
|
|
filename: json['filename'] as String?,
|
|
thumbnailUrl: resolve(rawThumbnail),
|
|
fileUrl: resolve(rawFile),
|
|
takenAt: json['taken_at'] as String?,
|
|
type: json['type'] as String?,
|
|
);
|
|
}
|
|
|
|
String get displayName {
|
|
final cleaned = (filename ?? '').trim();
|
|
return cleaned.isEmpty ? 'Photo Immich' : cleaned;
|
|
}
|
|
}
|
|
|
|
|
|
class FaceTransformPreview {
|
|
final Uint8List bytes;
|
|
final bool detected;
|
|
final String method;
|
|
final int landmarkCount;
|
|
|
|
FaceTransformPreview({
|
|
required this.bytes,
|
|
required this.detected,
|
|
required this.method,
|
|
required this.landmarkCount,
|
|
});
|
|
}
|
|
|
|
|
|
class SavedFaceLandmarks {
|
|
final Map<String, List<double>>? manualLandmarks;
|
|
final String? updatedAt;
|
|
|
|
SavedFaceLandmarks({required this.manualLandmarks, this.updatedAt});
|
|
|
|
factory SavedFaceLandmarks.fromJson(Map<String, dynamic> json) {
|
|
final raw = json['manual_landmarks'];
|
|
Map<String, List<double>>? parsed;
|
|
if (raw is Map) {
|
|
parsed = {};
|
|
raw.forEach((key, value) {
|
|
if (value is List && value.length >= 2) {
|
|
parsed![key.toString()] = [
|
|
(value[0] as num).toDouble(),
|
|
(value[1] as num).toDouble(),
|
|
];
|
|
}
|
|
});
|
|
}
|
|
return SavedFaceLandmarks(
|
|
manualLandmarks: parsed == null || parsed.isEmpty ? null : parsed,
|
|
updatedAt: json['updated_at']?.toString(),
|
|
);
|
|
}
|
|
}
|
|
|
|
class ApiService {
|
|
final String baseUrl;
|
|
|
|
ApiService({this.baseUrl = AppConfig.apiBaseUrl});
|
|
|
|
Future<List<Patient>> getPatients({String? query}) async {
|
|
final uri = Uri.parse('$baseUrl/patients').replace(
|
|
queryParameters: query == null || query.trim().isEmpty ? null : {'q': query.trim()},
|
|
);
|
|
final response = await http.get(uri);
|
|
_check(response);
|
|
final data = jsonDecode(response.body) as List<dynamic>;
|
|
return data.map((e) => Patient.fromJson(e as Map<String, dynamic>)).toList();
|
|
}
|
|
|
|
Future<Patient> createPatient({
|
|
required String firstName,
|
|
required String lastName,
|
|
String? birthDate,
|
|
String? phone,
|
|
String? email,
|
|
String? procedure,
|
|
String? status,
|
|
String? consentSigned,
|
|
String? notes,
|
|
}) async {
|
|
final uri = Uri.parse('$baseUrl/patients');
|
|
final response = await http.post(
|
|
uri,
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: jsonEncode({
|
|
'first_name': firstName,
|
|
'last_name': lastName,
|
|
'birth_date': _nullIfEmpty(birthDate),
|
|
'phone': _nullIfEmpty(phone),
|
|
'email': _nullIfEmpty(email),
|
|
'procedure': _nullIfEmpty(procedure),
|
|
'status': _nullIfEmpty(status) ?? 'actif',
|
|
'consent_signed': _nullIfEmpty(consentSigned) ?? 'non',
|
|
'notes': _nullIfEmpty(notes),
|
|
}),
|
|
);
|
|
_check(response);
|
|
return Patient.fromJson(jsonDecode(response.body) as Map<String, dynamic>);
|
|
}
|
|
|
|
Future<Patient> updatePatient({
|
|
required int patientId,
|
|
String? firstName,
|
|
String? lastName,
|
|
String? birthDate,
|
|
String? phone,
|
|
String? email,
|
|
String? procedure,
|
|
String? status,
|
|
String? consentSigned,
|
|
String? notes,
|
|
}) async {
|
|
final uri = Uri.parse('$baseUrl/patients/$patientId');
|
|
final response = await http.patch(
|
|
uri,
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: jsonEncode({
|
|
if (firstName != null) 'first_name': firstName,
|
|
if (lastName != null) 'last_name': lastName,
|
|
if (birthDate != null) 'birth_date': _nullIfEmpty(birthDate),
|
|
if (phone != null) 'phone': _nullIfEmpty(phone),
|
|
if (email != null) 'email': _nullIfEmpty(email),
|
|
if (procedure != null) 'procedure': _nullIfEmpty(procedure),
|
|
if (status != null) 'status': _nullIfEmpty(status),
|
|
if (consentSigned != null) 'consent_signed': _nullIfEmpty(consentSigned),
|
|
if (notes != null) 'notes': _nullIfEmpty(notes),
|
|
}),
|
|
);
|
|
_check(response);
|
|
return Patient.fromJson(jsonDecode(response.body) as Map<String, dynamic>);
|
|
}
|
|
|
|
Future<List<PatientPhoto>> getPatientPhotos(int patientId) async {
|
|
final uri = Uri.parse('$baseUrl/patients/$patientId/photos');
|
|
final response = await http.get(uri);
|
|
_check(response);
|
|
final data = jsonDecode(response.body) as List<dynamic>;
|
|
return data.map((e) => PatientPhoto.fromJson(e as Map<String, dynamic>)).toList();
|
|
}
|
|
|
|
Future<PatientPhoto> uploadPatientPhoto({
|
|
required int patientId,
|
|
required File file,
|
|
required String kind,
|
|
String? notes,
|
|
}) async {
|
|
final uri = Uri.parse('$baseUrl/patients/$patientId/photos');
|
|
final request = http.MultipartRequest('POST', uri);
|
|
request.fields['kind'] = kind;
|
|
if (notes != null && notes.trim().isNotEmpty) request.fields['notes'] = notes.trim();
|
|
request.files.add(await http.MultipartFile.fromPath('file', file.path));
|
|
|
|
final streamed = await request.send();
|
|
final response = await http.Response.fromStream(streamed);
|
|
_check(response);
|
|
return PatientPhoto.fromJson(jsonDecode(response.body) as Map<String, dynamic>);
|
|
}
|
|
|
|
String photoThumbnailUrl(int photoId) => '$baseUrl/photos/$photoId/thumbnail';
|
|
|
|
String photoFileUrl(int photoId) => '$baseUrl/photos/$photoId/file';
|
|
|
|
|
|
Map<String, dynamic> _faceTransformPayload({
|
|
required double globalStrength,
|
|
required double lipVolume,
|
|
required double upperLip,
|
|
required double lowerLip,
|
|
required double noseWidth,
|
|
required double noseTip,
|
|
required double chinProjection,
|
|
required double chinWidth,
|
|
required double jawline,
|
|
required double cheekbones,
|
|
required String? eyeColor,
|
|
required double eyeColorStrength,
|
|
required double eyeIrisSize,
|
|
List<List<double>>? eyePaintPoints,
|
|
required double eyePaintRadius,
|
|
required String? lipTint,
|
|
required double lipTintStrength,
|
|
required double cheekBlush,
|
|
required double browDarkness,
|
|
required double browLift,
|
|
required double skinSmooth,
|
|
required bool showGuides,
|
|
Map<String, List<double>>? manualLandmarks,
|
|
}) {
|
|
return {
|
|
'global_strength': globalStrength,
|
|
'lip_volume': lipVolume,
|
|
'upper_lip': upperLip,
|
|
'lower_lip': lowerLip,
|
|
'nose_width': noseWidth,
|
|
'nose_tip': noseTip,
|
|
'chin_projection': chinProjection,
|
|
'chin_width': chinWidth,
|
|
'jawline': jawline,
|
|
'cheekbones': cheekbones,
|
|
if (eyeColor != null && eyeColor.isNotEmpty) 'eye_color': eyeColor,
|
|
'eye_color_strength': eyeColorStrength,
|
|
'eye_iris_size': eyeIrisSize,
|
|
if (eyePaintPoints != null && eyePaintPoints.isNotEmpty) 'eye_paint_points': eyePaintPoints,
|
|
if (eyePaintPoints != null && eyePaintPoints.isNotEmpty) 'eye_paint_radius': eyePaintRadius,
|
|
if (lipTint != null && lipTint.isNotEmpty) 'lip_tint': lipTint,
|
|
'lip_tint_strength': lipTintStrength,
|
|
'cheek_blush': cheekBlush,
|
|
'brow_darkness': browDarkness,
|
|
'brow_lift': browLift,
|
|
'skin_smooth': skinSmooth,
|
|
'show_guides': showGuides,
|
|
if (manualLandmarks != null && manualLandmarks.isNotEmpty) 'manual_landmarks': manualLandmarks,
|
|
};
|
|
}
|
|
|
|
FaceTransformPreview _facePreviewFromResponse(http.Response response) {
|
|
return FaceTransformPreview(
|
|
bytes: response.bodyBytes,
|
|
detected: (response.headers['x-face-detected'] ?? '').toLowerCase() == 'true',
|
|
method: response.headers['x-face-method'] ?? 'unknown',
|
|
landmarkCount: int.tryParse(response.headers['x-face-landmark-count'] ?? '0') ?? 0,
|
|
);
|
|
}
|
|
|
|
Future<SavedFaceLandmarks> getSavedFaceLandmarks(int photoId) async {
|
|
final uri = Uri.parse('$baseUrl/photos/$photoId/face-landmarks');
|
|
final response = await http.get(uri);
|
|
_check(response);
|
|
return SavedFaceLandmarks.fromJson(jsonDecode(response.body) as Map<String, dynamic>);
|
|
}
|
|
|
|
Future<void> saveFaceLandmarks(int photoId, Map<String, List<double>> manualLandmarks) async {
|
|
final uri = Uri.parse('$baseUrl/photos/$photoId/face-landmarks');
|
|
final response = await http.put(
|
|
uri,
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: jsonEncode({'manual_landmarks': manualLandmarks}),
|
|
);
|
|
_check(response);
|
|
}
|
|
|
|
Future<void> deleteSavedFaceLandmarks(int photoId) async {
|
|
final uri = Uri.parse('$baseUrl/photos/$photoId/face-landmarks');
|
|
final response = await http.delete(uri);
|
|
_check(response);
|
|
}
|
|
|
|
Future<FaceTransformPreview> getFaceGuidesPreview(int photoId, {Map<String, List<double>>? manualLandmarks}) async {
|
|
final uri = Uri.parse('$baseUrl/photos/$photoId/face-guides');
|
|
final response = await http.post(
|
|
uri,
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: jsonEncode({
|
|
if (manualLandmarks != null && manualLandmarks.isNotEmpty) 'manual_landmarks': manualLandmarks,
|
|
}),
|
|
);
|
|
_check(response);
|
|
return _facePreviewFromResponse(response);
|
|
}
|
|
|
|
Future<FaceTransformPreview> previewFaceTransform({
|
|
required int photoId,
|
|
required double globalStrength,
|
|
required double lipVolume,
|
|
required double upperLip,
|
|
required double lowerLip,
|
|
required double noseWidth,
|
|
required double noseTip,
|
|
required double chinProjection,
|
|
required double chinWidth,
|
|
required double jawline,
|
|
required double cheekbones,
|
|
required String? eyeColor,
|
|
required double eyeColorStrength,
|
|
required double eyeIrisSize,
|
|
List<List<double>>? eyePaintPoints,
|
|
required double eyePaintRadius,
|
|
required String? lipTint,
|
|
required double lipTintStrength,
|
|
required double cheekBlush,
|
|
required double browDarkness,
|
|
required double browLift,
|
|
required double skinSmooth,
|
|
required bool showGuides,
|
|
Map<String, List<double>>? manualLandmarks,
|
|
}) async {
|
|
final uri = Uri.parse('$baseUrl/photos/$photoId/face-transform/preview');
|
|
final response = await http.post(
|
|
uri,
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: jsonEncode(_faceTransformPayload(
|
|
globalStrength: globalStrength,
|
|
lipVolume: lipVolume,
|
|
upperLip: upperLip,
|
|
lowerLip: lowerLip,
|
|
noseWidth: noseWidth,
|
|
noseTip: noseTip,
|
|
chinProjection: chinProjection,
|
|
chinWidth: chinWidth,
|
|
jawline: jawline,
|
|
cheekbones: cheekbones,
|
|
eyeColor: eyeColor,
|
|
eyeColorStrength: eyeColorStrength,
|
|
eyeIrisSize: eyeIrisSize,
|
|
eyePaintPoints: eyePaintPoints,
|
|
eyePaintRadius: eyePaintRadius,
|
|
lipTint: lipTint,
|
|
lipTintStrength: lipTintStrength,
|
|
cheekBlush: cheekBlush,
|
|
browDarkness: browDarkness,
|
|
browLift: browLift,
|
|
skinSmooth: skinSmooth,
|
|
showGuides: showGuides,
|
|
manualLandmarks: manualLandmarks,
|
|
)),
|
|
);
|
|
_check(response);
|
|
return _facePreviewFromResponse(response);
|
|
}
|
|
|
|
Future<PatientPhoto> saveFaceTransform({
|
|
required int photoId,
|
|
required double globalStrength,
|
|
required double lipVolume,
|
|
required double upperLip,
|
|
required double lowerLip,
|
|
required double noseWidth,
|
|
required double noseTip,
|
|
required double chinProjection,
|
|
required double chinWidth,
|
|
required double jawline,
|
|
required double cheekbones,
|
|
required String? eyeColor,
|
|
required double eyeColorStrength,
|
|
required double eyeIrisSize,
|
|
List<List<double>>? eyePaintPoints,
|
|
required double eyePaintRadius,
|
|
required String? lipTint,
|
|
required double lipTintStrength,
|
|
required double cheekBlush,
|
|
required double browDarkness,
|
|
required double browLift,
|
|
required double skinSmooth,
|
|
required bool showGuides,
|
|
Map<String, List<double>>? manualLandmarks,
|
|
}) async {
|
|
final uri = Uri.parse('$baseUrl/photos/$photoId/face-transform');
|
|
final response = await http.post(
|
|
uri,
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: jsonEncode(_faceTransformPayload(
|
|
globalStrength: globalStrength,
|
|
lipVolume: lipVolume,
|
|
upperLip: upperLip,
|
|
lowerLip: lowerLip,
|
|
noseWidth: noseWidth,
|
|
noseTip: noseTip,
|
|
chinProjection: chinProjection,
|
|
chinWidth: chinWidth,
|
|
jawline: jawline,
|
|
cheekbones: cheekbones,
|
|
eyeColor: eyeColor,
|
|
eyeColorStrength: eyeColorStrength,
|
|
eyeIrisSize: eyeIrisSize,
|
|
eyePaintPoints: eyePaintPoints,
|
|
eyePaintRadius: eyePaintRadius,
|
|
lipTint: lipTint,
|
|
lipTintStrength: lipTintStrength,
|
|
cheekBlush: cheekBlush,
|
|
browDarkness: browDarkness,
|
|
browLift: browLift,
|
|
skinSmooth: skinSmooth,
|
|
showGuides: showGuides,
|
|
manualLandmarks: manualLandmarks,
|
|
)),
|
|
);
|
|
_check(response);
|
|
return PatientPhoto.fromJson(jsonDecode(response.body) as Map<String, dynamic>);
|
|
}
|
|
|
|
Future<File> downloadPhotoFile(PatientPhoto photo) async {
|
|
final response = await http.get(Uri.parse(photoFileUrl(photo.id)));
|
|
_check(response);
|
|
final directory = await getTemporaryDirectory();
|
|
final extension = photo.filename.contains('.') ? photo.filename.split('.').last : 'jpg';
|
|
final file = File('${directory.path}/photo_${photo.id}_${DateTime.now().millisecondsSinceEpoch}.$extension');
|
|
await file.writeAsBytes(response.bodyBytes);
|
|
return file;
|
|
}
|
|
|
|
Future<PatientPhoto> updatePatientPhoto({
|
|
required int photoId,
|
|
String? kind,
|
|
String? notes,
|
|
}) async {
|
|
final uri = Uri.parse('$baseUrl/photos/$photoId');
|
|
final response = await http.patch(
|
|
uri,
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: jsonEncode({
|
|
if (kind != null) 'kind': _nullIfEmpty(kind),
|
|
if (notes != null) 'notes': _nullIfEmpty(notes),
|
|
}),
|
|
);
|
|
_check(response);
|
|
return PatientPhoto.fromJson(jsonDecode(response.body) as Map<String, dynamic>);
|
|
}
|
|
|
|
Future<void> deletePatientPhoto(int photoId) async {
|
|
final uri = Uri.parse('$baseUrl/photos/$photoId');
|
|
final response = await http.delete(uri);
|
|
_check(response);
|
|
}
|
|
|
|
|
|
Future<List<ImmichPerson>> getImmichPeople({String? query}) async {
|
|
final uri = Uri.parse('$baseUrl/immich/people').replace(
|
|
queryParameters: query == null || query.trim().isEmpty ? null : {'q': query.trim()},
|
|
);
|
|
final response = await http.get(uri);
|
|
_check(response);
|
|
final data = jsonDecode(response.body) as List<dynamic>;
|
|
return data.map((e) => ImmichPerson.fromJson(e as Map<String, dynamic>, baseUrl)).toList();
|
|
}
|
|
|
|
String immichPersonThumbnailUrl(String personId) => '$baseUrl/immich/people/$personId/thumbnail';
|
|
|
|
Future<List<ImmichAsset>> getImmichPersonAssets(String personId) async {
|
|
final uri = Uri.parse('$baseUrl/immich/people/$personId/assets');
|
|
final response = await http.get(uri);
|
|
_check(response);
|
|
final data = jsonDecode(response.body) as List<dynamic>;
|
|
return data.map((e) => ImmichAsset.fromJson(e as Map<String, dynamic>, baseUrl)).toList();
|
|
}
|
|
|
|
Future<PatientPhoto> importImmichAsset({
|
|
required int patientId,
|
|
required String assetId,
|
|
}) async {
|
|
final uri = Uri.parse('$baseUrl/patients/$patientId/photos/from-immich-asset/$assetId');
|
|
final response = await http.post(uri);
|
|
_check(response);
|
|
return PatientPhoto.fromJson(jsonDecode(response.body) as Map<String, dynamic>);
|
|
}
|
|
|
|
Future<PatientPhoto> importImmichPersonFace({
|
|
required int patientId,
|
|
required String personId,
|
|
}) async {
|
|
final uri = Uri.parse('$baseUrl/patients/$patientId/photos/from-immich-person/$personId');
|
|
final response = await http.post(uri);
|
|
_check(response);
|
|
return PatientPhoto.fromJson(jsonDecode(response.body) as Map<String, dynamic>);
|
|
}
|
|
|
|
|
|
Future<List<ConsultationRecord>> getPatientConsultations(int patientId) async {
|
|
final uri = Uri.parse('$baseUrl/patients/$patientId/consultations');
|
|
final response = await http.get(uri);
|
|
_check(response);
|
|
final data = jsonDecode(response.body) as List<dynamic>;
|
|
return data.map((e) => ConsultationRecord.fromJson(e as Map<String, dynamic>)).toList();
|
|
}
|
|
|
|
Future<ConsultationRecord> createConsultation({
|
|
required int patientId,
|
|
String? consultationDate,
|
|
String? procedure,
|
|
String? reason,
|
|
String? plan,
|
|
String? followUp,
|
|
String? status,
|
|
String? notes,
|
|
}) async {
|
|
final uri = Uri.parse('$baseUrl/patients/$patientId/consultations');
|
|
final response = await http.post(
|
|
uri,
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: jsonEncode({
|
|
'consultation_date': _nullIfEmpty(consultationDate),
|
|
'procedure': _nullIfEmpty(procedure),
|
|
'reason': _nullIfEmpty(reason),
|
|
'plan': _nullIfEmpty(plan),
|
|
'follow_up': _nullIfEmpty(followUp),
|
|
'status': _nullIfEmpty(status) ?? 'prévu',
|
|
'notes': _nullIfEmpty(notes),
|
|
}),
|
|
);
|
|
_check(response);
|
|
return ConsultationRecord.fromJson(jsonDecode(response.body) as Map<String, dynamic>);
|
|
}
|
|
|
|
Future<ConsultationRecord> updateConsultation({
|
|
required int consultationId,
|
|
String? consultationDate,
|
|
String? procedure,
|
|
String? reason,
|
|
String? plan,
|
|
String? followUp,
|
|
String? status,
|
|
String? notes,
|
|
}) async {
|
|
final uri = Uri.parse('$baseUrl/consultations/$consultationId');
|
|
final response = await http.patch(
|
|
uri,
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: jsonEncode({
|
|
if (consultationDate != null) 'consultation_date': _nullIfEmpty(consultationDate),
|
|
if (procedure != null) 'procedure': _nullIfEmpty(procedure),
|
|
if (reason != null) 'reason': _nullIfEmpty(reason),
|
|
if (plan != null) 'plan': _nullIfEmpty(plan),
|
|
if (followUp != null) 'follow_up': _nullIfEmpty(followUp),
|
|
if (status != null) 'status': _nullIfEmpty(status),
|
|
if (notes != null) 'notes': _nullIfEmpty(notes),
|
|
}),
|
|
);
|
|
_check(response);
|
|
return ConsultationRecord.fromJson(jsonDecode(response.body) as Map<String, dynamic>);
|
|
}
|
|
|
|
Future<void> deleteConsultation(int consultationId) async {
|
|
final uri = Uri.parse('$baseUrl/consultations/$consultationId');
|
|
final response = await http.delete(uri);
|
|
_check(response);
|
|
}
|
|
|
|
|
|
Future<List<DocumentRecord>> getPatientDocuments(int patientId) async {
|
|
final uri = Uri.parse('$baseUrl/patients/$patientId/documents');
|
|
final response = await http.get(uri);
|
|
_check(response);
|
|
final data = jsonDecode(response.body) as List<dynamic>;
|
|
return data.map((e) => DocumentRecord.fromJson(e as Map<String, dynamic>)).toList();
|
|
}
|
|
|
|
Future<DocumentRecord> createDocument({
|
|
required int patientId,
|
|
int? consultationId,
|
|
required String documentType,
|
|
required String title,
|
|
required String status,
|
|
String? content,
|
|
String? signedBy,
|
|
String? signedAt,
|
|
}) async {
|
|
final uri = Uri.parse('$baseUrl/patients/$patientId/documents');
|
|
final response = await http.post(
|
|
uri,
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: jsonEncode({
|
|
'consultation_id': consultationId,
|
|
'document_type': documentType,
|
|
'title': title,
|
|
'status': status,
|
|
'content': _nullIfEmpty(content),
|
|
'signed_by': _nullIfEmpty(signedBy),
|
|
'signed_at': _nullIfEmpty(signedAt),
|
|
}),
|
|
);
|
|
_check(response);
|
|
return DocumentRecord.fromJson(jsonDecode(response.body) as Map<String, dynamic>);
|
|
}
|
|
|
|
Future<DocumentRecord> updateDocument({
|
|
required int documentId,
|
|
int? consultationId,
|
|
String? documentType,
|
|
String? title,
|
|
String? status,
|
|
String? content,
|
|
String? signedBy,
|
|
String? signedAt,
|
|
}) async {
|
|
final uri = Uri.parse('$baseUrl/documents/$documentId');
|
|
final response = await http.patch(
|
|
uri,
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: jsonEncode({
|
|
if (consultationId != null) 'consultation_id': consultationId,
|
|
if (documentType != null) 'document_type': documentType,
|
|
if (title != null) 'title': _nullIfEmpty(title),
|
|
if (status != null) 'status': status,
|
|
if (content != null) 'content': _nullIfEmpty(content),
|
|
if (signedBy != null) 'signed_by': _nullIfEmpty(signedBy),
|
|
if (signedAt != null) 'signed_at': _nullIfEmpty(signedAt),
|
|
}),
|
|
);
|
|
_check(response);
|
|
return DocumentRecord.fromJson(jsonDecode(response.body) as Map<String, dynamic>);
|
|
}
|
|
|
|
Future<void> deleteDocument(int documentId) async {
|
|
final uri = Uri.parse('$baseUrl/documents/$documentId');
|
|
final response = await http.delete(uri);
|
|
_check(response);
|
|
}
|
|
|
|
String documentPdfUrl(int documentId) => '$baseUrl/documents/$documentId/pdf';
|
|
|
|
void _check(http.Response response) {
|
|
if (response.statusCode < 200 || response.statusCode >= 300) {
|
|
throw Exception('HTTP ${response.statusCode}: ${response.body}');
|
|
}
|
|
}
|
|
|
|
String? _nullIfEmpty(String? value) {
|
|
if (value == null) return null;
|
|
final cleaned = value.trim();
|
|
return cleaned.isEmpty ? null : cleaned;
|
|
}
|
|
}
|