diff --git a/package-lock.json b/package-lock.json
index 6a950ab..2069d37 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,17 +1,18 @@
{
- "name": "notsoweb.frontend",
+ "name": "pdv.frontend",
"version": "0.9.10",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
- "name": "notsoweb.frontend",
+ "name": "pdv.frontend",
"version": "0.9.10",
"dependencies": {
"@tailwindcss/postcss": "^4.0.9",
"@tailwindcss/vite": "^4.0.9",
"@vitejs/plugin-vue": "^5.2.1",
"axios": "^1.8.1",
+ "jspdf": "^3.0.4",
"laravel-echo": "^2.0.2",
"luxon": "^3.5.0",
"pinia": "^3.0.1",
@@ -89,6 +90,15 @@
"node": ">=6.0.0"
}
},
+ "node_modules/@babel/runtime": {
+ "version": "7.28.4",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz",
+ "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
"node_modules/@babel/types": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.1.tgz",
@@ -1230,12 +1240,32 @@
"integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==",
"license": "MIT"
},
+ "node_modules/@types/pako": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/@types/pako/-/pako-2.0.4.tgz",
+ "integrity": "sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw==",
+ "license": "MIT"
+ },
"node_modules/@types/qs": {
"version": "6.14.0",
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz",
"integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==",
"license": "MIT"
},
+ "node_modules/@types/raf": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.3.tgz",
+ "integrity": "sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==",
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/@types/trusted-types": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
+ "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
+ "license": "MIT",
+ "optional": true
+ },
"node_modules/@vitejs/plugin-vue": {
"version": "5.2.4",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz",
@@ -1492,6 +1522,16 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/base64-arraybuffer": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
+ "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">= 0.6.0"
+ }
+ },
"node_modules/birpc": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/birpc/-/birpc-2.3.0.tgz",
@@ -1617,6 +1657,26 @@
],
"license": "CC-BY-4.0"
},
+ "node_modules/canvg": {
+ "version": "3.0.11",
+ "resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.11.tgz",
+ "integrity": "sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@babel/runtime": "^7.12.5",
+ "@types/raf": "^3.4.0",
+ "core-js": "^3.8.3",
+ "raf": "^3.4.1",
+ "regenerator-runtime": "^0.13.7",
+ "rgbcolor": "^1.0.1",
+ "stackblur-canvas": "^2.0.0",
+ "svg-pathdata": "^6.0.3"
+ },
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
"node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
@@ -1744,6 +1804,28 @@
"url": "https://github.com/sponsors/mesqueeb"
}
},
+ "node_modules/core-js": {
+ "version": "3.47.0",
+ "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.47.0.tgz",
+ "integrity": "sha512-c3Q2VVkGAUyupsjRnaNX6u8Dq2vAdzm9iuPj5FW0fRxzlxgq9Q39MDq10IvmQSpLgHQNyQzQmOo6bgGHmH3NNg==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/core-js"
+ }
+ },
+ "node_modules/css-line-break": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz",
+ "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "utrie": "^1.0.2"
+ }
+ },
"node_modules/css-select": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz",
@@ -1860,6 +1942,16 @@
"url": "https://github.com/fb55/domhandler?sponsor=1"
}
},
+ "node_modules/dompurify": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.1.tgz",
+ "integrity": "sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==",
+ "license": "(MPL-2.0 OR Apache-2.0)",
+ "optional": true,
+ "optionalDependencies": {
+ "@types/trusted-types": "^2.0.7"
+ }
+ },
"node_modules/domutils": {
"version": "2.8.0",
"resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz",
@@ -2111,6 +2203,17 @@
"node": ">=8.6.0"
}
},
+ "node_modules/fast-png": {
+ "version": "6.4.0",
+ "resolved": "https://registry.npmjs.org/fast-png/-/fast-png-6.4.0.tgz",
+ "integrity": "sha512-kAqZq1TlgBjZcLr5mcN6NP5Rv4V2f22z00c3g8vRrwkcqjerx7BEhPbOnWCPqaHUl2XWQBJQvOT/FQhdMT7X/Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/pako": "^2.0.3",
+ "iobuffer": "^5.3.2",
+ "pako": "^2.1.0"
+ }
+ },
"node_modules/fastq": {
"version": "1.19.1",
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz",
@@ -2135,6 +2238,12 @@
}
}
},
+ "node_modules/fflate": {
+ "version": "0.8.2",
+ "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz",
+ "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==",
+ "license": "MIT"
+ },
"node_modules/filelist": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz",
@@ -2423,6 +2532,26 @@
"node": ">=12"
}
},
+ "node_modules/html2canvas": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
+ "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "css-line-break": "^2.1.0",
+ "text-segmentation": "^1.0.3"
+ },
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
+ "node_modules/iobuffer": {
+ "version": "5.4.0",
+ "resolved": "https://registry.npmjs.org/iobuffer/-/iobuffer-5.4.0.tgz",
+ "integrity": "sha512-DRebOWuqDvxunfkNJAlc3IzWIPD5xVxwUNbHr7xKB8E6aLJxIPfNX3CoMJghcFjpv6RWQsrcJbghtEwSPoJqMA==",
+ "license": "MIT"
+ },
"node_modules/is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
@@ -2515,6 +2644,23 @@
"graceful-fs": "^4.1.6"
}
},
+ "node_modules/jspdf": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/jspdf/-/jspdf-3.0.4.tgz",
+ "integrity": "sha512-dc6oQ8y37rRcHn316s4ngz/nOjayLF/FFxBF4V9zamQKRqXxyiH1zagkCdktdWhtoQId5K20xt1lB90XzkB+hQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.28.4",
+ "fast-png": "^6.2.0",
+ "fflate": "^0.8.1"
+ },
+ "optionalDependencies": {
+ "canvg": "^3.0.11",
+ "core-js": "^3.6.0",
+ "dompurify": "^3.2.4",
+ "html2canvas": "^1.0.0-rc.5"
+ }
+ },
"node_modules/laravel-echo": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/laravel-echo/-/laravel-echo-2.1.4.tgz",
@@ -2983,6 +3129,12 @@
"url": "https://github.com/fb55/nth-check?sponsor=1"
}
},
+ "node_modules/pako": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz",
+ "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==",
+ "license": "(MIT AND Zlib)"
+ },
"node_modules/param-case": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz",
@@ -3018,6 +3170,13 @@
"integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==",
"license": "MIT"
},
+ "node_modules/performance-now": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
+ "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==",
+ "license": "MIT",
+ "optional": true
+ },
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
@@ -3140,6 +3299,23 @@
],
"license": "MIT"
},
+ "node_modules/raf": {
+ "version": "3.4.1",
+ "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz",
+ "integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "performance-now": "^2.1.0"
+ }
+ },
+ "node_modules/regenerator-runtime": {
+ "version": "0.13.11",
+ "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
+ "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==",
+ "license": "MIT",
+ "optional": true
+ },
"node_modules/relateurl": {
"version": "0.2.7",
"resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz",
@@ -3167,6 +3343,16 @@
"integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==",
"license": "MIT"
},
+ "node_modules/rgbcolor": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/rgbcolor/-/rgbcolor-1.0.1.tgz",
+ "integrity": "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==",
+ "license": "MIT OR SEE LICENSE IN FEEL-FREE.md",
+ "optional": true,
+ "engines": {
+ "node": ">= 0.8.15"
+ }
+ },
"node_modules/rollup": {
"version": "4.41.0",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.41.0.tgz",
@@ -3299,6 +3485,16 @@
"node": ">=0.10.0"
}
},
+ "node_modules/stackblur-canvas": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.7.0.tgz",
+ "integrity": "sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==",
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">=0.1.14"
+ }
+ },
"node_modules/superjson": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.2.tgz",
@@ -3324,6 +3520,16 @@
"node": ">=8"
}
},
+ "node_modules/svg-pathdata": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-6.0.3.tgz",
+ "integrity": "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==",
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
"node_modules/tailwindcss": {
"version": "4.1.7",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.7.tgz",
@@ -3382,6 +3588,16 @@
"devOptional": true,
"license": "MIT"
},
+ "node_modules/text-segmentation": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz",
+ "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "utrie": "^1.0.2"
+ }
+ },
"node_modules/tinyglobby": {
"version": "0.2.13",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz",
@@ -3473,6 +3689,16 @@
"browserslist": ">= 4.21.0"
}
},
+ "node_modules/utrie": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz",
+ "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "base64-arraybuffer": "^1.0.2"
+ }
+ },
"node_modules/uuid": {
"version": "11.1.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz",
diff --git a/package.json b/package.json
index aa208d1..ff9ae63 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
- "name": "notsoweb.frontend",
- "copyright": "Notsoweb Software Inc.",
+ "name": "pdv.frontend",
+ "copyright": "Golsystems",
"private": true,
"version": "0.9.10",
"type": "module",
@@ -14,6 +14,7 @@
"@tailwindcss/vite": "^4.0.9",
"@vitejs/plugin-vue": "^5.2.1",
"axios": "^1.8.1",
+ "jspdf": "^3.0.4",
"laravel-echo": "^2.0.2",
"luxon": "^3.5.0",
"pinia": "^3.0.1",
diff --git a/public/Logo-hk.png b/public/Logo-hk.png
new file mode 100644
index 0000000..a9573b4
Binary files /dev/null and b/public/Logo-hk.png differ
diff --git a/src/components/Holos/Logo.vue b/src/components/Holos/Logo.vue
index f6b176c..7cb025c 100644
--- a/src/components/Holos/Logo.vue
+++ b/src/components/Holos/Logo.vue
@@ -19,6 +19,6 @@ const home = () => {
class="flex w-full justify-center items-center space-x-2 cursor-pointer"
@click="home"
>
-
+
\ No newline at end of file
diff --git a/src/components/Holos/Modal.vue b/src/components/Holos/Modal.vue
index 828cf0b..ddc2f8a 100644
--- a/src/components/Holos/Modal.vue
+++ b/src/components/Holos/Modal.vue
@@ -42,6 +42,9 @@ const maxWidthClass = computed(() => {
'lg': 'sm:max-w-lg',
'xl': 'sm:max-w-xl',
'2xl': 'sm:max-w-2xl',
+ '3xl': 'sm:max-w-3xl',
+ '4xl': 'sm:max-w-4xl',
+ '5xl': 'sm:max-w-5xl',
}[props.maxWidth];
});
diff --git a/src/components/Holos/Searcher.vue b/src/components/Holos/Searcher.vue
index 6085ef9..cb8902b 100644
--- a/src/components/Holos/Searcher.vue
+++ b/src/components/Holos/Searcher.vue
@@ -38,45 +38,36 @@ const clear = () => {
v-text="title"
/>
-
+
-
-
-
\ No newline at end of file
diff --git a/src/components/Holos/Skeleton/Header.vue b/src/components/Holos/Skeleton/Header.vue
index 5dc2abe..c7a8f6a 100644
--- a/src/components/Holos/Skeleton/Header.vue
+++ b/src/components/Holos/Skeleton/Header.vue
@@ -32,7 +32,7 @@ const loader = useLoader()
class="fixed px-2 w-[calc(100vw)] bg-transparent transition-all duration-300 z-50"
:class="{'md:w-[calc(100vw-16rem)]':leftSidebar.isOpened,'md:w-[calc(100vw)]':!leftSidebar.isClosed}"
>
-
+
-
+
-
+
@@ -41,11 +40,10 @@ const year = (new Date).getFullYear();
-
+
© {{year}} {{ APP_COPYRIGHT }}
-
- APP {{ APP_VERSION }} API {{ $page.app.version }}
+
diff --git a/src/components/Holos/Skeleton/Sidebar/Link.vue b/src/components/Holos/Skeleton/Sidebar/Link.vue
index d2c206a..7f019c4 100644
--- a/src/components/Holos/Skeleton/Sidebar/Link.vue
+++ b/src/components/Holos/Skeleton/Sidebar/Link.vue
@@ -18,10 +18,10 @@ const props = defineProps({
const classes = computed(() => {
let status = props.to === vroute.name
- ? 'bg-secondary/30 dark:bg-secondary-d/30 border-secondary dark:border-secondary-d'
- : 'border-transparent';
+ ? 'bg-indigo-50 dark:bg-indigo-900/20 border-indigo-600 dark:border-indigo-400 text-indigo-700 dark:text-indigo-300'
+ : 'border-transparent text-gray-700 dark:text-gray-300';
- return `flex items-center h-11 focus:outline-hidden hover:bg-secondary/30 dark:hover:bg-secondary-d/30 border-l-4 hover:border-secondary dark:hover:border-secondary-d pr-6 ${status} transition`
+ return `flex items-center h-11 focus:outline-hidden hover:bg-gray-100 dark:hover:bg-gray-800 border-l-4 hover:border-indigo-400 dark:hover:border-indigo-500 pr-6 ${status} transition`
});
const closeSidebar = () => {
diff --git a/src/components/Holos/Skeleton/Sidebar/Section.vue b/src/components/Holos/Skeleton/Sidebar/Section.vue
index 7a6cc53..57563b4 100644
--- a/src/components/Holos/Skeleton/Sidebar/Section.vue
+++ b/src/components/Holos/Skeleton/Sidebar/Section.vue
@@ -8,8 +8,8 @@ const props = defineProps({
-
-
+
diff --git a/src/components/Holos/Table.vue b/src/components/Holos/Table.vue
index 435bbe3..9d8b65d 100644
--- a/src/components/Holos/Table.vue
+++ b/src/components/Holos/Table.vue
@@ -16,15 +16,15 @@ const props = defineProps({
-
+
-
+
-
+
+import { computed } from 'vue';
+import GoogleIcon from '@Shared/GoogleIcon.vue';
+
+/** Props */
+const props = defineProps({
+ item: {
+ type: Object,
+ required: true
+ }
+});
+
+/** Emits */
+const emit = defineEmits(['update-quantity', 'remove']);
+
+/** Computados */
+const formattedUnitPrice = computed(() => {
+ return new Intl.NumberFormat('es-MX', {
+ style: 'currency',
+ currency: 'MXN'
+ }).format(props.item.unit_price);
+});
+
+const formattedSubtotal = computed(() => {
+ const subtotal = props.item.unit_price * props.item.quantity;
+ return new Intl.NumberFormat('es-MX', {
+ style: 'currency',
+ currency: 'MXN'
+ }).format(subtotal);
+});
+
+const canIncrement = computed(() => props.item.quantity < props.item.max_stock);
+
+/** Métodos */
+const increment = () => {
+ if (canIncrement.value) {
+ emit('update-quantity', props.item.inventory_id, props.item.quantity + 1);
+ }
+};
+
+const decrement = () => {
+ if (props.item.quantity > 1) {
+ emit('update-quantity', props.item.inventory_id, props.item.quantity - 1);
+ } else {
+ emit('remove', props.item.inventory_id);
+ }
+};
+
+const remove = () => {
+ emit('remove', props.item.inventory_id);
+};
+
+
+
+
+
+
+
+ {{ item.product_name }}
+
+
+
+
+
+
+
+
+
+ {{ item.sku }}
+
+ •
+
+ {{ formattedUnitPrice }}
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ item.quantity }}
+
+
+
+
+
+
+
+
+
+
+ {{ formattedSubtotal }}
+
+
+ {{ item.quantity }} × {{ formattedUnitPrice }}
+
+
+
+
+
diff --git a/src/components/POS/CheckoutModal.vue b/src/components/POS/CheckoutModal.vue
new file mode 100644
index 0000000..43c910a
--- /dev/null
+++ b/src/components/POS/CheckoutModal.vue
@@ -0,0 +1,242 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Cobrar
+
+
Resumen de compra y método de pago
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Resumen de compra
+
+
+
+
+
+
+
+ {{ item.product_name }}
+
+
+ {{ item.quantity }} × ${{ item.unit_price.toLocaleString('es-MX', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) }}
+
+
+
+ ${{ (item.quantity * item.unit_price).toLocaleString('es-MX', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) }}
+
+
+
+
+
+
+
+ Subtotal
+ {{ formattedSubtotal }}
+
+
+ IVA (16%)
+ {{ formattedTax }}
+
+
+
+
+
+
+ Total a pagar
+ {{ formattedTotal }}
+
+
+
+
+
+
+
+
+ Selecciona método de pago
+
+
+
+
+
+
+
+
+ {{ method.label }}
+
+
+ {{ method.subtitle }}
+
+
+
+
+
+
+
+
+
+
+
+ Cancelar
+
+
+
+ Procesando...
+ Confirmar Cobro
+
+
+
+
+
diff --git a/src/components/POS/ProductCard.vue b/src/components/POS/ProductCard.vue
new file mode 100644
index 0000000..496e809
--- /dev/null
+++ b/src/components/POS/ProductCard.vue
@@ -0,0 +1,103 @@
+
+
+
+
+
+
+
+ {{ product.sku }}
+
+
+ {{ isOutOfStock ? 'Sin stock' : `Stock: ${product.stock}` }}
+
+
+
+
+
+
+
+ {{ product.name }}
+
+
+
+
+
+
+ {{ product.category?.name || 'Sin categoría' }}
+
+
+
+
+
+
+
+ {{ formattedPrice }}
+
+
+ + IVA {{ product.price?.tax || 16 }}%
+
+
+
+
+
+
+
+
+
+
diff --git a/src/lang/es.js b/src/lang/es.js
index bc0039a..360ad08 100644
--- a/src/lang/es.js
+++ b/src/lang/es.js
@@ -449,4 +449,114 @@ export default {
},
title: 'Puestos de trabajos'
},
+ pos: {
+ title: 'Punto de Venta',
+ category: 'Categorías',
+ inventory: 'Inventario',
+ prices: 'Precios',
+ cashRegister: 'Caja',
+ point: 'Punto de Venta',
+ sales: 'Ventas',
+ },
+ cashRegister: {
+ title: 'Caja Registradora',
+ description: 'Gestión de apertura y cierre de caja',
+ open: 'Abrir Caja',
+ close: 'Cerrar Caja',
+ history: 'Historial de Cajas',
+ status: {
+ open: 'Abierta',
+ closed: 'Cerrada'
+ },
+ initialCash: 'Efectivo Inicial',
+ finalCash: 'Efectivo Final',
+ expectedCash: 'Efectivo Esperado',
+ cashSales: 'Ventas en Efectivo',
+ cardSales: 'Ventas con Tarjeta',
+ totalSales: 'Total de Ventas',
+ difference: 'Diferencia',
+ transactions: 'Transacciones',
+ openedBy: 'Abierta por',
+ openedAt: 'Fecha de Apertura',
+ closedAt: 'Fecha de Cierre',
+ notes: 'Notas / Observaciones'
+ },
+ inventory: {
+ title: 'Gestión De Inventario',
+ description: 'Administra los productos del inventario.',
+ create: {
+ title: 'Nuevo Producto',
+ },
+ edit: {
+ title: 'Editar Producto',
+ },
+ sku: 'SKU / Código',
+ product: 'Producto',
+ category: 'Categoría',
+ stock: 'Stock',
+ state: 'Estado',
+ cost: 'Costo',
+ retailPrice: 'Precio Venta',
+ tax: 'Impuesto',
+ active: 'Activo',
+ inactive: 'Inactivo',
+ },
+ category: {
+ title: 'Gestión De Categorías',
+ description: 'Administra las categorías de productos.',
+ create: {
+ title: 'Nueva Categoría',
+ },
+ edit: {
+ title: 'Editar Categoría',
+ },
+ },
+ prices: {
+ title: 'Precios',
+ },
+ sales: {
+ title: 'Historial de Ventas',
+ invoice: 'Factura',
+ invoiceNumber: 'Número de Factura',
+ paymentMethod: 'Método de pago',
+ status: 'Estado',
+ total: 'Total',
+ subtotal: 'Subtotal',
+ tax: 'Impuesto (IVA)',
+ cancel: 'Cancelar venta',
+ cancelConfirm: '¿Estás seguro de cancelar esta venta? Se restaurará el stock.',
+ cancelled: 'Venta cancelada exitosamente',
+ detail: 'Detalle de venta',
+ date: 'Fecha de venta',
+ cashier: 'Cajero',
+ empty: 'No hay ventas registradas',
+ methods: {
+ cash: 'Efectivo',
+ credit_card: 'Tarjeta de Crédito',
+ debit_card: 'Tarjeta de Débito'
+ },
+ statuses: {
+ completed: 'Completada',
+ cancelled: 'Cancelada'
+ }
+ },
+ cart: {
+ title: 'Carrito de Compras',
+ empty: 'El carrito está vacío',
+ emptyMessage: 'Agrega productos para comenzar una venta',
+ addProduct: 'Agregar producto',
+ removeProduct: 'Eliminar producto',
+ quantity: 'Cantidad',
+ unitPrice: 'Precio unitario',
+ subtotal: 'Subtotal',
+ clear: 'Vaciar carrito',
+ clearConfirm: '¿Estás seguro de vaciar el carrito?',
+ checkout: 'Cobrar',
+ selectPayment: 'Selecciona método de pago',
+ processing: 'Procesando venta...',
+ success: 'Venta realizada exitosamente',
+ error: 'Error al procesar la venta',
+ noStock: 'No hay suficiente stock disponible',
+ total: 'Total a pagar'
+ },
}
\ No newline at end of file
diff --git a/src/layouts/AppLayout.vue b/src/layouts/AppLayout.vue
index d33531b..a4bbfea 100644
--- a/src/layouts/AppLayout.vue
+++ b/src/layouts/AppLayout.vue
@@ -31,13 +31,30 @@ onMounted(() => {
+
diff --git a/src/pages/POS/CashRegister/CloseModal.vue b/src/pages/POS/CashRegister/CloseModal.vue
new file mode 100644
index 0000000..402ad7b
--- /dev/null
+++ b/src/pages/POS/CashRegister/CloseModal.vue
@@ -0,0 +1,267 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ Cerrar Caja Registradora - Corte de Caja
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/pages/POS/CashRegister/Detail.vue b/src/pages/POS/CashRegister/Detail.vue
new file mode 100644
index 0000000..e85bc22
--- /dev/null
+++ b/src/pages/POS/CashRegister/Detail.vue
@@ -0,0 +1,389 @@
+
+
+
+
+
+
+
+
+
+
+ Volver al historial
+
+
+
+ Detalle de Corte de Caja
+
+
+ Caja #{{ route.params.id }}
+
+
+
+
+ Descargar Ticket
+
+
+
+
+
+
+
+
+
+
Cargando información...
+
+
+
+
+
+
+
+
+
+
+ Información del Corte
+
+
+
+
+
+
Cajero
+
+ {{ cashRegister.user?.name || 'N/A' }}
+
+
+ {{ cashRegister.user?.email || '' }}
+
+
+
+
+
+
Apertura
+
+ {{ formatDate(cashRegister.opened_at) }}
+
+
+
+
+
+
Cierre
+
+ {{ formatDate(cashRegister.closed_at) }}
+
+
+
+
+
+
+
Notas del Cierre
+
{{ cashRegister.notes }}
+
+
+
+
+
+
+
+
+
+
+
+
+
Inicial
+
+ {{ formatCurrency(cashRegister.initial_cash) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
Efectivo
+
+ {{ formatCurrency(totalCashSales) }}
+
+
+
+
Ventas en efectivo
+
+
+
+
+
+
+
+
+
+
Tarjetas
+
+ {{ formatCurrency(totalCardSales) }}
+
+
+
+
+ Crédito: {{ formatCurrency(totalCreditCard) }} | Débito: {{ formatCurrency(totalDebitCard) }}
+
+
+
+
+
+
+
+
+
+
+
Diferencia
+
+ {{ difference >= 0 ? '+' : '' }}{{ formatCurrency(difference) }}
+
+
+
+
+ {{ Math.abs(difference) < 0.01 ? 'Cuadra exacto' : (difference > 0 ? 'Sobrante' : 'Faltante') }}
+
+
+
+
+
+
+
+
+
+ Ventas Realizadas
+
+ ({{ sales.length }} transacciones)
+
+
+
+
+
+
+
No hay ventas registradas en este período
+
+
+
+
+
+
+ Folio
+ Hora
+ Método
+ Total
+ Acciones
+
+
+
+
+
+
+ {{ sale.invoice_number || `#${String(sale.id).padStart(6, '0')}` }}
+
+
+
+
+ {{ new Date(sale.created_at).toLocaleTimeString('es-MX', {
+ hour: '2-digit',
+ minute: '2-digit'
+ }) }}
+
+
+
+
+
+
+ {{ getPaymentMethodLabel(sale.payment_method) }}
+
+
+
+
+
+ {{ formatCurrency(sale.total) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/pages/POS/CashRegister/History.vue b/src/pages/POS/CashRegister/History.vue
new file mode 100644
index 0000000..7900ec9
--- /dev/null
+++ b/src/pages/POS/CashRegister/History.vue
@@ -0,0 +1,239 @@
+
+
+
+
+
+
+
+ Volver a Caja
+
+
+
+
+
+
+ ID
+ Usuario
+ Apertura
+ Cierre
+ Inicial
+ Total Ventas
+ Diferencia
+ Estado
+ Acciones
+
+
+
+
+
+ #{{ register.id }}
+
+
+
+
+
+ {{ register.user?.name || 'N/A' }}
+
+
+ {{ register.user?.email || '' }}
+
+
+
+
+
+ {{ new Date(register.opened_at).toLocaleDateString('es-MX', {
+ year: 'numeric',
+ month: 'short',
+ day: 'numeric'
+ }) }}
+
+
+ {{ new Date(register.opened_at).toLocaleTimeString('es-MX', {
+ hour: '2-digit',
+ minute: '2-digit'
+ }) }}
+
+
+
+
+ {{ new Date(register.closed_at).toLocaleDateString('es-MX', {
+ year: 'numeric',
+ month: 'short',
+ day: 'numeric'
+ }) }}
+
+
+ {{ new Date(register.closed_at).toLocaleTimeString('es-MX', {
+ hour: '2-digit',
+ minute: '2-digit'
+ }) }}
+
+
+ Abierta
+
+
+
+
+ ${{ parseFloat(register.initial_cash || 0).toLocaleString('es-MX', { minimumFractionDigits: 2 }) }}
+
+
+
+
+ ${{ parseFloat(register.total_sales || 0).toLocaleString('es-MX', { minimumFractionDigits: 2 }) }}
+
+
+ {{ register.transaction_count || 0 }} ventas
+
+
+
+
+ {{ parseFloat(register.difference || 0) >= 0 ? '+' : '' }}${{ Math.abs(parseFloat(register.difference || 0)).toLocaleString('es-MX', { minimumFractionDigits: 2 }) }}
+
+
+ -
+
+
+
+
+ {{ register.status === 'closed' ? 'Cerrada' : 'Abierta' }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ No hay registros de cajas
+
+
+ Abre una caja para comenzar a registrar transacciones
+
+
+
+
+
+
+
+
diff --git a/src/pages/POS/CashRegister/Index.vue b/src/pages/POS/CashRegister/Index.vue
new file mode 100644
index 0000000..36c7aa7
--- /dev/null
+++ b/src/pages/POS/CashRegister/Index.vue
@@ -0,0 +1,355 @@
+
+
+
+
+
+
+
+
+
+
+ Caja Registradora
+
+
+ Gestión de apertura y cierre de caja
+
+
+
+
+
+
+
+
+ Historial
+
+
+
+
+
+
+
+
+
+
+
Cargando información de caja...
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Caja Cerrada
+
No hay ninguna caja abierta actualmente
+
+
+
+
+
+
+
+
+
+ Necesitas abrir la caja para comenzar
+
+
+ Al abrir la caja podrás registrar ventas y realizar transacciones.
+ Ingresa el monto inicial en efectivo para comenzar tu turno.
+
+
+
+
+
+
+ Ver Historial
+
+
+
+ Abrir Caja
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Caja Abierta
+
Operando normalmente
+
+
+
+
Abierta por
+
{{ cashRegisterStore.openedBy }}
+
+
+
+
+
+
+
+
+
+
+ Información General
+
+
+
+ ID de Caja:
+
+ #{{ cashRegisterStore.currentRegisterId }}
+
+
+
+ Fecha de Apertura:
+
+ {{ new Date(cashRegisterStore.openedAt).toLocaleString('es-MX', {
+ year: 'numeric',
+ month: 'short',
+ day: 'numeric',
+ hour: '2-digit',
+ minute: '2-digit'
+ }) }}
+
+
+
+ Efectivo Inicial:
+
+ ${{ cashRegisterStore.initialCash.toLocaleString('es-MX', { minimumFractionDigits: 2 }) }}
+
+
+
+
+
+
+
+
+ Resumen de Ventas
+
+
+
+ Total de Transacciones:
+
+ {{ cashRegisterStore.transactionCount }}
+
+
+
+ Total Vendido:
+
+ ${{ cashRegisterStore.totalSales.toLocaleString('es-MX', { minimumFractionDigits: 2 }) }}
+
+
+
+ Balance Estimado:
+
+ ${{ cashRegisterStore.currentBalance.toLocaleString('es-MX', { minimumFractionDigits: 2 }) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Efectivo
+
+ ${{ cashRegisterStore.expectedCash.toLocaleString('es-MX', { minimumFractionDigits: 2 }) }}
+
+
+
+
+ Efectivo esperado en caja
+
+
+
+
+
+
+
+
+
+
+
Tarjeta
+
+ ${{ cashRegisterStore.cardSales.toLocaleString('es-MX', { minimumFractionDigits: 2 }) }}
+
+
+
+
+ Pagos con tarjeta
+
+
+
+
+
+
+
+
+
+
+
Total
+
+ ${{ cashRegisterStore.totalSales.toLocaleString('es-MX', { minimumFractionDigits: 2 }) }}
+
+
+
+
+ {{ cashRegisterStore.transactionCount }} transacciones
+
+
+
+
+
+
+
Acciones Rápidas
+
+
+
+ Ir al Punto de Venta
+
+
+
+ Ver Historial
+
+
+
+ Cerrar Caja
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/pages/POS/CashRegister/OpenModal.vue b/src/pages/POS/CashRegister/OpenModal.vue
new file mode 100644
index 0000000..d5c946f
--- /dev/null
+++ b/src/pages/POS/CashRegister/OpenModal.vue
@@ -0,0 +1,161 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Abrir Caja Registradora
+
+
Inicio de turno
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Inicio de Turno
+
+
+ Ingresa el monto inicial en efectivo con el que comenzarás tu turno.
+ Este monto se utilizará como base para el corte de caja al finalizar.
+
+
+
+
+
+
+
+
+ Monto Inicial en Efectivo *
+
+
+
+ $
+
+
+
+ Ejemplo: Si comienzas con $1,000.00 en efectivo para dar cambio
+
+
+
+
+
+
+
+
+
+ Efectivo Inicial
+
+
+ ${{ (initialCash || 0).toLocaleString('es-MX', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) }}
+
+
+
+
+ Fecha y Hora
+
+
+ {{ new Date().toLocaleDateString('es-MX', {
+ year: 'numeric',
+ month: 'short',
+ day: 'numeric'
+ }) }}
+
+
+ {{ new Date().toLocaleTimeString('es-MX', {
+ hour: '2-digit',
+ minute: '2-digit'
+ }) }}
+
+
+
+
+
+
+
+
+ Cancelar
+
+
+
+ Abriendo...
+ Abrir Caja
+
+
+
+
+
+
diff --git a/src/pages/POS/Category/CreateModal.vue b/src/pages/POS/Category/CreateModal.vue
new file mode 100644
index 0000000..f50a37f
--- /dev/null
+++ b/src/pages/POS/Category/CreateModal.vue
@@ -0,0 +1,127 @@
+
+
+
+
+
+
+
+
+ Crear Categoría
+
+
+
+
+
+
+
+
+
+
+
+
+
+ NOMBRE
+
+
+
+
+
+
+
+
+ DESCRIPCIÓN
+
+
+
+
+
+
+
+
+ ESTADO
+
+
+ Activo
+ Inactivo
+
+
+
+
+
+
+
+ Cancelar
+
+
+ Guardando...
+ Guardar
+
+
+
+
+
+
diff --git a/src/pages/POS/Category/DeleteModal.vue b/src/pages/POS/Category/DeleteModal.vue
new file mode 100644
index 0000000..b0667b5
--- /dev/null
+++ b/src/pages/POS/Category/DeleteModal.vue
@@ -0,0 +1,117 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ Eliminar Categoría
+
+
+
+
+
+
+
+
+
+
+
+
+ ¿Estás seguro de que deseas eliminar esta categoría?
+
+
+
+
+
+
+
+
+
+ {{ category.name }}
+
+
+ {{ category.description }}
+
+
+ Sin descripción
+
+
+
+
+ {{ category.is_active ? 'Activo' : 'Inactivo' }}
+
+
+
+
+
+
+
+
+ Esta acción es permanente y no se puede deshacer. Los productos asociados a esta categoría perderán su categorización.
+
+
+
+
+
+
+
+ Cancelar
+
+
+
+ Eliminar Categoría
+
+
+
+
+
diff --git a/src/pages/POS/Category/EditModal.vue b/src/pages/POS/Category/EditModal.vue
new file mode 100644
index 0000000..5dee48b
--- /dev/null
+++ b/src/pages/POS/Category/EditModal.vue
@@ -0,0 +1,138 @@
+
+
+
+
+
+
+
+
+ Editar Categoría
+
+
+
+
+
+
+
+
+
+
+
+
+
+ NOMBRE
+
+
+
+
+
+
+
+
+ DESCRIPCIÓN
+
+
+
+
+
+
+
+
+ ESTADO
+
+
+ Activo
+ Inactivo
+
+
+
+
+
+
+
+ Cancelar
+
+
+ Actualizando...
+ Actualizar
+
+
+
+
+
+
diff --git a/src/pages/POS/Category/Index.vue b/src/pages/POS/Category/Index.vue
new file mode 100644
index 0000000..44fde4f
--- /dev/null
+++ b/src/pages/POS/Category/Index.vue
@@ -0,0 +1,200 @@
+
+
+
+
+
searcher.search(x)"
+ >
+
+
+ Nueva Categoría
+
+
+
+
+
+
+
+
+
+
+
+
+
+
searcher.pagination(page)"
+ >
+
+ NOMBRE
+ DESCRIPCIÓN
+ ESTADO
+ ACCIONES
+
+
+
+
+ {{ model.name }}
+
+
+ {{ model.description || '-' }}
+
+
+
+ {{ model.is_active ? 'Activo' : 'Inactivo' }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ $t('registers.empty') }}
+
+
+
+
+
+
+
+
diff --git a/src/pages/POS/Inventory/CreateModal.vue b/src/pages/POS/Inventory/CreateModal.vue
new file mode 100644
index 0000000..85f1b47
--- /dev/null
+++ b/src/pages/POS/Inventory/CreateModal.vue
@@ -0,0 +1,232 @@
+
+
+
+
+
+
+
+
+ Crear Producto
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ NOMBRE
+
+
+
+
+
+
+
+
+ SKU
+
+
+
+
+
+
+
+
+ CATEGORÍA
+
+
+ Seleccionar categoría
+
+ {{ category.name }}
+
+
+
+
+
+
+
+
+ STOCK INICIAL
+
+
+
+
+
+
+
+
+ COSTO
+
+
+
+
+
+
+
+
+ PRECIO VENTA
+
+
+
+
+
+
+
+
+ IMPUESTO (%)
+
+
+
+
+
+
+
+
+
+ Cancelar
+
+
+ Guardando...
+ Guardar
+
+
+
+
+
+
diff --git a/src/pages/POS/Inventory/DeleteModal.vue b/src/pages/POS/Inventory/DeleteModal.vue
new file mode 100644
index 0000000..eee55f7
--- /dev/null
+++ b/src/pages/POS/Inventory/DeleteModal.vue
@@ -0,0 +1,115 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ Eliminar Producto
+
+
+
+
+
+
+
+
+
+
+
+
+ ¿Estás seguro de que deseas eliminar este producto?
+
+
+
+
+
+
+ {{ product.name }}
+
+
+ SKU: {{ product.sku }}
+ •
+ {{ product.category?.name || 'Sin categoría' }}
+
+
+
+
Stock
+
+ {{ product.stock }}
+
+
+
+
+
+
+
+ Este producto tiene {{ product.stock }} unidades en stock . Al eliminarlo, se perderá el inventario registrado.
+
+
+
+
+
+
+
+ Esta acción es permanente y no se puede deshacer.
+
+
+
+
+
+
+
+ Cancelar
+
+
+
+ Eliminar Producto
+
+
+
+
+
diff --git a/src/pages/POS/Inventory/EditModal.vue b/src/pages/POS/Inventory/EditModal.vue
new file mode 100644
index 0000000..978ce3d
--- /dev/null
+++ b/src/pages/POS/Inventory/EditModal.vue
@@ -0,0 +1,245 @@
+
+
+
+
+
+
+
+
+ Editar Producto
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ NOMBRE
+
+
+
+
+
+
+
+
+ SKU
+
+
+
+
+
+
+
+
+ CATEGORÍA
+
+
+ Seleccionar categoría
+
+ {{ category.name }}
+
+
+
+
+
+
+
+
+ STOCK
+
+
+
+
+
+
+
+
+ COSTO
+
+
+
+
+
+
+
+
+ PRECIO VENTA
+
+
+
+
+
+
+
+
+ IMPUESTO (%)
+
+
+
+
+
+
+
+
+
+ Cancelar
+
+
+ Actualizando...
+ Actualizar
+
+
+
+
+
+
diff --git a/src/pages/POS/Inventory/Index.vue b/src/pages/POS/Inventory/Index.vue
new file mode 100644
index 0000000..0f9623c
--- /dev/null
+++ b/src/pages/POS/Inventory/Index.vue
@@ -0,0 +1,220 @@
+
+
+
+
+
searcher.search(x)"
+ >
+
+
+ Nuevo Producto
+
+
+
+
+
+
+
+
+
+
+
+
+
+
searcher.pagination(page)"
+ >
+
+ SKU / CÓDIGO
+ PRODUCTO
+ CATEGORÍA
+ PRECIO
+ STOCK
+ ACCIONES
+
+
+
+
+ {{ model.sku }}
+
+
+
+
{{ model.name }}
+
{{ model.description }}
+
+
+
+
+ {{ model.category?.name || '-' }}
+
+
+
+
+
+ ${{ parseFloat(model.price?.retail_price || 0).toLocaleString('es-MX', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) }}
+
+
+ Costo: ${{ parseFloat(model.price?.cost || 0).toLocaleString('es-MX', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) }}
+
+
+
+
+
+ {{ model.stock }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ $t('registers.empty') }}
+
+
+
+
+
+
+
+
diff --git a/src/pages/POS/Point.vue b/src/pages/POS/Point.vue
new file mode 100644
index 0000000..44571ee
--- /dev/null
+++ b/src/pages/POS/Point.vue
@@ -0,0 +1,310 @@
+
+
+
+
+
+
+
+
+
+
+
+ {{ $t('pos.title') }}
+
+
+ {{ $t('pos.subtitle') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ searchQuery ? 'No se encontraron productos' : 'No hay productos disponibles' }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ $t('cart.title') }}
+
+
+ {{ cart.itemCount }} {{ cart.itemCount === 1 ? 'item' : 'items' }}
+
+
+
+
+
+
+
+
+
+ {{ $t('cart.empty') }}
+
+
+ {{ $t('cart.emptyMessage') }}
+
+
+
+
+
+
+
+
+
+
+
+ {{ $t('cart.subtotal') }}
+ ${{ cart.subtotal.toFixed(2) }}
+
+
+ IVA (16%)
+ ${{ cart.tax.toFixed(2) }}
+
+
+
+
+ {{ $t('cart.total') }}
+
+
+ ${{ cart.total.toFixed(2) }}
+
+
+
+
+
+
+
+
+
+ {{ $t('cart.checkout') }}
+
+
+
+
+ {{ $t('cart.clear') }}
+
+
+
+
+
+
+
+
+
+
diff --git a/src/pages/POS/Sales/DetailModal.vue b/src/pages/POS/Sales/DetailModal.vue
new file mode 100644
index 0000000..20a680b
--- /dev/null
+++ b/src/pages/POS/Sales/DetailModal.vue
@@ -0,0 +1,337 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ $t('sales.detail') }}
+
+
+ Folio: {{ sale.invoice_number || `#${String(sale.id).padStart(6, '0')}` }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ $t('sales.date') }}
+
+
+ {{ formattedDate }}
+
+
+
+
+ {{ $t('sales.cashier') }}
+
+
+ {{ sale.user?.name || '-' }}
+
+
+
+
+ {{ $t('sales.paymentMethod') }}
+
+
+
+
+ {{ paymentMethodLabel }}
+
+
+
+
+
+ {{ $t('sales.status') }}
+
+
+ {{ sale.status === 'completed' ? 'Completada' : 'Cancelada' }}
+
+
+
+
+
+
+
+
+ Productos Vendidos
+
+
+
+
+
+
+
+
+
+ Producto
+
+
+ Cant.
+
+
+ P. Unit.
+
+
+ Subtotal
+
+
+
+
+
+
+
+ {{ item.product_name }}
+
+
+ SKU: {{ item.inventory.sku }}
+
+
+
+
+ {{ item.quantity }}
+
+
+
+
+ {{ formatCurrency(item.unit_price) }}
+
+
+
+
+ {{ formatCurrency(item.subtotal) }}
+
+
+
+
+
+
+
+
+
+ No hay productos en esta venta
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ $t('sales.subtotal') }}
+
+
+ {{ formattedSubtotal }}
+
+
+
+
+ {{ $t('sales.tax') }}
+
+
+ {{ formattedTax }}
+
+
+
+
+
+ {{ $t('sales.total') }}
+
+
+ {{ formattedTotal }}
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ $t('sales.cancel') }}
+
+
+
+
+
+
+
+
+ Descargar Ticket
+
+
+ Cerrar
+
+
+
+
+
+
diff --git a/src/pages/POS/Sales/Index.vue b/src/pages/POS/Sales/Index.vue
new file mode 100644
index 0000000..6bb3da9
--- /dev/null
+++ b/src/pages/POS/Sales/Index.vue
@@ -0,0 +1,240 @@
+
+
+
+
+
searcher.search(x)"
+ >
+
+
+
+
+
+
+
+
searcher.pagination(page)"
+ >
+
+ FOLIO
+ FECHA
+ CAJERO
+ MÉTODO DE PAGO
+ TOTAL
+ ESTADO
+ ACCIONES
+
+
+
+
+
+ #{{ String(model.id).padStart(6, '0') }}
+
+
+
+
+ {{ formatDate(model.created_at) }}
+
+
+
+
+ {{ model.user?.name || '-' }}
+
+
+
+
+
+
+ {{ getPaymentMethodLabel(model.payment_method) }}
+
+
+
+
+
+ {{ formatCurrency(model.total) }}
+
+
+
+
+ {{ getStatusLabel(model.status) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ $t('sales.empty') }}
+
+
+
+
+
+
+
+
diff --git a/src/router/Index.js b/src/router/Index.js
index 5fcdcb2..89722a9 100644
--- a/src/router/Index.js
+++ b/src/router/Index.js
@@ -28,6 +28,51 @@ const router = createRouter({
name: 'dashboard.index',
component: () => import('@Pages/Dashboard/Index.vue')
},
+ {
+ path: 'pos',
+ children: [
+ {
+ path: 'category',
+ name: 'pos.category.index',
+ component: () => import('@Pages/POS/Category/Index.vue')
+ },
+ {
+ path: 'inventory',
+ name: 'pos.inventory.index',
+ component: () => import('@Pages/POS/Inventory/Index.vue')
+ },
+ {
+ path: 'point',
+ name: 'pos.point',
+ component: () => import('@Pages/POS/Point.vue')
+ },
+ {
+ path: 'cash-register',
+ children: [
+ {
+ path: '',
+ name: 'pos.cashRegister.index',
+ component: () => import('@Pages/POS/CashRegister/Index.vue')
+ },
+ {
+ path: 'history',
+ name: 'pos.cashRegister.history',
+ component: () => import('@Pages/POS/CashRegister/History.vue')
+ },
+ {
+ path: ':id',
+ name: 'pos.cashRegister.detail',
+ component: () => import('@Pages/POS/CashRegister/Detail.vue')
+ }
+ ]
+ },
+ {
+ path: 'sales',
+ name: 'pos.sales.index',
+ component: () => import('@Pages/POS/Sales/Index.vue')
+ }
+ ]
+ },
{
path: 'profile',
children: [
diff --git a/src/services/cashRegisterService.js b/src/services/cashRegisterService.js
new file mode 100644
index 0000000..b6da5a3
--- /dev/null
+++ b/src/services/cashRegisterService.js
@@ -0,0 +1,126 @@
+import { api, apiURL } from '@Services/Api';
+
+/**
+ * Servicio para gestionar cajas registradoras
+ */
+const cashRegisterService = {
+ /**
+ * Helper para extraer datos del registro de diferentes formatos de respuesta
+ */
+ extractRegisterData(response) {
+ return response.register ||
+ response.cashRegister ||
+ response.cash_register ||
+ response.model ||
+ response;
+ },
+
+ /**
+ * Abrir una nueva caja registradora
+ * @param {Number} initialCash - Monto inicial en efectivo
+ * @returns {Promise}
+ */
+ async openCashRegister(initialCash) {
+ return new Promise((resolve, reject) => {
+ api.post(apiURL('cash-registers/open'), {
+ data: { initial_cash: initialCash },
+ onSuccess: (response) => resolve(this.extractRegisterData(response)),
+ onError: reject
+ });
+ });
+ },
+
+ /**
+ * Obtener la caja registradora actualmente abierta
+ * @returns {Promise}
+ */
+ async getCurrentCashRegister() {
+ return new Promise((resolve, reject) => {
+ api.get(apiURL('cash-registers/current'), {
+ onSuccess: (response) => {
+ const register = this.extractRegisterData(response);
+
+ // Mapear campos del backend a nombres esperados por el frontend
+ if (response.total_sales !== undefined) {
+ register.total_sales = response.total_sales;
+ }
+ if (response.total_cash !== undefined) {
+ register.cash_sales = response.total_cash;
+ }
+ if (response.sales_count !== undefined) {
+ register.transaction_count = response.sales_count;
+ }
+
+ // Calcular card_sales sumando credit_card + debit_card
+ const creditCard = parseFloat(response.total_credit_card || 0);
+ const debitCard = parseFloat(response.total_debit_card || 0);
+ if (creditCard > 0 || debitCard > 0) {
+ register.card_sales = creditCard + debitCard;
+ }
+
+ // Mantener payment_summary si existe
+ if (response.payment_summary) {
+ register.payment_summary = response.payment_summary;
+ }
+
+ resolve(register);
+ },
+ onError: (error) => {
+ // Si es 404, significa que no hay caja abierta (no es un error)
+ if (error.status === 404 || error.response?.status === 404) {
+ resolve(null);
+ } else {
+ reject(error);
+ }
+ }
+ });
+ });
+ },
+
+ /**
+ * Cerrar una caja registradora con el corte de caja
+ * @param {Number} id - ID de la caja registradora
+ * @param {Object} closeData - Datos del cierre (final_cash, notes, etc.)
+ * @returns {Promise}
+ */
+ async closeCashRegister(id, closeData) {
+ return new Promise((resolve, reject) => {
+ api.put(apiURL(`cash-registers/${id}/close`), {
+ data: closeData,
+ onSuccess: (response) => resolve(this.extractRegisterData(response)),
+ onError: reject
+ });
+ });
+ },
+
+ /**
+ * Obtener historial de cajas registradoras
+ * @param {Object} filters - Filtros de búsqueda (fecha, usuario, etc.)
+ * @returns {Promise}
+ */
+ async getCashRegisterHistory(filters = {}) {
+ return new Promise((resolve, reject) => {
+ api.get(apiURL('cash-registers'), {
+ params: filters,
+ onSuccess: (response) => resolve(response.registers || response.cashRegisters || response.cash_registers || response),
+ onError: reject
+ });
+ });
+ },
+
+ /**
+ * Obtener detalle de una caja registradora específica
+ * @param {Number} id - ID de la caja registradora
+ * @returns {Promise}
+ */
+ async getCashRegisterDetail(id) {
+ return new Promise((resolve, reject) => {
+ api.get(apiURL(`cash-registers/${id}`), {
+ onSuccess: (response) => resolve(this.extractRegisterData(response)),
+ onError: reject
+ });
+ });
+ }
+};
+
+export default cashRegisterService;
diff --git a/src/services/salesService.js b/src/services/salesService.js
new file mode 100644
index 0000000..3e0deb5
--- /dev/null
+++ b/src/services/salesService.js
@@ -0,0 +1,82 @@
+import { api, apiURL } from '@Services/Api';
+
+/**
+ * Servicio para gestionar ventas
+ */
+const salesService = {
+ /**
+ * Crear una nueva venta
+ * @param {Object} saleData - Datos de la venta
+ * @returns {Promise}
+ */
+ async createSale(saleData) {
+ return new Promise((resolve, reject) => {
+ api.post(apiURL('sales'), {
+ data: saleData,
+ onSuccess: (response) => {
+ resolve(response.model);
+ },
+ onError: (error) => {
+ reject(error);
+ }
+ });
+ });
+ },
+
+ /**
+ * Obtener lista de ventas con filtros
+ * @param {Object} filters - Filtros de búsqueda
+ * @returns {Promise}
+ */
+ async getSales(filters = {}) {
+ return new Promise((resolve, reject) => {
+ api.get(apiURL('sales'), {
+ params: filters,
+ onSuccess: (response) => {
+ resolve(response.sales);
+ },
+ onError: (error) => {
+ reject(error);
+ }
+ });
+ });
+ },
+
+ /**
+ * Obtener detalle de una venta
+ * @param {Number} saleId - ID de la venta
+ * @returns {Promise}
+ */
+ async getSaleDetails(saleId) {
+ return new Promise((resolve, reject) => {
+ api.get(apiURL(`sales/${saleId}`), {
+ onSuccess: (response) => {
+ resolve(response.model);
+ },
+ onError: (error) => {
+ reject(error);
+ }
+ });
+ });
+ },
+
+ /**
+ * Cancelar una venta (restaura stock)
+ * @param {Number} saleId - ID de la venta
+ * @returns {Promise}
+ */
+ async cancelSale(saleId) {
+ return new Promise((resolve, reject) => {
+ api.put(apiURL(`sales/${saleId}/cancel`), {
+ onSuccess: (response) => {
+ resolve(response.model);
+ },
+ onError: (error) => {
+ reject(error);
+ }
+ });
+ });
+ }
+};
+
+export default salesService;
\ No newline at end of file
diff --git a/src/services/ticketService.js b/src/services/ticketService.js
new file mode 100644
index 0000000..55d9bfc
--- /dev/null
+++ b/src/services/ticketService.js
@@ -0,0 +1,397 @@
+import jsPDF from 'jspdf';
+
+/**
+ * Servicio para generar tickets de venta en formato PDF
+ */
+const ticketService = {
+ /**
+ * Helper para formatear moneda
+ */
+ formatMoney(amount) {
+ return parseFloat(amount || 0).toFixed(2);
+ },
+
+ /**
+ * Detectar ubicación del usuario (ciudad y estado)
+ */
+ async getUserLocation() {
+ try {
+ const response = await fetch('https://ipapi.co/json/');
+ const data = await response.json();
+ return {
+ city: data.city || 'Ciudad de México',
+ state: data.region || 'CDMX',
+ country: data.country_name || 'México'
+ };
+ } catch (error) {
+ return {
+ city: 'Ciudad de México',
+ state: 'CDMX',
+ country: 'México'
+ };
+ }
+ },
+
+ /**
+ * Generar ticket de venta
+ * @param {Object} saleData - Datos de la venta
+ * @param {Object} options - Opciones de configuración
+ */
+ async generateSaleTicket(saleData, options = {}) {
+ const {
+ businessName = 'HIKVISION DISTRIBUIDOR',
+ autoDownload = true
+ } = options;
+
+ // Detectar ubicación del usuario
+ const location = await this.getUserLocation();
+ const businessAddress = `${location.city}, ${location.state}`;
+ const businessPhone = 'Tel: (55) 1234-5678';
+
+ // Crear documento PDF - Ticket térmico 80mm de ancho
+ const doc = new jsPDF({
+ orientation: 'portrait',
+ unit: 'mm',
+ format: [80, 297]
+ });
+
+ let yPosition = 10;
+ const leftMargin = 5;
+ const rightMargin = 75;
+ const centerX = 40;
+ const lineHeight = 5;
+
+ // Colores
+ const blackColor = [0, 0, 0];
+ const darkGrayColor = [80, 80, 80];
+
+ // ===== HEADER =====
+ // Nombre del negocio
+ doc.setFontSize(14);
+ doc.setFont('helvetica', 'bold');
+ doc.setTextColor(...blackColor);
+ doc.text(businessName, centerX, yPosition, { align: 'center' });
+ yPosition += 6;
+
+ // Dirección y teléfono
+ doc.setFontSize(8);
+ doc.setFont('helvetica', 'normal');
+ doc.setTextColor(...darkGrayColor);
+ doc.text(businessAddress, centerX, yPosition, { align: 'center' });
+ yPosition += 4;
+ doc.text(businessPhone, centerX, yPosition, { align: 'center' });
+ yPosition += 6;
+
+ // Línea separadora
+ doc.setDrawColor(...darkGrayColor);
+ doc.setLineWidth(0.3);
+ doc.line(leftMargin, yPosition, rightMargin, yPosition);
+ yPosition += 5;
+
+ // ===== TÍTULO =====
+ doc.setFontSize(12);
+ doc.setFont('helvetica', 'bold');
+ doc.setTextColor(...blackColor);
+ doc.text('TICKET DE VENTA', centerX, yPosition, { align: 'center' });
+ yPosition += 7;
+
+ // ===== INFORMACIÓN DE LA VENTA =====
+ doc.setFontSize(8);
+ doc.setFont('helvetica', 'normal');
+ doc.setTextColor(...darkGrayColor);
+
+ // Folio
+ const folio = saleData.invoice_number || `INV-${String(saleData.id || '').padStart(12, '0')}`;
+ doc.text(`Folio: ${folio}`, leftMargin, yPosition);
+ yPosition += 4;
+
+ // Fecha
+ const saleDate = new Date(saleData.created_at || Date.now());
+ const formattedDate = saleDate.toLocaleDateString('es-MX', {
+ year: 'numeric',
+ month: 'long',
+ day: 'numeric'
+ });
+ doc.text(`Fecha: ${formattedDate}`, leftMargin, yPosition);
+ yPosition += 4;
+
+ // Hora
+ const formattedTime = saleDate.toLocaleTimeString('es-MX', {
+ hour: '2-digit',
+ minute: '2-digit'
+ });
+ doc.text(`Hora: ${formattedTime} p.m.`, leftMargin, yPosition);
+ yPosition += 4;
+
+ // Cajero
+ if (saleData.user?.name) {
+ doc.text(`Cajero: ${saleData.user.name}`, leftMargin, yPosition);
+ yPosition += 4;
+ }
+
+ // Método de pago
+ const paymentMethods = {
+ cash: 'Efectivo',
+ credit_card: 'Tarjeta de Crédito',
+ debit_card: 'Tarjeta de Débito'
+ };
+ const paymentLabel = paymentMethods[saleData.payment_method] || saleData.payment_method;
+ doc.text(`Pago: ${paymentLabel}`, leftMargin, yPosition);
+ yPosition += 6;
+
+ // Línea separadora
+ doc.line(leftMargin, yPosition, rightMargin, yPosition);
+ yPosition += 5;
+
+ // ===== PRODUCTOS =====
+ doc.setFontSize(9);
+ doc.setFont('helvetica', 'bold');
+ doc.setTextColor(...blackColor);
+ doc.text('PRODUCTOS', centerX, yPosition, { align: 'center' });
+ yPosition += 5;
+
+ // Header de tabla
+ doc.setFontSize(7);
+ doc.setFont('helvetica', 'bold');
+ doc.setTextColor(...darkGrayColor);
+ doc.text('DESCRIPCIÓN', leftMargin, yPosition);
+ doc.text('CANT', 52, yPosition, { align: 'center' });
+ doc.text('PRECIO', rightMargin, yPosition, { align: 'right' });
+ yPosition += 4;
+
+ // Línea bajo header
+ doc.setLineWidth(0.2);
+ doc.line(leftMargin, yPosition, rightMargin, yPosition);
+ yPosition += 4;
+
+ // Iterar sobre los productos
+ doc.setFont('helvetica', 'normal');
+ doc.setTextColor(...blackColor);
+ const items = saleData.details || saleData.items || [];
+
+ items.forEach((item) => {
+ // Nombre del producto
+ const productName = item.product_name || item.name || 'Producto';
+ const nameLines = doc.splitTextToSize(productName, 45);
+
+ doc.setFontSize(8);
+ doc.setFont('helvetica', 'bold');
+ doc.text(nameLines, leftMargin, yPosition);
+ yPosition += nameLines.length * 3.5;
+
+ // SKU (opcional)
+ if (item.inventory?.sku || item.sku) {
+ doc.setFontSize(7);
+ doc.setFont('helvetica', 'normal');
+ doc.setTextColor(...darkGrayColor);
+ doc.text(`SKU: ${item.inventory?.sku || item.sku}`, leftMargin, yPosition);
+ yPosition += 3;
+ }
+
+ // Cantidad y Precio unitario
+ const quantity = item.quantity || 1;
+ const unitPrice = this.formatMoney(item.unit_price);
+
+ doc.setFontSize(8);
+ doc.setFont('helvetica', 'normal');
+ doc.setTextColor(...blackColor);
+
+ doc.text(String(quantity), 52, yPosition, { align: 'center' });
+ doc.text(`$${unitPrice}`, rightMargin, yPosition, { align: 'right' });
+
+ yPosition += 5;
+ });
+
+ yPosition += 2;
+
+ // Línea separadora
+ doc.setLineWidth(0.3);
+ doc.setDrawColor(...blackColor);
+ doc.line(leftMargin, yPosition, rightMargin, yPosition);
+ yPosition += 5;
+
+ // ===== TOTALES =====
+ doc.setFontSize(9);
+ doc.setFont('helvetica', 'normal');
+ doc.setTextColor(...darkGrayColor);
+
+ // Subtotal
+ doc.text('Subtotal:', leftMargin, yPosition);
+ doc.text(`$${this.formatMoney(saleData.subtotal)}`, rightMargin, yPosition, { align: 'right' });
+ yPosition += 5;
+
+ // IVA
+ doc.text('IVA (16%):', leftMargin, yPosition);
+ doc.text(`$${this.formatMoney(saleData.tax)}`, rightMargin, yPosition, { align: 'right' });
+ yPosition += 6;
+
+ // Línea antes del total
+ doc.setLineWidth(0.4);
+ doc.line(leftMargin, yPosition, rightMargin, yPosition);
+ yPosition += 5;
+
+ // Total
+ doc.setFontSize(12);
+ doc.setFont('helvetica', 'bold');
+ doc.setTextColor(...blackColor);
+ doc.text('TOTAL:', leftMargin, yPosition);
+ doc.text(`$${this.formatMoney(saleData.total)}`, rightMargin, yPosition, { align: 'right' });
+ yPosition += 8;
+
+ // Línea decorativa doble
+ doc.setLineWidth(0.3);
+ doc.setTextColor(...darkGrayColor);
+ doc.line(leftMargin, yPosition, rightMargin, yPosition);
+ yPosition += 0.5;
+ doc.setLineWidth(0.2);
+ doc.line(leftMargin, yPosition, rightMargin, yPosition);
+ yPosition += 6;
+
+ // ===== FOOTER =====
+ doc.setFontSize(9);
+ doc.setFont('helvetica', 'bold');
+ doc.setTextColor(...blackColor);
+ doc.text('¡Gracias por su compra!', centerX, yPosition, { align: 'center' });
+ yPosition += 5;
+
+ doc.setFontSize(8);
+ doc.setFont('helvetica', 'normal');
+ doc.setTextColor(...darkGrayColor);
+ doc.text('Vuelva pronto', centerX, yPosition, { align: 'center' });
+ yPosition += 6;
+
+ // Información de impresión
+ doc.setFontSize(7);
+ const currentDate = new Date().toLocaleString('es-MX', {
+ year: 'numeric',
+ month: '2-digit',
+ day: '2-digit',
+ hour: '2-digit',
+ minute: '2-digit'
+ });
+ doc.text(`Impreso: ${currentDate}`, centerX, yPosition, { align: 'center' });
+ yPosition += 3;
+ doc.text(`ID de venta: ${saleData.id || 'N/A'}`, centerX, yPosition, { align: 'center' });
+
+ // Guardar o retornar el PDF
+ if (autoDownload) {
+ const fileName = `ticket-${folio}.pdf`;
+ doc.save(fileName);
+ return doc;
+ } else {
+ return doc;
+ }
+ },
+
+ /**
+ * Generar ticket de corte de caja
+ * @param {Object} cashRegisterData - Datos del corte de caja
+ * @param {Object} options - Opciones de configuración
+ */
+ async generateCashRegisterTicket(cashRegisterData, options = {}) {
+ const {
+ businessName = 'HIKVISION DISTRIBUIDOR',
+ autoDownload = true
+ } = options;
+
+ // Detectar ubicación del usuario
+ const location = await this.getUserLocation();
+
+ const doc = new jsPDF({
+ orientation: 'portrait',
+ unit: 'mm',
+ format: [80, 297]
+ });
+
+ let yPosition = 10;
+ const leftMargin = 5;
+ const rightMargin = 75;
+ const centerX = 40;
+ const lineHeight = 5;
+
+ // Colores
+ const blackColor = [0, 0, 0];
+ const darkGrayColor = [80, 80, 80];
+
+ // Header
+ doc.setFontSize(14);
+ doc.setFont('helvetica', 'bold');
+ doc.setTextColor(...blackColor);
+ doc.text(businessName, centerX, yPosition, { align: 'center' });
+ yPosition += 6;
+
+ doc.setFontSize(8);
+ doc.setFont('helvetica', 'normal');
+ doc.setTextColor(...darkGrayColor);
+ doc.text(`${location.city}, ${location.state}`, centerX, yPosition, { align: 'center' });
+ yPosition += 6;
+
+ // Línea
+ doc.setDrawColor(...darkGrayColor);
+ doc.setLineWidth(0.3);
+ doc.line(leftMargin, yPosition, rightMargin, yPosition);
+ yPosition += 5;
+
+ // Título
+ doc.setFontSize(12);
+ doc.setFont('helvetica', 'bold');
+ doc.setTextColor(...blackColor);
+ doc.text('CORTE DE CAJA', centerX, yPosition, { align: 'center' });
+ yPosition += 7;
+
+ // Información
+ doc.setFontSize(8);
+ doc.setFont('helvetica', 'normal');
+ doc.setTextColor(...darkGrayColor);
+
+ const closeDate = new Date(cashRegisterData.closed_at || Date.now());
+ doc.text(`Fecha: ${closeDate.toLocaleDateString('es-MX')}`, leftMargin, yPosition);
+ yPosition += 4;
+ doc.text(`Hora: ${closeDate.toLocaleTimeString('es-MX', { hour: '2-digit', minute: '2-digit' })}`, leftMargin, yPosition);
+ yPosition += 4;
+ doc.text(`Cajero: ${cashRegisterData.user?.name || 'N/A'}`, leftMargin, yPosition);
+ yPosition += 6;
+
+ // Línea
+ doc.line(leftMargin, yPosition, rightMargin, yPosition);
+ yPosition += 5;
+
+ // Detalle
+ doc.setFont('helvetica', 'normal');
+ doc.setTextColor(...blackColor);
+
+ doc.text('Efectivo Inicial:', leftMargin, yPosition);
+ doc.text(`$${this.formatMoney(cashRegisterData.initial_cash)}`, rightMargin, yPosition, { align: 'right' });
+ yPosition += 5;
+
+ doc.text('Ventas Totales:', leftMargin, yPosition);
+ doc.text(`$${this.formatMoney(cashRegisterData.total_sales)}`, rightMargin, yPosition, { align: 'right' });
+ yPosition += 5;
+
+ doc.text('Efectivo Final:', leftMargin, yPosition);
+ doc.text(`$${this.formatMoney(cashRegisterData.final_cash)}`, rightMargin, yPosition, { align: 'right' });
+ yPosition += 6;
+
+ // Total
+ doc.setFont('helvetica', 'bold');
+ doc.text('Diferencia:', leftMargin, yPosition);
+ const difference = parseFloat(cashRegisterData.final_cash || 0) -
+ (parseFloat(cashRegisterData.initial_cash || 0) + parseFloat(cashRegisterData.cash_sales || 0));
+ doc.text(`$${this.formatMoney(difference)}`, rightMargin, yPosition, { align: 'right' });
+ yPosition += 8;
+
+ // Footer
+ doc.setFontSize(9);
+ doc.setFont('helvetica', 'bold');
+ doc.text('Cierre de turno', centerX, yPosition, { align: 'center' });
+
+ if (autoDownload) {
+ doc.save(`corte-caja-${cashRegisterData.id}.pdf`);
+ return doc;
+ } else {
+ return doc;
+ }
+ }
+};
+
+export default ticketService;
diff --git a/src/stores/cart.js b/src/stores/cart.js
new file mode 100644
index 0000000..bd84487
--- /dev/null
+++ b/src/stores/cart.js
@@ -0,0 +1,102 @@
+import { defineStore } from 'pinia';
+import Notify from '@Plugins/Notify';
+
+const useCart = defineStore('cart', {
+ state: () => ({
+ items: [], // Productos en el carrito
+ paymentMethod: 'cash' // Método de pago por defecto
+ }),
+
+ getters: {
+ // Total de items
+ itemCount: (state) => state.items.reduce((sum, item) => sum + item.quantity, 0),
+
+ // Subtotal (sin impuestos)
+ subtotal: (state) => state.items.reduce((sum, item) => sum + (item.unit_price * item.quantity), 0),
+
+ // Total de impuestos
+ tax: (state) => {
+ return state.items.reduce((sum, item) => {
+ const itemSubtotal = item.unit_price * item.quantity;
+ const itemTax = (itemSubtotal * item.tax_rate) / 100;
+ return sum + itemTax;
+ }, 0);
+ },
+
+ // Total final
+ total: (state) => {
+ const subtotal = state.items.reduce((sum, item) => sum + (item.unit_price * item.quantity), 0);
+ const tax = state.items.reduce((sum, item) => {
+ const itemSubtotal = item.unit_price * item.quantity;
+ const itemTax = (itemSubtotal * item.tax_rate) / 100;
+ return sum + itemTax;
+ }, 0);
+ return subtotal + tax;
+ },
+
+ // Verificar si el carrito está vacío
+ isEmpty: (state) => state.items.length === 0
+ },
+
+ actions: {
+ // Agregar producto al carrito
+ addProduct(product) {
+ const existingItem = this.items.find(item => item.inventory_id === product.id);
+
+ if (existingItem) {
+ // Si ya existe, incrementar cantidad
+ if (existingItem.quantity < product.stock) {
+ existingItem.quantity++;
+ } else {
+ Notify.warning('No hay suficiente stock disponible');
+ }
+ } else {
+ // Agregar nuevo item
+ this.items.push({
+ inventory_id: product.id,
+ product_name: product.name,
+ sku: product.sku,
+ quantity: 1,
+ unit_price: parseFloat(product.price?.retail_price || 0),
+ tax_rate: parseFloat(product.price?.tax || 16),
+ max_stock: product.stock
+ });
+ }
+ },
+
+ // Actualizar cantidad de un item
+ updateQuantity(inventoryId, quantity) {
+ const item = this.items.find(i => i.inventory_id === inventoryId);
+ if (item) {
+ if (quantity <= 0) {
+ this.removeProduct(inventoryId);
+ } else if (quantity <= item.max_stock) {
+ item.quantity = quantity;
+ } else {
+ Notify.warning('No hay suficiente stock disponible');
+ }
+ }
+ },
+
+ // Remover producto
+ removeProduct(inventoryId) {
+ const index = this.items.findIndex(i => i.inventory_id === inventoryId);
+ if (index !== -1) {
+ this.items.splice(index, 1);
+ }
+ },
+
+ // Limpiar carrito
+ clear() {
+ this.items = [];
+ this.paymentMethod = 'cash';
+ },
+
+ // Cambiar método de pago
+ setPaymentMethod(method) {
+ this.paymentMethod = method;
+ }
+ }
+});
+
+export default useCart;
\ No newline at end of file
diff --git a/src/stores/cashRegister.js b/src/stores/cashRegister.js
new file mode 100644
index 0000000..c635829
--- /dev/null
+++ b/src/stores/cashRegister.js
@@ -0,0 +1,184 @@
+import { defineStore } from 'pinia';
+import cashRegisterService from '@Services/cashRegisterService';
+
+const useCashRegister = defineStore('cashRegister', {
+ state: () => ({
+ currentRegister: null, // Caja actualmente abierta
+ isOpen: false, // Estado de la caja
+ loading: false, // Indicador de carga
+ error: null // Error al cargar
+ }),
+
+ getters: {
+ // Verificar si hay caja abierta
+ hasOpenRegister: (state) => state.isOpen && state.currentRegister !== null,
+
+ // ID de la caja actual
+ currentRegisterId: (state) => state.currentRegister?.id || null,
+
+ // Usuario que abrió la caja
+ openedBy: (state) => state.currentRegister?.user?.name || 'N/A',
+
+ // Fecha y hora de apertura
+ openedAt: (state) => state.currentRegister?.opened_at || null,
+
+ // Efectivo inicial
+ initialCash: (state) => parseFloat(state.currentRegister?.initial_cash || 0),
+
+ // Total de ventas realizadas
+ totalSales: (state) => parseFloat(state.currentRegister?.total_sales || 0),
+
+ // Número de transacciones
+ transactionCount: (state) => parseInt(state.currentRegister?.transaction_count || 0),
+
+ // Efectivo esperado (inicial + ventas en efectivo)
+ expectedCash: (state) => {
+ const initial = parseFloat(state.currentRegister?.initial_cash || 0);
+ const cashSales = parseFloat(state.currentRegister?.cash_sales || 0);
+ return initial + cashSales;
+ },
+
+ // Ventas con tarjeta
+ cardSales: (state) => parseFloat(state.currentRegister?.card_sales || 0),
+
+ // Balance actual estimado
+ currentBalance: (state) => {
+ const initial = parseFloat(state.currentRegister?.initial_cash || 0);
+ const total = parseFloat(state.currentRegister?.total_sales || 0);
+ return initial + total;
+ }
+ },
+
+ actions: {
+ /**
+ * Cargar la caja registradora actual desde el backend
+ */
+ async loadCurrentRegister() {
+ this.loading = true;
+ this.error = null;
+
+ try {
+ const register = await cashRegisterService.getCurrentCashRegister();
+
+ // El servicio ya maneja el 404 y devuelve null
+ // También verifica que el status sea 'open'
+ if (register === null) {
+ this.currentRegister = null;
+ this.isOpen = false;
+ } else if (register && register.id) {
+ // El servicio ya devuelve el objeto register directamente
+ this.currentRegister = register;
+ // Verificar que el status sea 'open'
+ this.isOpen = register.status === 'open';
+ } else {
+ this.currentRegister = null;
+ this.isOpen = false;
+ }
+ } catch (error) {
+ // Si no hay caja abierta (404), no es un error
+ if (error.status === 404 || error.response?.status === 404) {
+ this.currentRegister = null;
+ this.isOpen = false;
+ } else {
+ this.error = error.message || 'Error al cargar la caja registradora';
+ // Aún así limpiamos el estado
+ this.currentRegister = null;
+ this.isOpen = false;
+ }
+ } finally {
+ this.loading = false;
+ }
+ },
+
+ /**
+ * Abrir una nueva caja registradora
+ * @param {Number} initialCash - Monto inicial en efectivo
+ */
+ async openRegister(initialCash) {
+ // Validar que no haya caja ya abierta
+ if (this.isOpen) {
+ this.error = 'Ya hay una caja abierta';
+ return { success: false, error: this.error };
+ }
+
+ this.loading = true;
+ this.error = null;
+
+ try {
+ const register = await cashRegisterService.openCashRegister(initialCash);
+
+ if (register && register.id) {
+ // El servicio ya devuelve el objeto register directamente
+ this.currentRegister = register;
+ this.isOpen = register.status === 'open';
+ return { success: true, data: register };
+ } else {
+ this.error = 'Respuesta inválida del servidor';
+ return { success: false, error: this.error };
+ }
+ } catch (error) {
+ this.error = extractErrorMessage(error, 'Error al abrir la caja');
+ return { success: false, error: this.error };
+ } finally {
+ this.loading = false;
+ }
+ },
+
+ /**
+ * Cerrar la caja registradora actual
+ * @param {Object} closeData - Datos del cierre (final_cash, notes)
+ */
+ async closeRegister(closeData) {
+ if (!this.currentRegister?.id) {
+ this.error = 'No hay caja abierta para cerrar';
+ return { success: false, error: this.error };
+ }
+
+ this.loading = true;
+ this.error = null;
+
+ try {
+ const register = await cashRegisterService.closeCashRegister(
+ this.currentRegister.id,
+ closeData
+ );
+
+ if (register) {
+ // Limpiar estado
+ this.currentRegister = null;
+ this.isOpen = false;
+ return { success: true, data: register };
+ } else {
+ this.error = 'Respuesta inválida del servidor';
+ return { success: false, error: this.error };
+ }
+ } catch (error) {
+ this.error = extractErrorMessage(error, 'Error al cerrar la caja');
+ return { success: false, error: this.error };
+ } finally {
+ this.loading = false;
+ }
+ },
+
+ /**
+ * Actualizar datos de la caja actual (útil después de una venta)
+ */
+ async refreshCurrentRegister() {
+ if (this.isOpen) {
+ await this.loadCurrentRegister();
+ }
+ },
+
+ /**
+ * Limpiar el estado
+ */
+ reset() {
+ this.currentRegister = null;
+ this.isOpen = false;
+ this.loading = false;
+ this.error = null;
+ }
+ }
+});
+
+export default useCashRegister;