Added functionality to select article.

This commit is contained in:
2026-01-20 04:53:39 +01:00
parent 098972d000
commit 5e929dbb1e
9 changed files with 368 additions and 17 deletions
+81 -3
View File
@@ -4,11 +4,11 @@ use crate::models::origin::Origin;
use crate::models::variant::Variant; use crate::models::variant::Variant;
use sqlx::{Arguments, Pool, Row, Postgres, Error}; use sqlx::{Arguments, Pool, Row, Postgres, Error};
use sqlx::postgres::{PgArguments, PgQueryResult, PgRow}; use sqlx::postgres::{PgArguments, PgQueryResult, PgRow};
use log::{debug, info}; use log::{debug, error, info};
#[derive(Debug, Deserialize, Serialize, Clone, FromRow)] #[derive(Debug, Deserialize, Serialize, Clone, FromRow)]
pub struct Article { pub struct Article {
pub id: usize, pub id: i64,
pub name: String, pub name: String,
pub bio: bool, pub bio: bool,
pub origin: Origin, pub origin: Origin,
@@ -17,6 +17,85 @@ pub struct Article {
pub variants: Vec<Variant>, pub variants: Vec<Variant>,
} }
fn from_postgres_row_small(row: PgRow) -> Article {
Article {
id: row.get(0),
name: row.get(1),
bio: row.get(2),
origin: row.get(3),
description: row.get(4),
ingredients: row.get(5),
variants: vec![],
}
}
fn from_postgres_row(row: PgRow, pool: PgPool) -> Article {
Article {
id: row.get(0),
name: row.get(1),
bio: row.get(2),
origin: row.get(3),
description: row.get(4),
ingredients: row.get(5),
variants: vec![],
}
}
pub async fn load_articles(pool: PgPool) -> Option<Vec<Article>> {
let statement = format!(
"SELECT * FROM {}",
"article",
);
let res = sqlx::query(
statement.as_str())
.fetch_all(&pool).await;
let mut articles: Vec<Article> = Vec::new();
match res {
Ok(records) => {
for record in records {
articles.push(
from_postgres_row_small(record)
);
}
Some(articles)
}
Err(err) => {
error!("Error while loading all articles");
None
}
}
}
pub async fn load_article(id: i64, pool: PgPool) -> Option<Article> {
let statement = format!(
"SELECT * FROM {} WHERE id = $1",
"article"
);
let mut args = PgArguments::default();
args.add(id);
let res = sqlx::query_with(
statement.as_str(), args)
.fetch_one(&pool).await;
match res {
Ok(record) => {
let article = from_postgres_row(record, pool);
Some(article)
}
Err(err) => {
error!("Error while loading all article id: {}", id);
None
}
}
}
pub async fn insert_article(article: &Article, pool: PgPool) { pub async fn insert_article(article: &Article, pool: PgPool) {
let mut args = PgArguments::default(); let mut args = PgArguments::default();
args.add(&article.name); args.add(&article.name);
@@ -25,7 +104,6 @@ pub async fn insert_article(article: &Article, pool: PgPool) {
args.add(&article.description); args.add(&article.description);
args.add(&article.ingredients); args.add(&article.ingredients);
// Add to price history:
let statement = format!( let statement = format!(
"INSERT INTO {} (name, bio, origin, description, ingredients) VALUES ($1, $2, $3, $4, $5)", "INSERT INTO {} (name, bio, origin, description, ingredients) VALUES ($1, $2, $3, $4, $5)",
"article", "article",
+1 -1
View File
@@ -2,7 +2,7 @@ use std::fmt;
use std::fmt::Formatter; use std::fmt::Formatter;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[repr(i32)] #[repr(i64)]
#[derive(Deserialize, Serialize, Copy, Clone, PartialEq, sqlx::Type)] #[derive(Deserialize, Serialize, Copy, Clone, PartialEq, sqlx::Type)]
pub enum Origin { pub enum Origin {
EU, EU,
+1 -1
View File
@@ -3,6 +3,6 @@ use sqlx::FromRow;
#[derive(Debug, Deserialize, Serialize, Clone, FromRow)] #[derive(Debug, Deserialize, Serialize, Clone, FromRow)]
pub struct Variant { pub struct Variant {
pub id: usize, pub id: i64,
} }
@@ -0,0 +1,60 @@
<script setup lang="ts">
import {useLabelEditorStore} from "~/store/labelEditorStore";
let store = useLabelEditorStore();
</script>
<template>
<v-container>
<v-card v-if="store.article != null">
<v-card-title>{{ store.article.name }}</v-card-title>
<v-divider/>
<v-card-text>
<v-row no-gutters>
<v-col cols="2">
<v-sheet>
BIO:
</v-sheet>
</v-col>
<v-col>
<v-sheet>
{{ store.article.bio }}
</v-sheet>
</v-col>
</v-row>
<v-row no-gutters>
<v-col cols="2">
<v-sheet>
Herkunft:
</v-sheet>
</v-col>
<v-col>
<v-sheet>
{{ store.article.origin }}
</v-sheet>
</v-col>
</v-row>
<v-row no-gutters>
<v-col cols="2">
<v-sheet>
Varianten:
</v-sheet>
</v-col>
<v-col>
<v-sheet>
{{ store.article.variants.length }}
</v-sheet>
</v-col>
</v-row>
</v-card-text>
</v-card>
</v-container>
</template>
<style scoped>
</style>
@@ -0,0 +1,137 @@
<script setup lang="ts">
import {useLabelEditorStore} from "~/store/labelEditorStore";
import type {Article} from "~/types/article";
let config = useRuntimeConfig();
let store = useLabelEditorStore();
let dialog: Ref<boolean> = ref<boolean>(false);
let loading: Ref<boolean> = ref<boolean>(false);
let articles: Ref<Article[]> = ref<Article[]>([]);
let selectedArticle: Ref<Article | null> = ref<Article | null>(null);
watch(dialog, async (oldDialog) => {
if (oldDialog) {
console.log("Get articles");
articles.value = await loadArticles();
console.log("Got articles");
}
})
async function loadArticles(): Promise<Array<Article>> {
try {
const articles: Array<Article> = await $fetch<Array<Article>>(config.public.url + '/articles', {
method: 'GET',
});
return articles;
} catch {
return [];
}
}
async function getSelected() {
if (selectedArticle.value != null) {
loading.value = true;
const articleID: number = selectedArticle.value.id;
await store.loadArticle(articleID);
loading.value = false;
dialog.value = false;
}
}
function closeDialog() {
clearVariables();
dialog.value = false;
}
function clearVariables() {
selectedArticle.value = null;
loading.value = false;
}
</script>
<template>
<v-dialog
v-model="dialog"
:scrollable="true"
width="800"
max-height="500"
>
<template v-slot:activator="{ props }">
<v-btn
:flat="true"
v-bind="props"
>
<v-icon>mdi-magnify</v-icon>
<v-tooltip
activator="parent"
location="bottom"
>
Artikel auswählen
</v-tooltip>
</v-btn>
</template>
<v-card>
<v-card-title>
<v-icon size="small">
mdi-magnify
</v-icon>
Artikel auswählen
<v-progress-circular v-if="loading" indeterminate :size="15" :width="2"></v-progress-circular>
</v-card-title>
<v-divider/>
<v-card-text>
<v-autocomplete
v-model="selectedArticle"
:items="articles"
label="Artikel"
item-title="name"
:disabled="loading"
:clearable="true"
:autofocus="true"
:return-object="true"
:auto-select-first="true"
:open-on-clear="true"
no-data-text="Keine Artikel angelegt"
@keyup.enter="getSelected()"
></v-autocomplete>
</v-card-text>
<v-divider/>
<v-card-actions>
<v-spacer/>
<v-btn
variant="text"
:disabled="loading"
@click="closeDialog()"
>
Abbrechen
</v-btn>
<v-btn
variant="text"
:disabled="selectedArticle == null || loading"
@click="getSelected();"
>
Auswählen
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<style scoped>
</style>
@@ -1,5 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import Notifications from "~/components/notification/notifications.vue"; import Notifications from "~/components/notification/notifications.vue";
import PickArticle from "~/components/article/pickArticle.vue";
</script> </script>
<template> <template>
@@ -13,6 +14,8 @@
:vertical="true" :vertical="true"
/> />
<PickArticle/>
<ArticleCreate/> <ArticleCreate/>
<v-divider <v-divider
+15 -3
View File
@@ -1,11 +1,23 @@
<script setup lang="ts"> <script setup lang="ts">
import {useLabelEditorStore} from "~/store/labelEditorStore";
let store = useLabelEditorStore();
</script> </script>
<template> <template>
<v-card> <v-container
<v-card-title>Test Index</v-card-title> v-if="store.article != null"
</v-card> >
<Article/>
</v-container>
<v-container v-else>
<v-card>
<v-card-title>
Kein Artikel ausgewählt
</v-card-title>
</v-card>
</v-container>
</template> </template>
<style scoped> <style scoped>
+18 -6
View File
@@ -91,11 +91,11 @@ export const useLabelEditorStore = defineStore('label_editor_store', {
// Article // Article
async loadArticle(articleID: number) { async loadArticle(articleID: number) {
try { try {
const entity: Article = await $fetch<Article>(this.config.public.url + '/article/' + articleID, { const article: Article = await $fetch<Article>(this.config.public.url + '/article/' + articleID, {
method: 'GET', method: 'GET',
}); });
this.article = entity; this.article = article;
this.addNotification(false, 'Artikel geladen: ' + this.article.name); this.addNotification(false, 'Artikel geladen: ' + this.article.name);
} catch { } catch {
this.article = null; this.article = null;
@@ -103,6 +103,22 @@ export const useLabelEditorStore = defineStore('label_editor_store', {
} }
}, },
// TODO: Check if function from view can be replaced with store method
async loadArticles(): Promise<Array<Article>> {
console.log("Test");
try {
const articles: Array<Article> = await $fetch<Array<Article>>(this.config.public.url + '/articles', {
method: 'GET',
});
this.addNotification(false, 'Artikelliste geladen');
return articles;
} catch {
this.addNotification(true, 'Fehler beim laden der Artikeliste');
return [];
}
},
async createArticle(article: Article) { async createArticle(article: Article) {
try { try {
this.article = await $fetch<Article>(this.config.public.url + '/article', { this.article = await $fetch<Article>(this.config.public.url + '/article', {
@@ -132,9 +148,5 @@ export const useLabelEditorStore = defineStore('label_editor_store', {
this.addNotification(true, 'Es ist kein Artikel ausgewählt'); this.addNotification(true, 'Es ist kein Artikel ausgewählt');
} }
}, },
clearEntity() {
this.article = null;
},
}, },
}); });
+52 -3
View File
@@ -4,17 +4,17 @@ use axum::{
http::StatusCode, http::StatusCode,
Json, Router, Json, Router,
}; };
use axum::extract::State; use axum::extract::{Path, State};
use dotenv::dotenv; use dotenv::dotenv;
use env_logger::fmt::style::Reset; use env_logger::fmt::style::Reset;
use http::{header, Method}; use http::{header, Method};
use common::models::article::{insert_article, Article}; use common::models::article::{insert_article, load_article, load_articles, Article};
use common::models::origin::Origin; use common::models::origin::Origin;
use sqlx::{ postgres::PgPoolOptions, PgPool }; use sqlx::{ postgres::PgPoolOptions, PgPool };
use tower_http::cors::{AllowOrigin, Any, CorsLayer}; use tower_http::cors::{AllowOrigin, Any, CorsLayer};
use tracing::info; use tracing::info;
use http::header::{AUTHORIZATION, ACCEPT, ACCESS_CONTROL_ALLOW_ORIGIN, CONTENT_TYPE, ACCESS_CONTROL_ALLOW_CREDENTIALS}; use http::header::{AUTHORIZATION, ACCEPT, ACCESS_CONTROL_ALLOW_ORIGIN, CONTENT_TYPE, ACCESS_CONTROL_ALLOW_CREDENTIALS};
use common::models::article;
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
@@ -51,7 +51,9 @@ async fn main() {
let app = Router::new() let app = Router::new()
// `GET /` goes to `root` // `GET /` goes to `root`
.route("/", get(root)) .route("/", get(root))
.route("/articles", get(get_articles))
.route("/article", post(create_article)) .route("/article", post(create_article))
.route("/article/{id}", get(get_article))
.with_state(pool) .with_state(pool)
.layer(cors); .layer(cors);
@@ -66,6 +68,53 @@ async fn root() -> &'static str {
"Hello, World!" "Hello, World!"
} }
#[tracing::instrument(ret)]
async fn get_articles(
State(pool): State<PgPool>,
) -> (StatusCode, Json<Vec<Article>>) {
info!("Get all articles");
let result_articles: Option<Vec<Article>> = load_articles(pool).await;
match result_articles {
Some(articles) => {
(StatusCode::OK, Json(articles))
}
None => {
(StatusCode::INTERNAL_SERVER_ERROR, Json(vec![]))
}
}
}
#[tracing::instrument(ret)]
async fn get_article(
Path(id): Path<i64>,
State(pool): State<PgPool>,
) -> (StatusCode, Json<Article>) {
info!("Get article with id: {}", id);
let result_article: Option<Article> = load_article(id, pool).await;
match result_article {
Some(article) => {
(StatusCode::OK, Json(article))
}
None => {
let article = Article {
id,
name: "".to_string(),
bio: false,
origin: Origin::EU,
description: None,
ingredients: None,
variants: vec![],
};
(StatusCode::INTERNAL_SERVER_ERROR, Json(article))
}
}
}
#[tracing::instrument(ret)] #[tracing::instrument(ret)]
async fn create_article( async fn create_article(
State(pool): State<PgPool>, State(pool): State<PgPool>,