[BE] Detect QR, parse params & get data from DB
This commit is contained in:
parent
373ef5c74e
commit
605ee27b4f
@ -1,19 +1,20 @@
|
|||||||
use crate::json_result::JsonResult;
|
use crate::{json_result::JsonResult, model::register::Register};
|
||||||
use printpdf::{Mm, PdfDocument};
|
use printpdf::{Mm, PdfDocument};
|
||||||
use rocket::{http::Status, serde::json::Json};
|
use rocket::{http::Status, serde::json::Json};
|
||||||
use serde::Serialize;
|
use serde::{Deserialize, Serialize};
|
||||||
use std::{
|
use std::{
|
||||||
f32::consts::E,
|
f32::consts::E,
|
||||||
fs::{self, File},
|
fs::{self, File},
|
||||||
io::BufWriter,
|
io::BufWriter,
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
|
result,
|
||||||
time::{SystemTime, UNIX_EPOCH},
|
time::{SystemTime, UNIX_EPOCH},
|
||||||
};
|
};
|
||||||
|
|
||||||
const SCAN_PATH: &str = "/srv/srv/shares/eegsac/ESCANEOS/";
|
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)]
|
#[derive(Serialize, Deserialize)]
|
||||||
enum ScanInfo {
|
enum ScanInfo {
|
||||||
/// The url has both DNI & id
|
/// The url has both DNI & id
|
||||||
Full(String, i32, String),
|
Full(String, i32, String),
|
||||||
@ -38,6 +39,32 @@ pub async fn detect_scans() -> (Status, Json<JsonResult<Vec<(PathBuf, ScanInfo)>
|
|||||||
(Status::Ok, JsonResult::ok(file_details))
|
(Status::Ok, JsonResult::ok(file_details))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[options("/scans/convert")]
|
||||||
|
pub fn convert_scans_options() -> Status {
|
||||||
|
Status::Ok
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/scans/convert", format = "json", data = "<data>")]
|
||||||
|
pub async fn convert_scans(
|
||||||
|
data: Json<JsonResult<Vec<(PathBuf, ScanInfo)>>>,
|
||||||
|
) -> (Status, Json<JsonResult<()>>) {
|
||||||
|
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 {
|
||||||
|
Ok(_) => (Status::Ok, JsonResult::ok(())),
|
||||||
|
Err(reason) => (Status::InternalServerError, JsonResult::err(reason)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Reads the SCAN_PATH directory and returns a list of file paths
|
/// Reads the SCAN_PATH directory and returns a list of file paths
|
||||||
/// that are valid for processing.
|
/// that are valid for processing.
|
||||||
fn get_valid_files() -> Result<Vec<PathBuf>, String> {
|
fn get_valid_files() -> Result<Vec<PathBuf>, String> {
|
||||||
@ -131,7 +158,7 @@ fn get_image_info(path: PathBuf) -> ScanInfo {
|
|||||||
let width = img.width();
|
let width = img.width();
|
||||||
let height = img.height();
|
let height = img.height();
|
||||||
|
|
||||||
let third_point_height = (height / 3) * 2;
|
let third_point_height = (height / 4) * 3;
|
||||||
let mid_point_width = width / 2;
|
let mid_point_width = width / 2;
|
||||||
let remaining_height = height - third_point_height;
|
let remaining_height = height - third_point_height;
|
||||||
|
|
||||||
@ -141,6 +168,11 @@ fn get_image_info(path: PathBuf) -> ScanInfo {
|
|||||||
// 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() {
|
||||||
|
eprintln!("QR not found");
|
||||||
|
return ScanInfo::Error("QR no encontrado.".into());
|
||||||
|
}
|
||||||
|
|
||||||
let url = match &results[0] {
|
let url = match &results[0] {
|
||||||
Ok(url) => url,
|
Ok(url) => url,
|
||||||
Err(reason) => {
|
Err(reason) => {
|
||||||
@ -201,3 +233,41 @@ fn get_image_info(path: PathBuf) -> ScanInfo {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: this should return a list of files that succeeded & failed
|
||||||
|
async fn convert_scans_from_data(data: &Vec<(PathBuf, ScanInfo)>) -> Result<(), String> {
|
||||||
|
let (ids, dnis) = data.iter().fold(
|
||||||
|
(vec!["-1".to_string()], vec!["''".to_string()]),
|
||||||
|
|mut acc, (_, next)| {
|
||||||
|
match next {
|
||||||
|
ScanInfo::Full(_, id, _) => acc.0.push(id.to_string()),
|
||||||
|
ScanInfo::Partial(dni, _) => acc.1.push(format!("'{}'", dni)),
|
||||||
|
_ => (),
|
||||||
|
};
|
||||||
|
|
||||||
|
acc
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
let register_id_list = ids.join(",");
|
||||||
|
let person_dni_list = dnis.join("-");
|
||||||
|
|
||||||
|
log::info!("register_id_list: {}", register_id_list);
|
||||||
|
log::info!("person_dni_list: {}", person_dni_list);
|
||||||
|
|
||||||
|
let persons_info = Register::get_course_and_person(register_id_list, person_dni_list).await;
|
||||||
|
|
||||||
|
let persons_info = match persons_info {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(err) => {
|
||||||
|
eprintln!("Error getting persons info: {:?}", err);
|
||||||
|
return Err("Error obteniendo información de personas.".into());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
for e in persons_info {
|
||||||
|
println!("\n{:?}", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
@ -72,6 +72,8 @@ async fn rocket() -> _ {
|
|||||||
// Scans routes
|
// Scans routes
|
||||||
//
|
//
|
||||||
controller::scans::detect_scans,
|
controller::scans::detect_scans,
|
||||||
|
controller::scans::convert_scans_options,
|
||||||
|
controller::scans::convert_scans,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,14 +1,24 @@
|
|||||||
use rocket::form::validate::Contains;
|
use rocket::form::validate::Contains;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use sqlx::{Executor, Row};
|
||||||
|
|
||||||
use crate::db;
|
use crate::db;
|
||||||
|
|
||||||
use super::{course::Course, custom_label::CustomLabel};
|
use super::{course::Course, custom_label::CustomLabel};
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
#[serde(crate = "rocket::serde")]
|
pub struct ScanData {
|
||||||
|
pub course_name: String,
|
||||||
|
pub person_names: String,
|
||||||
|
pub person_paternal_surname: String,
|
||||||
|
pub person_maternal_surname: String,
|
||||||
|
pub person_dni: String,
|
||||||
|
pub register_id: i32,
|
||||||
|
}
|
||||||
|
|
||||||
/// Represents a single register send by the frontend
|
/// Represents a single register send by the frontend
|
||||||
/// to create a new register in the database
|
/// to create a new register in the database
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
pub struct RegisterCreate {
|
pub struct RegisterCreate {
|
||||||
person_id: i32,
|
person_id: i32,
|
||||||
course_id: i32,
|
course_id: i32,
|
||||||
@ -161,4 +171,58 @@ impl Register {
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn get_course_and_person(
|
||||||
|
register_id_list: String,
|
||||||
|
person_dni_list: String,
|
||||||
|
) -> Result<Vec<ScanData>, sqlx::Error> {
|
||||||
|
let db = db();
|
||||||
|
|
||||||
|
let sql = format!(
|
||||||
|
"
|
||||||
|
select
|
||||||
|
course.course_name,
|
||||||
|
register.register_id,
|
||||||
|
person.person_dni,
|
||||||
|
person.person_names,
|
||||||
|
person.person_paternal_surname,
|
||||||
|
person.person_maternal_surname
|
||||||
|
from register
|
||||||
|
inner join course on course.course_id = register.register_course_id
|
||||||
|
inner join person on person.person_id = register.register_person_id
|
||||||
|
where register_id IN ({register_id_list})
|
||||||
|
union
|
||||||
|
select
|
||||||
|
'' as 'course_name',
|
||||||
|
0 as register_id,
|
||||||
|
'' as person_dni,
|
||||||
|
person.person_names,
|
||||||
|
person.person_paternal_surname,
|
||||||
|
person.person_maternal_surname
|
||||||
|
from person
|
||||||
|
where person_dni IN ({person_dni_list})
|
||||||
|
"
|
||||||
|
);
|
||||||
|
|
||||||
|
log::info!("sql: {}", sql);
|
||||||
|
|
||||||
|
let result = db.fetch_all(sql.as_str()).await?;
|
||||||
|
|
||||||
|
log::info!("rows: {}", result.len());
|
||||||
|
|
||||||
|
let data = result
|
||||||
|
.iter()
|
||||||
|
.map(|row| ScanData {
|
||||||
|
course_name: row.try_get("course_name").unwrap(),
|
||||||
|
person_names: row.try_get("person_names").unwrap(),
|
||||||
|
register_id: row.try_get("register_id").unwrap(),
|
||||||
|
person_dni: row.try_get("person_dni").unwrap(),
|
||||||
|
person_paternal_surname: row.try_get("person_paternal_surname").unwrap(),
|
||||||
|
person_maternal_surname: row.try_get("person_maternal_surname").unwrap(),
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Ok(data)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,6 +4,7 @@ import { FilledCard } from "../components/FilledCard";
|
|||||||
import { LoadingIcon } from "../icons/LoadingIcon";
|
import { LoadingIcon } from "../icons/LoadingIcon";
|
||||||
import { MagnifyingGlassIcon } from "../icons/MagnifyingGlassIcon";
|
import { MagnifyingGlassIcon } from "../icons/MagnifyingGlassIcon";
|
||||||
import { LoadingStatus, useLoading } from "../utils/functions";
|
import { LoadingStatus, useLoading } from "../utils/functions";
|
||||||
|
import { PDFIcon } from "../icons/PDFIcon";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents the data about the scans that were detected.
|
* Represents the data about the scans that were detected.
|
||||||
@ -36,6 +37,7 @@ export function Scans() {
|
|||||||
|
|
||||||
const detectScans = () => {
|
const detectScans = () => {
|
||||||
setStatus(LoadingStatus.Loading);
|
setStatus(LoadingStatus.Loading);
|
||||||
|
setScanData(null);
|
||||||
|
|
||||||
fetch(`${import.meta.env.VITE_BACKEND_URL}/api/scans/detect`)
|
fetch(`${import.meta.env.VITE_BACKEND_URL}/api/scans/detect`)
|
||||||
.then((res) => res.json())
|
.then((res) => res.json())
|
||||||
@ -49,6 +51,37 @@ export function Scans() {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const convertScans = () => {
|
||||||
|
setStatus(LoadingStatus.Loading);
|
||||||
|
|
||||||
|
if (scanData() === null) {
|
||||||
|
setError("No se detectaron escaneos");
|
||||||
|
setStatus(LoadingStatus.Error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fetch(
|
||||||
|
`${import.meta.env.VITE_BACKEND_URL}/api/scans/convert`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(scanData()),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.then((res) => res.json())
|
||||||
|
.then((res) => {
|
||||||
|
setStatus(LoadingStatus.Ok);
|
||||||
|
console.log(res);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
setStatus(LoadingStatus.Error);
|
||||||
|
console.error(err);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="grid grid-cols-[25rem_25rem_auto]">
|
<div class="grid grid-cols-[25rem_25rem_auto]">
|
||||||
<div>
|
<div>
|
||||||
@ -126,6 +159,7 @@ export function Scans() {
|
|||||||
<div class="relative">
|
<div class="relative">
|
||||||
<FilledButton
|
<FilledButton
|
||||||
onClick={detectScans}
|
onClick={detectScans}
|
||||||
|
disabled={loading()}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="absolute top-[1.35rem] left-2"
|
class="absolute top-[1.35rem] left-2"
|
||||||
@ -174,11 +208,43 @@ export function Scans() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p class="p-4">
|
<p class="p-4">
|
||||||
Al convertir los escaneos a PDF, se eliminan los archivos JPG.
|
- Al convertir los escaneos a PDF se eliminan los archivos JPG.
|
||||||
<br />
|
<br />
|
||||||
La lista de escaneos a convertir puede haber cambiado desde
|
- Solo se convertiran los escaneos mostrados en esta lista.
|
||||||
la última vez que se realizó la detección.
|
<br />
|
||||||
|
- Si ejecutas la detección de escaneos nuevamente los nombres de
|
||||||
|
los archivos cambiarán. Esto es normal. El contenido de los JPG
|
||||||
|
es el mismo.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<div class="relative mx-4 mb-4">
|
||||||
|
<FilledButton
|
||||||
|
onClick={convertScans}
|
||||||
|
disabled={loading()}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="absolute top-[1.35rem] left-2"
|
||||||
|
style={{display: loading() ? "none" : "inline-block"}}
|
||||||
|
>
|
||||||
|
<PDFIcon
|
||||||
|
fill="var(--c-on-primary)"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
class="absolute top-[1.35rem] left-2"
|
||||||
|
style={{display: loading() ? "inline-block" : "none"}}
|
||||||
|
>
|
||||||
|
<LoadingIcon
|
||||||
|
class="animate-spin"
|
||||||
|
fill="var(--c-primary-container)"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span class="ml-5">
|
||||||
|
Convertir escaneos
|
||||||
|
</span>
|
||||||
|
</FilledButton>
|
||||||
|
</div>
|
||||||
</FilledCard>
|
</FilledCard>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -36,7 +36,7 @@ export type CertData<T> = {
|
|||||||
*/
|
*/
|
||||||
certDay: string,
|
certDay: string,
|
||||||
/**
|
/**
|
||||||
* Id of the certificate
|
* Id of the certificate in database (not the same as certCode)
|
||||||
*/
|
*/
|
||||||
certIId: number,
|
certIId: number,
|
||||||
/**
|
/**
|
||||||
|
@ -69,8 +69,7 @@ export async function getQR(data : {
|
|||||||
verticalOffset: number,
|
verticalOffset: number,
|
||||||
behindDocument?: boolean,
|
behindDocument?: boolean,
|
||||||
}): Promise<typeof ImageRun> {
|
}): Promise<typeof ImageRun> {
|
||||||
// Old URL: https://www.eegsac.com/alumnoscertificados.php?DNI=${dni()}
|
// https://eegsac.com/certificado/${dni()}
|
||||||
// New URL: https://eegsac.com/certificado/${dni()}
|
|
||||||
const qr = await QR.toDataURL(`https://eegsac.com/certificado/${data.dni}?iid=${data.iid}`, {margin: 1});
|
const qr = await QR.toDataURL(`https://eegsac.com/certificado/${data.dni}?iid=${data.iid}`, {margin: 1});
|
||||||
|
|
||||||
return new ImageRun({
|
return new ImageRun({
|
||||||
|
9
frontend/src/icons/PDFIcon.tsx
Normal file
9
frontend/src/icons/PDFIcon.tsx
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
|
||||||
|
|
||||||
|
export function PDFIcon(props: {fill: string}) {
|
||||||
|
return (
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="inline-block w-6" fill={props.fill} viewBox="0 0 256 256">
|
||||||
|
<path d="M224,152a8,8,0,0,1-8,8H192v16h16a8,8,0,0,1,0,16H192v16a8,8,0,0,1-16,0V152a8,8,0,0,1,8-8h32A8,8,0,0,1,224,152ZM92,172a28,28,0,0,1-28,28H56v8a8,8,0,0,1-16,0V152a8,8,0,0,1,8-8H64A28,28,0,0,1,92,172Zm-16,0a12,12,0,0,0-12-12H56v24h8A12,12,0,0,0,76,172Zm88,8a36,36,0,0,1-36,36H112a8,8,0,0,1-8-8V152a8,8,0,0,1,8-8h16A36,36,0,0,1,164,180Zm-16,0a20,20,0,0,0-20-20h-8v40h8A20,20,0,0,0,148,180ZM40,112V40A16,16,0,0,1,56,24h96a8,8,0,0,1,5.66,2.34l56,56A8,8,0,0,1,216,88v24a8,8,0,0,1-16,0V96H152a8,8,0,0,1-8-8V40H56v72a8,8,0,0,1-16,0ZM160,80h28.69L160,51.31Z" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user