senal88 commited on
Commit
933fa00
·
verified ·
1 Parent(s): 7c412a6

chore: deploy web_comercial from monorepo

Browse files
.dockerignore ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ **/.next
2
+ **/node_modules
3
+ **/.DS_Store
Dockerfile ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM node:22-alpine AS builder
2
+ WORKDIR /workspace
3
+
4
+ COPY apps/web_comercial ./apps/web_comercial
5
+ COPY packages/simulation-engine ./packages/simulation-engine
6
+
7
+ RUN npm ci --prefix apps/web_comercial
8
+ RUN npm --prefix apps/web_comercial run build
9
+
10
+ FROM node:22-alpine AS runner
11
+ WORKDIR /workspace
12
+ ENV NODE_ENV=production
13
+ ENV PORT=7860
14
+ ENV HOSTNAME=0.0.0.0
15
+
16
+ COPY --from=builder /workspace/apps/web_comercial ./apps/web_comercial
17
+ COPY --from=builder /workspace/packages/simulation-engine ./packages/simulation-engine
18
+
19
+ EXPOSE 7860
20
+ CMD ["sh", "-c", "cd apps/web_comercial && npm run start -- -H 0.0.0.0 -p 7860"]
README.md CHANGED
@@ -1,10 +1,10 @@
1
  ---
2
- title: Bumachar Doc Chat
3
- emoji:
4
  colorFrom: blue
5
  colorTo: indigo
6
  sdk: docker
7
  pinned: false
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
1
  ---
2
+ title: Simulador Consorcio
3
+ aemoji: chart_with_upwards_trend
4
  colorFrom: blue
5
  colorTo: indigo
6
  sdk: docker
7
  pinned: false
8
  ---
9
 
10
+ Docker Space para executar `apps/web_comercial` com o pacote interno `packages/simulation-engine`.
apps/web_comercial/.gitignore ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2
+
3
+ # dependencies
4
+ /node_modules
5
+ /.pnp
6
+ .pnp.*
7
+ .yarn/*
8
+ !.yarn/patches
9
+ !.yarn/plugins
10
+ !.yarn/releases
11
+ !.yarn/versions
12
+
13
+ # testing
14
+ /coverage
15
+
16
+ # next.js
17
+ /.next/
18
+ /out/
19
+
20
+ # production
21
+ /build
22
+
23
+ # misc
24
+ .DS_Store
25
+ *.pem
26
+
27
+ # debug
28
+ npm-debug.log*
29
+ yarn-debug.log*
30
+ yarn-error.log*
31
+ .pnpm-debug.log*
32
+
33
+ # env files (can opt-in for committing if needed)
34
+ .env*
35
+
36
+ # vercel
37
+ .vercel
38
+
39
+ # typescript
40
+ *.tsbuildinfo
41
+ next-env.d.ts
apps/web_comercial/README.md ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Web App - Simulador Consorcio
2
+
3
+ Aplicacao Next.js que consome o motor deterministico interno `@simulador-consorcio/simulation-engine`.
4
+
5
+ ## Desenvolvimento
6
+
7
+ ```bash
8
+ npm run dev
9
+ ```
10
+
11
+ ## Qualidade
12
+
13
+ ```bash
14
+ npm run lint
15
+ npm run build
16
+ ```
apps/web_comercial/eslint.config.mjs ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { defineConfig, globalIgnores } from "eslint/config";
2
+ import nextVitals from "eslint-config-next/core-web-vitals";
3
+ import nextTs from "eslint-config-next/typescript";
4
+
5
+ const eslintConfig = defineConfig([
6
+ ...nextVitals,
7
+ ...nextTs,
8
+ // Override default ignores of eslint-config-next.
9
+ globalIgnores([
10
+ // Default ignores of eslint-config-next:
11
+ ".next/**",
12
+ "out/**",
13
+ "build/**",
14
+ "next-env.d.ts",
15
+ ]),
16
+ ]);
17
+
18
+ export default eslintConfig;
apps/web_comercial/next.config.ts ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { NextConfig } from "next";
2
+
3
+ const nextConfig: NextConfig = {
4
+ transpilePackages: ["@simulador-consorcio/simulation-engine"],
5
+ experimental: {
6
+ externalDir: true,
7
+ },
8
+ };
9
+
10
+ export default nextConfig;
apps/web_comercial/package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
apps/web_comercial/package.json ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "consorcio-simulator-app",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "scripts": {
6
+ "dev": "next dev",
7
+ "build": "next build --webpack",
8
+ "start": "next start",
9
+ "lint": "eslint"
10
+ },
11
+ "dependencies": {
12
+ "next": "16.1.6",
13
+ "react": "19.2.3",
14
+ "react-dom": "19.2.3",
15
+ "@simulador-consorcio/simulation-engine": "file:../../packages/simulation-engine"
16
+ },
17
+ "devDependencies": {
18
+ "@tailwindcss/postcss": "^4",
19
+ "@types/node": "^20",
20
+ "@types/react": "^19",
21
+ "@types/react-dom": "^19",
22
+ "eslint": "^9",
23
+ "eslint-config-next": "16.1.6",
24
+ "tailwindcss": "^4",
25
+ "typescript": "^5"
26
+ }
27
+ }
apps/web_comercial/postcss.config.mjs ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ const config = {
2
+ plugins: {
3
+ "@tailwindcss/postcss": {},
4
+ },
5
+ };
6
+
7
+ export default config;
apps/web_comercial/public/file.svg ADDED
apps/web_comercial/public/globe.svg ADDED
apps/web_comercial/public/next.svg ADDED
apps/web_comercial/public/vercel.svg ADDED
apps/web_comercial/public/window.svg ADDED
apps/web_comercial/src/app/favicon.ico ADDED
apps/web_comercial/src/app/globals.css ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @import "tailwindcss";
2
+
3
+ :root {
4
+ --background: #ffffff;
5
+ --foreground: #171717;
6
+ }
7
+
8
+ @theme inline {
9
+ --color-background: var(--background);
10
+ --color-foreground: var(--foreground);
11
+ --font-sans: "Inter", "Segoe UI", "Roboto", "Helvetica Neue", Arial, sans-serif;
12
+ --font-mono: "SFMono-Regular", "Menlo", "Monaco", "Consolas", "Liberation Mono", monospace;
13
+ }
14
+
15
+ @media (prefers-color-scheme: dark) {
16
+ :root {
17
+ --background: #0a0a0a;
18
+ --foreground: #ededed;
19
+ }
20
+ }
21
+
22
+ body {
23
+ background: var(--background);
24
+ color: var(--foreground);
25
+ font-family: var(--font-sans);
26
+ }
apps/web_comercial/src/app/layout.tsx ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { Metadata } from "next";
2
+ import "./globals.css";
3
+
4
+ export const metadata: Metadata = {
5
+ title: "Apex Consorcios | Simulador",
6
+ description: "Simulador deterministico de consorcio imobiliario",
7
+ };
8
+
9
+ export default function RootLayout({
10
+ children,
11
+ }: Readonly<{
12
+ children: React.ReactNode;
13
+ }>) {
14
+ return (
15
+ <html lang="pt-BR">
16
+ <body className="antialiased">{children}</body>
17
+ </html>
18
+ );
19
+ }
apps/web_comercial/src/app/page.tsx ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { SimulatorForm } from "@/components/simulator/SimulatorForm";
2
+
3
+ export default function Home() {
4
+ return (
5
+ <div className="min-h-screen bg-black text-white selection:bg-blue-500/30">
6
+ <div className="absolute inset-0 z-0 bg-[radial-gradient(ellipse_at_top,_var(--tw-gradient-stops))] from-blue-900/20 via-black to-black"></div>
7
+
8
+ <main className="relative z-10 flex flex-col items-center min-h-screen p-8 sm:p-20 font-sans">
9
+ <header className="w-full max-w-6xl flex justify-between items-center mb-16 animate-in slide-in-from-top-8 duration-700">
10
+ <div className="flex items-center gap-3">
11
+ <div className="w-10 h-10 bg-gradient-to-tr from-blue-600 to-indigo-500 rounded-xl flex items-center justify-center shadow-lg shadow-blue-500/20">
12
+ <svg
13
+ className="w-6 h-6 text-white"
14
+ fill="none"
15
+ stroke="currentColor"
16
+ viewBox="0 0 24 24"
17
+ >
18
+ <path
19
+ strokeLinecap="round"
20
+ strokeLinejoin="round"
21
+ strokeWidth="2"
22
+ d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"
23
+ ></path>
24
+ </svg>
25
+ </div>
26
+ <h1 className="text-xl font-bold tracking-tight text-transparent bg-clip-text bg-gradient-to-r from-white to-gray-400">
27
+ Apex Consórcios
28
+ </h1>
29
+ </div>
30
+ <nav className="hidden md:flex gap-6 text-sm font-medium text-gray-400">
31
+ <a
32
+ href="#"
33
+ className="text-white hover:text-blue-400 transition-colors"
34
+ >
35
+ Simulador
36
+ </a>
37
+ <a href="#" className="hover:text-blue-400 transition-colors">
38
+ Mercado Secundário
39
+ </a>
40
+ <a href="#" className="hover:text-blue-400 transition-colors">
41
+ Configurações
42
+ </a>
43
+ </nav>
44
+ </header>
45
+
46
+ <section className="w-full flex-grow flex flex-col items-center mt-8">
47
+ <div className="text-center max-w-3xl mb-16 animate-in zoom-in-95 duration-700 delay-150">
48
+ <h2 className="text-5xl md:text-6xl font-extrabold tracking-tighter mb-6 leading-tight">
49
+ Simule o futuro do seu{" "}
50
+ <span className="text-transparent bg-clip-text bg-gradient-to-r from-blue-400 to-indigo-500">
51
+ patrimônio
52
+ </span>
53
+ </h2>
54
+ <p className="text-lg md:text-xl text-gray-400 max-w-2xl mx-auto leading-relaxed">
55
+ O modelo determinístico oficial da Rodobens parametrizado para
56
+ viabilidade primária e revenda secundária.
57
+ </p>
58
+ </div>
59
+
60
+ <SimulatorForm />
61
+ </section>
62
+
63
+ <footer className="w-full max-w-6xl mt-24 pt-8 border-t border-white/10 text-center text-sm text-gray-500 flex justify-between items-center">
64
+ <p>© 2026 Apex Consórcios. Motor Baseado em Regras Rodobens.</p>
65
+ <div className="flex gap-4">
66
+ <span className="flex items-center gap-1.5">
67
+ <div className="w-2 h-2 rounded-full bg-emerald-500"></div> API
68
+ Determinística Ativa
69
+ </span>
70
+ </div>
71
+ </footer>
72
+ </main>
73
+ </div>
74
+ );
75
+ }
apps/web_comercial/src/components/simulator/SimulatorForm.tsx ADDED
@@ -0,0 +1,248 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import { calcularSimulacao } from "@simulador-consorcio/simulation-engine";
5
+ import {
6
+ type ParametrosPlano,
7
+ type ResultadoSimulacao,
8
+ type SimulacaoInput,
9
+ } from "@simulador-consorcio/simulation-engine";
10
+
11
+ export function SimulatorForm() {
12
+ const [credito, setCredito] = useState<number>(300000);
13
+ const [prazo, setPrazo] = useState<number>(180);
14
+ const [temLance, setTemLance] = useState<boolean>(false);
15
+ const [lancePercentual, setLancePercentual] = useState<number>(30);
16
+ const [resultado, setResultado] = useState<ResultadoSimulacao | null>(null);
17
+
18
+ const formatCurrency = (value: number) => {
19
+ return new Intl.NumberFormat("pt-BR", {
20
+ style: "currency",
21
+ currency: "BRL",
22
+ }).format(value);
23
+ };
24
+
25
+ const handleSimular = () => {
26
+ const parametros: ParametrosPlano = {
27
+ id_plano: "PADRAO",
28
+ nome_plano: "Plano Padrão Imóvel",
29
+ prazo_meses: prazo,
30
+ taxa_administracao_total: 0.15, // 15%
31
+ fundo_reserva_mensal: 0.0005, // 0.05%
32
+ seguro_prestamista: 0.035, // 3.5%
33
+ };
34
+
35
+ const input: SimulacaoInput = {
36
+ credito_desejado: credito,
37
+ parametros,
38
+ tem_lance: temLance,
39
+ lance_percentual: temLance ? lancePercentual / 100 : undefined,
40
+ };
41
+
42
+ const res = calcularSimulacao(input);
43
+ setResultado(res);
44
+ };
45
+
46
+ return (
47
+ <div className="flex flex-col md:flex-row gap-8 w-full max-w-6xl mx-auto p-4">
48
+ {/* Formulário */}
49
+ <div className="w-full md:w-1/2 bg-white/5 backdrop-blur-xl border border-white/10 rounded-3xl p-8 shadow-2xl transition-all hover:shadow-[0_0_40px_rgba(59,130,246,0.15)]">
50
+ <h2 className="text-2xl font-semibold mb-6 text-white tracking-tight">
51
+ Novo Cenário
52
+ </h2>
53
+
54
+ <div className="space-y-6">
55
+ <div>
56
+ <label className="block text-sm font-medium text-gray-300 mb-2">
57
+ Crédito Desejado (R$)
58
+ </label>
59
+ <input
60
+ type="number"
61
+ value={credito}
62
+ onChange={(e) => setCredito(Number(e.target.value))}
63
+ step="10000"
64
+ className="w-full bg-black/20 border border-gray-700 text-white rounded-xl px-4 py-3 focus:outline-none focus:ring-2 focus:ring-blue-500 transition-all font-mono"
65
+ />
66
+ </div>
67
+
68
+ <div>
69
+ <label className="block text-sm font-medium text-gray-300 mb-2">
70
+ Prazo (Meses)
71
+ </label>
72
+ <select
73
+ value={prazo}
74
+ onChange={(e) => setPrazo(Number(e.target.value))}
75
+ className="w-full bg-black/20 border border-gray-700 text-white rounded-xl px-4 py-3 focus:outline-none focus:ring-2 focus:ring-blue-500 appearance-none transition-all"
76
+ >
77
+ <option value="120">120 meses</option>
78
+ <option value="150">150 meses</option>
79
+ <option value="180">180 meses</option>
80
+ <option value="216">216 meses</option>
81
+ </select>
82
+ </div>
83
+
84
+ <div className="flex items-center space-x-3 pt-2">
85
+ <button
86
+ type="button"
87
+ onClick={() => setTemLance(!temLance)}
88
+ className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${temLance ? "bg-blue-600" : "bg-gray-600"}`}
89
+ >
90
+ <span
91
+ className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${temLance ? "translate-x-6" : "translate-x-1"}`}
92
+ />
93
+ </button>
94
+ <label
95
+ className="text-sm font-medium text-gray-300 cursor-pointer"
96
+ onClick={() => setTemLance(!temLance)}
97
+ >
98
+ Incluir Lance
99
+ </label>
100
+ </div>
101
+
102
+ {temLance && (
103
+ <div className="animate-in fade-in slide-in-from-top-4 duration-300">
104
+ <label className="block text-sm font-medium text-gray-300 mb-2">
105
+ Percentual de Lance (%)
106
+ </label>
107
+ <input
108
+ type="number"
109
+ value={lancePercentual}
110
+ onChange={(e) => setLancePercentual(Number(e.target.value))}
111
+ min="10"
112
+ max="80"
113
+ className="w-full bg-black/20 border border-gray-700 text-white rounded-xl px-4 py-3 focus:outline-none focus:ring-2 focus:ring-blue-500 transition-all"
114
+ />
115
+ </div>
116
+ )}
117
+
118
+ <button
119
+ onClick={handleSimular}
120
+ className="w-full mt-8 bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-500 hover:to-indigo-500 text-white font-medium py-4 px-6 rounded-xl shadow-[0_0_20px_rgba(79,70,229,0.3)] hover:shadow-[0_0_30px_rgba(79,70,229,0.5)] transition-all transform hover:-translate-y-0.5 active:translate-y-0"
121
+ >
122
+ Calcular Cenário
123
+ </button>
124
+ </div>
125
+ </div>
126
+
127
+ {/* Resultados */}
128
+ <div className="w-full md:w-1/2 flex flex-col gap-6">
129
+ {resultado ? (
130
+ <div className="animate-in fade-in zoom-in-95 duration-500 space-y-6">
131
+ <div className="bg-gradient-to-br from-indigo-900/40 to-blue-900/20 border border-blue-500/30 rounded-3xl p-8 shadow-xl backdrop-blur-md">
132
+ <h3 className="text-lg text-blue-300 font-medium mb-1">
133
+ Parcela Integral (100%)
134
+ </h3>
135
+ <div className="text-5xl font-bold text-white tracking-tighter mb-6">
136
+ {formatCurrency(resultado.parcela_integral.total)}
137
+ </div>
138
+
139
+ <div className="grid grid-cols-2 gap-4 text-sm">
140
+ <div className="bg-black/20 p-3 rounded-lg border border-white/5">
141
+ <div className="text-gray-400 mb-1">Fundo Comum</div>
142
+ <div className="text-white font-mono">
143
+ {formatCurrency(resultado.parcela_integral.fundo_comum)}
144
+ </div>
145
+ </div>
146
+ <div className="bg-black/20 p-3 rounded-lg border border-white/5">
147
+ <div className="text-gray-400 mb-1">Taxa Admin (TA)</div>
148
+ <div className="text-white font-mono">
149
+ {formatCurrency(
150
+ resultado.parcela_integral.taxa_administracao,
151
+ )}
152
+ </div>
153
+ </div>
154
+ <div className="bg-black/20 p-3 rounded-lg border border-white/5">
155
+ <div className="text-gray-400 mb-1">Seguro/Outros</div>
156
+ <div className="text-white font-mono">
157
+ {formatCurrency(
158
+ resultado.parcela_integral.seguro +
159
+ resultado.parcela_integral.fundo_reserva,
160
+ )}
161
+ </div>
162
+ </div>
163
+ <div className="bg-black/20 p-3 rounded-lg border border-white/5">
164
+ <div className="text-gray-400 mb-1">Prazo</div>
165
+ <div className="text-white font-mono">
166
+ {resultado.prazo} meses
167
+ </div>
168
+ </div>
169
+ </div>
170
+ </div>
171
+
172
+ {resultado.parcela_reduzida && (
173
+ <div className="bg-white/5 backdrop-blur-xl border border-white/10 rounded-3xl p-6 shadow-lg hover:bg-white/10 transition-colors">
174
+ <h3 className="text-sm text-gray-400 font-medium mb-1">
175
+ Parcela Reduzida (Ate a contemplacao)
176
+ </h3>
177
+ <div className="text-3xl font-bold text-emerald-400 tracking-tight">
178
+ {formatCurrency(resultado.parcela_reduzida.total)}
179
+ </div>
180
+ </div>
181
+ )}
182
+
183
+ {resultado.valor_lance && resultado.prazo_apos_lance && (
184
+ <div className="bg-gradient-to-r from-amber-900/30 to-orange-900/20 border border-orange-500/30 rounded-3xl p-6 shadow-lg backdrop-blur-md">
185
+ <h3 className="text-sm text-orange-300 font-medium mb-4 flex items-center gap-2">
186
+ <svg
187
+ className="w-5 h-5"
188
+ fill="none"
189
+ stroke="currentColor"
190
+ viewBox="0 0 24 24"
191
+ >
192
+ <path
193
+ strokeLinecap="round"
194
+ strokeLinejoin="round"
195
+ strokeWidth="2"
196
+ d="M13 10V3L4 14h7v7l9-11h-7z"
197
+ ></path>
198
+ </svg>
199
+ Projeção com Lance
200
+ </h3>
201
+ <div className="grid grid-cols-2 gap-4">
202
+ <div>
203
+ <div className="text-gray-400 text-xs uppercase tracking-wider mb-1">
204
+ Valor do Lance
205
+ </div>
206
+ <div className="text-2xl font-semibold text-white">
207
+ {formatCurrency(resultado.valor_lance)}
208
+ </div>
209
+ </div>
210
+ <div>
211
+ <div className="text-gray-400 text-xs uppercase tracking-wider mb-1">
212
+ Prazo Reduzido
213
+ </div>
214
+ <div className="text-2xl font-semibold text-white">
215
+ {resultado.prazo_apos_lance} meses
216
+ </div>
217
+ </div>
218
+ </div>
219
+ </div>
220
+ )}
221
+ </div>
222
+ ) : (
223
+ <div className="h-full w-full flex items-center justify-center border-2 border-dashed border-gray-700/50 rounded-3xl p-12 text-center text-gray-500">
224
+ <div className="max-w-xs">
225
+ <svg
226
+ className="w-12 h-12 mx-auto mb-4 opacity-50 text-gray-400"
227
+ fill="none"
228
+ viewBox="0 0 24 24"
229
+ stroke="currentColor"
230
+ >
231
+ <path
232
+ strokeLinecap="round"
233
+ strokeLinejoin="round"
234
+ strokeWidth="1"
235
+ d="M9 17v-2m3 2v-4m3 4v-6m2 10H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
236
+ />
237
+ </svg>
238
+ <p>
239
+ Ajuste os parâmetros e clique em &quot;Calcular Cenario&quot; para exibir
240
+ as projeções.
241
+ </p>
242
+ </div>
243
+ </div>
244
+ )}
245
+ </div>
246
+ </div>
247
+ );
248
+ }
apps/web_comercial/tsconfig.json ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2017",
4
+ "lib": [
5
+ "dom",
6
+ "dom.iterable",
7
+ "esnext"
8
+ ],
9
+ "allowJs": true,
10
+ "skipLibCheck": true,
11
+ "strict": true,
12
+ "noEmit": true,
13
+ "esModuleInterop": true,
14
+ "module": "esnext",
15
+ "moduleResolution": "bundler",
16
+ "resolveJsonModule": true,
17
+ "isolatedModules": true,
18
+ "jsx": "react-jsx",
19
+ "incremental": true,
20
+ "plugins": [
21
+ {
22
+ "name": "next"
23
+ }
24
+ ],
25
+ "paths": {
26
+ "@/*": [
27
+ "./src/*"
28
+ ]
29
+ }
30
+ },
31
+ "include": [
32
+ "next-env.d.ts",
33
+ "**/*.ts",
34
+ "**/*.tsx",
35
+ ".next/types/**/*.ts",
36
+ ".next/dev/types/**/*.ts",
37
+ "**/*.mts"
38
+ ],
39
+ "exclude": [
40
+ "node_modules"
41
+ ]
42
+ }
packages/simulation-engine/package.json ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "@simulador-consorcio/simulation-engine",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "main": "./src/index.js",
7
+ "exports": {
8
+ ".": "./src/index.js"
9
+ },
10
+ "types": "./src/index.d.ts",
11
+ "scripts": {
12
+ "test": "node --test tests/*.test.js"
13
+ }
14
+ }
packages/simulation-engine/src/index.d.ts ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export interface ParametrosPlano {
2
+ id_plano: string;
3
+ nome_plano: string;
4
+ prazo_meses: number;
5
+ taxa_administracao_total: number;
6
+ fundo_reserva_mensal: number;
7
+ seguro_prestamista: number;
8
+ }
9
+
10
+ export interface SimulacaoInput {
11
+ credito_desejado: number;
12
+ parametros: ParametrosPlano;
13
+ tem_lance: boolean;
14
+ lance_percentual?: number;
15
+ }
16
+
17
+ export interface ResultadoParcela {
18
+ fundo_comum: number;
19
+ taxa_administracao: number;
20
+ fundo_reserva: number;
21
+ seguro: number;
22
+ total: number;
23
+ }
24
+
25
+ export interface ResultadoSimulacao {
26
+ credito: number;
27
+ prazo: number;
28
+ parcela_integral: ResultadoParcela;
29
+ parcela_reduzida?: ResultadoParcela;
30
+ valor_lance?: number;
31
+ prazo_apos_lance?: number;
32
+ }
33
+
34
+ export declare function calcularSimulacao(input: SimulacaoInput): ResultadoSimulacao;
packages/simulation-engine/src/index.js ADDED
@@ -0,0 +1,95 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ function ensurePositiveNumber(name, value) {
2
+ if (typeof value !== "number" || Number.isNaN(value) || value <= 0) {
3
+ throw new Error(`${name} deve ser um numero maior que zero`);
4
+ }
5
+ }
6
+
7
+ function ensureNonNegativeNumber(name, value) {
8
+ if (typeof value !== "number" || Number.isNaN(value) || value < 0) {
9
+ throw new Error(`${name} deve ser um numero nao negativo`);
10
+ }
11
+ }
12
+
13
+ function validateInput(input) {
14
+ if (!input || typeof input !== "object") {
15
+ throw new Error("input invalido");
16
+ }
17
+
18
+ ensurePositiveNumber("credito_desejado", input.credito_desejado);
19
+
20
+ if (!input.parametros || typeof input.parametros !== "object") {
21
+ throw new Error("parametros invalidos");
22
+ }
23
+
24
+ const p = input.parametros;
25
+
26
+ ensurePositiveNumber("prazo_meses", p.prazo_meses);
27
+ ensureNonNegativeNumber("taxa_administracao_total", p.taxa_administracao_total);
28
+ ensureNonNegativeNumber("fundo_reserva_mensal", p.fundo_reserva_mensal);
29
+ ensureNonNegativeNumber("seguro_prestamista", p.seguro_prestamista);
30
+
31
+ if (input.tem_lance && input.lance_percentual !== undefined) {
32
+ ensureNonNegativeNumber("lance_percentual", input.lance_percentual);
33
+ }
34
+ }
35
+
36
+ export function calcularSimulacao(input) {
37
+ validateInput(input);
38
+
39
+ const { credito_desejado, parametros, tem_lance, lance_percentual } = input;
40
+ const {
41
+ prazo_meses,
42
+ taxa_administracao_total,
43
+ fundo_reserva_mensal,
44
+ seguro_prestamista,
45
+ } = parametros;
46
+
47
+ const fcMensal = 1.0 / prazo_meses;
48
+ const taMensal = taxa_administracao_total / prazo_meses;
49
+
50
+ const valorFC = credito_desejado * fcMensal;
51
+ const valorTA = credito_desejado * taMensal;
52
+ const valorFR = credito_desejado * fundo_reserva_mensal;
53
+
54
+ const valorBaseSeguro =
55
+ credito_desejado + credito_desejado * taxa_administracao_total;
56
+ const taxaSeguroMensal = seguro_prestamista / prazo_meses;
57
+ const valorSeguro = valorBaseSeguro * taxaSeguroMensal;
58
+
59
+ const parcelaIntegral = {
60
+ fundo_comum: valorFC,
61
+ taxa_administracao: valorTA,
62
+ fundo_reserva: valorFR,
63
+ seguro: valorSeguro,
64
+ total: valorFC + valorTA + valorFR + valorSeguro,
65
+ };
66
+
67
+ const fatorReducao = 0.7;
68
+ const valorFCReduzido = valorFC * fatorReducao;
69
+
70
+ const parcelaReduzida = {
71
+ fundo_comum: valorFCReduzido,
72
+ taxa_administracao: valorTA,
73
+ fundo_reserva: valorFR,
74
+ seguro: valorSeguro,
75
+ total: valorFCReduzido + valorTA + valorFR + valorSeguro,
76
+ };
77
+
78
+ let valorLance = 0;
79
+ let prazoAposLance = prazo_meses;
80
+
81
+ if (tem_lance && lance_percentual && lance_percentual > 0) {
82
+ valorLance = credito_desejado * lance_percentual;
83
+ const numeroParcelasAntecipadas = Math.floor(valorLance / parcelaIntegral.total);
84
+ prazoAposLance = Math.max(1, prazo_meses - numeroParcelasAntecipadas);
85
+ }
86
+
87
+ return {
88
+ credito: credito_desejado,
89
+ prazo: prazo_meses,
90
+ parcela_integral: parcelaIntegral,
91
+ parcela_reduzida: parcelaReduzida,
92
+ valor_lance: tem_lance ? valorLance : undefined,
93
+ prazo_apos_lance: tem_lance ? prazoAposLance : undefined,
94
+ };
95
+ }
packages/simulation-engine/tests/simulation-engine.test.js ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { calcularSimulacao } from "../src/index.js";
4
+
5
+ const baseInput = {
6
+ credito_desejado: 300000,
7
+ parametros: {
8
+ id_plano: "PADRAO",
9
+ nome_plano: "Plano Padrao Imovel",
10
+ prazo_meses: 180,
11
+ taxa_administracao_total: 0.15,
12
+ fundo_reserva_mensal: 0.0005,
13
+ seguro_prestamista: 0.035,
14
+ },
15
+ tem_lance: false,
16
+ };
17
+
18
+ test("calcula parcela integral com valores esperados", () => {
19
+ const result = calcularSimulacao(baseInput);
20
+
21
+ assert.equal(result.prazo, 180);
22
+ assert.equal(result.credito, 300000);
23
+ assert.ok(result.parcela_integral.total > 0);
24
+ assert.equal(Number(result.parcela_integral.fundo_comum.toFixed(2)), 1666.67);
25
+ });
26
+
27
+ test("calcula cenario com lance e reduz prazo", () => {
28
+ const result = calcularSimulacao({
29
+ ...baseInput,
30
+ tem_lance: true,
31
+ lance_percentual: 0.3,
32
+ });
33
+
34
+ assert.ok(result.valor_lance);
35
+ assert.equal(result.valor_lance, 90000);
36
+ assert.ok(result.prazo_apos_lance);
37
+ assert.ok(result.prazo_apos_lance < result.prazo);
38
+ });
39
+
40
+ test("falha quando prazo_meses for invalido", () => {
41
+ assert.throws(() => {
42
+ calcularSimulacao({
43
+ ...baseInput,
44
+ parametros: {
45
+ ...baseInput.parametros,
46
+ prazo_meses: 0,
47
+ },
48
+ });
49
+ }, /prazo_meses/);
50
+ });