bottleneckcrm/mobile/lib/data/models/solution.dart
Ashwin 66421bbad0 Add ticket approval panel tests and enhance ticket board functionality
- Implement tests for ticket approval panel to verify approval request logic and visibility based on user roles.
- Add functionality to handle ticket movement based on server response for both tickets and leads.
- Introduce new tests for duplicate checking API to ensure correct handling of duplicates and merging logic.
- Remove outdated permissions tests that are no longer relevant.
- Update ticket board and lead board providers to include 'can_move' field for better control over ticket actions.
- Refactor existing tests to improve coverage and ensure accurate representation of API responses.
2026-09-26 23:33:20 +05:30

134 lines
4 KiB
Dart

import 'package:flutter/material.dart';
import '../../core/theme/app_colors.dart';
/// Knowledge Base solution: a reusable answer that can be linked to tickets.
/// Mirrors `cases.models.Solution`.
class Solution {
final String id;
final String title;
final String description;
final SolutionStatus status;
final bool isPublished;
final int caseCount;
/// Ids of the tags this article is filed under. Agent-side only: the portal
/// serializers carry no tags at all, because the vocabulary is shared with
/// deals and reads like "At Risk". The portal uses them to pick related
/// articles and never prints the names.
final List<String> tagIds;
final DateTime? createdAt;
final DateTime? updatedAt;
/// The server's `write` and `delete` rules for the signed-in user (admins and
/// the author), sent by the detail route only. False anywhere else, so a
/// list row never offers an edit or delete on a guess.
final bool canEdit;
final bool canDelete;
const Solution({
required this.id,
required this.title,
required this.description,
required this.status,
this.isPublished = false,
this.caseCount = 0,
this.tagIds = const [],
this.createdAt,
this.updatedAt,
this.canEdit = false,
this.canDelete = false,
});
factory Solution.fromJson(Map<String, dynamic> json) {
return Solution(
id: json['id']?.toString() ?? '',
title: json['title']?.toString() ?? '',
description: json['description']?.toString() ?? '',
status: SolutionStatus.fromString(json['status'] as String?),
isPublished: json['is_published'] as bool? ?? false,
caseCount: json['case_count'] as int? ?? 0,
tagIds: _tagIds(json['tags']),
createdAt: json['created_at'] != null
? DateTime.tryParse(json['created_at'].toString())
: null,
updatedAt: json['updated_at'] != null
? DateTime.tryParse(json['updated_at'].toString())
: null,
canEdit: json['can_edit'] == true,
canDelete: json['can_delete'] == true,
);
}
Map<String, dynamic> toCreatePayload() => {
'title': title,
'description': description,
'status': status.value,
'is_published': isPublished,
// Always sent. The form shows every active tag, so an unticked one is a
// deliberate "not this", and the API reads an explicit empty list as
// "clear" while an absent key means "leave alone".
'tags': tagIds,
};
Solution copyWith({
String? id,
String? title,
String? description,
SolutionStatus? status,
bool? isPublished,
int? caseCount,
List<String>? tagIds,
}) {
return Solution(
id: id ?? this.id,
title: title ?? this.title,
description: description ?? this.description,
status: status ?? this.status,
isPublished: isPublished ?? this.isPublished,
caseCount: caseCount ?? this.caseCount,
tagIds: tagIds ?? this.tagIds,
createdAt: createdAt,
updatedAt: updatedAt,
canEdit: canEdit,
canDelete: canDelete,
);
}
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is Solution && runtimeType == other.runtimeType && id == other.id;
@override
int get hashCode => id.hashCode;
}
/// The API nests `tags` as objects, not bare ids. Mirrors the helper of the
/// same shape in `account.dart`.
List<String> _tagIds(dynamic raw) {
if (raw is! List) return const [];
return raw
.map((e) => e is Map ? e['id']?.toString() : e?.toString())
.whereType<String>()
.where((s) => s.isNotEmpty)
.toList(growable: false);
}
enum SolutionStatus {
draft('draft', 'Draft', AppColors.gray500),
reviewed('reviewed', 'Reviewed', AppColors.warning500),
approved('approved', 'Approved', AppColors.success600);
final String value;
final String label;
final Color color;
const SolutionStatus(this.value, this.label, this.color);
static SolutionStatus fromString(String? value) {
return SolutionStatus.values.firstWhere(
(s) => s.value == value,
orElse: () => SolutionStatus.draft,
);
}
}