diff --git a/.husky/pre-commit b/.husky/pre-commit
index 2601a3b..d0a7784 100644
--- a/.husky/pre-commit
+++ b/.husky/pre-commit
@@ -1 +1 @@
-npm run precommit
\ No newline at end of file
+npx lint-staged
\ No newline at end of file
diff --git a/.lintstagedrc b/.lintstagedrc
index 9458d0b..4c57188 100644
--- a/.lintstagedrc
+++ b/.lintstagedrc
@@ -1,10 +1,9 @@
{
"*/**/*.{js,jsx,ts,tsx}": [
- "prettier --write",
- "eslint --fix",
- "eslint"
+ "npm run format",
+ "npm run lint:fix"
],
"*/**/*.{json,css,md}": [
- "prettier --write"
+ "npm run format"
]
}
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index 9a17168..99710f2 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,15 +1,16 @@
{
"name": "dashboard",
- "version": "0.3.0",
+ "version": "0.4.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "dashboard",
- "version": "0.3.0",
+ "version": "0.4.0",
"dependencies": {
"@heroicons/react": "^2.2.0",
- "@pharmatech/sdk": "^0.4.16",
+ "@pharmatech/sdk": "^0.4.21",
+ "@react-google-maps/api": "^2.20.6",
"@react-pdf/renderer": "^4.3.0",
"blob-stream": "^0.1.3",
"cloudinary": "^2.6.0",
@@ -1096,6 +1097,22 @@
"integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==",
"license": "MIT"
},
+ "node_modules/@googlemaps/js-api-loader": {
+ "version": "1.16.8",
+ "resolved": "https://registry.npmjs.org/@googlemaps/js-api-loader/-/js-api-loader-1.16.8.tgz",
+ "integrity": "sha512-CROqqwfKotdO6EBjZO/gQGVTbeDps5V7Mt9+8+5Q+jTg5CRMi3Ii/L9PmV3USROrt2uWxtGzJHORmByxyo9pSQ==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/@googlemaps/markerclusterer": {
+ "version": "2.5.3",
+ "resolved": "https://registry.npmjs.org/@googlemaps/markerclusterer/-/markerclusterer-2.5.3.tgz",
+ "integrity": "sha512-x7lX0R5yYOoiNectr10wLgCBasNcXFHiADIBdmn7jQllF2B5ENQw5XtZK+hIw4xnV0Df0xhN4LN98XqA5jaiOw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.3",
+ "supercluster": "^8.0.1"
+ }
+ },
"node_modules/@heroicons/react": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@heroicons/react/-/react-2.2.0.tgz",
@@ -1809,9 +1826,10 @@
}
},
"node_modules/@pharmatech/sdk": {
- "version": "0.4.16",
- "resolved": "https://registry.npmjs.org/@pharmatech/sdk/-/sdk-0.4.16.tgz",
- "integrity": "sha512-J8ycNdl+x7h7HH0PDJVXUzKyT9n0WfykHyg8gJTw37nziuQJcY5GVpo6gCmlG4bj78ucbUICQGJqc2zVE6Q8UQ==",
+ "version": "0.4.21",
+ "resolved": "https://registry.npmjs.org/@pharmatech/sdk/-/sdk-0.4.21.tgz",
+ "integrity": "sha512-fxyXlgKN3qLxuGVg6bmRS5DhqfLOLsCYazffax5x0iYFFBIQHQBVRLltm5h84jvOEM0oA7ipD08VVXWMyBj/vQ==",
+ "license": "MIT",
"dependencies": {
"axios": "^1.8.1"
}
@@ -1826,6 +1844,36 @@
"node": ">=14"
}
},
+ "node_modules/@react-google-maps/api": {
+ "version": "2.20.6",
+ "resolved": "https://registry.npmjs.org/@react-google-maps/api/-/api-2.20.6.tgz",
+ "integrity": "sha512-frxkSHWbd36ayyxrEVopSCDSgJUT1tVKXvQld2IyzU3UnDuqqNA3AZE4/fCdqQb2/zBQx3nrWnZB1wBXDcrjcw==",
+ "license": "MIT",
+ "dependencies": {
+ "@googlemaps/js-api-loader": "1.16.8",
+ "@googlemaps/markerclusterer": "2.5.3",
+ "@react-google-maps/infobox": "2.20.0",
+ "@react-google-maps/marker-clusterer": "2.20.0",
+ "@types/google.maps": "3.58.1",
+ "invariant": "2.2.4"
+ },
+ "peerDependencies": {
+ "react": "^16.8 || ^17 || ^18 || ^19",
+ "react-dom": "^16.8 || ^17 || ^18 || ^19"
+ }
+ },
+ "node_modules/@react-google-maps/infobox": {
+ "version": "2.20.0",
+ "resolved": "https://registry.npmjs.org/@react-google-maps/infobox/-/infobox-2.20.0.tgz",
+ "integrity": "sha512-03PJHjohhaVLkX6+NHhlr8CIlvUxWaXhryqDjyaZ8iIqqix/nV8GFdz9O3m5OsjtxtNho09F/15j14yV0nuyLQ==",
+ "license": "MIT"
+ },
+ "node_modules/@react-google-maps/marker-clusterer": {
+ "version": "2.20.0",
+ "resolved": "https://registry.npmjs.org/@react-google-maps/marker-clusterer/-/marker-clusterer-2.20.0.tgz",
+ "integrity": "sha512-tieX9Va5w1yP88vMgfH1pHTacDQ9TgDTjox3tLlisKDXRQWdjw+QeVVghhf5XqqIxXHgPdcGwBvKY6UP+SIvLw==",
+ "license": "MIT"
+ },
"node_modules/@react-pdf/fns": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@react-pdf/fns/-/fns-3.1.2.tgz",
@@ -2454,6 +2502,12 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@types/google.maps": {
+ "version": "3.58.1",
+ "resolved": "https://registry.npmjs.org/@types/google.maps/-/google.maps-3.58.1.tgz",
+ "integrity": "sha512-X9QTSvGJ0nCfMzYOnaVs/k6/4L+7F5uCS+4iUmkLEls6J9S/Phv+m/i3mDeyc49ZBgwab3EFO1HEoBY7k98EGQ==",
+ "license": "MIT"
+ },
"node_modules/@types/json-schema": {
"version": "7.0.15",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
@@ -3435,9 +3489,10 @@
}
},
"node_modules/axios": {
- "version": "1.8.3",
- "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.3.tgz",
- "integrity": "sha512-iP4DebzoNlP/YN2dpwCgb8zoCmhtkajzS48JvwmkSkXvPI3DHc7m+XYL5tGnSlJtR6nImXZmdCuN5aP8dh1d8A==",
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz",
+ "integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==",
+ "license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.0",
@@ -5241,6 +5296,7 @@
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
+ "license": "MIT",
"engines": {
"node": ">=4.0"
},
@@ -5821,6 +5877,15 @@
"node": ">=12"
}
},
+ "node_modules/invariant": {
+ "version": "2.2.4",
+ "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
+ "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.0.0"
+ }
+ },
"node_modules/is-array-buffer": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
@@ -6433,6 +6498,12 @@
"node": ">=18"
}
},
+ "node_modules/kdbush": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/kdbush/-/kdbush-4.0.2.tgz",
+ "integrity": "sha512-WbCVYJ27Sz8zi9Q7Q0xHC+05iwkm3Znipc2XTlrnJbsHMYktW4hPhXUE8Ys1engBrvffoSCqbil1JQAa7clRpA==",
+ "license": "ISC"
+ },
"node_modules/keyv": {
"version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
@@ -7662,7 +7733,8 @@
"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=="
+ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
+ "license": "MIT"
},
"node_modules/punycode": {
"version": "2.3.1",
@@ -8860,6 +8932,15 @@
"node": ">= 6"
}
},
+ "node_modules/supercluster": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/supercluster/-/supercluster-8.0.1.tgz",
+ "integrity": "sha512-IiOea5kJ9iqzD2t7QJq/cREyLHTtSmUT6gQsweojg9WH2sYJqZK9SswTu6jrscO6D1G5v5vYZ9ru/eq85lXeZQ==",
+ "license": "ISC",
+ "dependencies": {
+ "kdbush": "^4.0.2"
+ }
+ },
"node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
diff --git a/package.json b/package.json
index d0e55e5..0ad9c7c 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "dashboard",
- "version": "0.4.0",
+ "version": "1.0.0",
"private": true,
"scripts": {
"dev": "next dev",
@@ -17,7 +17,8 @@
},
"dependencies": {
"@heroicons/react": "^2.2.0",
- "@pharmatech/sdk": "^0.4.16",
+ "@pharmatech/sdk": "^0.4.21",
+ "@react-google-maps/api": "^2.20.6",
"@react-pdf/renderer": "^4.3.0",
"blob-stream": "^0.1.3",
"cloudinary": "^2.6.0",
diff --git a/src/app/(dashboard)/branches/[id]/edit/page.tsx b/src/app/(dashboard)/branches/[id]/edit/page.tsx
index 889adaa..e726129 100644
--- a/src/app/(dashboard)/branches/[id]/edit/page.tsx
+++ b/src/app/(dashboard)/branches/[id]/edit/page.tsx
@@ -203,7 +203,6 @@ export default function EditBranchPage() {
helperText={errors.name}
helperTextColor={Colors.semanticDanger}
borderSize="1px"
- borderColor="#E7E7E6"
onChange={(e) => setName(e.target.value)}
/>
@@ -249,7 +248,6 @@ export default function EditBranchPage() {
helperText={errors.address}
helperTextColor={Colors.semanticDanger}
borderSize="1px"
- borderColor="#E7E7E6"
onChange={(e) => setAddress(e.target.value)}
/>
@@ -263,7 +261,6 @@ export default function EditBranchPage() {
helperText={errors.latitude}
helperTextColor={Colors.semanticDanger}
borderSize="1px"
- borderColor="#E7E7E6"
onChange={(e) => setLatitude(e.target.value)}
/>
@@ -275,7 +272,6 @@ export default function EditBranchPage() {
helperText={errors.longitude}
helperTextColor={Colors.semanticDanger}
borderSize="1px"
- borderColor="#E7E7E6"
onChange={(e) => setLongitude(e.target.value)}
/>
diff --git a/src/app/(dashboard)/branches/[id]/page.tsx b/src/app/(dashboard)/branches/[id]/page.tsx
index afffbca..112c83e 100644
--- a/src/app/(dashboard)/branches/[id]/page.tsx
+++ b/src/app/(dashboard)/branches/[id]/page.tsx
@@ -12,6 +12,7 @@ import { REDIRECTION_TIMEOUT } from '@/lib/utils/contants';
import { useAuth } from '@/context/AuthContext';
import { BranchResponse } from '@pharmatech/sdk';
import Input from '@/components/Input/Input';
+import Loader from '@/components/Loader';
export default function BranchDetailsPage() {
const params = useParams();
@@ -165,7 +166,7 @@ export default function BranchDetailsPage() {
) : (
-
Cargando datos de la sucursal...
+
)}
>
);
diff --git a/src/app/(dashboard)/branches/new/page.tsx b/src/app/(dashboard)/branches/new/page.tsx
index cb090d9..460e991 100644
--- a/src/app/(dashboard)/branches/new/page.tsx
+++ b/src/app/(dashboard)/branches/new/page.tsx
@@ -10,6 +10,8 @@ import Dropdown from '@/components/Dropdown';
import { toast } from 'react-toastify';
import { StateResponse, CityResponse } from '@pharmatech/sdk';
import Input from '@/components/Input/Input';
+import GoogleMaps from '@/components/GoogleMap/GoogleMap';
+//import { BranchMarker } from '@/components/GoogleMap/GoogleMap';
/// This is a constant that represents the ID of Venezuela.
const COUNTRY_ID = '1238bc2a-45a5-47e4-9cc1-68d573089ca1';
@@ -29,6 +31,12 @@ export default function NewBranchPage() {
const [cityId, setCityId] = useState('');
const [errors, setErrors] = useState>({});
+ // No branches data available, so markers will be empty
+
+ const mapCenter = {
+ lat: parseFloat(latitude || '10.0653'),
+ lng: parseFloat(longitude || '-69.3235'),
+ };
const fetchStates = async () => {
try {
@@ -188,7 +196,6 @@ export default function NewBranchPage() {
helperText={errors.name}
helperTextColor={Colors.semanticDanger}
borderSize="1px"
- borderColor="#E7E7E6"
value={name}
onChange={(e) => setName(e.target.value)}
/>
@@ -233,7 +240,6 @@ export default function NewBranchPage() {
helperText={errors.address}
helperTextColor={Colors.semanticDanger}
borderSize="1px"
- borderColor="#E7E7E6"
onChange={(e) => setAddress(e.target.value)}
/>
@@ -247,7 +253,6 @@ export default function NewBranchPage() {
helperText={errors.latitude}
helperTextColor={Colors.semanticDanger}
borderSize="1px"
- borderColor="#E7E7E6"
onChange={(e) => setLatitude(e.target.value)}
/>
@@ -259,10 +264,21 @@ export default function NewBranchPage() {
helperText={errors.longitude}
helperTextColor={Colors.semanticDanger}
borderSize="1px"
- borderColor="#E7E7E6"
onChange={(e) => setLongitude(e.target.value)}
/>
+
+ {
+ setLatitude(lat.toFixed(6).toString());
+ setLongitude(lng.toFixed(6).toString());
+ }}
+ mapWidth="200%"
+ mapHeight="220px"
+ />
+
>
diff --git a/src/app/(dashboard)/branches/page.tsx b/src/app/(dashboard)/branches/page.tsx
index bc72cfa..1d70f03 100644
--- a/src/app/(dashboard)/branches/page.tsx
+++ b/src/app/(dashboard)/branches/page.tsx
@@ -163,11 +163,8 @@ export default function BranchesPage() {
},
itemsPerPageOptions: [5, 10, 15, 20],
}}
+ isLoading={isLoading}
/>
-
- {isLoading && (
- Cargando sucursales...
- )}
);
}
diff --git a/src/app/(dashboard)/categories/[id]/edit/page.tsx b/src/app/(dashboard)/categories/[id]/edit/page.tsx
index a85e717..0f45cfe 100644
--- a/src/app/(dashboard)/categories/[id]/edit/page.tsx
+++ b/src/app/(dashboard)/categories/[id]/edit/page.tsx
@@ -152,7 +152,6 @@ export default function EditCategoryPage() {
helperText={errors.name}
helperTextColor={Colors.semanticDanger}
borderSize="1px"
- borderColor="#d1d5db"
/>
@@ -130,7 +129,6 @@ export default function NewCategoryPage() {
}}
helperText={errors.description}
helperTextColor={Colors.semanticDanger}
- borderColor="#d1d5db"
type="text"
borderSize="1px"
isTextArea
diff --git a/src/app/(dashboard)/categories/page.tsx b/src/app/(dashboard)/categories/page.tsx
index aac3cad..d363d17 100644
--- a/src/app/(dashboard)/categories/page.tsx
+++ b/src/app/(dashboard)/categories/page.tsx
@@ -113,11 +113,8 @@ export default function CategoriesPage() {
},
itemsPerPageOptions: [5, 10, 15, 20],
}}
+ isLoading={isLoading}
/>
-
- {isLoading && (
- Cargando categorías...
- )}
);
}
diff --git a/src/app/(dashboard)/coupons/[code]/edit/page.tsx b/src/app/(dashboard)/coupons/[code]/edit/page.tsx
index 0ac5adf..9a4c0a6 100644
--- a/src/app/(dashboard)/coupons/[code]/edit/page.tsx
+++ b/src/app/(dashboard)/coupons/[code]/edit/page.tsx
@@ -10,6 +10,7 @@ import { api } from '@/lib/sdkConfig';
import { toast } from 'react-toastify';
import { couponSchema } from '@/lib/validations/couponsSchema';
import { useAuth } from '@/context/AuthContext';
+import { formatPrice } from '@/lib/utils/priceFormatter';
export default function EditCouponPage() {
const params = useParams();
@@ -55,7 +56,7 @@ export default function EditCouponPage() {
setTempCode(response.code);
setTempDiscount(response.discount);
- setTempMinPurchase(response.minPurchase);
+ setTempMinPurchase(Number(formatPrice(response.minPurchase)));
setTempMaxUses(response.maxUses);
setTempExpirationDate(expirationDate.toISOString());
} catch (error) {
@@ -134,7 +135,8 @@ export default function EditCouponPage() {
{
code: tempCode.trim(),
discount: Number(tempDiscount),
- minPurchase: Number(tempMinPurchase),
+ // Convert to cents
+ minPurchase: Number((Number(tempMinPurchase) * 100).toFixed(0)),
maxUses: Number(tempMaxUses),
expirationDate: new Date(tempExpirationDate),
},
diff --git a/src/app/(dashboard)/coupons/[code]/page.tsx b/src/app/(dashboard)/coupons/[code]/page.tsx
index be0e08a..1104e51 100644
--- a/src/app/(dashboard)/coupons/[code]/page.tsx
+++ b/src/app/(dashboard)/coupons/[code]/page.tsx
@@ -8,11 +8,12 @@ import ModalConfirm from '@/components/ModalConfirm';
import { Colors } from '@/styles/styles';
import { api } from '@/lib/sdkConfig';
import { toast } from 'react-toastify';
-import { format } from 'date-fns';
import { CouponResponse } from '@pharmatech/sdk/types';
import { useAuth } from '@/context/AuthContext';
import Loading from '../../loading';
import Input from '@/components/Input/Input';
+import { formatDateSafe } from '@/lib/utils/useFormatDate';
+import { formatPrice } from '@/lib/utils/priceFormatter';
export default function CouponDetailsPage() {
const params = useParams();
@@ -67,14 +68,6 @@ export default function CouponDetailsPage() {
}
};
- const formatDate = (date: Date) => {
- try {
- return format(date, 'dd/MM/yyyy');
- } catch {
- return date.toString();
- }
- };
-
if (!coupon) {
return ;
}
@@ -146,7 +139,7 @@ export default function CouponDetailsPage() {
@@ -175,7 +168,7 @@ export default function CouponDetailsPage() {
diff --git a/src/app/(dashboard)/coupons/new/page.tsx b/src/app/(dashboard)/coupons/new/page.tsx
index 5578949..0981f2f 100644
--- a/src/app/(dashboard)/coupons/new/page.tsx
+++ b/src/app/(dashboard)/coupons/new/page.tsx
@@ -64,7 +64,8 @@ export default function NewCouponPage() {
}
const payload = validationResult.data;
-
+ // Converts minPurchase to cents
+ payload.minPurchase = Number((payload.minPurchase * 100).toFixed(0));
await api.coupon.create(payload, token);
toast.success('Cupón creado exitosamente');
setTimeout(() => router.push('/coupons'), 1500);
@@ -136,7 +137,6 @@ export default function NewCouponPage() {
helperText={errors.code}
helperTextColor={Colors.semanticDanger}
borderSize="1px"
- borderColor="#E7E7E6"
/>
diff --git a/src/app/(dashboard)/coupons/page.tsx b/src/app/(dashboard)/coupons/page.tsx
index 0418629..0d9a040 100644
--- a/src/app/(dashboard)/coupons/page.tsx
+++ b/src/app/(dashboard)/coupons/page.tsx
@@ -9,6 +9,8 @@ import { api } from '@/lib/sdkConfig';
import { Pagination, CouponResponse } from '@pharmatech/sdk';
import { useAuth } from '@/context/AuthContext';
import Badge from '@/components/Badge';
+import { toast } from 'react-toastify';
+import { formatPrice } from '@/lib/utils/priceFormatter';
// Presets de rango de expiración que el backend soporta via expirationBetween
const expirationTranslations: Record = {
@@ -123,7 +125,7 @@ export default function CouponsPage() {
{
key: 'minPurchase',
label: 'Compra min.',
- render: (c: CouponResponse) => `$${c.minPurchase.toFixed(2)}`,
+ render: (c: CouponResponse) => `$${formatPrice(c.minPurchase)}`,
},
{
key: 'maxUses',
@@ -154,6 +156,64 @@ export default function CouponsPage() {
},
];
+ const handleSetExpiredAtToday = async (coupons: CouponResponse[]) => {
+ api.promo
+ .bulkUpdate(
+ {
+ ids: coupons.map((p) => p.id),
+ expiredAt: new Date(),
+ },
+ token!,
+ )
+ .then(() => {
+ toast.success('Cupones expirados');
+ fetchCoupons(
+ currentPage,
+ itemsPerPage,
+ searchQuery,
+ selectedExpirationPeriod,
+ );
+ })
+ .catch((err) => {
+ console.error('Error al expirar los cupones:', err);
+ toast.error('Error al expirar los cupones');
+ });
+ };
+
+ const handleDeleteCoupon = async (coupons: CouponResponse[]) => {
+ api.promo
+ .bulkDelete(
+ {
+ ids: coupons.map((p) => p.id),
+ },
+ token!,
+ )
+ .then(() => {
+ toast.success('Cupones eliminados');
+ fetchCoupons(
+ currentPage,
+ itemsPerPage,
+ searchQuery,
+ selectedExpirationPeriod,
+ );
+ })
+ .catch((err) => {
+ console.error('Error al eliminar los cupones:', err);
+ toast.error('Error al eliminar los cupones');
+ });
+ };
+
+ const actions = [
+ {
+ label: 'Expirar hoy',
+ onClick: handleSetExpiredAtToday,
+ },
+ {
+ label: 'Eliminar',
+ onClick: handleDeleteCoupon,
+ },
+ ];
+
return (
router.push('/coupons/new')}
addButtonText="Agregar Cupón"
onSearch={handleSearch}
+ actions={actions}
dropdownComponent={
-
- {isLoading &&
Cargando cupones...
}
);
}
diff --git a/src/app/(dashboard)/dashboard/page.tsx b/src/app/(dashboard)/dashboard/page.tsx
index 1515f78..e25a547 100644
--- a/src/app/(dashboard)/dashboard/page.tsx
+++ b/src/app/(dashboard)/dashboard/page.tsx
@@ -17,6 +17,8 @@ import {
Bar,
XAxis,
YAxis,
+ LineChart,
+ Line,
} from 'recharts';
import Calendar from '@/components/Calendar';
import { Colors, FontSizes } from '@/styles/styles';
@@ -27,6 +29,9 @@ export default function DashboardPage() {
const [stats, setStats] = useState(null);
const [prevStats, setPrevStats] = useState(null);
+ const [sales, setSales] = useState<
+ { date: string; total: number; predictedTotal: number }[]
+ >([]);
const pieData = [
{ name: 'Órdenes Abiertas', value: stats?.openOrders ?? 0 },
@@ -64,9 +69,46 @@ export default function DashboardPage() {
}
}, [token, fromDate, toDate]);
+ const fetchSales = useCallback(async () => {
+ if (!token) return;
+
+ try {
+ const params: ReportQueryParams = {
+ startDate: fromDate || '',
+ endDate: toDate || '',
+ };
+
+ const response = await api.report.getSalesReport(params, token);
+ setSales(
+ response.items.map((item) => ({
+ date: item.date.split('T')[0],
+ total: item.total,
+ predictedTotal: 0,
+ })),
+ );
+ const prediction = await api.salesPrediction.getPredictedSales(
+ { days: 5 },
+ token,
+ );
+ setSales((prevSales) => [
+ ...prevSales,
+ ...prediction.map((item) => ({
+ date: item.date.split('T')[0],
+ predictedTotal: Math.round(item.predictedTotal / 1000),
+ total: 0,
+ })),
+ ]);
+ } catch (error) {
+ console.error('Error fetching dashboard stats:', error);
+ toast.error('Error al cargar las ventas');
+ } finally {
+ }
+ }, [token, fromDate, toDate]);
+
useEffect(() => {
fetchDashboardStats();
- }, [fetchDashboardStats]);
+ fetchSales();
+ }, [fetchDashboardStats, fetchSales]);
return (
@@ -271,6 +313,26 @@ export default function DashboardPage() {
+
+
+ Predicción de Ventas
+
+
+
+
+
+
+
+
+
+
+
+
);
}
diff --git a/src/app/(dashboard)/inventories/page.tsx b/src/app/(dashboard)/inventories/page.tsx
new file mode 100644
index 0000000..b4be985
--- /dev/null
+++ b/src/app/(dashboard)/inventories/page.tsx
@@ -0,0 +1,183 @@
+'use client';
+
+import React, { useState, useEffect, useCallback } from 'react';
+import TableContainer from '@/components/TableContainer';
+import Dropdown from '@/components/Dropdown';
+import { api } from '@/lib/sdkConfig';
+import { useAuth } from '@/context/AuthContext';
+import {
+ Pagination,
+ BranchResponse,
+ UserRole,
+ InventoryResponse,
+} from '@pharmatech/sdk';
+import { toast } from 'react-toastify';
+import { formatDateSafe } from '@/lib/utils/useFormatDate';
+import InventoryStock from '@/components/Input/InventoryStock';
+import { formatPrice } from '@/lib/utils/priceFormatter';
+
+export default function InventoryListPage() {
+ const { token, user } = useAuth();
+
+ const [inventories, setInventories] = useState([]);
+ const [branches, setBranches] = useState([]);
+ const [selectedBranchId, setSelectedBranchId] = useState('');
+ const [currentPage, setCurrentPage] = useState(1);
+ const [itemsPerPage, setItemsPerPage] = useState(10);
+ const [totalItems, setTotalItems] = useState(0);
+
+ const [isLoading, setIsLoading] = useState(false);
+ const [error, setError] = useState(null);
+
+ const branchOptions = ['Todas', ...branches.map((branch) => branch.name)];
+ const handleBranchChange = (label: string) => {
+ const branch = branches.find((branch) => branch.name === label);
+ setSelectedBranchId(branch?.id ?? '');
+ setCurrentPage(1);
+ };
+
+ const fetchBranches = useCallback(async () => {
+ try {
+ const resp = await api.branch.findAll({ page: 1, limit: 100 });
+ setBranches(resp.results);
+ } catch (err: unknown) {
+ console.error('Error fetching Branches:', err);
+ toast.error('No se pudieron cargar las sucursales');
+ }
+ }, []);
+
+ useEffect(() => {
+ if (token && user?.sub && user.role == UserRole.ADMIN) fetchBranches();
+ }, [fetchBranches, token, user?.sub]);
+
+ const fetchInventories = useCallback(async () => {
+ if (!token || !user?.sub) return;
+ setIsLoading(true);
+ setError(null);
+ const params: Parameters[0] = {
+ page: currentPage,
+ limit: itemsPerPage,
+ };
+ if (user.role == UserRole.BRANCH_ADMIN) {
+ params.branchId = user.branch?.id;
+ } else {
+ params.branchId = selectedBranchId ? selectedBranchId : undefined;
+ }
+ try {
+ const response: Pagination =
+ await api.inventory.findAll(params);
+ setInventories(response.results);
+ setTotalItems(response.count);
+ } catch (err: unknown) {
+ console.error('Error fetching inventories:', err);
+ toast.error('Error al cargar los inventarios');
+ setError('No se pudieron cargar los inventarios.');
+ } finally {
+ setIsLoading(false);
+ }
+ }, [token, user?.sub, currentPage, itemsPerPage, selectedBranchId]);
+
+ useEffect(() => {
+ fetchInventories();
+ }, [fetchInventories]);
+
+ const totalPages = Math.ceil(totalItems / itemsPerPage);
+
+ const getColumns = () => {
+ const columns = [
+ {
+ key: 'name',
+ label: 'Producto',
+ render: (i: InventoryResponse) => i.productPresentation.product.name,
+ },
+ {
+ key: 'presentation',
+ label: 'Presentación',
+ render: (i: InventoryResponse) =>
+ i.productPresentation.presentation.name,
+ },
+ {
+ key: 'manufacturer',
+ label: 'Marca',
+ render: (i: InventoryResponse) =>
+ i.productPresentation.product.manufacturer.name,
+ },
+ {
+ key: 'updatedAt',
+ label: 'Actualizado',
+ render: (i: InventoryResponse) => formatDateSafe(i.updatedAt),
+ },
+ {
+ key: 'stockQuantity',
+ label: 'Existencia',
+ render: (i: InventoryResponse) => (
+
+ ),
+ },
+ {
+ key: 'price',
+ label: 'Precio',
+ render: (i: InventoryResponse) =>
+ `$${formatPrice(i.productPresentation.price)}`,
+ },
+ {
+ key: 'total',
+ label: 'Total',
+ render: (i: InventoryResponse) =>
+ `$${formatPrice(i.stockQuantity * i.productPresentation.price)}`,
+ },
+ ];
+ if (user?.role == UserRole.ADMIN) {
+ columns.unshift({
+ key: 'branch',
+ label: 'Sucursal',
+ render: (i: InventoryResponse) => i.branch.name,
+ });
+ }
+ return columns;
+ };
+
+ const renderDropdown = () => {
+ if (user?.role == UserRole.ADMIN) {
+ return (
+
+ );
+ }
+ return <>>;
+ };
+
+ return (
+
+ {error && (
+
{error}
+ )}
+
+
+ title="Inventario"
+ dropdownComponent={renderDropdown()}
+ tableData={inventories}
+ tableColumns={getColumns()}
+ pagination={{
+ currentPage,
+ totalPages,
+ totalItems,
+ itemsPerPage,
+ onPageChange: setCurrentPage,
+ onItemsPerPageChange: (val) => {
+ setItemsPerPage(val);
+ setCurrentPage(1);
+ },
+ itemsPerPageOptions: [5, 10, 15, 20],
+ }}
+ isLoading={isLoading}
+ />
+
+ );
+}
diff --git a/src/app/(dashboard)/lots/page.tsx b/src/app/(dashboard)/lots/page.tsx
new file mode 100644
index 0000000..32567e9
--- /dev/null
+++ b/src/app/(dashboard)/lots/page.tsx
@@ -0,0 +1,178 @@
+'use client';
+
+import React, { useState, useEffect, useCallback } from 'react';
+import TableContainer from '@/components/TableContainer';
+import Dropdown from '@/components/Dropdown';
+import { api } from '@/lib/sdkConfig';
+import { useAuth } from '@/context/AuthContext';
+import {
+ Pagination,
+ BranchResponse,
+ UserRole,
+ LotResponse,
+} from '@pharmatech/sdk';
+import { toast } from 'react-toastify';
+import { formatDateSafe } from '@/lib/utils/useFormatDate';
+import { formatPrice } from '@/lib/utils/priceFormatter';
+
+export default function InventoryListPage() {
+ const { token, user } = useAuth();
+
+ const [lots, setLots] = useState([]);
+ const [branches, setBranches] = useState([]);
+ const [selectedBranchId, setSelectedBranchId] = useState('');
+ const [currentPage, setCurrentPage] = useState(1);
+ const [itemsPerPage, setItemsPerPage] = useState(10);
+ const [totalItems, setTotalItems] = useState(0);
+
+ const [isLoading, setIsLoading] = useState(false);
+ const [error, setError] = useState(null);
+
+ const branchOptions = ['Todas', ...branches.map((branch) => branch.name)];
+ const handleBranchChange = (label: string) => {
+ const branch = branches.find((branch) => branch.name === label);
+ setSelectedBranchId(branch?.id ?? '');
+ setCurrentPage(1);
+ };
+
+ const fetchBranches = useCallback(async () => {
+ try {
+ const resp = await api.branch.findAll({ page: 1, limit: 100 });
+ setBranches(resp.results);
+ } catch (err: unknown) {
+ console.error('Error fetching Branches:', err);
+ toast.error('No se pudieron cargar las sucursales');
+ }
+ }, []);
+
+ useEffect(() => {
+ if (token && user?.sub && user.role == UserRole.ADMIN) fetchBranches();
+ }, [fetchBranches, token, user?.sub]);
+
+ const fetchLots = useCallback(async () => {
+ if (!token || !user?.sub) return;
+ setIsLoading(true);
+ setError(null);
+ const params: Parameters[0] = {
+ page: currentPage,
+ limit: itemsPerPage,
+ };
+ if (user.role == UserRole.BRANCH_ADMIN) {
+ params.branchId = user.branch?.id;
+ } else {
+ params.branchId = selectedBranchId ? selectedBranchId : undefined;
+ }
+ try {
+ const response: Pagination = await api.lot.findAll(params);
+ setLots(response.results);
+ setTotalItems(response.count);
+ } catch (err: unknown) {
+ console.error('Error fetching lots:', err);
+ toast.error('Error al cargar los lotes');
+ setError('No se pudieron cargar los lotes.');
+ } finally {
+ setIsLoading(false);
+ }
+ }, [token, user?.sub, currentPage, itemsPerPage, selectedBranchId]);
+
+ useEffect(() => {
+ fetchLots();
+ }, [fetchLots]);
+
+ const totalPages = Math.ceil(totalItems / itemsPerPage);
+
+ const getColumns = () => {
+ const columns = [
+ {
+ key: 'name',
+ label: 'Producto',
+ render: (i: LotResponse) => i.productPresentation.product.name,
+ },
+ {
+ key: 'presentation',
+ label: 'Presentación',
+ render: (i: LotResponse) => i.productPresentation.presentation.name,
+ },
+ {
+ key: 'manufacturer',
+ label: 'Marca',
+ render: (i: LotResponse) =>
+ i.productPresentation.product.manufacturer.name,
+ },
+ {
+ key: 'expirationDate',
+ label: 'Fecha de expiración',
+ render: (i: LotResponse) => formatDateSafe(i.expirationDate),
+ },
+ {
+ key: 'stockQuantity',
+ label: 'Existencia',
+ render: (i: LotResponse) => i.quantity,
+ },
+ {
+ key: 'price',
+ label: 'Precio',
+ render: (i: LotResponse) =>
+ `$${formatPrice(i.productPresentation.price)}`,
+ },
+ {
+ key: 'total',
+ label: 'Total',
+ render: (i: LotResponse) =>
+ `$${formatPrice(i.quantity * i.productPresentation.price)}`,
+ },
+ ];
+ if (user?.role == UserRole.ADMIN) {
+ columns.unshift({
+ key: 'branch',
+ label: 'Sucursal',
+ render: (i: LotResponse) => i.branch.name,
+ });
+ }
+ return columns;
+ };
+
+ const renderDropdown = () => {
+ if (user?.role == UserRole.ADMIN) {
+ return (
+
+ );
+ }
+ return <>>;
+ };
+
+ return (
+
+ {error && (
+
{error}
+ )}
+
+
+ title="Lotes"
+ dropdownComponent={renderDropdown()}
+ tableData={lots}
+ tableColumns={getColumns()}
+ pagination={{
+ currentPage,
+ totalPages,
+ totalItems,
+ itemsPerPage,
+ onPageChange: setCurrentPage,
+ onItemsPerPageChange: (val) => {
+ setItemsPerPage(val);
+ setCurrentPage(1);
+ },
+ itemsPerPageOptions: [5, 10, 15, 20],
+ }}
+ isLoading={isLoading}
+ />
+
+ );
+}
diff --git a/src/app/(dashboard)/orders/[id]/edit/page.tsx b/src/app/(dashboard)/orders/[id]/edit/page.tsx
index 9e014c1..e391005 100644
--- a/src/app/(dashboard)/orders/[id]/edit/page.tsx
+++ b/src/app/(dashboard)/orders/[id]/edit/page.tsx
@@ -26,22 +26,13 @@ import {
} from '@/lib/utils/orderTranslations';
import { useAuth } from '@/context/AuthContext';
import Loading from '@/app/(dashboard)/loading';
+import { formatPrice } from '@/lib/utils/priceFormatter';
export default function EditOrderStatusPage() {
const params = useParams();
const id = typeof params?.id === 'string' ? params.id : '';
const router = useRouter();
- const { token } = useAuth();
- const socket = io(SOCKET_URL, {
- transportOptions: {
- polling: {
- extraHeaders: {
- authorization: `Bearer ${token}`,
- },
- },
- },
- });
-
+ const { token, user } = useAuth();
const [order, setOrder] = useState(null);
const [orderStatus, setOrderStatus] = useState();
const [deliveryStatus, setDeliveryStatus] = useState();
@@ -51,33 +42,50 @@ export default function EditOrderStatusPage() {
string | null
>(null);
const [loading, setLoading] = useState(true);
- const [isConnected, setIsConnected] = useState(false);
- type SocketError = {
- message: string;
- data: { id: string };
- };
+ const socket = io(SOCKET_URL, {
+ transportOptions: {
+ polling: {
+ extraHeaders: {
+ authorization: `Bearer ${token}`,
+ },
+ },
+ },
+ });
useEffect(() => {
- socket.on('connect', () => {
- setIsConnected(true);
- console.log('Socket connected: ', isConnected);
- });
+ function onConnect() {
+ console.log('Connected to socket server');
+ }
- socket.on('disconnect', () => {
- setIsConnected(false);
- console.log('Socket connected: ', isConnected);
- });
+ function onDisconnect() {
+ console.log('Disconnected from socket server');
+ }
- socket.on('error', (error: SocketError) => {
- console.error('Socket error: ', error);
- toast.error('Error de conexión con el servidor');
- });
+ async function onDeliveryUpdated(delivery: {
+ orderDeliveryId: string;
+ status: OrderDeliveryStatus;
+ employeeId: string;
+ }) {
+ const response = await api.deliveryService.getById(
+ delivery.orderDeliveryId,
+ token!,
+ );
+ setDeliveryStatus(response.deliveryStatus);
+ setDeliveryId(response.id);
+ setSelectedDeliveryUserId(response.employeeId);
+ }
+
+ socket.on('connect', onConnect);
+ socket.on('disconnect', onDisconnect);
+ socket.on('deliveryUpdated', onDeliveryUpdated);
return () => {
- socket.disconnect();
+ socket.off('connect', onConnect);
+ socket.off('deliveryUpdated', onDeliveryUpdated);
+ socket.off('disconnect', onDisconnect);
};
- }, [isConnected, socket]);
+ }, []);
const fetchOrderData = useCallback(async () => {
if (!token || !id) return;
@@ -122,8 +130,16 @@ export default function EditOrderStatusPage() {
if (!token || !id) return;
try {
- socket.emit('updateOrder', { id, status: orderStatus });
- console.log('DeliveryId', selectedDeliveryUserId);
+ const orderUpdated = await api.order.update(
+ id,
+ { status: orderStatus },
+ token,
+ );
+ socket.emit('updateOrder', {
+ id: orderUpdated.id,
+ status: orderUpdated.status,
+ });
+
if (deliveryId && deliveryStatus) {
await api.deliveryService.update(
deliveryId,
@@ -136,6 +152,7 @@ export default function EditOrderStatusPage() {
}
toast.success('Orden actualizada correctamente');
+ socket.disconnect();
setTimeout(() => router.push('/orders'), 2000);
} catch (error) {
console.error('Error actualizando la orden:', error);
@@ -154,6 +171,16 @@ export default function EditOrderStatusPage() {
}
const isDelivery = order.type === OrderType.DELIVERY;
+ if (user?.branch?.id !== order.branch?.id && user?.role !== UserRole.ADMIN) {
+ return (
+
+
+ No tienes permiso para ver esta orden.
+
+
+ );
+ }
+
return (
<>
@@ -241,8 +268,48 @@ export default function EditOrderStatusPage() {
>
)}
+
+ {order.paymentConfirmation && (
+
+
Datos del Pago
+
+
+
+
+ {order.paymentConfirmation.reference}
+
+
+
+
+
+ {order.paymentConfirmation.bank}
+
+
+
+
+
+ {order.paymentConfirmation.phoneNumber}
+
+
+
+
+
+ ${formatPrice(order.totalPrice)}
+
+
+
+
+ )}
-
+
>
);
diff --git a/src/app/(dashboard)/orders/[id]/page.tsx b/src/app/(dashboard)/orders/[id]/page.tsx
index 274a507..3aad9f2 100644
--- a/src/app/(dashboard)/orders/[id]/page.tsx
+++ b/src/app/(dashboard)/orders/[id]/page.tsx
@@ -24,12 +24,13 @@ import {
orderStatusTranslationMap,
orderDeliveryStatusTranslationMap,
} from '@/lib/utils/orderTranslations';
+import { formatPrice } from '@/lib/utils/priceFormatter';
export default function ViewOrderStatusPage() {
const params = useParams();
const id = typeof params?.id === 'string' ? params.id : '';
const router = useRouter();
- const { token } = useAuth();
+ const { token, user } = useAuth();
const [order, setOrder] = useState(null);
const [orderStatus, setOrderStatus] = useState();
@@ -90,6 +91,16 @@ export default function ViewOrderStatusPage() {
if (loading || !order) return ;
const isDelivery = order.type === OrderType.DELIVERY;
+ if (user?.branch?.id !== order.branch?.id && user?.role !== UserRole.ADMIN) {
+ return (
+
+
+ No tienes permiso para ver esta orden.
+
+
+ );
+ }
+
return (
<>
@@ -170,8 +181,47 @@ export default function ViewOrderStatusPage() {
>
)}
+ {order.paymentConfirmation && (
+
+
Datos del Pago
+
+
+
+
+ {order.paymentConfirmation.reference}
+
+
+
+
+
+ {order.paymentConfirmation.bank}
+
+
+
+
+
+ {order.paymentConfirmation.phoneNumber}
+
+
+
+
+
+ ${formatPrice(order.totalPrice)}
+
+
+
+
+ )}
-
+
>
);
diff --git a/src/app/(dashboard)/orders/page.tsx b/src/app/(dashboard)/orders/page.tsx
index 558335e..9733235 100644
--- a/src/app/(dashboard)/orders/page.tsx
+++ b/src/app/(dashboard)/orders/page.tsx
@@ -12,29 +12,29 @@ import {
OrderResponse,
OrderStatus,
OrderType,
+ UserRole,
} from '@pharmatech/sdk';
import { orderStatusTranslationMap } from '@/lib/utils/orderTranslations';
import Badge from '@/components/Badge';
import { toast } from 'react-toastify';
+import { formatDateSafe } from '@/lib/utils/useFormatDate';
+import { formatPrice } from '@/lib/utils/priceFormatter';
export default function OrdersPage() {
- const { token } = useAuth();
+ const { token, user } = useAuth();
const router = useRouter();
- // datos y paginación
const [orders, setOrders] = useState([]);
- const [query, setQuery] = useState(''); // buscador libre (q)
+ const [query, setQuery] = useState('');
const [selectedStatus, setSelectedStatus] = useState('');
const [selectedType, setSelectedType] = useState('');
const [page, setPage] = useState(1);
const [limit, setLimit] = useState(10);
const [total, setTotal] = useState(0);
- // estados de carga y error
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
- // debounce para búsqueda
const DEBOUNCE_MS = 500;
const debounceRef = useRef(null);
const onSearch = (q: string) => {
@@ -45,7 +45,6 @@ export default function OrdersPage() {
}, DEBOUNCE_MS);
};
- // opciones de estado y tipo
const statusOptions = [
{ value: '', label: 'Todos' },
{ value: OrderStatus.REQUESTED, label: 'Solicitado' },
@@ -86,17 +85,8 @@ export default function OrdersPage() {
setPage(1);
};
- // formatea fechas
- const formatDate = (input: string | Date) =>
- new Date(input).toLocaleDateString('es-ES', {
- day: '2-digit',
- month: '2-digit',
- year: 'numeric',
- });
-
- // trae órdenes usando SOLO los filtros que el backend expone (q + status + type)
const fetchOrders = useCallback(async () => {
- if (!token) return;
+ if (!token || !user?.sub) return;
setIsLoading(true);
setError(null);
@@ -108,6 +98,9 @@ export default function OrdersPage() {
...(selectedStatus ? { status: selectedStatus } : {}),
...(selectedType ? { type: selectedType } : {}),
};
+ if (user.role == UserRole.BRANCH_ADMIN) {
+ params.branchId = user.branch?.id;
+ }
const resp: Pagination = await api.order.findAll(
params,
@@ -125,7 +118,6 @@ export default function OrdersPage() {
}
}, [page, limit, query, selectedStatus, selectedType, token]);
- // recarga al cambiar filtros o paginación
useEffect(() => {
fetchOrders();
}, [fetchOrders]);
@@ -137,12 +129,12 @@ export default function OrdersPage() {
{
key: 'createdAt',
label: 'Creación',
- render: (o) => formatDate(o.createdAt),
+ render: (o) => formatDateSafe(o.createdAt),
},
{
key: 'updatedAt',
label: 'Actualización',
- render: (o) => formatDate(o.updatedAt),
+ render: (o) => formatDateSafe(o.updatedAt),
},
{
key: 'status',
@@ -163,7 +155,72 @@ export default function OrdersPage() {
{
key: 'totalPrice',
label: 'Precio total',
- render: (o) => `$${o.totalPrice.toFixed(2)}`,
+ render: (o) => `$${formatPrice(o.totalPrice)}`,
+ },
+ ];
+
+ const handleStatusUpdate = async (
+ users: OrderResponse[],
+ status: OrderStatus,
+ ) => {
+ api.order
+ .bulkUpdate(
+ {
+ orders: users.map((u) => u.id),
+ status: status,
+ },
+ token!,
+ )
+ .then(() => {
+ toast.success('Órdenes actualizadas');
+ fetchOrders();
+ })
+ .catch((err) => {
+ console.error('Error al actualizar el status de las órdenes:', err);
+ toast.error('Error al actualizar el status de las órdenes');
+ });
+ };
+
+ const handleStatusUpdateToApproved = async (users: OrderResponse[]) => {
+ handleStatusUpdate(users, OrderStatus.APPROVED);
+ };
+
+ const handleStatusUpdateToCanceled = async (users: OrderResponse[]) => {
+ handleStatusUpdate(users, OrderStatus.CANCELED);
+ };
+
+ const handleStatusUpdateToReadyForPickup = async (users: OrderResponse[]) => {
+ handleStatusUpdate(users, OrderStatus.READY_FOR_PICKUP);
+ };
+
+ const handleStatusUpdateToCompleted = async (users: OrderResponse[]) => {
+ handleStatusUpdate(users, OrderStatus.COMPLETED);
+ };
+
+ const handleStatusUpdateToInProgress = async (users: OrderResponse[]) => {
+ handleStatusUpdate(users, OrderStatus.IN_PROGRESS);
+ };
+
+ const actions = [
+ {
+ label: 'Aprobar',
+ onClick: handleStatusUpdateToApproved,
+ },
+ {
+ label: 'Cancelar',
+ onClick: handleStatusUpdateToCanceled,
+ },
+ {
+ label: 'Listar para Retiro',
+ onClick: handleStatusUpdateToReadyForPickup,
+ },
+ {
+ label: 'Completar',
+ onClick: handleStatusUpdateToCompleted,
+ },
+ {
+ label: 'Marcar como En Proceso',
+ onClick: handleStatusUpdateToInProgress,
},
];
@@ -179,6 +236,7 @@ export default function OrdersPage() {
title="Órdenes"
onSearch={onSearch}
+ actions={actions}
dropdownComponent={
}
- onAddClick={() => router.push('/orders/new')}
- addButtonText="Agregar orden"
tableData={orders}
tableColumns={columns}
onView={(o) => router.push(`/orders/${o.id}`)}
@@ -219,9 +275,8 @@ export default function OrdersPage() {
},
itemsPerPageOptions: [5, 10, 15, 20],
}}
+ isLoading={isLoading}
/>
-
- {isLoading && Cargando órdenes...
}
);
}
diff --git a/src/app/(dashboard)/orders/requests/page.tsx b/src/app/(dashboard)/orders/requests/page.tsx
new file mode 100644
index 0000000..6a6e137
--- /dev/null
+++ b/src/app/(dashboard)/orders/requests/page.tsx
@@ -0,0 +1,226 @@
+'use client';
+
+import React, { useState, useEffect, useRef, useCallback } from 'react';
+import { useRouter } from 'next/navigation';
+import TableContainer from '@/components/TableContainer';
+import Dropdown from '@/components/Dropdown';
+import { Column } from '@/components/Table';
+import { api } from '@/lib/sdkConfig';
+import { useAuth } from '@/context/AuthContext';
+import {
+ Pagination,
+ OrderResponse,
+ OrderStatus,
+ OrderType,
+ UserRole,
+} from '@pharmatech/sdk';
+import { toast } from 'react-toastify';
+import { formatDateSafe } from '@/lib/utils/useFormatDate';
+import { formatPrice } from '@/lib/utils/priceFormatter';
+
+export default function OrdersPage() {
+ const { token, user } = useAuth();
+ const router = useRouter();
+
+ const [orders, setOrders] = useState([]);
+ const [query, setQuery] = useState('');
+ const [selectedType, setSelectedType] = useState('');
+ const [page, setPage] = useState(1);
+ const [limit, setLimit] = useState(10);
+ const [total, setTotal] = useState(0);
+
+ const [isLoading, setIsLoading] = useState(false);
+ const [error, setError] = useState(null);
+
+ const DEBOUNCE_MS = 500;
+ const debounceRef = useRef(null);
+ const onSearch = (q: string) => {
+ if (debounceRef.current) clearTimeout(debounceRef.current);
+ debounceRef.current = setTimeout(() => {
+ setQuery(q.trim());
+ setPage(1);
+ }, DEBOUNCE_MS);
+ };
+
+ const typeOptions = [
+ { value: '', label: 'Todos' },
+ { value: OrderType.PICKUP, label: 'Pickup' },
+ { value: OrderType.DELIVERY, label: 'Delivery' },
+ ] as const;
+
+ const handleTypeChange = (label: string) => {
+ const opt = typeOptions.find((o) => o.label === label);
+ setSelectedType(opt?.value ?? '');
+ setPage(1);
+ };
+
+ const fetchOrders = useCallback(async () => {
+ if (!token || !user?.sub) return;
+ setIsLoading(true);
+ setError(null);
+
+ try {
+ const params: Parameters[0] = {
+ page,
+ limit,
+ status: OrderStatus.REQUESTED,
+ ...(query ? { q: query } : {}),
+ ...(selectedType ? { type: selectedType } : {}),
+ };
+
+ if (user.role == UserRole.BRANCH_ADMIN) {
+ params.branchId = user.branch?.id;
+ }
+
+ const resp: Pagination = await api.order.findAll(
+ params,
+ token,
+ );
+
+ setOrders(resp.results);
+ setTotal(resp.count);
+ } catch (err: unknown) {
+ console.error('Error fetching orders:', err);
+ toast.error('Error al cargar órdenes');
+ setError('No se pudieron cargar las órdenes.');
+ } finally {
+ setIsLoading(false);
+ }
+ }, [page, limit, query, selectedType, token]);
+
+ useEffect(() => {
+ fetchOrders();
+ }, [fetchOrders]);
+
+ const totalPages = Math.ceil(total / limit);
+
+ const columns: Column[] = [
+ { key: 'id', label: 'ID', render: (o) => o.id.slice(0, 8) },
+ {
+ key: 'createdAt',
+ label: 'Creación',
+ render: (o) => formatDateSafe(o.createdAt),
+ },
+ {
+ key: 'updatedAt',
+ label: 'Actualización',
+ render: (o) => formatDateSafe(o.updatedAt),
+ },
+ { key: 'type', label: 'Tipo', render: (o) => o.type },
+ {
+ key: 'totalPrice',
+ label: 'Precio total',
+ render: (o) => `$${formatPrice(o.totalPrice)}`,
+ },
+ ];
+
+ const handleStatusUpdate = async (
+ users: OrderResponse[],
+ status: OrderStatus,
+ ) => {
+ api.order
+ .bulkUpdate(
+ {
+ orders: users.map((u) => u.id),
+ status: status,
+ },
+ token!,
+ )
+ .then(() => {
+ toast.success('Órdenes actualizadas');
+ fetchOrders();
+ })
+ .catch((err) => {
+ console.error('Error al actualizar el status de las órdenes:', err);
+ toast.error('Error al actualizar el status de las órdenes');
+ });
+ };
+
+ const handleStatusUpdateToApproved = async (users: OrderResponse[]) => {
+ handleStatusUpdate(users, OrderStatus.APPROVED);
+ };
+
+ const handleStatusUpdateToCanceled = async (users: OrderResponse[]) => {
+ handleStatusUpdate(users, OrderStatus.CANCELED);
+ };
+
+ const handleStatusUpdateToReadyForPickup = async (users: OrderResponse[]) => {
+ handleStatusUpdate(users, OrderStatus.READY_FOR_PICKUP);
+ };
+
+ const handleStatusUpdateToCompleted = async (users: OrderResponse[]) => {
+ handleStatusUpdate(users, OrderStatus.COMPLETED);
+ };
+
+ const handleStatusUpdateToInProgress = async (users: OrderResponse[]) => {
+ handleStatusUpdate(users, OrderStatus.IN_PROGRESS);
+ };
+
+ const actions = [
+ {
+ label: 'Aprobar',
+ onClick: handleStatusUpdateToApproved,
+ },
+ {
+ label: 'Cancelar',
+ onClick: handleStatusUpdateToCanceled,
+ },
+ {
+ label: 'Listar para Retiro',
+ onClick: handleStatusUpdateToReadyForPickup,
+ },
+ {
+ label: 'Completar',
+ onClick: handleStatusUpdateToCompleted,
+ },
+ {
+ label: 'Marcar como En Proceso',
+ onClick: handleStatusUpdateToInProgress,
+ },
+ ];
+
+ return (
+
+ {error && (
+
{error}
+ )}
+
+
+ title="Órdenes Solicitadas"
+ onSearch={onSearch}
+ actions={actions}
+ dropdownComponent={
+ o.label)}
+ onChange={handleTypeChange}
+ selected={
+ typeOptions.find((o) => o.value === selectedType)?.label ||
+ 'Todos'
+ }
+ />
+ }
+ tableData={orders}
+ tableColumns={columns}
+ onView={(o) => router.push(`/orders/${o.id}`)}
+ onEdit={(o) => router.push(`/orders/${o.id}/edit`)}
+ pagination={{
+ currentPage: page,
+ totalPages,
+ totalItems: total,
+ itemsPerPage: limit,
+ onPageChange: setPage,
+ onItemsPerPageChange: (val) => {
+ setLimit(val);
+ setPage(1);
+ },
+ itemsPerPageOptions: [5, 10, 15, 20],
+ }}
+ isLoading={isLoading}
+ />
+
+ );
+}
diff --git a/src/app/(dashboard)/presentations/[id]/edit/page.tsx b/src/app/(dashboard)/presentations/[id]/edit/page.tsx
index b50d10d..952467c 100644
--- a/src/app/(dashboard)/presentations/[id]/edit/page.tsx
+++ b/src/app/(dashboard)/presentations/[id]/edit/page.tsx
@@ -152,7 +152,6 @@ export default function EditPresentationPage() {
onChange={(e) => setName(e.target.value)}
helperText={errors.name}
helperTextColor={Colors.semanticDanger}
- borderColor="#d1d5db"
borderSize="1px"
/>
@@ -181,7 +180,6 @@ export default function EditPresentationPage() {
onChange={(e) => setQuantity(e.target.value)}
helperText={errors.quantity}
helperTextColor={Colors.semanticDanger}
- borderColor="#d1d5db"
type="number"
borderSize="1px"
/>
@@ -195,7 +193,6 @@ export default function EditPresentationPage() {
onChange={(e) => setDescription(e.target.value)}
helperText={errors.description}
helperTextColor={Colors.semanticDanger}
- borderColor="#d1d5db"
type="text"
borderSize="1px"
isTextArea
diff --git a/src/app/(dashboard)/presentations/new/page.tsx b/src/app/(dashboard)/presentations/new/page.tsx
index 63313d0..5472c84 100644
--- a/src/app/(dashboard)/presentations/new/page.tsx
+++ b/src/app/(dashboard)/presentations/new/page.tsx
@@ -134,7 +134,6 @@ export default function NewPresentationPage() {
) => setName(e.target.value)}
helperText={errors.name}
helperTextColor={Colors.semanticDanger}
- borderColor="#d1d5db"
borderSize="1px"
/>
@@ -165,7 +164,6 @@ export default function NewPresentationPage() {
) => setQuantity(e.target.value)}
helperText={errors.quantity}
helperTextColor={Colors.semanticDanger}
- borderColor="#d1d5db"
type="number"
borderSize="1px"
/>
@@ -181,7 +179,6 @@ export default function NewPresentationPage() {
) => setDescription(e.target.value)}
helperText={errors.description}
helperTextColor={Colors.semanticDanger}
- borderColor="#d1d5db"
type="text"
borderSize="1px"
isTextArea
diff --git a/src/app/(dashboard)/presentations/page.tsx b/src/app/(dashboard)/presentations/page.tsx
index 8c97e0b..9a3607e 100644
--- a/src/app/(dashboard)/presentations/page.tsx
+++ b/src/app/(dashboard)/presentations/page.tsx
@@ -107,11 +107,8 @@ export default function PresentationListPage() {
},
itemsPerPageOptions: [5, 10, 15, 20],
}}
+ isLoading={isLoading}
/>
-
- {isLoading && (
- Cargando presentaciones...
- )}
);
}
diff --git a/src/app/(dashboard)/products/[id]/edit/page.tsx b/src/app/(dashboard)/products/[id]/edit/page.tsx
index d350183..ac2bcc1 100644
--- a/src/app/(dashboard)/products/[id]/edit/page.tsx
+++ b/src/app/(dashboard)/products/[id]/edit/page.tsx
@@ -221,7 +221,6 @@ export default function EditProductPage() {
onChange={(e) => setGenericName(e.target.value)}
helperText={errors.genericName}
helperTextColor={Colors.semanticDanger}
- borderColor="#d1d5db"
borderSize="1px"
/>
@@ -251,7 +250,6 @@ export default function EditProductPage() {
onChange={(e) => setName(e.target.value)}
helperText={errors.name}
helperTextColor={Colors.semanticDanger}
- borderColor="#d1d5db"
borderSize="1px"
/>
@@ -263,7 +261,6 @@ export default function EditProductPage() {
onChange={(e) => setPriority(e.target.value)}
helperText={errors.priority}
helperTextColor={Colors.semanticDanger}
- borderColor="#d1d5db"
type="number"
borderSize="1px"
/>
@@ -277,7 +274,6 @@ export default function EditProductPage() {
onChange={(e) => setDescription(e.target.value)}
helperText={errors.description}
helperTextColor={Colors.semanticDanger}
- borderColor="#d1d5db"
type="text"
borderSize="1px"
isTextArea
diff --git a/src/app/(dashboard)/products/[id]/page.tsx b/src/app/(dashboard)/products/[id]/page.tsx
index 224e1c1..1af4e9b 100644
--- a/src/app/(dashboard)/products/[id]/page.tsx
+++ b/src/app/(dashboard)/products/[id]/page.tsx
@@ -18,6 +18,7 @@ import TableContainer from '@/components/TableContainer';
import { useAuth } from '@/context/AuthContext';
import Loading from '../../loading';
import Input from '@/components/Input/Input';
+import { formatPrice } from '@/lib/utils/priceFormatter';
type ProductPresentationItem = ProductPresentationResponse;
export default function GenericProductDetailPage() {
@@ -46,7 +47,7 @@ export default function GenericProductDetailPage() {
{
key: 'price',
label: 'Precio',
- render: (item) => `$${item.price.toFixed(2)}`,
+ render: (item) => `$${formatPrice(item.price)}`,
},
{
key: 'promo',
diff --git a/src/app/(dashboard)/products/[id]/presentations/[presentationId]/edit/page.tsx b/src/app/(dashboard)/products/[id]/presentations/[presentationId]/edit/page.tsx
index 795a875..4b6acee 100644
--- a/src/app/(dashboard)/products/[id]/presentations/[presentationId]/edit/page.tsx
+++ b/src/app/(dashboard)/products/[id]/presentations/[presentationId]/edit/page.tsx
@@ -55,6 +55,8 @@ export default function EditProductPresentationPage() {
),
api.promo.findAll({ page: 1, limit: 50 }, token),
]);
+ // Convert price from cents to dollars
+ presData.price = presData.price / 100;
setPresentationData(presData);
setPromos(promoResp.results);
setPrice(presData.price.toString());
@@ -95,9 +97,9 @@ export default function EditProductPresentationPage() {
}
const payload = result.data;
-
+ // Convert to cents
+ payload.price = Number((payload.price * 100).toFixed(0));
try {
- console.log('Payload:', payload);
await api.productPresentation.update(productId, presentationId, payload);
toast.success('Presentación actualizada exitosamente');
setTimeout(() => {
@@ -184,7 +186,6 @@ export default function EditProductPresentationPage() {
onChange={(e) => setPrice(e.target.value)}
helperText={errors.price}
helperTextColor={Colors.semanticDanger}
- borderColor="#d1d5db"
borderSize="1px"
type="number"
/>
diff --git a/src/app/(dashboard)/products/[id]/presentations/[presentationId]/page.tsx b/src/app/(dashboard)/products/[id]/presentations/[presentationId]/page.tsx
index d6102a1..a042f65 100644
--- a/src/app/(dashboard)/products/[id]/presentations/[presentationId]/page.tsx
+++ b/src/app/(dashboard)/products/[id]/presentations/[presentationId]/page.tsx
@@ -12,6 +12,7 @@ import { toast } from 'react-toastify';
import { REDIRECTION_TIMEOUT } from '@/lib/utils/contants';
import { useAuth } from '@/context/AuthContext';
import Input from '@/components/Input/Input';
+import { formatPrice } from '@/lib/utils/priceFormatter';
export default function ViewProductPresentationPage() {
const params = useParams();
@@ -162,7 +163,7 @@ export default function ViewProductPresentationPage() {
diff --git a/src/app/(dashboard)/products/[id]/presentations/new/page.tsx b/src/app/(dashboard)/products/[id]/presentations/new/page.tsx
index 68d61fe..6414cf5 100644
--- a/src/app/(dashboard)/products/[id]/presentations/new/page.tsx
+++ b/src/app/(dashboard)/products/[id]/presentations/new/page.tsx
@@ -72,9 +72,9 @@ export default function AddProductPresentationPage() {
}
const payload = result.data;
-
+ // Convert to cents
+ payload.price = Number((payload.price * 100).toFixed(0));
try {
- console.log('Payload:', payload);
await api.productPresentation.create(productId, payload);
toast.success('Presentación añadida al producto');
setTimeout(() => {
@@ -177,7 +177,6 @@ export default function AddProductPresentationPage() {
onChange={(e) => setPrice(e.target.value)}
helperText={errors.price}
helperTextColor={Colors.semanticDanger}
- borderColor="#d1d5db"
type="number"
borderSize="1px"
/>
diff --git a/src/app/(dashboard)/products/new/page.tsx b/src/app/(dashboard)/products/new/page.tsx
index e84a7f6..4c67e01 100644
--- a/src/app/(dashboard)/products/new/page.tsx
+++ b/src/app/(dashboard)/products/new/page.tsx
@@ -153,7 +153,6 @@ export default function NewGenericProductPage() {
helperText={errors.genericName}
helperTextColor={Colors.semanticDanger}
borderSize="1px"
- borderColor="#d1d5db"
/>
@@ -180,7 +179,6 @@ export default function NewGenericProductPage() {
helperText={errors.name}
helperTextColor={Colors.semanticDanger}
borderSize="1px"
- borderColor="#d1d5db"
/>
@@ -192,7 +190,6 @@ export default function NewGenericProductPage() {
helperText={errors.priority}
helperTextColor={Colors.semanticDanger}
borderSize="1px"
- borderColor="#d1d5db"
type="number"
/>
@@ -206,7 +203,6 @@ export default function NewGenericProductPage() {
helperText={errors.description}
helperTextColor={Colors.semanticDanger}
borderSize="1px"
- borderColor="#d1d5db"
type="text"
isTextArea
rows={4}
diff --git a/src/app/(dashboard)/products/page.tsx b/src/app/(dashboard)/products/page.tsx
index 5627e72..bb20895 100644
--- a/src/app/(dashboard)/products/page.tsx
+++ b/src/app/(dashboard)/products/page.tsx
@@ -165,9 +165,8 @@ export default function GenericProductListPage() {
},
itemsPerPageOptions: [5, 10, 15, 20],
}}
+ isLoading={isLoading}
/>
-
- {isLoading && Cargando productos…
}
);
}
diff --git a/src/app/(dashboard)/profile/edit/page.tsx b/src/app/(dashboard)/profile/edit/page.tsx
index 7a600af..d31a033 100644
--- a/src/app/(dashboard)/profile/edit/page.tsx
+++ b/src/app/(dashboard)/profile/edit/page.tsx
@@ -103,7 +103,6 @@ export default function EditProfilePage() {
label="Nombre"
value={firstName}
onChange={(e) => setFirstName(e.target.value)}
- borderColor="#E7E7E6"
/>
{errors.firstName && (
{errors.firstName}
@@ -114,7 +113,6 @@ export default function EditProfilePage() {
label="Apellido"
value={lastName}
onChange={(e) => setLastName(e.target.value)}
- borderColor="#E7E7E6"
/>
{errors.lastName && (
{errors.lastName}
@@ -126,7 +124,6 @@ export default function EditProfilePage() {
type="text"
helperText={errors.documentId}
value={profile.documentId}
- borderColor="#E7E7E6"
readViewOnly
/>
@@ -136,7 +133,6 @@ export default function EditProfilePage() {
type="text"
helperText={errors.documentId}
value={profile.email}
- borderColor="#E7E7E6"
readViewOnly
/>
@@ -155,7 +151,6 @@ export default function EditProfilePage() {
label="Número de Teléfono"
value={phone}
onChange={(e) => setPhone(e.target.value)}
- borderColor="#E7E7E6"
/>
{errors.phone && (
{errors.phone}
diff --git a/src/app/(dashboard)/profile/layout.tsx b/src/app/(dashboard)/profile/layout.tsx
index fb54c53..47dcbcb 100644
--- a/src/app/(dashboard)/profile/layout.tsx
+++ b/src/app/(dashboard)/profile/layout.tsx
@@ -9,6 +9,7 @@ import { usePathname } from 'next/navigation';
import { UserList } from '@pharmatech/sdk';
import Breadcrumb from '@/components/Breadcrumb';
import { toast } from 'react-toastify';
+import Loader from '@/components/Loader';
interface ProfileLayoutProps {
children: ReactNode;
@@ -48,7 +49,7 @@ export default function ProfileLayout({ children }: ProfileLayoutProps) {
})();
}, [user, token]);
- if (!profile) return Cargando perfil…
;
+ if (!profile) return ;
const handleImageUpload = async (file: File) => {
setUploading(true);
diff --git a/src/app/(dashboard)/profile/page.tsx b/src/app/(dashboard)/profile/page.tsx
index 2d6c7c9..1e96a33 100644
--- a/src/app/(dashboard)/profile/page.tsx
+++ b/src/app/(dashboard)/profile/page.tsx
@@ -6,7 +6,6 @@ import Input from '@/components/Input/Input';
import Button from '@/components/Button';
import { useAuth } from '@/context/AuthContext';
import { api } from '@/lib/sdkConfig';
-import Loader from '@/components/Loader';
import { toast } from 'react-toastify';
import { UserList } from '@pharmatech/sdk';
@@ -14,17 +13,14 @@ export default function ProfilePage() {
const { user, token } = useAuth();
const router = useRouter();
const [profile, setProfile] = useState(null);
- const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const fetchProfile = useCallback(async () => {
if (!user?.sub || !token) {
setError('Usuario no autenticado');
- setLoading(false);
return;
}
try {
- setLoading(true);
const res = await api.user.getProfile(user.sub, token);
setProfile(res);
setError(null);
@@ -33,7 +29,6 @@ export default function ProfilePage() {
toast.error('Error cargando perfil');
setError('No se pudo cargar tu perfil');
} finally {
- setLoading(false);
}
}, [user, token]);
@@ -41,7 +36,6 @@ export default function ProfilePage() {
fetchProfile();
}, [fetchProfile]);
- if (loading) return ;
if (error) return {error}
;
if (!profile) return null;
diff --git a/src/app/(dashboard)/profile/security/page.tsx b/src/app/(dashboard)/profile/security/page.tsx
index df97111..de8065d 100644
--- a/src/app/(dashboard)/profile/security/page.tsx
+++ b/src/app/(dashboard)/profile/security/page.tsx
@@ -57,7 +57,6 @@ export default function SecurityPage() {
value={currentPassword}
onChange={(e) => setCurrentPassword(e.target.value)}
helperText={errors.currentPassword}
- borderColor="#E7E7E6"
/>
setNewPassword(e.target.value)}
helperText={errors.newPassword}
- borderColor="#E7E7E6"
/>
setConfirmPassword(e.target.value)}
helperText={errors.confirmPassword}
- borderColor="#E7E7E6"
/>
diff --git a/src/app/(dashboard)/promos/[id]/edit/page.tsx b/src/app/(dashboard)/promos/[id]/edit/page.tsx
index 6ae6432..43a4e1f 100644
--- a/src/app/(dashboard)/promos/[id]/edit/page.tsx
+++ b/src/app/(dashboard)/promos/[id]/edit/page.tsx
@@ -165,7 +165,6 @@ export default function EditPromoPage() {
onChange={(e) => setTempName(e.target.value)}
helperText={errors.name}
helperTextColor={Colors.semanticDanger}
- borderColor="#d1d5db"
/>
diff --git a/src/app/(dashboard)/promos/[id]/page.tsx b/src/app/(dashboard)/promos/[id]/page.tsx
index 7b8f7ab..8b41983 100644
--- a/src/app/(dashboard)/promos/[id]/page.tsx
+++ b/src/app/(dashboard)/promos/[id]/page.tsx
@@ -8,11 +8,11 @@ import ModalConfirm from '@/components/ModalConfirm';
import { Colors } from '@/styles/styles';
import { api } from '@/lib/sdkConfig';
import { toast } from 'react-toastify';
-import { format } from 'date-fns';
import { PromoResponse } from '@pharmatech/sdk/types';
import { useAuth } from '@/context/AuthContext';
import Loading from '../../loading';
import Input from '@/components/Input/Input';
+import { formatDateSafe } from '@/lib/utils/useFormatDate';
export default function PromoDetailsPage() {
const params = useParams();
@@ -76,14 +76,6 @@ export default function PromoDetailsPage() {
const handleCancelDelete = () => setShowDeleteModal(false);
- const formatDate = (date: Date) => {
- try {
- return format(date, 'dd/MM/yyyy');
- } catch {
- return date.toString();
- }
- };
-
if (!promo) {
return
;
}
@@ -168,7 +160,7 @@ export default function PromoDetailsPage() {
@@ -176,7 +168,7 @@ export default function PromoDetailsPage() {
diff --git a/src/app/(dashboard)/promos/new/page.tsx b/src/app/(dashboard)/promos/new/page.tsx
index 465dd0c..ae67653 100644
--- a/src/app/(dashboard)/promos/new/page.tsx
+++ b/src/app/(dashboard)/promos/new/page.tsx
@@ -11,6 +11,7 @@ import { api } from '@/lib/sdkConfig';
import { toast } from 'react-toastify';
import { promoSchema } from '@/lib/validations/promoSchema';
import { useAuth } from '@/context/AuthContext';
+import { parseApiDate } from '@/lib/utils/useFormatDate';
export default function NewPromotionPage() {
const router = useRouter();
@@ -48,8 +49,8 @@ export default function NewPromotionPage() {
const validationData = {
name: name.trim(),
discount: Number(discount),
- startAt,
- expiredAt,
+ startAt: startAt,
+ expiredAt: expiredAt,
};
const validationResult = promoSchema.safeParse(validationData);
@@ -141,7 +142,6 @@ export default function NewPromotionPage() {
onChange={(e) => setName(e.target.value)}
helperText={errors.name}
helperTextColor={Colors.semanticDanger}
- borderColor="#d1d5db"
/>
@@ -154,7 +154,6 @@ export default function NewPromotionPage() {
}
helperText={errors.discount}
helperTextColor={Colors.semanticDanger}
- borderColor="#d1d5db"
type="number"
/>
@@ -163,7 +162,7 @@ export default function NewPromotionPage() {
- setStartAt(new Date(date))} />
+ setStartAt(parseApiDate(date))} />
{errors.startAt && (
{errors.startAt}
)}
@@ -172,7 +171,9 @@ export default function NewPromotionPage() {
- setExpiredAt(new Date(date))} />
+ setExpiredAt(parseApiDate(date))}
+ />
{errors.expiredAt && (
{errors.expiredAt}
)}
diff --git a/src/app/(dashboard)/promos/page.tsx b/src/app/(dashboard)/promos/page.tsx
index 84aaf1c..93548fb 100644
--- a/src/app/(dashboard)/promos/page.tsx
+++ b/src/app/(dashboard)/promos/page.tsx
@@ -10,6 +10,7 @@ import { Pagination, PromoResponse } from '@pharmatech/sdk';
import { useAuth } from '@/context/AuthContext';
import Badge from '@/components/Badge';
import { toast } from 'react-toastify';
+import { formatDateSafe, parseApiDate } from '@/lib/utils/useFormatDate';
// Presets de rango de expiración que el backend acepta via expirationBetween
const expirationTranslations: Record = {
@@ -94,11 +95,15 @@ export default function PromosPage() {
const totalPages = Math.ceil(totalItems / limit);
// calcular estado para columna
- const calcStatus = (start: Date, end: Date): 'Activa' | 'Finalizada' => {
+
+ const calcStatus = (
+ start: string | Date,
+ end: string | Date,
+ ): 'Activa' | 'Finalizada' => {
const now = new Date();
- return now >= start && now <= end ? 'Activa' : 'Finalizada';
+ const parsedEnd = parseApiDate(end);
+ return now <= parsedEnd ? 'Activa' : 'Finalizada';
};
-
// definición de columnas
const columns: Column[] = [
{ key: 'name', label: 'Nombre', render: (p: PromoResponse) => p.name },
@@ -110,20 +115,18 @@ export default function PromosPage() {
{
key: 'startAt',
label: 'Inicio',
- render: (p: PromoResponse) =>
- new Date(p.startAt).toLocaleDateString('es-ES'),
+ render: (p: PromoResponse) => formatDateSafe(p.startAt),
},
{
key: 'expiredAt',
label: 'Fin',
- render: (p: PromoResponse) =>
- new Date(p.expiredAt).toLocaleDateString('es-ES'),
+ render: (p: PromoResponse) => formatDateSafe(p.expiredAt),
},
{
key: 'status',
label: 'Estado',
render: (p: PromoResponse) => {
- const status = calcStatus(new Date(p.startAt), new Date(p.expiredAt));
+ const status = calcStatus(p.startAt, p.expiredAt);
return (
{
+ api.promo
+ .bulkUpdate(
+ {
+ ids: promos.map((p) => p.id),
+ expiredAt: new Date(),
+ },
+ token!,
+ )
+ .then(() => {
+ toast.success('Promociones expiradas');
+ fetchPromos();
+ })
+ .catch((err) => {
+ console.error('Error al expirar las promociones:', err);
+ toast.error('Error al expirar las promociones');
+ });
+ };
+
+ const handleDeletePromos = async (promos: PromoResponse[]) => {
+ api.promo
+ .bulkDelete(
+ {
+ ids: promos.map((p) => p.id),
+ },
+ token!,
+ )
+ .then(() => {
+ toast.success('Promociones eliminadas');
+ fetchPromos();
+ })
+ .catch((err) => {
+ console.error('Error al eliminar las promociones:', err);
+ toast.error('Error al eliminar las promociones');
+ });
+ };
+
+ const actions = [
+ {
+ label: 'Expirar hoy',
+ onClick: handleSetExpiredAtToday,
+ },
+ {
+ label: 'Eliminar',
+ onClick: handleDeletePromos,
+ },
+ ];
+
return (
title="Promociones"
onSearch={handleSearch}
+ actions={actions}
dropdownComponent={
-
- {isLoading && (
-
Cargando promociones...
- )}
);
}
diff --git a/src/app/(dashboard)/reports/inventory/page.tsx b/src/app/(dashboard)/reports/inventory/page.tsx
index 1da2a94..4e745b9 100644
--- a/src/app/(dashboard)/reports/inventory/page.tsx
+++ b/src/app/(dashboard)/reports/inventory/page.tsx
@@ -8,11 +8,12 @@ import { api } from '@/lib/sdkConfig';
import PDFReportTemplate from '@/components/FileHelper/PDFReportTemplate';
import {
- ProductPresentationResponse,
- ProductPresentationDetailResponse,
StateResponse,
CityResponse,
+ ProductPresentation,
} from '@pharmatech/sdk';
+import { formatPrice } from '@/lib/utils/priceFormatter';
+import Button from '@/components/Button';
const COUNTRY_ID = '1238bc2a-45a5-47e4-9cc1-68d573089ca1';
@@ -24,20 +25,12 @@ export default function InventoryReportPreview() {
const [cities, setCities] = useState([]);
const [selectedState, setSelectedState] = useState('');
const [selectedCity, setSelectedCity] = useState('');
-
- const [productData, setProductData] = useState(
- [],
- );
- const [detailsMap, setDetailsMap] = useState<
- Record
- >({});
+ const [productData, setProductData] = useState([]);
useEffect(() => {
if (!token || !user?.sub) return;
-
(async () => {
- const profile = await api.user.getProfile(user.sub, token);
- setUserName(`${profile.firstName} ${profile.lastName}`);
+ setUserName(user.name);
const stateRes = await api.state.findAll({
page: 1,
@@ -65,17 +58,6 @@ export default function InventoryReportPreview() {
const fetchData = async () => {
const res = await api.product.getProducts({ page: 1, limit: 100 });
setProductData(res.results);
-
- const detailMap: Record = {};
- for (const prod of res.results) {
- const detail = await api.productPresentation.getByPresentationId(
- prod.product.id,
- prod.presentation.id,
- );
- detailMap[prod.presentation.id] = detail;
- }
-
- setDetailsMap(detailMap);
};
const columns: {
@@ -96,9 +78,8 @@ export default function InventoryReportPreview() {
const tableData = useMemo(() => {
return productData.map((p) => {
- const detail = detailsMap[p.presentation.id];
- const genericName = detail?.product?.genericName || '-';
- const presentationName = detail?.presentation?.name || '-';
+ const genericName = p.product.genericName;
+ const presentationName = p.product.name;
const stock = p.stock ?? 0;
const price = p.price;
@@ -106,17 +87,18 @@ export default function InventoryReportPreview() {
genericName,
presentationName,
stockQuantity: stock,
- price,
- totalValue: stock * price,
+ price: formatPrice(price),
+ totalValue: formatPrice(stock * price),
};
});
- }, [productData, detailsMap]);
+ }, [productData]);
const handleDownload = async () => {
const printDate = new Date().toLocaleDateString('es-VE');
- const totalValue = tableData
- .reduce((acc, row) => acc + row.totalValue, 0)
- .toFixed(2);
+ const totalValue = tableData.reduce(
+ (acc, row) => acc + Number(row.totalValue) * 100,
+ 0,
+ );
const blob = await pdf(
,
).toBlob();
@@ -135,14 +120,16 @@ export default function InventoryReportPreview() {
};
return (
-
-
+
+
Reporte de Inventario
-
+
-
+
-
+
+
-
-
-
+
+
{tableData.length > 0 && (
-
+
+
+
)}
);
diff --git a/src/app/(dashboard)/reports/sales/page.tsx b/src/app/(dashboard)/reports/sales/page.tsx
index 093fa5f..0322f1f 100644
--- a/src/app/(dashboard)/reports/sales/page.tsx
+++ b/src/app/(dashboard)/reports/sales/page.tsx
@@ -16,6 +16,8 @@ import {
CityResponse,
BranchResponse,
} from '@pharmatech/sdk';
+import { formatPrice } from '@/lib/utils/priceFormatter';
+import Button from '@/components/Button';
const COUNTRY_ID = '1238bc2a-45a5-47e4-9cc1-68d573089ca1';
@@ -38,8 +40,7 @@ export default function ReportPreviewPage() {
(async () => {
try {
- const profile = await api.user.getProfile(user.sub, token);
- setUserName(`${profile.firstName} ${profile.lastName}`);
+ setUserName(user.name);
const stateResponse = await api.state.findAll({
page: 1,
@@ -110,8 +111,18 @@ export default function ReportPreviewPage() {
{ key: 'discount', label: 'Descuento' },
{ key: 'total', label: 'Total' },
];
-
- const formatCurrency = (n: number) => `$${n.toFixed(2)}`;
+ const tableData = useMemo(() => {
+ return reportData?.items.map((item) => {
+ return {
+ ...item,
+ orderId: `#${String(item.orderId).slice(0, 4)}`,
+ subtotal: Number(formatPrice(item.subtotal)),
+ discount: Number(formatPrice(item.discount)),
+ date: new Date(item.date).toLocaleDateString('es-VE'),
+ total: Number(formatPrice(item.total)),
+ };
+ });
+ }, [reportData]);
const handleDownload = async () => {
if (!reportData || !startDate || !endDate) return;
@@ -121,13 +132,13 @@ export default function ReportPreviewPage() {
const totals = [
{
label: 'Subtotal General',
- value: formatCurrency(reportData.totals.subtotal),
+ value: formatPrice(reportData.totals.subtotal),
},
{
label: 'Descuento Total',
- value: formatCurrency(reportData.totals.discount),
+ value: formatPrice(reportData.totals.discount),
},
- { label: 'Total Final', value: formatCurrency(reportData.totals.total) },
+ { label: 'Total Final', value: formatPrice(reportData.totals.total) },
];
const blob = await pdf(
@@ -137,15 +148,7 @@ export default function ReportPreviewPage() {
userName={userName}
printDate={printDate}
columns={columns}
- data={reportData.items.map((item) => {
- const dateObj = new Date(item.date);
- const formattedDate = dateObj.toLocaleDateString('es-VE');
- return {
- ...item,
- orderId: `#${String(item.orderId).slice(0, 4)}`,
- date: formattedDate,
- };
- })}
+ data={tableData!}
totals={totals}
/>,
).toBlob();
@@ -154,25 +157,31 @@ export default function ReportPreviewPage() {
};
return (
-
+
Reporte de Ventas
{/* Filtros de fecha */}
-
+
-
+
{/* Filtros de ubicación */}
-
-
-
+
+
+
-
-
+
+
-
-
+
+
-
+
);
diff --git a/src/app/(dashboard)/upload-files/page.tsx b/src/app/(dashboard)/upload-files/page.tsx
index ed69162..5f0f6b5 100644
--- a/src/app/(dashboard)/upload-files/page.tsx
+++ b/src/app/(dashboard)/upload-files/page.tsx
@@ -4,8 +4,10 @@ import CsvUploader from '@/components/FileHelper/UploadCsv';
export default function InventoryPage() {
return (
-
-
Gestión de Inventario
+
+
+ Gestión de Inventario
+
);
diff --git a/src/app/(dashboard)/users/[id]/edit/page.tsx b/src/app/(dashboard)/users/[id]/edit/page.tsx
index 5c9c095..d613b5d 100644
--- a/src/app/(dashboard)/users/[id]/edit/page.tsx
+++ b/src/app/(dashboard)/users/[id]/edit/page.tsx
@@ -15,6 +15,7 @@ import Input from '@/components/Input/Input';
import Dropdown from '@/components/Dropdown';
import RadioButton from '@/components/RadioButton';
import { FontSizes } from '@/styles/styles';
+import { convertSlashDateToIso } from '@/lib/utils/useFormatDate';
const roleLabels: Record
= {
[UserRole.ADMIN]: 'Administrador',
@@ -23,13 +24,6 @@ const roleLabels: Record = {
[UserRole.DELIVERY]: 'Delivery',
};
-const formatDate = (dateStr: string): string => {
- const parts = dateStr.split('/');
- if (parts.length !== 3) return dateStr;
- const [day, month, year] = parts;
- return `${year}-${month.padStart(2, '0')}-${day.padStart(2, '0')}`;
-};
-
export default function EditUserPage() {
const params = useParams();
const id = params?.id && typeof params.id === 'string' ? params.id : '';
@@ -96,25 +90,25 @@ export default function EditUserPage() {
const genero = gender === UserGender.MALE ? 'hombre' : 'mujer';
const result = registerSchema.safeParse({
- nombre: firstName,
- apellido: lastName,
+ firstName: firstName,
+ lastName: lastName,
email,
- cedula: documentId,
- telefono: phoneNumber,
- fechaNacimiento: birthDate ? formatDate(birthDate) : null,
- genero,
+ documentId: documentId,
+ phoneNumber: phoneNumber,
+ birthDate: birthDate ? convertSlashDateToIso(birthDate) : null,
+ gender: genero,
});
if (!result.success) {
const fieldErrors = result.error.flatten().fieldErrors;
setErrors({
- firstName: fieldErrors.nombre?.[0] || '',
- lastName: fieldErrors.apellido?.[0] || '',
+ firstName: fieldErrors.firstName?.[0] || '',
+ lastName: fieldErrors.lastName?.[0] || '',
email: fieldErrors.email?.[0] || '',
- documentId: fieldErrors.cedula?.[0] || '',
- phoneNumber: fieldErrors.telefono?.[0] || '',
- birthDate: fieldErrors.fechaNacimiento?.[0] || '',
- gender: fieldErrors.genero?.[0] || '',
+ documentId: fieldErrors.documentId?.[0] || '',
+ phoneNumber: fieldErrors.phoneNumber?.[0] || '',
+ birthDate: fieldErrors.birthDate?.[0] || '',
+ gender: fieldErrors.gender?.[0] || '',
});
toast.error('Por favor, revisa los errores en el formulario');
return;
@@ -132,7 +126,7 @@ export default function EditUserPage() {
firstName: firstName ?? undefined,
lastName: lastName ?? undefined,
phoneNumber: phoneNumber ?? undefined,
- birthDate: birthDate ? formatDate(birthDate) : undefined,
+ birthDate: birthDate ? convertSlashDateToIso(birthDate) : undefined,
gender,
role: role ?? undefined,
};
@@ -206,7 +200,6 @@ export default function EditUserPage() {
helperText={errors.firstName}
helperTextColor={Colors.semanticDanger}
borderSize="1px"
- borderColor={Colors.stroke}
/>
@@ -219,7 +212,6 @@ export default function EditUserPage() {
helperText={errors.documentId}
helperTextColor={Colors.semanticDanger}
borderSize="1px"
- borderColor={Colors.stroke}
readViewOnly
/>
@@ -233,7 +225,6 @@ export default function EditUserPage() {
onChange={(e) => setBirthDate(e.target.value)}
helperText={errors.birthDate}
helperTextColor={Colors.semanticDanger}
- borderColor={Colors.stroke}
borderSize="1px"
/>
@@ -247,7 +238,6 @@ export default function EditUserPage() {
onChange={(e) => setPhoneNumber(e.target.value)}
helperText={errors.phoneNumber}
helperTextColor={Colors.semanticDanger}
- borderColor={Colors.stroke}
borderSize="1px"
/>
@@ -264,7 +254,6 @@ export default function EditUserPage() {
onChange={(e) => setLastName(e.target.value)}
helperText={errors.lastName}
helperTextColor={Colors.semanticDanger}
- borderColor={Colors.stroke}
borderSize="1px"
/>
@@ -330,7 +319,6 @@ export default function EditUserPage() {
onChange={(e) => setEmail(e.target.value)}
helperText={errors.email}
helperTextColor={Colors.semanticDanger}
- borderColor={Colors.stroke}
borderSize="1px"
readViewOnly
/>
diff --git a/src/app/(dashboard)/users/[id]/page.tsx b/src/app/(dashboard)/users/[id]/page.tsx
index 9c6ec3f..f43d390 100644
--- a/src/app/(dashboard)/users/[id]/page.tsx
+++ b/src/app/(dashboard)/users/[id]/page.tsx
@@ -14,6 +14,7 @@ import { useAuth } from '@/context/AuthContext';
import Loading from '../../loading';
import { UserList, UserRole } from '@pharmatech/sdk';
import Input from '@/components/Input/Input';
+import { formatDateSafe } from '@/lib/utils/useFormatDate';
const roleLabels = {
[UserRole.ADMIN]: 'Administrador',
@@ -79,9 +80,8 @@ export default function UserDetailsPage() {
const handleCancel = () => setShowModal(false);
const formattedBirthDate = user?.profile?.birthDate
- ? new Date(user.profile.birthDate).toLocaleDateString('es-ES')
+ ? formatDateSafe(user.profile.birthDate)
: '';
-
const displayGender =
user?.profile?.gender === 'm'
? 'Masculino'
diff --git a/src/app/(dashboard)/users/new/page.tsx b/src/app/(dashboard)/users/new/page.tsx
index 9aa5309..2b85050 100644
--- a/src/app/(dashboard)/users/new/page.tsx
+++ b/src/app/(dashboard)/users/new/page.tsx
@@ -14,6 +14,7 @@ import { registerSchema } from '@/lib/validations/registerSchema';
import { REDIRECTION_TIMEOUT } from '@/lib/utils/contants';
import { UserGender, UserRole } from '@pharmatech/sdk';
import Input from '@/components/Input/Input';
+import { convertSlashDateToIso } from '@/lib/utils/useFormatDate';
// Mapeo para mostrar etiquetas en el dropdown y obtener el valor que espera la API
const roleMapping: Record
= {
@@ -23,14 +24,6 @@ const roleMapping: Record = {
Delivery: UserRole.DELIVERY,
};
-// Función para convertir fecha de "DD/MM/YYYY" a "YYYY-MM-DD"
-const formatDate = (dateStr: string): string => {
- const parts = dateStr.split('/');
- if (parts.length !== 3) return dateStr;
- const [day, month, year] = parts;
- return `${year}-${month.padStart(2, '0')}-${day.padStart(2, '0')}`;
-};
-
function getErrorMessage(error: unknown): string {
if (typeof error === 'object' && error !== null && 'messages' in error) {
const errorObj = error as Record;
@@ -88,25 +81,25 @@ export default function NewUserPage() {
// Prepara los datos para la validación usando el schema
const result = registerSchema.safeParse({
- nombre: firstName,
- apellido: lastName,
+ firstName: firstName,
+ lastName: lastName,
email,
- cedula: documentId,
- telefono: phoneNumber,
- fechaNacimiento: formatDate(birthDate), // Se espera formato yyyy-mm-dd
- genero,
+ documentId: documentId,
+ phoneNumber: phoneNumber,
+ birthDate: convertSlashDateToIso(birthDate), // Se espera formato yyyy-mm-dd
+ gender: genero,
});
if (!result.success) {
const { fieldErrors } = result.error.flatten();
setErrors({
- firstName: fieldErrors.nombre?.[0] || '',
- lastName: fieldErrors.apellido?.[0] || '',
+ firstName: fieldErrors.firstName?.[0] || '',
+ lastName: fieldErrors.lastName?.[0] || '',
email: fieldErrors.email?.[0] || '',
- documentId: fieldErrors.cedula?.[0] || '',
- phoneNumber: fieldErrors.telefono?.[0] || '',
- birthDate: fieldErrors.fechaNacimiento?.[0] || '',
- gender: fieldErrors.genero?.[0] || '',
+ documentId: fieldErrors.documentId?.[0] || '',
+ phoneNumber: fieldErrors.phoneNumber?.[0] || '',
+ birthDate: fieldErrors.birthDate?.[0] || '',
+ gender: fieldErrors.gender?.[0] || '',
});
toast.error('Por favor, revisa los errores en el formulario');
return;
@@ -127,7 +120,7 @@ export default function NewUserPage() {
}
const mappedRole = roleMapping[role] || UserRole.CUSTOMER;
- const formattedBirthDate = formatDate(birthDate);
+ const formattedBirthDate = convertSlashDateToIso(birthDate);
const payload = {
firstName,
@@ -209,7 +202,6 @@ export default function NewUserPage() {
helperText={errors.firstName}
helperTextColor={Colors.semanticDanger}
borderSize="1px"
- borderColor="#E7E7E6"
/>
@@ -222,7 +214,6 @@ export default function NewUserPage() {
helperText={errors.lastName}
helperTextColor={Colors.semanticDanger}
borderSize="1px"
- borderColor="#E7E7E6"
/>
@@ -237,7 +228,6 @@ export default function NewUserPage() {
helperText={errors.documentId}
helperTextColor={Colors.semanticDanger}
borderSize="1px"
- borderColor="#E7E7E6"
/>
@@ -302,7 +292,6 @@ export default function NewUserPage() {
helperText={errors.phoneNumber}
helperTextColor={Colors.semanticDanger}
borderSize="1px"
- borderColor="#E7E7E6"
/>
@@ -315,7 +304,6 @@ export default function NewUserPage() {
helperText={errors.email}
helperTextColor={Colors.semanticDanger}
borderSize="1px"
- borderColor="#E7E7E6"
/>
diff --git a/src/app/(dashboard)/users/page.tsx b/src/app/(dashboard)/users/page.tsx
index b1fa950..dacb807 100644
--- a/src/app/(dashboard)/users/page.tsx
+++ b/src/app/(dashboard)/users/page.tsx
@@ -7,6 +7,7 @@ import Dropdown from '@/components/Dropdown';
import { api } from '@/lib/sdkConfig';
import { Pagination, UserList, UserRole } from '@pharmatech/sdk';
import { useAuth } from '@/context/AuthContext';
+import { toast } from 'react-toastify';
const roleTranslations: Record = {
'': 'Todos',
@@ -119,6 +120,60 @@ export default function UsersPage() {
setCurrentPage(1);
};
+ const handleRoleUpdate = async (users: UserList[], userRole: UserRole) => {
+ api.user
+ .bulkUpdate(
+ {
+ users: users.map((u) => u.id),
+ role: userRole,
+ },
+ token!,
+ )
+ .then(() => {
+ toast.success('Usuarios actualizados');
+ fetchUsers(currentPage, itemsPerPage, searchQuery, selectedRole);
+ })
+ .catch((err) => {
+ console.error('Error al actualizar el rol de los usuarios:', err);
+ toast.error('Error al actualizar el rol de los usuarios');
+ });
+ };
+
+ const handleRoleUpdateToCustomer = async (users: UserList[]) => {
+ handleRoleUpdate(users, UserRole.CUSTOMER);
+ };
+
+ const handleRoleUpdateToDelivery = async (users: UserList[]) => {
+ handleRoleUpdate(users, UserRole.DELIVERY);
+ };
+
+ const handleRoleUpdateToBranchAdmin = async (users: UserList[]) => {
+ handleRoleUpdate(users, UserRole.BRANCH_ADMIN);
+ };
+
+ const handleRoleUpdateToAdmin = async (users: UserList[]) => {
+ handleRoleUpdate(users, UserRole.ADMIN);
+ };
+
+ const actions = [
+ {
+ label: 'Cambiar a Cliente',
+ onClick: handleRoleUpdateToCustomer,
+ },
+ {
+ label: 'Cambiar a Repartidor',
+ onClick: handleRoleUpdateToDelivery,
+ },
+ {
+ label: 'Cambiar a Administrador de Sucursal',
+ onClick: handleRoleUpdateToBranchAdmin,
+ },
+ {
+ label: 'Cambiar a Administrador',
+ onClick: handleRoleUpdateToAdmin,
+ },
+ ];
+
return (
router.push('/users/new')}
addButtonText="Agregar Usuario"
onSearch={handleSearch}
+ actions={actions}
dropdownComponent={
- {isLoading && (
-
Cargando usuarios...
- )}
);
}
diff --git a/src/components/ActionsTable.tsx b/src/components/ActionsTable.tsx
index 5c2fc2f..76ff70d 100644
--- a/src/components/ActionsTable.tsx
+++ b/src/components/ActionsTable.tsx
@@ -1,72 +1,136 @@
'use client';
-import { useCallback, useState } from 'react';
+import { useCallback, useEffect, useRef, useState } from 'react';
import {
EllipsisVerticalIcon,
PlusIcon,
MagnifyingGlassIcon,
} from '@heroicons/react/24/outline';
+import { toast } from 'react-toastify';
-interface ActionsTableProps {
+type Action = {
+ label: string;
+ onClick: (values: T[]) => void;
+};
+
+interface ActionsTableProps {
addButtonText: string;
+ selectedRows: T[];
+ setSelectedRows: (rows: T[]) => void;
onAddClick?: () => void;
onSearch?: (query: string) => void;
+ actions?: Action[];
}
-export default function ActionTable({
+export default function ActionTable({
addButtonText,
+ selectedRows,
+ setSelectedRows,
onAddClick,
onSearch,
-}: ActionsTableProps) {
+ actions,
+}: ActionsTableProps) {
const [searchTerm, setSearchTerm] = useState('');
-
+ const [isActionsOpen, setIsActionsOpen] = useState(false);
+ const actionsRef = useRef(null);
const handleSearchClick = useCallback(() => {
onSearch?.(searchTerm);
console.log('Buscando:', searchTerm);
}, [onSearch, searchTerm]);
+ useEffect(() => {
+ function handleClickOutside(event: MouseEvent) {
+ if (
+ actionsRef.current &&
+ !actionsRef.current.contains(event.target as Node)
+ ) {
+ setIsActionsOpen(false);
+ }
+ }
+ if (isActionsOpen) {
+ document.addEventListener('mousedown', handleClickOutside);
+ }
+ return () => {
+ document.removeEventListener('mousedown', handleClickOutside);
+ };
+ }, [isActionsOpen]);
+
+ const onActionClick = (action: Action) => {
+ if (selectedRows.length > 0) {
+ action.onClick(selectedRows);
+ setSelectedRows([]);
+ } else {
+ toast.error('No hay filas seleccionadas');
+ }
+ setIsActionsOpen(false);
+ };
+
return (
{/* Botón de "Acciones" */}
-
-
-
+ {actions && actions.length > 0 && (
+
+
+ {isActionsOpen && (
+
+
+ {actions.map((action, index) => (
+ -
+
+
+ ))}
+
+
+ )}
+
+ )}
{/* Contenedor del SearchBar y botón */}
{/* SearchBar (El que esta en components no cumple las propiedades...) */}
-
-
- {/* Botón "Agregar" */}
-
+ )}
);
diff --git a/src/components/FileHelper/UploadCsv.tsx b/src/components/FileHelper/UploadCsv.tsx
index 595498f..28097f1 100644
--- a/src/components/FileHelper/UploadCsv.tsx
+++ b/src/components/FileHelper/UploadCsv.tsx
@@ -4,6 +4,7 @@ import { useRef } from 'react';
import { useCsvUploader } from '@/lib/utils/useCsvUploader';
import InventoryTable from '@/components/InventoryTable';
import FileUploader from '@/components/FileUploader';
+import Loader from '../Loader';
export default function CsvUploader() {
const { csvData, fileName, loading, parseCsv, clearCsv } = useCsvUploader();
@@ -26,15 +27,13 @@ export default function CsvUploader() {
return (
-
Importar Inventario CSV
-
- {loading &&
Cargando archivo...
}
+ {loading &&
}
{fileName && (
diff --git a/src/components/GoogleMap/GoogleMap.tsx b/src/components/GoogleMap/GoogleMap.tsx
new file mode 100644
index 0000000..fea384c
--- /dev/null
+++ b/src/components/GoogleMap/GoogleMap.tsx
@@ -0,0 +1,132 @@
+'use client';
+
+import { useState, useEffect } from 'react';
+import {
+ GoogleMap,
+ useJsApiLoader,
+ Marker,
+ InfoWindow,
+} from '@react-google-maps/api';
+import { GOOGLE_MAPS_OPTIONS } from '@/lib/helpers/googleMapsConfig';
+
+export interface BranchMarker {
+ id: string;
+ name: string;
+ latitude: number;
+ longitude: number;
+ address: string;
+}
+
+interface GoogleMapsProps {
+ markers?: BranchMarker[];
+ draggable?: boolean;
+ center?: { lat: number; lng: number };
+ onCoordinateChange?: (lat: number, lng: number) => void;
+ onAddressChange?: (address: string) => void;
+ mapHeight?: string;
+ mapWidth?: string;
+}
+
+const GoogleMaps = ({
+ markers = [],
+ draggable = false,
+ center = { lat: 10.063, lng: -69.323 },
+ onCoordinateChange,
+ onAddressChange,
+ mapHeight = '500px',
+ mapWidth = '100%',
+}: GoogleMapsProps) => {
+ const { isLoaded } = useJsApiLoader(GOOGLE_MAPS_OPTIONS);
+ const [activeMarker, setActiveMarker] = useState
(null);
+ const [selectedPosition, setSelectedPosition] = useState(center);
+
+ useEffect(() => {
+ const areCoordsDifferent =
+ center.lat !== selectedPosition.lat ||
+ center.lng !== selectedPosition.lng;
+
+ if (areCoordsDifferent) {
+ setSelectedPosition(center);
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [center]); // solo center, ignoramos selectedPosition
+
+ const handleMapClick = async (e: google.maps.MapMouseEvent) => {
+ if (!draggable || !e.latLng) return;
+ const lat = e.latLng.lat();
+ const lng = e.latLng.lng();
+ setSelectedPosition({ lat, lng });
+ onCoordinateChange?.(lat, lng);
+ reverseGeocode(lat, lng);
+ };
+
+ const reverseGeocode = (lat: number, lng: number) => {
+ if (!window.google?.maps?.Geocoder) return;
+
+ const geocoder = new window.google.maps.Geocoder();
+ const latLng = new window.google.maps.LatLng(lat, lng);
+
+ geocoder.geocode({ location: latLng }, (results, status) => {
+ if (status === 'OK' && results?.length) {
+ onAddressChange?.(results[0].formatted_address);
+ } else {
+ onAddressChange?.('Dirección no encontrada');
+ }
+ });
+ };
+
+ if (!isLoaded) return Cargando mapa...
;
+
+ return (
+
+ {/* Pines múltiples para sucursales */}
+ {markers.map((branch) => (
+ setActiveMarker(branch.id)}
+ onMouseOut={() => setActiveMarker(null)}
+ icon={{
+ url: 'http://maps.google.com/mapfiles/ms/icons/red-dot.png',
+ scaledSize: new google.maps.Size(40, 40),
+ }}
+ >
+ {activeMarker === branch.id && (
+ setActiveMarker(null)}
+ >
+
+
{branch.name}
+
{branch.address}
+
+
+ )}
+
+ ))}
+
+ {/* Pin único y draggable para selección de ubicación */}
+ {draggable && (
+ {
+ if (!e.latLng) return;
+ const lat = e.latLng.lat();
+ const lng = e.latLng.lng();
+ setSelectedPosition({ lat, lng });
+ onCoordinateChange?.(lat, lng);
+ reverseGeocode(lat, lng);
+ }}
+ />
+ )}
+
+ );
+};
+
+export default GoogleMaps;
diff --git a/src/components/GoogleMap/UserAddressPoput.tsx b/src/components/GoogleMap/UserAddressPoput.tsx
new file mode 100644
index 0000000..dd19842
--- /dev/null
+++ b/src/components/GoogleMap/UserAddressPoput.tsx
@@ -0,0 +1,124 @@
+'use client';
+
+import { useEffect, useState } from 'react';
+import Button from '@/components/Button';
+import GoogleMaps from '@/components/GoogleMap/GoogleMap';
+import { Colors, FontSizes } from '@/styles/styles';
+
+type LocationPopupProps = {
+ onAdd: (
+ location: { lat: number; lng: number },
+ address: string,
+ cityId: string,
+ ) => void;
+ onBack: () => void;
+ setLatitude: React.Dispatch>;
+ setLongitude: React.Dispatch>;
+ latitude: number;
+ longitude: number;
+};
+
+const LocationPopup: React.FC = ({
+ onAdd,
+ onBack,
+ setLatitude,
+ setLongitude,
+ latitude,
+ longitude,
+}) => {
+ const [address, setAddress] = useState('');
+ const [showGuide, setShowGuide] = useState(true);
+ const [selectedCityId, setSelectedCityId] = useState('');
+
+ useEffect(() => {
+ setSelectedCityId('7ec2b919-b961-458d-8275-4ee957010336');
+ }, []);
+
+ const handleCloseModal = (submit: boolean) => {
+ if (submit) {
+ onAdd({ lat: latitude, lng: longitude }, address, selectedCityId);
+ }
+ onBack();
+ };
+
+ return (
+ <>
+
+
+
+
+ Ubica la dirección exacta
+
+
+ {showGuide && (
+
+
+ Mueve el pin hasta tu ubicación exacta
+
+
+
+ )}
+
+
+ {
+ setLatitude(lat);
+ setLongitude(lng);
+ const geocoder = new window.google.maps.Geocoder();
+ const latLng = new window.google.maps.LatLng(lat, lng);
+ geocoder.geocode({ location: latLng }, (results, status) => {
+ if (
+ status === google.maps.GeocoderStatus.OK &&
+ results?.length
+ ) {
+ setAddress(results[0]?.formatted_address || '');
+ } else {
+ setAddress('Dirección no encontrada');
+ }
+ });
+ }}
+ />
+
+
+
+
+ Ubicación seleccionada: {address || 'Obteniendo dirección...'}
+
+
+
+
+
+
+
+
+
+ >
+ );
+};
+
+export default LocationPopup;
diff --git a/src/components/Image/UploadedImage.tsx b/src/components/Image/UploadedImage.tsx
index 4530fee..c069675 100644
--- a/src/components/Image/UploadedImage.tsx
+++ b/src/components/Image/UploadedImage.tsx
@@ -8,6 +8,7 @@ import { useAuth } from '@/context/AuthContext';
import { api } from '@/lib/sdkConfig';
import { toast } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
+import Loader from '../Loader';
type UploadedImage = {
id: string;
@@ -67,7 +68,7 @@ export default function UploadedImages({ productId }: UploadedImagesProps) {
fetchImages();
}, [productId, token]);
- if (loading) return Cargando imágenes...
;
+ if (loading) return ;
return (
diff --git a/src/components/Input/Input.tsx b/src/components/Input/Input.tsx
index 0a6bb85..fb41a08 100644
--- a/src/components/Input/Input.tsx
+++ b/src/components/Input/Input.tsx
@@ -2,7 +2,7 @@
import React, { useState } from 'react';
import { EyeIcon, EyeSlashIcon } from '@heroicons/react/24/outline';
-import theme from '@/styles/styles';
+import theme, { Colors } from '@/styles/styles';
type IconType = React.FC
>;
@@ -64,7 +64,7 @@ const Input: React.FC = ({
showPasswordToggle = false,
showPasswordToggleIconColor = 'text-gray-500',
borderSize = '2px',
- borderColor = '#000000',
+ borderColor = Colors.stroke,
readViewOnly = false,
isTextArea = false,
rows = 3,
diff --git a/src/components/Input/InventoryStock.tsx b/src/components/Input/InventoryStock.tsx
new file mode 100644
index 0000000..271ca57
--- /dev/null
+++ b/src/components/Input/InventoryStock.tsx
@@ -0,0 +1,62 @@
+import { useState } from 'react';
+import Input from './Input';
+import { api } from '@/lib/sdkConfig';
+import { useAuth } from '@/context/AuthContext';
+import { toast } from 'react-toastify';
+import { CheckCircleIcon } from '@heroicons/react/24/outline';
+import { Colors } from '@/styles/styles';
+
+export default function InventoryStock({
+ stock,
+ inventoryId,
+}: {
+ stock: number;
+ inventoryId: string;
+}) {
+ const { token } = useAuth();
+ const [value, setValue] = useState(stock);
+ const [updated, setUpdated] = useState(false);
+ const [loading, setLoading] = useState(false);
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+ setLoading(true);
+ try {
+ await api.inventory.update(inventoryId, { stockQuantity: value }, token!);
+ toast.success('Stock actualizado correctamente');
+ } catch (error) {
+ // Optionally handle error
+ console.error('Error updating stock:', error);
+ toast.error('Error al actualizar el stock');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleChange = (
+ e: React.ChangeEvent,
+ ) => {
+ const newValue = Number(e.target.value);
+ setValue(newValue);
+ setUpdated(newValue !== stock);
+ };
+
+ return (
+
+ );
+}
diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx
index 08a3eb1..b8bb6e8 100644
--- a/src/components/Navbar.tsx
+++ b/src/components/Navbar.tsx
@@ -1,12 +1,10 @@
'use client';
import React, { useEffect, useState } from 'react';
-import { BellIcon, QueueListIcon } from '@heroicons/react/24/outline';
+import { BellIcon } from '@heroicons/react/24/outline';
import Avatar from '@/components/Avatar';
-import SearchBar from '@/components/SearchBar';
-import { Colors } from '@/styles/styles';
import { useAuth } from '@/context/AuthContext';
-import { api } from '@/lib/sdkConfig';
+import Loader from './Loader';
interface AdminProfile {
name: string;
@@ -20,59 +18,41 @@ export default function AdminNavBar() {
const { token, user } = useAuth();
const [userData, setUserData] = useState(null);
- const handleSearch = (query: string) => {
- console.log('Buscando:', query);
- };
-
useEffect(() => {
if (!token || !user?.sub) {
setUserData(null);
return;
}
- (async () => {
- try {
- const profile = await api.user.getProfile(user.sub, token);
-
- // Adaptar el perfil a nuestro modelo esperado
- const adaptedProfile: AdminProfile = {
- name: `${profile.firstName} ${profile.lastName}`,
- email: profile.email,
- profile: {
- profilePicture: profile.profile.profilePicture,
- },
- };
-
- setUserData(adaptedProfile);
- } catch (err) {
- console.error('Error al obtener perfil del admin:', err);
- setUserData(null);
- }
+ (() => {
+ const adaptedProfile: AdminProfile = {
+ name: user.name,
+ email: user.email,
+ profile: {
+ profilePicture: user.profilePicture || '',
+ },
+ };
+ setUserData(adaptedProfile);
})();
}, [token, user]);
- if (!token || !userData) return null;
-
- return (
-