Merge remote-tracking branch 'origin/Frontend_UI'

This commit is contained in:
EgorVorobev 2025-02-19 17:44:19 +03:00
commit 064a29139f
16 changed files with 306 additions and 141 deletions

122
README.md
View File

@ -1,121 +1 @@
[![Android Studio version](https://img.shields.io/endpoint?url=https%3A%2F%2Fsicampus.ru%2Fgitea%2Fcore%2Fdocs%2Fraw%2Fbranch%2Fmain%2Fandroid-studio-label.json)](https://sicampus.ru/gitea/core/docs/src/branch/main/how-upload-project.md)
# НТО 2024. II отборочный этап. Командные задани — клиентская часть
## 📖 Предыстория
В компании S контроль доступа в офис осуществляется с помощью СКУД (системы контроля управления доступом). На данный момент у каждого сотрудника компании есть карта-пропуск с NFC меткой. А у каждой входной двери есть считыватель с обеих сторон. При поднесении карты к считывателю, дверь открывается, а информация о времени входа или выхода сотрудника фиксируется в базе данных.
Администрации компании S требуется мобильное приложение, как для рядовых сотрудников, так и для администрации с возможностью просмотра посещений и работой электронного пропуска как временной замены обычного (при помощи сканировании QR кода, который находится на считывателе). Поскольку в приложении есть возможность использовать телефон как пропуск - то к данному приложению повышенные требования к безопасности всех данных, находящихся внутри него.
## 📋 Системные требования
| **Параметр** | **Требование** |
|-----------------------------|---------------------------------------|
| **Минимальная версия Android** | 9.0 (API 28) |
| **Целевая версия Android** | 14 (API 34) |
| **Поддерживаемые устройства** | смартфоны, планшеты |
| **Ориентация экранов** | портретная |
| **Языки** | русский, английский |
| **Разрешения** | доступ к интернету, камера (при необходимости) |
## 📱 Техническое задание
Требуется разработать нативное мобильное приложение, которое будет содержать следующие экраны.
### 1. Экран авторизации
> Данный экран должен быть показан при первом входе в приложение, а также в ситуациях, когда пользователь не зарегистрировался в приложении.
#### Элементы, которые должны присутствовать на экране:
- Поле ввода (`id/username`), в котором пользователю необходимо ввести свой логин.
- Кнопка входа (`id/login`), по нажатию на которую пользователь авторизуется в системе.
- По умолчанию скрытое (`GONE`) текстовое поле с ошибкой (`id/error`).
#### Требования к компонентам:
1. В пустом поле ввода должна отображаться подсказка, что требуется ввести пользователю.
2. Если хотя бы одно из условий ниже соблюдено - кнопка должна быть неактивной:
- Поле ввода пустое.
- Количество символов менее 3х.
- Логин начинается с цифры.
- Логин содержит символы, отличные от латинского алфавита и цифр.
3. Поле ввода и кнопку должно быть видно при раскрытии клавиатуры.
4. - При нажатии на кнопку входа необходимо проверить, что данный пользователь существует с помощью запроса `api/<LOGIN>/auth` (подробное описание представлено в техническом задании серверной части).
5. В случае отсутствия логина или любой другой неполадки - необходимо вывести ошибку, пока пользователь не изменит текстовое поле или повторно не нажмёт на кнопку.
6. После нажатия на кнопку - логин должен быть сохранён и при следующем открытии приложения экран авторизации не должен быть показан.
7. После нажатия на кнопку - при нажатии стрелки назад - экран авторизации не должен быть показан повторно.
8. Экран авторизации показывается только в случае, если пользователь неавторизован.
### 2. Главный экран
> Данный экран содержит общую информацию о пользователе:
>- ФИО
>- Фото
>- Должность
>- Время последнего входа
#### Элементы, которые должны присутствовать на экране:
- Текстовое поле (`id/fullname`), в котором написано имя пользователя.
- Изображение (`id/photo`), на котором отображено фото пользователя.
- Текстовое поле (`id/position`), в котором написана должность пользователя.
- Текстовое поле (`id/lastEntry`), в котором написана дата и время последнего входа пользователя.
- Кнопка (`id/logout`) для выхода пользователя из аккаунта.
- Кнопка (`id/refresh`) для обновления данных.
- Кнопка (`id/scan`) для сканирования QR кода.
- По умолчанию скрытое текстовое поле с ошибкой (`id/error`).
#### Требования к компонентам:
- В случае любой ошибки необходимо скрыть все элементы, кроме текстового поля с ошибкой и кнопки обновления данных.
- Для получения данных необходимо использовать сетевой запрос `/api/<LOGIN>/info`.
- Формат даты и времени последнего входа пользователя: `yyyy-MM-dd HH:mm` (например: 2024-02-31 08:31). Время необходимо отображать с сервера, без поправок на часовой пояс или локальное смещение.
- При нажатии на кнопку выход все данные (если есть) пользователя должны быть очищены, а приложение должно открыть экран авторизации.
- При нажатии кнопки сканирования необходимо открыть экран сканирования QR кода.
- При нажатии на кнопку обновления данных - необходимо повторно вызывать сетевой запрос для получения актуальных данных.
### 3. Экран сканирования QR-кода
> Данный экран позволяет отсканировать код на турникете и войти с помощью смартфона. В данном случае данный экран будет уже написан и представлен dам в готовом виде в заготовке. Вам необходимо только подписаться на его результат с помощью **Result API** и обработать считанные данные из QR кода. **Данный экран нельзя модифицировать. Он поставляется как есть.**
### 4. Экран с результатом сканирования QR кода
> На данном экране необходимо вывести успешность или неуспешность входа с помощью метода QR кода.
#### Элементы, которые должны присутствовать на экране:
- Текстовое поле (`id/result`), где содержится текст об успешности или неуспешности входа.
- Кнопка (`id/close`) для закрытия данного экрана.
#### Требования к компонентам:
- В случае, если результат пришёл пустым или со статусом “Отмена” - необходимо вывести пользователю текст:
*"Вход был отменён/Operation was cancelled"*
- В случае, если данные пришли, то необходимо их отправить на сервер с запросом `api/<LOGIN>/open`, добавив данные из результата и получить ответ.
- Если сервер ответил успешно - то отображаем текст:
*"Успешно/Success"*
- Если сервер ответил любой ошибкой - то отображаем текст:
*"Что-то пошло не так/Something wrong"*
- Кнопка закрытия всегда открывает главный экран.
## 🛠 Решение
Необходимо загрузить свое решение в систему [по ссылке](https://innovationcampus.ru/lms/mod/quiz/view.php?id=2149).
Отметим, что работу необходимо осуществлять в представленных проектах-заготовках (шаблонах).
## ✅ Особенности оценивания
Оценивание происходит с помощью автоматической системы тестирования, которая в автоматическом режиме находит элементы и взаимодействует с ними (именно для этого у каждого элемента указан уникальный идентификатор, по которому будет производится поиск). Каждый тест происходит с чистой установки приложения.
В случае тестирования сервера на него поочередно отправляются команды, описанные в API и ожидаются определенные корректные ответы.
Сервер и приложение тестируются независимо.
Figma: https://www.figma.com/design/v9YlfUjxz6ChHS5mNWSQPN/TheDevs_Final?node-id=2-3&t=Hnk1mGCVo7joisAC-1

View File

@ -41,6 +41,10 @@ dependencies {
implementation ("com.squareup.retrofit2:converter-gson:2.9.0")
implementation ("com.squareup.okhttp3:okhttp:4.9.0")
implementation ("com.github.bumptech.glide:glide:4.15.1")
implementation("androidx.appcompat:appcompat:1.6.1")
implementation("com.google.android.material:material:1.10.0")
implementation("androidx.activity:activity:1.10.0")
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
kapt ("com.github.bumptech.glide:compiler:4.15.1")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0")

View File

@ -1,4 +1,4 @@
package ru.myitschool.work.ui.admin
package ru.myitschool.work.ui.Main
import android.os.Bundle
import android.view.View

View File

@ -0,0 +1,5 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#F0F0F0" />
<corners android:radius="16dp" />
</shape>

View File

@ -0,0 +1,55 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:gravity="center"
android:padding="16dp"
android:background="@android:color/white">
<EditText
android:id="@+id/username"
android:layout_width="300dp"
android:layout_height="wrap_content"
android:hint="@string/username_hint"
android:inputType="text"
android:padding="12dp"
android:background="@drawable/ic_android_black_24dp"
android:layout_marginBottom="16dp"/>
<EditText
android:id="@+id/password"
android:layout_width="300dp"
android:layout_height="wrap_content"
android:hint="@string/password_hint"
android:inputType="textPassword"
android:padding="12dp"
android:background="@drawable/ic_android_black_24dp"
android:layout_marginBottom="16dp"/>
<Button
android:id="@+id/login"
android:layout_width="300dp"
android:layout_height="wrap_content"
android:text="@string/login_button"
android:backgroundTint="@color/colorPrimary"
android:textColor="@android:color/white"
android:padding="12dp"
app:cornerRadius="16dp"
android:layout_marginBottom="16dp"/>
<TextView
android:id="@+id/error"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone"
android:textColor="@android:color/holo_red_light"
android:layout_marginBottom="16dp"/>
<ProgressBar
android:id="@+id/loading"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:visibility="gone" />
</LinearLayout>

View File

@ -0,0 +1,124 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="horizontal"
android:gravity="bottom"
android:padding="16dp"
android:background="@android:color/white">
<LinearLayout
android:layout_width="250dp"
android:layout_height="match_parent"
android:orientation="vertical">
<!-- Поле для ФИО -->
<TextView
android:id="@+id/fullname"
android:layout_width="100dp"
android:layout_height="wrap_content"
android:text="@string/fullname_label"
android:textSize="18sp"
android:layout_marginBottom="5dp"
android:visibility="gone" />
<!-- Фото пользователя. -->
<ImageView
android:id="@+id/photo"
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_gravity="center"
android:contentDescription="@string/photo_description"
android:layout_marginBottom="5dp"
android:visibility="gone" />
<!-- Поле для должности -->
<TextView
android:id="@+id/position"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/position_label"
android:layout_marginBottom="5dp"
android:visibility="gone" />
<!-- Поле для даты последнего входа -->
<TextView
android:id="@+id/lastEntry"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="2024-02-31 08:31"
android:layout_marginBottom="75dp"
android:visibility="gone" />
<!-- RecyclerView для списка проходов -->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:visibility="gone" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<!-- Кнопка обновления -->
<Button
android:id="@+id/refresh"
android:layout_width="280dp"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:text="@string/refresh"
app:cornerRadius="16dp"
android:backgroundTint="@color/colorPrimary"
android:textColor="@android:color/white"
android:padding="12dp"/>
<!-- Поле ошибки -->
<TextView
android:id="@+id/error"
android:layout_width="280dp"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:gravity="center"
android:text="@string/error_placeholder"
android:textColor="@android:color/holo_red_dark"
android:visibility="gone" />
<!-- Кнопки -->
<Button
android:id="@+id/scan"
android:layout_width="280dp"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:text="@string/scan_qr_code"
app:cornerRadius="16dp"
android:backgroundTint="@color/colorPrimary"
android:textColor="@android:color/white"
android:visibility="gone" />
<Button
android:id="@+id/logout"
android:layout_width="280dp"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:text="@string/logout"
app:cornerRadius="16dp"
android:backgroundTint="@color/colorPrimary"
android:textColor="@android:color/white"
android:visibility="gone" />
<Button
android:id="@+id/admin_panel"
android:layout_width="280dp"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:text="@string/admin_panel"
app:cornerRadius="16dp"
android:backgroundTint="@color/colorPrimary"
android:visibility="gone" />
</LinearLayout>
</LinearLayout>

View File

@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.camera.view.PreviewView
android:id="@+id/viewFinder"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ProgressBar
android:id="@+id/loading"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/close"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:contentDescription="@string/close_button"
android:src="@drawable/ic_close"
app:elevation="0dp"
android:backgroundTint="@color/colorPrimary"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/qrScanResultLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp"
android:gravity="center">
<TextView
android:id="@+id/result"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/result"
android:textSize="18sp"
android:gravity="center"
android:padding="16dp" />
<Button
android:id="@+id/close"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/close_button"
app:cornerRadius="16dp"
android:backgroundTint="@color/colorPrimary"
android:textColor="@android:color/white"
android:padding="12dp"
android:layout_marginTop="24dp" />
</LinearLayout>

View File

@ -1,13 +1,14 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Введите логин сотрудника:"
android:text="@string/enter_worker_username"
android:textSize="18sp"
android:layout_marginBottom="8dp" />
@ -15,14 +16,19 @@
android:id="@+id/employee_login"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Логин сотрудника"
android:hint="@string/worker_username"
android:padding="12dp"
android:background="@drawable/ic_android_black_24dp"
android:inputType="text" />
<Button
android:id="@+id/view_employee_info"
android:layout_width="match_parent"
android:layout_width="280dp"
android:layout_gravity="center_horizontal"
android:layout_height="wrap_content"
android:text="Просмотреть информацию о сотруднике"
android:text="@string/watch_info_about_worker"
android:backgroundTint="@color/colorPrimary"
app:cornerRadius="16dp"
android:layout_marginTop="16dp" />
<TextView
@ -37,7 +43,7 @@
android:id="@+id/toggle_access"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Блокировать/Разблокировать доступ"
android:text="@string/block_or_unblock"
android:layout_marginTop="16dp"
android:visibility="gone" />

View File

@ -11,10 +11,10 @@
android:id="@+id/username"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Введите логин"
android:hint="@string/username_hint"
android:inputType="text"
android:padding="12dp"
android:background="#F0F0F0"
android:background="@drawable/ic_android_black_24dp"
android:layout_marginBottom="16dp"/>
<EditText
@ -24,17 +24,18 @@
android:hint="Введите пароль"
android:inputType="textPassword"
android:padding="12dp"
android:background="#F0F0F0"
android:background="@drawable/ic_android_black_24dp"
android:layout_marginBottom="16dp"/>
<Button
android:id="@+id/login"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Войти"
android:text="@string/login_button"
android:backgroundTint="@color/colorPrimary"
android:textColor="@android:color/white"
android:padding="12dp"
app:cornerRadius="16dp"
android:layout_marginBottom="16dp"/>
<TextView

View File

@ -12,7 +12,7 @@
android:id="@+id/fullname"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Имя Фамилия"
android:text="@string/fullname_label"
android:textSize="18sp"
android:layout_marginBottom="5dp"
android:visibility="gone" />
@ -32,7 +32,7 @@
android:id="@+id/position"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Должность"
android:text="@string/position_label"
android:layout_marginBottom="5dp"
android:visibility="gone" />
@ -51,6 +51,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/refresh"
app:cornerRadius="16dp"
android:backgroundTint="@color/colorPrimary"
android:textColor="@android:color/white"
android:layout_marginBottom="12dp"
@ -71,7 +72,7 @@
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:visibility="visible" />
android:visibility="gone" />
<!-- Кнопки -->
<Button
@ -81,6 +82,7 @@
android:text="@string/scan_qr_code"
android:layout_marginBottom="12dp"
android:backgroundTint="@color/colorPrimary"
app:cornerRadius="16dp"
android:textColor="@android:color/white"
android:visibility="gone" />
@ -90,6 +92,7 @@
android:layout_height="wrap_content"
android:text="@string/logout"
android:layout_marginBottom="50dp"
app:cornerRadius="16dp"
android:backgroundTint="@color/colorPrimary"
android:textColor="@android:color/white"
android:visibility="gone" />
@ -98,7 +101,9 @@
android:id="@+id/admin_panel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Admin Panel"
android:text="@string/admin_panel"
app:cornerRadius="16dp"
android:backgroundTint="@color/colorPrimary"
android:layout_marginTop="16dp"
android:visibility="gone" />

View File

@ -30,6 +30,7 @@
android:contentDescription="@string/close_button"
android:src="@drawable/ic_close"
app:elevation="0dp"
android:backgroundTint="@color/colorPrimary"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/qrScanResultLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
@ -11,7 +12,7 @@
android:id="@+id/result"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Результат"
android:text="@string/result"
android:textSize="18sp"
android:gravity="center"
android:padding="16dp" />
@ -20,7 +21,10 @@
android:id="@+id/close"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Закрыть"
android:text="@string/close_button"
app:cornerRadius="16dp"
android:backgroundTint="@color/colorPrimary"
android:textColor="@android:color/white"
android:padding="12dp"
android:layout_marginTop="24dp" />

View File

@ -32,4 +32,12 @@
<string name="qr_scan_success">Successfully</string>
<string name="qr_scan_failure">Something went wrong.</string>
<string name="qr_scan_cancelled">Operation was cancelled</string>
<string name="close_button">Close</string>
<string name="result">Result</string>
<!-- Админ панель -->
<string name="enter_worker_username">Enter the employee\'s username:</string>
<string name="worker_username">Employee\'s login</string>
<string name="watch_info_about_worker">View employee information</string>
<string name="block_or_unblock">Block/Unblock access</string>
</resources>

View File

@ -27,13 +27,18 @@
<string name="position_label">Должность</string>
<string name="last_entry_label">Время последнего входа</string>
<string name="error_no_user_data">Нет данных о пользователе.</string>
<string name="admin_panel" translatable="false">Admin Panel</string>
<!-- Строки для экрана сканирования QR-кода -->
<string name="qr_scan_success">Успешно</string>
<string name="qr_scan_failure">Что-то пошло не так</string>
<string name="qr_scan_cancelled">Вход был отменён / Operation was cancelled</string>
<!-- TODO: Remove or change this placeholder text -->
<string name="hello_blank_fragment">Hello blank fragment</string>
<string name="close_button">Закрыть</string>
<string name="result">Результат</string>
<!-- Админ панель -->
<string name="enter_worker_username">Введите логин сотрудника:</string>
<string name="worker_username">Логин сотрудника</string>
<string name="watch_info_about_worker">Просмотреть информацию о сотруднике</string>
<string name="block_or_unblock">Блокировать/Разблокировать доступ</string>
</resources>

View File

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="close_button" translatable="false">Close</string>
</resources>