Compare commits
9 Commits
fa232f8634
...
970e7063c5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
970e7063c5 | ||
|
|
22dece52ef | ||
|
|
7cd7c6fc9e | ||
|
|
744faa3a9c | ||
|
|
dbf6a972ba | ||
|
|
120434aba0 | ||
|
|
45dcd2171e | ||
|
|
07b8488714 | ||
|
|
3a2c8a808f |
BIN
android/app/src/main/res/drawable-hdpi/android12splash.png
Normal file
|
After Width: | Height: | Size: 162 KiB |
|
After Width: | Height: | Size: 28 KiB |
BIN
android/app/src/main/res/drawable-hdpi/splash.png
Normal file
|
After Width: | Height: | Size: 162 KiB |
BIN
android/app/src/main/res/drawable-mdpi/android12splash.png
Normal file
|
After Width: | Height: | Size: 68 KiB |
|
After Width: | Height: | Size: 14 KiB |
BIN
android/app/src/main/res/drawable-mdpi/splash.png
Normal file
|
After Width: | Height: | Size: 68 KiB |
BIN
android/app/src/main/res/drawable-night-hdpi/android12splash.png
Normal file
|
After Width: | Height: | Size: 162 KiB |
BIN
android/app/src/main/res/drawable-night-mdpi/android12splash.png
Normal file
|
After Width: | Height: | Size: 68 KiB |
BIN
android/app/src/main/res/drawable-night-v21/background.png
Normal file
|
After Width: | Height: | Size: 69 B |
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item>
|
||||
<bitmap android:gravity="fill" android:src="@drawable/background"/>
|
||||
</item>
|
||||
<item>
|
||||
<bitmap android:gravity="center" android:src="@drawable/splash"/>
|
||||
</item>
|
||||
</layer-list>
|
||||
|
After Width: | Height: | Size: 298 KiB |
|
After Width: | Height: | Size: 782 KiB |
|
After Width: | Height: | Size: 1.4 MiB |
BIN
android/app/src/main/res/drawable-night/background.png
Normal file
|
After Width: | Height: | Size: 69 B |
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item>
|
||||
<bitmap android:gravity="fill" android:src="@drawable/background"/>
|
||||
</item>
|
||||
<item>
|
||||
<bitmap android:gravity="center" android:src="@drawable/splash"/>
|
||||
</item>
|
||||
</layer-list>
|
||||
BIN
android/app/src/main/res/drawable-v21/background.png
Normal file
|
After Width: | Height: | Size: 69 B |
@@ -1,12 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Modify this file to customize your launch splash screen -->
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="?android:colorBackground" />
|
||||
|
||||
<!-- You can insert your own image assets here -->
|
||||
<!-- <item>
|
||||
<bitmap
|
||||
android:gravity="center"
|
||||
android:src="@mipmap/launch_image" />
|
||||
</item> -->
|
||||
<item>
|
||||
<bitmap android:gravity="fill" android:src="@drawable/background"/>
|
||||
</item>
|
||||
<item>
|
||||
<bitmap android:gravity="center" android:src="@drawable/splash"/>
|
||||
</item>
|
||||
</layer-list>
|
||||
|
||||
BIN
android/app/src/main/res/drawable-xhdpi/android12splash.png
Normal file
|
After Width: | Height: | Size: 298 KiB |
|
After Width: | Height: | Size: 48 KiB |
BIN
android/app/src/main/res/drawable-xhdpi/splash.png
Normal file
|
After Width: | Height: | Size: 298 KiB |
BIN
android/app/src/main/res/drawable-xxhdpi/android12splash.png
Normal file
|
After Width: | Height: | Size: 782 KiB |
|
After Width: | Height: | Size: 111 KiB |
BIN
android/app/src/main/res/drawable-xxhdpi/splash.png
Normal file
|
After Width: | Height: | Size: 782 KiB |
BIN
android/app/src/main/res/drawable-xxxhdpi/android12splash.png
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 208 KiB |
BIN
android/app/src/main/res/drawable-xxxhdpi/splash.png
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
BIN
android/app/src/main/res/drawable/background.png
Normal file
|
After Width: | Height: | Size: 69 B |
@@ -1,12 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Modify this file to customize your launch splash screen -->
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="@android:color/white" />
|
||||
|
||||
<!-- You can insert your own image assets here -->
|
||||
<!-- <item>
|
||||
<bitmap
|
||||
android:gravity="center"
|
||||
android:src="@mipmap/launch_image" />
|
||||
</item> -->
|
||||
<item>
|
||||
<bitmap android:gravity="fill" android:src="@drawable/background"/>
|
||||
</item>
|
||||
<item>
|
||||
<bitmap android:gravity="center" android:src="@drawable/splash"/>
|
||||
</item>
|
||||
</layer-list>
|
||||
|
||||
BIN
android/app/src/main/res/drawable/unionflow_logo.png
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
|
Before Width: | Height: | Size: 544 B After Width: | Height: | Size: 7.5 KiB |
|
Before Width: | Height: | Size: 442 B After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 721 B After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 39 KiB |
21
android/app/src/main/res/values-night-v31/styles.xml
Normal file
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
|
||||
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||
<item name="android:forceDarkAllowed">false</item>
|
||||
<item name="android:windowFullscreen">false</item>
|
||||
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
|
||||
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
|
||||
<item name="android:windowSplashScreenBackground">#0A0D1A</item>
|
||||
<item name="android:windowSplashScreenAnimatedIcon">@drawable/android12splash</item>
|
||||
</style>
|
||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||
This theme determines the color of the Android Window while your
|
||||
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||
running.
|
||||
|
||||
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||
<item name="android:windowBackground">?android:colorBackground</item>
|
||||
</style>
|
||||
</resources>
|
||||
@@ -5,6 +5,10 @@
|
||||
<!-- Show a splash screen on the activity. Automatically removed when
|
||||
the Flutter engine draws its first frame -->
|
||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||
<item name="android:forceDarkAllowed">false</item>
|
||||
<item name="android:windowFullscreen">false</item>
|
||||
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
|
||||
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
|
||||
</style>
|
||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||
This theme determines the color of the Android Window while your
|
||||
|
||||
21
android/app/src/main/res/values-v31/styles.xml
Normal file
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
|
||||
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||
<item name="android:forceDarkAllowed">false</item>
|
||||
<item name="android:windowFullscreen">false</item>
|
||||
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
|
||||
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
|
||||
<item name="android:windowSplashScreenBackground">#FFFFFF</item>
|
||||
<item name="android:windowSplashScreenAnimatedIcon">@drawable/android12splash</item>
|
||||
</style>
|
||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||
This theme determines the color of the Android Window while your
|
||||
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||
running.
|
||||
|
||||
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||
<item name="android:windowBackground">?android:colorBackground</item>
|
||||
</style>
|
||||
</resources>
|
||||
4
android/app/src/main/res/values/colors.xml
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_launcher_background">#FFFFFF</color>
|
||||
</resources>
|
||||
@@ -5,6 +5,10 @@
|
||||
<!-- Show a splash screen on the activity. Automatically removed when
|
||||
the Flutter engine draws its first frame -->
|
||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||
<item name="android:forceDarkAllowed">false</item>
|
||||
<item name="android:windowFullscreen">false</item>
|
||||
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
|
||||
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
|
||||
</style>
|
||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||
This theme determines the color of the Android Window while your
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G -XX:+HeapDumpOnOutOfMemoryError -Djava.net.preferIPv4Stack=true
|
||||
org.gradle.parallel=false
|
||||
org.gradle.workers.max=1
|
||||
android.useAndroidX=true
|
||||
android.enableJetifier=true
|
||||
|
||||
|
||||
BIN
assets/images/branding/lions_dev_dark.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
assets/images/branding/lions_dev_white.png
Normal file
|
After Width: | Height: | Size: 220 KiB |
BIN
assets/images/unionflow-logo.png
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
BIN
flutter_01.png
Normal file
|
After Width: | Height: | Size: 57 KiB |
0
flutter_02.png
Normal file
BIN
flutter_03.png
Normal file
|
After Width: | Height: | Size: 142 KiB |
BIN
flutter_04.png
Normal file
|
After Width: | Height: | Size: 148 KiB |
@@ -427,7 +427,7 @@
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
@@ -484,7 +484,7 @@
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
|
||||
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 295 B After Width: | Height: | Size: 1016 B |
|
Before Width: | Height: | Size: 406 B After Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 450 B After Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 282 B After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 462 B After Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 704 B After Width: | Height: | Size: 8.5 KiB |
|
Before Width: | Height: | Size: 406 B After Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 586 B After Width: | Height: | Size: 7.4 KiB |
|
Before Width: | Height: | Size: 862 B After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 862 B After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 6.3 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 762 B After Width: | Height: | Size: 6.9 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 26 KiB |
22
ios/Runner/Assets.xcassets/LaunchBackground.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "background.png",
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"filename" : "darkbackground.png",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
ios/Runner/Assets.xcassets/LaunchBackground.imageset/background.png
vendored
Normal file
|
After Width: | Height: | Size: 69 B |
BIN
ios/Runner/Assets.xcassets/LaunchBackground.imageset/darkbackground.png
vendored
Normal file
|
After Width: | Height: | Size: 69 B |
@@ -1,23 +1,23 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "LaunchImage.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "LaunchImage@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "LaunchImage@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 68 B After Width: | Height: | Size: 68 KiB |
|
Before Width: | Height: | Size: 68 B After Width: | Height: | Size: 298 KiB |
|
Before Width: | Height: | Size: 68 B After Width: | Height: | Size: 782 KiB |
@@ -16,13 +16,19 @@
|
||||
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
|
||||
</imageView>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleToFill" image="LaunchBackground" translatesAutoresizingMaskIntoConstraints="NO" id="tWc-Dq-wcI"/>
|
||||
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4"></imageView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<constraints>
|
||||
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
|
||||
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>
|
||||
<constraint firstItem="YRO-k0-Ey4" firstAttribute="leading" secondItem="Ze5-6b-2t3" secondAttribute="leading" id="3T2-ad-Qdv"/>
|
||||
<constraint firstItem="tWc-Dq-wcI" firstAttribute="bottom" secondItem="Ze5-6b-2t3" secondAttribute="bottom" id="RPx-PI-7Xg"/>
|
||||
<constraint firstItem="tWc-Dq-wcI" firstAttribute="top" secondItem="Ze5-6b-2t3" secondAttribute="top" id="SdS-ul-q2q"/>
|
||||
<constraint firstAttribute="trailing" secondItem="tWc-Dq-wcI" secondAttribute="trailing" id="Swv-Gf-Rwn"/>
|
||||
<constraint firstAttribute="trailing" secondItem="YRO-k0-Ey4" secondAttribute="trailing" id="TQA-XW-tRk"/>
|
||||
<constraint firstItem="YRO-k0-Ey4" firstAttribute="bottom" secondItem="Ze5-6b-2t3" secondAttribute="bottom" id="duK-uY-Gun"/>
|
||||
<constraint firstItem="tWc-Dq-wcI" firstAttribute="leading" secondItem="Ze5-6b-2t3" secondAttribute="leading" id="kV7-tw-vXt"/>
|
||||
<constraint firstItem="YRO-k0-Ey4" firstAttribute="top" secondItem="Ze5-6b-2t3" secondAttribute="top" id="xPn-NY-SIU"/>
|
||||
</constraints>
|
||||
</view>
|
||||
</viewController>
|
||||
@@ -32,6 +38,7 @@
|
||||
</scene>
|
||||
</scenes>
|
||||
<resources>
|
||||
<image name="LaunchImage" width="168" height="185"/>
|
||||
<image name="LaunchImage" width="1024" height="1024"/>
|
||||
<image name="LaunchBackground" width="1" height="1"/>
|
||||
</resources>
|
||||
</document>
|
||||
|
||||
@@ -66,5 +66,7 @@
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>UIStatusBarHidden</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -46,7 +46,13 @@ class UnionFlowApp extends StatelessWidget {
|
||||
],
|
||||
child: Consumer2<LocaleProvider, ThemeProvider>(
|
||||
builder: (context, locale, theme, child) {
|
||||
return MaterialApp(
|
||||
return BlocListener<AuthBloc, AuthState>(
|
||||
listenWhen: (prev, curr) =>
|
||||
curr is AuthAuthenticated && prev is! AuthAuthenticated,
|
||||
listener: (context, _) {
|
||||
context.read<OrgSwitcherBloc>().add(const OrgSwitcherLoadRequested());
|
||||
},
|
||||
child: MaterialApp(
|
||||
title: 'UnionFlow',
|
||||
debugShowCheckedModeBanner: false,
|
||||
scaffoldMessengerKey: UnionFlowApp.scaffoldMessengerKey,
|
||||
@@ -79,6 +85,7 @@ class UnionFlowApp extends StatelessWidget {
|
||||
child: child ?? const SizedBox(),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
@@ -20,7 +20,7 @@ import '../../features/adhesions/presentation/pages/adhesions_page_wrapper.dart'
|
||||
import '../../features/settings/presentation/pages/system_settings_page.dart';
|
||||
import '../../features/dashboard/presentation/pages/advanced_dashboard_page.dart';
|
||||
import '../../features/admin/presentation/pages/user_management_page.dart';
|
||||
import '../../features/communication/presentation/pages/conversations_page.dart';
|
||||
import '../../features/communication/presentation/pages/conversations_page_wrapper.dart';
|
||||
import '../../features/finance_workflow/presentation/pages/pending_approvals_page.dart';
|
||||
import '../../features/finance_workflow/presentation/pages/budgets_list_page.dart';
|
||||
import '../../core/navigation/main_navigation_layout.dart';
|
||||
@@ -86,7 +86,7 @@ class AppRouter {
|
||||
'/reports': (context) => const ReportsPageWrapper(),
|
||||
'/finances': (context) => const CotisationsPageWrapper(),
|
||||
'/adhesions': (context) => const AdhesionsPageWrapper(),
|
||||
'/messages': (context) => const ConversationsPage(),
|
||||
'/messages': (context) => const ConversationsPageWrapper(),
|
||||
'/settings': (context) => const SystemSettingsPage(),
|
||||
'/analytics': (context) {
|
||||
final authState = context.read<AuthBloc>().state;
|
||||
|
||||
@@ -118,7 +118,7 @@ class _MainNavigationLayoutState extends State<MainNavigationLayout> {
|
||||
statusBarIconBrightness: Brightness.dark,
|
||||
),
|
||||
child: Scaffold(
|
||||
backgroundColor: ColorTokens.background,
|
||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||
body: SafeArea(
|
||||
top: true,
|
||||
bottom: false,
|
||||
@@ -199,12 +199,13 @@ class _PillNavigationBar extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: ColorTokens.surface,
|
||||
color: scheme.surface,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: ColorTokens.shadow,
|
||||
color: scheme.shadow.withOpacity(0.12),
|
||||
blurRadius: 12,
|
||||
offset: const Offset(0, -2),
|
||||
),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../shared/design_system/tokens/app_colors.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import '../../app/app.dart';
|
||||
@@ -35,17 +36,7 @@ class ApiClient {
|
||||
),
|
||||
);
|
||||
|
||||
// Intercepteur de Log (Uniquement en Dev)
|
||||
if (AppConfig.enableLogging) {
|
||||
_dio.interceptors.add(LogInterceptor(
|
||||
requestHeader: true,
|
||||
requestBody: true,
|
||||
responseBody: true,
|
||||
logPrint: (obj) => print('🌐 [API] $obj'),
|
||||
));
|
||||
}
|
||||
|
||||
// Intercepteur de Token & Refresh automatique
|
||||
// Intercepteur de Token & Refresh automatique (doit être AVANT le logger)
|
||||
_dio.interceptors.add(
|
||||
InterceptorsWrapper(
|
||||
onRequest: (options, handler) async {
|
||||
@@ -112,6 +103,16 @@ class ApiClient {
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
// Intercepteur de Log (après le token pour voir le Bearer dans les logs)
|
||||
if (AppConfig.enableLogging) {
|
||||
_dio.interceptors.add(LogInterceptor(
|
||||
requestHeader: true,
|
||||
requestBody: true,
|
||||
responseBody: true,
|
||||
logPrint: (obj) => print('🌐 [API] $obj'),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
void _forceLogout() {
|
||||
@@ -132,7 +133,7 @@ class ApiClient {
|
||||
),
|
||||
],
|
||||
),
|
||||
backgroundColor: Colors.orange.shade700,
|
||||
backgroundColor: AppColors.warning,
|
||||
duration: const Duration(seconds: 4),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
|
||||
@@ -77,6 +77,17 @@ abstract class WebSocketEvent {
|
||||
organizationId: json['organizationId'] as String?,
|
||||
);
|
||||
|
||||
case 'NOUVEAU_MESSAGE':
|
||||
case 'MESSAGE_SUPPRIME':
|
||||
case 'CONVERSATION_LUE':
|
||||
return ChatMessageEvent(
|
||||
eventType: eventType,
|
||||
timestamp: timestamp,
|
||||
data: data,
|
||||
conversationId: json['conversationId'] as String?,
|
||||
organizationId: json['organizationId'] as String?,
|
||||
);
|
||||
|
||||
default:
|
||||
return GenericEvent(
|
||||
eventType: eventType,
|
||||
@@ -144,6 +155,19 @@ class ContributionEvent extends WebSocketEvent {
|
||||
});
|
||||
}
|
||||
|
||||
class ChatMessageEvent extends WebSocketEvent {
|
||||
final String? conversationId;
|
||||
final String? organizationId;
|
||||
|
||||
ChatMessageEvent({
|
||||
required super.eventType,
|
||||
required super.timestamp,
|
||||
required super.data,
|
||||
this.conversationId,
|
||||
this.organizationId,
|
||||
});
|
||||
}
|
||||
|
||||
class GenericEvent extends WebSocketEvent {
|
||||
GenericEvent({
|
||||
required super.eventType,
|
||||
|
||||
@@ -41,9 +41,10 @@ class _UserManagementViewState extends State<_UserManagementView> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: AppColors.background,
|
||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||
appBar: UFAppBar(
|
||||
title: 'Gestion des utilisateurs',
|
||||
moduleGradient: ModuleColors.systemeGradient,
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.refresh, size: 20),
|
||||
@@ -51,7 +52,9 @@ class _UserManagementViewState extends State<_UserManagementView> {
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Column(
|
||||
body: SafeArea(
|
||||
top: false,
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
@@ -63,18 +66,18 @@ class _UserManagementViewState extends State<_UserManagementView> {
|
||||
prefixIcon: const Icon(Icons.search, size: 18),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(RadiusTokens.md),
|
||||
borderSide: const BorderSide(color: AppColors.lightBorder),
|
||||
borderSide: BorderSide(color: Theme.of(context).colorScheme.outline),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(RadiusTokens.md),
|
||||
borderSide: const BorderSide(color: AppColors.lightBorder),
|
||||
borderSide: BorderSide(color: Theme.of(context).colorScheme.outline),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(RadiusTokens.md),
|
||||
borderSide: const BorderSide(color: AppColors.primaryGreen),
|
||||
borderSide: BorderSide(color: Theme.of(context).colorScheme.primary),
|
||||
),
|
||||
filled: true,
|
||||
fillColor: AppColors.lightSurface,
|
||||
fillColor: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
),
|
||||
onSubmitted: (v) => context.read<AdminUsersBloc>().add(
|
||||
@@ -131,6 +134,7 @@ class _UserManagementViewState extends State<_UserManagementView> {
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -168,10 +172,10 @@ class _UserManagementViewState extends State<_UserManagementView> {
|
||||
],
|
||||
),
|
||||
),
|
||||
const Icon(
|
||||
Icon(
|
||||
Icons.chevron_right,
|
||||
size: 16,
|
||||
color: AppColors.textSecondaryLight,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -167,9 +167,56 @@ class KeycloakAuthService {
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Logout : révoque la session SSO côté Keycloak via le back-channel
|
||||
/// (POST /logout silencieux, sans navigateur), puis purge le stockage local.
|
||||
///
|
||||
/// Conforme OIDC RP-Initiated Logout. Ne lève jamais d'exception : la purge
|
||||
/// locale est garantie même si Keycloak est injoignable. Le statut du
|
||||
/// back-channel est tracé dans les logs pour diagnostic.
|
||||
Future<void> logout() async {
|
||||
final refresh = await _storage.read(key: _refreshK);
|
||||
|
||||
if (refresh == null || refresh.isEmpty) {
|
||||
AppLogger.info('KeycloakAuthService: no refresh token, skipping backchannel logout');
|
||||
} else {
|
||||
try {
|
||||
final response = await _dio.post(
|
||||
KeycloakConfig.logoutEndpoint,
|
||||
data: {
|
||||
'client_id': KeycloakConfig.clientId,
|
||||
'refresh_token': refresh,
|
||||
},
|
||||
options: Options(
|
||||
contentType: Headers.formUrlEncodedContentType,
|
||||
// Accepte tout statut < 600 — on interprète nous-mêmes ci-dessous.
|
||||
validateStatus: (_) => true,
|
||||
),
|
||||
);
|
||||
final code = response.statusCode ?? 0;
|
||||
if (code == 200 || code == 204) {
|
||||
AppLogger.info('KeycloakAuthService: SSO session revoked at Keycloak (HTTP $code)');
|
||||
} else if (code == 400) {
|
||||
// Refresh token déjà invalide côté Keycloak → idempotent, OK.
|
||||
AppLogger.info('KeycloakAuthService: refresh token already invalid (HTTP 400) — session considered revoked');
|
||||
} else {
|
||||
AppLogger.error(
|
||||
'KeycloakAuthService: backchannel logout returned HTTP $code — '
|
||||
'SSO session may still be active. Body: ${response.data}',
|
||||
);
|
||||
}
|
||||
} catch (e, st) {
|
||||
AppLogger.error(
|
||||
'KeycloakAuthService: backchannel logout network error — local logout will still proceed',
|
||||
error: e,
|
||||
stackTrace: st,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Purge locale toujours effectuée — l'utilisateur "se sent" déconnecté
|
||||
// immédiatement même si le serveur n'a pas pu être notifié.
|
||||
await _storage.deleteAll();
|
||||
AppLogger.info('KeycloakAuthService: session cleared');
|
||||
AppLogger.info('KeycloakAuthService: local session cleared');
|
||||
}
|
||||
|
||||
Future<void> _saveTokens(Map<String, dynamic> data) async {
|
||||
|
||||
@@ -15,7 +15,7 @@ enum UserRole {
|
||||
level: 100,
|
||||
displayName: 'Super Administrateur',
|
||||
description: 'Accès complet système et multi-organisations',
|
||||
color: 0xFF6C5CE7, // Violet sophistiqué
|
||||
color: 0xFF7616E8, // Violet UnionFlow (super admin)
|
||||
permissions: _superAdminPermissions,
|
||||
),
|
||||
|
||||
@@ -25,7 +25,7 @@ enum UserRole {
|
||||
level: 80,
|
||||
displayName: 'Administrateur',
|
||||
description: 'Gestion complète de l\'organisation',
|
||||
color: 0xFF0984E3, // Bleu corporate
|
||||
color: 0xFF2563EB, // Bleu primaire UnionFlow
|
||||
permissions: _orgAdminPermissions,
|
||||
),
|
||||
|
||||
@@ -45,7 +45,7 @@ enum UserRole {
|
||||
level: 58,
|
||||
displayName: 'Consultant',
|
||||
description: 'Accès consultant et conseil',
|
||||
color: 0xFF6C5CE7, // Violet
|
||||
color: 0xFF5297FF, // Bleu intermédiaire (consultant)
|
||||
permissions: _consultantPermissions,
|
||||
),
|
||||
|
||||
@@ -55,7 +55,7 @@ enum UserRole {
|
||||
level: 52,
|
||||
displayName: 'Gestionnaire RH',
|
||||
description: 'Gestion des ressources humaines',
|
||||
color: 0xFF0984E3, // Bleu
|
||||
color: 0xFF1D4ED8, // Bleu foncé (RH)
|
||||
permissions: _hrManagerPermissions,
|
||||
),
|
||||
|
||||
@@ -65,7 +65,7 @@ enum UserRole {
|
||||
level: 40,
|
||||
displayName: 'Membre Actif',
|
||||
description: 'Participation active aux activités',
|
||||
color: 0xFF00B894, // Vert communauté
|
||||
color: 0xFF22C55E, // Vert succès (membre actif)
|
||||
permissions: _activeMemberPermissions,
|
||||
),
|
||||
|
||||
@@ -75,7 +75,7 @@ enum UserRole {
|
||||
level: 20,
|
||||
displayName: 'Membre',
|
||||
description: 'Accès aux informations de base',
|
||||
color: 0xFF00CEC9, // Teal simple
|
||||
color: 0xFF60A5FA, // Bleu clair (membre simple)
|
||||
permissions: _simpleMemberPermissions,
|
||||
),
|
||||
|
||||
@@ -85,7 +85,7 @@ enum UserRole {
|
||||
level: 0,
|
||||
displayName: 'Visiteur',
|
||||
description: 'Accès aux informations publiques',
|
||||
color: 0xFF6C5CE7, // Indigo accueillant
|
||||
color: 0xFF94A3B8, // Gris neutre (visiteur)
|
||||
permissions: _visitorPermissions,
|
||||
);
|
||||
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
/// Datasource distant pour la communication (API)
|
||||
/// Datasource distant pour la messagerie v4 — /api/messagerie/*
|
||||
library messaging_remote_datasource;
|
||||
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
import 'package:injectable/injectable.dart';
|
||||
|
||||
import '../../../../core/config/environment.dart';
|
||||
import '../../../../core/error/exceptions.dart';
|
||||
import '../../../authentication/data/datasources/keycloak_auth_service.dart';
|
||||
import '../models/message_model.dart';
|
||||
import '../models/conversation_model.dart';
|
||||
import '../../domain/entities/message.dart';
|
||||
import '../models/message_model.dart';
|
||||
import '../models/contact_policy_model.dart';
|
||||
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
@lazySingleton
|
||||
class MessagingRemoteDatasource {
|
||||
@@ -22,7 +24,7 @@ class MessagingRemoteDatasource {
|
||||
required this.authService,
|
||||
});
|
||||
|
||||
/// Headers HTTP avec authentification — rafraîchit le token si expiré (fix IC-03)
|
||||
/// Headers HTTP avec authentification — rafraîchit le token si expiré
|
||||
Future<Map<String, String>> _getHeaders() async {
|
||||
final token = await authService.getValidAccessToken();
|
||||
return {
|
||||
@@ -32,290 +34,271 @@ class MessagingRemoteDatasource {
|
||||
};
|
||||
}
|
||||
|
||||
// === CONVERSATIONS ===
|
||||
String get _base => '${AppConfig.apiBaseUrl}/api/messagerie';
|
||||
|
||||
Future<List<ConversationModel>> getConversations({
|
||||
String? organizationId,
|
||||
bool includeArchived = false,
|
||||
}) async {
|
||||
final uri = Uri.parse('${AppConfig.apiBaseUrl}/api/conversations')
|
||||
.replace(queryParameters: {
|
||||
if (organizationId != null) 'organisationId': organizationId,
|
||||
'includeArchived': includeArchived.toString(),
|
||||
});
|
||||
// ── Conversations ─────────────────────────────────────────────────────────
|
||||
|
||||
final response = await client.get(uri, headers: await _getHeaders());
|
||||
Future<List<ConversationSummaryModel>> getMesConversations() async {
|
||||
final response = await client.get(
|
||||
Uri.parse('$_base/conversations'),
|
||||
headers: await _getHeaders(),
|
||||
);
|
||||
|
||||
_checkAuth(response);
|
||||
if (response.statusCode == 200) {
|
||||
final List<dynamic> jsonList = json.decode(response.body);
|
||||
return jsonList
|
||||
.map((json) => ConversationModel.fromJson(json))
|
||||
final list = json.decode(response.body) as List<dynamic>;
|
||||
return list
|
||||
.map((j) => ConversationSummaryModel.fromJson(j as Map<String, dynamic>))
|
||||
.toList();
|
||||
} else if (response.statusCode == 401) {
|
||||
throw UnauthorizedException();
|
||||
} else {
|
||||
throw ServerException('Erreur lors de la récupération des conversations');
|
||||
}
|
||||
throw ServerException('Erreur récupération conversations (${response.statusCode})');
|
||||
}
|
||||
|
||||
Future<ConversationModel> getConversation(String id) async {
|
||||
final response = await client.get(
|
||||
Uri.parse('$_base/conversations/$id'),
|
||||
headers: await _getHeaders(),
|
||||
);
|
||||
|
||||
_checkAuth(response);
|
||||
if (response.statusCode == 200) {
|
||||
return ConversationModel.fromJson(json.decode(response.body) as Map<String, dynamic>);
|
||||
}
|
||||
if (response.statusCode == 404) throw NotFoundException('Conversation non trouvée');
|
||||
throw ServerException('Erreur récupération conversation (${response.statusCode})');
|
||||
}
|
||||
|
||||
Future<ConversationModel> demarrerConversationDirecte({
|
||||
required String destinataireId,
|
||||
required String organisationId,
|
||||
String? premierMessage,
|
||||
}) async {
|
||||
final body = json.encode({
|
||||
'destinataireId': destinataireId,
|
||||
'organisationId': organisationId,
|
||||
if (premierMessage != null) 'premierMessage': premierMessage,
|
||||
});
|
||||
|
||||
final response = await client.post(
|
||||
Uri.parse('$_base/conversations/directe'),
|
||||
headers: await _getHeaders(),
|
||||
body: body,
|
||||
);
|
||||
|
||||
_checkAuth(response);
|
||||
if (response.statusCode == 201 || response.statusCode == 200) {
|
||||
return ConversationModel.fromJson(json.decode(response.body) as Map<String, dynamic>);
|
||||
}
|
||||
throw ServerException('Erreur démarrage conversation directe (${response.statusCode})');
|
||||
}
|
||||
|
||||
Future<ConversationModel> demarrerConversationRole({
|
||||
required String roleCible,
|
||||
required String organisationId,
|
||||
String? premierMessage,
|
||||
}) async {
|
||||
final body = json.encode({
|
||||
'roleCible': roleCible,
|
||||
'organisationId': organisationId,
|
||||
if (premierMessage != null) 'premierMessage': premierMessage,
|
||||
});
|
||||
|
||||
final response = await client.post(
|
||||
Uri.parse('$_base/conversations/role'),
|
||||
headers: await _getHeaders(),
|
||||
body: body,
|
||||
);
|
||||
|
||||
_checkAuth(response);
|
||||
if (response.statusCode == 201 || response.statusCode == 200) {
|
||||
return ConversationModel.fromJson(json.decode(response.body) as Map<String, dynamic>);
|
||||
}
|
||||
throw ServerException('Erreur démarrage conversation rôle (${response.statusCode})');
|
||||
}
|
||||
|
||||
Future<void> archiverConversation(String id) async {
|
||||
final response = await client.delete(
|
||||
Uri.parse('$_base/conversations/$id'),
|
||||
headers: await _getHeaders(),
|
||||
);
|
||||
|
||||
_checkAuth(response);
|
||||
if (response.statusCode != 200 && response.statusCode != 204) {
|
||||
throw ServerException('Erreur archivage conversation (${response.statusCode})');
|
||||
}
|
||||
}
|
||||
|
||||
Future<ConversationModel> getConversationById(String conversationId) async {
|
||||
final uri = Uri.parse(
|
||||
'${AppConfig.apiBaseUrl}/api/conversations/$conversationId');
|
||||
// ── Messages ──────────────────────────────────────────────────────────────
|
||||
|
||||
Future<MessageModel> envoyerMessage(
|
||||
String conversationId, {
|
||||
required String typeMessage,
|
||||
String? contenu,
|
||||
String? urlFichier,
|
||||
int? dureeAudio,
|
||||
String? messageParentId,
|
||||
}) async {
|
||||
final body = json.encode({
|
||||
'typeMessage': typeMessage,
|
||||
if (contenu != null) 'contenu': contenu,
|
||||
if (urlFichier != null) 'urlFichier': urlFichier,
|
||||
if (dureeAudio != null) 'dureeAudio': dureeAudio,
|
||||
if (messageParentId != null) 'messageParentId': messageParentId,
|
||||
});
|
||||
|
||||
final response = await client.post(
|
||||
Uri.parse('$_base/conversations/$conversationId/messages'),
|
||||
headers: await _getHeaders(),
|
||||
body: body,
|
||||
);
|
||||
|
||||
_checkAuth(response);
|
||||
if (response.statusCode == 201 || response.statusCode == 200) {
|
||||
return MessageModel.fromJson(json.decode(response.body) as Map<String, dynamic>);
|
||||
}
|
||||
throw ServerException('Erreur envoi message (${response.statusCode})');
|
||||
}
|
||||
|
||||
Future<List<MessageModel>> getMessages(String conversationId, {int page = 0}) async {
|
||||
final uri = Uri.parse('$_base/conversations/$conversationId/messages')
|
||||
.replace(queryParameters: {'page': page.toString()});
|
||||
|
||||
final response = await client.get(uri, headers: await _getHeaders());
|
||||
|
||||
_checkAuth(response);
|
||||
if (response.statusCode == 200) {
|
||||
return ConversationModel.fromJson(json.decode(response.body));
|
||||
} else if (response.statusCode == 404) {
|
||||
throw NotFoundException('Conversation non trouvée');
|
||||
} else if (response.statusCode == 401) {
|
||||
throw UnauthorizedException();
|
||||
} else {
|
||||
throw ServerException('Erreur lors de la récupération de la conversation');
|
||||
final list = json.decode(response.body) as List<dynamic>;
|
||||
return list
|
||||
.map((j) => MessageModel.fromJson(j as Map<String, dynamic>))
|
||||
.toList();
|
||||
}
|
||||
throw ServerException('Erreur récupération messages (${response.statusCode})');
|
||||
}
|
||||
|
||||
Future<ConversationModel> createConversation({
|
||||
required String name,
|
||||
required List<String> participantIds,
|
||||
String? organizationId,
|
||||
String? description,
|
||||
}) async {
|
||||
final uri =
|
||||
Uri.parse('${AppConfig.apiBaseUrl}/api/conversations');
|
||||
|
||||
final body = json.encode({
|
||||
'name': name,
|
||||
'participantIds': participantIds,
|
||||
'type': 'GROUP', // Default to GROUP for multi-participant conversations
|
||||
if (organizationId != null) 'organisationId': organizationId,
|
||||
if (description != null) 'description': description,
|
||||
});
|
||||
|
||||
final response = await client.post(
|
||||
uri,
|
||||
headers: await _getHeaders(),
|
||||
body: body,
|
||||
);
|
||||
|
||||
if (response.statusCode == 201 || response.statusCode == 200) {
|
||||
return ConversationModel.fromJson(json.decode(response.body));
|
||||
} else if (response.statusCode == 401) {
|
||||
throw UnauthorizedException();
|
||||
} else {
|
||||
throw ServerException('Erreur lors de la création de la conversation');
|
||||
}
|
||||
}
|
||||
|
||||
// === MESSAGES ===
|
||||
|
||||
Future<List<MessageModel>> getMessages({
|
||||
required String conversationId,
|
||||
int? limit,
|
||||
String? beforeMessageId,
|
||||
}) async {
|
||||
final uri = Uri.parse('${AppConfig.apiBaseUrl}/api/messages')
|
||||
.replace(queryParameters: {
|
||||
'conversationId': conversationId,
|
||||
if (limit != null) 'limit': limit.toString(),
|
||||
// beforeMessageId not supported by backend yet, omit
|
||||
});
|
||||
|
||||
final response = await client.get(uri, headers: await _getHeaders());
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final List<dynamic> jsonList = json.decode(response.body);
|
||||
return jsonList.map((json) => MessageModel.fromJson(json)).toList();
|
||||
} else if (response.statusCode == 401) {
|
||||
throw UnauthorizedException();
|
||||
} else {
|
||||
throw ServerException('Erreur lors de la récupération des messages');
|
||||
}
|
||||
}
|
||||
|
||||
Future<MessageModel> sendMessage({
|
||||
required String conversationId,
|
||||
required String content,
|
||||
List<String>? attachments,
|
||||
MessagePriority priority = MessagePriority.normal,
|
||||
}) async {
|
||||
final uri = Uri.parse('${AppConfig.apiBaseUrl}/api/messages');
|
||||
|
||||
final body = json.encode({
|
||||
'conversationId': conversationId,
|
||||
'content': content,
|
||||
if (attachments != null) 'attachments': attachments,
|
||||
'priority': priority.name.toUpperCase(),
|
||||
});
|
||||
|
||||
final response = await client.post(
|
||||
uri,
|
||||
headers: await _getHeaders(),
|
||||
body: body,
|
||||
);
|
||||
|
||||
if (response.statusCode == 201 || response.statusCode == 200) {
|
||||
return MessageModel.fromJson(json.decode(response.body));
|
||||
} else if (response.statusCode == 401) {
|
||||
throw UnauthorizedException();
|
||||
} else {
|
||||
throw ServerException('Erreur lors de l\'envoi du message');
|
||||
}
|
||||
}
|
||||
|
||||
Future<MessageModel> sendBroadcast({
|
||||
required String organizationId,
|
||||
required String subject,
|
||||
required String content,
|
||||
MessagePriority priority = MessagePriority.normal,
|
||||
List<String>? attachments,
|
||||
}) async {
|
||||
final uri = Uri.parse('${AppConfig.apiBaseUrl}/api/messaging/broadcast');
|
||||
|
||||
final body = json.encode({
|
||||
'organizationId': organizationId,
|
||||
'subject': subject,
|
||||
'content': content,
|
||||
'priority': priority.name,
|
||||
if (attachments != null) 'attachments': attachments,
|
||||
});
|
||||
|
||||
final response = await client.post(
|
||||
uri,
|
||||
headers: await _getHeaders(),
|
||||
body: body,
|
||||
);
|
||||
|
||||
if (response.statusCode == 201 || response.statusCode == 200) {
|
||||
return MessageModel.fromJson(json.decode(response.body));
|
||||
} else if (response.statusCode == 401) {
|
||||
throw UnauthorizedException();
|
||||
} else if (response.statusCode == 403) {
|
||||
throw ForbiddenException('Permission insuffisante pour envoyer un broadcast');
|
||||
} else {
|
||||
throw ServerException('Erreur lors de l\'envoi du broadcast');
|
||||
}
|
||||
}
|
||||
|
||||
// === CONVERSATION ACTIONS ===
|
||||
|
||||
Future<void> archiveConversation(String conversationId, {bool archive = true}) async {
|
||||
final uri = Uri.parse(
|
||||
'${AppConfig.apiBaseUrl}/api/conversations/$conversationId/archive')
|
||||
.replace(queryParameters: {'archive': archive.toString()});
|
||||
|
||||
final response = await client.put(uri, headers: await _getHeaders());
|
||||
|
||||
if (response.statusCode != 200 && response.statusCode != 204) {
|
||||
if (response.statusCode == 401) {
|
||||
throw UnauthorizedException();
|
||||
} else {
|
||||
throw ServerException('Erreur lors de l\'archivage de la conversation');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> markConversationAsRead(String conversationId) async {
|
||||
final uri = Uri.parse(
|
||||
'${AppConfig.apiBaseUrl}/api/conversations/$conversationId/mark-read');
|
||||
|
||||
final response = await client.put(uri, headers: await _getHeaders());
|
||||
|
||||
if (response.statusCode != 200 && response.statusCode != 204) {
|
||||
if (response.statusCode == 401) {
|
||||
throw UnauthorizedException();
|
||||
} else {
|
||||
throw ServerException('Erreur lors du marquage de la conversation comme lue');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> toggleMuteConversation(String conversationId) async {
|
||||
final uri = Uri.parse(
|
||||
'${AppConfig.apiBaseUrl}/api/conversations/$conversationId/toggle-mute');
|
||||
|
||||
final response = await client.put(uri, headers: await _getHeaders());
|
||||
|
||||
if (response.statusCode != 200 && response.statusCode != 204) {
|
||||
if (response.statusCode == 401) {
|
||||
throw UnauthorizedException();
|
||||
} else {
|
||||
throw ServerException('Erreur lors du toggle mute de la conversation');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> togglePinConversation(String conversationId) async {
|
||||
final uri = Uri.parse(
|
||||
'${AppConfig.apiBaseUrl}/api/conversations/$conversationId/toggle-pin');
|
||||
|
||||
final response = await client.put(uri, headers: await _getHeaders());
|
||||
|
||||
if (response.statusCode != 200 && response.statusCode != 204) {
|
||||
if (response.statusCode == 401) {
|
||||
throw UnauthorizedException();
|
||||
} else {
|
||||
throw ServerException('Erreur lors du toggle pin de la conversation');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// === MESSAGE ACTIONS ===
|
||||
|
||||
Future<MessageModel> editMessage({
|
||||
required String messageId,
|
||||
required String newContent,
|
||||
}) async {
|
||||
final uri = Uri.parse('${AppConfig.apiBaseUrl}/api/messages/$messageId');
|
||||
|
||||
final body = json.encode({'content': newContent});
|
||||
|
||||
Future<void> marquerLu(String conversationId) async {
|
||||
final response = await client.put(
|
||||
uri,
|
||||
Uri.parse('$_base/conversations/$conversationId/lire'),
|
||||
headers: await _getHeaders(),
|
||||
);
|
||||
|
||||
_checkAuth(response);
|
||||
if (response.statusCode != 200 && response.statusCode != 204) {
|
||||
throw ServerException('Erreur marquage lu (${response.statusCode})');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> supprimerMessage(String conversationId, String messageId) async {
|
||||
final response = await client.delete(
|
||||
Uri.parse('$_base/conversations/$conversationId/messages/$messageId'),
|
||||
headers: await _getHeaders(),
|
||||
);
|
||||
|
||||
_checkAuth(response);
|
||||
if (response.statusCode != 200 && response.statusCode != 204) {
|
||||
throw ServerException('Erreur suppression message (${response.statusCode})');
|
||||
}
|
||||
}
|
||||
|
||||
// ── Blocages ──────────────────────────────────────────────────────────────
|
||||
|
||||
Future<void> bloquerMembre({
|
||||
required String membreABloquerId,
|
||||
String? organisationId,
|
||||
String? raison,
|
||||
}) async {
|
||||
final body = json.encode({
|
||||
'membreABloquerId': membreABloquerId,
|
||||
if (organisationId != null) 'organisationId': organisationId,
|
||||
if (raison != null) 'raison': raison,
|
||||
});
|
||||
|
||||
final response = await client.post(
|
||||
Uri.parse('$_base/blocages'),
|
||||
headers: await _getHeaders(),
|
||||
body: body,
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
return MessageModel.fromJson(json.decode(response.body));
|
||||
} else if (response.statusCode == 401) {
|
||||
throw UnauthorizedException();
|
||||
} else if (response.statusCode == 404) {
|
||||
throw NotFoundException('Message non trouvé');
|
||||
} else {
|
||||
throw ServerException('Erreur lors de l\'édition du message');
|
||||
_checkAuth(response);
|
||||
if (response.statusCode != 200 && response.statusCode != 204) {
|
||||
throw ServerException('Erreur blocage membre (${response.statusCode})');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> deleteMessage(String messageId) async {
|
||||
final uri = Uri.parse('${AppConfig.apiBaseUrl}/api/messages/$messageId');
|
||||
Future<void> debloquerMembre(String membreId, {String? organisationId}) async {
|
||||
final uri = Uri.parse('$_base/blocages/$membreId').replace(
|
||||
queryParameters: {
|
||||
if (organisationId != null) 'organisationId': organisationId,
|
||||
},
|
||||
);
|
||||
|
||||
final response = await client.delete(uri, headers: await _getHeaders());
|
||||
|
||||
_checkAuth(response);
|
||||
if (response.statusCode != 200 && response.statusCode != 204) {
|
||||
if (response.statusCode == 401) {
|
||||
throw UnauthorizedException();
|
||||
} else if (response.statusCode == 404) {
|
||||
throw NotFoundException('Message non trouvé');
|
||||
} else {
|
||||
throw ServerException('Erreur lors de la suppression du message');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> markMessageAsRead(String messageId) async {
|
||||
// Backend has no per-message read endpoint — use markConversationAsRead
|
||||
if (AppConfig.enableLogging) {
|
||||
debugPrint('[Messaging] markMessageAsRead ignored (no per-message endpoint), messageId=$messageId');
|
||||
throw ServerException('Erreur déblocage membre (${response.statusCode})');
|
||||
}
|
||||
}
|
||||
|
||||
Future<int> getUnreadCount({String? organizationId}) async {
|
||||
try {
|
||||
final conversations = await getConversations(organizationId: organizationId);
|
||||
return conversations.fold<int>(0, (sum, c) => sum + c.unreadCount);
|
||||
} catch (_) {
|
||||
return 0;
|
||||
}
|
||||
Future<List<Map<String, dynamic>>> getMesBlocages() async {
|
||||
final response = await client.get(
|
||||
Uri.parse('$_base/blocages'),
|
||||
headers: await _getHeaders(),
|
||||
);
|
||||
|
||||
_checkAuth(response);
|
||||
if (response.statusCode == 200) {
|
||||
final list = json.decode(response.body) as List<dynamic>;
|
||||
return list.map((j) => j as Map<String, dynamic>).toList();
|
||||
}
|
||||
throw ServerException('Erreur récupération blocages (${response.statusCode})');
|
||||
}
|
||||
|
||||
// ── Politique de communication ────────────────────────────────────────────
|
||||
|
||||
Future<ContactPolicyModel> getPolitique(String organisationId) async {
|
||||
final response = await client.get(
|
||||
Uri.parse('$_base/politique/$organisationId'),
|
||||
headers: await _getHeaders(),
|
||||
);
|
||||
|
||||
_checkAuth(response);
|
||||
if (response.statusCode == 200) {
|
||||
return ContactPolicyModel.fromJson(json.decode(response.body) as Map<String, dynamic>);
|
||||
}
|
||||
throw ServerException('Erreur récupération politique (${response.statusCode})');
|
||||
}
|
||||
|
||||
Future<ContactPolicyModel> mettreAJourPolitique(
|
||||
String organisationId, {
|
||||
required String typePolitique,
|
||||
required bool autoriserMembreVersMembre,
|
||||
required bool autoriserMembreVersRole,
|
||||
required bool autoriserNotesVocales,
|
||||
}) async {
|
||||
final body = json.encode({
|
||||
'typePolitique': typePolitique,
|
||||
'autoriserMembreVersMembre': autoriserMembreVersMembre,
|
||||
'autoriserMembreVersRole': autoriserMembreVersRole,
|
||||
'autoriserNotesVocales': autoriserNotesVocales,
|
||||
});
|
||||
|
||||
final response = await client.put(
|
||||
Uri.parse('$_base/politique/$organisationId'),
|
||||
headers: await _getHeaders(),
|
||||
body: body,
|
||||
);
|
||||
|
||||
_checkAuth(response);
|
||||
if (response.statusCode == 200) {
|
||||
return ContactPolicyModel.fromJson(json.decode(response.body) as Map<String, dynamic>);
|
||||
}
|
||||
throw ServerException('Erreur mise à jour politique (${response.statusCode})');
|
||||
}
|
||||
|
||||
// ── Helpers ────────────────────────────────────────────────────────────────
|
||||
|
||||
void _checkAuth(http.Response response) {
|
||||
if (response.statusCode == 401) throw UnauthorizedException();
|
||||
if (response.statusCode == 403) throw ForbiddenException('Permission insuffisante');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
/// Modèle de données ContactPolicy v4 avec désérialisation JSON
|
||||
library contact_policy_model;
|
||||
|
||||
import '../../domain/entities/contact_policy.dart';
|
||||
|
||||
/// Modèle ContactPolicy v4
|
||||
class ContactPolicyModel extends ContactPolicy {
|
||||
const ContactPolicyModel({
|
||||
super.id,
|
||||
super.organisationId,
|
||||
required super.typePolitique,
|
||||
super.autoriserMembreVersMembre,
|
||||
super.autoriserMembreVersRole,
|
||||
super.autoriserNotesVocales,
|
||||
});
|
||||
|
||||
factory ContactPolicyModel.fromJson(Map<String, dynamic> json) {
|
||||
return ContactPolicyModel(
|
||||
id: json['id']?.toString(),
|
||||
organisationId: json['organisationId']?.toString(),
|
||||
typePolitique: json['typePolitique']?.toString() ?? 'OUVERT',
|
||||
autoriserMembreVersMembre: json['autoriserMembreVersMembre'] == true,
|
||||
autoriserMembreVersRole: json['autoriserMembreVersRole'] == true,
|
||||
autoriserNotesVocales: json['autoriserNotesVocales'] == true,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,70 +1,110 @@
|
||||
/// Model de données Conversation avec sérialisation JSON
|
||||
/// Modèles de données Conversation v4 avec désérialisation JSON
|
||||
library conversation_model;
|
||||
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import '../../domain/entities/conversation.dart';
|
||||
import '../../domain/entities/message.dart';
|
||||
import 'message_model.dart';
|
||||
|
||||
part 'conversation_model.g.dart';
|
||||
|
||||
@JsonSerializable(explicitToJson: true)
|
||||
class ConversationModel extends Conversation {
|
||||
@JsonKey(
|
||||
fromJson: _messageFromJson,
|
||||
toJson: _messageToJson,
|
||||
)
|
||||
@override
|
||||
final Message? lastMessage;
|
||||
|
||||
const ConversationModel({
|
||||
/// Modèle de résumé de conversation (liste)
|
||||
class ConversationSummaryModel extends ConversationSummary {
|
||||
const ConversationSummaryModel({
|
||||
required super.id,
|
||||
required super.name,
|
||||
super.description,
|
||||
required super.type,
|
||||
required super.participantIds,
|
||||
super.organizationId,
|
||||
this.lastMessage,
|
||||
super.unreadCount,
|
||||
super.isMuted,
|
||||
super.isPinned,
|
||||
super.isArchived,
|
||||
required super.createdAt,
|
||||
super.updatedAt,
|
||||
super.avatarUrl,
|
||||
super.metadata,
|
||||
}) : super(lastMessage: lastMessage);
|
||||
required super.typeConversation,
|
||||
required super.titre,
|
||||
required super.statut,
|
||||
super.dernierMessageApercu,
|
||||
super.dernierMessageType,
|
||||
super.dernierMessageAt,
|
||||
super.nonLus,
|
||||
super.organisationId,
|
||||
});
|
||||
|
||||
static Message? _messageFromJson(Map<String, dynamic>? json) =>
|
||||
json == null ? null : MessageModel.fromJson(json);
|
||||
|
||||
static Map<String, dynamic>? _messageToJson(Message? message) =>
|
||||
message == null ? null : MessageModel.fromEntity(message).toJson();
|
||||
|
||||
factory ConversationModel.fromJson(Map<String, dynamic> json) =>
|
||||
_$ConversationModelFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$ConversationModelToJson(this);
|
||||
|
||||
factory ConversationModel.fromEntity(Conversation conversation) {
|
||||
return ConversationModel(
|
||||
id: conversation.id,
|
||||
name: conversation.name,
|
||||
description: conversation.description,
|
||||
type: conversation.type,
|
||||
participantIds: conversation.participantIds,
|
||||
organizationId: conversation.organizationId,
|
||||
lastMessage: conversation.lastMessage,
|
||||
unreadCount: conversation.unreadCount,
|
||||
isMuted: conversation.isMuted,
|
||||
isPinned: conversation.isPinned,
|
||||
isArchived: conversation.isArchived,
|
||||
createdAt: conversation.createdAt,
|
||||
updatedAt: conversation.updatedAt,
|
||||
avatarUrl: conversation.avatarUrl,
|
||||
metadata: conversation.metadata,
|
||||
factory ConversationSummaryModel.fromJson(Map<String, dynamic> json) {
|
||||
return ConversationSummaryModel(
|
||||
id: json['id']?.toString() ?? '',
|
||||
typeConversation: json['typeConversation']?.toString() ?? 'DIRECTE',
|
||||
titre: json['titre']?.toString() ?? '',
|
||||
statut: json['statut']?.toString() ?? 'ACTIVE',
|
||||
dernierMessageApercu: json['dernierMessageApercu']?.toString(),
|
||||
dernierMessageType: json['dernierMessageType']?.toString(),
|
||||
dernierMessageAt: json['dernierMessageAt'] != null
|
||||
? DateTime.tryParse(json['dernierMessageAt'].toString())
|
||||
: null,
|
||||
nonLus: _parseInt(json['nonLus']),
|
||||
organisationId: json['organisationId']?.toString(),
|
||||
);
|
||||
}
|
||||
|
||||
Conversation toEntity() => this;
|
||||
}
|
||||
|
||||
/// Modèle de participant
|
||||
class ConversationParticipantModel extends ConversationParticipant {
|
||||
const ConversationParticipantModel({
|
||||
required super.membreId,
|
||||
super.prenom,
|
||||
super.nom,
|
||||
super.roleDansConversation,
|
||||
super.luJusqua,
|
||||
});
|
||||
|
||||
factory ConversationParticipantModel.fromJson(Map<String, dynamic> json) {
|
||||
return ConversationParticipantModel(
|
||||
membreId: json['membreId']?.toString() ?? '',
|
||||
prenom: json['prenom']?.toString(),
|
||||
nom: json['nom']?.toString(),
|
||||
roleDansConversation: json['roleDansConversation']?.toString(),
|
||||
luJusqua: json['luJusqua'] != null
|
||||
? DateTime.tryParse(json['luJusqua'].toString())
|
||||
: null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Modèle de conversation complète (détail)
|
||||
class ConversationModel extends Conversation {
|
||||
const ConversationModel({
|
||||
required super.id,
|
||||
required super.typeConversation,
|
||||
required super.titre,
|
||||
required super.statut,
|
||||
super.organisationId,
|
||||
super.organisationNom,
|
||||
super.dateCreation,
|
||||
super.nombreMessages,
|
||||
super.participants,
|
||||
super.messages,
|
||||
super.nonLus,
|
||||
super.roleCible,
|
||||
});
|
||||
|
||||
factory ConversationModel.fromJson(Map<String, dynamic> json) {
|
||||
final participantsJson = json['participants'] as List<dynamic>? ?? [];
|
||||
final messagesJson = json['messages'] as List<dynamic>? ?? [];
|
||||
|
||||
return ConversationModel(
|
||||
id: json['id']?.toString() ?? '',
|
||||
typeConversation: json['typeConversation']?.toString() ?? 'DIRECTE',
|
||||
titre: json['titre']?.toString() ?? '',
|
||||
statut: json['statut']?.toString() ?? 'ACTIVE',
|
||||
organisationId: json['organisationId']?.toString(),
|
||||
organisationNom: json['organisationNom']?.toString(),
|
||||
dateCreation: json['dateCreation'] != null
|
||||
? DateTime.tryParse(json['dateCreation'].toString())
|
||||
: null,
|
||||
nombreMessages: _parseInt(json['nombreMessages']),
|
||||
participants: participantsJson
|
||||
.map((p) => ConversationParticipantModel.fromJson(p as Map<String, dynamic>))
|
||||
.toList(),
|
||||
messages: messagesJson
|
||||
.map((m) => MessageModel.fromJson(m as Map<String, dynamic>))
|
||||
.toList(),
|
||||
nonLus: _parseInt(json['nonLus']),
|
||||
roleCible: json['roleCible']?.toString(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
int _parseInt(dynamic value) {
|
||||
if (value == null) return 0;
|
||||
if (value is int) return value;
|
||||
if (value is double) return value.toInt();
|
||||
return int.tryParse(value.toString()) ?? 0;
|
||||
}
|
||||
|
||||
@@ -1,57 +1,2 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'conversation_model.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
ConversationModel _$ConversationModelFromJson(Map<String, dynamic> json) =>
|
||||
ConversationModel(
|
||||
id: json['id'] as String,
|
||||
name: json['name'] as String,
|
||||
description: json['description'] as String?,
|
||||
type: $enumDecode(_$ConversationTypeEnumMap, json['type']),
|
||||
participantIds: (json['participantIds'] as List<dynamic>)
|
||||
.map((e) => e as String)
|
||||
.toList(),
|
||||
organizationId: json['organizationId'] as String?,
|
||||
lastMessage: ConversationModel._messageFromJson(
|
||||
json['lastMessage'] as Map<String, dynamic>?),
|
||||
unreadCount: (json['unreadCount'] as num?)?.toInt() ?? 0,
|
||||
isMuted: json['isMuted'] as bool? ?? false,
|
||||
isPinned: json['isPinned'] as bool? ?? false,
|
||||
isArchived: json['isArchived'] as bool? ?? false,
|
||||
createdAt: DateTime.parse(json['createdAt'] as String),
|
||||
updatedAt: json['updatedAt'] == null
|
||||
? null
|
||||
: DateTime.parse(json['updatedAt'] as String),
|
||||
avatarUrl: json['avatarUrl'] as String?,
|
||||
metadata: json['metadata'] as Map<String, dynamic>?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$ConversationModelToJson(ConversationModel instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'name': instance.name,
|
||||
'description': instance.description,
|
||||
'type': _$ConversationTypeEnumMap[instance.type]!,
|
||||
'participantIds': instance.participantIds,
|
||||
'organizationId': instance.organizationId,
|
||||
'unreadCount': instance.unreadCount,
|
||||
'isMuted': instance.isMuted,
|
||||
'isPinned': instance.isPinned,
|
||||
'isArchived': instance.isArchived,
|
||||
'createdAt': instance.createdAt.toIso8601String(),
|
||||
'updatedAt': instance.updatedAt?.toIso8601String(),
|
||||
'avatarUrl': instance.avatarUrl,
|
||||
'metadata': instance.metadata,
|
||||
'lastMessage': ConversationModel._messageToJson(instance.lastMessage),
|
||||
};
|
||||
|
||||
const _$ConversationTypeEnumMap = {
|
||||
ConversationType.individual: 'individual',
|
||||
ConversationType.group: 'group',
|
||||
ConversationType.broadcast: 'broadcast',
|
||||
ConversationType.announcement: 'announcement',
|
||||
};
|
||||
// Modèles v4 : désérialisation manuelle, code generation non utilisé.
|
||||
|
||||
@@ -1,83 +1,48 @@
|
||||
/// Model de données Message avec sérialisation JSON
|
||||
/// Modèle de données Message v4 avec désérialisation JSON
|
||||
library message_model;
|
||||
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import '../../domain/entities/message.dart';
|
||||
|
||||
part 'message_model.g.dart';
|
||||
|
||||
@JsonSerializable(explicitToJson: true)
|
||||
/// Modèle Message v4
|
||||
class MessageModel extends Message {
|
||||
const MessageModel({
|
||||
required super.id,
|
||||
required super.conversationId,
|
||||
required super.senderId,
|
||||
required super.senderName,
|
||||
super.senderAvatar,
|
||||
required super.content,
|
||||
required super.type,
|
||||
required super.status,
|
||||
super.priority,
|
||||
required super.recipientIds,
|
||||
super.recipientRoles,
|
||||
super.organizationId,
|
||||
required super.createdAt,
|
||||
super.readAt,
|
||||
super.metadata,
|
||||
super.attachments,
|
||||
super.isEdited,
|
||||
super.editedAt,
|
||||
super.isDeleted,
|
||||
required super.typeMessage,
|
||||
super.contenu,
|
||||
super.urlFichier,
|
||||
super.dureeAudio,
|
||||
super.supprime,
|
||||
super.expediteurId,
|
||||
super.expediteurNom,
|
||||
super.expediteurPrenom,
|
||||
super.messageParentId,
|
||||
super.messageParentApercu,
|
||||
super.dateEnvoi,
|
||||
});
|
||||
|
||||
factory MessageModel.fromJson(Map<String, dynamic> json) =>
|
||||
_$MessageModelFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$MessageModelToJson(this);
|
||||
|
||||
factory MessageModel.fromEntity(Message message) {
|
||||
factory MessageModel.fromJson(Map<String, dynamic> json) {
|
||||
return MessageModel(
|
||||
id: message.id,
|
||||
conversationId: message.conversationId,
|
||||
senderId: message.senderId,
|
||||
senderName: message.senderName,
|
||||
senderAvatar: message.senderAvatar,
|
||||
content: message.content,
|
||||
type: message.type,
|
||||
status: message.status,
|
||||
priority: message.priority,
|
||||
recipientIds: message.recipientIds,
|
||||
recipientRoles: message.recipientRoles,
|
||||
organizationId: message.organizationId,
|
||||
createdAt: message.createdAt,
|
||||
readAt: message.readAt,
|
||||
metadata: message.metadata,
|
||||
attachments: message.attachments,
|
||||
isEdited: message.isEdited,
|
||||
editedAt: message.editedAt,
|
||||
isDeleted: message.isDeleted,
|
||||
id: json['id']?.toString() ?? '',
|
||||
typeMessage: json['typeMessage']?.toString() ?? 'TEXTE',
|
||||
contenu: json['contenu']?.toString(),
|
||||
urlFichier: json['urlFichier']?.toString(),
|
||||
dureeAudio: _parseInt(json['dureeAudio']),
|
||||
supprime: json['supprime'] == true,
|
||||
expediteurId: json['expediteurId']?.toString(),
|
||||
expediteurNom: json['expediteurNom']?.toString(),
|
||||
expediteurPrenom: json['expediteurPrenom']?.toString(),
|
||||
messageParentId: json['messageParentId']?.toString(),
|
||||
messageParentApercu: json['messageParentApercu']?.toString(),
|
||||
dateEnvoi: json['dateEnvoi'] != null
|
||||
? DateTime.tryParse(json['dateEnvoi'].toString())
|
||||
: null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Message toEntity() => Message(
|
||||
id: id,
|
||||
conversationId: conversationId,
|
||||
senderId: senderId,
|
||||
senderName: senderName,
|
||||
senderAvatar: senderAvatar,
|
||||
content: content,
|
||||
type: type,
|
||||
status: status,
|
||||
priority: priority,
|
||||
recipientIds: recipientIds,
|
||||
recipientRoles: recipientRoles,
|
||||
organizationId: organizationId,
|
||||
createdAt: createdAt,
|
||||
readAt: readAt,
|
||||
metadata: metadata,
|
||||
attachments: attachments,
|
||||
isEdited: isEdited,
|
||||
editedAt: editedAt,
|
||||
isDeleted: isDeleted,
|
||||
);
|
||||
int? _parseInt(dynamic value) {
|
||||
if (value == null) return null;
|
||||
if (value is int) return value;
|
||||
if (value is double) return value.toInt();
|
||||
return int.tryParse(value.toString());
|
||||
}
|
||||
|
||||
@@ -1,84 +1,2 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'message_model.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
MessageModel _$MessageModelFromJson(Map<String, dynamic> json) => MessageModel(
|
||||
id: json['id'] as String,
|
||||
conversationId: json['conversationId'] as String,
|
||||
senderId: json['senderId'] as String,
|
||||
senderName: json['senderName'] as String,
|
||||
senderAvatar: json['senderAvatar'] as String?,
|
||||
content: json['content'] as String,
|
||||
type: $enumDecode(_$MessageTypeEnumMap, json['type']),
|
||||
status: $enumDecode(_$MessageStatusEnumMap, json['status']),
|
||||
priority:
|
||||
$enumDecodeNullable(_$MessagePriorityEnumMap, json['priority']) ??
|
||||
MessagePriority.normal,
|
||||
recipientIds: (json['recipientIds'] as List<dynamic>)
|
||||
.map((e) => e as String)
|
||||
.toList(),
|
||||
recipientRoles: (json['recipientRoles'] as List<dynamic>?)
|
||||
?.map((e) => e as String)
|
||||
.toList(),
|
||||
organizationId: json['organizationId'] as String?,
|
||||
createdAt: DateTime.parse(json['createdAt'] as String),
|
||||
readAt: json['readAt'] == null
|
||||
? null
|
||||
: DateTime.parse(json['readAt'] as String),
|
||||
metadata: json['metadata'] as Map<String, dynamic>?,
|
||||
attachments: (json['attachments'] as List<dynamic>?)
|
||||
?.map((e) => e as String)
|
||||
.toList(),
|
||||
isEdited: json['isEdited'] as bool? ?? false,
|
||||
editedAt: json['editedAt'] == null
|
||||
? null
|
||||
: DateTime.parse(json['editedAt'] as String),
|
||||
isDeleted: json['isDeleted'] as bool? ?? false,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$MessageModelToJson(MessageModel instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'conversationId': instance.conversationId,
|
||||
'senderId': instance.senderId,
|
||||
'senderName': instance.senderName,
|
||||
'senderAvatar': instance.senderAvatar,
|
||||
'content': instance.content,
|
||||
'type': _$MessageTypeEnumMap[instance.type]!,
|
||||
'status': _$MessageStatusEnumMap[instance.status]!,
|
||||
'priority': _$MessagePriorityEnumMap[instance.priority]!,
|
||||
'recipientIds': instance.recipientIds,
|
||||
'recipientRoles': instance.recipientRoles,
|
||||
'organizationId': instance.organizationId,
|
||||
'createdAt': instance.createdAt.toIso8601String(),
|
||||
'readAt': instance.readAt?.toIso8601String(),
|
||||
'metadata': instance.metadata,
|
||||
'attachments': instance.attachments,
|
||||
'isEdited': instance.isEdited,
|
||||
'editedAt': instance.editedAt?.toIso8601String(),
|
||||
'isDeleted': instance.isDeleted,
|
||||
};
|
||||
|
||||
const _$MessageTypeEnumMap = {
|
||||
MessageType.individual: 'individual',
|
||||
MessageType.broadcast: 'broadcast',
|
||||
MessageType.targeted: 'targeted',
|
||||
MessageType.system: 'system',
|
||||
};
|
||||
|
||||
const _$MessageStatusEnumMap = {
|
||||
MessageStatus.sent: 'sent',
|
||||
MessageStatus.delivered: 'delivered',
|
||||
MessageStatus.read: 'read',
|
||||
MessageStatus.failed: 'failed',
|
||||
};
|
||||
|
||||
const _$MessagePriorityEnumMap = {
|
||||
MessagePriority.normal: 'normal',
|
||||
MessagePriority.high: 'high',
|
||||
MessagePriority.urgent: 'urgent',
|
||||
};
|
||||
// Modèles v4 : désérialisation manuelle, code generation non utilisé.
|
||||
|
||||
@@ -1,416 +1,252 @@
|
||||
/// Implémentation du repository de messagerie
|
||||
/// Implémentation du repository de messagerie v4
|
||||
library messaging_repository_impl;
|
||||
|
||||
import 'package:dartz/dartz.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
|
||||
import '../../../../core/error/exceptions.dart';
|
||||
import '../../../../core/error/failures.dart';
|
||||
import '../../../../core/network/network_info.dart';
|
||||
import '../../domain/entities/conversation.dart';
|
||||
import '../../domain/entities/message.dart';
|
||||
import '../../domain/entities/message_template.dart';
|
||||
import '../../domain/entities/contact_policy.dart';
|
||||
import '../../domain/repositories/messaging_repository.dart';
|
||||
import '../datasources/messaging_remote_datasource.dart';
|
||||
|
||||
@LazySingleton(as: MessagingRepository)
|
||||
class MessagingRepositoryImpl implements MessagingRepository {
|
||||
final MessagingRemoteDatasource remoteDatasource;
|
||||
final NetworkInfo networkInfo;
|
||||
|
||||
MessagingRepositoryImpl({
|
||||
required this.remoteDatasource,
|
||||
required this.networkInfo,
|
||||
});
|
||||
MessagingRepositoryImpl({required this.remoteDatasource});
|
||||
|
||||
@override
|
||||
Future<Either<Failure, List<Conversation>>> getConversations({
|
||||
String? organizationId,
|
||||
bool includeArchived = false,
|
||||
}) async {
|
||||
if (!await networkInfo.isConnected) {
|
||||
return Left(NetworkFailure('Pas de connexion Internet'));
|
||||
}
|
||||
|
||||
Future<List<ConversationSummary>> getMesConversations() async {
|
||||
try {
|
||||
final conversations = await remoteDatasource.getConversations(
|
||||
organizationId: organizationId,
|
||||
includeArchived: includeArchived,
|
||||
);
|
||||
return Right(conversations);
|
||||
return await remoteDatasource.getMesConversations();
|
||||
} on UnauthorizedException {
|
||||
return Left(UnauthorizedFailure('Session expirée'));
|
||||
throw Exception('Session expirée — veuillez vous reconnecter');
|
||||
} on ServerException catch (e) {
|
||||
return Left(ServerFailure(e.message));
|
||||
throw Exception(e.message);
|
||||
} catch (e) {
|
||||
return Left(UnexpectedFailure('Erreur inattendue: $e'));
|
||||
throw Exception('Erreur inattendue: $e');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<Failure, Conversation>> getConversationById(
|
||||
String conversationId) async {
|
||||
if (!await networkInfo.isConnected) {
|
||||
return Left(NetworkFailure('Pas de connexion Internet'));
|
||||
}
|
||||
|
||||
Future<Conversation> getConversation(String conversationId) async {
|
||||
try {
|
||||
final conversation =
|
||||
await remoteDatasource.getConversationById(conversationId);
|
||||
return Right(conversation);
|
||||
return await remoteDatasource.getConversation(conversationId);
|
||||
} on NotFoundException {
|
||||
return Left(NotFoundFailure('Conversation non trouvée'));
|
||||
throw Exception('Conversation non trouvée');
|
||||
} on UnauthorizedException {
|
||||
return Left(UnauthorizedFailure('Session expirée'));
|
||||
throw Exception('Session expirée — veuillez vous reconnecter');
|
||||
} on ServerException catch (e) {
|
||||
return Left(ServerFailure(e.message));
|
||||
throw Exception(e.message);
|
||||
} catch (e) {
|
||||
return Left(UnexpectedFailure('Erreur inattendue: $e'));
|
||||
throw Exception('Erreur inattendue: $e');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<Failure, Conversation>> createConversation({
|
||||
required String name,
|
||||
required List<String> participantIds,
|
||||
String? organizationId,
|
||||
String? description,
|
||||
Future<Conversation> demarrerConversationDirecte({
|
||||
required String destinataireId,
|
||||
required String organisationId,
|
||||
String? premierMessage,
|
||||
}) async {
|
||||
if (!await networkInfo.isConnected) {
|
||||
return Left(NetworkFailure('Pas de connexion Internet'));
|
||||
}
|
||||
|
||||
try {
|
||||
final conversation = await remoteDatasource.createConversation(
|
||||
name: name,
|
||||
participantIds: participantIds,
|
||||
organizationId: organizationId,
|
||||
description: description,
|
||||
return await remoteDatasource.demarrerConversationDirecte(
|
||||
destinataireId: destinataireId,
|
||||
organisationId: organisationId,
|
||||
premierMessage: premierMessage,
|
||||
);
|
||||
return Right(conversation);
|
||||
} on UnauthorizedException {
|
||||
return Left(UnauthorizedFailure('Session expirée'));
|
||||
throw Exception('Session expirée — veuillez vous reconnecter');
|
||||
} on ServerException catch (e) {
|
||||
return Left(ServerFailure(e.message));
|
||||
throw Exception(e.message);
|
||||
} catch (e) {
|
||||
return Left(UnexpectedFailure('Erreur inattendue: $e'));
|
||||
throw Exception('Erreur inattendue: $e');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<Failure, List<Message>>> getMessages({
|
||||
required String conversationId,
|
||||
int? limit,
|
||||
String? beforeMessageId,
|
||||
Future<Conversation> demarrerConversationRole({
|
||||
required String roleCible,
|
||||
required String organisationId,
|
||||
String? premierMessage,
|
||||
}) async {
|
||||
if (!await networkInfo.isConnected) {
|
||||
return Left(NetworkFailure('Pas de connexion Internet'));
|
||||
}
|
||||
|
||||
try {
|
||||
final messages = await remoteDatasource.getMessages(
|
||||
conversationId: conversationId,
|
||||
limit: limit,
|
||||
beforeMessageId: beforeMessageId,
|
||||
return await remoteDatasource.demarrerConversationRole(
|
||||
roleCible: roleCible,
|
||||
organisationId: organisationId,
|
||||
premierMessage: premierMessage,
|
||||
);
|
||||
return Right(messages);
|
||||
} on UnauthorizedException {
|
||||
return Left(UnauthorizedFailure('Session expirée'));
|
||||
throw Exception('Session expirée — veuillez vous reconnecter');
|
||||
} on ServerException catch (e) {
|
||||
return Left(ServerFailure(e.message));
|
||||
throw Exception(e.message);
|
||||
} catch (e) {
|
||||
return Left(UnexpectedFailure('Erreur inattendue: $e'));
|
||||
throw Exception('Erreur inattendue: $e');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<Failure, Message>> sendMessage({
|
||||
required String conversationId,
|
||||
required String content,
|
||||
List<String>? attachments,
|
||||
MessagePriority priority = MessagePriority.normal,
|
||||
}) async {
|
||||
if (!await networkInfo.isConnected) {
|
||||
return Left(NetworkFailure('Pas de connexion Internet'));
|
||||
}
|
||||
|
||||
Future<void> archiverConversation(String conversationId) async {
|
||||
try {
|
||||
final message = await remoteDatasource.sendMessage(
|
||||
conversationId: conversationId,
|
||||
content: content,
|
||||
attachments: attachments,
|
||||
priority: priority,
|
||||
);
|
||||
return Right(message);
|
||||
await remoteDatasource.archiverConversation(conversationId);
|
||||
} on UnauthorizedException {
|
||||
return Left(UnauthorizedFailure('Session expirée'));
|
||||
throw Exception('Session expirée — veuillez vous reconnecter');
|
||||
} on ServerException catch (e) {
|
||||
return Left(ServerFailure(e.message));
|
||||
throw Exception(e.message);
|
||||
} catch (e) {
|
||||
return Left(UnexpectedFailure('Erreur inattendue: $e'));
|
||||
throw Exception('Erreur inattendue: $e');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<Failure, Message>> sendBroadcast({
|
||||
required String organizationId,
|
||||
required String subject,
|
||||
required String content,
|
||||
MessagePriority priority = MessagePriority.normal,
|
||||
List<String>? attachments,
|
||||
Future<Message> envoyerMessage(
|
||||
String conversationId, {
|
||||
required String typeMessage,
|
||||
String? contenu,
|
||||
String? urlFichier,
|
||||
int? dureeAudio,
|
||||
String? messageParentId,
|
||||
}) async {
|
||||
if (!await networkInfo.isConnected) {
|
||||
return Left(NetworkFailure('Pas de connexion Internet'));
|
||||
}
|
||||
|
||||
try {
|
||||
final message = await remoteDatasource.sendBroadcast(
|
||||
organizationId: organizationId,
|
||||
subject: subject,
|
||||
content: content,
|
||||
priority: priority,
|
||||
attachments: attachments,
|
||||
return await remoteDatasource.envoyerMessage(
|
||||
conversationId,
|
||||
typeMessage: typeMessage,
|
||||
contenu: contenu,
|
||||
urlFichier: urlFichier,
|
||||
dureeAudio: dureeAudio,
|
||||
messageParentId: messageParentId,
|
||||
);
|
||||
return Right(message);
|
||||
} on UnauthorizedException {
|
||||
throw Exception('Session expirée — veuillez vous reconnecter');
|
||||
} on ForbiddenException catch (e) {
|
||||
return Left(ForbiddenFailure(e.message));
|
||||
} on UnauthorizedException {
|
||||
return Left(UnauthorizedFailure('Session expirée'));
|
||||
throw Exception(e.message);
|
||||
} on ServerException catch (e) {
|
||||
return Left(ServerFailure(e.message));
|
||||
throw Exception(e.message);
|
||||
} catch (e) {
|
||||
return Left(UnexpectedFailure('Erreur inattendue: $e'));
|
||||
throw Exception('Erreur inattendue: $e');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<Failure, void>> markMessageAsRead(String messageId) async {
|
||||
if (!await networkInfo.isConnected) {
|
||||
return Left(NetworkFailure('Pas de connexion Internet'));
|
||||
}
|
||||
|
||||
Future<List<Message>> getMessages(String conversationId, {int page = 0}) async {
|
||||
try {
|
||||
await remoteDatasource.markMessageAsRead(messageId);
|
||||
return const Right(null);
|
||||
return await remoteDatasource.getMessages(conversationId, page: page);
|
||||
} on UnauthorizedException {
|
||||
return Left(UnauthorizedFailure('Session expirée'));
|
||||
throw Exception('Session expirée — veuillez vous reconnecter');
|
||||
} on ServerException catch (e) {
|
||||
return Left(ServerFailure(e.message));
|
||||
throw Exception(e.message);
|
||||
} catch (e) {
|
||||
return Left(UnexpectedFailure('Erreur inattendue: $e'));
|
||||
throw Exception('Erreur inattendue: $e');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<Failure, int>> getUnreadCount({String? organizationId}) async {
|
||||
if (!await networkInfo.isConnected) {
|
||||
return Left(NetworkFailure('Pas de connexion Internet'));
|
||||
}
|
||||
|
||||
Future<void> marquerLu(String conversationId) async {
|
||||
try {
|
||||
final count =
|
||||
await remoteDatasource.getUnreadCount(organizationId: organizationId);
|
||||
return Right(count);
|
||||
} on UnauthorizedException {
|
||||
return Left(UnauthorizedFailure('Session expirée'));
|
||||
} on ServerException catch (e) {
|
||||
return Left(ServerFailure(e.message));
|
||||
} catch (e) {
|
||||
return Left(UnexpectedFailure('Erreur inattendue: $e'));
|
||||
await remoteDatasource.marquerLu(conversationId);
|
||||
} catch (_) {
|
||||
// Non-bloquant — ignorer les erreurs de marquage
|
||||
}
|
||||
}
|
||||
|
||||
// === CONVERSATION ACTIONS ===
|
||||
|
||||
@override
|
||||
Future<Either<Failure, void>> archiveConversation(String conversationId) async {
|
||||
if (!await networkInfo.isConnected) {
|
||||
return Left(NetworkFailure('Pas de connexion Internet'));
|
||||
}
|
||||
|
||||
Future<void> supprimerMessage(String conversationId, String messageId) async {
|
||||
try {
|
||||
await remoteDatasource.archiveConversation(conversationId);
|
||||
return const Right(null);
|
||||
await remoteDatasource.supprimerMessage(conversationId, messageId);
|
||||
} on UnauthorizedException {
|
||||
return Left(UnauthorizedFailure('Session expirée'));
|
||||
throw Exception('Session expirée — veuillez vous reconnecter');
|
||||
} on ServerException catch (e) {
|
||||
return Left(ServerFailure(e.message));
|
||||
throw Exception(e.message);
|
||||
} catch (e) {
|
||||
return Left(UnexpectedFailure('Erreur inattendue: $e'));
|
||||
throw Exception('Erreur inattendue: $e');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<Failure, Message>> sendTargetedMessage({
|
||||
required String organizationId,
|
||||
required List<String> targetRoles,
|
||||
required String subject,
|
||||
required String content,
|
||||
MessagePriority priority = MessagePriority.normal,
|
||||
Future<void> bloquerMembre({
|
||||
required String membreABloquerId,
|
||||
String? organisationId,
|
||||
String? raison,
|
||||
}) async {
|
||||
// TODO: Backend needs specific endpoint for targeted messages
|
||||
return Left(NotImplementedFailure('Fonctionnalité en cours de développement'));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<Failure, void>> markConversationAsRead(String conversationId) async {
|
||||
if (!await networkInfo.isConnected) {
|
||||
return Left(NetworkFailure('Pas de connexion Internet'));
|
||||
}
|
||||
|
||||
try {
|
||||
await remoteDatasource.markConversationAsRead(conversationId);
|
||||
return const Right(null);
|
||||
} on UnauthorizedException {
|
||||
return Left(UnauthorizedFailure('Session expirée'));
|
||||
} on ServerException catch (e) {
|
||||
return Left(ServerFailure(e.message));
|
||||
} catch (e) {
|
||||
return Left(UnexpectedFailure('Erreur inattendue: $e'));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<Failure, void>> toggleMuteConversation(String conversationId) async {
|
||||
if (!await networkInfo.isConnected) {
|
||||
return Left(NetworkFailure('Pas de connexion Internet'));
|
||||
}
|
||||
|
||||
try {
|
||||
await remoteDatasource.toggleMuteConversation(conversationId);
|
||||
return const Right(null);
|
||||
} on UnauthorizedException {
|
||||
return Left(UnauthorizedFailure('Session expirée'));
|
||||
} on ServerException catch (e) {
|
||||
return Left(ServerFailure(e.message));
|
||||
} catch (e) {
|
||||
return Left(UnexpectedFailure('Erreur inattendue: $e'));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<Failure, void>> togglePinConversation(String conversationId) async {
|
||||
if (!await networkInfo.isConnected) {
|
||||
return Left(NetworkFailure('Pas de connexion Internet'));
|
||||
}
|
||||
|
||||
try {
|
||||
await remoteDatasource.togglePinConversation(conversationId);
|
||||
return const Right(null);
|
||||
} on UnauthorizedException {
|
||||
return Left(UnauthorizedFailure('Session expirée'));
|
||||
} on ServerException catch (e) {
|
||||
return Left(ServerFailure(e.message));
|
||||
} catch (e) {
|
||||
return Left(UnexpectedFailure('Erreur inattendue: $e'));
|
||||
}
|
||||
}
|
||||
|
||||
// === MESSAGE ACTIONS ===
|
||||
|
||||
@override
|
||||
Future<Either<Failure, Message>> editMessage({
|
||||
required String messageId,
|
||||
required String newContent,
|
||||
}) async {
|
||||
if (!await networkInfo.isConnected) {
|
||||
return Left(NetworkFailure('Pas de connexion Internet'));
|
||||
}
|
||||
|
||||
try {
|
||||
final message = await remoteDatasource.editMessage(
|
||||
messageId: messageId,
|
||||
newContent: newContent,
|
||||
await remoteDatasource.bloquerMembre(
|
||||
membreABloquerId: membreABloquerId,
|
||||
organisationId: organisationId,
|
||||
raison: raison,
|
||||
);
|
||||
return Right(message);
|
||||
} on NotFoundException {
|
||||
return Left(NotFoundFailure('Message non trouvé'));
|
||||
} on UnauthorizedException {
|
||||
return Left(UnauthorizedFailure('Session expirée'));
|
||||
throw Exception('Session expirée — veuillez vous reconnecter');
|
||||
} on ServerException catch (e) {
|
||||
return Left(ServerFailure(e.message));
|
||||
throw Exception(e.message);
|
||||
} catch (e) {
|
||||
return Left(UnexpectedFailure('Erreur inattendue: $e'));
|
||||
throw Exception('Erreur inattendue: $e');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<Failure, void>> deleteMessage(String messageId) async {
|
||||
if (!await networkInfo.isConnected) {
|
||||
return Left(NetworkFailure('Pas de connexion Internet'));
|
||||
}
|
||||
|
||||
Future<void> debloquerMembre(String membreId, {String? organisationId}) async {
|
||||
try {
|
||||
await remoteDatasource.deleteMessage(messageId);
|
||||
return const Right(null);
|
||||
} on NotFoundException {
|
||||
return Left(NotFoundFailure('Message non trouvé'));
|
||||
await remoteDatasource.debloquerMembre(membreId, organisationId: organisationId);
|
||||
} on UnauthorizedException {
|
||||
return Left(UnauthorizedFailure('Session expirée'));
|
||||
throw Exception('Session expirée — veuillez vous reconnecter');
|
||||
} on ServerException catch (e) {
|
||||
return Left(ServerFailure(e.message));
|
||||
throw Exception(e.message);
|
||||
} catch (e) {
|
||||
return Left(UnexpectedFailure('Erreur inattendue: $e'));
|
||||
throw Exception('Erreur inattendue: $e');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<Failure, List<MessageTemplate>>> getTemplates({
|
||||
String? organizationId,
|
||||
TemplateCategory? category,
|
||||
Future<List<Map<String, dynamic>>> getMesBlocages() async {
|
||||
try {
|
||||
return await remoteDatasource.getMesBlocages();
|
||||
} on UnauthorizedException {
|
||||
throw Exception('Session expirée — veuillez vous reconnecter');
|
||||
} on ServerException catch (e) {
|
||||
throw Exception(e.message);
|
||||
} catch (e) {
|
||||
throw Exception('Erreur inattendue: $e');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<ContactPolicy> getPolitique(String organisationId) async {
|
||||
try {
|
||||
return await remoteDatasource.getPolitique(organisationId);
|
||||
} on UnauthorizedException {
|
||||
throw Exception('Session expirée — veuillez vous reconnecter');
|
||||
} on ServerException catch (e) {
|
||||
throw Exception(e.message);
|
||||
} catch (e) {
|
||||
throw Exception('Erreur inattendue: $e');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<ContactPolicy> mettreAJourPolitique(
|
||||
String organisationId, {
|
||||
required String typePolitique,
|
||||
required bool autoriserMembreVersMembre,
|
||||
required bool autoriserMembreVersRole,
|
||||
required bool autoriserNotesVocales,
|
||||
}) async {
|
||||
return Left(NotImplementedFailure('Fonctionnalité en cours de développement'));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<Failure, MessageTemplate>> getTemplateById(String templateId) async {
|
||||
return Left(NotImplementedFailure('Fonctionnalité en cours de développement'));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<Failure, MessageTemplate>> createTemplate({
|
||||
required String name,
|
||||
required String description,
|
||||
required TemplateCategory category,
|
||||
required String subject,
|
||||
required String body,
|
||||
List<Map<String, dynamic>>? variables,
|
||||
String? organizationId,
|
||||
}) async {
|
||||
return Left(NotImplementedFailure('Fonctionnalité en cours de développement'));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<Failure, MessageTemplate>> updateTemplate({
|
||||
required String templateId,
|
||||
String? name,
|
||||
String? description,
|
||||
String? subject,
|
||||
String? body,
|
||||
bool? isActive,
|
||||
}) async {
|
||||
return Left(NotImplementedFailure('Fonctionnalité en cours de développement'));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<Failure, void>> deleteTemplate(String templateId) async {
|
||||
return Left(NotImplementedFailure('Fonctionnalité en cours de développement'));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<Failure, Message>> sendFromTemplate({
|
||||
required String templateId,
|
||||
required Map<String, String> variables,
|
||||
required List<String> recipientIds,
|
||||
}) async {
|
||||
return Left(NotImplementedFailure('Fonctionnalité en cours de développement'));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<Failure, Map<String, dynamic>>> getMessagingStats({
|
||||
required String organizationId,
|
||||
DateTime? startDate,
|
||||
DateTime? endDate,
|
||||
}) async {
|
||||
return Left(NotImplementedFailure('Fonctionnalité en cours de développement'));
|
||||
try {
|
||||
return await remoteDatasource.mettreAJourPolitique(
|
||||
organisationId,
|
||||
typePolitique: typePolitique,
|
||||
autoriserMembreVersMembre: autoriserMembreVersMembre,
|
||||
autoriserMembreVersRole: autoriserMembreVersRole,
|
||||
autoriserNotesVocales: autoriserNotesVocales,
|
||||
);
|
||||
} on UnauthorizedException {
|
||||
throw Exception('Session expirée — veuillez vous reconnecter');
|
||||
} on ForbiddenException catch (e) {
|
||||
throw Exception(e.message);
|
||||
} on ServerException catch (e) {
|
||||
throw Exception(e.message);
|
||||
} catch (e) {
|
||||
throw Exception('Erreur inattendue: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
/// Entité métier ContactPolicy v4
|
||||
///
|
||||
/// Correspond au DTO backend : ContactPolicyResponse
|
||||
library contact_policy;
|
||||
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
/// Politique de communication d'une organisation
|
||||
class ContactPolicy extends Equatable {
|
||||
final String? id;
|
||||
final String? organisationId;
|
||||
final String typePolitique; // OUVERT | BUREAU_SEULEMENT | GROUPES_INTERNES
|
||||
final bool autoriserMembreVersMembre;
|
||||
final bool autoriserMembreVersRole;
|
||||
final bool autoriserNotesVocales;
|
||||
|
||||
const ContactPolicy({
|
||||
this.id,
|
||||
this.organisationId,
|
||||
required this.typePolitique,
|
||||
this.autoriserMembreVersMembre = true,
|
||||
this.autoriserMembreVersRole = true,
|
||||
this.autoriserNotesVocales = true,
|
||||
});
|
||||
|
||||
bool get isOuvert => typePolitique == 'OUVERT';
|
||||
bool get isBureauSeulement => typePolitique == 'BUREAU_SEULEMENT';
|
||||
bool get isGroupesInternes => typePolitique == 'GROUPES_INTERNES';
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
id, organisationId, typePolitique,
|
||||
autoriserMembreVersMembre, autoriserMembreVersRole, autoriserNotesVocales,
|
||||
];
|
||||
}
|
||||
@@ -1,127 +1,120 @@
|
||||
/// Entité métier Conversation
|
||||
/// Entités métier Conversation v4
|
||||
///
|
||||
/// Représente une conversation (fil de messages) dans UnionFlow
|
||||
/// Correspond aux DTOs backend : ConversationSummaryResponse et ConversationResponse
|
||||
library conversation;
|
||||
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'message.dart';
|
||||
|
||||
/// Type de conversation
|
||||
enum ConversationType {
|
||||
/// Conversation individuelle (1-1)
|
||||
individual,
|
||||
// ── Résumé de conversation (liste) ───────────────────────────────────────────
|
||||
|
||||
/// Conversation de groupe
|
||||
group,
|
||||
|
||||
/// Canal broadcast (lecture seule pour la plupart)
|
||||
broadcast,
|
||||
|
||||
/// Canal d'annonces organisation
|
||||
announcement,
|
||||
}
|
||||
|
||||
/// Entité Conversation
|
||||
class Conversation extends Equatable {
|
||||
/// Résumé d'une conversation pour l'affichage en liste
|
||||
class ConversationSummary extends Equatable {
|
||||
final String id;
|
||||
final String name;
|
||||
final String? description;
|
||||
final ConversationType type;
|
||||
final List<String> participantIds;
|
||||
final String? organizationId;
|
||||
final Message? lastMessage;
|
||||
final int unreadCount;
|
||||
final bool isMuted;
|
||||
final bool isPinned;
|
||||
final bool isArchived;
|
||||
final DateTime createdAt;
|
||||
final DateTime? updatedAt;
|
||||
final String? avatarUrl;
|
||||
final Map<String, dynamic>? metadata;
|
||||
final String typeConversation; // DIRECTE | ROLE_CANAL | GROUPE
|
||||
final String titre;
|
||||
final String statut; // ACTIVE | ARCHIVEE
|
||||
final String? dernierMessageApercu;
|
||||
final String? dernierMessageType;
|
||||
final DateTime? dernierMessageAt;
|
||||
final int nonLus;
|
||||
final String? organisationId;
|
||||
|
||||
const Conversation({
|
||||
const ConversationSummary({
|
||||
required this.id,
|
||||
required this.name,
|
||||
this.description,
|
||||
required this.type,
|
||||
required this.participantIds,
|
||||
this.organizationId,
|
||||
this.lastMessage,
|
||||
this.unreadCount = 0,
|
||||
this.isMuted = false,
|
||||
this.isPinned = false,
|
||||
this.isArchived = false,
|
||||
required this.createdAt,
|
||||
this.updatedAt,
|
||||
this.avatarUrl,
|
||||
this.metadata,
|
||||
required this.typeConversation,
|
||||
required this.titre,
|
||||
required this.statut,
|
||||
this.dernierMessageApercu,
|
||||
this.dernierMessageType,
|
||||
this.dernierMessageAt,
|
||||
this.nonLus = 0,
|
||||
this.organisationId,
|
||||
});
|
||||
|
||||
/// Vérifie si la conversation a des messages non lus
|
||||
bool get hasUnread => unreadCount > 0;
|
||||
|
||||
/// Vérifie si c'est une conversation individuelle
|
||||
bool get isIndividual => type == ConversationType.individual;
|
||||
|
||||
/// Vérifie si c'est un broadcast
|
||||
bool get isBroadcast => type == ConversationType.broadcast;
|
||||
|
||||
/// Nombre de participants
|
||||
int get participantCount => participantIds.length;
|
||||
|
||||
/// Copie avec modifications
|
||||
Conversation copyWith({
|
||||
String? id,
|
||||
String? name,
|
||||
String? description,
|
||||
ConversationType? type,
|
||||
List<String>? participantIds,
|
||||
String? organizationId,
|
||||
Message? lastMessage,
|
||||
int? unreadCount,
|
||||
bool? isMuted,
|
||||
bool? isPinned,
|
||||
bool? isArchived,
|
||||
DateTime? createdAt,
|
||||
DateTime? updatedAt,
|
||||
String? avatarUrl,
|
||||
Map<String, dynamic>? metadata,
|
||||
}) {
|
||||
return Conversation(
|
||||
id: id ?? this.id,
|
||||
name: name ?? this.name,
|
||||
description: description ?? this.description,
|
||||
type: type ?? this.type,
|
||||
participantIds: participantIds ?? this.participantIds,
|
||||
organizationId: organizationId ?? this.organizationId,
|
||||
lastMessage: lastMessage ?? this.lastMessage,
|
||||
unreadCount: unreadCount ?? this.unreadCount,
|
||||
isMuted: isMuted ?? this.isMuted,
|
||||
isPinned: isPinned ?? this.isPinned,
|
||||
isArchived: isArchived ?? this.isArchived,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
updatedAt: updatedAt ?? this.updatedAt,
|
||||
avatarUrl: avatarUrl ?? this.avatarUrl,
|
||||
metadata: metadata ?? this.metadata,
|
||||
);
|
||||
}
|
||||
bool get hasUnread => nonLus > 0;
|
||||
bool get isDirecte => typeConversation == 'DIRECTE';
|
||||
bool get isRoleCanal => typeConversation == 'ROLE_CANAL';
|
||||
bool get isGroupe => typeConversation == 'GROUPE';
|
||||
bool get isActive => statut == 'ACTIVE';
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
type,
|
||||
participantIds,
|
||||
organizationId,
|
||||
lastMessage,
|
||||
unreadCount,
|
||||
isMuted,
|
||||
isPinned,
|
||||
isArchived,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
avatarUrl,
|
||||
metadata,
|
||||
id, typeConversation, titre, statut,
|
||||
dernierMessageApercu, dernierMessageType, dernierMessageAt,
|
||||
nonLus, organisationId,
|
||||
];
|
||||
}
|
||||
|
||||
// ── Participant ───────────────────────────────────────────────────────────────
|
||||
|
||||
/// Participant dans une conversation
|
||||
class ConversationParticipant extends Equatable {
|
||||
final String membreId;
|
||||
final String? prenom;
|
||||
final String? nom;
|
||||
final String? roleDansConversation;
|
||||
final DateTime? luJusqua;
|
||||
|
||||
const ConversationParticipant({
|
||||
required this.membreId,
|
||||
this.prenom,
|
||||
this.nom,
|
||||
this.roleDansConversation,
|
||||
this.luJusqua,
|
||||
});
|
||||
|
||||
String get nomComplet {
|
||||
if (prenom != null && nom != null) return '$prenom $nom';
|
||||
if (prenom != null) return prenom!;
|
||||
if (nom != null) return nom!;
|
||||
return membreId;
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [membreId, prenom, nom, roleDansConversation, luJusqua];
|
||||
}
|
||||
|
||||
// ── Conversation complète (détail) ───────────────────────────────────────────
|
||||
|
||||
/// Conversation complète avec participants et messages
|
||||
class Conversation extends Equatable {
|
||||
final String id;
|
||||
final String typeConversation;
|
||||
final String titre;
|
||||
final String statut;
|
||||
final String? organisationId;
|
||||
final String? organisationNom;
|
||||
final DateTime? dateCreation;
|
||||
final int nombreMessages;
|
||||
final List<ConversationParticipant> participants;
|
||||
final List<Message> messages;
|
||||
final int nonLus;
|
||||
final String? roleCible;
|
||||
|
||||
const Conversation({
|
||||
required this.id,
|
||||
required this.typeConversation,
|
||||
required this.titre,
|
||||
required this.statut,
|
||||
this.organisationId,
|
||||
this.organisationNom,
|
||||
this.dateCreation,
|
||||
this.nombreMessages = 0,
|
||||
this.participants = const [],
|
||||
this.messages = const [],
|
||||
this.nonLus = 0,
|
||||
this.roleCible,
|
||||
});
|
||||
|
||||
bool get isDirecte => typeConversation == 'DIRECTE';
|
||||
bool get isRoleCanal => typeConversation == 'ROLE_CANAL';
|
||||
bool get isGroupe => typeConversation == 'GROUPE';
|
||||
bool get isActive => statut == 'ACTIVE';
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
id, typeConversation, titre, statut, organisationId, organisationNom,
|
||||
dateCreation, nombreMessages, participants, messages, nonLus, roleCible,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1,173 +1,68 @@
|
||||
/// Entité métier Message
|
||||
/// Entité métier Message v4
|
||||
///
|
||||
/// Représente un message dans le système de communication UnionFlow
|
||||
/// Correspond au DTO backend : MessageResponse
|
||||
library message;
|
||||
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
/// Type de message
|
||||
enum MessageType {
|
||||
/// Message individuel (membre à membre)
|
||||
individual,
|
||||
|
||||
/// Broadcast organisation (OrgAdmin → tous)
|
||||
broadcast,
|
||||
|
||||
/// Message ciblé par rôle (Moderator → groupe)
|
||||
targeted,
|
||||
|
||||
/// Notification système
|
||||
system,
|
||||
}
|
||||
|
||||
/// Statut de lecture du message
|
||||
enum MessageStatus {
|
||||
/// Envoyé mais non lu
|
||||
sent,
|
||||
|
||||
/// Livré (reçu par le serveur)
|
||||
delivered,
|
||||
|
||||
/// Lu par le destinataire
|
||||
read,
|
||||
|
||||
/// Échec d'envoi
|
||||
failed,
|
||||
}
|
||||
|
||||
/// Priorité du message
|
||||
enum MessagePriority {
|
||||
/// Priorité normale
|
||||
normal,
|
||||
|
||||
/// Priorité élevée (important)
|
||||
high,
|
||||
|
||||
/// Priorité urgente (critique)
|
||||
urgent,
|
||||
}
|
||||
|
||||
/// Entité Message
|
||||
/// Message dans une conversation
|
||||
class Message extends Equatable {
|
||||
final String id;
|
||||
final String conversationId;
|
||||
final String senderId;
|
||||
final String senderName;
|
||||
final String? senderAvatar;
|
||||
final String content;
|
||||
final MessageType type;
|
||||
final MessageStatus status;
|
||||
final MessagePriority priority;
|
||||
final List<String> recipientIds;
|
||||
final List<String>? recipientRoles;
|
||||
final String? organizationId;
|
||||
final DateTime createdAt;
|
||||
final DateTime? readAt;
|
||||
final Map<String, dynamic>? metadata;
|
||||
final List<String>? attachments;
|
||||
final bool isEdited;
|
||||
final DateTime? editedAt;
|
||||
final bool isDeleted;
|
||||
final String typeMessage; // TEXTE | VOCAL | IMAGE | SYSTEME
|
||||
final String? contenu;
|
||||
final String? urlFichier;
|
||||
final int? dureeAudio;
|
||||
final bool supprime;
|
||||
final String? expediteurId;
|
||||
final String? expediteurNom;
|
||||
final String? expediteurPrenom;
|
||||
final String? messageParentId;
|
||||
final String? messageParentApercu;
|
||||
final DateTime? dateEnvoi;
|
||||
|
||||
const Message({
|
||||
required this.id,
|
||||
required this.conversationId,
|
||||
required this.senderId,
|
||||
required this.senderName,
|
||||
this.senderAvatar,
|
||||
required this.content,
|
||||
required this.type,
|
||||
required this.status,
|
||||
this.priority = MessagePriority.normal,
|
||||
required this.recipientIds,
|
||||
this.recipientRoles,
|
||||
this.organizationId,
|
||||
required this.createdAt,
|
||||
this.readAt,
|
||||
this.metadata,
|
||||
this.attachments,
|
||||
this.isEdited = false,
|
||||
this.editedAt,
|
||||
this.isDeleted = false,
|
||||
required this.typeMessage,
|
||||
this.contenu,
|
||||
this.urlFichier,
|
||||
this.dureeAudio,
|
||||
this.supprime = false,
|
||||
this.expediteurId,
|
||||
this.expediteurNom,
|
||||
this.expediteurPrenom,
|
||||
this.messageParentId,
|
||||
this.messageParentApercu,
|
||||
this.dateEnvoi,
|
||||
});
|
||||
|
||||
/// Vérifie si le message a été lu
|
||||
bool get isRead => status == MessageStatus.read;
|
||||
String get expediteurNomComplet {
|
||||
if (expediteurPrenom != null && expediteurNom != null) {
|
||||
return '$expediteurPrenom $expediteurNom';
|
||||
}
|
||||
if (expediteurPrenom != null) return expediteurPrenom!;
|
||||
if (expediteurNom != null) return expediteurNom!;
|
||||
return '';
|
||||
}
|
||||
|
||||
/// Vérifie si le message est urgent
|
||||
bool get isUrgent => priority == MessagePriority.urgent;
|
||||
bool get isTexte => typeMessage == 'TEXTE';
|
||||
bool get isVocal => typeMessage == 'VOCAL';
|
||||
bool get isImage => typeMessage == 'IMAGE';
|
||||
bool get isSysteme => typeMessage == 'SYSTEME';
|
||||
bool get hasParent => messageParentId != null;
|
||||
|
||||
/// Vérifie si le message est un broadcast
|
||||
bool get isBroadcast => type == MessageType.broadcast;
|
||||
|
||||
/// Vérifie si le message a des pièces jointes
|
||||
bool get hasAttachments => attachments != null && attachments!.isNotEmpty;
|
||||
|
||||
/// Copie avec modifications
|
||||
Message copyWith({
|
||||
String? id,
|
||||
String? conversationId,
|
||||
String? senderId,
|
||||
String? senderName,
|
||||
String? senderAvatar,
|
||||
String? content,
|
||||
MessageType? type,
|
||||
MessageStatus? status,
|
||||
MessagePriority? priority,
|
||||
List<String>? recipientIds,
|
||||
List<String>? recipientRoles,
|
||||
String? organizationId,
|
||||
DateTime? createdAt,
|
||||
DateTime? readAt,
|
||||
Map<String, dynamic>? metadata,
|
||||
List<String>? attachments,
|
||||
bool? isEdited,
|
||||
DateTime? editedAt,
|
||||
bool? isDeleted,
|
||||
}) {
|
||||
return Message(
|
||||
id: id ?? this.id,
|
||||
conversationId: conversationId ?? this.conversationId,
|
||||
senderId: senderId ?? this.senderId,
|
||||
senderName: senderName ?? this.senderName,
|
||||
senderAvatar: senderAvatar ?? this.senderAvatar,
|
||||
content: content ?? this.content,
|
||||
type: type ?? this.type,
|
||||
status: status ?? this.status,
|
||||
priority: priority ?? this.priority,
|
||||
recipientIds: recipientIds ?? this.recipientIds,
|
||||
recipientRoles: recipientRoles ?? this.recipientRoles,
|
||||
organizationId: organizationId ?? this.organizationId,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
readAt: readAt ?? this.readAt,
|
||||
metadata: metadata ?? this.metadata,
|
||||
attachments: attachments ?? this.attachments,
|
||||
isEdited: isEdited ?? this.isEdited,
|
||||
editedAt: editedAt ?? this.editedAt,
|
||||
isDeleted: isDeleted ?? this.isDeleted,
|
||||
);
|
||||
/// Texte à afficher dans la liste (aperçu)
|
||||
String get apercu {
|
||||
if (supprime) return '🚫 Message supprimé';
|
||||
if (isVocal) return '🎙️ Note vocale${dureeAudio != null ? ' (${dureeAudio}s)' : ''}';
|
||||
if (isImage) return '📷 Image';
|
||||
if (isSysteme) return contenu ?? '🔔 Notification système';
|
||||
return contenu ?? '';
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
id,
|
||||
conversationId,
|
||||
senderId,
|
||||
senderName,
|
||||
senderAvatar,
|
||||
content,
|
||||
type,
|
||||
status,
|
||||
priority,
|
||||
recipientIds,
|
||||
recipientRoles,
|
||||
organizationId,
|
||||
createdAt,
|
||||
readAt,
|
||||
metadata,
|
||||
attachments,
|
||||
isEdited,
|
||||
editedAt,
|
||||
isDeleted,
|
||||
id, typeMessage, contenu, urlFichier, dureeAudio, supprime,
|
||||
expediteurId, expediteurNom, expediteurPrenom,
|
||||
messageParentId, messageParentApercu, dateEnvoi,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1,145 +1,87 @@
|
||||
/// Repository interface pour la communication
|
||||
/// Repository interface pour la messagerie v4
|
||||
///
|
||||
/// Contrat de données pour les messages, conversations et templates
|
||||
/// Contrat de données aligné sur l'API backend v4 (/api/messagerie/*)
|
||||
library messaging_repository;
|
||||
|
||||
import 'package:dartz/dartz.dart';
|
||||
import '../../../../core/error/failures.dart';
|
||||
import '../entities/message.dart';
|
||||
import '../entities/conversation.dart';
|
||||
import '../entities/message_template.dart';
|
||||
import '../entities/message.dart';
|
||||
import '../entities/contact_policy.dart';
|
||||
|
||||
/// Interface du repository de messagerie
|
||||
abstract class MessagingRepository {
|
||||
// === CONVERSATIONS ===
|
||||
|
||||
/// Récupère toutes les conversations de l'utilisateur
|
||||
Future<Either<Failure, List<Conversation>>> getConversations({
|
||||
String? organizationId,
|
||||
bool includeArchived = false,
|
||||
// ── Conversations ─────────────────────────────────────────────────────────
|
||||
|
||||
/// Récupère les conversations résumées de l'utilisateur connecté
|
||||
Future<List<ConversationSummary>> getMesConversations();
|
||||
|
||||
/// Récupère la conversation complète (avec participants et messages)
|
||||
Future<Conversation> getConversation(String conversationId);
|
||||
|
||||
/// Démarre une conversation directe avec un membre
|
||||
Future<Conversation> demarrerConversationDirecte({
|
||||
required String destinataireId,
|
||||
required String organisationId,
|
||||
String? premierMessage,
|
||||
});
|
||||
|
||||
/// Récupère une conversation par son ID
|
||||
Future<Either<Failure, Conversation>> getConversationById(String conversationId);
|
||||
|
||||
/// Crée une nouvelle conversation
|
||||
Future<Either<Failure, Conversation>> createConversation({
|
||||
required String name,
|
||||
required List<String> participantIds,
|
||||
String? organizationId,
|
||||
String? description,
|
||||
/// Démarre un canal de communication avec un rôle officiel
|
||||
Future<Conversation> demarrerConversationRole({
|
||||
required String roleCible,
|
||||
required String organisationId,
|
||||
String? premierMessage,
|
||||
});
|
||||
|
||||
/// Archive une conversation
|
||||
Future<Either<Failure, void>> archiveConversation(String conversationId);
|
||||
Future<void> archiverConversation(String conversationId);
|
||||
|
||||
/// Marque une conversation comme lue
|
||||
Future<Either<Failure, void>> markConversationAsRead(String conversationId);
|
||||
// ── Messages ──────────────────────────────────────────────────────────────
|
||||
|
||||
/// Mute/démute une conversation
|
||||
Future<Either<Failure, void>> toggleMuteConversation(String conversationId);
|
||||
|
||||
/// Pin/unpin une conversation
|
||||
Future<Either<Failure, void>> togglePinConversation(String conversationId);
|
||||
|
||||
// === MESSAGES ===
|
||||
|
||||
/// Récupère les messages d'une conversation
|
||||
Future<Either<Failure, List<Message>>> getMessages({
|
||||
required String conversationId,
|
||||
int? limit,
|
||||
String? beforeMessageId,
|
||||
/// Envoie un message dans une conversation
|
||||
Future<Message> envoyerMessage(
|
||||
String conversationId, {
|
||||
required String typeMessage,
|
||||
String? contenu,
|
||||
String? urlFichier,
|
||||
int? dureeAudio,
|
||||
String? messageParentId,
|
||||
});
|
||||
|
||||
/// Envoie un message individuel
|
||||
Future<Either<Failure, Message>> sendMessage({
|
||||
required String conversationId,
|
||||
required String content,
|
||||
List<String>? attachments,
|
||||
MessagePriority priority = MessagePriority.normal,
|
||||
/// Récupère l'historique des messages (paginé)
|
||||
Future<List<Message>> getMessages(String conversationId, {int page = 0});
|
||||
|
||||
/// Marque tous les messages d'une conversation comme lus
|
||||
Future<void> marquerLu(String conversationId);
|
||||
|
||||
/// Supprime un message (soft-delete)
|
||||
Future<void> supprimerMessage(String conversationId, String messageId);
|
||||
|
||||
// ── Blocages ──────────────────────────────────────────────────────────────
|
||||
|
||||
/// Bloque un membre
|
||||
Future<void> bloquerMembre({
|
||||
required String membreABloquerId,
|
||||
String? organisationId,
|
||||
String? raison,
|
||||
});
|
||||
|
||||
/// Envoie un broadcast à toute l'organisation
|
||||
Future<Either<Failure, Message>> sendBroadcast({
|
||||
required String organizationId,
|
||||
required String subject,
|
||||
required String content,
|
||||
MessagePriority priority = MessagePriority.normal,
|
||||
List<String>? attachments,
|
||||
});
|
||||
/// Débloque un membre
|
||||
Future<void> debloquerMembre(String membreId, {String? organisationId});
|
||||
|
||||
/// Envoie un message ciblé par rôles
|
||||
Future<Either<Failure, Message>> sendTargetedMessage({
|
||||
required String organizationId,
|
||||
required List<String> targetRoles,
|
||||
required String subject,
|
||||
required String content,
|
||||
MessagePriority priority = MessagePriority.normal,
|
||||
});
|
||||
/// Récupère la liste des membres bloqués
|
||||
Future<List<Map<String, dynamic>>> getMesBlocages();
|
||||
|
||||
/// Marque un message comme lu
|
||||
Future<Either<Failure, void>> markMessageAsRead(String messageId);
|
||||
// ── Politique de communication ────────────────────────────────────────────
|
||||
|
||||
/// Édite un message
|
||||
Future<Either<Failure, Message>> editMessage({
|
||||
required String messageId,
|
||||
required String newContent,
|
||||
});
|
||||
/// Récupère la politique de communication d'une organisation
|
||||
Future<ContactPolicy> getPolitique(String organisationId);
|
||||
|
||||
/// Supprime un message
|
||||
Future<Either<Failure, void>> deleteMessage(String messageId);
|
||||
|
||||
// === TEMPLATES ===
|
||||
|
||||
/// Récupère tous les templates disponibles
|
||||
Future<Either<Failure, List<MessageTemplate>>> getTemplates({
|
||||
String? organizationId,
|
||||
TemplateCategory? category,
|
||||
});
|
||||
|
||||
/// Récupère un template par son ID
|
||||
Future<Either<Failure, MessageTemplate>> getTemplateById(String templateId);
|
||||
|
||||
/// Crée un nouveau template
|
||||
Future<Either<Failure, MessageTemplate>> createTemplate({
|
||||
required String name,
|
||||
required String description,
|
||||
required TemplateCategory category,
|
||||
required String subject,
|
||||
required String body,
|
||||
List<Map<String, dynamic>>? variables,
|
||||
String? organizationId,
|
||||
});
|
||||
|
||||
/// Met à jour un template
|
||||
Future<Either<Failure, MessageTemplate>> updateTemplate({
|
||||
required String templateId,
|
||||
String? name,
|
||||
String? description,
|
||||
String? subject,
|
||||
String? body,
|
||||
bool? isActive,
|
||||
});
|
||||
|
||||
/// Supprime un template
|
||||
Future<Either<Failure, void>> deleteTemplate(String templateId);
|
||||
|
||||
/// Envoie un message à partir d'un template
|
||||
Future<Either<Failure, Message>> sendFromTemplate({
|
||||
required String templateId,
|
||||
required Map<String, String> variables,
|
||||
required List<String> recipientIds,
|
||||
});
|
||||
|
||||
// === STATISTIQUES ===
|
||||
|
||||
/// Récupère le nombre de messages non lus
|
||||
Future<Either<Failure, int>> getUnreadCount({String? organizationId});
|
||||
|
||||
/// Récupère les statistiques de communication
|
||||
Future<Either<Failure, Map<String, dynamic>>> getMessagingStats({
|
||||
required String organizationId,
|
||||
DateTime? startDate,
|
||||
DateTime? endDate,
|
||||
/// Met à jour la politique de communication (ADMIN seulement)
|
||||
Future<ContactPolicy> mettreAJourPolitique(
|
||||
String organisationId, {
|
||||
required String typePolitique,
|
||||
required bool autoriserMembreVersMembre,
|
||||
required bool autoriserMembreVersRole,
|
||||
required bool autoriserNotesVocales,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
/// Use case: Récupérer les conversations
|
||||
/// Use case: Récupérer les conversations v4
|
||||
library get_conversations;
|
||||
|
||||
import 'package:dartz/dartz.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
import '../../../../core/error/failures.dart';
|
||||
|
||||
import '../entities/conversation.dart';
|
||||
import '../repositories/messaging_repository.dart';
|
||||
|
||||
@@ -13,13 +12,7 @@ class GetConversations {
|
||||
|
||||
GetConversations(this.repository);
|
||||
|
||||
Future<Either<Failure, List<Conversation>>> call({
|
||||
String? organizationId,
|
||||
bool includeArchived = false,
|
||||
}) async {
|
||||
return await repository.getConversations(
|
||||
organizationId: organizationId,
|
||||
includeArchived: includeArchived,
|
||||
);
|
||||
Future<List<ConversationSummary>> call() async {
|
||||
return await repository.getMesConversations();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
/// Use case: Récupérer les messages d'une conversation
|
||||
/// Use case: Récupérer les messages d'une conversation v4
|
||||
library get_messages;
|
||||
|
||||
import 'package:dartz/dartz.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
import '../../../../core/error/failures.dart';
|
||||
|
||||
import '../entities/message.dart';
|
||||
import '../repositories/messaging_repository.dart';
|
||||
|
||||
@@ -13,19 +12,10 @@ class GetMessages {
|
||||
|
||||
GetMessages(this.repository);
|
||||
|
||||
Future<Either<Failure, List<Message>>> call({
|
||||
Future<List<Message>> call({
|
||||
required String conversationId,
|
||||
int? limit,
|
||||
String? beforeMessageId,
|
||||
int page = 0,
|
||||
}) async {
|
||||
if (conversationId.isEmpty) {
|
||||
return Left(ValidationFailure('ID conversation requis'));
|
||||
}
|
||||
|
||||
return await repository.getMessages(
|
||||
conversationId: conversationId,
|
||||
limit: limit,
|
||||
beforeMessageId: beforeMessageId,
|
||||
);
|
||||
return await repository.getMessages(conversationId, page: page);
|
||||
}
|
||||
}
|
||||
|
||||