feat: dynamic file upload limit per document requirement (frontend & backend)
This commit is contained in:
1 parent
01d636ea19
commit
d8dd86ba04
8 files changed
+168
-52
No files matched your search
@@ -8,6 +8,7 @@ class DocumentRequirementModel {
|
||||
final DateTime? dueDate;
|
||||
final String acceptedFileTypes;
|
||||
final String schoolLevels;
|
||||
final int maxFiles;
|
||||
final DateTime? createdAt;
|
||||
final DateTime? updatedAt;
|
||||
|
||||
@@ -21,11 +22,15 @@ class DocumentRequirementModel {
|
||||
this.dueDate,
|
||||
this.acceptedFileTypes = 'pdf,jpg,jpeg,png',
|
||||
this.schoolLevels = 'JHS,SHS',
|
||||
this.maxFiles = 1,
|
||||
this.createdAt,
|
||||
this.updatedAt,
|
||||
});
|
||||
|
||||
factory DocumentRequirementModel.fromJson(Map<String, dynamic> json) {
|
||||
final rawMax = json['max_files'] ?? json['maxFiles'];
|
||||
final parsedMax = rawMax != null ? (rawMax as num).toInt() : 1;
|
||||
|
||||
return DocumentRequirementModel(
|
||||
id: json['id'] as int,
|
||||
name: json['name'] as String,
|
||||
@@ -39,6 +44,7 @@ class DocumentRequirementModel {
|
||||
: null,
|
||||
acceptedFileTypes: json['accepted_file_types'] ?? 'pdf,jpg,jpeg,png',
|
||||
schoolLevels: json['school_levels'] ?? 'JHS,SHS',
|
||||
maxFiles: parsedMax >= 1 ? parsedMax : 1,
|
||||
createdAt:
|
||||
(json['created_at'] != null &&
|
||||
json['created_at'].toString().isNotEmpty)
|
||||
@@ -63,6 +69,8 @@ class DocumentRequirementModel {
|
||||
'dueDate': dueDate?.toIso8601String(),
|
||||
'acceptedFileTypes': acceptedFileTypes,
|
||||
'schoolLevels': schoolLevels,
|
||||
'maxFiles': maxFiles,
|
||||
'max_files': maxFiles,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -76,6 +84,7 @@ class DocumentRequirementModel {
|
||||
DateTime? dueDate,
|
||||
String? acceptedFileTypes,
|
||||
String? schoolLevels,
|
||||
int? maxFiles,
|
||||
}) {
|
||||
return DocumentRequirementModel(
|
||||
id: id ?? this.id,
|
||||
@@ -87,6 +96,7 @@ class DocumentRequirementModel {
|
||||
dueDate: dueDate ?? this.dueDate,
|
||||
acceptedFileTypes: acceptedFileTypes ?? this.acceptedFileTypes,
|
||||
schoolLevels: schoolLevels ?? this.schoolLevels,
|
||||
maxFiles: maxFiles ?? this.maxFiles,
|
||||
createdAt: createdAt,
|
||||
updatedAt: updatedAt,
|
||||
);
|
||||
|
||||
@@ -162,8 +162,8 @@ class _RequirementsSettingsModalState
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Switch(
|
||||
value: req.isEnabled ?? true,
|
||||
activeColor: AppColors.primaryGreen,
|
||||
value: req.isEnabled,
|
||||
activeThumbColor: AppColors.primaryGreen,
|
||||
onChanged: (val) {
|
||||
_toggleRequirement(req, val);
|
||||
},
|
||||
@@ -193,17 +193,7 @@ class _RequirementsSettingsModalState
|
||||
|
||||
void _toggleRequirement(DocumentRequirementModel req, bool isEnabled) async {
|
||||
try {
|
||||
final updatedReq = DocumentRequirementModel(
|
||||
id: req.id,
|
||||
name: req.name,
|
||||
description: req.description,
|
||||
category: req.category,
|
||||
isMandatory: req.isMandatory,
|
||||
isEnabled: isEnabled,
|
||||
dueDate: req.dueDate,
|
||||
acceptedFileTypes: req.acceptedFileTypes,
|
||||
schoolLevels: req.schoolLevels,
|
||||
);
|
||||
final updatedReq = req.copyWith(isEnabled: isEnabled);
|
||||
await ref
|
||||
.read(requirementMutationProvider.notifier)
|
||||
.updateRequirement(updatedReq);
|
||||
|
||||
@@ -18,6 +18,7 @@ import '../../../shared/inputs/custom_text_field.dart';
|
||||
import '../../../shared/buttons/primary_button.dart';
|
||||
import '../../../shared/dialogs/error_dialog.dart';
|
||||
import '../../../../domain/entities/student_model.dart';
|
||||
import '../../../../domain/entities/document_requirement_model.dart';
|
||||
import 'package:wolt_modal_sheet/wolt_modal_sheet.dart';
|
||||
import 'upload_modal_header.dart';
|
||||
|
||||
@@ -727,23 +728,32 @@ class _UploadOcrModalState extends ConsumerState<UploadOcrModal> {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for duplicate document types within the same upload batch
|
||||
final docTypeCounts = <String, int>{};
|
||||
// Check against max allowed files per requirement within the same upload batch
|
||||
final requirements = ref.read(documentRequirementsProvider).value ?? [];
|
||||
final reqMap = <int, dynamic>{for (final r in requirements) r.id: r};
|
||||
|
||||
final reqCounts = <int, int>{};
|
||||
for (final e in pending) {
|
||||
final dt = e.selectedDocumentType;
|
||||
if (dt != null && dt.isNotEmpty) {
|
||||
docTypeCounts[dt] = (docTypeCounts[dt] ?? 0) + 1;
|
||||
if (e.selectedRequirementId != null) {
|
||||
reqCounts[e.selectedRequirementId!] = (reqCounts[e.selectedRequirementId!] ?? 0) + 1;
|
||||
}
|
||||
}
|
||||
final duplicatesInBatch = docTypeCounts.entries
|
||||
.where((entry) => entry.value > 1)
|
||||
.map((entry) => entry.key)
|
||||
.toList();
|
||||
if (duplicatesInBatch.isNotEmpty) {
|
||||
|
||||
final exceededInBatch = <String>[];
|
||||
for (final entry in reqCounts.entries) {
|
||||
final req = reqMap[entry.key];
|
||||
final maxAllowed = (req is DocumentRequirementModel) ? req.maxFiles : 1;
|
||||
if (entry.value > maxAllowed) {
|
||||
final reqName = req != null ? req.name : 'Selected Requirement';
|
||||
exceededInBatch.add('• $reqName (${entry.value} files selected, max allowed: $maxAllowed)');
|
||||
}
|
||||
}
|
||||
|
||||
if (exceededInBatch.isNotEmpty) {
|
||||
showErrorDialog(
|
||||
context,
|
||||
'Duplicate Document Types',
|
||||
'Multiple files have the same document type (${duplicatesInBatch.join(", ")}).\n\nOnly one file per document type is allowed per student. Please assign distinct document types.',
|
||||
'Upload Limit Exceeded',
|
||||
'The following document requirement(s) exceed their allowed file limit:\n\n${exceededInBatch.join('\n')}\n\nPlease adjust your selected files or document types.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -1353,9 +1363,11 @@ class _UploadOcrModalState extends ConsumerState<UploadOcrModal> {
|
||||
),
|
||||
));
|
||||
for (final r in jhs) {
|
||||
final maxF = (r is DocumentRequirementModel) ? r.maxFiles : 1;
|
||||
final limitHint = maxF > 1 ? ' (Max $maxF files)' : '';
|
||||
entries.add(DropdownMenuEntry<int>(
|
||||
value: r.id as int,
|
||||
label: '${r.name}${r.isMandatory ? " *" : ""}',
|
||||
label: '${r.name}${r.isMandatory ? " *" : ""}$limitHint',
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -1371,9 +1383,11 @@ class _UploadOcrModalState extends ConsumerState<UploadOcrModal> {
|
||||
),
|
||||
));
|
||||
for (final r in shs) {
|
||||
final maxF = (r is DocumentRequirementModel) ? r.maxFiles : 1;
|
||||
final limitHint = maxF > 1 ? ' (Max $maxF files)' : '';
|
||||
entries.add(DropdownMenuEntry<int>(
|
||||
value: r.id as int,
|
||||
label: '${r.name}${r.isMandatory ? " *" : ""}',
|
||||
label: '${r.name}${r.isMandatory ? " *" : ""}$limitHint',
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -885,6 +885,39 @@ class _RequirementsModalState extends ConsumerState<RequirementsModal> {
|
||||
),
|
||||
),
|
||||
|
||||
// Max Files Tag
|
||||
const SizedBox(width: 6),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: (isDark ? AppColors.darkSurface2 : const Color(0xFFF1F3F5)),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
border: Border.all(
|
||||
color: isDark ? AppColors.darkBorder : Colors.grey.shade300,
|
||||
width: 0.5,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.file_copy_outlined,
|
||||
size: 10,
|
||||
color: isDark ? AppColors.darkTextSecondary : AppColors.textSecondary,
|
||||
),
|
||||
const SizedBox(width: 3),
|
||||
Text(
|
||||
req.maxFiles == 1 ? '1 file' : 'Max: ${req.maxFiles} files',
|
||||
style: TextStyle(
|
||||
fontSize: 10.5,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: isDark ? AppColors.darkTextSecondary : AppColors.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Inactive Tag
|
||||
if (!req.isEnabled) ...[
|
||||
const SizedBox(width: 6),
|
||||
@@ -1030,6 +1063,7 @@ class _RequirementFormModalState extends ConsumerState<RequirementFormModal> {
|
||||
late bool _isMandatory;
|
||||
late bool _isEnabled;
|
||||
late String _acceptedFileTypes;
|
||||
late int _maxFiles;
|
||||
|
||||
bool _isLoading = false;
|
||||
|
||||
@@ -1057,6 +1091,7 @@ class _RequirementFormModalState extends ConsumerState<RequirementFormModal> {
|
||||
|
||||
_isMandatory = req?.isMandatory ?? true;
|
||||
_isEnabled = req?.isEnabled ?? true;
|
||||
_maxFiles = req?.maxFiles ?? 1;
|
||||
|
||||
String savedTypes = (req?.acceptedFileTypes ?? 'pdf,jpg,jpeg,png').replaceAll(' ', '');
|
||||
const validItems = [
|
||||
@@ -1241,6 +1276,26 @@ class _RequirementFormModalState extends ConsumerState<RequirementFormModal> {
|
||||
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Maximum Files Allowed Dropdown
|
||||
DropdownButtonFormField<int>(
|
||||
initialValue: _maxFiles,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Maximum Files Allowed',
|
||||
helperText: 'Number of files that can be uploaded for this requirement',
|
||||
border: OutlineInputBorder(),
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 12),
|
||||
),
|
||||
items: [1, 2, 3, 4, 5, 10, 15, 20].map((n) {
|
||||
return DropdownMenuItem<int>(
|
||||
value: n,
|
||||
child: Text(n == 1 ? '1 file (Single upload)' : '$n files'),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: (v) => setState(() => _maxFiles = v ?? 1),
|
||||
),
|
||||
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Mandatory Switch
|
||||
SwitchListTile(
|
||||
title: const Text('Mandatory Requirement', style: TextStyle(fontWeight: FontWeight.w600, fontSize: 13.5)),
|
||||
@@ -1328,6 +1383,7 @@ class _RequirementFormModalState extends ConsumerState<RequirementFormModal> {
|
||||
dueDate: dueDate,
|
||||
acceptedFileTypes: _acceptedFileTypes,
|
||||
schoolLevels: 'JHS,SHS',
|
||||
maxFiles: _maxFiles,
|
||||
);
|
||||
|
||||
if (widget.requirement != null && categories.length == 1) {
|
||||
|
||||
Reference in new issue
Block a user