Intro & Fetch

This commit is contained in:
2024-09-22 17:25:23 -03:00
parent 9dff07dc0a
commit 9850887285
21 changed files with 543 additions and 41 deletions

25
package-lock.json generated
View File

@@ -8,6 +8,7 @@
"name": "digitalpower-stock",
"version": "0.1.0",
"dependencies": {
"axios": "^1.7.7",
"bootstrap": "^5.3.3",
"boxicons": "^2.1.4",
"pinia": "^2.2.2",
@@ -2683,7 +2684,6 @@
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"dev": true,
"license": "MIT"
},
"node_modules/at-least-node": {
@@ -2734,6 +2734,17 @@
"postcss": "^8.1.0"
}
},
"node_modules/axios": {
"version": "1.7.7",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz",
"integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@@ -3730,7 +3741,6 @@
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"dev": true,
"license": "MIT",
"dependencies": {
"delayed-stream": "~1.0.0"
@@ -4696,7 +4706,6 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.4.0"
@@ -5902,7 +5911,6 @@
"version": "1.15.9",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
"integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
"dev": true,
"funding": [
{
"type": "individual",
@@ -6017,7 +6025,6 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
"dev": true,
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
@@ -8057,7 +8064,6 @@
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"devOptional": true,
"license": "MIT",
"engines": {
"node": ">= 0.6"
@@ -8067,7 +8073,6 @@
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
@@ -10047,6 +10052,12 @@
"node": ">= 0.10"
}
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"license": "MIT"
},
"node_modules/pseudomap": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz",

View File

@@ -21,6 +21,7 @@
}
},
"dependencies": {
"axios": "^1.7.7",
"bootstrap": "^5.3.3",
"boxicons": "^2.1.4",
"pinia": "^2.2.2",

BIN
public/DPIntro.mp4 Normal file

Binary file not shown.

BIN
public/DigitalPower.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 237 KiB

BIN
public/login.mp4 Normal file

Binary file not shown.

BIN
public/notification.mp3 Normal file

Binary file not shown.

View File

@@ -4,6 +4,9 @@
</head>
<div class="window-container">
<div id="title-bar" noselect></div>
<div id="notification-container" noselect></div>
<Intro v-if="ui.intro"/>
<Nav/>
<router-view/>
</div>
@@ -13,7 +16,10 @@
import {onMounted, ref} from 'vue';
import {useRouter} from 'vue-router';
import Nav from "@/components/app/Nav.vue";
import Intro from "@/components/app/Intro.vue";
import {useUi} from "@/pinia/ui";
const ui = useUi()
const router = useRouter();
onMounted(() => {
@@ -61,6 +67,10 @@ body {
height: 720px;
}
label {
text-align: left !important;
}
.window-container {
box-shadow: 0px 0px 20px 0px #0a0a0a;
border-radius: 15px;
@@ -68,6 +78,8 @@ body {
height: 720px;
background: white;
overflow-y: auto;
position: relative;
}
#title-bar {
@@ -104,6 +116,13 @@ body {
animation: slideOut var(--duration) forwards;
}
#notification-container {
top: unset !important;
bottom: 2em !important;
margin-top: 6em;
right: 4% !important;
}
[selectedNav="true"] {
background: white;

View File

@@ -0,0 +1,105 @@
<script setup>
import {onMounted} from "vue";
import {useUi} from "@/pinia/ui";
const ui = useUi()
onMounted(() => {
const audio = new Audio('/DPIntro.mp4');
audio.play()
let $image = document.querySelector("#intro-image")
let $text = document.querySelector("#intro-text")
let $back = document.querySelector("#intro")
setTimeout(() => {
$image.setAttribute("class", 'active');
}, 3500)
setTimeout(() => {
$text.setAttribute("class", 'active');
}, 4500)
setTimeout(() => {
$text.setAttribute("class", 'inactive');
$image.setAttribute("class", 'inactive');
setTimeout(() => {
//ui.intro = false;
$back.setAttribute("class", 'inactive');
setTimeout(() => {
ui.intro = false;
}, 3000)
}, 3000)
}, 9000)
})
</script>
<template>
<div id="intro" flex flex-center>
<div flex flex-column flex-center>
<img id="intro-image" src="/DigitalPower.png">
<h2 id="intro-text" noselect="" class="mt-4">Hacemos tus sueños <br> realidad</h2>
</div>
</div>
</template>
<style scoped lang="scss">
#intro {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: white;
z-index: 100000000;
}
.active {
animation: activate 3s forwards;
}
.inactive {
animation: deactivate 3s forwards;
}
img {
width: 240px;
pointer-events: none;
user-select: none;
margin-bottom: 1em;
}
h2 {
color: var(--primary);
margin: unset !important;
text-align: center;
}
img, h2 {
opacity: 0;
}
@keyframes activate {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
@keyframes deactivate {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}
</style>

View File

@@ -1,10 +1,21 @@
<script setup>
import {useUserStore} from "@/pinia/user";
import {onBeforeMount} from "vue";
import {useRoute} from "vue-router";
const userStore = useUserStore();
const route = useRoute();
onBeforeMount(async () => {
const user = await userStore.getUser();
if (!user && !["/#/login", "/#/register"].includes(route.fullPath)) {
location.href = ('/#/login');
}
})
</script>
<template>
<div id="app-layout">
<slot></slot>
<slot v-if="userStore.user"></slot>
</div>
</template>

View File

@@ -1,17 +1,27 @@
<script setup>
import NavMenu from "@/components/app/NavMenu.vue";
import {ref} from "vue";
import {onMounted, ref} from "vue";
import {useUi} from "@/pinia/ui";
import {useRouter} from "vue-router";
import {useUserStore} from "@/pinia/user";
const router = useRouter();
const ui = useUi();
const userStore = useUserStore();
const options = ref([
{
name: "Aplicacion", items: [
{name: "Manual"},
{name: "Solicitar Ayuda"},
{name: "Digital Power"},
{
name: "Cerrar sesion",
callback: () => {
localStorage.clear();
ui.reset();
router.push("/login");
}
},
{
name: "Cerrar",
callback: () => {
@@ -40,6 +50,7 @@ const options = ref([
},
]);
function openMenu(items, name, url) {
ui.setCurrentMenu(null);
if (!items) return;
@@ -68,10 +79,14 @@ function openMenu(items, name, url) {
function handleClick(item) {
item?.callback();
}
onMounted(() => {
})
</script>
<template>
<nav>
<nav v-if="userStore.user">
<div class="list" noselect>
<div class="item" :selectedNav="opt.name === ui.selectedMenu" :id="opt?.name" v-for="(opt, key) in options" :key
@click="openMenu(opt?.items, opt?.name, opt?.url)"><p>

View File

@@ -1,21 +1,26 @@
<script setup>
import {useUi} from "@/pinia/ui";
import {onMounted} from "vue";
import {useProductStore} from "@/pinia/products";
const props = defineProps(["items", "headers"])
const productStore = useProductStore();
onMounted(async () => {
await productStore.fetchProducts();
})
</script>
<template>
<div id="table-container" radius-border>
<div id="table-container" radius-border fade>
<table>
<thead>
<tr>
<th v-for="(head, key) in headers" :key>{{ head?.name }}</th>
<th noselect v-for="(head, key) in headers" :key>{{ head?.name }}</th>
</tr>
</thead>
<tbody>
<tr v-for="(item, key) in items">
<td v-for="(head, key) in headers" :class="head">{{ item?.[head?.name] }}</td>
<tr v-for="(item, key) in productStore.productos">
<td noselect v-for="(head, key) in headers" :class="head">{{ item?.[head?.name?.toLowerCase()] }}</td>
</tr>
</tbody>
</table>

View File

@@ -1,17 +1,20 @@
import {defineStore} from "pinia";
import {DBService, post} from "@/services/apiReq";
export const productos = defineStore('products', {
export const useProductStore = defineStore('products', {
state: () => ({
productos: [],
filter: {}
}),
getters: {
getProducts() {
}
},
actions: {
fetchProducts() {
async fetchProducts() {
let response = await post("/get", {
limit: this.filter.limit,
page: this.filter.page,
table: 'Productos',
}, DBService)
this.productos = response.data?.rows;
}
}
})

View File

@@ -4,6 +4,8 @@ export const useUi = defineStore('ui', {
state: () => ({
currentMenu: null,
selectedMenu: null,
intro: true,
login: true,
}),
getters: {},
actions: {
@@ -11,5 +13,9 @@ export const useUi = defineStore('ui', {
if (!state) this.selectedMenu = null;
this.currentMenu = state;
},
reset() {
this.currentMenu = null;
this.selectedMenu = null;
}
}
})

60
src/pinia/user.js Normal file
View File

@@ -0,0 +1,60 @@
import {defineStore} from "pinia";
import {authUrl, post, site_name} from "../services/apiReq.js";
import axios from "axios";
import {clear, getObject, saveObject} from "@/services/storage";
import {show} from "@/services/notification";
export const useUserStore = defineStore('user', {
state: () => ({
user: null,
}),
getters: {},
actions: {
async login(email, password) {
clear();
let response = await axios.post(authUrl + '/login', {email, password, site_name, panel: 0});
const {User, message} = response.data;
let user = User;
if (message) {
show(response.data.message);
}
/*
if (user.role !== 'super-admin' && user.role !== 'mod-admin' && user.role !== 'admin') {
show("Usuario no encontrado");
return;
}
*/
if (user) {
this.user = user;
saveObject("user", user);
return true;
}
return false;
},
logout() {
clear();
location.reload();
},
async get() {
// || Api request
let _user = getObject("user");
if (!_user) return;
else {
let response = await axios.post(authUrl + '/user', _user);
_user = response.data?.user;
if (_user) {
this.user = _user;
saveObject("user", _user);
}
}
},
async getUser() {
if (!this.user) {
await this.get();
}
return this.user;
}
}
});

View File

@@ -1,4 +1,4 @@
import { createRouter, createWebHistory } from 'vue-router'
import {createRouter, createWebHashHistory} from 'vue-router'
import HomeView from '../views/HomeView.vue'
const routes = [
@@ -8,19 +8,14 @@ const routes = [
component: HomeView
},
{
path: '/about',
name: 'about',
// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: function () {
return import(/* webpackChunkName: "about" */ '../views/AboutView.vue')
}
path: '/login',
name: 'login',
component: () => import('../views/Login.vue')
}
]
const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
history: createWebHashHistory(process.env.BASE_URL),
routes
})

135
src/services/apiReq.js Normal file
View File

@@ -0,0 +1,135 @@
import axios from "axios";
import {show} from "./notification";
export const site_name = "DPStock.db";
export const backendUrl = "https://backend.digitalpower.ar";
//export const databaseUrl = "https://database.digitalpower.ar";
//export const authUrl = "https://auth.digitalpower.ar";
export const authUrl = "http://localhost:3013";
export const databaseUrl = "http://localhost:3014";
const Service = axios.create({
baseURL: `${backendUrl}/api`,
});
export const DBService = axios.create({
baseURL: `${databaseUrl}/api`,
});
export const AuthService = axios.create({
baseURL: `${authUrl}/`,
});
let user = {token: null, id: null, site_name: null};
if (localStorage.getItem("user") !== "undefined") user = JSON.parse(localStorage?.getItem("user"))
if (localStorage.getItem("user") !== "undefined")
user = JSON.parse(localStorage.getItem("user"));
let config = {
headers: {
Authorization: `Bearer ${user?.token}`,
Accept: "application/json",
"Content-Type": "application/json",
},
};
export const post = (path, body, service = Service) => {
if (!user) {
user = JSON.parse(localStorage.getItem("user"));
config = {
headers: {
Authorization: `Bearer ${user?.token}`,
Accept: "application/json",
"Content-Type": "application/json",
},
};
console.log("USER", user);
}
if (!body) {
body = {};
}
if (!body?.site_name) body.site_name = user?.site_name ?? site_name;
if (user?.id) body.user_id = user?.id;
return new Promise(async (resolve) => {
service
.post(path, body, config)
.then((response) => {
let message = response?.message ?? response.data?.message;
let status = response?.status ?? response.data?.status;
if (response.data?.data) response = response?.data;
if (message) {
show(message);
}
setTimeout(() => {
if (response?.data?.redirect) {
location.href = response?.data?.redirect;
}
}, 1000);
if (status === 401) {
location.href = "/admin";
}
resolve(response);
})
.catch((err) => {
console.log(err)
if (err?.response?.status === 401) {
//localStorage.removeItem("dp_user");
//location.href = "/#/login";
}
if (err?.response?.data?.message) {
show(err?.response?.data?.message);
} else
show("Ha ocurrido un error, intente mas tarde");
})
.finally(() => {
});
});
};
export const files = (path, body) => {
if (body?.append) {
body.append("site_name", user?.site_name);
if (user?.id) body.append("user_id", user?.id);
} else {
body.site_name = user?.site_name;
if (user?.id) body.user_id = user?.id;
}
return new Promise((resolve) => {
Service
.post(path, body, {
headers: {
"Content-Type": "multipart/form-data",
Authorization: `Bearer ${user?.token}`,
Accept: "application/json",
},
})
.then((response) => {
if (response.data?.message) {
show(response.data.message);
}
setTimeout(() => {
if (response?.data?.data?.redirect)
location.href = response?.data?.data?.redirect;
}, 1000);
if (response.data?.status === 401) {
location.href = "/admin";
}
resolve(response.data);
})
.catch((err) => {
if (err?.response?.status === 401) {
location.href = "/#/login";
}
show("Ha ocurrido un error, intente mas tarde");
})
.finally(() => {
});
});
};

View File

@@ -0,0 +1,63 @@
export const show = (text, audio) => {
let html = `
${text}
`;
let element = document.createElement("div");
element.innerHTML = html;
element.classList.add("notification");
element.classList.add("notification-enter");
const container = document.getElementById("notification-container");
container?.appendChild(element);
PlayNotificationAudio(audio)
setTimeout(() => {
element.classList.remove("notification-enter")
element.classList.add("notification-exit");
}, 3000);
setTimeout(() => {
element.remove();
}, 3240);
}
export const ConfirmModal = () => {
return new Promise((resolve) => {
let container = document.getElementById("app-layout");
let html = document.createElement("div");
html.setAttribute("id", "confirmModal");
html.innerHTML = `
<div class="overlay" overlay></div>
<div class="custom-modal-html alert-modal" shadow fade radius-border>
<div class="title alert-title">
<h1>Alerta</h1>
</div>
<div class="content">
<p>¿Esta seguro que desea continuar con esta accion? Es posible que no tenga retroceso</p>
<br>
<div class="actions" flex-center>
<a class="custom-button-html red" id="confirmModalButton">Continuar</a>
<a class="custom-button-html" id="cancelModalButton">Cancelar</a>
</div>
</div>
</div>
`;
container?.appendChild(html);
const confirm = document.getElementById("confirmModalButton");
const cancel = document.getElementById("cancelModalButton")
confirm?.addEventListener("click", (e) => {
html.remove();
resolve(true);
})
cancel?.addEventListener("click", (e) => {
html.remove();
resolve(false);
})
})
}
export function PlayNotificationAudio(path = '/notification.mp3') {
const audio = new Audio(path);
audio.play()
}

13
src/services/storage.js Normal file
View File

@@ -0,0 +1,13 @@
export function clear() {
localStorage.clear();
}
export function saveObject(key, object) {
localStorage.setItem(key, JSON.stringify(object));
}
export function getObject(key) {
const item = localStorage.getItem(key);
if (item) return JSON.parse(item);
return null;
}

View File

@@ -382,7 +382,7 @@ p {
[fade] {
animation-name: fadeAnim;
animation-duration: 0.2s;
animation-duration: .5s;
}
[delayedfade] {

View File

@@ -11,11 +11,20 @@ import Table from "@/components/productos/Table.vue";
import {computed, onMounted, ref} from "vue";
import Filters from "@/components/productos/Filters.vue";
import {useUi} from "@/pinia/ui";
import {show} from "@/services/notification";
import {useUserStore} from "@/pinia/user";
const ui = useUi();
const userStore = useUserStore()
const headers = computed(() => {
const producto = productos.value?.[0];
if (!producto) return [];
if (!producto) return [
{value: 'Nombre', name: 'Nombre'},
{value: 'Precio', name: 'Precio'},
{value: 'Stock', name: 'Stock'},
{value: 'Vendidos', name: 'Vendidos'},
{value: 'Acciones', name: 'Acciones'},
];
return Object.keys(producto).map((x) => {
return {value: x, name: x};

51
src/views/Login.vue Normal file
View File

@@ -0,0 +1,51 @@
<script setup>
import {Input} from 'vuedigitalpowerui'
import {onMounted, ref} from "vue";
import {useUserStore} from "@/pinia/user";
import {useRouter} from "vue-router";
import {show} from "@/services/notification";
const email = ref();
const password = ref();
const userStore = useUserStore();
const router = useRouter();
onMounted(() => {
localStorage.clear();
userStore.user = null;
})
async function login() {
await userStore.login(email.value, password.value);
if (userStore.user) {
show("Bienvenido " + userStore.user.name, "/login.mp4");
setTimeout(() => {
router.push("/");
}, 1)
}
}
</script>
<template>
<div id="app-layout" flex flex-center fade>
<form @submit.prevent="login" shadow radius-border="">
<h1>Iniciar Sesion</h1>
<p>Ingrese sus credenciales para continuar</p>
<Input v-model="email" type="text" label="Email"/>
<Input v-model="password" class="mt-3" type="password" label="Contraseña"/>
<Input class="mt-4" type="button" value="Ingresar" background="var(--primary)" color="white" @click="login"/>
</form>
</div>
</template>
<style scoped lang="scss">
form {
width: 600px;
height: 450px;
padding: 4em;
}
#app-layout {
height: calc(90vh - var(--header-height) + 60px);
}
</style>