Écran « Activité » sur GET /api/Audit, réservé au SuperAdmin comme la policy de l'endpoint (menuId 13, conditionné à role.value == 0). Filtres instance, type d'entité, utilisateur et plage de dates, pagination 50, clic sur une ligne pour le détail avant/après en table plutôt que le JSON brut. Les ids d'instance et d'utilisateur sont résolus en noms, un id absent de l'annuaire restant affiché tel quel : le journal doit rester lisible après la suppression de ce qu'il décrit. manager_api_new n'est pas touché. L'écran passe par un http.get direct au Bearer, comme _loadKnowledge / _loadInsights / _reindex du Guide IA : une lecture seule ne justifie pas d'étendre un client qui s'édite à la main, et le déclencheur de génération n'existe plus depuis le lot A. Côté utilisateurs, compteur « X / 5 » et bouton d'ajout désactivé au plafond. Le SuperAdmin en est exclu : son GET /api/User renvoie toutes les instances, compter cette liste contre un plafond par instance n'aurait aucun sens. Le masquage du rôle SuperAdmin pour un InstanceAdmin, resté en question dans todo-features.md, était déjà fait — _allowedRoles filtre sur r >= callerRole. Trois pièges relevés en câblant : - AuditController renvoie les entités brutes, pas un DTO — les champs sont ceux d'AuditLog en camelCase. - invokeAPI ne lève pas sur un code d'erreur et son résultat était ignoré dans users_screen : un e-mail déjà utilisé (409) ou un rôle refusé (403) ne produisait aucun message. Le statut est désormais testé et le corps affiché, ce qui rendra visible sans retouche le futur 422 du plafond. - DropdownButtonFormField ne relit pas initialValue sur reconstruction (FormField.didUpdateWidget ne traite que forceErrorText) : « Réinitialiser les filtres » vidait la requête sans vider l'affichage. Remplacé par un DropdownButton piloté. 40 clés i18n FR/EN/NL, flutter gen-l10n relancé. flutter analyze propre sur les dossiers travaillés, flutter build web vert. Deux dettes serveur restent ouvertes, hors périmètre de ce commit : le plafond de 5 n'est pas appliqué par UserController (ce qui est livré ici est un garde-fou d'interface, un POST direct passe toujours), et aucune section n'est journalisée — AuditedTypes.Contains exige l'égalité exacte alors que Section est abstraite. Détail dans DOCS/todo-features.md et v1-plan.md lot F. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
193 lines
5.5 KiB
Dart
193 lines
5.5 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:manager_app/Models/resourceTypeModel.dart';
|
|
import 'package:manager_api_new/api.dart';
|
|
|
|
// Colors - TO FILL WIT CORRECT COLOR
|
|
//const kBackgroundColor = Color(0xFFFFFFFF);
|
|
const kTitleTextColor = Color(0xFF303030);
|
|
const kBodyTextColor = Color(0xFF393939); // TODO
|
|
|
|
const kBackgroundColor = Color(0xFFf5f5f7);
|
|
const kPrimaryColor = Color(0xFF264863); // #264863 // 308aae
|
|
const kError = Color(0xFFCA413F);
|
|
const kTextLightColor = Color(0xFFFCFDFD);
|
|
const kSecond = Color(0xFFC2C9D6);
|
|
const kWhite = Color(0xFFFFFFFF);
|
|
const kBlack = Color(0xFF000000);
|
|
const kSuccess = Color(0xFF8bc34a);
|
|
|
|
// Responsive
|
|
const kBreakpointMobile = 850.0;
|
|
|
|
// Shadows
|
|
const kDefaultShadow = BoxShadow(
|
|
color: kSecond,
|
|
spreadRadius: 0.5,
|
|
blurRadius: 5,
|
|
offset: Offset(0, 1.5),
|
|
);
|
|
|
|
// TextStyles for cards
|
|
const kCardTitleStyle = TextStyle(
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.w600,
|
|
color: kWhite,
|
|
);
|
|
const kCardSubtitleStyle = TextStyle(
|
|
fontSize: 11,
|
|
fontWeight: FontWeight.w300,
|
|
color: Color(0xCCFFFFFF),
|
|
);
|
|
const kSectionLabelStyle = TextStyle(
|
|
fontSize: 13,
|
|
fontWeight: FontWeight.w600,
|
|
color: kTitleTextColor,
|
|
);
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Socle visuel — l'échelle typographique et les espacements viennent du CSS des
|
|
// maquettes validées (DOCS/claude design/guide-ia-screen.html et
|
|
// statistics-screen.html, tokens identiques dans les deux).
|
|
//
|
|
// Les couleurs, elles, restent celles de l'app : un écran neuf doit se fondre
|
|
// dans manager-app, pas y ouvrir une seconde palette. Seules les valeurs sans
|
|
// équivalent existant sont reprises des maquettes — les remplissages discrets,
|
|
// les bordures, le gris atténué et l'ambre d'avertissement.
|
|
// Pas de thème sombre : l'app n'en a pas.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const kGround = kBackgroundColor;
|
|
const kSurface = kWhite;
|
|
const kSurface2 = Color(0xFFF3F5F8);
|
|
const kSurface3 = Color(0xFFE7EBF1);
|
|
|
|
const kInk = kTitleTextColor;
|
|
const kInk2 = kBodyTextColor;
|
|
const kInk3 = Color(0xFF7C8B9A);
|
|
|
|
const kLine = Color(0xFFD6DDE5);
|
|
const kLineSoft = Color(0xFFE6EBF1);
|
|
|
|
const kBrand = kPrimaryColor;
|
|
const kBrand2 = Color(0xFF3C6C90);
|
|
const kBrandSoft = kSecond;
|
|
const kOnBrand = kWhite;
|
|
|
|
const kWarning = Color(0xFF9A6608);
|
|
const kSerious = kError;
|
|
const kGood = kSuccess;
|
|
|
|
const kMonoFamily = 'monospace';
|
|
|
|
const kSpace1 = 4.0;
|
|
const kSpace2 = 6.0;
|
|
const kSpace3 = 8.0;
|
|
const kSpace4 = 12.0;
|
|
const kSpace5 = 16.0;
|
|
const kSpace6 = 18.0;
|
|
const kSpace7 = 24.0;
|
|
const kSpace8 = 32.0;
|
|
|
|
const kRadiusInput = 5.0;
|
|
const kRadiusCard = 8.0;
|
|
const kRadiusShell = 10.0;
|
|
const kRadiusTrack = 4.0;
|
|
const kRadiusPill = 999.0;
|
|
|
|
const kCardPadding = EdgeInsets.fromLTRB(18, 17, 18, 19);
|
|
const kPagePadding = EdgeInsets.fromLTRB(28, 24, 28, 30);
|
|
|
|
const kCardShadow = [kDefaultShadow];
|
|
|
|
const kTitleScreen = TextStyle(
|
|
fontSize: 23,
|
|
fontWeight: FontWeight.w700,
|
|
letterSpacing: -0.41,
|
|
color: kInk,
|
|
);
|
|
const kTitleCard = TextStyle(
|
|
fontSize: 14.5,
|
|
fontWeight: FontWeight.w700,
|
|
color: kInk,
|
|
);
|
|
const kSubtitleCard = TextStyle(
|
|
fontSize: 12.5,
|
|
color: kInk3,
|
|
);
|
|
const kTextBody = TextStyle(
|
|
fontSize: 13.5,
|
|
color: kInk,
|
|
);
|
|
const kTextSmall = TextStyle(
|
|
fontSize: 13,
|
|
color: kInk2,
|
|
);
|
|
const kLabelField = TextStyle(
|
|
fontSize: 12.5,
|
|
fontWeight: FontWeight.w600,
|
|
color: kInk2,
|
|
);
|
|
const kTextHint = TextStyle(
|
|
fontSize: 12,
|
|
color: kInk3,
|
|
);
|
|
const kOverline = TextStyle(
|
|
fontSize: 11.5,
|
|
letterSpacing: 0.69,
|
|
color: kInk3,
|
|
);
|
|
const kOverlineMono = TextStyle(
|
|
fontFamily: kMonoFamily,
|
|
fontSize: 10.5,
|
|
letterSpacing: 0.95,
|
|
color: kInk3,
|
|
);
|
|
const kMetricLarge = TextStyle(
|
|
fontFamily: kMonoFamily,
|
|
fontSize: 29,
|
|
fontWeight: FontWeight.w600,
|
|
letterSpacing: -0.73,
|
|
height: 1,
|
|
color: kInk,
|
|
fontFeatures: [FontFeature.tabularFigures()],
|
|
);
|
|
const kMetricMedium = TextStyle(
|
|
fontFamily: kMonoFamily,
|
|
fontSize: 26,
|
|
fontWeight: FontWeight.w600,
|
|
letterSpacing: -0.52,
|
|
color: kInk,
|
|
fontFeatures: [FontFeature.tabularFigures()],
|
|
);
|
|
|
|
const List<String> section_types = ["Map", "Slider", "Video", "Web", "Menu", "Quiz", "Article", "PDF", "Game", "Agenda", "Weather", "Event", "Parcours"];
|
|
const List<String> map_types = ["none", "normal", "satellite", "terrain", "hybrid"];
|
|
const List<String> languages = ["FR", "NL", "EN", "DE", "IT", "ES", "CN", "PL", "AR", "UK"];
|
|
const List<String> map_types_mapBox = ["standard", "streets", "outdoors", "light", "dark", "satellite", "satellite_streets"];
|
|
const List<String> map_providers = ["Google", "MapBox"];
|
|
|
|
/// Plafond d'utilisateurs par instance, SuperAdmin exclu.
|
|
/// ⚠️ Contrôle d'interface uniquement : `UserController` ne compte rien côté serveur,
|
|
/// un POST direct sur l'API passe toujours.
|
|
const kMaxUsersPerInstance = 5;
|
|
|
|
const kTitleMaxLength = 165;
|
|
const kDescriptionMaxLength = 2500;
|
|
|
|
List<ResourceTypeModel> resource_types = [
|
|
ResourceTypeModel(label: "Image", type: ResourceType.Image),
|
|
ResourceTypeModel(label: "Image url", type: ResourceType.ImageUrl),
|
|
ResourceTypeModel(label: "Vidéo", type: ResourceType.Video),
|
|
ResourceTypeModel(label: "Vidéo url", type: ResourceType.VideoUrl),
|
|
ResourceTypeModel(label: "Audio", type: ResourceType.Audio),
|
|
ResourceTypeModel(label: "PDF", type: ResourceType.Pdf),
|
|
ResourceTypeModel(label: "JSON", type: ResourceType.Json),
|
|
ResourceTypeModel(label: "JSON url", type: ResourceType.JsonUrl)
|
|
]; // "video url" , "video", {"label": "image"}, "image url", "audio"
|
|
|
|
/*
|
|
const kTextStyle = TextStyle(
|
|
fontSize: 23,
|
|
color: kPrimaryColor,
|
|
fontWeight: FontWeight.w600,
|
|
);*/ |