diff --git a/frontend/package.json b/frontend/package.json index 090a9a4..2dd19a4 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -17,6 +17,7 @@ "react-router-dom": "^7.15.1", "react-toastify": "^11.1.0", "tw-animate-css": "^1.4.0", + "xlsx": "^0.18.5", "zustand": "^5.0.13" }, "devDependencies": { @@ -25,6 +26,7 @@ "@types/node": "^24.12.3", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", + "@types/xlsx": "^0.0.36", "@vitejs/plugin-react": "^6.0.1", "eslint": "^10.3.0", "eslint-plugin-react-hooks": "^7.1.1", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 03a0a39..c8bc538 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -29,6 +29,9 @@ importers: tw-animate-css: specifier: ^1.4.0 version: 1.4.0 + xlsx: + specifier: ^0.18.5 + version: 0.18.5 zustand: specifier: ^5.0.13 version: 5.0.13(@types/react@19.2.15)(react@19.2.6) @@ -48,6 +51,9 @@ importers: '@types/react-dom': specifier: ^19.2.3 version: 19.2.3(@types/react@19.2.15) + '@types/xlsx': + specifier: ^0.0.36 + version: 0.0.36 '@vitejs/plugin-react': specifier: ^6.0.1 version: 6.0.2(vite@8.0.14(@types/node@24.12.4)(jiti@2.7.0)) @@ -533,6 +539,10 @@ packages: '@types/react@19.2.15': resolution: {integrity: sha512-eRwcGNHve+E8qtEQSSRl6urh+rFop4v8gm6O8rGv25CodbvFdLjA1vVQ1KkiFE0w0UPOnb8tDiFKL5lp0rtY5Q==} + '@types/xlsx@0.0.36': + resolution: {integrity: sha512-mvfrKiKKMErQzLMF8ElYEH21qxWCZtN59pHhWGmWCWFJStYdMWjkDSAy6mGowFxHXaXZWe5/TW7pBUiWclIVOw==} + deprecated: This is a stub types definition for xlsx (https://github.com/sheetjs/js-xlsx). xlsx provides its own type definitions, so you don't need @types/xlsx installed! + '@typescript-eslint/eslint-plugin@8.60.0': resolution: {integrity: sha512-QYb/sa74/s7OKMbACMjrYnGspj9Hs5YI5aaffSL65UfeBUzVzBJfVo3oWSpbzPurvm7yaCCo2Lk7lVj610HqKw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -615,6 +625,10 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + adler-32@1.3.1: + resolution: {integrity: sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==} + engines: {node: '>=0.8'} + agent-base@6.0.2: resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} engines: {node: '>= 6.0.0'} @@ -664,10 +678,18 @@ packages: caniuse-lite@1.0.30001793: resolution: {integrity: sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==} + cfb@1.2.2: + resolution: {integrity: sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==} + engines: {node: '>=0.8'} + clsx@2.1.1: resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} engines: {node: '>=6'} + codepage@1.15.0: + resolution: {integrity: sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==} + engines: {node: '>=0.8'} + combined-stream@1.0.8: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} engines: {node: '>= 0.8'} @@ -688,6 +710,11 @@ packages: typescript: optional: true + crc-32@1.2.2: + resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==} + engines: {node: '>=0.8'} + hasBin: true + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -862,6 +889,10 @@ packages: resolution: {integrity: sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==} engines: {node: '>= 6'} + frac@1.1.2: + resolution: {integrity: sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==} + engines: {node: '>=0.8'} + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -1244,6 +1275,10 @@ packages: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} + ssf@0.11.2: + resolution: {integrity: sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==} + engines: {node: '>=0.8'} + svg-parser@2.0.4: resolution: {integrity: sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==} @@ -1351,10 +1386,23 @@ packages: engines: {node: '>= 8'} hasBin: true + wmf@1.0.2: + resolution: {integrity: sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==} + engines: {node: '>=0.8'} + word-wrap@1.2.5: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} + word@0.3.0: + resolution: {integrity: sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==} + engines: {node: '>=0.8'} + + xlsx@0.18.5: + resolution: {integrity: sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==} + engines: {node: '>=0.8'} + hasBin: true + yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} @@ -1803,6 +1851,10 @@ snapshots: dependencies: csstype: 3.2.3 + '@types/xlsx@0.0.36': + dependencies: + xlsx: 0.18.5 + '@typescript-eslint/eslint-plugin@8.60.0(@typescript-eslint/parser@8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3)': dependencies: '@eslint-community/regexpp': 4.12.2 @@ -1905,6 +1957,8 @@ snapshots: acorn@8.16.0: {} + adler-32@1.3.1: {} + agent-base@6.0.2: dependencies: debug: 4.4.3 @@ -1959,8 +2013,15 @@ snapshots: caniuse-lite@1.0.30001793: {} + cfb@1.2.2: + dependencies: + adler-32: 1.3.1 + crc-32: 1.2.2 + clsx@2.1.1: {} + codepage@1.15.0: {} + combined-stream@1.0.8: dependencies: delayed-stream: 1.0.0 @@ -1978,6 +2039,8 @@ snapshots: optionalDependencies: typescript: 6.0.3 + crc-32@1.2.2: {} + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -2158,6 +2221,8 @@ snapshots: hasown: 2.0.4 mime-types: 2.1.35 + frac@1.1.2: {} + fsevents@2.3.3: optional: true @@ -2483,6 +2548,10 @@ snapshots: source-map-js@1.2.1: {} + ssf@0.11.2: + dependencies: + frac: 1.1.2 + svg-parser@2.0.4: {} tailwindcss@4.3.0: {} @@ -2558,8 +2627,22 @@ snapshots: dependencies: isexe: 2.0.0 + wmf@1.0.2: {} + word-wrap@1.2.5: {} + word@0.3.0: {} + + xlsx@0.18.5: + dependencies: + adler-32: 1.3.1 + cfb: 1.2.2 + codepage: 1.15.0 + crc-32: 1.2.2 + ssf: 0.11.2 + wmf: 1.0.2 + word: 0.3.0 + yallist@3.1.1: {} yocto-queue@0.1.0: {} diff --git a/frontend/src/components/reportes/BalanceTable.tsx b/frontend/src/components/reportes/BalanceTable.tsx new file mode 100644 index 0000000..ad9136e --- /dev/null +++ b/frontend/src/components/reportes/BalanceTable.tsx @@ -0,0 +1,89 @@ +import type { BalanceRecord } from './typesReportes'; +import { fmtLps, fmtDate } from './utilsReportes'; +import { PAGE_SIZE, ROW_H, MIN_W, PaginationFooter } from './tableUtils'; + +const COLS = '100px 120px 130px 1.2fr 2fr 130px'; +const HEADERS = ['Código', 'Fecha', 'Movimiento', 'Categoría', 'Descripción', 'Monto']; + +interface Props { + records: BalanceRecord[]; + page: number; + onPage: (p: number) => void; +} + +export function BalanceTable({ records, page, onPage }: Props) { + const totalPages = Math.max(1, Math.ceil(records.length / PAGE_SIZE)); + const safe = Math.min(page - 1, totalPages - 1); + const pageItems = records.slice(safe * PAGE_SIZE, (safe + 1) * PAGE_SIZE); + const slots = Array.from({ length: PAGE_SIZE }).map((_, i) => pageItems[i] ?? null); + + return ( +
+
+ + {/* Encabezados */} +
+ {HEADERS.map((h, i) => ( +
+ + {h} + +
+ ))} +
+ + {/* Filas */} +
+ {slots.map((r, i) => { + const isIngreso = r?.tipo === 'Ingreso'; + return ( +
+ {r ? ( + <> + {/* Código */} +
{r.codigo}
+ + {/* Fecha */} +
{fmtDate(r.fecha)}
+ + {/* Movimiento — solo color, sin ícono */} +
+ + {r.tipo} + +
+ + {/* Categoría */} +
{r.categoria}
+ + {/* Descripción */} +
{r.descripcion}
+ + {/* Monto */} +
+ {isIngreso ? '+' : '-'} {fmtLps(r.monto)} +
+ + ) : ( +
+ )} +
+ ); + })} +
+ + +
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/components/reportes/MorososTable.tsx b/frontend/src/components/reportes/MorososTable.tsx new file mode 100644 index 0000000..8cbf273 --- /dev/null +++ b/frontend/src/components/reportes/MorososTable.tsx @@ -0,0 +1,86 @@ +import type { MorosoRecord } from './typesReportes'; +import { fmtLps } from './utilsReportes'; +import { PAGE_SIZE, ROW_H, MIN_W, PaginationFooter } from './tableUtils'; + +const COLS = '1.4fr 100px 1fr 110px 1.4fr 120px'; +const HEADERS = ['Residente', 'DNI', 'Ubicación', 'Atraso', 'Detalle de Deuda', 'Monto Total']; + +interface Props { + records: MorosoRecord[]; + page: number; + onPage: (p: number) => void; +} + +export function MorososTable({ records, page, onPage }: Props) { + const totalPages = Math.max(1, Math.ceil(records.length / PAGE_SIZE)); + const safe = Math.min(page - 1, totalPages - 1); + const pageItems = records.slice(safe * PAGE_SIZE, (safe + 1) * PAGE_SIZE); + const slots = Array.from({ length: PAGE_SIZE }).map((_, i) => pageItems[i] ?? null); + + return ( +
+
+ + {/* Encabezados */} +
+ {HEADERS.map((h, i) => ( +
+ + {h} + +
+ ))} +
+ + {/* Filas */} +
+ {slots.map((r, i) => ( +
+ {r ? ( + <> + {/* Residente */} +
{r.residente}
+ + {/* DNI */} +
{r.dni}
+ + {/* Ubicación */} +
+ Blq. {r.ubicacion.bloque}, Lte. {r.ubicacion.lote} +
+ + {/* Atraso */} +
+ = 3 ? '#FEE2E2' : '#FEF3C7', + color: r.mesesAtraso >= 3 ? '#c0392b' : '#b7791f', + }} + > + {r.mesesAtraso} {r.mesesAtraso === 1 ? 'mes' : 'meses'} + +
+ + {/* Detalle */} +
{r.detalleDeuda}
+ + {/* Monto */} +
{fmtLps(r.montoTotal)}
+ + ) : ( +
+ )} +
+ ))} +
+ + +
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/components/reportes/ReportesFilters.tsx b/frontend/src/components/reportes/ReportesFilters.tsx new file mode 100644 index 0000000..29ebc3f --- /dev/null +++ b/frontend/src/components/reportes/ReportesFilters.tsx @@ -0,0 +1,139 @@ +import { useRef } from 'react'; +import { Calendar, X } from 'lucide-react'; +import type { ReportesFilterValues } from './useReportes'; +import { DEFAULT_REPORTES_FILTERS } from './useReportes'; +import type { ReporteTab, TipoBalance } from './typesReportes'; +import { fmtDate } from './utilsReportes'; + +interface Props { + filters: ReportesFilterValues; + onChange: (f: ReportesFilterValues) => void; + activeTab: ReporteTab; +} + +export function ReportesFilters({ filters, onChange, activeTab }: Props) { + const set = (key: keyof ReportesFilterValues, value: ReportesFilterValues[typeof key]) => + onChange({ ...filters, [key]: value }); + + const toggleTipo = (tipo: TipoBalance) => { + const next = filters.tipos.includes(tipo) + ? filters.tipos.filter(t => t !== tipo) + : [...filters.tipos, tipo]; + set('tipos', next); + }; + + const hasAny = filters.search || filters.dateFrom || filters.dateTo || filters.tipos.length > 0; + + return ( +
+ +
+ Filtros + {hasAny && ( + + )} +
+ + +