add dleete image projects
This commit is contained in:
@@ -139,6 +139,14 @@ curl --location 'https://land.dbiz.com//api/method/upload_file' \
|
|||||||
--form 'docname="p9ti8veq2g"' \
|
--form 'docname="p9ti8veq2g"' \
|
||||||
--form 'optimize="true"'
|
--form 'optimize="true"'
|
||||||
|
|
||||||
|
#delete image file of project
|
||||||
|
curl --location 'https://land.dbiz.com//api/method/frappe.desk.form.utils.remove_attach' \
|
||||||
|
--header 'Cookie: sid=a0cbe3ea6f9a7e9cf083bbe3139eada68d2357eac0167bcc66cda17d; full_name=Ha%20Duy%20Lam; sid=a0cbe3ea6f9a7e9cf083bbe3139eada68d2357eac0167bcc66cda17d; system_user=yes; user_id=lamhd%40gmail.com; user_image=/files/avatar_0986788766_1763627962.jpg' \
|
||||||
|
--header 'X-Frappe-Csrf-Token: 6ff3be4d1f887dbebf86ba4502b05d94b30c0b0569de49b74a7171a9' \
|
||||||
|
--form 'fid="67803d2e95"' \ #file id to be deleted
|
||||||
|
--form 'dt="Architectural Project"' \ #doctye
|
||||||
|
--form 'dn="p9ti8veq2g"' #docname
|
||||||
|
|
||||||
#get detail of a project
|
#get detail of a project
|
||||||
curl --location 'https://land.dbiz.com//api/method/building_material.building_material.api.project.get_detail' \
|
curl --location 'https://land.dbiz.com//api/method/building_material.building_material.api.project.get_detail' \
|
||||||
--header 'Cookie: sid=a0cbe3ea6f9a7e9cf083bbe3139eada68d2357eac0167bcc66cda17d; full_name=Ha%20Duy%20Lam; sid=a0cbe3ea6f9a7e9cf083bbe3139eada68d2357eac0167bcc66cda17d; system_user=yes; user_id=lamhd%40gmail.com; user_image=/files/avatar_0986788766_1763627962.jpg' \
|
--header 'Cookie: sid=a0cbe3ea6f9a7e9cf083bbe3139eada68d2357eac0167bcc66cda17d; full_name=Ha%20Duy%20Lam; sid=a0cbe3ea6f9a7e9cf083bbe3139eada68d2357eac0167bcc66cda17d; system_user=yes; user_id=lamhd%40gmail.com; user_image=/files/avatar_0986788766_1763627962.jpg' \
|
||||||
|
|||||||
@@ -315,6 +315,11 @@ class ApiConstants {
|
|||||||
static const String getProjectDetail =
|
static const String getProjectDetail =
|
||||||
'/building_material.building_material.api.project.get_detail';
|
'/building_material.building_material.api.project.get_detail';
|
||||||
|
|
||||||
|
/// Delete project file/attachment (requires sid and csrf_token)
|
||||||
|
/// POST /api/method/frappe.desk.form.utils.remove_attach
|
||||||
|
/// Form-data: { "fid": "file_id", "dt": "Architectural Project", "dn": "project_name" }
|
||||||
|
static const String removeProjectFile = '/frappe.desk.form.utils.remove_attach';
|
||||||
|
|
||||||
/// Create new project (legacy endpoint - may be deprecated)
|
/// Create new project (legacy endpoint - may be deprecated)
|
||||||
/// POST /projects
|
/// POST /projects
|
||||||
static const String createProject = '/projects';
|
static const String createProject = '/projects';
|
||||||
|
|||||||
@@ -43,6 +43,14 @@ abstract class SubmissionsRemoteDataSource {
|
|||||||
required String projectName,
|
required String projectName,
|
||||||
required String filePath,
|
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
|
/// Submissions Remote Data Source Implementation
|
||||||
@@ -303,4 +311,41 @@ class SubmissionsRemoteDataSourceImpl implements SubmissionsRemoteDataSource {
|
|||||||
throw Exception('Failed to upload project file: $e');
|
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');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -171,4 +171,19 @@ class SubmissionsRepositoryImpl implements SubmissionsRepository {
|
|||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> deleteProjectFile({
|
||||||
|
required String fileId,
|
||||||
|
required String projectName,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
return await _remoteDataSource.deleteProjectFile(
|
||||||
|
fileId: fileId,
|
||||||
|
projectName: projectName,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,4 +54,12 @@ abstract class SubmissionsRepository {
|
|||||||
required String projectName,
|
required String projectName,
|
||||||
required String filePath,
|
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,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ class _SubmissionCreatePageState extends ConsumerState<SubmissionCreatePage> {
|
|||||||
List<ProjectFile> _existingFiles = []; // Existing files from API
|
List<ProjectFile> _existingFiles = []; // Existing files from API
|
||||||
bool _isSubmitting = false;
|
bool _isSubmitting = false;
|
||||||
bool _isLoadingDetail = false;
|
bool _isLoadingDetail = false;
|
||||||
|
String? _deletingFileId; // Track which file is being deleted
|
||||||
|
|
||||||
/// Whether we're editing an existing submission
|
/// Whether we're editing an existing submission
|
||||||
bool get isEditing => widget.submission != null;
|
bool get isEditing => widget.submission != null;
|
||||||
@@ -858,17 +859,22 @@ class _SubmissionCreatePageState extends ConsumerState<SubmissionCreatePage> {
|
|||||||
|
|
||||||
Widget _buildExistingFileItem(ProjectFile file, int index) {
|
Widget _buildExistingFileItem(ProjectFile file, int index) {
|
||||||
final fileName = file.fileUrl.split('/').last;
|
final fileName = file.fileUrl.split('/').last;
|
||||||
|
final isDeleting = _deletingFileId == file.id;
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.all(12),
|
padding: const EdgeInsets.all(12),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFFF8F9FA),
|
color: const Color(0xFFF8F9FA),
|
||||||
border: Border.all(color: AppColors.success),
|
border: Border.all(
|
||||||
|
color: isDeleting ? AppColors.grey500 : AppColors.success,
|
||||||
|
),
|
||||||
borderRadius: BorderRadius.circular(6),
|
borderRadius: BorderRadius.circular(6),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
// Network image
|
// Network image with delete overlay
|
||||||
|
Stack(
|
||||||
|
children: [
|
||||||
ClipRRect(
|
ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(4),
|
borderRadius: BorderRadius.circular(4),
|
||||||
child: CachedNetworkImage(
|
child: CachedNetworkImage(
|
||||||
@@ -902,6 +908,28 @@ class _SubmissionCreatePageState extends ConsumerState<SubmissionCreatePage> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
// 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),
|
const SizedBox(width: 12),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -909,25 +937,36 @@ class _SubmissionCreatePageState extends ConsumerState<SubmissionCreatePage> {
|
|||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
fileName,
|
fileName,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
color: AppColors.grey900,
|
color: isDeleting ? AppColors.grey500 : AppColors.grey900,
|
||||||
),
|
),
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 2),
|
const SizedBox(height: 2),
|
||||||
const Text(
|
Text(
|
||||||
'Đã tải lên',
|
isDeleting ? 'Đang xóa...' : 'Đã tải lên',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: AppColors.success,
|
color: isDeleting ? AppColors.grey500 : AppColors.success,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
// Checkmark icon
|
// 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(
|
const FaIcon(
|
||||||
FontAwesomeIcons.circleCheck,
|
FontAwesomeIcons.circleCheck,
|
||||||
size: 16,
|
size: 16,
|
||||||
@@ -938,6 +977,147 @@ class _SubmissionCreatePageState extends ConsumerState<SubmissionCreatePage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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() {
|
Widget _buildSubmitButton() {
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
|
|||||||
Reference in New Issue
Block a user