Commit d33f7701 by asranov0003

feat: post time limits

parent b3440087
...@@ -16,7 +16,11 @@ ...@@ -16,7 +16,11 @@
"selectDevice": "Select Device", "selectDevice": "Select Device",
"noData": "No data found", "noData": "No data found",
"downloadForAndroid": "Download for Android", "downloadForAndroid": "Download for Android",
"downloadForIos": "Download for iOS" "downloadForIos": "Download for iOS",
"pageInDevelopment": "Page in Development",
"pageInDevelopmentDesc": "This page is currently under development. Use the application to view recordings.",
"hours": "Hours",
"minutes": "Minutes"
}, },
"auth": { "auth": {
"entrance": "Sign In", "entrance": "Sign In",
...@@ -29,6 +33,7 @@ ...@@ -29,6 +33,7 @@
"forgetPassword": "Forgot password?", "forgetPassword": "Forgot password?",
"recover": "Recover", "recover": "Recover",
"passwordRecovery": "Password recovery", "passwordRecovery": "Password recovery",
"passwordChange": "Password change",
"newPassword": "New password", "newPassword": "New password",
"newPasswordPlaceholder": "Enter new password", "newPasswordPlaceholder": "Enter new password",
"repeatPassword": "Repeat password", "repeatPassword": "Repeat password",
...@@ -107,6 +112,8 @@ ...@@ -107,6 +112,8 @@
"account": "Account", "account": "Account",
"deleteAccount": "Delete account", "deleteAccount": "Delete account",
"recoverPassword": "Recover password", "recoverPassword": "Recover password",
"changePassword": "Change password",
"subscribeNow": "Subscribe now",
"logout": "Logout" "logout": "Logout"
}, },
"permissions": { "permissions": {
...@@ -144,12 +151,23 @@ ...@@ -144,12 +151,23 @@
}, },
"messengers": { "messengers": {
"title": "Chats", "title": "Chats",
"empty": "No chats found." "messages": "Messages",
"allChats": "All chats",
"empty": "No chats found.",
"from": "From",
"to": "To"
}, },
"usageLimits": { "usageLimits": {
"title": "Phone usage time limits", "title": "Phone usage time limits",
"desc": "Set a daily phone usage limit.", "desc": "Set a daily phone usage limit.",
"allowedApps": "Allowed apps will be still accessible." "allowedApps": "Allowed apps will be still accessible.",
"setLimit": "Set a limit",
"hoursRequired": "Hours are required",
"minHour": "Minimum hours is 0",
"maxHour": "Maximum hours is 23",
"minutesRequired": "Minutes are required",
"minMinutes": "Minimum minutes is 0",
"maxMinutes": "Maximum minutes is 59"
}, },
"pincode": { "pincode": {
"enterTitle": "Enter your PIN Code", "enterTitle": "Enter your PIN Code",
...@@ -180,7 +198,16 @@ ...@@ -180,7 +198,16 @@
"selectTariff": "Select a tariff", "selectTariff": "Select a tariff",
"selectPaymentMethod": "Select a payment method", "selectPaymentMethod": "Select a payment method",
"cancellPaymentTitle": "Payment can be cancelled within 24 hours", "cancellPaymentTitle": "Payment can be cancelled within 24 hours",
"pay": "Pay for a subscription" "pay": "Pay for a subscription",
"notActive": "Subscription is not active",
"notActiveDesc": "The functionality of the cabinet has been limited. To restore functionality, you need to subscribe.",
"toTariffs": "To tariffs"
},
"download": {
"android": "Download for Android",
"ios": "Download for iOS",
"rustore": "Download from RuStore",
"webApp": "TgApp - for parents' convenience"
}, },
"button": { "button": {
"login": "Login", "login": "Login",
......
...@@ -16,7 +16,11 @@ ...@@ -16,7 +16,11 @@
"selectDevice": "Выбрать устройство", "selectDevice": "Выбрать устройство",
"noData": "Данные не найдены", "noData": "Данные не найдены",
"downloadForAndroid": "Скачать для Android", "downloadForAndroid": "Скачать для Android",
"downloadForIos": "Скачать для iOS" "downloadForIos": "Скачать для iOS",
"pageInDevelopment": "Страница в разработке",
"pageInDevelopmentDesc": "Эта страница в настоящее время разрабатывается. Используйте приложение для просмотра записей.",
"hours": "Часы",
"minutes": "Минуты"
}, },
"auth": { "auth": {
"entrance": "Вход", "entrance": "Вход",
...@@ -29,6 +33,7 @@ ...@@ -29,6 +33,7 @@
"forgetPassword": "Забыли пароль?", "forgetPassword": "Забыли пароль?",
"recover": "Восстановить", "recover": "Восстановить",
"passwordRecovery": "Восстановление пароля", "passwordRecovery": "Восстановление пароля",
"passwordChange": "Смена пароля",
"newPassword": "Новый пароль", "newPassword": "Новый пароль",
"newPasswordPlaceholder": "Введите новый пароль", "newPasswordPlaceholder": "Введите новый пароль",
"repeatPassword": "Повторите пароль", "repeatPassword": "Повторите пароль",
...@@ -107,6 +112,8 @@ ...@@ -107,6 +112,8 @@
"account": "Аккаунт", "account": "Аккаунт",
"deleteAccount": "Удалить аккаунт", "deleteAccount": "Удалить аккаунт",
"recoverPassword": "Восстановить пароль", "recoverPassword": "Восстановить пароль",
"changePassword": "Сменить пароль",
"subscribeNow": "Оформить подписку",
"logout": "Выйти" "logout": "Выйти"
}, },
"permissions": { "permissions": {
...@@ -144,12 +151,23 @@ ...@@ -144,12 +151,23 @@
}, },
"messengers": { "messengers": {
"title": "Чаты", "title": "Чаты",
"empty": "Чаты не найдены." "messages": "Сообщения",
"allChats": "Все чаты",
"empty": "Чаты не найдены.",
"from": "От",
"to": "Кому"
}, },
"usageLimits": { "usageLimits": {
"title": "Ограничения времени использования телефона", "title": "Ограничения времени использования телефона",
"desc": "Установите дневное ограничение на использование телефона.", "desc": "Установите дневное ограничение на использование телефона.",
"allowedApps": "Разрешенные приложения будут по-прежнему доступны." "allowedApps": "Разрешенные приложения будут по-прежнему доступны.",
"setLimit": "Установить лимит",
"hoursRequired": "Часы обязательны",
"minHour": "Минимум часов - 0",
"maxHour": "Максимум часов - 23",
"minutesRequired": "Минуты обязательны",
"minMinutes": "Минимум минут - 0",
"maxMinutes": "Максимум минут - 59"
}, },
"pincode": { "pincode": {
"enterTitle": "Введите PIN-код", "enterTitle": "Введите PIN-код",
...@@ -180,7 +198,16 @@ ...@@ -180,7 +198,16 @@
"selectTariff": "Выберите тариф", "selectTariff": "Выберите тариф",
"selectPaymentMethod": "Выберите способ оплаты", "selectPaymentMethod": "Выберите способ оплаты",
"cancellPaymentTitle": "Платеж можно отменить в течении 24 часов", "cancellPaymentTitle": "Платеж можно отменить в течении 24 часов",
"pay": "Оплатить подписку" "pay": "Оплатить подписку",
"notActive": "Подписка не активна",
"notActiveDesc": "Функционал кабинета был ограничен. Для восстановления функционала необходимо оформить подписку.",
"toTariffs": "К тарифам"
},
"download": {
"android": "Скачать для Android",
"ios": "Скачать для iOS",
"rustore": "Скачать из RuStore",
"webApp": "TgApp - для удобства родителей"
}, },
"button": { "button": {
"login": "Войти", "login": "Войти",
......
...@@ -16,7 +16,11 @@ ...@@ -16,7 +16,11 @@
"selectDevice": "Qurilmani tanlang", "selectDevice": "Qurilmani tanlang",
"noData": "Hech qanday ma'lumot topilmadi", "noData": "Hech qanday ma'lumot topilmadi",
"downloadForAndroid": "Android uchun yuklab olish", "downloadForAndroid": "Android uchun yuklab olish",
"downloadForIos": "iOS uchun yuklab olish" "downloadForIos": "iOS uchun yuklab olish",
"pageInDevelopment": "Sahifa ishlab chiqilmoqda",
"pageInDevelopmentDesc": "Ushbu sahifa hozirda ishlab chiqilmoqda. Yozuvlarni ko'rish uchun ilovadan foydalaning.",
"hours": "Soatlar",
"minutes": "Daqiqalar"
}, },
"auth": { "auth": {
"entrance": "Kirish", "entrance": "Kirish",
...@@ -29,6 +33,7 @@ ...@@ -29,6 +33,7 @@
"forgetPassword": "Parolni unutdingizmi?", "forgetPassword": "Parolni unutdingizmi?",
"recover": "Tiklash", "recover": "Tiklash",
"passwordRecovery": "Parolni tiklash", "passwordRecovery": "Parolni tiklash",
"passwordChange": "Parolni o'zgartirish",
"newPassword": "Yangi parol", "newPassword": "Yangi parol",
"newPasswordPlaceholder": "Yangi parolni kiriting", "newPasswordPlaceholder": "Yangi parolni kiriting",
"repeatPassword": "Parolni takrorlang", "repeatPassword": "Parolni takrorlang",
...@@ -107,6 +112,8 @@ ...@@ -107,6 +112,8 @@
"account": "Hisob", "account": "Hisob",
"deleteAccount": "Hisobni o'chirish", "deleteAccount": "Hisobni o'chirish",
"recoverPassword": "Parolni tiklash", "recoverPassword": "Parolni tiklash",
"changePassword": "Parolni o'zgartirish",
"subscribeNow": "Obuna bo'lish",
"logout": "Chiqish" "logout": "Chiqish"
}, },
"permissions": { "permissions": {
...@@ -144,12 +151,23 @@ ...@@ -144,12 +151,23 @@
}, },
"messengers": { "messengers": {
"title": "Chats", "title": "Chats",
"empty": "Xabarlar topilmadi." "messages": "Xabarlar",
"allChats": "Barcha chatlar",
"empty": "Chatlar topilmadi.",
"from": "Kimdan",
"to": "Kimga"
}, },
"usageLimits": { "usageLimits": {
"title": "Telefon foydalanish vaqti chegaralari", "title": "Telefon foydalanish vaqti chegaralari",
"desc": "Kunlik telefon foydalanish cheklovini o'rnating.", "desc": "Kunlik telefon foydalanish cheklovini o'rnating.",
"allowedApps": "Ruxsat berilgan ilovalar hali ham foydalanishga ochiq bo'ladi." "allowedApps": "Ruxsat berilgan ilovalar hali ham foydalanishga ochiq bo'ladi.",
"setLimit": "Cheklov o'rnating",
"hoursRequired": "Soatlar talab qilinadi",
"minHour": "Minimal soatlar - 0",
"maxHour": "Maksimal soatlar - 23",
"minutesRequired": "Daqiqalar talab qilinadi",
"minMinutes": "Minimal daqiqalar - 0",
"maxMinutes": "Maksimal daqiqalar - 59"
}, },
"pincode": { "pincode": {
"enterTitle": "PIN kodni kiriting", "enterTitle": "PIN kodni kiriting",
...@@ -180,7 +198,16 @@ ...@@ -180,7 +198,16 @@
"selectTariff": "Tarifni tanlang", "selectTariff": "Tarifni tanlang",
"selectPaymentMethod": "To'lov usulini tanlang", "selectPaymentMethod": "To'lov usulini tanlang",
"cancellPaymentTitle": "To'lovni 24 soat ichida bekor qilish mumkin", "cancellPaymentTitle": "To'lovni 24 soat ichida bekor qilish mumkin",
"pay": "Obunani to'lash" "pay": "Obunani to'lash",
"notActive": "Obuna faol emas",
"notActiveDesc": "Shaxsiy kabinet funksiyalari cheklangan. Funktsiyalarni tiklash uchun obuna bo'lishingiz kerak.",
"toTariffs": "Tariflarga"
},
"download": {
"android": "Android uchun yuklab olish",
"ios": "iOS uchun yuklab olish",
"rustore": "RuStore dan yuklab olish",
"webApp": "TgApp - ota-onalar uchun qulaylik"
}, },
"button": { "button": {
"login": "Kirish", "login": "Kirish",
......
import React, { useEffect } from "react"; import React, { useEffect, useState } from "react";
import "./UsageLimits.css"; import "./UsageLimits.css";
import SectionHeader from "../../layouts/SectionHeader"; import SectionHeader from "../../layouts/SectionHeader";
import CButton from "../../components/CButton"; import CButton from "../../components/CButton";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useAppDispatch, type RootState } from "../../stores/store"; import { useAppDispatch, type RootState } from "../../stores/store";
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
import { fetchTimeLimits } from "../../stores/slices/usageLimitSlice"; import {
fetchTimeLimits,
postTimeLimits,
} from "../../stores/slices/usageLimitSlice";
import CLoading from "../../components/CLoading"; import CLoading from "../../components/CLoading";
import { useForm } from "react-hook-form";
import CInput from "../../components/CInput";
import CModal from "../../components/CModal";
type FormValues = {
hours: string;
minutes: string;
};
const UsageLimits: React.FC = () => { const UsageLimits: React.FC = () => {
const [isOpenModal, setIsOpenModal] = useState(false);
const [selectedDayIndex, setSelectedDayIndex] = useState<number | null>(null);
const { selectedDevice } = useSelector((state: RootState) => state.device); const { selectedDevice } = useSelector((state: RootState) => state.device);
const { days, isLoadingDays } = useSelector( const { days, isLoadingDays, loadingPostLimits } = useSelector(
(state: RootState) => state.usageLimit (state: RootState) => state.usageLimit
); );
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { t } = useTranslation(); const { t } = useTranslation();
const {
register,
setValue,
handleSubmit,
formState: { errors },
} = useForm<FormValues>({
defaultValues: {
hours: "",
minutes: "",
},
});
const onToggleModal = () => {
setIsOpenModal((prev) => !prev);
};
const openDayModal = (day: number, index: number) => {
setSelectedDayIndex(index);
const hrs = Math.floor(day / 60);
const mins = day % 60;
setValue("hours", String(hrs));
setValue("minutes", String(mins));
setIsOpenModal(true);
};
const onSubmit = async (data: FormValues) => {
if (selectedDayIndex === null || !selectedDevice?.id) return;
const hrs = parseInt(data.hours) || 0;
const mins = parseInt(data.minutes) || 0;
const totalMinutes = hrs * 60 + mins;
const updatedDays = [...days];
updatedDays[selectedDayIndex] = totalMinutes;
try {
await dispatch(
postTimeLimits({
deviceId: selectedDevice.id,
days: updatedDays,
})
).unwrap();
setIsOpenModal(false);
} catch (error) {
console.error("Error updating time limits: ", error);
}
};
useEffect(() => { useEffect(() => {
if (!selectedDevice?.id) return; if (!selectedDevice?.id) return;
dispatch(fetchTimeLimits(selectedDevice?.id)); dispatch(fetchTimeLimits(selectedDevice?.id));
}, [dispatch, selectedDevice]); }, [dispatch, selectedDevice]);
...@@ -58,7 +123,11 @@ const UsageLimits: React.FC = () => { ...@@ -58,7 +123,11 @@ const UsageLimits: React.FC = () => {
<div className="usagelimits__days"> <div className="usagelimits__days">
{days.map((day, index) => { {days.map((day, index) => {
return ( return (
<div className="usagelimits__day" key={index}> <div
className="usagelimits__day"
key={index}
onClick={() => openDayModal(day, index)}
>
<p>{weekDays[index]}</p> <p>{weekDays[index]}</p>
<p>{formatTime(day)}</p> <p>{formatTime(day)}</p>
</div> </div>
...@@ -67,9 +136,52 @@ const UsageLimits: React.FC = () => { ...@@ -67,9 +136,52 @@ const UsageLimits: React.FC = () => {
</div> </div>
)} )}
</div> </div>
</div>
<CModal
isOpen={isOpenModal}
onToggle={onToggleModal}
content={
<form className="modal__box" onSubmit={handleSubmit(onSubmit)}>
<h3 className="modal__box__title">Set a limit</h3>
<div className="modal__box__actions">
<CInput
label="Hours"
placeholder="0"
{...register("hours", {
required: "Hours are required",
min: { value: 0, message: "Minimum hours is 0" },
max: { value: 23, message: "Maximum hours is 23" },
})}
error={errors.hours?.message}
/>
<CInput
label="Minutes"
placeholder="0"
{...register("minutes", {
required: "Minutes are required",
min: { value: 0, message: "Minimum minutes is 0" },
max: { value: 59, message: "Maximum minutes is 59" },
})}
error={errors.minutes?.message}
/>
</div>
<div className="modal__box__actions">
<CButton title={t("button.cancel")} onClick={onToggleModal} />
<CButton title={t("button.save")} /> <CButton
title={t("button.confirm")}
type="submit"
variant="primary"
isLoading={loadingPostLimits}
/>
</div> </div>
</form>
}
/>
</div> </div>
); );
}; };
......
...@@ -4,12 +4,14 @@ import { sendRpcRequest } from "../../services/apiClient"; ...@@ -4,12 +4,14 @@ import { sendRpcRequest } from "../../services/apiClient";
interface IUsageLimitState { interface IUsageLimitState {
days: number[]; days: number[];
isLoadingDays: boolean; isLoadingDays: boolean;
loadingPostLimits: boolean;
errorDays: string | null; errorDays: string | null;
} }
const initialState: IUsageLimitState = { const initialState: IUsageLimitState = {
days: [], days: [],
isLoadingDays: false, isLoadingDays: false,
loadingPostLimits: false,
errorDays: null, errorDays: null,
}; };
...@@ -35,6 +37,31 @@ export const fetchTimeLimits = createAsyncThunk( ...@@ -35,6 +37,31 @@ export const fetchTimeLimits = createAsyncThunk(
} }
); );
export const postTimeLimits = createAsyncThunk(
"timeLimits/postTimeLimits",
async (
{ deviceId, days }: { deviceId: string; days: number[] },
{ rejectWithValue }
) => {
try {
const response = await sendRpcRequest<{ days: number[] }>(
"apps.timelimitset",
{
deviceId,
days,
}
);
return response.days;
} catch (error: unknown) {
if (typeof error === "object" && error !== null && "message" in error) {
return rejectWithValue(error.message);
}
return rejectWithValue("An unknown error occurred");
}
}
);
const usageLimitSlice = createSlice({ const usageLimitSlice = createSlice({
name: "usageLimit", name: "usageLimit",
initialState, initialState,
...@@ -52,6 +79,18 @@ const usageLimitSlice = createSlice({ ...@@ -52,6 +79,18 @@ const usageLimitSlice = createSlice({
.addCase(fetchTimeLimits.rejected, (state, action) => { .addCase(fetchTimeLimits.rejected, (state, action) => {
state.isLoadingDays = false; state.isLoadingDays = false;
state.errorDays = action.payload as string; state.errorDays = action.payload as string;
})
.addCase(postTimeLimits.pending, (state) => {
state.loadingPostLimits = true;
})
.addCase(postTimeLimits.fulfilled, (state, action) => {
state.loadingPostLimits = false;
state.days = action.payload;
})
.addCase(postTimeLimits.rejected, (state, action) => {
state.loadingPostLimits = false;
state.errorDays = action.payload as string;
}); });
}, },
}); });
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment