Initial upload: aesthetic-immich-starter
This commit is contained in:
@@ -0,0 +1,839 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user