feat: complete leave approval MVP

This commit is contained in:
selfrelease
2026-07-18 19:20:07 +08:00
parent 2105fe3bac
commit 090a7e33ce
133 changed files with 7845 additions and 100 deletions
@@ -0,0 +1,12 @@
import 'package:aioa_mobile/core/auth/authenticated_http_client.dart';
import 'package:aioa_mobile/core/config/runtime_config.dart';
import 'package:aioa_mobile/features/form/data/ai_leave_suggestion_repository.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
final aiLeaveSuggestionRepositoryProvider =
Provider<AiLeaveSuggestionRepository>(
(ref) => AiLeaveSuggestionRepository(
client: ref.watch(authenticatedHttpClientProvider),
baseUrl: RuntimeConfig.apiBaseUrl,
),
);
@@ -0,0 +1,15 @@
import 'package:aioa_mobile/core/auth/authenticated_http_client.dart';
import 'package:aioa_mobile/core/config/runtime_config.dart';
import 'package:aioa_mobile/features/form/data/form_definition_repository.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
final formDefinitionRepositoryProvider = Provider<FormDefinitionRepository>(
(ref) => FormDefinitionRepository(
client: ref.watch(authenticatedHttpClientProvider),
baseUrl: RuntimeConfig.apiBaseUrl,
),
);
final leaveFormDefinitionProvider = FutureProvider<LoadedFormDefinition>((ref) {
return ref.watch(formDefinitionRepositoryProvider).loadLeaveRequest();
});
@@ -0,0 +1,97 @@
import 'package:aioa_mobile/core/auth/authenticated_http_client.dart';
import 'package:aioa_mobile/core/config/runtime_config.dart';
import 'dart:typed_data';
import 'package:aioa_mobile/features/form/data/leave_attachment_repository.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
final leaveAttachmentRepositoryProvider = Provider<LeaveAttachmentRepository>(
(ref) => LeaveAttachmentRepository(
client: ref.watch(authenticatedHttpClientProvider),
baseUrl: RuntimeConfig.apiBaseUrl,
),
);
class LeaveAttachmentState {
const LeaveAttachmentState({
this.items = const [],
this.uploading = false,
this.progress = 0,
this.error,
});
final List<LeaveAttachmentItem> items;
final bool uploading;
final double progress;
final String? error;
LeaveAttachmentState copyWith({
List<LeaveAttachmentItem>? items,
bool? uploading,
double? progress,
String? error,
bool clearError = false,
}) => LeaveAttachmentState(
items: items ?? this.items,
uploading: uploading ?? this.uploading,
progress: progress ?? this.progress,
error: clearError ? null : error ?? this.error,
);
}
final leaveAttachmentProvider =
NotifierProvider.family<
LeaveAttachmentController,
LeaveAttachmentState,
String
>((leaveRequestId) => LeaveAttachmentController(leaveRequestId));
class LeaveAttachmentController extends Notifier<LeaveAttachmentState> {
LeaveAttachmentController(this._leaveRequestId);
final String _leaveRequestId;
@override
LeaveAttachmentState build() => const LeaveAttachmentState();
Future<void> load() async {
try {
final items = await ref
.read(leaveAttachmentRepositoryProvider)
.list(_leaveRequestId);
state = state.copyWith(items: items, clearError: true);
} on LeaveAttachmentException catch (error) {
state = state.copyWith(error: error.message);
}
}
Future<void> upload({
required String fileName,
required String contentType,
required Uint8List bytes,
}) async {
if (state.uploading) return;
state = state.copyWith(uploading: true, progress: 0, clearError: true);
try {
final item = await ref
.read(leaveAttachmentRepositoryProvider)
.upload(
leaveRequestId: _leaveRequestId,
fileName: fileName,
contentType: contentType,
bytes: bytes,
onProgress: (progress) {
state = state.copyWith(progress: progress);
},
);
state = state.copyWith(
items: [...state.items, item],
uploading: false,
progress: 1,
clearError: true,
);
} on LeaveAttachmentException catch (error) {
state = state.copyWith(uploading: false, error: error.message);
}
}
}
@@ -1,21 +1,47 @@
import 'dart:async';
import 'package:aioa_mobile/core/forms/schema/form_schema.dart';
import 'package:aioa_mobile/core/forms/schema/form_validator.dart';
import 'package:aioa_mobile/features/form/domain/leave_form_definition.dart';
import 'package:aioa_mobile/features/form/data/leave_local_draft_repository.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
final leaveLocalDraftRepositoryProvider = Provider<LeaveLocalDraftRepository>(
(ref) => LeaveLocalDraftRepository(),
);
final leaveDraftProvider =
NotifierProvider<LeaveDraftController, DynamicFormState>(
LeaveDraftController.new,
);
class LeaveDraftController extends Notifier<DynamicFormState> {
late final LeaveLocalDraftRepository _repository;
@override
DynamicFormState build() => const DynamicFormState();
DynamicFormState build() {
_repository = ref.watch(leaveLocalDraftRepositoryProvider);
unawaited(_restore());
return const DynamicFormState();
}
Future<void> _restore() async {
final restored = await _repository.load();
if (restored == null || state.values.isNotEmpty) return;
state = DynamicFormState(
values: restored.values,
restoredAt: restored.savedAt,
);
}
void setValue(String field, Object? value) {
final values = {...state.values, field: value};
final errors = {...state.errors}..remove(field);
state = state.copyWith(values: values, errors: errors);
state = state.copyWith(
values: values,
errors: errors,
clearRestoredAt: true,
);
unawaited(_repository.save(values));
}
void applyAiSuggestion() {
@@ -36,13 +62,22 @@ class LeaveDraftController extends Notifier<DynamicFormState> {
'reason': '办理个人事务,已提前完成工作交接。',
},
);
unawaited(_repository.save(state.values));
}
bool validate() {
final errors = validateDynamicForm(
leaveFormDefinition.dataSchema,
state.values,
);
void applySuggestion(Map<String, Object?> suggestion) {
final values = {...state.values, ...suggestion};
state = DynamicFormState(values: values);
unawaited(_repository.save(values));
}
Future<void> clear() async {
await _repository.clear();
state = const DynamicFormState();
}
bool validate(JsonFormSchema schema) {
final errors = validateDynamicForm(schema, state.values);
final startsAt = DateTime.tryParse(
state.values['startsAt'] as String? ?? '',
);
@@ -0,0 +1,44 @@
import 'package:aioa_mobile/core/auth/authenticated_http_client.dart';
import 'package:aioa_mobile/core/config/runtime_config.dart';
import 'package:aioa_mobile/features/form/data/leave_draft_submission_repository.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
final leaveDraftSubmissionRepositoryProvider =
Provider<LeaveDraftSubmissionRepository>(
(ref) => LeaveDraftSubmissionRepository(
client: ref.watch(authenticatedHttpClientProvider),
baseUrl: RuntimeConfig.apiBaseUrl,
),
);
final leaveSubmissionProvider =
NotifierProvider<LeaveSubmissionController, LeaveSubmissionState>(
LeaveSubmissionController.new,
);
class LeaveSubmissionState {
const LeaveSubmissionState({this.submitting = false, this.error});
final bool submitting;
final String? error;
}
class LeaveSubmissionController extends Notifier<LeaveSubmissionState> {
@override
LeaveSubmissionState build() => const LeaveSubmissionState();
Future<CreatedLeaveDraft?> submit(Map<String, Object?> values) async {
if (state.submitting) return null;
state = const LeaveSubmissionState(submitting: true);
try {
final created = await ref
.read(leaveDraftSubmissionRepositoryProvider)
.create(values);
state = const LeaveSubmissionState();
return created;
} on LeaveDraftSubmissionException catch (error) {
state = LeaveSubmissionState(error: error.message);
return null;
}
}
}