[BE][Scans] Differentiate between QR not found & errors. Convert scans without QR into PDF
This commit is contained in:
parent
88da49a052
commit
9fee445ef9
@ -1,3 +1,4 @@
|
|||||||
|
use crate::model::register::ScanData;
|
||||||
use crate::{json_result::JsonResult, model::register::Register};
|
use crate::{json_result::JsonResult, model::register::Register};
|
||||||
use rocket::{http::Status, serde::json::Json};
|
use rocket::{http::Status, serde::json::Json};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
@ -13,20 +14,26 @@ const SCAN_PATH: &str = "/srv/srv/shares/eegsac/ESCANEOS/";
|
|||||||
/// Represents the result of parsing an eegsac URL
|
/// Represents the result of parsing an eegsac URL
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub enum ScanInfo {
|
pub enum ScanInfo {
|
||||||
/// The url has both DNI & id
|
/// The url has both DNI & id.
|
||||||
|
/// The fields are: DNI, id, timestamp
|
||||||
Full(String, i32, String),
|
Full(String, i32, String),
|
||||||
/// The url only has a DNI
|
/// The url only has a DNI
|
||||||
|
/// The fields are: DNI, timestamp
|
||||||
Partial(String, String),
|
Partial(String, String),
|
||||||
/// Te url is invalid
|
/// There was an error getting the QR code.
|
||||||
|
/// The field is: timestamp
|
||||||
|
Empty(String),
|
||||||
|
/// The url is invalid
|
||||||
|
/// The field is: reason
|
||||||
Error(String),
|
Error(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/scans/detect")]
|
#[get("/scans/detect")]
|
||||||
pub async fn detect_scans() -> (Status, Json<JsonResult<Vec<(PathBuf, ScanInfo)>>>) {
|
pub async fn detect_scans() -> (Status, Json<JsonResult<Vec<ScanInfo>>>) {
|
||||||
let files = match get_valid_files() {
|
let files = match get_valid_files() {
|
||||||
Ok(f) => f,
|
Ok(f) => f,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
eprintln!("Error getting valid files: {:?}", err);
|
log::error!("Error getting valid files: {:?}", err);
|
||||||
return (Status::InternalServerError, JsonResult::err(err));
|
return (Status::InternalServerError, JsonResult::err(err));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -42,19 +49,8 @@ pub fn convert_scans_options() -> Status {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[post("/scans/convert", format = "json", data = "<data>")]
|
#[post("/scans/convert", format = "json", data = "<data>")]
|
||||||
pub async fn convert_scans(
|
pub async fn convert_scans(data: Json<Vec<ScanInfo>>) -> (Status, Json<JsonResult<()>>) {
|
||||||
data: Json<JsonResult<Vec<(PathBuf, ScanInfo)>>>,
|
|
||||||
) -> (Status, Json<JsonResult<()>>) {
|
|
||||||
let data = data.into_inner();
|
let data = data.into_inner();
|
||||||
let data = match data {
|
|
||||||
JsonResult::Ok(v) => v,
|
|
||||||
JsonResult::Error(err) => {
|
|
||||||
return (
|
|
||||||
Status::BadRequest,
|
|
||||||
JsonResult::err("Se recibió un error.".into()),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
match convert_scans_from_data(&data).await {
|
match convert_scans_from_data(&data).await {
|
||||||
Ok(_) => (Status::Ok, JsonResult::ok(())),
|
Ok(_) => (Status::Ok, JsonResult::ok(())),
|
||||||
@ -68,32 +64,18 @@ fn get_valid_files() -> Result<Vec<PathBuf>, String> {
|
|||||||
let paths = match fs::read_dir(SCAN_PATH) {
|
let paths = match fs::read_dir(SCAN_PATH) {
|
||||||
Ok(paths) => paths,
|
Ok(paths) => paths,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
eprintln!("Error reading dir: {:?}", err);
|
log::error!("Error reading dir: {:?}", err);
|
||||||
return Err("Error leyendo directorio".into());
|
return Err("Error leyendo directorio".into());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut result = Vec::<PathBuf>::new();
|
let mut result = Vec::<PathBuf>::new();
|
||||||
|
|
||||||
/*
|
|
||||||
TODO: The system will detect info from an image.
|
|
||||||
If successful, that image will be renamed to:
|
|
||||||
eeg_<unix_timestamp>.jpg
|
|
||||||
and the timestamp will be sent to the frontend
|
|
||||||
along with the detected info.
|
|
||||||
|
|
||||||
Then the frontend will send back the list of timestamps & info
|
|
||||||
and the backend will do the conversion.
|
|
||||||
|
|
||||||
This way, we can ensure that only the images sent to & from the FE
|
|
||||||
can be converted.
|
|
||||||
*/
|
|
||||||
|
|
||||||
for p in paths {
|
for p in paths {
|
||||||
let p = match p {
|
let p = match p {
|
||||||
Ok(p) => p,
|
Ok(p) => p,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
eprintln!("Error reading path: {:?}", err);
|
log::error!("Error reading path: {:?}", err);
|
||||||
return Err("Error recuperando archivo de lista de archivos".into());
|
return Err("Error recuperando archivo de lista de archivos".into());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -107,7 +89,7 @@ fn get_valid_files() -> Result<Vec<PathBuf>, String> {
|
|||||||
let filename = match file_path.file_name() {
|
let filename = match file_path.file_name() {
|
||||||
Some(filename) => filename,
|
Some(filename) => filename,
|
||||||
None => {
|
None => {
|
||||||
eprintln!("Error getting file name: {:?}", file_path);
|
log::error!("Error getting file name: {:?}", file_path);
|
||||||
return Err("Error leyendo archivo en directorio".into());
|
return Err("Error leyendo archivo en directorio".into());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -115,12 +97,12 @@ fn get_valid_files() -> Result<Vec<PathBuf>, String> {
|
|||||||
let filename = match filename.to_str() {
|
let filename = match filename.to_str() {
|
||||||
Some(p) => p,
|
Some(p) => p,
|
||||||
None => {
|
None => {
|
||||||
eprintln!("Error getting file as string: {:?}", p);
|
log::error!("Error getting file as string: {:?}", p);
|
||||||
return Err("Error leyendo nombre de archivo de escaneo".into());
|
return Err("Error leyendo nombre de archivo de escaneo".into());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if filename.ends_with(".jpg") && filename.starts_with("eeg") {
|
if filename.starts_with("eeg") && filename.ends_with(".jpg") {
|
||||||
result.push(file_path);
|
result.push(file_path);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -129,11 +111,8 @@ fn get_valid_files() -> Result<Vec<PathBuf>, String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Detects the QR code data from every file
|
/// Detects the QR code data from every file
|
||||||
fn get_details_from_paths(paths: Vec<PathBuf>) -> Vec<(PathBuf, ScanInfo)> {
|
fn get_details_from_paths(paths: Vec<PathBuf>) -> Vec<ScanInfo> {
|
||||||
paths
|
paths.into_iter().map(|path| get_image_info(path)).collect()
|
||||||
.into_iter()
|
|
||||||
.map(|path| (path.clone(), get_image_info(path)))
|
|
||||||
.collect()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_image_info(path: PathBuf) -> ScanInfo {
|
fn get_image_info(path: PathBuf) -> ScanInfo {
|
||||||
@ -144,7 +123,7 @@ fn get_image_info(path: PathBuf) -> ScanInfo {
|
|||||||
let current_time = match current_time {
|
let current_time = match current_time {
|
||||||
Ok(t) => t,
|
Ok(t) => t,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
eprintln!("Error obteniendo hora actual: {:?}", err);
|
log::error!("Error obteniendo hora actual: {:?}", err);
|
||||||
return ScanInfo::Error("Error obteniendo hora actual: TIME WENT BACKWARDS.".into());
|
return ScanInfo::Error("Error obteniendo hora actual: TIME WENT BACKWARDS.".into());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -161,21 +140,35 @@ fn get_image_info(path: PathBuf) -> ScanInfo {
|
|||||||
// crop image
|
// crop image
|
||||||
let cropped_img = img.crop_imm(0, third_point_height, mid_point_width, remaining_height);
|
let cropped_img = img.crop_imm(0, third_point_height, mid_point_width, remaining_height);
|
||||||
|
|
||||||
// TODO: threshold image before getting qr
|
//
|
||||||
|
// TODO: threshold cropped image before getting qr
|
||||||
|
//
|
||||||
|
|
||||||
// get qr from cropped image
|
// get qr from cropped image
|
||||||
let results = bardecoder::default_decoder().decode(&cropped_img);
|
let results = bardecoder::default_decoder().decode(&cropped_img);
|
||||||
|
|
||||||
if results.is_empty() {
|
if results.is_empty() {
|
||||||
eprintln!("QR not found");
|
log::info!("QR not found");
|
||||||
return ScanInfo::Error("QR no encontrado.".into());
|
|
||||||
|
// Rename file
|
||||||
|
let mut new_path = path.clone();
|
||||||
|
new_path.set_file_name(format!("eeg_{}.jpg", current_ms));
|
||||||
|
|
||||||
|
return match fs::rename(path, new_path) {
|
||||||
|
Ok(_) => ScanInfo::Empty(current_ms.to_string()),
|
||||||
|
Err(err) => {
|
||||||
|
log::error!("Error renombrando archivo: {:?}", err);
|
||||||
|
|
||||||
|
ScanInfo::Error("Error renombrando archivo.".into())
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let url = match &results[0] {
|
let url = match &results[0] {
|
||||||
Ok(url) => url,
|
Ok(url) => url,
|
||||||
Err(reason) => {
|
Err(reason) => {
|
||||||
eprintln!("Error decoding qr: {:?}", reason);
|
log::error!("Error decoding qr: {:?}", reason);
|
||||||
return ScanInfo::Error("QR no encontrado.".into());
|
return ScanInfo::Error("Error recuperando QR.".into());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -206,7 +199,7 @@ fn get_image_info(path: PathBuf) -> ScanInfo {
|
|||||||
match fs::rename(path, new_path) {
|
match fs::rename(path, new_path) {
|
||||||
Ok(_) => ScanInfo::Full(dni, v, current_ms.to_string()),
|
Ok(_) => ScanInfo::Full(dni, v, current_ms.to_string()),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
eprintln!("Error renombrando archivo: {:?}", err);
|
log::error!("Error renombrando archivo: {:?}", err);
|
||||||
|
|
||||||
ScanInfo::Error("Error renombrando archivo.".into())
|
ScanInfo::Error("Error renombrando archivo.".into())
|
||||||
}
|
}
|
||||||
@ -223,7 +216,7 @@ fn get_image_info(path: PathBuf) -> ScanInfo {
|
|||||||
match fs::rename(path, new_path) {
|
match fs::rename(path, new_path) {
|
||||||
Ok(_) => ScanInfo::Partial(url.chars().skip(31).collect(), current_ms.to_string()),
|
Ok(_) => ScanInfo::Partial(url.chars().skip(31).collect(), current_ms.to_string()),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
eprintln!("Error renombrando archivo: {:?}", err);
|
log::error!("Error renombrando archivo: {:?}", err);
|
||||||
|
|
||||||
ScanInfo::Error("Error renombrando archivo.".into())
|
ScanInfo::Error("Error renombrando archivo.".into())
|
||||||
}
|
}
|
||||||
@ -233,10 +226,10 @@ fn get_image_info(path: PathBuf) -> ScanInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO: this should return a list of files that succeeded & failed
|
// TODO: this should return a list of files that succeeded & failed
|
||||||
async fn convert_scans_from_data(data: &Vec<(PathBuf, ScanInfo)>) -> Result<(), String> {
|
async fn convert_scans_from_data(data: &Vec<ScanInfo>) -> Result<(), String> {
|
||||||
let (ids, dnis) = data.iter().fold(
|
let (ids, dnis) = data.iter().fold(
|
||||||
(vec!["-1".to_string()], vec!["''".to_string()]),
|
(vec!["-1".to_string()], vec!["''".to_string()]),
|
||||||
|mut acc, (_, next)| {
|
|mut acc, next| {
|
||||||
match next {
|
match next {
|
||||||
ScanInfo::Full(_, id, _) => acc.0.push(id.to_string()),
|
ScanInfo::Full(_, id, _) => acc.0.push(id.to_string()),
|
||||||
ScanInfo::Partial(dni, _) => acc.1.push(format!("'{}'", dni)),
|
ScanInfo::Partial(dni, _) => acc.1.push(format!("'{}'", dni)),
|
||||||
@ -258,22 +251,21 @@ async fn convert_scans_from_data(data: &Vec<(PathBuf, ScanInfo)>) -> Result<(),
|
|||||||
let persons_info = match persons_info {
|
let persons_info = match persons_info {
|
||||||
Ok(v) => v,
|
Ok(v) => v,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
eprintln!("Error getting persons info: {:?}", err);
|
log::error!("Error getting persons info: {:?}", err);
|
||||||
return Err("Error obteniendo información de personas.".into());
|
return Err("Error obteniendo información de personas.".into());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Match the results from DB with the data from the frontend,
|
// Match the results from DB with the data from the frontend,
|
||||||
// & convert the files to PDFs
|
// & convert the files to PDFs
|
||||||
|
for info in data {
|
||||||
for (_, info) in data {
|
|
||||||
let (person_info, timestamp) = match info {
|
let (person_info, timestamp) = match info {
|
||||||
ScanInfo::Full(dni, id, timestamp) => {
|
ScanInfo::Full(dni, id, timestamp) => {
|
||||||
let person_info = persons_info.iter().find(|e| e.register_id == *id);
|
let person_info = persons_info.iter().find(|e| e.register_id == *id);
|
||||||
match person_info {
|
match person_info {
|
||||||
Some(p) => (p, timestamp),
|
Some(p) => (p.clone(), timestamp),
|
||||||
None => {
|
None => {
|
||||||
log::error!("Register not found in data from DB: {} {}", id, dni);
|
log::error!("Register not found in data from DB: id {}, dni {}", id, dni);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -283,13 +275,26 @@ async fn convert_scans_from_data(data: &Vec<(PathBuf, ScanInfo)>) -> Result<(),
|
|||||||
.iter()
|
.iter()
|
||||||
.find(|e| e.person_dni == *dni && e.register_id == 0);
|
.find(|e| e.person_dni == *dni && e.register_id == 0);
|
||||||
match person_info {
|
match person_info {
|
||||||
Some(p) => (p, timestamp),
|
Some(p) => (p.clone(), timestamp),
|
||||||
None => {
|
None => {
|
||||||
log::error!("Register not found in data from DB: {}", dni);
|
log::error!("Register not found in data from DB: dni {}", dni);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
ScanInfo::Empty(timestamp) => {
|
||||||
|
// This is dummy data, to convert files without QR
|
||||||
|
let scan_data = ScanData {
|
||||||
|
course_name: "pendiente".into(),
|
||||||
|
person_names: "EEG".into(),
|
||||||
|
person_paternal_surname: "".into(),
|
||||||
|
person_maternal_surname: "".into(),
|
||||||
|
person_dni: "".into(),
|
||||||
|
register_id: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
(scan_data, timestamp)
|
||||||
|
}
|
||||||
ScanInfo::Error(reason) => {
|
ScanInfo::Error(reason) => {
|
||||||
log::info!("Found an error while matching data with DB: {}", reason);
|
log::info!("Found an error while matching data with DB: {}", reason);
|
||||||
continue;
|
continue;
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { For, Show, createMemo } from "solid-js";
|
import { Show, createMemo } from "solid-js";
|
||||||
import { ScanData, ScanResult } from ".";
|
import { ScanResult } from ".";
|
||||||
import { FilledButton } from "../components/FilledButton";
|
import { FilledButton } from "../components/FilledButton";
|
||||||
import { FilledCard } from "../components/FilledCard";
|
import { FilledCard } from "../components/FilledCard";
|
||||||
import { LoadingIcon } from "../icons/LoadingIcon";
|
import { LoadingIcon } from "../icons/LoadingIcon";
|
||||||
@ -7,33 +7,35 @@ import { PDFIcon } from "../icons/PDFIcon";
|
|||||||
import { LoadingStatus, useLoading } from "../utils/functions";
|
import { LoadingStatus, useLoading } from "../utils/functions";
|
||||||
import { QuestionIcon } from "../icons/QuestionIcon";
|
import { QuestionIcon } from "../icons/QuestionIcon";
|
||||||
|
|
||||||
export function ScansList(props: {scanData: ScanData | null}) {
|
export function ScansList(props: {scanData: Array<ScanResult> | null}) {
|
||||||
const {status, setStatus, error, setError} = useLoading();
|
const {status, setStatus, error, setError} = useLoading();
|
||||||
|
|
||||||
const loading = createMemo(() => status() === LoadingStatus.Loading);
|
const loading = createMemo(() => status() === LoadingStatus.Loading);
|
||||||
|
|
||||||
const empty = createMemo(() => props.scanData?.Ok.length === 0);
|
const empty = createMemo(() => props.scanData?.length === 0);
|
||||||
|
|
||||||
const entriesCount = createMemo(() => {
|
const entriesCount = createMemo(() => {
|
||||||
if (props.scanData === null) return [0, 0];
|
if (props.scanData === null) return [0, 0];
|
||||||
|
|
||||||
let fullCount = 0;
|
let fullCount = 0;
|
||||||
let partialCount = 0;
|
let partialCount = 0;
|
||||||
let invalidCount = 0;
|
let emptyCount = 0;
|
||||||
|
|
||||||
for (const [, result] of props.scanData.Ok) {
|
for (const entry of props.scanData) {
|
||||||
if ("Full" in result) {
|
if ("Full" in entry) {
|
||||||
fullCount += 1;
|
fullCount += 1;
|
||||||
} else if ("Partial" in result) {
|
} else if ("Partial" in entry) {
|
||||||
partialCount += 1;
|
partialCount += 1;
|
||||||
} else {
|
} else if ("Empty" in entry) {
|
||||||
invalidCount += 1;
|
emptyCount += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return [fullCount, partialCount, invalidCount];
|
return [fullCount, partialCount, emptyCount];
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const erroredEntries = createMemo(() => (props.scanData?.filter((entry) => "Error" in entry) ?? []) as Array<{Error: string}>);
|
||||||
|
|
||||||
const convertScans = () => {
|
const convertScans = () => {
|
||||||
setStatus(LoadingStatus.Loading);
|
setStatus(LoadingStatus.Loading);
|
||||||
|
|
||||||
@ -43,7 +45,6 @@ export function ScansList(props: {scanData: ScanData | null}) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fetch(
|
fetch(
|
||||||
`${import.meta.env.VITE_BACKEND_URL}/api/scans/convert`,
|
`${import.meta.env.VITE_BACKEND_URL}/api/scans/convert`,
|
||||||
{
|
{
|
||||||
@ -100,6 +101,20 @@ export function ScansList(props: {scanData: ScanData | null}) {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Show when={erroredEntries().length > 0}>
|
||||||
|
<div class="px-4 pt-2">
|
||||||
|
<p class="font-medium">
|
||||||
|
Errores:
|
||||||
|
</p>
|
||||||
|
<ul class="list-disc list-inside">
|
||||||
|
{erroredEntries().map((entry) => (
|
||||||
|
<li>
|
||||||
|
{entry.Error}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -146,13 +161,3 @@ export function ScansList(props: {scanData: ScanData | null}) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function dataFromScanResult(result: ScanResult) {
|
|
||||||
if ("Full" in result) {
|
|
||||||
return `DNI: ${result.Full[0]}, iid: ${result.Full[1]}`;
|
|
||||||
} else if ("Partial" in result) {
|
|
||||||
return `DNI: ${result.Partial[0]}`;
|
|
||||||
} else if ("Error" in result) {
|
|
||||||
return `Error: ${result.Error}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -11,19 +11,20 @@ import { ScansList } from "./ScansList";
|
|||||||
* Serialization & Deserialization is done by the backend.
|
* Serialization & Deserialization is done by the backend.
|
||||||
*/
|
*/
|
||||||
export interface ScanData {
|
export interface ScanData {
|
||||||
Ok: Array<[string, ScanResult]>,
|
Ok: Array<ScanResult>,
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ScanResult =
|
export type ScanResult =
|
||||||
| {Full: [string, number, string]}
|
| {Full: [string, number, string]}
|
||||||
| {Partial: [string, string]}
|
| {Partial: [string, string]}
|
||||||
|
| {Empty: string}
|
||||||
| {Error: string};
|
| {Error: string};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export function Scans() {
|
export function Scans() {
|
||||||
const {status, setStatus, error, setError} = useLoading();
|
const {status, setStatus, error, setError} = useLoading();
|
||||||
const [scanData, setScanData] = createSignal<ScanData | null>(null);
|
const [scanData, setScanData] = createSignal<Array<ScanResult> | null>(null);
|
||||||
|
|
||||||
const loading = createMemo(() => status() === LoadingStatus.Loading);
|
const loading = createMemo(() => status() === LoadingStatus.Loading);
|
||||||
|
|
||||||
@ -35,11 +36,12 @@ export function Scans() {
|
|||||||
.then((res) => res.json())
|
.then((res) => res.json())
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
setStatus(LoadingStatus.Ok);
|
setStatus(LoadingStatus.Ok);
|
||||||
setScanData(data);
|
setScanData(data.Ok);
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
setStatus(LoadingStatus.Error);
|
setStatus(LoadingStatus.Error);
|
||||||
console.error(err);
|
console.error(err);
|
||||||
|
setError(err.Error);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user