feat: mejorar UI con Vuetify 3, POS tipo venta real, dashboard
This commit is contained in:
Binary file not shown.
152
apps/toldo-app/package-lock.json
generated
152
apps/toldo-app/package-lock.json
generated
@@ -10,13 +10,15 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@capacitor/android": "^6.2.1",
|
"@capacitor/android": "^6.2.1",
|
||||||
"@capacitor/core": "^6.2.1",
|
"@capacitor/core": "^6.2.1",
|
||||||
|
"@mdi/font": "^7.4.47",
|
||||||
"@toldo/core": "file:../../packages/core",
|
"@toldo/core": "file:../../packages/core",
|
||||||
"@toldo/data": "file:../../packages/data",
|
"@toldo/data": "file:../../packages/data",
|
||||||
"@toldo/services": "file:../../packages/services",
|
"@toldo/services": "file:../../packages/services",
|
||||||
"@toldo/ui": "file:../../packages/ui",
|
"@toldo/ui": "file:../../packages/ui",
|
||||||
"dexie": "^4.0.10",
|
"dexie": "^4.0.10",
|
||||||
"vue": "^3.5.44",
|
"vue": "^3.5.44",
|
||||||
"vue-router": "file:vue-router-4.4.5.tgz"
|
"vue-router": "file:vue-router-4.4.5.tgz",
|
||||||
|
"vuetify": "^4.3.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@capacitor/cli": "^6.2.1",
|
"@capacitor/cli": "^6.2.1",
|
||||||
@@ -910,6 +912,11 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz",
|
||||||
"integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw=="
|
"integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw=="
|
||||||
},
|
},
|
||||||
|
"node_modules/@mdi/font": {
|
||||||
|
"version": "7.4.47",
|
||||||
|
"resolved": "https://registry.npmjs.org/@mdi/font/-/font-7.4.47.tgz",
|
||||||
|
"integrity": "sha512-43MtGpd585SNzHZPcYowu/84Vz2a2g31TvPMTm9uTiCSWzaheQySUcSyUH/46fPnuPQWof2yd0pGBtzee/IQWw=="
|
||||||
|
},
|
||||||
"node_modules/@napi-rs/lzma-linux-x64-gnu": {
|
"node_modules/@napi-rs/lzma-linux-x64-gnu": {
|
||||||
"version": "1.5.1",
|
"version": "1.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz",
|
||||||
@@ -1791,6 +1798,26 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz",
|
||||||
"integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g=="
|
"integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g=="
|
||||||
},
|
},
|
||||||
|
"node_modules/@vue/devtools-kit": {
|
||||||
|
"version": "8.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-8.2.1.tgz",
|
||||||
|
"integrity": "sha512-FIGIuq3AWReEpbAHY/cRGeHDfI0qOb8OCQ3YjbEAX04uaxIDbGc9rhkbVcG7rnfHPXE3RsU5KrWOu9V/okd8AQ==",
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@vue/devtools-shared": "^8.2.1",
|
||||||
|
"birpc": "^2.6.1",
|
||||||
|
"hookable": "^5.5.3",
|
||||||
|
"perfect-debounce": "^2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vue/devtools-shared": {
|
||||||
|
"version": "8.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-8.2.1.tgz",
|
||||||
|
"integrity": "sha512-Fkac7lUdGReh6pVOi3AYPRGe82LQqRmAfThW7RRligOAP0ZA/Z1z9XLHDM9dv34pV2HRc79DK8uKPeG2fLnA/g==",
|
||||||
|
"optional": true,
|
||||||
|
"peer": true
|
||||||
|
},
|
||||||
"node_modules/@vue/language-core": {
|
"node_modules/@vue/language-core": {
|
||||||
"version": "3.3.13",
|
"version": "3.3.13",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-3.3.13.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-3.3.13.tgz",
|
||||||
@@ -2055,6 +2082,16 @@
|
|||||||
"node": ">=0.6"
|
"node": ">=0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/birpc": {
|
||||||
|
"version": "2.9.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/birpc/-/birpc-2.9.0.tgz",
|
||||||
|
"integrity": "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==",
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/antfu"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/bplist-parser": {
|
"node_modules/bplist-parser": {
|
||||||
"version": "0.3.2",
|
"version": "0.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.3.2.tgz",
|
||||||
@@ -2915,6 +2952,13 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/hookable": {
|
||||||
|
"version": "5.5.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz",
|
||||||
|
"integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==",
|
||||||
|
"optional": true,
|
||||||
|
"peer": true
|
||||||
|
},
|
||||||
"node_modules/html-encoding-sniffer": {
|
"node_modules/html-encoding-sniffer": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz",
|
||||||
@@ -3971,6 +4015,13 @@
|
|||||||
"integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==",
|
"integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/perfect-debounce": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==",
|
||||||
|
"optional": true,
|
||||||
|
"peer": true
|
||||||
|
},
|
||||||
"node_modules/picocolors": {
|
"node_modules/picocolors": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||||
@@ -6042,6 +6093,105 @@
|
|||||||
"typescript": ">=5.0.0"
|
"typescript": ">=5.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/vuetify": {
|
||||||
|
"version": "4.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/vuetify/-/vuetify-4.3.0.tgz",
|
||||||
|
"integrity": "sha512-tQpJ/2oKCNeZQb7p3PpwGVca4B9IFUp4LuTpE/bOZ/lIC4U74EJxtriMdB8xj0wtZx40PFGbHPrdfJzZg1FX0A==",
|
||||||
|
"dependencies": {
|
||||||
|
"@vuetify/v0": "^1.2.3"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/johnleider"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"typescript": ">=4.7",
|
||||||
|
"vite-plugin-vuetify": ">=2.1.0",
|
||||||
|
"vue": "^3.5.0 || ^3.6.0-0",
|
||||||
|
"webpack-plugin-vuetify": ">=3.1.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"typescript": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"vite-plugin-vuetify": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"webpack-plugin-vuetify": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/vuetify/node_modules/@vue/devtools-api": {
|
||||||
|
"version": "8.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-8.2.1.tgz",
|
||||||
|
"integrity": "sha512-6u4vXBlIBAC1wMplIZgpyPn7uh/s4Bf6F5bMzvLv+EdJ0aHs/+4B7Ygv864EStQSjRbsRzTko/kUG1A1IejQ3A==",
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@vue/devtools-kit": "^8.2.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/vuetify/node_modules/@vuetify/v0": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vuetify/v0/-/v0-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-Tvknc64ciwaOdKzifg3maMOiczbEenWxYINoZIotfKQ8VWBs/yeHbvHffW4w1j+xYnNyYi0S9LK5UCzyQrYg9A==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/johnleider"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/vuetify"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"peerDependencies": {
|
||||||
|
"@adobe/leonardo-contrast-colors": ">=1.0.0",
|
||||||
|
"@ant-design/colors": ">=7.0.0",
|
||||||
|
"@flagsmith/flagsmith": ">=11.0.0",
|
||||||
|
"@floating-ui/dom": ">=1.8.0",
|
||||||
|
"@js-temporal/polyfill": ">=0.5.0",
|
||||||
|
"@material/material-color-utilities": ">=0.3.0",
|
||||||
|
"@vue/devtools-api": ">=7.0.0",
|
||||||
|
"launchdarkly-js-client-sdk": "^3.0.0",
|
||||||
|
"posthog-js": "^1.0.0",
|
||||||
|
"vue": ">=3.5.0 || >=3.6.0-0",
|
||||||
|
"vue-i18n": ">=10.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@adobe/leonardo-contrast-colors": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@ant-design/colors": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@flagsmith/flagsmith": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@floating-ui/dom": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@js-temporal/polyfill": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@material/material-color-utilities": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@vue/devtools-api": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"launchdarkly-js-client-sdk": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"posthog-js": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"vue-i18n": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/w3c-xmlserializer": {
|
"node_modules/w3c-xmlserializer": {
|
||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
|
||||||
|
|||||||
@@ -13,13 +13,15 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@capacitor/android": "^6.2.1",
|
"@capacitor/android": "^6.2.1",
|
||||||
"@capacitor/core": "^6.2.1",
|
"@capacitor/core": "^6.2.1",
|
||||||
|
"@mdi/font": "^7.4.47",
|
||||||
"@toldo/core": "file:../../packages/core",
|
"@toldo/core": "file:../../packages/core",
|
||||||
"@toldo/data": "file:../../packages/data",
|
"@toldo/data": "file:../../packages/data",
|
||||||
"@toldo/services": "file:../../packages/services",
|
"@toldo/services": "file:../../packages/services",
|
||||||
"@toldo/ui": "file:../../packages/ui",
|
"@toldo/ui": "file:../../packages/ui",
|
||||||
"dexie": "^4.0.10",
|
"dexie": "^4.0.10",
|
||||||
"vue": "^3.5.44",
|
"vue": "^3.5.44",
|
||||||
"vue-router": "file:vue-router-4.4.5.tgz"
|
"vue-router": "file:vue-router-4.4.5.tgz",
|
||||||
|
"vuetify": "^4.3.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@capacitor/cli": "^6.2.1",
|
"@capacitor/cli": "^6.2.1",
|
||||||
|
|||||||
@@ -1,29 +1,27 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="layout">
|
<v-app>
|
||||||
<header>
|
<v-app-bar color="primary" density="compact">
|
||||||
<h1>ToldoConjunto - CESTA</h1>
|
<v-app-bar-title>ToldoConjunto - CESTA</v-app-bar-title>
|
||||||
<nav>
|
<v-spacer></v-spacer>
|
||||||
<router-link to="/">Inicio</router-link>
|
</v-app-bar>
|
||||||
<router-link to="/inventario">Inventario</router-link>
|
|
||||||
<router-link to="/ventas">Ventas</router-link>
|
|
||||||
<router-link to="/gastos">Gastos</router-link>
|
|
||||||
<router-link to="/cuadre">Cuadre</router-link>
|
|
||||||
<router-link to="/reportes">Reportes</router-link>
|
|
||||||
<router-link to="/ajustes">Ajustes</router-link>
|
|
||||||
<router-link to="/exportar">Exportar/Importar</router-link>
|
|
||||||
</nav>
|
|
||||||
</header>
|
|
||||||
<main>
|
|
||||||
<router-view />
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
<v-navigation-drawer permanent>
|
||||||
.layout { font-family: system-ui, sans-serif; }
|
<v-list density="compact">
|
||||||
header { background: #2d5f3b; color: white; padding: 1rem; }
|
<v-list-item to="/" prepend-icon="mdi-home" title="Inicio"></v-list-item>
|
||||||
nav { display: flex; gap: 1rem; flex-wrap: wrap; margin-top: 0.5rem; }
|
<v-list-item to="/inventario" prepend-icon="mdi-package-variant" title="Inventario"></v-list-item>
|
||||||
nav a { color: white; text-decoration: none; }
|
<v-list-item to="/ventas" prepend-icon="mdi-cart" title="Ventas (POS)"></v-list-item>
|
||||||
nav a.router-link-active { text-decoration: underline; }
|
<v-list-item to="/gastos" prepend-icon="mdi-cash-minus" title="Gastos"></v-list-item>
|
||||||
main { padding: 1rem; }
|
<v-list-item to="/cuadre" prepend-icon="mdi-cash-register" title="Cierre de Caja"></v-list-item>
|
||||||
</style>
|
<v-list-item to="/ajustes" prepend-icon="mdi-pencil" title="Ajustes"></v-list-item>
|
||||||
|
<v-list-item to="/reportes" prepend-icon="mdi-chart-bar" title="Reportes"></v-list-item>
|
||||||
|
<v-list-item to="/exportar" prepend-icon="mdi-export" title="Exportar/Importar"></v-list-item>
|
||||||
|
</v-list>
|
||||||
|
</v-navigation-drawer>
|
||||||
|
|
||||||
|
<v-main>
|
||||||
|
<v-container fluid>
|
||||||
|
<router-view />
|
||||||
|
</v-container>
|
||||||
|
</v-main>
|
||||||
|
</v-app>
|
||||||
|
</template>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { createApp } from 'vue'
|
import { createApp } from 'vue'
|
||||||
import App from './App.vue'
|
import App from './App.vue'
|
||||||
import router from './router'
|
import router from './router'
|
||||||
|
import vuetify from './plugins/vuetify'
|
||||||
|
|
||||||
createApp(App).use(router).mount('#app')
|
createApp(App).use(router).use(vuetify).mount('#app')
|
||||||
|
|||||||
21
apps/toldo-app/src/plugins/vuetify.ts
Normal file
21
apps/toldo-app/src/plugins/vuetify.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import 'vuetify/styles'
|
||||||
|
import { createVuetify } from 'vuetify'
|
||||||
|
import * as components from 'vuetify/components'
|
||||||
|
import * as directives from 'vuetify/directives'
|
||||||
|
|
||||||
|
export default createVuetify({
|
||||||
|
components,
|
||||||
|
directives,
|
||||||
|
theme: {
|
||||||
|
defaultTheme: 'light',
|
||||||
|
themes: {
|
||||||
|
light: {
|
||||||
|
colors: {
|
||||||
|
primary: '#2d5f3b',
|
||||||
|
secondary: '#4a7c59',
|
||||||
|
accent: '#6b8e23',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -1,7 +1,73 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<h2>Inicio</h2>
|
<h1 class="text-h4 mb-4">Panel de Control</h1>
|
||||||
<p>Sistema de gestión para el Toldo de CESTA</p>
|
<v-row>
|
||||||
<p>Funciona offline - Datos guardados localmente</p>
|
<v-col cols="12" md="3">
|
||||||
|
<v-card>
|
||||||
|
<v-card-text>
|
||||||
|
<div class="text-h6">Ventas del día</div>
|
||||||
|
<div class="text-h4">${{ totalVentas }}</div>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="3">
|
||||||
|
<v-card>
|
||||||
|
<v-card-text>
|
||||||
|
<div class="text-h6">Gastos del día</div>
|
||||||
|
<div class="text-h4">${{ totalGastos }}</div>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="3">
|
||||||
|
<v-card>
|
||||||
|
<v-card-text>
|
||||||
|
<div class="text-h6">Productos</div>
|
||||||
|
<div class="text-h4">{{ productos.length }}</div>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="3">
|
||||||
|
<v-card>
|
||||||
|
<v-card-text>
|
||||||
|
<div class="text-h6">Productores</div>
|
||||||
|
<div class="text-h4">{{ productores.length }}</div>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
<v-row class="mt-4">
|
||||||
|
<v-col>
|
||||||
|
<v-card>
|
||||||
|
<v-card-title>Acciones Rápidas</v-card-title>
|
||||||
|
<v-card-text>
|
||||||
|
<v-btn to="/ventas" color="primary" class="mr-2">Nueva Venta</v-btn>
|
||||||
|
<v-btn to="/gastos" color="secondary" class="mr-2">Registrar Gasto</v-btn>
|
||||||
|
<v-btn to="/inventario" color="accent">Gestionar Inventario</v-btn>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted } from 'vue';
|
||||||
|
import { db } from '@toldo/data';
|
||||||
|
import { VentasService, GastosService } from '@toldo/services';
|
||||||
|
|
||||||
|
const ventasService = new VentasService();
|
||||||
|
const gastosService = new GastosService();
|
||||||
|
const totalVentas = ref(0);
|
||||||
|
const totalGastos = ref(0);
|
||||||
|
const productos = ref<any[]>([]);
|
||||||
|
const productores = ref<any[]>([]);
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
productos.value = await db.productos.toArray();
|
||||||
|
productores.value = await db.productores.toArray();
|
||||||
|
const ventas = await ventasService.getVentasDelDia(new Date());
|
||||||
|
const gastos = await gastosService.getGastosDelDia(new Date());
|
||||||
|
totalVentas.value = ventas.reduce((acc, v) => acc + v.total, 0);
|
||||||
|
totalGastos.value = gastos.reduce((acc, g) => acc + g.monto, 0);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -1,50 +1,67 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<h2>Inventario</h2>
|
<h1 class="text-h4 mb-4">Inventario y Productos</h1>
|
||||||
|
|
||||||
<section>
|
<v-row>
|
||||||
<h3>Productores</h3>
|
<v-col cols="12" md="6">
|
||||||
<input v-model="nuevoProductor" placeholder="Nombre del productor" />
|
<v-card class="mb-4">
|
||||||
<button @click="crearProductor">Agregar Productor</button>
|
<v-card-title>Productores</v-card-title>
|
||||||
<ul>
|
<v-card-text>
|
||||||
<li v-for="p in productores" :key="p.id">{{ p.nombre }}</li>
|
<v-row>
|
||||||
</ul>
|
<v-col cols="8">
|
||||||
</section>
|
<v-text-field v-model="nuevoProductor" label="Nombre del productor" density="compact"></v-text-field>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="4">
|
||||||
|
<v-btn @click="crearProductor" color="primary" block>Agregar</v-btn>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
<v-list>
|
||||||
|
<v-list-item v-for="p in productores" :key="p.id" :title="p.nombre"></v-list-item>
|
||||||
|
</v-list>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
|
||||||
<section>
|
<v-col cols="12" md="6">
|
||||||
<h3>Productos (por productor)</h3>
|
<v-card class="mb-4">
|
||||||
<select v-model="productorSel">
|
<v-card-title>Productos</v-card-title>
|
||||||
<option value="">Seleccionar productor</option>
|
<v-card-text>
|
||||||
<option v-for="p in productores" :key="p.id" :value="p.id">{{ p.nombre }}</option>
|
<v-select v-model="productorSel" :items="productores" item-title="nombre" item-value="id" label="Productor" density="compact" class="mb-2"></v-select>
|
||||||
</select>
|
<v-text-field v-model="nombreProd" label="Nombre/Presentación (ej: Café molido 250gr)" density="compact" class="mb-2"></v-text-field>
|
||||||
<input v-model="nombreProd" placeholder="Ej: Café molido 250gr" />
|
<v-text-field v-model.number="precioProd" label="Precio unitario" type="number" density="compact" class="mb-2"></v-text-field>
|
||||||
<input v-model.number="precioProd" type="number" placeholder="Precio unitario" />
|
<v-btn @click="crearProducto" color="primary" block>Agregar Producto</v-btn>
|
||||||
<button @click="crearProducto">Agregar Producto</button>
|
<v-list class="mt-3">
|
||||||
<ul>
|
<v-list-item v-for="prod in productos" :key="prod.id" :title="prod.nombrePresentacion" :subtitle="'$' + prod.precioUnitario"></v-list-item>
|
||||||
<li v-for="prod in productos" :key="prod.id">{{ prod.nombrePresentacion }} - ${{ prod.precioUnitario }}</li>
|
</v-list>
|
||||||
</ul>
|
</v-card-text>
|
||||||
</section>
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
<section>
|
<v-row>
|
||||||
<h3>Ingreso Inicial</h3>
|
<v-col cols="12" md="6">
|
||||||
<select v-model="prodInv">
|
<v-card>
|
||||||
<option value="">Seleccionar producto</option>
|
<v-card-title>Ingreso Inicial del Día</v-card-title>
|
||||||
<option v-for="prod in productos" :key="prod.id" :value="prod.id">{{ prod.nombrePresentacion }}</option>
|
<v-card-text>
|
||||||
</select>
|
<v-select v-model="prodInv" :items="productos" item-title="nombrePresentacion" item-value="id" label="Producto" density="compact" class="mb-2"></v-select>
|
||||||
<input v-model.number="cantInv" type="number" placeholder="Cantidad" />
|
<v-text-field v-model.number="cantInv" label="Cantidad" type="number" density="compact" class="mb-2"></v-text-field>
|
||||||
<button @click="ingresoInicial">Registrar Ingreso Inicial</button>
|
<v-btn @click="ingresoInicial" color="success" block>Registrar Ingreso Inicial</v-btn>
|
||||||
</section>
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
|
||||||
<section>
|
<v-col cols="12" md="6">
|
||||||
<h3>Ingreso entre semana</h3>
|
<v-card>
|
||||||
<select v-model="prodInv2">
|
<v-card-title>Ingreso entre Semana</v-card-title>
|
||||||
<option value="">Seleccionar producto</option>
|
<v-card-text>
|
||||||
<option v-for="prod in productos" :key="prod.id" :value="prod.id">{{ prod.nombrePresentacion }}</option>
|
<v-select v-model="prodInv2" :items="productos" item-title="nombrePresentacion" item-value="id" label="Producto" density="compact" class="mb-2"></v-select>
|
||||||
</select>
|
<v-text-field v-model.number="cantInv2" label="Cantidad" type="number" density="compact" class="mb-2"></v-text-field>
|
||||||
<input v-model.number="cantInv2" type="number" placeholder="Cantidad" />
|
<v-text-field v-model="recibidoPor" label="Recibido por" density="compact" class="mb-2"></v-text-field>
|
||||||
<input v-model="recibidoPor" placeholder="Recibido por" />
|
<v-btn @click="ingresoSemana" color="info" block>Registrar Ingreso</v-btn>
|
||||||
<button @click="ingresoSemana">Registrar Ingreso</button>
|
</v-card-text>
|
||||||
</section>
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -73,14 +90,14 @@ async function cargar() {
|
|||||||
|
|
||||||
async function crearProductor() {
|
async function crearProductor() {
|
||||||
if (!nuevoProductor.value) return;
|
if (!nuevoProductor.value) return;
|
||||||
await db.productores.add({ id: crypto.randomUUID(), nombre: nuevoProductor.value, activo: true, createdAt: new Date() });
|
await db.productores.add({ id: crypto.randomUUID(), nombre: nuevoProductor.value, activo: true, createdAt: new Date() } as any);
|
||||||
nuevoProductor.value = '';
|
nuevoProductor.value = '';
|
||||||
await cargar();
|
await cargar();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function crearProducto() {
|
async function crearProducto() {
|
||||||
if (!productorSel.value || !nombreProd.value) return;
|
if (!productorSel.value || !nombreProd.value) return;
|
||||||
await db.productos.add({ id: crypto.randomUUID(), productorId: productorSel.value, nombrePresentacion: nombreProd.value, precioUnitario: precioProd.value, activo: true, createdAt: new Date() });
|
await db.productos.add({ id: crypto.randomUUID(), productorId: productorSel.value, nombrePresentacion: nombreProd.value, precioUnitario: Number(precioProd.value), activo: true, createdAt: new Date() } as any);
|
||||||
nombreProd.value = '';
|
nombreProd.value = '';
|
||||||
precioProd.value = 0;
|
precioProd.value = 0;
|
||||||
await cargar();
|
await cargar();
|
||||||
@@ -88,14 +105,14 @@ async function crearProducto() {
|
|||||||
|
|
||||||
async function ingresoInicial() {
|
async function ingresoInicial() {
|
||||||
if (!prodInv.value || cantInv.value <= 0) return;
|
if (!prodInv.value || cantInv.value <= 0) return;
|
||||||
await invService.registrarIngresoInicial(prodInv.value, cantInv.value, new Date());
|
await invService.registrarIngresoInicial(prodInv.value, Number(cantInv.value), new Date());
|
||||||
cantInv.value = 0;
|
cantInv.value = 0;
|
||||||
alert('Ingreso inicial registrado');
|
alert('Ingreso inicial registrado');
|
||||||
}
|
}
|
||||||
|
|
||||||
async function ingresoSemana() {
|
async function ingresoSemana() {
|
||||||
if (!prodInv2.value || cantInv2.value <= 0 || !recibidoPor.value) return;
|
if (!prodInv2.value || cantInv2.value <= 0 || !recibidoPor.value) return;
|
||||||
await invService.registrarIngresoSemana(prodInv2.value, cantInv2.value, new Date(), recibidoPor.value);
|
await invService.registrarIngresoSemana(prodInv2.value, Number(cantInv2.value), new Date(), recibidoPor.value);
|
||||||
cantInv2.value = 0;
|
cantInv2.value = 0;
|
||||||
recibidoPor.value = '';
|
recibidoPor.value = '';
|
||||||
alert('Ingreso entre semana registrado');
|
alert('Ingreso entre semana registrado');
|
||||||
|
|||||||
@@ -1,38 +1,54 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<h2>Ventas</h2>
|
<h1 class="text-h4 mb-4">Punto de Venta (POS)</h1>
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12" md="8">
|
||||||
|
<v-card>
|
||||||
|
<v-card-title>Productos Disponibles</v-card-title>
|
||||||
|
<v-card-text>
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12" sm="6" md="4" v-for="prod in productos" :key="prod.id">
|
||||||
|
<v-card @click="agregarProducto(prod)" class="cursor-pointer" hover>
|
||||||
|
<v-card-text>
|
||||||
|
<div class="text-subtitle-1 font-weight-bold">{{ prod.nombrePresentacion }}</div>
|
||||||
|
<div class="text-h6 text-primary">${{ prod.precioUnitario }}</div>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
|
||||||
<section>
|
<v-col cols="12" md="4">
|
||||||
<h3>Nueva Venta</h3>
|
<v-card>
|
||||||
<select v-model="productoSel">
|
<v-card-title>Carrito de Compras</v-card-title>
|
||||||
<option value="">Seleccionar producto</option>
|
<v-card-text>
|
||||||
<option v-for="prod in productos" :key="prod.id" :value="prod.id">{{ prod.nombrePresentacion }} - ${{ prod.precioUnitario }}</option>
|
<v-list v-if="lineas.length">
|
||||||
</select>
|
<v-list-item v-for="(l, i) in lineas" :key="i">
|
||||||
<input v-model.number="cantidad" type="number" placeholder="Cantidad" />
|
<v-list-item-title>{{ getProdNombre(l.productoId) }}</v-list-item-title>
|
||||||
<select v-model="formaPago">
|
<v-list-item-subtitle>
|
||||||
<option value="EFECTIVO">Efectivo</option>
|
<v-row align="center" class="mt-1">
|
||||||
<option value="QR">QR</option>
|
<v-col cols="4">
|
||||||
</select>
|
<v-text-field v-model.number="l.cantidad" type="number" density="compact" hide-details @update:model-value="actualizarLinea(i)"></v-text-field>
|
||||||
<button @click="agregarLinea">Agregar Línea</button>
|
</v-col>
|
||||||
</section>
|
<v-col cols="4">${{ l.precioUnitario }}</v-col>
|
||||||
|
<v-col cols="4">${{ l.subtotal }}</v-col>
|
||||||
<section>
|
</v-row>
|
||||||
<h3>Carrito</h3>
|
</v-list-item-subtitle>
|
||||||
<table v-if="lineas.length">
|
<template v-slot:append>
|
||||||
<thead><tr><th>Producto</th><th>Cant</th><th>Precio</th><th>Subtotal</th><th>Forma</th></tr></thead>
|
<v-btn icon="mdi-delete" size="small" @click="eliminarLinea(i)"></v-btn>
|
||||||
<tbody>
|
</template>
|
||||||
<tr v-for="(l, i) in lineas" :key="i">
|
</v-list-item>
|
||||||
<td>{{ getProdNombre(l.productoId) }}</td>
|
</v-list>
|
||||||
<td>{{ l.cantidad }}</td>
|
<v-divider class="my-3"></v-divider>
|
||||||
<td>${{ l.precioUnitario }}</td>
|
<div class="text-h5">Total: ${{ total }}</div>
|
||||||
<td>${{ l.subtotal }}</td>
|
<v-select v-model="formaPago" :items="['EFECTIVO', 'QR']" label="Forma de Pago" class="mt-3"></v-select>
|
||||||
<td>{{ l.formaPago }}</td>
|
<v-btn @click="registrarVenta" color="success" block size="large" :disabled="lineas.length === 0" class="mt-2">COBRAR</v-btn>
|
||||||
</tr>
|
</v-card-text>
|
||||||
</tbody>
|
</v-card>
|
||||||
</table>
|
</v-col>
|
||||||
<p v-if="lineas.length"><strong>Total: ${{ total }}</strong></p>
|
</v-row>
|
||||||
<button v-if="lineas.length" @click="registrarVenta">Registrar Venta</button>
|
|
||||||
</section>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -43,10 +59,8 @@ import { VentasService } from '@toldo/services';
|
|||||||
|
|
||||||
const ventasService = new VentasService();
|
const ventasService = new VentasService();
|
||||||
const productos = ref<any[]>([]);
|
const productos = ref<any[]>([]);
|
||||||
const productoSel = ref('');
|
|
||||||
const cantidad = ref(1);
|
|
||||||
const formaPago = ref('EFECTIVO');
|
|
||||||
const lineas = ref<any[]>([]);
|
const lineas = ref<any[]>([]);
|
||||||
|
const formaPago = ref('EFECTIVO');
|
||||||
|
|
||||||
async function cargar() {
|
async function cargar() {
|
||||||
productos.value = await db.productos.toArray();
|
productos.value = await db.productos.toArray();
|
||||||
@@ -56,39 +70,49 @@ function getProdNombre(id: string) {
|
|||||||
return productos.value.find(p => p.id === id)?.nombrePresentacion || id;
|
return productos.value.find(p => p.id === id)?.nombrePresentacion || id;
|
||||||
}
|
}
|
||||||
|
|
||||||
function agregarLinea() {
|
function agregarProducto(prod: any) {
|
||||||
if (!productoSel.value || cantidad.value <= 0) return;
|
const existente = lineas.value.findIndex(l => l.productoId === prod.id);
|
||||||
const prod = productos.value.find(p => p.id === productoSel.value);
|
if (existente >= 0) {
|
||||||
if (!prod) return;
|
lineas.value[existente].cantidad++;
|
||||||
|
lineas.value[existente].subtotal = lineas.value[existente].cantidad * lineas.value[existente].precioUnitario;
|
||||||
|
} else {
|
||||||
lineas.value.push({
|
lineas.value.push({
|
||||||
id: crypto.randomUUID(),
|
id: crypto.randomUUID(),
|
||||||
ventaId: '',
|
ventaId: '',
|
||||||
productoId: prod.id,
|
productoId: prod.id,
|
||||||
cantidad: cantidad.value,
|
cantidad: 1,
|
||||||
precioUnitario: prod.precioUnitario,
|
precioUnitario: prod.precioUnitario,
|
||||||
subtotal: prod.precioUnitario * cantidad.value,
|
subtotal: prod.precioUnitario,
|
||||||
formaPago: formaPago.value
|
formaPago: formaPago.value
|
||||||
});
|
});
|
||||||
productoSel.value = '';
|
}
|
||||||
cantidad.value = 1;
|
}
|
||||||
|
|
||||||
|
function actualizarLinea(i: number) {
|
||||||
|
lineas.value[i].subtotal = lineas.value[i].cantidad * lineas.value[i].precioUnitario;
|
||||||
|
}
|
||||||
|
|
||||||
|
function eliminarLinea(i: number) {
|
||||||
|
lineas.value.splice(i, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const total = computed(() => lineas.value.reduce((acc, l) => acc + l.subtotal, 0));
|
const total = computed(() => lineas.value.reduce((acc, l) => acc + l.subtotal, 0));
|
||||||
|
|
||||||
async function registrarVenta() {
|
async function registrarVenta() {
|
||||||
|
if (lineas.value.length === 0) return;
|
||||||
const ventaId = crypto.randomUUID();
|
const ventaId = crypto.randomUUID();
|
||||||
|
const lineasVenta = lineas.value.map(l => ({ ...l, ventaId, formaPago: formaPago.value }));
|
||||||
const venta = {
|
const venta = {
|
||||||
id: ventaId,
|
id: ventaId,
|
||||||
fecha: new Date(),
|
fecha: new Date(),
|
||||||
lineas: lineas.value.map(l => ({ ...l, ventaId })),
|
lineas: lineasVenta,
|
||||||
subtotal: total.value,
|
subtotal: total.value,
|
||||||
total: total.value,
|
total: total.value,
|
||||||
createdAt: new Date()
|
createdAt: new Date()
|
||||||
};
|
};
|
||||||
const lineasVenta = lineas.value.map(l => ({ ...l, ventaId }));
|
|
||||||
await ventasService.registrarVenta(venta, lineasVenta);
|
await ventasService.registrarVenta(venta, lineasVenta);
|
||||||
lineas.value = [];
|
lineas.value = [];
|
||||||
alert('Venta registrada');
|
alert(`Venta registrada por $${total.value} - ${formaPago.value}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(cargar);
|
onMounted(cargar);
|
||||||
|
|||||||
20
openspec/changes/improve-ui-ux-pos/design.md
Normal file
20
openspec/changes/improve-ui-ux-pos/design.md
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# Design: improve-ui-ux-pos
|
||||||
|
|
||||||
|
## UI Library
|
||||||
|
Use Vuetify 3 with Material Design 3, responsive for tablet.
|
||||||
|
|
||||||
|
## POS Flow
|
||||||
|
- Product grid by category/producer
|
||||||
|
- Cart with quantities
|
||||||
|
- Payment method selection
|
||||||
|
- Quick sale completion
|
||||||
|
|
||||||
|
## UX Improvements
|
||||||
|
- Dashboard with day summary
|
||||||
|
- Guided setup on first run
|
||||||
|
- Data tables for all entities
|
||||||
|
- Better forms with validation
|
||||||
|
- Consistent theming
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
Playwright tests for web UI covering realistic scenarios
|
||||||
24
openspec/changes/improve-ui-ux-pos/proposal.md
Normal file
24
openspec/changes/improve-ui-ux-pos/proposal.md
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# Proposal: improve-ui-ux-pos
|
||||||
|
|
||||||
|
## Why
|
||||||
|
Current UI is too simple/minimalist, lacks styling, and the POS flow (create sale) is not intuitive. Users need a realistic POS experience with:
|
||||||
|
- Proper product catalog by producer
|
||||||
|
- Easy product selection for sales (like buying 3 lbs coffee, 2 condiments)
|
||||||
|
- Clear day closure/cash reconciliation flow
|
||||||
|
- Better UX with a UI component library
|
||||||
|
- Seed data/onboarding to create producers/products easily
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
- Integrate Vuetify 3 for professional UI, themes, components
|
||||||
|
- Redesign POS/sales screen like real POS systems
|
||||||
|
- Improve product/producer management with better forms
|
||||||
|
- Add quick start wizard (setup producers/products)
|
||||||
|
- Improve cash closure flow with detailed breakdown
|
||||||
|
- Add realistic test scenarios
|
||||||
|
- Enhance all views with proper layouts
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
- apps/toldo-app: UI overhaul with Vuetify
|
||||||
|
- Better UX for tablet use
|
||||||
|
- More realistic workflows
|
||||||
|
- Maintain offline-first, Capacitor compatible
|
||||||
29
openspec/changes/improve-ui-ux-pos/tasks.md
Normal file
29
openspec/changes/improve-ui-ux-pos/tasks.md
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
# Tasks
|
||||||
|
|
||||||
|
## 1. Research & Setup
|
||||||
|
- [ ] 1.1 Research POS UI patterns
|
||||||
|
- [ ] 1.2 Install Vuetify 3, mdi icons
|
||||||
|
- [ ] 1.3 Configure Vuetify theme
|
||||||
|
|
||||||
|
## 2. UI Overhaul
|
||||||
|
- [ ] 2.1 Redesign Layout with Vuetify (app bar, navigation)
|
||||||
|
- [ ] 2.2 Redesign Dashboard with day summary
|
||||||
|
- [ ] 2.3 Redesign Inventory/Producers/Products with data tables
|
||||||
|
- [ ] 2.4 Redesign POS Sales screen (product grid + cart)
|
||||||
|
- [ ] 2.5 Redesign Expenses, Adjustments
|
||||||
|
- [ ] 2.6 Redesign Cash Closure with detailed breakdown
|
||||||
|
- [ ] 2.7 Redesign Reports
|
||||||
|
- [ ] 2.8 Redesign Export/Import
|
||||||
|
|
||||||
|
## 3. UX Features
|
||||||
|
- [ ] 3.1 Add setup wizard for first run
|
||||||
|
- [ ] 3.2 Add quick product creation
|
||||||
|
- [ ] 3.3 Improve validation
|
||||||
|
|
||||||
|
## 4. Testing
|
||||||
|
- [ ] 4.1 Setup Playwright
|
||||||
|
- [ ] 4.2 Create realistic test scenarios
|
||||||
|
- [ ] 4.3 Test full day flow
|
||||||
|
|
||||||
|
## 5. Build & Deploy
|
||||||
|
- [ ] 5.1 Build web and APK
|
||||||
Reference in New Issue
Block a user