chore(wip): consolidation intermédiaire multi-tickets (sprints Statistiques, UI, Bug resolution, Serveur-client)

Regroupe l'état de travail en cours réalisé dans un même worktree sur
plusieurs tickets/sprints (#85, #136, #145, #155-160, #162-164),
mélangeant des tickets QA et inProgress. Ne constitue pas une feature
terminée : commit de sauvegarde avant triage/split par ticket en
branches feature/* dédiées. Exclut les dossiers d'environnement de
build locaux et le heap dump parasite (.gitignore mis à jour).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-28 16:48:54 +02:00
parent 58272e354a
commit 917777e18b
279 changed files with 13546 additions and 674 deletions

View File

@ -30,14 +30,21 @@ final class ShareApi {
}
try {
final body = await _readJsonObject(request);
final shareKind = _optionalString(body, 'shareKind') ?? 'single';
final result = await createShare.execute(
senderUserId: authenticated.user.id,
resourceType: _requiredString(body, 'resourceType'),
payloadJson: _requiredObject(body, 'payload'),
shareKind: shareKind,
packName: _optionalString(body, 'packName'),
resourceType: _optionalString(body, 'resourceType'),
payloadJson: _optionalObject(body, 'payload'),
packItems: shareKind == ShareKind.pack.wireName
? _packItems(body['items'])
: const [],
recipientEmails: _requiredStringList(body, 'recipientEmails'),
);
return _jsonResponse(201, {
'shareId': result.share.id,
'shareKind': result.share.kind.wireName,
'recipientUserIds': result.recipientUserIds,
'unresolvedEmails': result.unresolvedEmails,
});
@ -62,7 +69,9 @@ final class ShareApi {
{
'shareId': item.share.id,
'senderUserId': item.share.senderUserId,
'resourceType': item.share.resourceType.wireName,
'shareKind': item.share.kind.wireName,
'packName': item.share.packName,
'resourceType': item.share.resourceType?.wireName,
'payload': item.share.payloadJson,
'status': item.recipient.status.wireName,
'createdAt': item.share.createdAt.toIso8601String(),
@ -83,7 +92,12 @@ final class ShareApi {
recipientUserId: authenticated.user.id,
);
return _jsonResponse(200, {
'createdResource': _resourceJson(result.createdResource),
if (result.createdResources.length == 1)
'createdResource': _resourceJson(result.createdResource),
'createdResources': [
for (final resource in result.createdResources)
_resourceJson(resource),
],
});
} on ShareNotFoundException catch (error) {
return _errorResponse(404, error.message);
@ -140,6 +154,26 @@ Map<String, Object?> _resourceJson(SyncedResource resource) {
};
}
List<SharePackItemInput> _packItems(Object? rawItems) {
if (rawItems is! List) {
throw const FormatException('items must be an array.');
}
return [
for (final rawItem in rawItems)
if (rawItem is Map)
_packItem(Map<String, Object?>.from(rawItem))
else
throw const FormatException('items must contain only objects.'),
];
}
SharePackItemInput _packItem(Map<String, Object?> item) {
return SharePackItemInput(
resourceType: _requiredString(item, 'resourceType'),
payloadJson: _requiredObject(item, 'payload'),
);
}
Future<Map<String, Object?>> _readJsonObject(Request request) async {
final raw = await request.readAsString();
final decoded = jsonDecode(raw);
@ -157,6 +191,18 @@ String _requiredString(Map<String, Object?> body, String key) {
return value;
}
String? _optionalString(Map<String, Object?> body, String key) {
final value = body[key];
if (value == null) {
return null;
}
if (value is! String) {
throw FormatException('$key must be a string.');
}
final trimmed = value.trim();
return trimmed.isEmpty ? null : trimmed;
}
Map<String, Object?> _requiredObject(Map<String, Object?> body, String key) {
final value = body[key];
if (value is Map<String, Object?>) {
@ -168,6 +214,13 @@ Map<String, Object?> _requiredObject(Map<String, Object?> body, String key) {
throw FormatException('$key must be a JSON object.');
}
Map<String, Object?>? _optionalObject(Map<String, Object?> body, String key) {
if (!body.containsKey(key)) {
return null;
}
return _requiredObject(body, key);
}
List<String> _requiredStringList(Map<String, Object?> body, String key) {
final value = body[key];
if (value is! List) {