Phase 3: closing-overlay fix + goodbye-composer dedupe

Customer chat refreshSessionStatus now clears sessionExpired carryover so the
goodbye composer renders correctly when re-opening a closing session from
history. Backend /api/shared/chat/:id/info returns goodbye_submitted_by_me;
both apps suppress the composer for the side that has already submitted and
render an awaiting-banner view instead.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-27 13:43:19 +08:00
parent 05ab1e10df
commit 6801001b64
6 changed files with 157 additions and 10 deletions

View File

@@ -29,6 +29,7 @@ class MitraChatConnectedData extends MitraChatData {
final int? remainingSeconds;
final bool sessionExpired;
final bool sessionClosing;
final bool goodbyeSubmitted;
final Map<String, dynamic>? extensionRequest;
final TopicSensitivity topicSensitivity;
@@ -38,6 +39,7 @@ class MitraChatConnectedData extends MitraChatData {
this.remainingSeconds,
this.sessionExpired = false,
this.sessionClosing = false,
this.goodbyeSubmitted = false,
this.extensionRequest,
this.topicSensitivity = TopicSensitivity.regular,
});
@@ -48,6 +50,7 @@ class MitraChatConnectedData extends MitraChatData {
int? remainingSeconds,
bool? sessionExpired,
bool? sessionClosing,
bool? goodbyeSubmitted,
Map<String, dynamic>? extensionRequest,
bool clearExtensionRequest = false,
TopicSensitivity? topicSensitivity,
@@ -58,6 +61,7 @@ class MitraChatConnectedData extends MitraChatData {
remainingSeconds: remainingSeconds ?? this.remainingSeconds,
sessionExpired: sessionExpired ?? this.sessionExpired,
sessionClosing: sessionClosing ?? this.sessionClosing,
goodbyeSubmitted: goodbyeSubmitted ?? this.goodbyeSubmitted,
extensionRequest: clearExtensionRequest ? null : (extensionRequest ?? this.extensionRequest),
topicSensitivity: topicSensitivity ?? this.topicSensitivity,
);
@@ -124,6 +128,7 @@ class MitraChat extends _$MitraChat {
}
final isClosing = sessionStatus == SessionStatus.closing;
final goodbyeSubmittedByMe = sessionData?['goodbye_submitted_by_me'] as bool? ?? false;
final sessionTopic = TopicSensitivity.fromString(sessionData?['topic_sensitivity'] as String?);
final response = await _apiClient.get('/api/shared/chat/$sessionId/messages');
@@ -162,7 +167,12 @@ class MitraChat extends _$MitraChat {
'session_id': sessionId,
}));
state = MitraChatConnectedData(messages: messages, sessionClosing: isClosing, topicSensitivity: sessionTopic);
state = MitraChatConnectedData(
messages: messages,
sessionClosing: isClosing,
goodbyeSubmitted: goodbyeSubmittedByMe,
topicSensitivity: sessionTopic,
);
} catch (e) {
state = const MitraChatErrorData('Gagal terhubung ke chat.');
}

View File

@@ -268,11 +268,19 @@ class _MitraChatScreenState extends ConsumerState<MitraChatScreen> {
return _buildExtensionView(state.extensionRequest!, extState);
}
// Goodbye view
if (state.sessionClosing || extState is ExtensionShowGoodbyeData || extState is ExtensionSubmittingData) {
// Goodbye view — suppress if mitra has already submitted their goodbye;
// session can stay in `closing` while waiting for the customer to submit
// theirs or for the 5-min grace timer to auto-complete.
final showGoodbye = !state.goodbyeSubmitted &&
(state.sessionClosing || extState is ExtensionShowGoodbyeData || extState is ExtensionSubmittingData);
if (showGoodbye) {
return _buildGoodbyeView(extState);
}
if (state.sessionClosing && state.goodbyeSubmitted) {
return _buildAwaitingCustomerGoodbyeView(state);
}
final bgTint = SensitivityTheme.of(state.topicSensitivity).bgTint;
return Stack(
@@ -544,4 +552,55 @@ class _MitraChatScreenState extends ConsumerState<MitraChatScreen> {
),
);
}
Widget _buildAwaitingCustomerGoodbyeView(MitraChatConnectedData state) {
final bgTint = SensitivityTheme.of(state.topicSensitivity).bgTint;
return Stack(
children: [
Positioned.fill(
child: Container(
color: bgTint,
child: Image.asset(
'assets/images/chat_pattern.png',
repeat: ImageRepeat.repeat,
fit: BoxFit.none,
),
),
),
Column(
children: [
Container(
width: double.infinity,
color: Colors.amber.shade100,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Row(
children: [
Icon(Icons.hourglass_top, color: Colors.amber.shade900, size: 20),
const SizedBox(width: 8),
Expanded(
child: Text(
'Pesan penutupmu sudah terkirim. Menunggu user...',
style: TextStyle(color: Colors.amber.shade900, fontWeight: FontWeight.w600),
),
),
],
),
),
Expanded(
child: ListView.builder(
controller: _scrollController,
padding: const EdgeInsets.all(16),
itemCount: state.messages.length,
itemBuilder: (context, index) {
final msg = state.messages[index];
final isMe = msg.senderType == UserType.mitra;
return _buildMessageBubble(msg, isMe);
},
),
),
],
),
],
);
}
}