Compare commits

4 Commits
main ... stock

Author SHA1 Message Date
c7d03346ee Add Product 2024-09-24 12:21:30 -03:00
1d66aaf7c5 Modal 2024-09-22 23:45:44 -03:00
9850887285 Intro & Fetch 2024-09-22 17:25:23 -03:00
9dff07dc0a Productos view 2024-09-22 01:31:21 -03:00
27 changed files with 2218 additions and 133 deletions

View File

@@ -1,18 +1,22 @@
const { app, BrowserWindow } = require('electron'); const {app, BrowserWindow} = require('electron');
const path = require('path'); const path = require('path');
function createWindow() { function createWindow() {
const win = new BrowserWindow({ const win = new BrowserWindow({
width: 1024, width: 1100,
height: 720, height: 790,
resizable: false,
frame: false,
transparent: true,
autoHideMenuBar: true,
webPreferences: { webPreferences: {
preload: path.join(__dirname, 'preload.js'), preload: path.join(__dirname, 'preload.js'),
nodeIntegration: true, nodeIntegration: true,
contextIsolation: false contextIsolation: false,
} }
}); });
win.loadURL('http://localhost:8080'); win.loadURL('http://localhost:8080');
//win.loadFile(path.join(__dirname, 'dist/index.html')); // win.loadFile(path.join(__dirname, 'dist/index.html'));
} }
app.whenReady().then(createWindow); app.whenReady().then(createWindow);
@@ -21,7 +25,6 @@ app.on('window-all-closed', () => {
app.quit(); app.quit();
} }
}); });
app.on('activate', () => { app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) { if (BrowserWindow.getAllWindows().length === 0) {
createWindow(); createWindow();

567
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -21,16 +21,22 @@
} }
}, },
"dependencies": { "dependencies": {
"axios": "^1.7.7",
"bootstrap": "^5.3.3",
"boxicons": "^2.1.4",
"pinia": "^2.2.2", "pinia": "^2.2.2",
"vue": "^3.2.13", "vue": "^3.2.13",
"vue-router": "^4.0.3" "vue-router": "^4.0.3",
"vuedigitalpowerui": "^0.1.7"
}, },
"devDependencies": { "devDependencies": {
"@vue/cli-plugin-router": "~5.0.0", "@vue/cli-plugin-router": "~5.0.0",
"@vue/cli-service": "~5.0.0", "@vue/cli-service": "~5.0.0",
"electron": "^32.1.2", "electron": "^32.1.2",
"electron-builder": "^25.0.5", "electron-builder": "^25.0.5",
"sass": "^1.32.7", "jquery": "^3.7.1",
"sass-loader": "^12.0.0" "popper.js": "^1.16.1",
"sass": "^1.79.3",
"sass-loader": "^12.6.0"
} }
} }

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

@@ -1,15 +1,25 @@
<template> <template>
<nav> <head>
<router-link to="/">Home</router-link> | <link rel="stylesheet" href="https://digitalpower.ar/cdn/digitalpower.css">
<router-link to="/about">About</router-link> </head>
</nav> <div class="window-container">
<router-view /> <div id="title-bar" noselect></div>
<div id="notification-container" noselect></div>
<Intro v-if="ui.intro"/>
<Nav/>
<router-view/>
</div>
</template> </template>
<script setup> <script setup>
import { onMounted, ref } from 'vue'; import {onMounted, ref} from 'vue';
import {useRouter} from 'vue-router'; 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(); const router = useRouter();
onMounted(() => { onMounted(() => {
@@ -18,6 +28,16 @@ onMounted(() => {
</script> </script>
<style lang="scss"> <style lang="scss">
:root {
--header-height: 150px !important;
--app-shadow: rgba(0, 0, 0, 0.45);
--navmenu-height: 210px;
}
body {
background: transparent !important;
}
#app { #app {
font-family: Avenir, Helvetica, Arial, sans-serif; font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
@@ -26,6 +46,23 @@ onMounted(() => {
color: #2c3e50; color: #2c3e50;
} }
#title-bar {
-webkit-app-region: drag;
height: 20px;
background: var(--primary);
color: white;
display: flex;
align-items: center;
padding: 0 10px;
}
#notification-container {
top: unset !important;
bottom: 2em !important;
margin-top: 6em;
right: 4% !important;
}
nav { nav {
padding: 30px; padding: 30px;
@@ -38,4 +75,61 @@ nav {
} }
} }
} }
body {
margin: 0;
padding: 0;
padding: 2em;
width: 1024px;
height: 720px;
}
label {
text-align: left !important;
text-transform: capitalize;
}
.window-container {
box-shadow: 0px 0px 20px 0px #0a0a0a;
border-radius: 15px;
width: 1024px;
height: 720px;
background: white;
overflow-y: auto;
position: relative;
}
.select-wrapper::after {
content: ">" !important;
}
@keyframes slideOut {
from {
transform: translateX(0);
opacity: 1;
}
to {
transform: translateX(-100%);
opacity: 0;
}
}
.slide-leave-active {
transition: opacity var(--duration), transform var(--duration);
}
.slide-leave-to {
animation: slideOut var(--duration) forwards;
}
[selectedNav="true"] {
background: white;
& > p {
color: var(--text-color) !important;
}
}
</style> </style>

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">La fabrica de aplicaciones <br> Web</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

@@ -0,0 +1,28 @@
<script setup>
import {useUserStore} from "@/pinia/user";
import {onBeforeMount} from "vue";
import {useRoute, useRouter} from "vue-router";
const userStore = useUserStore();
const route = useRoute();
const router = useRouter();
onBeforeMount(async () => {
const user = await userStore.getUser();
if (!user && !["/#/login", "/#/register"].includes(route.fullPath)) {
location.href = "/#/login";
}
})
</script>
<template>
<div id="app-layout">
<slot v-if="userStore.user"></slot>
</div>
</template>
<style scoped lang="scss">
#app-layout {
padding: 1em 2em;
height: calc(100vh - var(--header-height));
}
</style>

157
src/components/app/Nav.vue Normal file
View File

@@ -0,0 +1,157 @@
<script setup>
import NavMenu from "@/components/app/NavMenu.vue";
import {onMounted, ref} from "vue";
import {useUi} from "@/pinia/ui";
import {useRouter} from "vue-router";
import {useUserStore} from "@/pinia/user";
import {useProductStore} from "@/pinia/products";
const router = useRouter();
const ui = useUi();
const userStore = useUserStore();
const productStore = useProductStore();
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: () => {
window.close();
}
},
]
},
{
name: "Productos", items: [
{
name: "Agregar Producto", callback: () => {
ui.reset()
productStore.product_modal = true;
}
},
{name: "Generar Venta"},
]
},
{
name: "Imprimir",
items: [
{name: "Listado de productos"},
{name: "Estadisticas"},
],
},
{
name: "Estadisticas",
items: [],
url: '/',
},
{
name: "Chat",
items: [],
url: '/',
},
]);
function openMenu(items, name, url) {
if (ui.selectedMenu === name) return;
ui.setCurrentMenu(null);
if (!items) return;
if (url) {
router.push(url);
return;
}
setTimeout(() => {
ui.setCurrentMenu(items);
ui.selectedMenu = name;
}, 5)
setTimeout(() => {
let $menu = document.querySelector("#menu-dropdown");
let $option = document.querySelector("#" + name);
const rect = ($option.getBoundingClientRect())
$menu.setAttribute("style", `
position: fixed;
left: ${rect.left}px;
bottom: ${rect.bottom}px;
top: ${rect.top + rect.height}px;
`)
}, 20)
}
function handleClick(item) {
item?.callback();
}
onMounted(() => {
})
</script>
<template>
<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>
{{
opt?.name
}}</p></div>
</div>
<NavMenu @callback="handleClick" v-if="ui.currentMenu"></NavMenu>
</nav>
</template>
<style scoped lang="scss">
nav {
background: var(--primary);
width: 100%;
height: 60px;
padding: 0;
z-index: 20;
}
.list {
width: 100%;
height: 100%;
display: flex;
justify-content: start;
align-items: end;
}
.item {
transition: var(--duration);
border-top-right-radius: 15px;
&:hover {
cursor: pointer;
background: white;
& > p {
color: var(--text-color);
}
}
& > p {
transition: var(--duration);
margin: 0;
padding: 1em;
border-top-right-radius: 15px;
color: white;
}
}
</style>

View File

@@ -0,0 +1,58 @@
<script setup>
import {useUi} from "@/pinia/ui";
const emits = defineEmits(["callback"]);
const ui = useUi();
</script>
<template>
<div class="nav-menu" id="menu-dropdown">
<div v-for="(item, key) in ui.currentMenu" :key class="item" @click="emits('callback', item)">
{{ item?.name }}
</div>
</div>
</template>
<style scoped lang="scss">
.nav-menu {
width: 250px;
height: var(--navmenu-height);
box-shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.4);
background: white;
border-bottom-left-radius: 15px;
border-bottom-right-radius: 15px;
animation: fade .2s ease-in-out forwards;
overflow-y: auto;
&::-webkit-scrollbar {
display: none;
}
}
.item {
padding: .5em 1em;
border-bottom: 1px solid rgba(0, 0, 0, 0.2);
text-align: left;
cursor: pointer;
transition: var(--duration);
&:hover {
color: white;
background: var(--primary);
}
}
@keyframes fade {
0% {
opacity: 0;
height: 0
}
100% {
opacity: 1;
height: var(--navmenu-height);
}
}
</style>

View File

@@ -0,0 +1,42 @@
<script setup>
import {Input} from 'vuedigitalpowerui'
import {ref} from "vue";
import {useProductStore} from "@/pinia/products";
const emits = defineEmits(["search"]);
const props = defineProps(["filters"])
const filter = ref("");
const value = ref("");
const productStore = useProductStore()
function search() {
emits("search", {filter: filter.value, value: value.value})
}
async function update() {
await productStore.fetchProducts();
}
</script>
<template>
<div id="filters" class="mt-3">
<form @submit.prevent="search" flex gapped flex-wrap>
<Input class="filter" type="select" v-model="filter" :options="filters"/>
<Input class="filter" placeholder="Busqueda" v-model="value"/>
<Input type="button" value="Buscar" background="var(--primary)" color="white" @click="search"/>
<Input type="button" value="Limpiar" background="var(--primary)" color="white"/>
<Input type="button" value="Actualizar" background="var(--primary)" color="white" @click="update"/>
</form>
</div>
</template>
<style scoped lang="scss">
#filters {
width: 100%;
}
.filter {
width: 250px !important;
}
</style>

View File

@@ -0,0 +1,38 @@
<script setup>
import {Modal, Input} from 'vuedigitalpowerui'
import {useProductStore} from "@/pinia/products";
import {reactive} from "vue";
const productStore = useProductStore()
const props = defineProps(["headers"])
const emits = defineEmits(["confirm"])
const newProduct = reactive({});
</script>
<template>
<Modal title="Producto" @close="() => {productStore.product_modal = false}">
<div class="entradas" gapped>
<div v-for="(item, key) in headers" class="entrada">
<Input class="mt-3" :key :label="item?.name" v-if="item?.name !== 'acciones'" v-model="newProduct[item?.name]"/>
</div>
</div>
<div flex gapped class="mt-3">
<Input value="Guardar" type="button" background="var(--primary)" color="white"
@click="() => emits('confirm', newProduct)"/>
<Input value="Cancelar" type="button" background="var(--red)" color="white"
@click="() => {productStore.product_modal = false}"/>
</div>
</Modal>
</template>
<style scoped lang="scss">
.entradas {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
}
.entrada {
width: 47%;
}
</style>

View File

@@ -0,0 +1,96 @@
<script setup>
import {useUi} from "@/pinia/ui";
import {onMounted} from "vue";
import {useProductStore} from "@/pinia/products";
import {limit} from "../../services/utils";
const props = defineProps(["items", "headers"])
const productStore = useProductStore();
onMounted(async () => {
await productStore.fetchProducts();
})
</script>
<template>
<div id="table-container" radius-border fade>
<table>
<thead>
<tr>
<th noselect v-for="(head, key) in headers" :key>{{ head?.name }}</th>
</tr>
</thead>
<tbody>
<tr v-for="(item, key) in productStore.productos">
<td noselect v-for="(head, key) in headers" :class="head">{{
limit(item?.[head?.name?.toLowerCase()], 30)
}}
</td>
</tr>
</tbody>
</table>
</div>
</template>
<style scoped lang="scss">
#table-container {
box-shadow: 0px 0px 5px 0px var(--app-shadow);
border-radius: 15px;
height: 80%;
overflow-y: auto;
&::-webkit-scrollbar-track {
background: transparent;
}
}
table {
width: 100%;
border-collapse: collapse;
}
thead > tr > th {
border-top: none;
text-transform: capitalize;
&:first-child {
border-left: none;
}
&:last-child {
border-right: none;
}
}
tbody > tr {
transition: var(--duration);
&:last-child {
& > td {
border-bottom: none !important;
}
}
&:hover {
background: var(--primary);
color: white;
}
}
th {
padding: 1em 2em;
//border: 1px solid var(--text-color);
border: 1px solid rgba(0, 0, 0, 0.27);
}
td {
padding: 1em 2em;
border-bottom: 1px solid rgba(0, 0, 0, 0.27);
}
.Nombre {
width: 400px;
}
</style>

View File

@@ -2,7 +2,13 @@ import {createApp} from 'vue'
import App from './App.vue' import App from './App.vue'
import router from './router' import router from './router'
import {createPinia} from "pinia"; import {createPinia} from "pinia";
import "../src/styles/digitalpower.css"
import 'boxicons'
import * as bootstrap from 'bootstrap'
import 'bootstrap'
import 'bootstrap/dist/css/bootstrap.min.css'
import 'jquery/src/jquery.js'
import 'bootstrap/dist/js/bootstrap.min.js'
const pinia = createPinia(); const pinia = createPinia();
const app = createApp(App).use(pinia).use(router).mount('#app') const app = createApp(App).use(pinia).use(router).mount('#app')

View File

@@ -1,17 +1,21 @@
import {defineStore} from "pinia"; import {defineStore} from "pinia";
import {DBService, post} from "@/services/apiReq";
export const productos = defineStore('products', { export const useProductStore = defineStore('products', {
state: () => ({ state: () => ({
productos: [], productos: [],
filter: {},
product_modal: false,
}), }),
getters: {
getProducts() {
}
},
actions: { 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;
} }
} }
}) })

21
src/pinia/ui.js Normal file
View File

@@ -0,0 +1,21 @@
import {defineStore} from "pinia";
export const useUi = defineStore('ui', {
state: () => ({
currentMenu: null,
selectedMenu: null,
intro: true,
login: true,
}),
getters: {},
actions: {
setCurrentMenu(state) {
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,27 +1,22 @@
import { createRouter, createWebHistory } from 'vue-router' import {createRouter, createWebHashHistory} from 'vue-router'
import HomeView from '../views/HomeView.vue' import HomeView from '../views/HomeView.vue'
const routes = [ const routes = [
{ {
path: '/', path: '/',
name: 'home', name: 'home',
component: HomeView component: HomeView
}, },
{ {
path: '/about', path: '/login',
name: 'about', name: 'login',
// route level code-splitting component: () => import('../views/Login.vue')
// 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')
} }
}
] ]
const router = createRouter({ const router = createRouter({
history: createWebHistory(process.env.BASE_URL), history: createWebHashHistory(process.env.BASE_URL),
routes routes
}) })
export default router export default router

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;
}

12
src/services/utils.js Normal file
View File

@@ -0,0 +1,12 @@
export const limit = (text, limit) => {
if (!text) return "";
if (text?.length <= limit) {
return text;
} else {
try {
return text?.slice(0, limit) + '...';
} catch (e) {
return text;
}
}
}

663
src/styles/digitalpower.css Normal file
View File

@@ -0,0 +1,663 @@
@import url("https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap");
* {
box-sizing: border-box;
font-family: "Poppins", sans-serif;
/* Esto afectará a cualquier scrollbar en tu página web */
::-webkit-scrollbar {
width: 3px;
height: 5px;
}
/* Track */
::-webkit-scrollbar-track {
background: var(--gray-light);
}
/* Handle */
::-webkit-scrollbar-thumb {
background: var(--primary);
}
/* Handle on hover */
::-webkit-scrollbar-thumb:hover {
background: var(--secondary);
}
}
:root {
--gray: rgba(0, 0, 0, 0.3);
--gray-light: rgba(0, 0, 0, 0.1);
--gray-very-light: rgba(0, 0, 0, 0.08);
--primary-blue: #3a7ca5;
--primary: dodgerblue;
--primary-active-item: #efefef;
--secondary: #0887a1;
--red: #d14c4c;
--back-color: white;
--text-color: rgba(62, 62, 62, 0.7);
--sidebar-color: dodgerblue;
--sidebar-color-hover: white;
--input-bg: white;
--input-button: #efefef;
--input-bg-hover: #dbdbdb;
--shadow: 0px 0px 5px 0px var(--gray);
--admin-title-height: 67px;
--duration: 0.3s;
--header-height: 50px;
}
body {
margin: 0;
overflow-x: hidden;
background: var(--back-color);
}
#app-layout {
max-width: 100%;
}
#notification-container {
position: fixed;
top: calc(var(--header-height) + 1.5em);
right: 0;
margin-right: 1em;
z-index: 1000;
}
h1 {
font-weight: bold;
font-size: 2em;
}
h1, h2, h3, h4, h5, p, b {
width: fit-content;
}
pre {
background: lightgray;
}
.page-enter-active,
.page-leave-active {
transition: all 0.2s;
}
.page-enter-from,
.page-leave-to {
opacity: 0;
}
.custom-modal-html {
width: 80vw;
height: fit-content;
max-height: 100vh;
max-width: 100vh;
position: fixed;
top: 50%;
left: 50%;
z-index: 200000;
color: var(--text-color);
background: var(--back-color);
overflow-y: auto;
transform: translate(-50%, -50%);
.title {
padding: 0.5em;
border-bottom: 1px solid var(--gray);
h5,
h1 {
text-align: center;
margin: 0;
}
}
.content {
display: flex;
align-items: center;
flex-direction: column;
justify-content: center;
padding: 2em;
p {
margin-bottom: 2em;
}
.actions {
gap: 1em;
}
}
}
a {
color: dodgerblue;
text-decoration: none;
cursor: pointer;
}
p {
font-size: 1em;
}
.active-item {
background: var(--secondary);
color: var(--sidebar-color-hover) !important;
.icon {
fill: var(--sidebar-color-hover) !important;
}
}
.notification {
padding: 1em;
background: var(--primary);
color: white;
margin: 1em 0;
min-width: 200px;
pointer-events: none;
user-select: none;
z-index: 1000;
}
.notification-enter {
animation-name: notificationAnimationEnter;
animation-duration: 0.25s;
}
.notification-exit {
animation-name: notificationAnimationExit;
animation-duration: 0.25s;
}
.ck-editor {
display: block;
width: 100% !important;
}
.ck {
color: var(--text-color) !important;
background: var(--input-bg) !important;
border: none !important;
}
.red {
background: var(--red) !important;
}
[limited-text] {
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
text-wrap: nowrap;
}
[flex] {
display: flex !important;
}
[flex-wrap] {
flex-wrap: wrap;
}
[nopointer] {
pointer-events: none;
}
[subitem] {
padding-left: 1em;
}
[groupitem] {
border-bottom: 1px solid var(--gray);
}
[grouplabel] {
background: var(--gray-light);
color: white;
}
[menuitem] {
border-bottom: 1px solid var(--gray);
}
[menuitem],
[subitem] {
transition: 0.2s;
&:hover {
background: var(--secondary);
color: white;
}
}
[shadow] {
box-shadow: var(--shadow);
}
[borderbottom] {
border-bottom: 1px solid var(--gray);
}
[bordered] {
border: 1px solid var(--gray);
}
[radius-border] {
border-radius: 5px;
}
[bound] {
background: var(--gray-light);
background-image: linear-gradient(
to right,
transparent 20%,
var(--gray-very-light) 60%
);
background-size: 200% 100%;
transition: background-position 0.2s ease-out;
animation: boundAnimation 2s ease infinite alternate;
}
[custominput] {
border: 1px solid var(--gray-light);
border-radius: 5px;
&:focus {
outline: none;
border: 1px solid var(--gray);
box-shadow: 0px 0px 5px 0px var(--gray);
}
}
[customselect] {
width: 100%;
select {
padding: 0.6em 1em;
width: 100%;
appearance: none;
background: white;
}
}
.select-wrapper {
position: relative;
&::after {
content: "â–¼";
font-size: 1rem;
top: 50%;
right: 1em;
position: absolute;
color: var(--gray);
transform: translateY(-50%);
z-index: 100000;
}
}
.custom-button-html {
padding: 0.6em 1.5em;
border-radius: 5px;
border: none;
cursor: pointer;
font-size: 1em;
display: block;
width: fit-content;
transition: 0.2s;
color: white;
background: var(--primary);
margin-bottom: 0.5em;
&:hover {
filter: brightness(0.8);
}
&:focus {
outline: none;
}
}
[flex-center] {
display: flex !important;
align-items: center;
justify-content: center;
}
[flex-start] {
display: flex !important;
align-items: start;
justify-content: start;
}
[flex-column] {
flex-direction: column;
}
[block-center] {
display: block;
margin: auto;
width: fit-content;
}
[overlay] {
position: fixed;
top: 2em !important;
left: 2em !important;
width: calc(100vw - 4.6em) !important;
height: calc(100vh - 4.2em) !important;
z-index: 15;
background: var(--gray);
border-radius: 15px;
}
[inside-verlay] {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: none;
transition: var(--duration);
z-index: 15;
background: rgba(0, 0, 0, 0.4);
}
[fade] {
animation-name: fadeAnim;
animation-duration: .5s;
}
[delayedfade] {
animation: fadeAnim 0.2s forwards;
}
[absolute-centered] {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
[list] {
list-style: none;
margin: 0;
padding: 0;
}
[gapped] {
gap: 1em;
}
[noselect] {
user-select: none;
}
[underline] {
/*
border-bottom: 1px solid var(--primary);
*/
padding-bottom: 10px;
margin-bottom: 1em;
position: relative;
&::after {
content: "";
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 2px;
background: var(--primary);
animation-name: underlineAnim;
animation-duration: var(--duration);
}
}
[nowrap] {
text-wrap: nowrap;
}
[pointer] {
pointer-events: all;
}
[select] {
user-select: all;
}
[slide] {
animation: slide;
animation-duration: var(--duration);
}
[slideRight] {
animation: slideRight;
animation-duration: var(--duration);
}
[slideUp] {
animation: slideUp;
animation-duration: var(--duration);
}
[slideDown] {
animation: slideDown;
animation-duration: var(--duration);
}
[fixed] {
position: fixed;
top: 0;
left: 0;
z-index: 100;
height: 100%;
}
.loader {
width: 48px;
height: 48px;
border-radius: 50%;
display: inline-block;
position: relative;
border: 3px solid;
border-color: #fff #fff transparent transparent;
box-sizing: border-box;
animation: rotation 1s linear infinite;
}
.loader::after,
.loader::before {
content: "";
box-sizing: border-box;
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
margin: auto;
border: 3px solid;
border-color: transparent transparent var(--primary-blue) var(--primary-blue);
width: 40px;
height: 40px;
border-radius: 50%;
box-sizing: border-box;
animation: rotationBack 0.5s linear infinite;
transform-origin: center center;
}
.loader::before {
width: 32px;
height: 32px;
border-color: #fff #fff transparent transparent;
animation: rotation 1.5s linear infinite;
}
@keyframes rotation {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
@keyframes rotationBack {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(-360deg);
}
}
@keyframes rotate {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
@keyframes prixClipFix {
0% {
clip-path: polygon(50% 50%, 0 0, 0 0, 0 0, 0 0, 0 0);
}
50% {
clip-path: polygon(50% 50%, 0 0, 100% 0, 100% 0, 100% 0, 100% 0);
}
75%,
100% {
clip-path: polygon(50% 50%, 0 0, 100% 0, 100% 100%, 100% 100%, 100% 100%);
}
}
@keyframes boundAnimation {
0% {
background-position: 0 0;
}
100% {
background-position: 100% 0;
}
}
@keyframes notificationAnimationEnter {
0% {
transform: translateX(150%);
}
100% {
transform: translateX(0%);
}
}
@keyframes notificationAnimationExit {
0% {
transform: translateX(0%);
}
100% {
transform: translateX(150%);
}
}
@keyframes fadeAnim {
0% {
opacity: 0%;
}
100% {
opacity: 100%;
}
}
@keyframes underlineAnim {
0% {
width: 0%;
}
100% {
width: 100%;
}
}
@keyframes slide {
0% {
transform: translateX(-100%);
}
100% {
transform: translateX(0%);
}
}
@keyframes slideRight {
0% {
transform: translateX(100%);
}
100% {
transform: translateX(0%);
}
}
@keyframes slideUp {
0% {
transform: translateY(100%);
}
100% {
transform: translateY(0%);
}
}
@keyframes slideDown {
0% {
transform: translateY(-100%);
}
100% {
transform: translateY(0%);
}
}
@media screen and (max-width: 1000px) {
.dp-container {
h1 {
text-align: center;
font-size: 1.5em;
}
}
.custom-modal-html {
height: 60vh;
.actions {
flex-direction: column;
gap: 0.5em !important;
}
}
}

View File

@@ -1,11 +1,55 @@
<template> <template>
<div class="home"> <Layout @click="handleClick">
<img alt="Vue logo" src="../assets/logo.png"> <Table :headers :items="productos"></Table>
<HelloWorld msg="Welcome to Your Vue.js App"/> <Filters :filters="headers"></Filters>
</div> <ProductModal v-if="productStore.product_modal" :headers @confirm="saveProduct"/>
</Layout>
</template> </template>
<script setup> <script setup>
import HelloWorld from '@/components/HelloWorld.vue' import Layout from "@/components/app/Layout.vue";
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 {useUserStore} from "@/pinia/user";
import ProductModal from "@/components/productos/ProductModal.vue";
import {useProductStore} from "@/pinia/products";
import {DBService, post} from "@/services/apiReq";
const ui = useUi();
const productStore = useProductStore()
const headers = computed(() => {
const producto = productStore.productos?.[0];
if (!producto) return [
{value: 'Nombre', name: 'Nombre'},
{value: 'Precio', name: 'Precio'},
{value: 'Stock', name: 'Stock'},
{value: 'Vendidos', name: 'Vendidos'},
{value: 'Acciones', name: 'Acciones'},
];
delete producto.id;
producto.acciones = null;
return Object.keys(producto).map((x) => {
return {value: x, name: x};
})
})
const productos = ref([
{Nombre: "Producto de prueba de longitud", Precio: "Precio", Stock: "Stock", Vendidos: 0, Acciones: 0},
]);
function handleClick() {
ui.setCurrentMenu(null)
}
async function saveProduct(product) {
let response = await post('/insert', {
table: 'productos',
data: product,
}, DBService);
productStore.product_modal = false;
await productStore.fetchProducts();
}
</script> </script>

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>