add dleete image projects

This commit is contained in:
Phuoc Nguyen
2025-11-28 13:47:47 +07:00
parent 6e7e848ad6
commit ed6cc4cebc
6 changed files with 300 additions and 39 deletions

View File

@@ -43,6 +43,14 @@ abstract class SubmissionsRemoteDataSource {
required String projectName,
required String filePath,
});
/// Delete a file from a project submission
/// [fileId] is the file ID to delete
/// [projectName] is the project name (docname)
Future<void> deleteProjectFile({
required String fileId,
required String projectName,
});
}
/// Submissions Remote Data Source Implementation
@@ -303,4 +311,41 @@ class SubmissionsRemoteDataSourceImpl implements SubmissionsRemoteDataSource {
throw Exception('Failed to upload project file: $e');
}
}
/// Delete a file from a project submission
///
/// Calls: POST /api/method/frappe.desk.form.utils.remove_attach
/// Form-data: fid, dt, dn
@override
Future<void> deleteProjectFile({
required String fileId,
required String projectName,
}) async {
try {
final formData = FormData.fromMap({
'fid': fileId,
'dt': 'Architectural Project',
'dn': projectName,
});
final response = await _dioClient.post<Map<String, dynamic>>(
'${ApiConstants.frappeApiMethod}${ApiConstants.removeProjectFile}',
data: formData,
);
final data = response.data;
if (data == null) {
throw Exception('No data received from deleteProjectFile API');
}
// Check for error in response
if (data['exc_type'] != null || data['exception'] != null) {
final errorMessage =
data['_server_messages'] ?? data['exception'] ?? 'Unknown error';
throw Exception('API error: $errorMessage');
}
} catch (e) {
throw Exception('Failed to delete project file: $e');
}
}
}

View File

@@ -171,4 +171,19 @@ class SubmissionsRepositoryImpl implements SubmissionsRepository {
rethrow;
}
}
@override
Future<void> deleteProjectFile({
required String fileId,
required String projectName,
}) async {
try {
return await _remoteDataSource.deleteProjectFile(
fileId: fileId,
projectName: projectName,
);
} catch (e) {
rethrow;
}
}
}

View File

@@ -54,4 +54,12 @@ abstract class SubmissionsRepository {
required String projectName,
required String filePath,
});
/// Delete a file from a project submission
/// [fileId] is the file ID to delete
/// [projectName] is the project name (docname)
Future<void> deleteProjectFile({
required String fileId,
required String projectName,
});
}

View File

@@ -51,6 +51,7 @@ class _SubmissionCreatePageState extends ConsumerState<SubmissionCreatePage> {
List<ProjectFile> _existingFiles = []; // Existing files from API
bool _isSubmitting = false;
bool _isLoadingDetail = false;
String? _deletingFileId; // Track which file is being deleted
/// Whether we're editing an existing submission
bool get isEditing => widget.submission != null;
@@ -858,49 +859,76 @@ class _SubmissionCreatePageState extends ConsumerState<SubmissionCreatePage> {
Widget _buildExistingFileItem(ProjectFile file, int index) {
final fileName = file.fileUrl.split('/').last;
final isDeleting = _deletingFileId == file.id;
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: const Color(0xFFF8F9FA),
border: Border.all(color: AppColors.success),
border: Border.all(
color: isDeleting ? AppColors.grey500 : AppColors.success,
),
borderRadius: BorderRadius.circular(6),
),
child: Row(
children: [
// Network image
ClipRRect(
borderRadius: BorderRadius.circular(4),
child: CachedNetworkImage(
imageUrl: file.fileUrl,
width: 48,
height: 48,
fit: BoxFit.cover,
placeholder: (context, url) => Container(
width: 48,
height: 48,
color: AppColors.grey100,
child: const Center(
child: SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(strokeWidth: 2),
// Network image with delete overlay
Stack(
children: [
ClipRRect(
borderRadius: BorderRadius.circular(4),
child: CachedNetworkImage(
imageUrl: file.fileUrl,
width: 48,
height: 48,
fit: BoxFit.cover,
placeholder: (context, url) => Container(
width: 48,
height: 48,
color: AppColors.grey100,
child: const Center(
child: SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(strokeWidth: 2),
),
),
),
errorWidget: (context, url, error) => Container(
width: 48,
height: 48,
color: AppColors.grey100,
child: const Center(
child: FaIcon(
FontAwesomeIcons.image,
size: 24,
color: AppColors.grey500,
),
),
),
),
),
errorWidget: (context, url, error) => Container(
width: 48,
height: 48,
color: AppColors.grey100,
child: const Center(
child: FaIcon(
FontAwesomeIcons.image,
size: 24,
color: AppColors.grey500,
// Deleting overlay
if (isDeleting)
Positioned.fill(
child: Container(
decoration: BoxDecoration(
color: Colors.black.withValues(alpha: 0.5),
borderRadius: BorderRadius.circular(4),
),
child: const Center(
child: SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
),
),
),
),
),
),
),
],
),
const SizedBox(width: 12),
Expanded(
@@ -909,33 +937,185 @@ class _SubmissionCreatePageState extends ConsumerState<SubmissionCreatePage> {
children: [
Text(
fileName,
style: const TextStyle(
style: TextStyle(
fontWeight: FontWeight.w500,
color: AppColors.grey900,
color: isDeleting ? AppColors.grey500 : AppColors.grey900,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 2),
const Text(
'Đã tải lên',
Text(
isDeleting ? 'Đang xóa...' : 'Đã tải lên',
style: TextStyle(
fontSize: 12,
color: AppColors.success,
color: isDeleting ? AppColors.grey500 : AppColors.success,
),
),
],
),
),
// Checkmark icon
const FaIcon(
FontAwesomeIcons.circleCheck,
size: 16,
color: AppColors.success,
// Delete button or checkmark
if (!_isSubmitting && !isDeleting)
IconButton(
icon: const FaIcon(
FontAwesomeIcons.trash,
size: 16,
color: AppColors.danger,
),
onPressed: () => _showDeleteConfirmDialog(file),
tooltip: 'Xóa ảnh',
)
else if (!isDeleting)
const FaIcon(
FontAwesomeIcons.circleCheck,
size: 16,
color: AppColors.success,
),
],
),
);
}
/// Show confirmation dialog before deleting an existing file
Future<void> _showDeleteConfirmDialog(ProjectFile file) async {
final fileName = file.fileUrl.split('/').last;
final confirmed = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Row(
children: [
FaIcon(
FontAwesomeIcons.triangleExclamation,
color: AppColors.danger,
size: 20,
),
SizedBox(width: 12),
Text('Xác nhận xóa'),
],
),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Bạn có chắc chắn muốn xóa ảnh này?'),
const SizedBox(height: 12),
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: AppColors.grey100,
borderRadius: BorderRadius.circular(8),
),
child: Row(
children: [
ClipRRect(
borderRadius: BorderRadius.circular(4),
child: CachedNetworkImage(
imageUrl: file.fileUrl,
width: 48,
height: 48,
fit: BoxFit.cover,
placeholder: (context, url) => Container(
width: 48,
height: 48,
color: AppColors.grey100,
),
errorWidget: (context, url, error) => Container(
width: 48,
height: 48,
color: AppColors.grey100,
child: const Center(child: FaIcon(FontAwesomeIcons.image, size: 20)),
),
),
),
const SizedBox(width: 12),
Expanded(
child: Text(
fileName,
style: const TextStyle(
fontSize: 13,
color: AppColors.grey500,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
],
),
),
const SizedBox(height: 12),
const Text(
'Hành động này không thể hoàn tác.',
style: TextStyle(
fontSize: 13,
color: AppColors.danger,
fontStyle: FontStyle.italic,
),
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: const Text('Hủy'),
),
ElevatedButton(
onPressed: () => Navigator.pop(context, true),
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.danger,
foregroundColor: Colors.white,
),
child: const Text('Xóa'),
),
],
),
);
if (confirmed == true && mounted) {
await _deleteExistingFile(file);
}
}
/// Delete an existing file from the project
Future<void> _deleteExistingFile(ProjectFile file) async {
if (!isEditing || widget.submission == null) return;
setState(() => _deletingFileId = file.id);
try {
final repository = await ref.read(submissionsRepositoryProvider.future);
await repository.deleteProjectFile(
fileId: file.id,
projectName: widget.submission!.submissionId,
);
if (!mounted) return;
// Remove from local list
setState(() {
_existingFiles = _existingFiles.where((f) => f.id != file.id).toList();
_deletingFileId = null;
});
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Đã xóa ảnh thành công'),
backgroundColor: AppColors.success,
),
);
} catch (e) {
if (mounted) {
setState(() => _deletingFileId = null);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Lỗi xóa ảnh: ${e.toString().replaceAll('Exception: ', '')}'),
backgroundColor: AppColors.danger,
),
);
}
}
}
Widget _buildSubmitButton() {