feature-comercial-module-ts #13

Merged
edgar.mendez merged 38 commits from feature-comercial-module-ts into develop 2026-03-04 15:07:09 +00:00
11 changed files with 656 additions and 328 deletions
Showing only changes of commit e1521ef9c7 - Show all commits

2
components.d.ts vendored
View File

@ -20,8 +20,10 @@ declare module 'vue' {
Column: typeof import('primevue/column')['default']
DataTable: typeof import('primevue/datatable')['default']
HelloWorld: typeof import('./src/components/HelloWorld.vue')['default']
IconField: typeof import('primevue/iconfield')['default']
InputGroup: typeof import('primevue/inputgroup')['default']
InputGroupAddon: typeof import('primevue/inputgroupaddon')['default']
InputIcon: typeof import('primevue/inputicon')['default']
InputText: typeof import('primevue/inputtext')['default']
KpiCard: typeof import('./src/components/shared/KpiCard.vue')['default']
Menu: typeof import('primevue/menu')['default']

280
package-lock.json generated
View File

@ -12,6 +12,7 @@
"@primevue/auto-import-resolver": "^4.4.1",
"@tailwindcss/vite": "^4.1.16",
"@vueuse/core": "^14.0.0",
"axios": "^1.13.2",
"primeicons": "^7.0.0",
"primevue": "^4.4.1",
"tailwindcss-primeui": "^0.6.1",
@ -1448,6 +1449,36 @@
"dev": true,
"license": "MIT"
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"license": "MIT"
},
"node_modules/axios": {
"version": "1.13.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz",
"integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.4",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/call-bind-apply-helpers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/chokidar": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
@ -1463,6 +1494,18 @@
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"license": "MIT",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/confbox": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.2.tgz",
@ -1492,6 +1535,15 @@
}
}
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"license": "MIT",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/detect-libc": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
@ -1501,6 +1553,20 @@
"node": ">=8"
}
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
"es-errors": "^1.3.0",
"gopd": "^1.2.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/enhanced-resolve": {
"version": "5.18.3",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz",
@ -1526,6 +1592,51 @@
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
"node_modules/es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-errors": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-object-atoms": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-set-tostringtag": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.6",
"has-tostringtag": "^1.0.2",
"hasown": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/esbuild": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
@ -1596,6 +1707,42 @@
}
}
},
"node_modules/follow-redirects": {
"version": "1.15.11",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"license": "MIT",
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/form-data": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.2",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@ -1610,12 +1757,109 @@
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-intrinsic": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"es-define-property": "^1.0.1",
"es-errors": "^1.3.0",
"es-object-atoms": "^1.1.1",
"function-bind": "^1.1.2",
"get-proto": "^1.0.1",
"gopd": "^1.2.0",
"has-symbols": "^1.1.0",
"hasown": "^2.0.2",
"math-intrinsics": "^1.1.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"license": "MIT",
"dependencies": {
"dunder-proto": "^1.0.1",
"es-object-atoms": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/graceful-fs": {
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
"license": "ISC"
},
"node_modules/has-symbols": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-tostringtag": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"license": "MIT",
"dependencies": {
"has-symbols": "^1.0.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"license": "MIT",
"dependencies": {
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/jiti": {
"version": "2.6.1",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz",
@ -1900,6 +2144,36 @@
"@jridgewell/sourcemap-codec": "^1.5.5"
}
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mlly": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.0.tgz",
@ -2052,6 +2326,12 @@
"node": ">=12.11.0"
}
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"license": "MIT"
},
"node_modules/quansync": {
"version": "0.2.11",
"resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz",

View File

@ -13,6 +13,7 @@
"@primevue/auto-import-resolver": "^4.4.1",
"@tailwindcss/vite": "^4.1.16",
"@vueuse/core": "^14.0.0",
"axios": "^1.13.2",
"primeicons": "^7.0.0",
"primevue": "^4.4.1",
"tailwindcss-primeui": "^0.6.1",

View File

@ -2,7 +2,7 @@
import { ref } from 'vue';
import { useRouter } from 'vue-router';
import { useLayout } from "../../composables/useLayout";
import { useAuth } from "../../stores/auth";
import { useAuth } from "../../modules/auth/composables/useAuth";
const router = useRouter();
const { isDarkMode, toggleDarkMode } = useLayout();

View File

@ -7,7 +7,7 @@ import StyleClass from "primevue/styleclass";
import { createApp } from "vue";
import App from "./App.vue";
import router from "./router";
import { useAuth } from "./stores/auth";
import { useAuth } from "./modules/auth/composables/useAuth";
// Crear un preset personalizado basado en Aura con color azul
const MyPreset = definePreset(Aura, {

View File

@ -0,0 +1,195 @@
<script setup lang="ts">
import { ref, computed } from 'vue';
import { useAuth } from '../composables/useAuth';
import { useRouter } from 'vue-router';
const router = useRouter();
const { login, isLoading } = useAuth();
const email = ref('');
const password = ref('');
const remember = ref(false);
const showPassword = ref(false);
const errorMessage = ref('');
const isFormValid = computed(() => {
return email.value.trim() !== '' && password.value.trim() !== '';
});
const handleLogin = async () => {
errorMessage.value = '';
const result = await login({
email: email.value,
password: password.value
});
if (result.success) {
router.push('/');
} else {
errorMessage.value = result.error || 'Error al iniciar sesión';
}
};
const handleKeyPress = (event: KeyboardEvent) => {
if (event.key === 'Enter' && isFormValid.value) {
handleLogin();
}
};
</script>
<template>
<div class="flex min-h-screen bg-surface-50 dark:bg-surface-950">
<!-- Left Column: Image Panel -->
<div
class="relative hidden lg:flex lg:w-1/2 flex-col justify-between p-8 text-white bg-cover bg-center bg-no-repeat"
style="background-image: url('https://lh3.googleusercontent.com/aida-public/AB6AXuBTdTKvMQYppS4BwZhrgylD8oKxLJa13js2PIRUwAQF5XbEqqNU2VCYyvJEQTWdKdoCTYHiAm_RGoobkJ3Rt4xd-XJUcshYKfCBrF0fZcBJOFR9UJ0Vh2WuLIA8g_xmKA8Fn7hdh4KPqJC7X_IOg5UPi5RRzqxB-Xn2tMbaEk-n1h8mYXfnKSIV7C0up-YreeZdP9GeznZN6DCzjy8TLyxw03gXdmhHziZC6hzahwWemMrtS8W5sX028-G1tnxivu1v2o8oETFyh5Wi')"
>
<!-- Overlay oscuro -->
<div class="absolute inset-0 bg-black/50"></div>
<!-- Logo y nombre -->
<div class="relative z-10 flex items-center gap-3">
<i class="pi pi-box text-4xl"></i>
<span class="text-xl font-bold">Golscontrols ERP</span>
</div>
<!-- Texto principal -->
<div class="relative z-10">
<h1 class="text-4xl font-black leading-tight tracking-tight">
Sistema Integral de Gestión Empresarial
</h1>
<p class="mt-2 max-w-md text-lg text-white/80">
Optimiza tus operaciones, gestiona inventarios y aumenta la eficiencia desde un solo panel.
</p>
</div>
</div>
<!-- Right Column: Form Panel -->
<div class="flex w-full lg:w-1/2 flex-col items-center justify-center px-4 py-12">
<div class="w-full max-w-md space-y-8">
<!-- Header -->
<div>
<h1 class="text-3xl font-bold tracking-tight text-surface-900 dark:text-white">
Bienvenido de nuevo
</h1>
<p class="mt-2 text-base text-surface-600 dark:text-surface-300">
Ingresa tus credenciales para acceder a tu cuenta.
</p>
</div>
<!-- Mensaje de error -->
<Message
v-if="errorMessage"
severity="error"
:closable="true"
@close="errorMessage = ''"
>
{{ errorMessage }}
</Message>
<!-- Info de prueba (solo para desarrollo) -->
<Message severity="info" :closable="false">
<div class="text-sm">
<p class="font-semibold mb-1">Credenciales de prueba:</p>
<p><strong>Email:</strong> admin@gols.com</p>
<p><strong>Password:</strong> admin123</p>
</div>
</Message>
<!-- Formulario -->
<form @submit.prevent="handleLogin" class="space-y-6">
<!-- Email -->
<div class="flex flex-col gap-2">
<label for="email" class="text-sm font-medium text-surface-900 dark:text-surface-200">
Correo Electrónico
</label>
<IconField>
<InputIcon class="pi pi-user" />
<InputText
id="email"
v-model="email"
type="email"
placeholder="you@example.com"
:disabled="isLoading"
@keypress="handleKeyPress"
class="w-full"
autocomplete="email"
size="large"
/>
</IconField>
</div>
<!-- Password -->
<div class="flex flex-col gap-2">
<label for="password" class="text-sm font-medium text-surface-900 dark:text-surface-200">
Contraseña
</label>
<IconField>
<InputIcon class="pi pi-lock" />
<InputText
id="password"
v-model="password"
:type="showPassword ? 'text' : 'password'"
placeholder="Ingresa tu contraseña"
:disabled="isLoading"
@keypress="handleKeyPress"
class="w-full pr-10"
autocomplete="current-password"
size="large"
/>
<button
type="button"
@click="showPassword = !showPassword"
class="absolute right-3 top-1/2 -translate-y-1/2 text-surface-400 hover:text-surface-600 dark:hover:text-surface-200"
tabindex="-1"
>
<i :class="showPassword ? 'pi pi-eye-slash' : 'pi pi-eye'"></i>
</button>
</IconField>
</div>
<!-- Remember me y Forgot password -->
<!-- <div class="flex items-center justify-between">
<div class="flex items-center gap-2">
<Checkbox
inputId="remember-me"
v-model="remember"
:binary="true"
/>
<label for="remember-me" class="text-sm text-surface-700 dark:text-surface-300 cursor-pointer">
Remember Me
</label>
</div>
<a href="#" class="text-sm font-medium text-primary hover:text-primary/80">
Forgot Password?
</a>
</div> -->
<!-- Botón de Login -->
<Button
type="submit"
label="Iniciar Sesión"
:loading="isLoading"
:disabled="!isFormValid || isLoading"
class="w-full"
size="large"
severity="primary"
/>
</form>
<!-- Copyright -->
<p class="text-center text-sm text-surface-500 dark:text-surface-400">
© 2025 Grupo Golsystems, Todos los derechos reservados.
</p>
</div>
</div>
</div>
</template>
<style scoped>
/* Ajuste para el botón de toggle password */
.p-iconfield {
position: relative;
}
</style>

View File

@ -0,0 +1,135 @@
import api from '../../../services/api';
import type { User, LoginCredentials } from '../composables/useAuth';
export interface LoginResponse {
user: User;
token: string;
}
export interface RegisterData {
name: string;
email: string;
password: string;
password_confirmation: string;
}
class AuthService {
/**
* Iniciar sesión
*/
async login(credentials: LoginCredentials): Promise<LoginResponse> {
try {
// En producción, esto haría una llamada real al backend
// const response = await api.post<LoginResponse>('/auth/login', credentials);
// Simulación para desarrollo
return new Promise((resolve, reject) => {
setTimeout(() => {
if (credentials.email === 'admin@gols.com' && credentials.password === 'admin123') {
resolve({
user: {
id: 1,
name: 'John Doe',
email: credentials.email,
avatar: '',
role: 'admin'
},
token: 'mock-jwt-token-' + Date.now()
});
} else {
reject(new Error('Credenciales inválidas'));
}
}, 1000);
});
} catch (error: any) {
throw new Error(error.response?.data?.message || 'Error al iniciar sesión');
}
}
/**
* Registrar nuevo usuario
*/
async register(data: RegisterData): Promise<LoginResponse> {
try {
const response = await api.post<LoginResponse>('/auth/register', data);
return response.data;
} catch (error: any) {
throw new Error(error.response?.data?.message || 'Error al registrar usuario');
}
}
/**
* Cerrar sesión
*/
async logout(): Promise<void> {
try {
// Notificar al backend que se cerró sesión
await api.post('/auth/logout');
} catch (error) {
console.error('Error al cerrar sesión:', error);
}
}
/**
* Obtener usuario actual
*/
async getCurrentUser(): Promise<User> {
try {
const response = await api.get<User>('/auth/me');
return response.data;
} catch (error: any) {
throw new Error(error.response?.data?.message || 'Error al obtener usuario');
}
}
/**
* Actualizar perfil de usuario
*/
async updateProfile(data: Partial<User>): Promise<User> {
try {
const response = await api.put<User>('/auth/profile', data);
return response.data;
} catch (error: any) {
throw new Error(error.response?.data?.message || 'Error al actualizar perfil');
}
}
/**
* Cambiar contraseña
*/
async changePassword(currentPassword: string, newPassword: string): Promise<void> {
try {
await api.post('/auth/change-password', {
current_password: currentPassword,
new_password: newPassword
});
} catch (error: any) {
throw new Error(error.response?.data?.message || 'Error al cambiar contraseña');
}
}
/**
* Solicitar recuperación de contraseña
*/
async forgotPassword(email: string): Promise<void> {
try {
await api.post('/auth/forgot-password', { email });
} catch (error: any) {
throw new Error(error.response?.data?.message || 'Error al solicitar recuperación');
}
}
/**
* Resetear contraseña con token
*/
async resetPassword(token: string, password: string): Promise<void> {
try {
await api.post('/auth/reset-password', { token, password });
} catch (error: any) {
throw new Error(error.response?.data?.message || 'Error al resetear contraseña');
}
}
}
export const authService = new AuthService();
export default authService;

View File

@ -1,210 +0,0 @@
<script setup lang="ts">
import { ref, computed } from 'vue';
import { useAuth } from '../../stores/auth';
import { useRouter } from 'vue-router';
const router = useRouter();
const { login, isLoading } = useAuth();
const email = ref('');
const password = ref('');
const remember = ref(false);
const showPassword = ref(false);
const errorMessage = ref('');
const isFormValid = computed(() => {
return email.value.trim() !== '' && password.value.trim() !== '';
});
const handleLogin = async () => {
errorMessage.value = '';
const result = await login({
email: email.value,
password: password.value
});
if (result.success) {
router.push('/');
} else {
errorMessage.value = result.error || 'Error al iniciar sesión';
}
};
const handleKeyPress = (event: KeyboardEvent) => {
if (event.key === 'Enter' && isFormValid.value) {
handleLogin();
}
};
</script>
<template>
<div class="min-h-screen flex items-center justify-center bg-surface-50 dark:bg-surface-950 p-4">
<div class="w-full max-w-md">
<!-- Card de Login -->
<div class="bg-surface-0 dark:bg-surface-900 rounded-2xl shadow-xl border border-surface-200 dark:border-surface-800 overflow-hidden">
<!-- Header con logo y título -->
<div class="bg-linear-to-br from-primary to-primary-600 p-8 text-center">
<div class="flex items-center justify-center mb-4">
<div class="w-16 h-16 bg-white/20 backdrop-blur-sm rounded-2xl flex items-center justify-center">
<i class="pi pi-chart-line text-4xl text-white"></i>
</div>
</div>
<h1 class="text-3xl font-bold text-white mb-2">GOLS Control</h1>
<p class="text-primary-100">Sistema de Gestión Empresarial</p>
</div>
<!-- Formulario -->
<div class="p-8">
<h2 class="text-2xl font-semibold text-surface-900 dark:text-surface-0 mb-2">
Iniciar Sesión
</h2>
<p class="text-surface-600 dark:text-surface-400 mb-6">
Ingresa tus credenciales para continuar
</p>
<!-- Mensaje de error -->
<Message
v-if="errorMessage"
severity="error"
:closable="false"
class="mb-4"
>
{{ errorMessage }}
</Message>
<!-- Info de prueba -->
<Message severity="info" :closable="false" class="mb-6">
<div class="text-sm">
<p class="font-semibold mb-1">Credenciales de prueba:</p>
<p><strong>Email:</strong> admin@gols.com</p>
<p><strong>Password:</strong> admin123</p>
</div>
</Message>
<form @submit.prevent="handleLogin" class="space-y-6">
<!-- Email -->
<div class="space-y-2">
<label for="email" class="block text-sm font-medium text-surface-700 dark:text-surface-300">
Correo Electrónico
</label>
<InputGroup>
<InputGroupAddon>
<i class="pi pi-envelope"></i>
</InputGroupAddon>
<InputText
id="email"
v-model="email"
type="email"
placeholder="tu@email.com"
:disabled="isLoading"
@keypress="handleKeyPress"
class="w-full"
autocomplete="email"
/>
</InputGroup>
</div>
<!-- Password -->
<div class="space-y-2">
<label for="password" class="block text-sm font-medium text-surface-700 dark:text-surface-300">
Contraseña
</label>
<InputGroup>
<InputGroupAddon>
<i class="pi pi-lock"></i>
</InputGroupAddon>
<InputText
id="password"
v-model="password"
:type="showPassword ? 'text' : 'password'"
placeholder="••••••••"
:disabled="isLoading"
@keypress="handleKeyPress"
class="w-full"
autocomplete="current-password"
/>
<InputGroupAddon class="cursor-pointer" @click="showPassword = !showPassword">
<i :class="showPassword ? 'pi pi-eye-slash' : 'pi pi-eye'"></i>
</InputGroupAddon>
</InputGroup>
</div>
<!-- Recordar sesión -->
<div class="flex items-center justify-between">
<div class="flex items-center gap-2">
<Checkbox
inputId="remember"
v-model="remember"
:binary="true"
/>
<label for="remember" class="text-sm text-surface-700 dark:text-surface-300 cursor-pointer">
Recordar sesión
</label>
</div>
<a href="#" class="text-sm text-primary hover:text-primary-700 font-medium">
¿Olvidaste tu contraseña?
</a>
</div>
<!-- Botón de login -->
<Button
type="submit"
label="Iniciar Sesión"
icon="pi pi-sign-in"
:loading="isLoading"
:disabled="!isFormValid || isLoading"
class="w-full"
size="large"
/>
</form>
<!-- Separador -->
<div class="relative my-6">
<div class="absolute inset-0 flex items-center">
<div class="w-full border-t border-surface-300 dark:border-surface-700"></div>
</div>
<div class="relative flex justify-center text-sm">
<span class="px-4 bg-surface-0 dark:bg-surface-900 text-surface-500 dark:text-surface-400">
O continuar con
</span>
</div>
</div>
<!-- Botones sociales -->
<div class="grid grid-cols-2 gap-3">
<Button
label="Google"
icon="pi pi-google"
outlined
severity="secondary"
class="w-full"
/>
<Button
label="Microsoft"
icon="pi pi-microsoft"
outlined
severity="secondary"
class="w-full"
/>
</div>
</div>
<!-- Footer -->
<div class="px-8 py-6 bg-surface-50 dark:bg-surface-950 border-t border-surface-200 dark:border-surface-800 text-center">
<p class="text-sm text-surface-600 dark:text-surface-400">
¿No tienes una cuenta?
<a href="#" class="text-primary hover:text-primary-700 font-medium">
Solicitar acceso
</a>
</p>
</div>
</div>
<!-- Copyright -->
<p class="text-center text-sm text-surface-500 dark:text-surface-400 mt-8">
© 2025 GOLS Control. Todos los derechos reservados.
</p>
</div>
</div>
</template>

View File

@ -1,8 +1,8 @@
import { createRouter, createWebHistory, type RouteRecordRaw } from 'vue-router';
import { useAuth } from '../stores/auth';
import { useAuth } from '../modules/auth/composables/useAuth';
// Importar vistas
import Login from '../pages/Auth/Login.vue';
import Login from '../modules/auth/components/Login.vue';
import MainLayout from '../MainLayout.vue';
import WarehouseDashboard from '../modules/warehouse/components/WarehouseDashboard.vue';

View File

@ -1,120 +1,45 @@
// Servicio API base para todas las peticiones HTTP
import axios from 'axios';
import type { InternalAxiosRequestConfig } from 'axios';
const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:3000/api';
const api = axios.create({
baseURL: import.meta.env.VITE_API_URL || 'http://localhost:3000',
timeout: 10000,
headers: {
'Content-Type': 'application/json',
},
});
interface RequestOptions extends RequestInit {
params?: Record<string, string>;
}
class ApiService {
private baseUrl: string;
constructor(baseUrl: string) {
this.baseUrl = baseUrl;
// Interceptor para agregar el Bearer Token a cada petición
api.interceptors.request.use(
(config: InternalAxiosRequestConfig) => {
const token = localStorage.getItem('auth_token'); // Ajusta según dónde guardes el token
if (token && config.headers) {
config.headers['Authorization'] = `Bearer ${token}`;
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
/**
* Construye la URL con query params
*/
private buildUrl(endpoint: string, params?: Record<string, string>): string {
const url = new URL(`${this.baseUrl}${endpoint}`);
if (params) {
Object.entries(params).forEach(([key, value]) => {
url.searchParams.append(key, value);
});
}
return url.toString();
// Interceptor para manejar errores de forma global
api.interceptors.response.use(
(response) => response,
(error) => {
if (error.response) {
// El servidor respondió con un código de estado fuera del rango 2xx
console.error(error.response.data);
// Manejar error 401 (no autorizado) - redirigir al login
if (error.response.status === 401) {
localStorage.removeItem('auth_token');
localStorage.removeItem('auth_user');
window.location.href = '/login';
}
}
return Promise.reject(error);
}
);
/**
* Maneja la respuesta de la API
*/
private async handleResponse<T>(response: Response): Promise<T> {
if (!response.ok) {
const error = await response.json().catch(() => ({ message: 'Error desconocido' }));
throw new Error(error.message || `HTTP Error: ${response.status}`);
}
return response.json();
}
/**
* GET request
*/
async get<T>(endpoint: string, options?: RequestOptions): Promise<T> {
const url = this.buildUrl(endpoint, options?.params);
const response = await fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
...options?.headers,
},
});
return this.handleResponse<T>(response);
}
/**
* POST request
*/
async post<T>(endpoint: string, data?: any, options?: RequestOptions): Promise<T> {
const url = this.buildUrl(endpoint, options?.params);
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...options?.headers,
},
body: JSON.stringify(data),
});
return this.handleResponse<T>(response);
}
/**
* PUT request
*/
async put<T>(endpoint: string, data?: any, options?: RequestOptions): Promise<T> {
const url = this.buildUrl(endpoint, options?.params);
const response = await fetch(url, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
...options?.headers,
},
body: JSON.stringify(data),
});
return this.handleResponse<T>(response);
}
/**
* DELETE request
*/
async delete<T>(endpoint: string, options?: RequestOptions): Promise<T> {
const url = this.buildUrl(endpoint, options?.params);
const response = await fetch(url, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
...options?.headers,
},
});
return this.handleResponse<T>(response);
}
/**
* PATCH request
*/
async patch<T>(endpoint: string, data?: any, options?: RequestOptions): Promise<T> {
const url = this.buildUrl(endpoint, options?.params);
const response = await fetch(url, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
...options?.headers,
},
body: JSON.stringify(data),
});
return this.handleResponse<T>(response);
}
}
// Exportar instancia singleton
export const api = new ApiService(API_BASE_URL);
export default api;