Bladeren bron

implement db to save tax info

Matthew Trejo 1 maand geleden
bovenliggende
commit
9b598ea6d7

+ 411 - 4
package-lock.json

@@ -8,6 +8,7 @@
       "name": "sumire",
       "version": "0.1.0",
       "dependencies": {
+        "@prisma/client": "^6.18.0",
         "@radix-ui/react-checkbox": "^1.3.3",
         "@radix-ui/react-dialog": "^1.1.15",
         "@radix-ui/react-dropdown-menu": "^2.1.16",
@@ -25,7 +26,8 @@
         "react": "19.2.0",
         "react-dom": "19.2.0",
         "sonner": "^2.0.7",
-        "tailwind-merge": "^3.3.1"
+        "tailwind-merge": "^3.3.1",
+        "zod": "^4.1.12"
       },
       "devDependencies": {
         "@tailwindcss/postcss": "^4",
@@ -34,6 +36,7 @@
         "@types/react-dom": "^19",
         "eslint": "^9",
         "eslint-config-next": "16.0.1",
+        "prisma": "^6.18.0",
         "tailwindcss": "^4",
         "tw-animate-css": "^1.4.0",
         "typescript": "^5"
@@ -1268,6 +1271,91 @@
         "node": ">=12.4.0"
       }
     },
+    "node_modules/@prisma/client": {
+      "version": "6.18.0",
+      "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.18.0.tgz",
+      "integrity": "sha512-jnL2I9gDnPnw4A+4h5SuNn8Gc+1mL1Z79U/3I9eE2gbxJG1oSA+62ByPW4xkeDgwE0fqMzzpAZ7IHxYnLZ4iQA==",
+      "hasInstallScript": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=18.18"
+      },
+      "peerDependencies": {
+        "prisma": "*",
+        "typescript": ">=5.1.0"
+      },
+      "peerDependenciesMeta": {
+        "prisma": {
+          "optional": true
+        },
+        "typescript": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@prisma/config": {
+      "version": "6.18.0",
+      "resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.18.0.tgz",
+      "integrity": "sha512-rgFzspCpwsE+q3OF/xkp0fI2SJ3PfNe9LLMmuSVbAZ4nN66WfBiKqJKo/hLz3ysxiPQZf8h1SMf2ilqPMeWATQ==",
+      "devOptional": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "c12": "3.1.0",
+        "deepmerge-ts": "7.1.5",
+        "effect": "3.18.4",
+        "empathic": "2.0.0"
+      }
+    },
+    "node_modules/@prisma/debug": {
+      "version": "6.18.0",
+      "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.18.0.tgz",
+      "integrity": "sha512-PMVPMmxPj0ps1VY75DIrT430MoOyQx9hmm174k6cmLZpcI95rAPXOQ+pp8ANQkJtNyLVDxnxVJ0QLbrm/ViBcg==",
+      "devOptional": true,
+      "license": "Apache-2.0"
+    },
+    "node_modules/@prisma/engines": {
+      "version": "6.18.0",
+      "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.18.0.tgz",
+      "integrity": "sha512-i5RzjGF/ex6AFgqEe2o1IW8iIxJGYVQJVRau13kHPYEL1Ck8Zvwuzamqed/1iIljs5C7L+Opiz5TzSsUebkriA==",
+      "devOptional": true,
+      "hasInstallScript": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@prisma/debug": "6.18.0",
+        "@prisma/engines-version": "6.18.0-8.34b5a692b7bd79939a9a2c3ef97d816e749cda2f",
+        "@prisma/fetch-engine": "6.18.0",
+        "@prisma/get-platform": "6.18.0"
+      }
+    },
+    "node_modules/@prisma/engines-version": {
+      "version": "6.18.0-8.34b5a692b7bd79939a9a2c3ef97d816e749cda2f",
+      "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-6.18.0-8.34b5a692b7bd79939a9a2c3ef97d816e749cda2f.tgz",
+      "integrity": "sha512-T7Af4QsJQnSgWN1zBbX+Cha5t4qjHRxoeoWpK4JugJzG/ipmmDMY5S+O0N1ET6sCBNVkf6lz+Y+ZNO9+wFU8pQ==",
+      "devOptional": true,
+      "license": "Apache-2.0"
+    },
+    "node_modules/@prisma/fetch-engine": {
+      "version": "6.18.0",
+      "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.18.0.tgz",
+      "integrity": "sha512-TdaBvTtBwP3IoqVYoGIYpD4mWlk0pJpjTJjir/xLeNWlwog7Sl3bD2J0jJ8+5+q/6RBg+acb9drsv5W6lqae7A==",
+      "devOptional": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@prisma/debug": "6.18.0",
+        "@prisma/engines-version": "6.18.0-8.34b5a692b7bd79939a9a2c3ef97d816e749cda2f",
+        "@prisma/get-platform": "6.18.0"
+      }
+    },
+    "node_modules/@prisma/get-platform": {
+      "version": "6.18.0",
+      "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.18.0.tgz",
+      "integrity": "sha512-uXNJCJGhxTCXo2B25Ta91Rk1/Nmlqg9p7G9GKh8TPhxvAyXCvMNQoogj4JLEUy+3ku8g59cpyQIKFhqY2xO2bg==",
+      "devOptional": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@prisma/debug": "6.18.0"
+      }
+    },
     "node_modules/@radix-ui/number": {
       "version": "1.1.1",
       "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz",
@@ -2070,6 +2158,13 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/@standard-schema/spec": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz",
+      "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==",
+      "devOptional": true,
+      "license": "MIT"
+    },
     "node_modules/@swc/helpers": {
       "version": "0.5.15",
       "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
@@ -3355,6 +3450,35 @@
         "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
       }
     },
+    "node_modules/c12": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/c12/-/c12-3.1.0.tgz",
+      "integrity": "sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw==",
+      "devOptional": true,
+      "license": "MIT",
+      "dependencies": {
+        "chokidar": "^4.0.3",
+        "confbox": "^0.2.2",
+        "defu": "^6.1.4",
+        "dotenv": "^16.6.1",
+        "exsolve": "^1.0.7",
+        "giget": "^2.0.0",
+        "jiti": "^2.4.2",
+        "ohash": "^2.0.11",
+        "pathe": "^2.0.3",
+        "perfect-debounce": "^1.0.0",
+        "pkg-types": "^2.2.0",
+        "rc9": "^2.1.2"
+      },
+      "peerDependencies": {
+        "magicast": "^0.3.5"
+      },
+      "peerDependenciesMeta": {
+        "magicast": {
+          "optional": true
+        }
+      }
+    },
     "node_modules/call-bind": {
       "version": "1.0.8",
       "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz",
@@ -3452,6 +3576,32 @@
         "url": "https://github.com/chalk/chalk?sponsor=1"
       }
     },
+    "node_modules/chokidar": {
+      "version": "4.0.3",
+      "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
+      "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
+      "devOptional": true,
+      "license": "MIT",
+      "dependencies": {
+        "readdirp": "^4.0.1"
+      },
+      "engines": {
+        "node": ">= 14.16.0"
+      },
+      "funding": {
+        "url": "https://paulmillr.com/funding/"
+      }
+    },
+    "node_modules/citty": {
+      "version": "0.1.6",
+      "resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz",
+      "integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==",
+      "devOptional": true,
+      "license": "MIT",
+      "dependencies": {
+        "consola": "^3.2.3"
+      }
+    },
     "node_modules/class-variance-authority": {
       "version": "0.7.1",
       "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz",
@@ -3506,6 +3656,23 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/confbox": {
+      "version": "0.2.2",
+      "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.2.tgz",
+      "integrity": "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==",
+      "devOptional": true,
+      "license": "MIT"
+    },
+    "node_modules/consola": {
+      "version": "3.4.2",
+      "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz",
+      "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==",
+      "devOptional": true,
+      "license": "MIT",
+      "engines": {
+        "node": "^14.18.0 || >=16.10.0"
+      }
+    },
     "node_modules/convert-source-map": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
@@ -3621,6 +3788,16 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/deepmerge-ts": {
+      "version": "7.1.5",
+      "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz",
+      "integrity": "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==",
+      "devOptional": true,
+      "license": "BSD-3-Clause",
+      "engines": {
+        "node": ">=16.0.0"
+      }
+    },
     "node_modules/define-data-property": {
       "version": "1.1.4",
       "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
@@ -3657,6 +3834,20 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/defu": {
+      "version": "6.1.4",
+      "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz",
+      "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==",
+      "devOptional": true,
+      "license": "MIT"
+    },
+    "node_modules/destr": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz",
+      "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==",
+      "devOptional": true,
+      "license": "MIT"
+    },
     "node_modules/detect-libc": {
       "version": "2.1.2",
       "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
@@ -3686,6 +3877,19 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/dotenv": {
+      "version": "16.6.1",
+      "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
+      "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
+      "devOptional": true,
+      "license": "BSD-2-Clause",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://dotenvx.com"
+      }
+    },
     "node_modules/dunder-proto": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
@@ -3701,6 +3905,17 @@
         "node": ">= 0.4"
       }
     },
+    "node_modules/effect": {
+      "version": "3.18.4",
+      "resolved": "https://registry.npmjs.org/effect/-/effect-3.18.4.tgz",
+      "integrity": "sha512-b1LXQJLe9D11wfnOKAk3PKxuqYshQ0Heez+y5pnkd3jLj1yx9QhM72zZ9uUrOQyNvrs2GZZd/3maL0ZV18YuDA==",
+      "devOptional": true,
+      "license": "MIT",
+      "dependencies": {
+        "@standard-schema/spec": "^1.0.0",
+        "fast-check": "^3.23.1"
+      }
+    },
     "node_modules/electron-to-chromium": {
       "version": "1.5.244",
       "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.244.tgz",
@@ -3715,6 +3930,16 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/empathic": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.0.tgz",
+      "integrity": "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==",
+      "devOptional": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=14"
+      }
+    },
     "node_modules/enhanced-resolve": {
       "version": "5.18.3",
       "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz",
@@ -4353,6 +4578,36 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/exsolve": {
+      "version": "1.0.7",
+      "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.7.tgz",
+      "integrity": "sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==",
+      "devOptional": true,
+      "license": "MIT"
+    },
+    "node_modules/fast-check": {
+      "version": "3.23.2",
+      "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.23.2.tgz",
+      "integrity": "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==",
+      "devOptional": true,
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://github.com/sponsors/dubzzz"
+        },
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/fast-check"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "pure-rand": "^6.1.0"
+      },
+      "engines": {
+        "node": ">=8.0.0"
+      }
+    },
     "node_modules/fast-deep-equal": {
       "version": "3.1.3",
       "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@@ -4634,6 +4889,24 @@
         "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
       }
     },
+    "node_modules/giget": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/giget/-/giget-2.0.0.tgz",
+      "integrity": "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==",
+      "devOptional": true,
+      "license": "MIT",
+      "dependencies": {
+        "citty": "^0.1.6",
+        "consola": "^3.4.0",
+        "defu": "^6.1.4",
+        "node-fetch-native": "^1.6.6",
+        "nypm": "^0.6.0",
+        "pathe": "^2.0.3"
+      },
+      "bin": {
+        "giget": "dist/cli.mjs"
+      }
+    },
     "node_modules/glob-parent": {
       "version": "6.0.2",
       "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
@@ -5318,7 +5591,7 @@
       "version": "2.6.1",
       "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz",
       "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==",
-      "dev": true,
+      "devOptional": true,
       "license": "MIT",
       "bin": {
         "jiti": "lib/jiti-cli.mjs"
@@ -5972,6 +6245,13 @@
         "node": "^10 || ^12 || >=14"
       }
     },
+    "node_modules/node-fetch-native": {
+      "version": "1.6.7",
+      "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz",
+      "integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==",
+      "devOptional": true,
+      "license": "MIT"
+    },
     "node_modules/node-releases": {
       "version": "2.0.27",
       "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz",
@@ -5979,6 +6259,26 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/nypm": {
+      "version": "0.6.2",
+      "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.2.tgz",
+      "integrity": "sha512-7eM+hpOtrKrBDCh7Ypu2lJ9Z7PNZBdi/8AT3AX8xoCj43BBVHD0hPSTEvMtkMpfs8FCqBGhxB+uToIQimA111g==",
+      "devOptional": true,
+      "license": "MIT",
+      "dependencies": {
+        "citty": "^0.1.6",
+        "consola": "^3.4.2",
+        "pathe": "^2.0.3",
+        "pkg-types": "^2.3.0",
+        "tinyexec": "^1.0.1"
+      },
+      "bin": {
+        "nypm": "dist/cli.mjs"
+      },
+      "engines": {
+        "node": "^14.16.0 || >=16.10.0"
+      }
+    },
     "node_modules/object-assign": {
       "version": "4.1.1",
       "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
@@ -6102,6 +6402,13 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/ohash": {
+      "version": "2.0.11",
+      "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz",
+      "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==",
+      "devOptional": true,
+      "license": "MIT"
+    },
     "node_modules/optionator": {
       "version": "0.9.4",
       "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
@@ -6210,6 +6517,20 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/pathe": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
+      "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
+      "devOptional": true,
+      "license": "MIT"
+    },
+    "node_modules/perfect-debounce": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz",
+      "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==",
+      "devOptional": true,
+      "license": "MIT"
+    },
     "node_modules/picocolors": {
       "version": "1.1.1",
       "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
@@ -6229,6 +6550,18 @@
         "url": "https://github.com/sponsors/jonschlinkert"
       }
     },
+    "node_modules/pkg-types": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz",
+      "integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==",
+      "devOptional": true,
+      "license": "MIT",
+      "dependencies": {
+        "confbox": "^0.2.2",
+        "exsolve": "^1.0.7",
+        "pathe": "^2.0.3"
+      }
+    },
     "node_modules/possible-typed-array-names": {
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
@@ -6278,6 +6611,32 @@
         "node": ">= 0.8.0"
       }
     },
+    "node_modules/prisma": {
+      "version": "6.18.0",
+      "resolved": "https://registry.npmjs.org/prisma/-/prisma-6.18.0.tgz",
+      "integrity": "sha512-bXWy3vTk8mnRmT+SLyZBQoC2vtV9Z8u7OHvEu+aULYxwiop/CPiFZ+F56KsNRNf35jw+8wcu8pmLsjxpBxAO9g==",
+      "devOptional": true,
+      "hasInstallScript": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@prisma/config": "6.18.0",
+        "@prisma/engines": "6.18.0"
+      },
+      "bin": {
+        "prisma": "build/index.js"
+      },
+      "engines": {
+        "node": ">=18.18"
+      },
+      "peerDependencies": {
+        "typescript": ">=5.1.0"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        }
+      }
+    },
     "node_modules/prop-types": {
       "version": "15.8.1",
       "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
@@ -6300,6 +6659,23 @@
         "node": ">=6"
       }
     },
+    "node_modules/pure-rand": {
+      "version": "6.1.0",
+      "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz",
+      "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==",
+      "devOptional": true,
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://github.com/sponsors/dubzzz"
+        },
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/fast-check"
+        }
+      ],
+      "license": "MIT"
+    },
     "node_modules/queue-microtask": {
       "version": "1.2.3",
       "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
@@ -6321,6 +6697,17 @@
       ],
       "license": "MIT"
     },
+    "node_modules/rc9": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/rc9/-/rc9-2.1.2.tgz",
+      "integrity": "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==",
+      "devOptional": true,
+      "license": "MIT",
+      "dependencies": {
+        "defu": "^6.1.4",
+        "destr": "^2.0.3"
+      }
+    },
     "node_modules/react": {
       "version": "19.2.0",
       "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz",
@@ -6418,6 +6805,20 @@
         }
       }
     },
+    "node_modules/readdirp": {
+      "version": "4.1.2",
+      "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
+      "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
+      "devOptional": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 14.18.0"
+      },
+      "funding": {
+        "type": "individual",
+        "url": "https://paulmillr.com/funding/"
+      }
+    },
     "node_modules/reflect.getprototypeof": {
       "version": "1.0.10",
       "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
@@ -7069,6 +7470,13 @@
         "url": "https://opencollective.com/webpack"
       }
     },
+    "node_modules/tinyexec": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.1.tgz",
+      "integrity": "sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw==",
+      "devOptional": true,
+      "license": "MIT"
+    },
     "node_modules/tinyglobby": {
       "version": "0.2.15",
       "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
@@ -7280,7 +7688,7 @@
       "version": "5.9.3",
       "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
       "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
-      "dev": true,
+      "devOptional": true,
       "license": "Apache-2.0",
       "bin": {
         "tsc": "bin/tsc",
@@ -7598,7 +8006,6 @@
       "version": "4.1.12",
       "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.12.tgz",
       "integrity": "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==",
-      "dev": true,
       "license": "MIT",
       "funding": {
         "url": "https://github.com/sponsors/colinhacks"

+ 4 - 1
package.json

@@ -9,6 +9,7 @@
     "lint": "eslint"
   },
   "dependencies": {
+    "@prisma/client": "^6.18.0",
     "@radix-ui/react-checkbox": "^1.3.3",
     "@radix-ui/react-dialog": "^1.1.15",
     "@radix-ui/react-dropdown-menu": "^2.1.16",
@@ -26,7 +27,8 @@
     "react": "19.2.0",
     "react-dom": "19.2.0",
     "sonner": "^2.0.7",
-    "tailwind-merge": "^3.3.1"
+    "tailwind-merge": "^3.3.1",
+    "zod": "^4.1.12"
   },
   "devDependencies": {
     "@tailwindcss/postcss": "^4",
@@ -35,6 +37,7 @@
     "@types/react-dom": "^19",
     "eslint": "^9",
     "eslint-config-next": "16.0.1",
+    "prisma": "^6.18.0",
     "tailwindcss": "^4",
     "tw-animate-css": "^1.4.0",
     "typescript": "^5"

BIN
prisma/dev.db


+ 29 - 0
prisma/schema.prisma

@@ -0,0 +1,29 @@
+// This is your Prisma schema file,
+// learn more about it in the docs: https://pris.ly/d/prisma-schema
+
+generator client {
+  provider = "prisma-client-js"
+}
+
+datasource db {
+  provider = "sqlite"
+  url      = env("DATABASE_URL")
+}
+
+model ConfiguracionTributaria {
+  id                String @id @default(cuid())
+  ambiente          String // "1" para pruebas, "2" para producción
+  tipoEmision       String // "1" para normal
+  razonSocial       String
+  nombreComercial   String
+  ruc               String @unique
+  dirMatriz         String
+  estab             String // establecimiento
+  ptoEmi            String // punto de emisión
+  secuencial        String // secuencial actual
+  activo            Boolean @default(true)
+  createdAt         DateTime @default(now())
+  updatedAt         DateTime @updatedAt
+
+  @@map("configuraciones_tributarias")
+}

+ 47 - 0
src/app/api/configuraciones-tributarias/[id]/incrementar-secuencial/route.ts

@@ -0,0 +1,47 @@
+import { NextRequest, NextResponse } from 'next/server'
+import { prisma } from '@/lib/prisma'
+
+// POST - Incrementar el secuencial de una configuración tributaria
+export async function POST(
+  request: NextRequest,
+  { params }: { params: Promise<{ id: string }> }
+) {
+  try {
+    const { id } = await params
+    
+    // Obtener la configuración actual
+    const configuracion = await prisma.configuracionTributaria.findUnique({
+      where: { id }
+    })
+
+    if (!configuracion) {
+      return NextResponse.json(
+        { error: 'Configuración tributaria no encontrada' },
+        { status: 404 }
+      )
+    }
+
+    // Convertir el secuencial actual a número, incrementarlo y volver a convertir a string con padding
+    const secuencialActual = parseInt(configuracion.secuencial, 10)
+    const nuevoSecuencial = (secuencialActual + 1).toString().padStart(9, '0')
+
+    // Actualizar el secuencial
+    const configuracionActualizada = await prisma.configuracionTributaria.update({
+      where: { id },
+      data: { secuencial: nuevoSecuencial }
+    })
+
+    return NextResponse.json({
+      mensaje: 'Secuencial incrementado correctamente',
+      secuencialAnterior: configuracion.secuencial,
+      nuevoSecuencial: nuevoSecuencial,
+      configuracion: configuracionActualizada
+    })
+  } catch (error) {
+    console.error('Error al incrementar secuencial:', error)
+    return NextResponse.json(
+      { error: 'Error al incrementar secuencial' },
+      { status: 500 }
+    )
+  }
+}

+ 73 - 0
src/app/api/configuraciones-tributarias/[id]/reiniciar-secuencial/route.ts

@@ -0,0 +1,73 @@
+import { NextRequest, NextResponse } from 'next/server'
+import { prisma } from '@/lib/prisma'
+import { z } from 'zod'
+
+const reiniciarSecuencialSchema = z.object({
+  nuevoSecuencial: z.string()
+    .min(1, 'El secuencial es requerido')
+    .max(9, 'El secuencial debe tener máximo 9 dígitos')
+    .regex(/^\d+$/, 'El secuencial debe contener solo números')
+})
+
+// POST - Reiniciar el secuencial de una configuración tributaria
+export async function POST(
+  request: NextRequest,
+  { params }: { params: Promise<{ id: string }> }
+) {
+  try {
+    const { id } = await params
+    const body = await request.json()
+    const { nuevoSecuencial } = reiniciarSecuencialSchema.parse(body)
+
+    // Obtener la configuración actual
+    const configuracion = await prisma.configuracionTributaria.findUnique({
+      where: { id }
+    })
+
+    if (!configuracion) {
+      return NextResponse.json(
+        { error: 'Configuración tributaria no encontrada' },
+        { status: 404 }
+      )
+    }
+
+    // Validar que el nuevo secuencial sea un número positivo
+    const secuencialNumero = parseInt(nuevoSecuencial, 10)
+    if (isNaN(secuencialNumero) || secuencialNumero < 0) {
+      return NextResponse.json(
+        { error: 'El secuencial debe ser un número positivo' },
+        { status: 400 }
+      )
+    }
+
+    // Formatear el secuencial con padding de 9 dígitos
+    const secuencialFormateado = secuencialNumero.toString().padStart(9, '0')
+
+    // Actualizar el secuencial
+    const configuracionActualizada = await prisma.configuracionTributaria.update({
+      where: { id },
+      data: { secuencial: secuencialFormateado }
+    })
+
+    return NextResponse.json({
+      mensaje: 'Secuencial reiniciado correctamente',
+      secuencialAnterior: configuracion.secuencial,
+      nuevoSecuencial: secuencialFormateado,
+      configuracion: configuracionActualizada
+    })
+  } catch (error) {
+    console.error('Error al reiniciar secuencial:', error)
+    
+    if (error instanceof z.ZodError) {
+      return NextResponse.json(
+        { error: 'Datos inválidos', details: error.issues },
+        { status: 400 }
+      )
+    }
+
+    return NextResponse.json(
+      { error: 'Error al reiniciar secuencial' },
+      { status: 500 }
+    )
+  }
+}

+ 118 - 0
src/app/api/configuraciones-tributarias/[id]/route.ts

@@ -0,0 +1,118 @@
+import { NextRequest, NextResponse } from 'next/server'
+import { prisma } from '@/lib/prisma'
+import { z } from 'zod'
+
+const updateConfiguracionSchema = z.object({
+  ambiente: z.string().length(1).optional(),
+  tipoEmision: z.string().length(1).optional(),
+  razonSocial: z.string().min(1).optional(),
+  nombreComercial: z.string().min(1).optional(),
+  ruc: z.string().length(13).optional(),
+  dirMatriz: z.string().min(1).optional(),
+  estab: z.string().length(3).optional(),
+  ptoEmi: z.string().length(3).optional(),
+  secuencial: z.string().length(9).optional(),
+  activo: z.boolean().optional(),
+})
+
+// GET - Obtener una configuración tributaria por ID
+export async function GET(
+  request: NextRequest,
+  { params }: { params: Promise<{ id: string }> }
+) {
+  try {
+    const { id } = await params
+    
+    const configuracion = await prisma.configuracionTributaria.findUnique({
+      where: { id }
+    })
+
+    if (!configuracion) {
+      return NextResponse.json(
+        { error: 'Configuración tributaria no encontrada' },
+        { status: 404 }
+      )
+    }
+
+    return NextResponse.json(configuracion)
+  } catch (error) {
+    console.error('Error al obtener configuración tributaria:', error)
+    return NextResponse.json(
+      { error: 'Error al obtener configuración tributaria' },
+      { status: 500 }
+    )
+  }
+}
+
+// PUT - Actualizar una configuración tributaria
+export async function PUT(
+  request: NextRequest,
+  { params }: { params: Promise<{ id: string }> }
+) {
+  try {
+    const { id } = await params
+    const body = await request.json()
+    const validatedData = updateConfiguracionSchema.parse(body)
+
+    // Si se está actualizando el RUC, verificar que no exista otra configuración con ese RUC
+    if (validatedData.ruc) {
+      const existingConfig = await prisma.configuracionTributaria.findFirst({
+        where: {
+          ruc: validatedData.ruc,
+          id: { not: id }
+        }
+      })
+
+      if (existingConfig) {
+        return NextResponse.json(
+          { error: 'Ya existe otra configuración con este RUC' },
+          { status: 400 }
+        )
+      }
+    }
+
+    const configuracion = await prisma.configuracionTributaria.update({
+      where: { id },
+      data: validatedData
+    })
+
+    return NextResponse.json(configuracion)
+  } catch (error) {
+    console.error('Error al actualizar configuración tributaria:', error)
+    
+    if (error instanceof z.ZodError) {
+      return NextResponse.json(
+        { error: 'Datos inválidos', details: error.issues },
+        { status: 400 }
+      )
+    }
+
+    return NextResponse.json(
+      { error: 'Error al actualizar configuración tributaria' },
+      { status: 500 }
+    )
+  }
+}
+
+// DELETE - Eliminar (desactivar) una configuración tributaria
+export async function DELETE(
+  request: NextRequest,
+  { params }: { params: Promise<{ id: string }> }
+) {
+  try {
+    const { id } = await params
+    
+    const configuracion = await prisma.configuracionTributaria.update({
+      where: { id },
+      data: { activo: false }
+    })
+
+    return NextResponse.json({ message: 'Configuración tributaria eliminada correctamente' })
+  } catch (error) {
+    console.error('Error al eliminar configuración tributaria:', error)
+    return NextResponse.json(
+      { error: 'Error al eliminar configuración tributaria' },
+      { status: 500 }
+    )
+  }
+}

+ 77 - 0
src/app/api/configuraciones-tributarias/route.ts

@@ -0,0 +1,77 @@
+import { NextRequest, NextResponse } from 'next/server'
+import { prisma } from '@/lib/prisma'
+import { z } from 'zod'
+
+const createConfiguracionSchema = z.object({
+  ambiente: z.string().length(1),
+  tipoEmision: z.string().length(1),
+  razonSocial: z.string().min(1),
+  nombreComercial: z.string().min(1),
+  ruc: z.string().length(13),
+  dirMatriz: z.string().min(1),
+  estab: z.string().length(3),
+  ptoEmi: z.string().length(3),
+  secuencial: z.string().length(9),
+})
+
+// GET - Obtener todas las configuraciones tributarias
+export async function GET() {
+  try {
+    const configuraciones = await prisma.configuracionTributaria.findMany({
+      where: {
+        activo: true
+      },
+      orderBy: {
+        createdAt: 'desc'
+      }
+    })
+
+    return NextResponse.json(configuraciones)
+  } catch (error) {
+    console.error('Error al obtener configuraciones tributarias:', error)
+    return NextResponse.json(
+      { error: 'Error al obtener configuraciones tributarias' },
+      { status: 500 }
+    )
+  }
+}
+
+// POST - Crear una nueva configuración tributaria
+export async function POST(request: NextRequest) {
+  try {
+    const body = await request.json()
+    const validatedData = createConfiguracionSchema.parse(body)
+
+    // Verificar si ya existe una configuración con el mismo RUC
+    const existingConfig = await prisma.configuracionTributaria.findUnique({
+      where: { ruc: validatedData.ruc }
+    })
+
+    if (existingConfig) {
+      return NextResponse.json(
+        { error: 'Ya existe una configuración con este RUC' },
+        { status: 400 }
+      )
+    }
+
+    const configuracion = await prisma.configuracionTributaria.create({
+      data: validatedData
+    })
+
+    return NextResponse.json(configuracion, { status: 201 })
+  } catch (error) {
+    console.error('Error al crear configuración tributaria:', error)
+    
+    if (error instanceof z.ZodError) {
+      return NextResponse.json(
+        { error: 'Datos inválidos', details: error.issues },
+        { status: 400 }
+      )
+    }
+
+    return NextResponse.json(
+      { error: 'Error al crear configuración tributaria' },
+      { status: 500 }
+    )
+  }
+}

+ 11 - 0
src/app/configuracion/page.tsx

@@ -0,0 +1,11 @@
+"use client"
+
+import { ConfiguracionTributariaManager } from '@/components/configuracion/ConfiguracionTributariaManager'
+
+export default function ConfiguracionPage() {
+  return (
+    <div className="container mx-auto py-8">
+      <ConfiguracionTributariaManager />
+    </div>
+  )
+}

+ 1 - 1
src/components/app-sidebar.tsx

@@ -38,7 +38,7 @@ const items = [
   },
   {
     title: "Configuración",
-    url: "#",
+    url: "/configuracion",
     icon: Settings,
   },
 ]

+ 484 - 0
src/components/configuracion/ConfiguracionTributariaManager.tsx

@@ -0,0 +1,484 @@
+"use client"
+
+import { useState } from 'react'
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
+import { Button } from "@/components/ui/button"
+import { Input } from "@/components/ui/input"
+import { Label } from "@/components/ui/label"
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
+import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"
+import { Badge } from "@/components/ui/badge"
+import { Switch } from "@/components/ui/switch"
+import { useConfiguracionesTributarias } from '@/hooks/useConfiguracionesTributarias'
+import { Plus, Edit, Trash2, Save, RotateCcw } from 'lucide-react'
+
+export function ConfiguracionTributariaManager() {
+  const {
+    configuraciones,
+    loading,
+    error,
+    createConfiguracion,
+    updateConfiguracion,
+    deleteConfiguracion,
+    incrementarSecuencial,
+    reiniciarSecuencial,
+  } = useConfiguracionesTributarias()
+
+  const [editingConfig, setEditingConfig] = useState<string | null>(null)
+  const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false)
+  const [isResetDialogOpen, setIsResetDialogOpen] = useState(false)
+  const [resetConfigId, setResetConfigId] = useState<string | null>(null)
+  const [resetSecuencial, setResetSecuencial] = useState('')
+  const [formData, setFormData] = useState({
+    ambiente: '1',
+    tipoEmision: '1',
+    razonSocial: '',
+    nombreComercial: '',
+    ruc: '',
+    dirMatriz: '',
+    estab: '001',
+    ptoEmi: '001',
+    secuencial: '000000001',
+    activo: true,
+  })
+
+  const resetForm = () => {
+    setFormData({
+      ambiente: '1',
+      tipoEmision: '1',
+      razonSocial: '',
+      nombreComercial: '',
+      ruc: '',
+      dirMatriz: '',
+      estab: '001',
+      ptoEmi: '001',
+      secuencial: '000000001',
+      activo: true,
+    })
+  }
+
+  const handleCreate = async () => {
+    try {
+      await createConfiguracion(formData)
+      setIsCreateDialogOpen(false)
+      resetForm()
+    } catch (error) {
+      console.error('Error creating configuration:', error)
+    }
+  }
+
+  const handleUpdate = async (id: string) => {
+    try {
+      await updateConfiguracion(id, formData)
+      setEditingConfig(null)
+      resetForm()
+    } catch (error) {
+      console.error('Error updating configuration:', error)
+    }
+  }
+
+  const handleDelete = async (id: string) => {
+    if (confirm('¿Está seguro de que desea eliminar esta configuración?')) {
+      try {
+        await deleteConfiguracion(id)
+      } catch (error) {
+        console.error('Error deleting configuration:', error)
+      }
+    }
+  }
+
+  const handleIncrementSecuencial = async (id: string) => {
+    try {
+      await incrementarSecuencial(id)
+    } catch (error) {
+      console.error('Error incrementing secuencial:', error)
+    }
+  }
+
+  const handleResetSecuencial = (id: string, currentSecuencial: string) => {
+    setResetConfigId(id)
+    setResetSecuencial(currentSecuencial)
+    setIsResetDialogOpen(true)
+  }
+
+  const confirmResetSecuencial = async () => {
+    if (!resetConfigId || !resetSecuencial) return
+
+    try {
+      await reiniciarSecuencial(resetConfigId, resetSecuencial)
+      setIsResetDialogOpen(false)
+      setResetConfigId(null)
+      setResetSecuencial('')
+    } catch (error) {
+      console.error('Error resetting secuencial:', error)
+    }
+  }
+
+  const cancelReset = () => {
+    setIsResetDialogOpen(false)
+    setResetConfigId(null)
+    setResetSecuencial('')
+  }
+
+  const startEdit = (config: any) => {
+    setEditingConfig(config.id)
+    setFormData({
+      ambiente: config.ambiente,
+      tipoEmision: config.tipoEmision,
+      razonSocial: config.razonSocial,
+      nombreComercial: config.nombreComercial,
+      ruc: config.ruc,
+      dirMatriz: config.dirMatriz,
+      estab: config.estab,
+      ptoEmi: config.ptoEmi,
+      secuencial: config.secuencial,
+      activo: config.activo,
+    })
+  }
+
+  const cancelEdit = () => {
+    setEditingConfig(null)
+    resetForm()
+  }
+
+  if (loading && configuraciones.length === 0) {
+    return <div className="flex justify-center items-center h-64">Cargando...</div>
+  }
+
+  if (error) {
+    return <div className="text-red-500 text-center p-4">Error: {error}</div>
+  }
+
+  return (
+    <div className="space-y-6">
+      <div className="flex justify-between items-center">
+        <div>
+          <h2 className="text-2xl font-bold">Configuraciones Tributarias</h2>
+          <p className="text-gray-600">Gestiona la información tributaria de tu empresa</p>
+        </div>
+        
+        <Dialog open={isCreateDialogOpen} onOpenChange={setIsCreateDialogOpen}>
+          <DialogTrigger asChild>
+            <Button onClick={() => { resetForm(); setIsCreateDialogOpen(true); }}>
+              <Plus className="w-4 h-4 mr-2" />
+              Nueva Configuración
+            </Button>
+          </DialogTrigger>
+          <DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
+            <DialogHeader>
+              <DialogTitle>Nueva Configuración Tributaria</DialogTitle>
+              <DialogDescription>
+                Ingresa los datos de la configuración tributaria
+              </DialogDescription>
+            </DialogHeader>
+            <ConfiguracionForm
+              formData={formData}
+              setFormData={setFormData}
+              onSave={handleCreate}
+              onCancel={() => setIsCreateDialogOpen(false)}
+            />
+          </DialogContent>
+        </Dialog>
+      </div>
+
+      <div className="grid gap-4">
+        {configuraciones.map((config) => (
+          <Card key={config.id}>
+            <CardHeader>
+              <div className="flex justify-between items-start">
+                <div>
+                  <CardTitle className="flex items-center gap-2">
+                    {config.razonSocial}
+                    <Badge variant={config.activo ? "default" : "secondary"}>
+                      {config.activo ? "Activa" : "Inactiva"}
+                    </Badge>
+                  </CardTitle>
+                  <CardDescription>
+                    RUC: {config.ruc} | {config.nombreComercial}
+                  </CardDescription>
+                </div>
+                <div className="flex gap-2">
+                  {editingConfig === config.id ? (
+                    <>
+                      <Button
+                        size="sm"
+                        onClick={() => handleUpdate(config.id)}
+                        disabled={loading}
+                      >
+                        <Save className="w-4 h-4" />
+                      </Button>
+                      <Button
+                        size="sm"
+                        variant="outline"
+                        onClick={cancelEdit}
+                        disabled={loading}
+                      >
+                        Cancelar
+                      </Button>
+                    </>
+                  ) : (
+                    <>
+                      <Button
+                        size="sm"
+                        variant="outline"
+                        onClick={() => startEdit(config)}
+                      >
+                        <Edit className="w-4 h-4" />
+                      </Button>
+                      <Button
+                        size="sm"
+                        variant="outline"
+                        onClick={() => handleIncrementSecuencial(config.id)}
+                        disabled={loading}
+                      >
+                        + Secuencial
+                      </Button>
+                      <Button
+                        size="sm"
+                        variant="outline"
+                        onClick={() => handleResetSecuencial(config.id, config.secuencial)}
+                        disabled={loading}
+                      >
+                        <RotateCcw className="w-4 h-4" />
+                      </Button>
+                      <Button
+                        size="sm"
+                        variant="destructive"
+                        onClick={() => handleDelete(config.id)}
+                        disabled={loading}
+                      >
+                        <Trash2 className="w-4 h-4" />
+                      </Button>
+                    </>
+                  )}
+                </div>
+              </div>
+            </CardHeader>
+            <CardContent>
+              {editingConfig === config.id ? (
+                <ConfiguracionForm
+                  formData={formData}
+                  setFormData={setFormData}
+                  onSave={() => handleUpdate(config.id)}
+                  onCancel={cancelEdit}
+                  isEdit
+                />
+              ) : (
+                <div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
+                  <div>
+                    <Label className="text-gray-600">Ambiente</Label>
+                    <p>{config.ambiente === '1' ? 'Pruebas' : 'Producción'}</p>
+                  </div>
+                  <div>
+                    <Label className="text-gray-600">Establecimiento</Label>
+                    <p>{config.estab}</p>
+                  </div>
+                  <div>
+                    <Label className="text-gray-600">Punto Emisión</Label>
+                    <p>{config.ptoEmi}</p>
+                  </div>
+                  <div>
+                    <Label className="text-gray-600">Secuencial</Label>
+                    <p>{config.secuencial}</p>
+                  </div>
+                  <div className="col-span-2 md:col-span-4">
+                    <Label className="text-gray-600">Dirección Matriz</Label>
+                    <p>{config.dirMatriz}</p>
+                  </div>
+                </div>
+              )}
+            </CardContent>
+          </Card>
+        ))}
+        
+        {configuraciones.length === 0 && !loading && (
+          <Card>
+            <CardContent className="text-center py-8">
+              <p className="text-gray-500">No hay configuraciones tributarias registradas</p>
+              <Button
+                className="mt-4"
+                onClick={() => setIsCreateDialogOpen(true)}
+              >
+                <Plus className="w-4 h-4 mr-2" />
+                Crear Primera Configuración
+              </Button>
+            </CardContent>
+          </Card>
+        )}
+      </div>
+
+      {/* Diálogo para reiniciar secuencial */}
+      <Dialog open={isResetDialogOpen} onOpenChange={setIsResetDialogOpen}>
+        <DialogContent>
+          <DialogHeader>
+            <DialogTitle>Reiniciar Secuencial</DialogTitle>
+            <DialogDescription>
+              Ingresa el nuevo número de secuencial para esta configuración.
+            </DialogDescription>
+          </DialogHeader>
+          <div className="space-y-4">
+            <div className="space-y-2">
+              <Label htmlFor="resetSecuencial">Nuevo Secuencial</Label>
+              <Input
+                id="resetSecuencial"
+                value={resetSecuencial}
+                onChange={(e) => setResetSecuencial(e.target.value)}
+                placeholder="000000001"
+                maxLength={9}
+              />
+              <p className="text-sm text-gray-500">
+                Ingresa un número entre 0 y 999999999. El sistema lo formateará automáticamente.
+              </p>
+            </div>
+            <div className="flex justify-end gap-2">
+              <Button variant="outline" onClick={cancelReset}>
+                Cancelar
+              </Button>
+              <Button 
+                onClick={confirmResetSecuencial}
+                disabled={!resetSecuencial || loading}
+              >
+                Reiniciar
+              </Button>
+            </div>
+          </div>
+        </DialogContent>
+      </Dialog>
+    </div>
+  )
+}
+
+interface ConfiguracionFormProps {
+  formData: any
+  setFormData: (data: any) => void
+  onSave: () => void
+  onCancel: () => void
+  isEdit?: boolean
+}
+
+function ConfiguracionForm({ formData, setFormData, onSave, onCancel, isEdit = false }: ConfiguracionFormProps) {
+  return (
+    <div className="space-y-4">
+      <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
+        <div className="space-y-2">
+          <Label htmlFor="ambiente">Ambiente</Label>
+          <Select value={formData.ambiente} onValueChange={(value) => setFormData({ ...formData, ambiente: value })}>
+            <SelectTrigger>
+              <SelectValue />
+            </SelectTrigger>
+            <SelectContent>
+              <SelectItem value="1">1 - Pruebas</SelectItem>
+              <SelectItem value="2">2 - Producción</SelectItem>
+            </SelectContent>
+          </Select>
+        </div>
+        
+        <div className="space-y-2">
+          <Label htmlFor="tipoEmision">Tipo Emisión</Label>
+          <Select value={formData.tipoEmision} onValueChange={(value) => setFormData({ ...formData, tipoEmision: value })}>
+            <SelectTrigger>
+              <SelectValue />
+            </SelectTrigger>
+            <SelectContent>
+              <SelectItem value="1">1 - Normal</SelectItem>
+            </SelectContent>
+          </Select>
+        </div>
+        
+        <div className="space-y-2">
+          <Label htmlFor="ruc">RUC *</Label>
+          <Input
+            id="ruc"
+            value={formData.ruc}
+            onChange={(e) => setFormData({ ...formData, ruc: e.target.value })}
+            placeholder="13 dígitos"
+            maxLength={13}
+          />
+        </div>
+        
+        <div className="space-y-2">
+          <Label htmlFor="razonSocial">Razón Social *</Label>
+          <Input
+            id="razonSocial"
+            value={formData.razonSocial}
+            onChange={(e) => setFormData({ ...formData, razonSocial: e.target.value })}
+            placeholder="Empresa S.A."
+          />
+        </div>
+        
+        <div className="space-y-2">
+          <Label htmlFor="nombreComercial">Nombre Comercial *</Label>
+          <Input
+            id="nombreComercial"
+            value={formData.nombreComercial}
+            onChange={(e) => setFormData({ ...formData, nombreComercial: e.target.value })}
+            placeholder="Nombre Comercial"
+          />
+        </div>
+        
+        <div className="space-y-2">
+          <Label htmlFor="estab">Establecimiento</Label>
+          <Input
+            id="estab"
+            value={formData.estab}
+            onChange={(e) => setFormData({ ...formData, estab: e.target.value })}
+            placeholder="001"
+            maxLength={3}
+          />
+        </div>
+        
+        <div className="space-y-2">
+          <Label htmlFor="ptoEmi">Punto Emisión</Label>
+          <Input
+            id="ptoEmi"
+            value={formData.ptoEmi}
+            onChange={(e) => setFormData({ ...formData, ptoEmi: e.target.value })}
+            placeholder="001"
+            maxLength={3}
+          />
+        </div>
+        
+        <div className="space-y-2">
+          <Label htmlFor="secuencial">Secuencial</Label>
+          <Input
+            id="secuencial"
+            value={formData.secuencial}
+            onChange={(e) => setFormData({ ...formData, secuencial: e.target.value })}
+            placeholder="000000001"
+            maxLength={9}
+          />
+        </div>
+        
+        <div className="space-y-2 md:col-span-2">
+          <Label htmlFor="dirMatriz">Dirección Matriz *</Label>
+          <Input
+            id="dirMatriz"
+            value={formData.dirMatriz}
+            onChange={(e) => setFormData({ ...formData, dirMatriz: e.target.value })}
+            placeholder="Av. Principal 123 y Secundaria"
+          />
+        </div>
+        
+        {isEdit && (
+          <div className="flex items-center space-x-2">
+            <Switch
+              id="activo"
+              checked={formData.activo}
+              onCheckedChange={(checked) => setFormData({ ...formData, activo: checked })}
+            />
+            <Label htmlFor="activo">Activa</Label>
+          </div>
+        )}
+      </div>
+      
+      <div className="flex justify-end gap-2 pt-4">
+        <Button variant="outline" onClick={onCancel}>
+          Cancelar
+        </Button>
+        <Button onClick={onSave}>
+          {isEdit ? 'Actualizar' : 'Guardar'}
+        </Button>
+      </div>
+    </div>
+  )
+}

+ 54 - 2
src/components/factura/InfoTributariaForm.tsx

@@ -1,8 +1,13 @@
+"use client"
+
 import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
 import { Input } from "@/components/ui/input"
 import { Label } from "@/components/ui/label"
 import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
 import type { InfoTributaria } from "@/types/factura"
+import { useConfiguracionesTributarias } from '@/hooks/useConfiguracionesTributarias'
+import { Button } from "@/components/ui/button"
+import { ExternalLink } from 'lucide-react'
 
 interface InfoTributariaFormProps {
   infoTributaria: InfoTributaria
@@ -10,11 +15,58 @@ interface InfoTributariaFormProps {
 }
 
 export function InfoTributariaForm({ infoTributaria, onChange }: InfoTributariaFormProps) {
+  const { configuraciones } = useConfiguracionesTributarias()
+
+  const loadConfiguracion = (config: any) => {
+    onChange('ambiente', config.ambiente)
+    onChange('tipoEmision', config.tipoEmision)
+    onChange('razonSocial', config.razonSocial)
+    onChange('nombreComercial', config.nombreComercial)
+    onChange('ruc', config.ruc)
+    onChange('dirMatriz', config.dirMatriz)
+    onChange('estab', config.estab)
+    onChange('ptoEmi', config.ptoEmi)
+    onChange('secuencial', config.secuencial)
+    // Nota: claveAcceso no se guarda en la base de datos por seguridad
+  }
+
   return (
     <Card>
       <CardHeader>
-        <CardTitle>Información Tributaria</CardTitle>
-        <CardDescription>Datos del emisor de la factura</CardDescription>
+        <div className="flex justify-between items-start">
+          <div>
+            <CardTitle>Información Tributaria</CardTitle>
+            <CardDescription>Datos del emisor de la factura</CardDescription>
+          </div>
+          {configuraciones.length > 0 && (
+            <div className="flex gap-2">
+              <Select onValueChange={(value) => {
+                const config = configuraciones.find(c => c.id === value)
+                if (config) {
+                  loadConfiguracion(config)
+                }
+              }}>
+                <SelectTrigger className="w-[200px]">
+                  <SelectValue placeholder="Cargar configuración" />
+                </SelectTrigger>
+                <SelectContent>
+                  {configuraciones.filter(c => c.activo).map((config) => (
+                    <SelectItem key={config.id} value={config.id}>
+                      {config.razonSocial} ({config.ruc})
+                    </SelectItem>
+                  ))}
+                </SelectContent>
+              </Select>
+              <Button
+                variant="outline"
+                size="sm"
+                onClick={() => window.open('/configuracion', '_blank')}
+              >
+                <ExternalLink className="w-4 h-4" />
+              </Button>
+            </div>
+          )}
+        </div>
       </CardHeader>
       <CardContent>
         <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">

+ 270 - 0
src/hooks/useConfiguracionesTributarias.ts

@@ -0,0 +1,270 @@
+"use client"
+
+import { useState, useEffect } from 'react'
+
+interface ConfiguracionTributaria {
+  id: string
+  ambiente: string
+  tipoEmision: string
+  razonSocial: string
+  nombreComercial: string
+  ruc: string
+  dirMatriz: string
+  estab: string
+  ptoEmi: string
+  secuencial: string
+  activo: boolean
+  createdAt: string
+  updatedAt: string
+}
+
+interface CreateConfiguracionData {
+  ambiente: string
+  tipoEmision: string
+  razonSocial: string
+  nombreComercial: string
+  ruc: string
+  dirMatriz: string
+  estab: string
+  ptoEmi: string
+  secuencial: string
+}
+
+interface UpdateConfiguracionData {
+  ambiente?: string
+  tipoEmision?: string
+  razonSocial?: string
+  nombreComercial?: string
+  ruc?: string
+  dirMatriz?: string
+  estab?: string
+  ptoEmi?: string
+  secuencial?: string
+  activo?: boolean
+}
+
+export function useConfiguracionesTributarias() {
+  const [configuraciones, setConfiguraciones] = useState<ConfiguracionTributaria[]>([])
+  const [loading, setLoading] = useState(false)
+  const [error, setError] = useState<string | null>(null)
+
+  // Obtener todas las configuraciones
+  const fetchConfiguraciones = async () => {
+    try {
+      setLoading(true)
+      setError(null)
+      
+      const response = await fetch('/api/configuraciones-tributarias')
+      
+      if (!response.ok) {
+        throw new Error('Error al obtener configuraciones')
+      }
+      
+      const data = await response.json()
+      setConfiguraciones(data)
+    } catch (err) {
+      setError(err instanceof Error ? err.message : 'Error desconocido')
+    } finally {
+      setLoading(false)
+    }
+  }
+
+  // Crear una nueva configuración
+  const createConfiguracion = async (data: CreateConfiguracionData) => {
+    try {
+      setLoading(true)
+      setError(null)
+      
+      const response = await fetch('/api/configuraciones-tributarias', {
+        method: 'POST',
+        headers: {
+          'Content-Type': 'application/json',
+        },
+        body: JSON.stringify(data),
+      })
+      
+      if (!response.ok) {
+        const errorData = await response.json()
+        throw new Error(errorData.error || 'Error al crear configuración')
+      }
+      
+      const nuevaConfiguracion = await response.json()
+      setConfiguraciones(prev => [nuevaConfiguracion, ...prev])
+      
+      return nuevaConfiguracion
+    } catch (err) {
+      setError(err instanceof Error ? err.message : 'Error desconocido')
+      throw err
+    } finally {
+      setLoading(false)
+    }
+  }
+
+  // Actualizar una configuración
+  const updateConfiguracion = async (id: string, data: UpdateConfiguracionData) => {
+    try {
+      setLoading(true)
+      setError(null)
+      
+      const response = await fetch(`/api/configuraciones-tributarias/${id}`, {
+        method: 'PUT',
+        headers: {
+          'Content-Type': 'application/json',
+        },
+        body: JSON.stringify(data),
+      })
+      
+      if (!response.ok) {
+        const errorData = await response.json()
+        throw new Error(errorData.error || 'Error al actualizar configuración')
+      }
+      
+      const configuracionActualizada = await response.json()
+      setConfiguraciones(prev => 
+        prev.map(config => 
+          config.id === id ? configuracionActualizada : config
+        )
+      )
+      
+      return configuracionActualizada
+    } catch (err) {
+      setError(err instanceof Error ? err.message : 'Error desconocido')
+      throw err
+    } finally {
+      setLoading(false)
+    }
+  }
+
+  // Eliminar (desactivar) una configuración
+  const deleteConfiguracion = async (id: string) => {
+    try {
+      setLoading(true)
+      setError(null)
+      
+      const response = await fetch(`/api/configuraciones-tributarias/${id}`, {
+        method: 'DELETE',
+      })
+      
+      if (!response.ok) {
+        const errorData = await response.json()
+        throw new Error(errorData.error || 'Error al eliminar configuración')
+      }
+      
+      setConfiguraciones(prev => 
+        prev.map(config => 
+          config.id === id ? { ...config, activo: false } : config
+        )
+      )
+    } catch (err) {
+      setError(err instanceof Error ? err.message : 'Error desconocido')
+      throw err
+    } finally {
+      setLoading(false)
+    }
+  }
+
+  // Incrementar secuencial
+  const incrementarSecuencial = async (id: string) => {
+    try {
+      setLoading(true)
+      setError(null)
+      
+      const response = await fetch(`/api/configuraciones-tributarias/${id}/incrementar-secuencial`, {
+        method: 'POST',
+      })
+      
+      if (!response.ok) {
+        const errorData = await response.json()
+        throw new Error(errorData.error || 'Error al incrementar secuencial')
+      }
+      
+      const result = await response.json()
+      setConfiguraciones(prev => 
+        prev.map(config => 
+          config.id === id ? result.configuracion : config
+        )
+      )
+      
+      return result
+    } catch (err) {
+      setError(err instanceof Error ? err.message : 'Error desconocido')
+      throw err
+    } finally {
+      setLoading(false)
+    }
+  }
+
+  // Reiniciar secuencial
+  const reiniciarSecuencial = async (id: string, nuevoSecuencial: string) => {
+    try {
+      setLoading(true)
+      setError(null)
+      
+      const response = await fetch(`/api/configuraciones-tributarias/${id}/reiniciar-secuencial`, {
+        method: 'POST',
+        headers: {
+          'Content-Type': 'application/json',
+        },
+        body: JSON.stringify({ nuevoSecuencial }),
+      })
+      
+      if (!response.ok) {
+        const errorData = await response.json()
+        throw new Error(errorData.error || 'Error al reiniciar secuencial')
+      }
+      
+      const result = await response.json()
+      setConfiguraciones(prev => 
+        prev.map(config => 
+          config.id === id ? result.configuracion : config
+        )
+      )
+      
+      return result
+    } catch (err) {
+      setError(err instanceof Error ? err.message : 'Error desconocido')
+      throw err
+    } finally {
+      setLoading(false)
+    }
+  }
+
+  // Obtener una configuración por ID
+  const getConfiguracionById = async (id: string) => {
+    try {
+      setLoading(true)
+      setError(null)
+      
+      const response = await fetch(`/api/configuraciones-tributarias/${id}`)
+      
+      if (!response.ok) {
+        const errorData = await response.json()
+        throw new Error(errorData.error || 'Error al obtener configuración')
+      }
+      
+      return await response.json()
+    } catch (err) {
+      setError(err instanceof Error ? err.message : 'Error desconocido')
+      throw err
+    } finally {
+      setLoading(false)
+    }
+  }
+
+  useEffect(() => {
+    fetchConfiguraciones()
+  }, [])
+
+  return {
+    configuraciones,
+    loading,
+    error,
+    fetchConfiguraciones,
+    createConfiguracion,
+    updateConfiguracion,
+    deleteConfiguracion,
+    incrementarSecuencial,
+    reiniciarSecuencial,
+    getConfiguracionById,
+  }
+}

+ 9 - 0
src/lib/prisma.ts

@@ -0,0 +1,9 @@
+import { PrismaClient } from '@prisma/client'
+
+const globalForPrisma = globalThis as unknown as {
+  prisma: PrismaClient | undefined
+}
+
+export const prisma = globalForPrisma.prisma ?? new PrismaClient()
+
+if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma