diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index b7e54fc..b040539 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -74,10 +74,10 @@ jobs:
uses: actions/checkout@v4
- name: Setup Java
- uses: oracle-actions/setup-java@v1
+ uses: actions/setup-java@v4
with:
- release: 19
- version: 19.0.2
+ distribution: oracle
+ java-version: 19
- name: View Java status
run: java --version
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 916f2ff..68c3eff 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -14,7 +14,16 @@ permissions:
jobs:
checkout:
name: Checkout repository
- if: ${{ github.event_name == 'push' || github.event.workflow_run.conclusion == 'success' }}
+ if: |
+ (
+ github.event_name == 'push' &&
+ github.actor != 'dependabot[bot]'
+ ) || (
+ github.event_name == 'workflow_run' &&
+ github.event.workflow_run.conclusion == 'success' &&
+ github.event.workflow_run.actor != 'dependabot[bot]'
+ )
+
runs-on: ubuntu-latest
outputs:
number: ${{ steps.pr-info-output.outputs.number }}
@@ -52,7 +61,7 @@ jobs:
path: .
python:
- name: Test web application
+ name: Web application test
needs: checkout
runs-on: ubuntu-latest
strategy:
@@ -89,23 +98,24 @@ jobs:
- name: Run tests
run: coverage run -m pytest -v .
- - name: Combine coverage reports
+ - name: Collect coverage data
run: coverage combine
- name: Report coverage
run: coverage report -m
- - name: Generate HTML coverage report
- run: coverage html -d htmlcov
+ - name: Rename coverage report
+ run: mv .coverage .coverage.python-${{ matrix.python-version }}
- - name: Upload HTML coverage report
+ - name: Upload coverage report
uses: actions/upload-artifact@v4
with:
- name: coverage-html-${{ matrix.python-version }}
- path: htmlcov
+ name: coverage-python-${{ matrix.python-version }}
+ path: .coverage.python-${{ matrix.python-version }}
+ include-hidden-files: true
flutter:
- name: Test client application
+ name: Client application test
needs: checkout
runs-on: ${{ matrix.os }}
strategy:
@@ -113,8 +123,38 @@ jobs:
matrix:
os: [macos-latest, ubuntu-latest, windows-latest]
+ steps:
+ - name: Download repository
+ uses: actions/download-artifact@v4
+ with:
+ name: repository
+
+ - name: Setup Flutter
+ uses: subosito/flutter-action@v2
+ with:
+ flutter-version: 3.24.3
+ channel: stable
+
+ - name: View Flutter status
+ run: |
+ flutter --version
+ flutter doctor -v
+
+ - name: Run tests
+ working-directory: app/resident_manager
+ run: flutter test
+
+ flutter-integration:
+ name: Client integration test
+ needs: checkout
+ runs-on: ubuntu-latest
+
env:
+ ODBC_CONNECTION_STRING: ${{ secrets.ODBC_CONNECTION_STRING}}
+ VNPAY_TMN_CODE: ${{ secrets.VNPAY_TMN_CODE }}
+ VNPAY_SECRET_KEY: ${{ secrets.VNPAY_SECRET_KEY }}
PRIVATE_KEY_SEED: ${{ secrets.PRIVATE_KEY_SEED }}
+ PORT: 8000
steps:
- name: Download repository
@@ -128,22 +168,116 @@ jobs:
flutter-version: 3.24.3
channel: stable
+ - name: Install extra apt dependencies
+ run: sudo apt-get install -y ninja-build libgtk-3-dev
+
- name: View Flutter status
run: |
flutter --version
flutter doctor -v
- - name: Run tests
+ - name: Setup Python
+ uses: actions/setup-python@v5
+ with:
+ python-version: "3.11"
+
+ - name: Install dependencies
+ run: pip install -r dev-requirements.txt
+
+ - name: Install ODBC driver 18
+ run: |
+ chmod +x scripts/odbc.sh
+ scripts/odbc.sh
+
+ - name: Start API server
+ run: |
+ uvicorn main:app --host 0.0.0.0 --port $PORT &
+ echo $! > /tmp/serverpid.txt
+
+ - name: Run integration tests
+ timeout-minutes: 30
working-directory: app/resident_manager
- run: flutter test
+ run: xvfb-run flutter test integration_test
+
+ - name: Stop API server
+ run: |
+ kill $(cat /tmp/serverpid.txt)
+ sleep 5
+
+ - name: Collect coverage data
+ run: coverage combine
+
+ - name: Report coverage
+ run: coverage report -m
+
+ - name: Rename coverage report
+ run: mv .coverage .coverage.flutter-integration
+
+ - name: Upload coverage report
+ uses: actions/upload-artifact@v4
+ with:
+ name: coverage-flutter-integration
+ path: .coverage.flutter-integration
+ include-hidden-files: true
+
+ python-coverage:
+ name: Combine coverage reports
+ needs: [python, flutter-integration]
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Download repository
+ uses: actions/download-artifact@v4
+ with:
+ name: repository
+
+ - name: Download coverage reports
+ uses: actions/download-artifact@v4
+ with:
+ pattern: coverage-*
+ path: .
+ merge-multiple: true
+
+ - name: Install dependencies
+ run: pip install -r dev-requirements.txt
+
+ - name: Combine coverage reports
+ run: coverage combine
+
+ - name: Report coverage
+ run: coverage report -m
+
+ - name: Save coverage report
+ run: coverage report -m > textcov.txt
+
+ - name: Upload coverage report
+ uses: actions/upload-artifact@v4
+ with:
+ name: coverage-txt
+ path: textcov.txt
+
+ - name: Generate HTML coverage report
+ run: coverage html -d htmlcov
+
+ - name: Upload HTML coverage report
+ uses: actions/upload-artifact@v4
+ with:
+ name: coverage-html
+ path: htmlcov
notification:
name: Comment in pull request
- needs: [checkout, python, flutter]
+ needs: [checkout, python, flutter, flutter-integration, python-coverage]
if: ${{ always() && needs.checkout.result == 'success' && github.event_name == 'workflow_run' }}
runs-on: ubuntu-latest
steps:
+ - name: Download coverage report
+ if: ${{ needs.python-coverage.result == 'success' }}
+ uses: actions/download-artifact@v4
+ with:
+ name: coverage-txt
+
- name: Create comment
uses: actions/github-script@v7
with:
@@ -152,9 +286,18 @@ jobs:
const url = "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}";
const sha = "${{ needs.checkout.outputs.sha}}".substr(0, 7);
let body = null;
- if (${{ needs.python.result == 'success' && needs.flutter.result == 'success' }})
+ if (${{ needs.python.result == 'success' && needs.flutter.result == 'success' && needs.flutter-integration.result == 'success' }})
{
body = `🎉 [All tests](${url}) of \`${sha}\` passed successfully.`;
+
+ if (${{ needs.python-coverage.result == 'success' }})
+ {
+ const fs = require("fs/promises");
+ const data = await fs.readFile("textcov.txt", { encoding: "utf8" });
+
+ const wrapped = `\`\`\`\n${data}\`\`\``;
+ body += `\n\nCoverage report
\n\n${wrapped}\n\n `;
+ }
}
else
{
diff --git a/README.md b/README.md
index c1e5bd2..cd3a9de 100644
--- a/README.md
+++ b/README.md
@@ -1,8 +1,8 @@
# resident-manager
-[![Azure deployment](https://github.com/Serious-senpai/resident-manager/actions/workflows/deploy.yml/badge.svg)](https://github.com/Serious-senpai/resident-manager/actions/workflows/deploy.yml)
-[![Flutter build](https://github.com/Serious-senpai/resident-manager/actions/workflows/build.yml/badge.svg)](https://github.com/Serious-senpai/resident-manager/actions/workflows/build.yml)
-[![Lint](https://github.com/Serious-senpai/resident-manager/actions/workflows/lint.yml/badge.svg)](https://github.com/Serious-senpai/resident-manager/actions/workflows/lint.yml)
-[![Run tests](https://github.com/Serious-senpai/resident-manager/actions/workflows/tests.yml/badge.svg)](https://github.com/Serious-senpai/resident-manager/actions/workflows/tests.yml)
+[![Azure deployment](https://github.com/Serious-senpai/resident-manager/actions/workflows/deploy.yml/badge.svg?branch=main&event=push)](https://github.com/Serious-senpai/resident-manager/actions/workflows/deploy.yml)
+[![Flutter build](https://github.com/Serious-senpai/resident-manager/actions/workflows/build.yml/badge.svg?branch=main&event=push)](https://github.com/Serious-senpai/resident-manager/actions/workflows/build.yml)
+[![Lint](https://github.com/Serious-senpai/resident-manager/actions/workflows/lint.yml/badge.svg?branch=main&event=push)](https://github.com/Serious-senpai/resident-manager/actions/workflows/lint.yml)
+[![Run tests](https://github.com/Serious-senpai/resident-manager/actions/workflows/tests.yml/badge.svg?branch=main&event=push)](https://github.com/Serious-senpai/resident-manager/actions/workflows/tests.yml)
Management system for residents sharing an apartment.
diff --git a/app/resident_manager/assets/landscape.png b/app/resident_manager/assets/landscape.png
new file mode 100644
index 0000000..79ccb64
Binary files /dev/null and b/app/resident_manager/assets/landscape.png differ
diff --git a/app/resident_manager/integration_test/widget_test.dart b/app/resident_manager/integration_test/widget_test.dart
new file mode 100644
index 0000000..457701a
--- /dev/null
+++ b/app/resident_manager/integration_test/widget_test.dart
@@ -0,0 +1,287 @@
+import "dart:math";
+
+import "package:flutter/material.dart";
+import "package:flutter_test/flutter_test.dart";
+import "package:integration_test/integration_test.dart";
+import "package:resident_manager/main.dart";
+import "package:resident_manager/src/config.dart";
+import "package:resident_manager/src/state.dart";
+import "package:resident_manager/src/widgets/home.dart";
+import "package:resident_manager/src/widgets/login.dart";
+import "package:resident_manager/src/widgets/register.dart";
+import "package:resident_manager/src/widgets/admin/reg_queue.dart";
+import "package:resident_manager/src/widgets/admin/residents.dart";
+import "package:resident_manager/src/widgets/admin/rooms.dart";
+
+final rng = Random();
+const WAIT_DURATION = Duration(seconds: 10);
+
+String randomString(int length) {
+ const chars = "abcdefghijklmnopqrstuvwxyz0123456789";
+ return String.fromCharCodes(Iterable.generate(length, (_) => chars.codeUnitAt(rng.nextInt(chars.length))));
+}
+
+String randomDigits(int length) {
+ const chars = "0123456789";
+ return String.fromCharCodes(Iterable.generate(length, (_) => chars.codeUnitAt(rng.nextInt(chars.length))));
+}
+
+void main() {
+ IntegrationTestWidgetsFlutterBinding.ensureInitialized();
+
+ testWidgets(
+ "Administrator login",
+ (tester) async {
+ final state = ApplicationState();
+ await state.prepare();
+ await state.deauthorize(); // Start integration test without existing authorization data
+
+ await tester.pumpWidget(MainApplication(state: state));
+ await tester.pumpAndSettle();
+
+ // Authorization fields
+ final fields = find.byWidgetPredicate((widget) => widget is TextField);
+ expect(fields, findsExactly(2));
+
+ await tester.enterText(fields.at(0), DEFAULT_ADMIN_USERNAME);
+ await tester.enterText(fields.at(1), DEFAULT_ADMIN_PASSWORD);
+
+ // Press the "Login as administrator" button
+ await tester.tap(find.byIcon(Icons.admin_panel_settings_outlined));
+ await tester.pumpAndSettle(WAIT_DURATION);
+
+ expect(find.byWidgetPredicate((widget) => widget is RegisterQueuePage), findsOneWidget);
+
+ // Open drawer
+ await tester.tap(find.byIcon(Icons.menu_outlined));
+ await tester.pumpAndSettle();
+
+ // Open residents list
+ await tester.tap(find.byIcon(Icons.people_outlined));
+ await tester.pumpAndSettle(WAIT_DURATION);
+
+ expect(find.byWidgetPredicate((widget) => widget is ResidentsPage), findsOneWidget);
+
+ // Open drawer
+ await tester.tap(find.byIcon(Icons.menu_outlined));
+ await tester.pumpAndSettle();
+
+ // Open rooms list
+ await tester.tap(find.byIcon(Icons.room_outlined));
+ await tester.pumpAndSettle(WAIT_DURATION);
+
+ expect(find.byWidgetPredicate((widget) => widget is RoomsPage), findsOneWidget);
+
+ // Open drawer
+ await tester.tap(find.byIcon(Icons.menu_outlined));
+ await tester.pumpAndSettle();
+
+ // Logout
+ await tester.tap(find.byIcon(Icons.logout_outlined));
+ await tester.pumpAndSettle();
+
+ expect(find.byWidgetPredicate((widget) => widget is LoginPage), findsOneWidget);
+ },
+ );
+
+ testWidgets(
+ "Resident registration",
+ (tester) async {
+ final state = ApplicationState();
+ await state.prepare();
+ await state.deauthorize(); // Start integration test without existing authorization data
+
+ await tester.pumpWidget(MainApplication(state: state));
+ await tester.pumpAndSettle();
+
+ // Press the "Register as resident" button
+ await tester.tap(find.byIcon(Icons.how_to_reg_outlined));
+ await tester.pumpAndSettle();
+
+ expect(find.byWidgetPredicate((widget) => widget is RegisterPage), findsOneWidget);
+
+ // Registration form fields
+ final registrationFields = find.byWidgetPredicate((widget) => widget is TextFormField);
+ expect(registrationFields, findsExactly(8));
+
+ final fullname = randomString(20);
+ final room = rng.nextInt(32767);
+ final phone = randomDigits(10);
+ final email = "$fullname@test.com";
+ final username = randomString(12);
+ final password = randomString(12);
+
+ // Fill in registration fields
+ await tester.enterText(registrationFields.at(0), fullname);
+ await tester.enterText(registrationFields.at(1), room.toString());
+ // Skip birthday
+ await tester.enterText(registrationFields.at(3), phone);
+ await tester.enterText(registrationFields.at(4), email);
+ await tester.enterText(registrationFields.at(5), username);
+ await tester.enterText(registrationFields.at(6), password);
+ await tester.enterText(registrationFields.at(7), password);
+
+ // Tap the "Register" button
+ await tester.tap(find.byIcon(Icons.how_to_reg_outlined));
+ await tester.pumpAndSettle(WAIT_DURATION);
+
+ // Open drawer
+ await tester.tap(find.byIcon(Icons.menu_outlined));
+ await tester.pumpAndSettle();
+
+ // Open login menu
+ await tester.tap(find.byIcon(Icons.lock_outlined));
+ await tester.pumpAndSettle();
+
+ expect(find.byWidgetPredicate((widget) => widget is LoginPage), findsOneWidget);
+
+ // Login as administrator
+ final adminLoginFields = find.byWidgetPredicate((widget) => widget is TextField);
+ expect(adminLoginFields, findsExactly(2));
+
+ await tester.enterText(adminLoginFields.at(0), DEFAULT_ADMIN_USERNAME);
+ await tester.enterText(adminLoginFields.at(1), DEFAULT_ADMIN_PASSWORD);
+
+ // Press the "Login as administrator" button
+ await tester.tap(find.byIcon(Icons.admin_panel_settings_outlined));
+ await tester.pumpAndSettle(WAIT_DURATION);
+
+ expect(find.byWidgetPredicate((widget) => widget is RegisterQueuePage), findsOneWidget);
+
+ // Open search interface
+ await tester.tap(find.byIcon(Icons.search_outlined));
+ await tester.pumpAndSettle();
+
+ final searchDialog = find.byWidgetPredicate((widget) => widget is SimpleDialog);
+ expect(searchDialog, findsOneWidget);
+
+ final searchFields = find.descendant(
+ of: searchDialog,
+ matching: find.byWidgetPredicate((widget) => widget is TextFormField),
+ );
+ expect(searchFields, findsExactly(3));
+
+ // Fill in search fields
+ await tester.enterText(searchFields.at(0), fullname);
+ await tester.enterText(searchFields.at(1), room.toString());
+ await tester.enterText(searchFields.at(2), username);
+ await tester.tap(find.descendant(of: searchDialog, matching: find.byIcon(Icons.done_outlined)));
+ await tester.pumpAndSettle(WAIT_DURATION);
+
+ // Exactly 2 checkboxes: 1 for "Select all", 1 for our search result
+ final checkboxes = find.byWidgetPredicate((widget) => widget is Checkbox);
+ expect(checkboxes, findsExactly(2));
+
+ // Toggle 3 times
+ await tester.tap(checkboxes.first);
+ await tester.tap(checkboxes.last);
+ await tester.tap(checkboxes.first);
+ await tester.pumpAndSettle();
+
+ // Approve the registration request
+ await tester.tap(find.byIcon(Icons.done_outlined));
+ await tester.pumpAndSettle(WAIT_DURATION);
+
+ // Open drawer
+ await tester.tap(find.byIcon(Icons.menu_outlined));
+ await tester.pumpAndSettle();
+
+ // Logout
+ await tester.tap(find.byIcon(Icons.logout_outlined));
+ await tester.pumpAndSettle();
+
+ expect(find.byWidgetPredicate((widget) => widget is LoginPage), findsOneWidget);
+
+ // Login as the newly created resident user
+ final loginFields = find.byWidgetPredicate((widget) => widget is TextField);
+ expect(loginFields, findsExactly(2));
+
+ await tester.enterText(loginFields.at(0), username);
+ await tester.enterText(loginFields.at(1), password);
+
+ // Press the "Login as resident" button
+ await tester.tap(find.byIcon(Icons.login_outlined));
+ await tester.pumpAndSettle(WAIT_DURATION);
+
+ // Reach the home page
+ expect(find.byWidgetPredicate((widget) => widget is HomePage), findsOneWidget);
+
+ // Open drawer
+ await tester.tap(find.byIcon(Icons.menu_outlined));
+ await tester.pumpAndSettle();
+
+ // Logout
+ await tester.tap(find.byIcon(Icons.logout_outlined));
+ await tester.pumpAndSettle();
+
+ expect(find.byWidgetPredicate((widget) => widget is LoginPage), findsOneWidget);
+
+ // Login as administrator
+ final adminLoginFields2 = find.byWidgetPredicate((widget) => widget is TextField);
+ expect(adminLoginFields2, findsExactly(2));
+
+ await tester.enterText(adminLoginFields2.at(0), DEFAULT_ADMIN_USERNAME);
+ await tester.enterText(adminLoginFields2.at(1), DEFAULT_ADMIN_PASSWORD);
+
+ // Press the "Login as administrator" button
+ await tester.tap(find.byIcon(Icons.admin_panel_settings_outlined));
+ await tester.pumpAndSettle(WAIT_DURATION);
+
+ expect(find.byWidgetPredicate((widget) => widget is RegisterQueuePage), findsOneWidget);
+
+ // Open drawer
+ await tester.tap(find.byIcon(Icons.menu_outlined));
+ await tester.pumpAndSettle();
+
+ // View resident list
+ await tester.tap(find.byIcon(Icons.people_outlined));
+ await tester.pumpAndSettle();
+
+ expect(find.byWidgetPredicate((widget) => widget is ResidentsPage), findsOneWidget);
+
+ // Open search interface
+ await tester.tap(find.byIcon(Icons.search_outlined));
+ await tester.pumpAndSettle();
+
+ final searchDialog2 = find.byWidgetPredicate((widget) => widget is SimpleDialog);
+ expect(searchDialog2, findsOneWidget);
+
+ final searchFields2 = find.descendant(
+ of: searchDialog2,
+ matching: find.byWidgetPredicate((widget) => widget is TextFormField),
+ );
+ expect(searchFields2, findsExactly(3));
+
+ // Fill in search fields
+ await tester.enterText(searchFields2.at(0), fullname);
+ await tester.enterText(searchFields2.at(1), room.toString());
+ await tester.enterText(searchFields2.at(2), username);
+ await tester.tap(find.descendant(of: searchDialog, matching: find.byIcon(Icons.done_outlined)));
+ await tester.pumpAndSettle(WAIT_DURATION);
+
+ // Exactly 2 checkboxes: 1 for "Select all", 1 for our search result
+ final checkboxes2 = find.byWidgetPredicate((widget) => widget is Checkbox);
+ expect(checkboxes2, findsExactly(2));
+
+ // Toggle 3 times
+ await tester.tap(checkboxes2.first);
+ await tester.tap(checkboxes2.last);
+ await tester.tap(checkboxes2.first);
+ await tester.pumpAndSettle();
+
+ // Delete the created account
+ await tester.tap(find.byIcon(Icons.delete_outlined));
+ await tester.pumpAndSettle(WAIT_DURATION);
+
+ // Open drawer
+ await tester.tap(find.byIcon(Icons.menu_outlined));
+ await tester.pumpAndSettle();
+
+ // Logout
+ await tester.tap(find.byIcon(Icons.logout_outlined));
+ await tester.pumpAndSettle();
+
+ expect(find.byWidgetPredicate((widget) => widget is LoginPage), findsOneWidget);
+ },
+ );
+}
diff --git a/app/resident_manager/lib/main.dart b/app/resident_manager/lib/main.dart
index 8cdb38f..b74be75 100644
--- a/app/resident_manager/lib/main.dart
+++ b/app/resident_manager/lib/main.dart
@@ -2,8 +2,8 @@ import "package:flutter/material.dart";
import "package:flutter_localization/flutter_localization.dart";
import "src/routes.dart";
-import "src/core/state.dart";
-import "src/core/translations.dart";
+import "src/state.dart";
+import "src/translations.dart";
import "src/widgets/state.dart";
import "src/widgets/common.dart";
import "src/widgets/home.dart";
@@ -11,6 +11,7 @@ import "src/widgets/login.dart";
import "src/widgets/register.dart";
import "src/widgets/admin/reg_queue.dart";
import "src/widgets/admin/residents.dart";
+import "src/widgets/admin/rooms.dart";
class MainApplication extends StateAwareWidget {
const MainApplication({super.key, required super.state});
@@ -42,6 +43,7 @@ class MainApplicationState extends AbstractCommonState {
ApplicationRoute.home: (context) => HomePage(state: state),
ApplicationRoute.adminRegisterQueue: (context) => RegisterQueuePage(state: state),
ApplicationRoute.adminResidentsPage: (context) => ResidentsPage(state: state),
+ ApplicationRoute.adminRoomsPage: (context) => RoomsPage(state: state),
},
initialRoute: initialRoute,
localizationsDelegates: state.localization.localizationsDelegates,
diff --git a/app/resident_manager/lib/src/core/config.dart b/app/resident_manager/lib/src/config.dart
similarity index 72%
rename from app/resident_manager/lib/src/core/config.dart
rename to app/resident_manager/lib/src/config.dart
index 56f0b3c..ecff93f 100644
--- a/app/resident_manager/lib/src/core/config.dart
+++ b/app/resident_manager/lib/src/config.dart
@@ -5,3 +5,5 @@ import "package:pinenacl/x25519.dart";
final epoch = DateTime.utc(2024, 1, 1);
const int DB_PAGINATION_QUERY = 50;
final serverKey = PublicKey(base64.decode("FUgK7Fi7O7eSDi5Ekd/hbmjIN3k/WcLevFTgZqmn9Bo="));
+const DEFAULT_ADMIN_USERNAME = "admin";
+const DEFAULT_ADMIN_PASSWORD = "NgaiLongGey";
diff --git a/app/resident_manager/lib/src/core/models/residents.dart b/app/resident_manager/lib/src/core/models/residents.dart
deleted file mode 100644
index 80743b9..0000000
--- a/app/resident_manager/lib/src/core/models/residents.dart
+++ /dev/null
@@ -1,85 +0,0 @@
-import "dart:convert";
-
-import "info.dart";
-import "../state.dart";
-
-/// Represents a resident.
-class Resident extends PublicInfo {
- /// Constructs a [Resident] object with the given [id], [name], [room], [birthday], [phone], and [email].
- Resident({
- required super.id,
- required super.name,
- required super.room,
- super.birthday,
- super.phone,
- super.email,
- super.username,
- super.hashedPassword,
- });
-
- Resident.fromJson(super.data) : super.fromJson();
-
- Map toJson() => {
- "id": id,
- "name": name,
- "room": room,
- "birthday": birthday?.toIso8601String(),
- "phone": phone,
- "email": email,
- };
-
- static Future> query({
- required ApplicationState state,
- required int offset,
- int? id,
- String? name,
- int? room,
- String? username,
- String? orderBy,
- bool? ascending,
- }) async {
- final result = [];
- final authorization = state.authorization;
- if (authorization == null) {
- return result;
- }
-
- final params = {"offset": offset.toString()};
- if (id != null) {
- params["id"] = id.toString();
- }
- if (name != null && name.isNotEmpty) {
- params["name"] = name;
- }
- if (room != null) {
- params["room"] = room.toString();
- }
- if (username != null && username.isNotEmpty) {
- params["username"] = username;
- }
- if (orderBy != null && orderBy.isNotEmpty) {
- params["order_by"] = orderBy;
- }
- if (ascending != null) {
- params["ascending"] = ascending.toString();
- }
-
- final response = await state.get("/api/v1/admin/residents", queryParameters: params);
- if (response.statusCode == 200) {
- final data = json.decode(utf8.decode(response.bodyBytes)) as List;
- result.addAll(data.map(Resident.fromJson));
- }
-
- return result;
- }
-
- /// Count the number of residents.
- static Future count({required ApplicationState state}) async {
- final response = await state.get("/api/v1/admin/residents/count");
- if (response.statusCode == 200) {
- return json.decode(utf8.decode(response.bodyBytes));
- }
-
- return null;
- }
-}
diff --git a/app/resident_manager/lib/src/core/errors.dart b/app/resident_manager/lib/src/errors.dart
similarity index 100%
rename from app/resident_manager/lib/src/core/errors.dart
rename to app/resident_manager/lib/src/errors.dart
diff --git a/app/resident_manager/lib/src/core/http.dart b/app/resident_manager/lib/src/http.dart
similarity index 100%
rename from app/resident_manager/lib/src/core/http.dart
rename to app/resident_manager/lib/src/http.dart
diff --git a/app/resident_manager/lib/src/core/models/auth.dart b/app/resident_manager/lib/src/models/auth.dart
similarity index 100%
rename from app/resident_manager/lib/src/core/models/auth.dart
rename to app/resident_manager/lib/src/models/auth.dart
diff --git a/app/resident_manager/lib/src/core/models/info.dart b/app/resident_manager/lib/src/models/info.dart
similarity index 91%
rename from app/resident_manager/lib/src/core/models/info.dart
rename to app/resident_manager/lib/src/models/info.dart
index 5c20071..09ff9fe 100644
--- a/app/resident_manager/lib/src/core/models/info.dart
+++ b/app/resident_manager/lib/src/models/info.dart
@@ -58,10 +58,10 @@ class PublicInfo extends PersonalInfo with Snowflake {
@override
final int id;
- /// The username for the registration request, this data is only available from the admin endpoint.
+ /// The username information, this data is only available from the admin endpoint.
String? username;
- /// The hashed password for the registration request, this data is only available from the admin endpoint.
+ /// The hashed password information, this data is only available from the admin endpoint.
String? hashedPassword;
/// Constructs a [PublicInfo] object with the given [id], [name], [room], [birthday], [phone], and [email].
diff --git a/app/resident_manager/lib/src/core/models/reg_request.dart b/app/resident_manager/lib/src/models/reg_request.dart
similarity index 75%
rename from app/resident_manager/lib/src/core/models/reg_request.dart
rename to app/resident_manager/lib/src/models/reg_request.dart
index a3df578..4103ee8 100644
--- a/app/resident_manager/lib/src/core/models/reg_request.dart
+++ b/app/resident_manager/lib/src/models/reg_request.dart
@@ -43,27 +43,18 @@ class RegisterRequest extends PublicInfo {
return result;
}
- final params = {"offset": offset.toString()};
- if (id != null) {
- params["id"] = id.toString();
- }
- if (name != null && name.isNotEmpty) {
- params["name"] = name;
- }
- if (room != null) {
- params["room"] = room.toString();
- }
- if (username != null && username.isNotEmpty) {
- params["username"] = username;
- }
- if (orderBy != null && orderBy.isNotEmpty) {
- params["order_by"] = orderBy;
- }
- if (ascending != null) {
- params["ascending"] = ascending.toString();
- }
-
- final response = await state.get("/api/v1/admin/reg-request", queryParameters: params);
+ final response = await state.get(
+ "/api/v1/admin/reg-request",
+ queryParameters: {
+ "offset": offset.toString(),
+ if (id != null) "id": id.toString(),
+ if (name != null && name.isNotEmpty) "name": name,
+ if (room != null) "room": room.toString(),
+ if (username != null && username.isNotEmpty) "username": username,
+ if (orderBy != null && orderBy.isNotEmpty) "order_by": orderBy,
+ if (ascending != null) "ascending": ascending.toString(),
+ },
+ );
if (response.statusCode == 200) {
final data = json.decode(utf8.decode(response.bodyBytes)) as List;
result.addAll(data.map(RegisterRequest.fromJson));
@@ -122,8 +113,22 @@ class RegisterRequest extends PublicInfo {
}
/// Count the number of registration requests.
- static Future count({required ApplicationState state}) async {
- final response = await state.get("/api/v1/admin/reg-request/count");
+ static Future count({
+ required ApplicationState state,
+ int? id,
+ String? name,
+ int? room,
+ String? username,
+ }) async {
+ final response = await state.get(
+ "/api/v1/admin/reg-request/count",
+ queryParameters: {
+ if (id != null) "id": id.toString(),
+ if (name != null && name.isNotEmpty) "name": name,
+ if (room != null) "room": room.toString(),
+ if (username != null && username.isNotEmpty) "username": username,
+ },
+ );
if (response.statusCode == 200) {
return json.decode(utf8.decode(response.bodyBytes));
}
diff --git a/app/resident_manager/lib/src/models/residents.dart b/app/resident_manager/lib/src/models/residents.dart
new file mode 100644
index 0000000..8c670cb
--- /dev/null
+++ b/app/resident_manager/lib/src/models/residents.dart
@@ -0,0 +1,102 @@
+import "dart:convert";
+
+import "info.dart";
+import "snowflake.dart";
+import "../state.dart";
+
+/// Represents a resident.
+class Resident extends PublicInfo {
+ /// Constructs a [Resident] object with the given [id], [name], [room], [birthday], [phone], and [email].
+ Resident({
+ required super.id,
+ required super.name,
+ required super.room,
+ super.birthday,
+ super.phone,
+ super.email,
+ super.username,
+ super.hashedPassword,
+ });
+
+ Resident.fromJson(super.data) : super.fromJson();
+
+ Map toJson() => {
+ "id": id,
+ "name": name,
+ "room": room,
+ "birthday": birthday?.toIso8601String(),
+ "phone": phone,
+ "email": email,
+ };
+
+ static Future delete({
+ required ApplicationState state,
+ required Iterable objects,
+ }) async {
+ final headers = {"content-type": "application/json"};
+ final data = List