@yakuzaa/jade-runtime 0.1.1 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/apis/auth_service.d.ts +5 -1
- package/dist/apis/auth_service.d.ts.map +1 -1
- package/dist/apis/auth_service.js +29 -9
- package/dist/apis/auth_service.js.map +1 -1
- package/dist/apis/console_api.d.ts +14 -0
- package/dist/apis/console_api.d.ts.map +1 -1
- package/dist/apis/console_api.js +18 -0
- package/dist/apis/console_api.js.map +1 -1
- package/dist/apis/http_client.d.ts +1 -0
- package/dist/apis/http_client.d.ts.map +1 -1
- package/dist/apis/http_client.js +36 -0
- package/dist/apis/http_client.js.map +1 -1
- package/dist/browser.d.ts +31 -0
- package/dist/browser.d.ts.map +1 -0
- package/dist/browser.js +3327 -0
- package/dist/browser.js.map +1 -0
- package/dist/core/entity_manager.d.ts +33 -0
- package/dist/core/entity_manager.d.ts.map +1 -0
- package/dist/core/entity_manager.js +52 -0
- package/dist/core/entity_manager.js.map +1 -0
- package/dist/core/event_loop.d.ts +1 -2
- package/dist/core/event_loop.d.ts.map +1 -1
- package/dist/core/event_loop.js +32 -13
- package/dist/core/event_loop.js.map +1 -1
- package/dist/core/rule_engine.d.ts +61 -0
- package/dist/core/rule_engine.d.ts.map +1 -0
- package/dist/core/rule_engine.js +88 -0
- package/dist/core/rule_engine.js.map +1 -0
- package/dist/core/runtime.d.ts +4 -1
- package/dist/core/runtime.d.ts.map +1 -1
- package/dist/core/runtime.js +14 -1
- package/dist/core/runtime.js.map +1 -1
- package/dist/persistence/local_datastore.d.ts +1 -0
- package/dist/persistence/local_datastore.d.ts.map +1 -1
- package/dist/persistence/local_datastore.js +8 -4
- package/dist/persistence/local_datastore.js.map +1 -1
- package/dist/persistence/preferencias.d.ts +37 -0
- package/dist/persistence/preferencias.d.ts.map +1 -0
- package/dist/persistence/preferencias.js +92 -0
- package/dist/persistence/preferencias.js.map +1 -0
- package/dist/stdlib/fiscal.d.ts +100 -0
- package/dist/stdlib/fiscal.d.ts.map +1 -0
- package/dist/stdlib/fiscal.js +158 -0
- package/dist/stdlib/fiscal.js.map +1 -0
- package/dist/stdlib/wms.d.ts +109 -0
- package/dist/stdlib/wms.d.ts.map +1 -0
- package/dist/stdlib/wms.js +257 -0
- package/dist/stdlib/wms.js.map +1 -0
- package/dist/ui/components/cartao.d.ts +4 -0
- package/dist/ui/components/cartao.d.ts.map +1 -0
- package/dist/ui/components/cartao.js +4 -0
- package/dist/ui/components/cartao.js.map +1 -0
- package/dist/ui/responsive.d.ts +51 -0
- package/dist/ui/responsive.d.ts.map +1 -0
- package/dist/ui/responsive.js +526 -0
- package/dist/ui/responsive.js.map +1 -0
- package/dist/ui/router.d.ts.map +1 -1
- package/dist/ui/router.js +5 -2
- package/dist/ui/router.js.map +1 -1
- package/dist/ui/ui_engine.d.ts +25 -2
- package/dist/ui/ui_engine.d.ts.map +1 -1
- package/dist/ui/ui_engine.js +76 -179
- package/dist/ui/ui_engine.js.map +1 -1
- package/package.json +5 -3
package/dist/browser.js
ADDED
|
@@ -0,0 +1,3327 @@
|
|
|
1
|
+
// core/memory_manager.ts
|
|
2
|
+
var JadeBuffer = class {
|
|
3
|
+
memory;
|
|
4
|
+
ptr;
|
|
5
|
+
size;
|
|
6
|
+
offset = 0;
|
|
7
|
+
constructor(memory, ptr, size) {
|
|
8
|
+
this.memory = memory;
|
|
9
|
+
this.ptr = ptr;
|
|
10
|
+
this.size = size;
|
|
11
|
+
}
|
|
12
|
+
// Escreve string UTF-8 na posição atual
|
|
13
|
+
escrever(texto) {
|
|
14
|
+
const encoded = new TextEncoder().encode(texto);
|
|
15
|
+
if (this.offset + encoded.length > this.size) {
|
|
16
|
+
throw new RangeError(
|
|
17
|
+
`[JADE Buffer] Overflow: tentou escrever ${encoded.length} bytes mas s\xF3 restam ${this.size - this.offset} bytes.`
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
const bytes = new Uint8Array(this.memory.buffer, this.ptr + this.offset, encoded.length);
|
|
21
|
+
bytes.set(encoded);
|
|
22
|
+
this.offset += encoded.length;
|
|
23
|
+
}
|
|
24
|
+
// Lê string UTF-8 a partir do início
|
|
25
|
+
ler() {
|
|
26
|
+
const bytes = new Uint8Array(this.memory.buffer, this.ptr, this.offset);
|
|
27
|
+
return new TextDecoder().decode(bytes);
|
|
28
|
+
}
|
|
29
|
+
// Escreve número i32
|
|
30
|
+
escreverInt(valor) {
|
|
31
|
+
if (this.offset + 4 > this.size) {
|
|
32
|
+
throw new RangeError("[JADE Buffer] Overflow ao escrever inteiro.");
|
|
33
|
+
}
|
|
34
|
+
new DataView(this.memory.buffer).setInt32(this.ptr + this.offset, valor, true);
|
|
35
|
+
this.offset += 4;
|
|
36
|
+
}
|
|
37
|
+
// Escreve número f64
|
|
38
|
+
escreverDecimal(valor) {
|
|
39
|
+
if (this.offset + 8 > this.size) {
|
|
40
|
+
throw new RangeError("[JADE Buffer] Overflow ao escrever decimal.");
|
|
41
|
+
}
|
|
42
|
+
new DataView(this.memory.buffer).setFloat64(this.ptr + this.offset, valor, true);
|
|
43
|
+
this.offset += 8;
|
|
44
|
+
}
|
|
45
|
+
// Reseta cursor para o início (sem apagar dados)
|
|
46
|
+
resetar() {
|
|
47
|
+
this.offset = 0;
|
|
48
|
+
}
|
|
49
|
+
tamanho() {
|
|
50
|
+
return this.size;
|
|
51
|
+
}
|
|
52
|
+
usado() {
|
|
53
|
+
return this.offset;
|
|
54
|
+
}
|
|
55
|
+
disponivel() {
|
|
56
|
+
return this.size - this.offset;
|
|
57
|
+
}
|
|
58
|
+
ponteiro() {
|
|
59
|
+
return this.ptr;
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
var MemoryManager = class {
|
|
63
|
+
memory;
|
|
64
|
+
heapStart;
|
|
65
|
+
// offset onde o heap começa
|
|
66
|
+
nextFree;
|
|
67
|
+
// próximo endereço livre (bump allocator)
|
|
68
|
+
freeList = [];
|
|
69
|
+
allocationSizes = /* @__PURE__ */ new Map();
|
|
70
|
+
allocationsByOwner = /* @__PURE__ */ new Map();
|
|
71
|
+
constructor(initialPages = 1) {
|
|
72
|
+
this.memory = new WebAssembly.Memory({ initial: initialPages, maximum: 256 });
|
|
73
|
+
this.heapStart = 1024;
|
|
74
|
+
this.nextFree = this.heapStart;
|
|
75
|
+
}
|
|
76
|
+
// Retorna a memória para passar como import ao WASM
|
|
77
|
+
getMemory() {
|
|
78
|
+
return this.memory;
|
|
79
|
+
}
|
|
80
|
+
// Conecta a memória exportada pelo módulo WASM ao MemoryManager.
|
|
81
|
+
// Chamado pelo runtime após instanciar o WASM.
|
|
82
|
+
// A partir deste momento, readString/writeString operam no buffer do WASM.
|
|
83
|
+
connectWasmMemory(wasmMemory) {
|
|
84
|
+
this.memory = wasmMemory;
|
|
85
|
+
}
|
|
86
|
+
// Aloca `size` bytes, retorna ponteiro (i32)
|
|
87
|
+
malloc(size) {
|
|
88
|
+
const aligned = Math.ceil(size / 8) * 8;
|
|
89
|
+
const freeIdx = this.freeList.findIndex((b) => b.size >= aligned);
|
|
90
|
+
if (freeIdx !== -1) {
|
|
91
|
+
const block = this.freeList.splice(freeIdx, 1)[0];
|
|
92
|
+
this.allocationSizes.set(block.ptr, aligned);
|
|
93
|
+
return block.ptr;
|
|
94
|
+
}
|
|
95
|
+
const ptr = this.nextFree;
|
|
96
|
+
this.nextFree += aligned;
|
|
97
|
+
const required = Math.ceil(this.nextFree / 65536);
|
|
98
|
+
const current = this.memory.buffer.byteLength / 65536;
|
|
99
|
+
if (required > current) {
|
|
100
|
+
this.memory.grow(required - current);
|
|
101
|
+
}
|
|
102
|
+
this.allocationSizes.set(ptr, aligned);
|
|
103
|
+
return ptr;
|
|
104
|
+
}
|
|
105
|
+
// Libera memória no ponteiro `ptr`
|
|
106
|
+
free(ptr) {
|
|
107
|
+
const size = this.allocationSizes.get(ptr);
|
|
108
|
+
if (size === void 0) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
this.freeList.push({ ptr, size });
|
|
112
|
+
this.allocationSizes.delete(ptr);
|
|
113
|
+
}
|
|
114
|
+
// malloc rastreado — use para alocações de componentes UI
|
|
115
|
+
mallocTracked(size, owner) {
|
|
116
|
+
const ptr = this.malloc(size);
|
|
117
|
+
if (!this.allocationsByOwner.has(owner)) {
|
|
118
|
+
this.allocationsByOwner.set(owner, []);
|
|
119
|
+
}
|
|
120
|
+
this.allocationsByOwner.get(owner).push(ptr);
|
|
121
|
+
return ptr;
|
|
122
|
+
}
|
|
123
|
+
// Libera toda memória de um dono de uma vez
|
|
124
|
+
// Chamar quando uma tela/componente for destruído
|
|
125
|
+
freeOwner(owner) {
|
|
126
|
+
const ptrs = this.allocationsByOwner.get(owner) ?? [];
|
|
127
|
+
for (const ptr of ptrs) {
|
|
128
|
+
this.free(ptr);
|
|
129
|
+
}
|
|
130
|
+
this.allocationsByOwner.delete(owner);
|
|
131
|
+
}
|
|
132
|
+
// Retorna estatísticas de uso por dono (para debug)
|
|
133
|
+
getOwnerStats() {
|
|
134
|
+
const stats = {};
|
|
135
|
+
for (const [owner, ptrs] of this.allocationsByOwner.entries()) {
|
|
136
|
+
stats[owner] = ptrs.length;
|
|
137
|
+
}
|
|
138
|
+
return stats;
|
|
139
|
+
}
|
|
140
|
+
createBuffer(ptr, size) {
|
|
141
|
+
return new JadeBuffer(this.memory, ptr, size);
|
|
142
|
+
}
|
|
143
|
+
// Atalho: aloca e já retorna buffer pronto
|
|
144
|
+
allocBuffer(size, owner) {
|
|
145
|
+
const ptr = owner ? this.mallocTracked(size, owner) : this.malloc(size);
|
|
146
|
+
return new JadeBuffer(this.memory, ptr, size);
|
|
147
|
+
}
|
|
148
|
+
// Escreve string UTF-8 na memória, retorna ponteiro
|
|
149
|
+
writeString(str) {
|
|
150
|
+
const encoded = new TextEncoder().encode(str);
|
|
151
|
+
const ptr = this.malloc(encoded.length + 4);
|
|
152
|
+
const view = new DataView(this.memory.buffer);
|
|
153
|
+
view.setUint32(ptr, encoded.length, true);
|
|
154
|
+
const bytes = new Uint8Array(this.memory.buffer, ptr + 4, encoded.length);
|
|
155
|
+
bytes.set(encoded);
|
|
156
|
+
return ptr;
|
|
157
|
+
}
|
|
158
|
+
// Lê string UTF-8 da memória a partir do ponteiro (null-terminated)
|
|
159
|
+
readString(ptr) {
|
|
160
|
+
const view = new DataView(this.memory.buffer);
|
|
161
|
+
let offset = ptr;
|
|
162
|
+
const bytes = [];
|
|
163
|
+
while (true) {
|
|
164
|
+
const byte = view.getUint8(offset);
|
|
165
|
+
if (byte === 0) break;
|
|
166
|
+
bytes.push(byte);
|
|
167
|
+
offset++;
|
|
168
|
+
}
|
|
169
|
+
return new TextDecoder().decode(new Uint8Array(bytes));
|
|
170
|
+
}
|
|
171
|
+
// Versão alternativa para strings com tamanho prefixado (mantida para compatibilidade)
|
|
172
|
+
readStringWithLength(ptr) {
|
|
173
|
+
const view = new DataView(this.memory.buffer);
|
|
174
|
+
const length = view.getUint32(ptr, true);
|
|
175
|
+
const bytes = new Uint8Array(this.memory.buffer, ptr + 4, length);
|
|
176
|
+
return new TextDecoder().decode(bytes);
|
|
177
|
+
}
|
|
178
|
+
// Escreve struct de entidade na memória
|
|
179
|
+
// fields: array de { name, type, value } na mesma ordem dos campos
|
|
180
|
+
writeStruct(fields) {
|
|
181
|
+
const ptr = this.malloc(fields.length * 8);
|
|
182
|
+
const view = new DataView(this.memory.buffer);
|
|
183
|
+
let offset = ptr;
|
|
184
|
+
for (const field of fields) {
|
|
185
|
+
if (field.type === "i32" || field.type === "i1") {
|
|
186
|
+
view.setInt32(offset, Number(field.value), true);
|
|
187
|
+
offset += 8;
|
|
188
|
+
} else if (field.type === "f64") {
|
|
189
|
+
view.setFloat64(offset, Number(field.value), true);
|
|
190
|
+
offset += 8;
|
|
191
|
+
} else {
|
|
192
|
+
const strPtr = typeof field.value === "string" ? this.writeString(field.value) : Number(field.value);
|
|
193
|
+
view.setInt32(offset, strPtr, true);
|
|
194
|
+
offset += 8;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return ptr;
|
|
198
|
+
}
|
|
199
|
+
// Lê campo de uma struct pelo offset do campo (índice × 8)
|
|
200
|
+
readField(ptr, fieldIndex, type) {
|
|
201
|
+
const view = new DataView(this.memory.buffer);
|
|
202
|
+
const offset = ptr + fieldIndex * 8;
|
|
203
|
+
if (type === "i32" || type === "i1") return view.getInt32(offset, true);
|
|
204
|
+
if (type === "f64") return view.getFloat64(offset, true);
|
|
205
|
+
return view.getInt32(offset, true);
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
// core/event_loop.ts
|
|
210
|
+
var MAX_CADEIA = 100;
|
|
211
|
+
var EventLoop = class {
|
|
212
|
+
handlers = /* @__PURE__ */ new Map();
|
|
213
|
+
queue = [];
|
|
214
|
+
running = false;
|
|
215
|
+
_fromHandler = false;
|
|
216
|
+
// Registra handler para um evento
|
|
217
|
+
on(event, handler) {
|
|
218
|
+
if (!this.handlers.has(event)) {
|
|
219
|
+
this.handlers.set(event, []);
|
|
220
|
+
}
|
|
221
|
+
this.handlers.get(event).push(handler);
|
|
222
|
+
}
|
|
223
|
+
// Remove handler
|
|
224
|
+
off(event, handler) {
|
|
225
|
+
const handlers = this.handlers.get(event);
|
|
226
|
+
if (handlers) {
|
|
227
|
+
const idx = handlers.indexOf(handler);
|
|
228
|
+
if (idx !== -1) handlers.splice(idx, 1);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
// Emite evento — coloca na fila (não bloqueia)
|
|
232
|
+
emit(event, ...args) {
|
|
233
|
+
this.queue.push({ event, args, fromHandler: this._fromHandler });
|
|
234
|
+
if (!this.running) {
|
|
235
|
+
this.running = true;
|
|
236
|
+
this.processQueue().catch((e) => console.error("[JADE EventLoop]", e));
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
// Emite evento de forma síncrona (para uso interno do runtime)
|
|
240
|
+
emitSync(event, ...args) {
|
|
241
|
+
const handlers = this.handlers.get(event) || [];
|
|
242
|
+
for (const handler of handlers) {
|
|
243
|
+
handler(...args);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
async processQueue() {
|
|
247
|
+
let cadeia = 0;
|
|
248
|
+
while (this.queue.length > 0) {
|
|
249
|
+
const { event, args, fromHandler } = this.queue.shift();
|
|
250
|
+
if (!fromHandler) {
|
|
251
|
+
cadeia = 0;
|
|
252
|
+
} else if (++cadeia > MAX_CADEIA) {
|
|
253
|
+
this.queue = [];
|
|
254
|
+
this.running = false;
|
|
255
|
+
throw new Error(
|
|
256
|
+
`[JADE EventLoop] Poss\xEDvel loop infinito: mais de ${MAX_CADEIA} eventos gerados em cadeia por handlers`
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
const handlers = this.handlers.get(event) || [];
|
|
260
|
+
for (const handler of handlers) {
|
|
261
|
+
this._fromHandler = true;
|
|
262
|
+
let result;
|
|
263
|
+
try {
|
|
264
|
+
result = handler(...args);
|
|
265
|
+
} catch (e) {
|
|
266
|
+
this._fromHandler = false;
|
|
267
|
+
console.error(`Erro no handler do evento '${event}':`, e);
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
this._fromHandler = false;
|
|
271
|
+
try {
|
|
272
|
+
await (result ?? Promise.resolve());
|
|
273
|
+
} catch (e) {
|
|
274
|
+
console.error(`Erro no handler do evento '${event}':`, e);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
this.running = false;
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
// core/runtime.ts
|
|
283
|
+
var JadeRuntime = class {
|
|
284
|
+
memory;
|
|
285
|
+
events;
|
|
286
|
+
wasmInstance = null;
|
|
287
|
+
exports = {};
|
|
288
|
+
debug;
|
|
289
|
+
constructor(config = {}) {
|
|
290
|
+
this.memory = new MemoryManager();
|
|
291
|
+
this.events = new EventLoop();
|
|
292
|
+
this.debug = config.debug ?? false;
|
|
293
|
+
}
|
|
294
|
+
// Carrega e instancia um módulo WASM.
|
|
295
|
+
// Aceita: BufferSource (Uint8Array), Response (streaming), ou WebAssembly.Module
|
|
296
|
+
// eventHandlers: lista gerada pelo compilador mapeando evento → função WASM exportada
|
|
297
|
+
async load(wasmSource, eventHandlers) {
|
|
298
|
+
const imports = this.buildImports();
|
|
299
|
+
let instance;
|
|
300
|
+
if (wasmSource instanceof WebAssembly.Module) {
|
|
301
|
+
instance = await WebAssembly.instantiate(wasmSource, imports);
|
|
302
|
+
} else if (wasmSource instanceof Response) {
|
|
303
|
+
const result = await WebAssembly.instantiateStreaming(wasmSource, imports);
|
|
304
|
+
instance = result.instance;
|
|
305
|
+
} else {
|
|
306
|
+
const result = await WebAssembly.instantiate(wasmSource, imports);
|
|
307
|
+
instance = result.instance;
|
|
308
|
+
}
|
|
309
|
+
this.wasmInstance = instance;
|
|
310
|
+
this.exports = instance.exports;
|
|
311
|
+
if (instance.exports.memory instanceof WebAssembly.Memory) {
|
|
312
|
+
this.memory.connectWasmMemory(instance.exports.memory);
|
|
313
|
+
}
|
|
314
|
+
if (eventHandlers) {
|
|
315
|
+
for (const { eventName, functionName } of eventHandlers) {
|
|
316
|
+
const exportName = functionName.startsWith("@") ? functionName.slice(1) : functionName;
|
|
317
|
+
const fn = this.exports[exportName];
|
|
318
|
+
if (typeof fn === "function") {
|
|
319
|
+
this.events.on(eventName, fn);
|
|
320
|
+
if (this.debug) console.log(`[JADE Runtime] Handler registrado: ${eventName} \u2192 ${exportName}`);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
if (this.debug) {
|
|
325
|
+
console.log("[JADE Runtime] M\xF3dulo carregado. Exports:", Object.keys(this.exports));
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
// Chama uma função exportada pelo WASM
|
|
329
|
+
call(funcName, ...args) {
|
|
330
|
+
if (!this.exports[funcName]) {
|
|
331
|
+
throw new Error(`Fun\xE7\xE3o '${funcName}' n\xE3o encontrada no m\xF3dulo WASM`);
|
|
332
|
+
}
|
|
333
|
+
return this.exports[funcName](...args);
|
|
334
|
+
}
|
|
335
|
+
// Registra handler para evento JADE
|
|
336
|
+
on(event, handler) {
|
|
337
|
+
this.events.on(event, handler);
|
|
338
|
+
}
|
|
339
|
+
// Acesso ao gerenciador de memória (para testes e integração)
|
|
340
|
+
getMemory() {
|
|
341
|
+
return this.memory;
|
|
342
|
+
}
|
|
343
|
+
// Constrói o objeto de imports que o WASM recebe
|
|
344
|
+
buildImports() {
|
|
345
|
+
return {
|
|
346
|
+
jade: {
|
|
347
|
+
log_i32: (value) => {
|
|
348
|
+
if (this.debug) console.log("[JADE]", value);
|
|
349
|
+
},
|
|
350
|
+
log_f64: (value) => {
|
|
351
|
+
if (this.debug) console.log("[JADE]", value);
|
|
352
|
+
},
|
|
353
|
+
log_str: (ptr) => {
|
|
354
|
+
const str = this.memory.readString(ptr);
|
|
355
|
+
if (this.debug) console.log("[JADE]", str);
|
|
356
|
+
},
|
|
357
|
+
malloc: (size) => {
|
|
358
|
+
return this.memory.malloc(size);
|
|
359
|
+
},
|
|
360
|
+
free: (ptr) => {
|
|
361
|
+
this.memory.free(ptr);
|
|
362
|
+
},
|
|
363
|
+
erro: (msgPtr) => {
|
|
364
|
+
const msg = this.memory.readString(msgPtr);
|
|
365
|
+
throw new Error(`[JADE Erro] ${msg}`);
|
|
366
|
+
},
|
|
367
|
+
emitir_evento: (nomePtr, dadosPtr) => {
|
|
368
|
+
const nome = this.memory.readString(nomePtr);
|
|
369
|
+
this.events.emit(nome, dadosPtr);
|
|
370
|
+
if (this.debug) console.log(`[JADE Evento] ${nome}`);
|
|
371
|
+
},
|
|
372
|
+
lista_tamanho: (listaPtr) => {
|
|
373
|
+
const view = new DataView(this.memory.getMemory().buffer);
|
|
374
|
+
return view.getInt32(listaPtr, true);
|
|
375
|
+
},
|
|
376
|
+
lista_obter: (listaPtr, index) => {
|
|
377
|
+
const view = new DataView(this.memory.getMemory().buffer);
|
|
378
|
+
return view.getInt32(listaPtr + 4 + index * 4, true);
|
|
379
|
+
},
|
|
380
|
+
concat: (ptrA, ptrB) => {
|
|
381
|
+
const strA = this.memory.readString(ptrA);
|
|
382
|
+
const strB = this.memory.readString(ptrB);
|
|
383
|
+
const result = strA + strB;
|
|
384
|
+
const encoded = new TextEncoder().encode(result);
|
|
385
|
+
const ptr = this.memory.malloc(encoded.length + 1);
|
|
386
|
+
const bytes = new Uint8Array(this.memory.getMemory().buffer, ptr, encoded.length + 1);
|
|
387
|
+
bytes.set(encoded);
|
|
388
|
+
bytes[encoded.length] = 0;
|
|
389
|
+
return ptr;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
// ui/reactive.ts
|
|
397
|
+
var currentEffect = null;
|
|
398
|
+
var effectsByOwner = /* @__PURE__ */ new Map();
|
|
399
|
+
var currentOwner = null;
|
|
400
|
+
function setEffectOwner(owner) {
|
|
401
|
+
currentOwner = owner;
|
|
402
|
+
}
|
|
403
|
+
function disposeOwner(owner) {
|
|
404
|
+
for (const h of effectsByOwner.get(owner) ?? []) {
|
|
405
|
+
h.disposed = true;
|
|
406
|
+
}
|
|
407
|
+
effectsByOwner.delete(owner);
|
|
408
|
+
}
|
|
409
|
+
var Signal = class {
|
|
410
|
+
_value;
|
|
411
|
+
subs = /* @__PURE__ */ new Set();
|
|
412
|
+
constructor(initialValue) {
|
|
413
|
+
this._value = initialValue;
|
|
414
|
+
}
|
|
415
|
+
/** Lê o valor e registra o efeito atual como dependente. */
|
|
416
|
+
get() {
|
|
417
|
+
if (currentEffect) this.subs.add(currentEffect);
|
|
418
|
+
return this._value;
|
|
419
|
+
}
|
|
420
|
+
/** Atualiza o valor e re-executa todos os efeitos dependentes. */
|
|
421
|
+
set(newValue) {
|
|
422
|
+
if (newValue === this._value) return;
|
|
423
|
+
this._value = newValue;
|
|
424
|
+
for (const h of [...this.subs]) {
|
|
425
|
+
if (h.disposed) {
|
|
426
|
+
this.subs.delete(h);
|
|
427
|
+
} else {
|
|
428
|
+
h.fn();
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
/** Lê o valor sem registrar dependência. */
|
|
433
|
+
peek() {
|
|
434
|
+
return this._value;
|
|
435
|
+
}
|
|
436
|
+
};
|
|
437
|
+
function createEffect(fn) {
|
|
438
|
+
const handle = { fn: () => {
|
|
439
|
+
}, disposed: false };
|
|
440
|
+
const wrapped = () => {
|
|
441
|
+
if (handle.disposed) return;
|
|
442
|
+
const prev = currentEffect;
|
|
443
|
+
currentEffect = handle;
|
|
444
|
+
try {
|
|
445
|
+
fn();
|
|
446
|
+
} finally {
|
|
447
|
+
currentEffect = prev;
|
|
448
|
+
}
|
|
449
|
+
};
|
|
450
|
+
handle.fn = wrapped;
|
|
451
|
+
if (currentOwner !== null) {
|
|
452
|
+
const arr = effectsByOwner.get(currentOwner) ?? [];
|
|
453
|
+
arr.push(handle);
|
|
454
|
+
effectsByOwner.set(currentOwner, arr);
|
|
455
|
+
}
|
|
456
|
+
wrapped();
|
|
457
|
+
return () => {
|
|
458
|
+
handle.disposed = true;
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
var Store = class {
|
|
462
|
+
signals = /* @__PURE__ */ new Map();
|
|
463
|
+
set(key, value) {
|
|
464
|
+
if (this.signals.has(key)) {
|
|
465
|
+
this.signals.get(key).set(value);
|
|
466
|
+
} else {
|
|
467
|
+
this.signals.set(key, new Signal(value));
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
get(key, defaultValue) {
|
|
471
|
+
if (!this.signals.has(key)) {
|
|
472
|
+
this.signals.set(key, new Signal(defaultValue));
|
|
473
|
+
}
|
|
474
|
+
return this.signals.get(key);
|
|
475
|
+
}
|
|
476
|
+
has(key) {
|
|
477
|
+
return this.signals.has(key);
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* Remove todas as chaves com o prefixo indicado.
|
|
481
|
+
* CORREÇÃO: evita acúmulo de dados de telas antigas na memória.
|
|
482
|
+
* Exemplo: clearNamespace('tela-produtos.') remove só os dados dessa tela.
|
|
483
|
+
*/
|
|
484
|
+
clearNamespace(prefix) {
|
|
485
|
+
for (const key of this.signals.keys()) {
|
|
486
|
+
if (key.startsWith(prefix)) this.signals.delete(key);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
clear() {
|
|
490
|
+
this.signals.clear();
|
|
491
|
+
}
|
|
492
|
+
size() {
|
|
493
|
+
return this.signals.size;
|
|
494
|
+
}
|
|
495
|
+
};
|
|
496
|
+
|
|
497
|
+
// ui/binding.ts
|
|
498
|
+
function bind(signal, node, property) {
|
|
499
|
+
createEffect(() => {
|
|
500
|
+
const value = signal.get();
|
|
501
|
+
if (property === "textContent") {
|
|
502
|
+
node.textContent = String(value ?? "");
|
|
503
|
+
} else if (node instanceof HTMLElement) {
|
|
504
|
+
if (property === "style.display") {
|
|
505
|
+
node.style.display = value ? "" : "none";
|
|
506
|
+
} else if (property.startsWith("style.")) {
|
|
507
|
+
node.style[property.slice(6)] = String(value);
|
|
508
|
+
} else if (property === "disabled") {
|
|
509
|
+
node.disabled = Boolean(value);
|
|
510
|
+
} else if (property === "class") {
|
|
511
|
+
node.className = String(value ?? "");
|
|
512
|
+
} else {
|
|
513
|
+
node[property] = value;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
function bindInput(node, signal) {
|
|
519
|
+
bind(signal, node, "value");
|
|
520
|
+
node.addEventListener("input", () => signal.set(node.value));
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// ui/refs.ts
|
|
524
|
+
var RefManager = class {
|
|
525
|
+
refs = /* @__PURE__ */ new Map();
|
|
526
|
+
registrar(nome, elemento) {
|
|
527
|
+
this.refs.set(nome, elemento);
|
|
528
|
+
}
|
|
529
|
+
obter(nome) {
|
|
530
|
+
return this.refs.get(nome) ?? null;
|
|
531
|
+
}
|
|
532
|
+
focar(nome) {
|
|
533
|
+
const el = this.refs.get(nome);
|
|
534
|
+
if (!el) return;
|
|
535
|
+
if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) {
|
|
536
|
+
el.focus();
|
|
537
|
+
el.select();
|
|
538
|
+
} else {
|
|
539
|
+
el.focus();
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
rolar(nome, comportamento = "smooth") {
|
|
543
|
+
this.refs.get(nome)?.scrollIntoView({ behavior: comportamento, block: "nearest" });
|
|
544
|
+
}
|
|
545
|
+
limpar() {
|
|
546
|
+
this.refs.clear();
|
|
547
|
+
}
|
|
548
|
+
};
|
|
549
|
+
|
|
550
|
+
// ui/theme.ts
|
|
551
|
+
var temaDefault = {
|
|
552
|
+
cor_primaria: "#2563eb",
|
|
553
|
+
cor_secundaria: "#64748b",
|
|
554
|
+
cor_fundo: "#f8fafc",
|
|
555
|
+
cor_texto: "#1e293b",
|
|
556
|
+
cor_borda: "#e2e8f0",
|
|
557
|
+
fonte_principal: "system-ui, -apple-system, sans-serif",
|
|
558
|
+
raio_borda: "6px",
|
|
559
|
+
espacamento_pequeno: "4px",
|
|
560
|
+
espacamento_medio: "12px",
|
|
561
|
+
espacamento_grande: "24px"
|
|
562
|
+
};
|
|
563
|
+
function aplicarTema(tema = {}) {
|
|
564
|
+
const t = { ...temaDefault, ...tema };
|
|
565
|
+
document.getElementById("jade-theme")?.remove();
|
|
566
|
+
const style = document.createElement("style");
|
|
567
|
+
style.id = "jade-theme";
|
|
568
|
+
style.textContent = `
|
|
569
|
+
:root {
|
|
570
|
+
--jade-primaria: ${t.cor_primaria};
|
|
571
|
+
--jade-secundaria: ${t.cor_secundaria};
|
|
572
|
+
--jade-fundo: ${t.cor_fundo};
|
|
573
|
+
--jade-texto: ${t.cor_texto};
|
|
574
|
+
--jade-borda: ${t.cor_borda};
|
|
575
|
+
--jade-fonte: ${t.fonte_principal};
|
|
576
|
+
--jade-raio: ${t.raio_borda};
|
|
577
|
+
--jade-esp-p: ${t.espacamento_pequeno};
|
|
578
|
+
--jade-esp-m: ${t.espacamento_medio};
|
|
579
|
+
--jade-esp-g: ${t.espacamento_grande};
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
583
|
+
body { font-family: var(--jade-fonte); color: var(--jade-texto); background: var(--jade-fundo); }
|
|
584
|
+
|
|
585
|
+
/* Layout principal */
|
|
586
|
+
.jade-app { display: flex; flex-direction: column; min-height: 100vh; }
|
|
587
|
+
.jade-layout { display: flex; flex: 1; }
|
|
588
|
+
.jade-menu { width: 220px; background: #fff; border-right: 1px solid var(--jade-borda);
|
|
589
|
+
padding: var(--jade-esp-m); flex-shrink: 0; }
|
|
590
|
+
.jade-conteudo { flex: 1; padding: var(--jade-esp-g); overflow-y: auto; }
|
|
591
|
+
|
|
592
|
+
/* Menu lateral */
|
|
593
|
+
.jade-menu-item { display: block; padding: 8px var(--jade-esp-m); border-radius: var(--jade-raio);
|
|
594
|
+
text-decoration: none; color: var(--jade-texto); font-size: 14px; cursor: pointer;
|
|
595
|
+
transition: background 0.15s; }
|
|
596
|
+
.jade-menu-item:hover { background: var(--jade-fundo); }
|
|
597
|
+
.jade-menu-item.ativo { background: var(--jade-primaria); color: #fff; }
|
|
598
|
+
|
|
599
|
+
/* Tela */
|
|
600
|
+
.jade-tela { max-width: 1200px; }
|
|
601
|
+
.jade-tela-titulo { font-size: 22px; font-weight: 500; margin-bottom: var(--jade-esp-g);
|
|
602
|
+
color: var(--jade-texto); }
|
|
603
|
+
|
|
604
|
+
/* Tabela */
|
|
605
|
+
.jade-tabela-wrapper { width: 100%; }
|
|
606
|
+
.jade-tabela-controles { display: flex; gap: var(--jade-esp-m); margin-bottom: var(--jade-esp-m);
|
|
607
|
+
align-items: center; flex-wrap: wrap; }
|
|
608
|
+
.jade-tabela-busca { padding: 7px 12px; border: 1px solid var(--jade-borda);
|
|
609
|
+
border-radius: var(--jade-raio); font-size: 14px; font-family: var(--jade-fonte);
|
|
610
|
+
outline: none; min-width: 200px; }
|
|
611
|
+
.jade-tabela-busca:focus { border-color: var(--jade-primaria); }
|
|
612
|
+
.jade-tabela { width: 100%; border: 1px solid var(--jade-borda); border-radius: var(--jade-raio);
|
|
613
|
+
overflow: hidden; }
|
|
614
|
+
.jade-tabela table { width: 100%; border-collapse: collapse; }
|
|
615
|
+
.jade-tabela th { background: #f1f5f9; padding: 10px 14px; text-align: left; font-size: 13px;
|
|
616
|
+
font-weight: 500; color: var(--jade-secundaria); border-bottom: 1px solid var(--jade-borda);
|
|
617
|
+
white-space: nowrap; }
|
|
618
|
+
.jade-tabela th.ordenavel { cursor: pointer; user-select: none; }
|
|
619
|
+
.jade-tabela th.ordenavel:hover { background: #e2e8f0; }
|
|
620
|
+
.jade-tabela th .jade-sort-icon { margin-left: 4px; opacity: 0.4; }
|
|
621
|
+
.jade-tabela th.sort-asc .jade-sort-icon,
|
|
622
|
+
.jade-tabela th.sort-desc .jade-sort-icon { opacity: 1; }
|
|
623
|
+
.jade-tabela td { padding: 10px 14px; font-size: 14px; border-bottom: 1px solid var(--jade-borda); }
|
|
624
|
+
.jade-tabela tr:last-child td { border-bottom: none; }
|
|
625
|
+
.jade-tabela tr:hover td { background: #f8fafc; }
|
|
626
|
+
.jade-tabela-paginacao { display: flex; gap: var(--jade-esp-p); align-items: center;
|
|
627
|
+
padding: var(--jade-esp-m); justify-content: flex-end; border-top: 1px solid var(--jade-borda);
|
|
628
|
+
font-size: 13px; color: var(--jade-secundaria); }
|
|
629
|
+
.jade-pag-btn { padding: 4px 10px; border: 1px solid var(--jade-borda); border-radius: var(--jade-raio);
|
|
630
|
+
background: #fff; cursor: pointer; font-size: 13px; }
|
|
631
|
+
.jade-pag-btn:hover:not(:disabled) { background: var(--jade-fundo); }
|
|
632
|
+
.jade-pag-btn:disabled { opacity: 0.4; cursor: not-allowed; }
|
|
633
|
+
.jade-pag-btn.ativo { background: var(--jade-primaria); color: #fff; border-color: var(--jade-primaria); }
|
|
634
|
+
.jade-tabela-vazio { padding: 32px; text-align: center; color: var(--jade-secundaria);
|
|
635
|
+
font-size: 14px; }
|
|
636
|
+
|
|
637
|
+
/* Formul\xE1rio */
|
|
638
|
+
.jade-formulario { display: flex; flex-direction: column; gap: var(--jade-esp-m); max-width: 600px; }
|
|
639
|
+
.jade-campo { display: flex; flex-direction: column; gap: 4px; }
|
|
640
|
+
.jade-campo label { font-size: 13px; font-weight: 500; color: var(--jade-secundaria); }
|
|
641
|
+
.jade-campo input, .jade-campo select, .jade-campo textarea {
|
|
642
|
+
padding: 8px 12px; border: 1px solid var(--jade-borda); border-radius: var(--jade-raio);
|
|
643
|
+
font-size: 14px; font-family: var(--jade-fonte); outline: none;
|
|
644
|
+
transition: border-color 0.15s, box-shadow 0.15s; }
|
|
645
|
+
.jade-campo input:focus, .jade-campo select:focus, .jade-campo textarea:focus {
|
|
646
|
+
border-color: var(--jade-primaria); box-shadow: 0 0 0 3px rgba(37,99,235,0.1); }
|
|
647
|
+
.jade-campo-erro input, .jade-campo-erro select, .jade-campo-erro textarea {
|
|
648
|
+
border-color: #dc2626; }
|
|
649
|
+
.jade-campo-msg-erro { font-size: 12px; color: #dc2626; margin-top: 2px; }
|
|
650
|
+
|
|
651
|
+
/* Bot\xF5es */
|
|
652
|
+
.jade-botao { padding: 8px 18px; border-radius: var(--jade-raio); font-size: 14px;
|
|
653
|
+
font-family: var(--jade-fonte); cursor: pointer; border: none; font-weight: 500;
|
|
654
|
+
transition: background 0.15s; display: inline-flex; align-items: center; gap: 6px; }
|
|
655
|
+
.jade-botao-primario { background: var(--jade-primaria); color: #fff; }
|
|
656
|
+
.jade-botao-primario:hover:not(:disabled) { background: #1d4ed8; }
|
|
657
|
+
.jade-botao-secundario { background: #fff; color: var(--jade-texto); border: 1px solid var(--jade-borda); }
|
|
658
|
+
.jade-botao-secundario:hover:not(:disabled) { background: var(--jade-fundo); }
|
|
659
|
+
.jade-botao-perigo { background: #dc2626; color: #fff; }
|
|
660
|
+
.jade-botao-perigo:hover:not(:disabled) { background: #b91c1c; }
|
|
661
|
+
.jade-botao:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
662
|
+
.jade-botoes { display: flex; gap: var(--jade-esp-m); margin-top: var(--jade-esp-m); flex-wrap: wrap; }
|
|
663
|
+
|
|
664
|
+
/* Card de m\xE9trica */
|
|
665
|
+
.jade-card { background: #fff; border: 1px solid var(--jade-borda); border-radius: var(--jade-raio);
|
|
666
|
+
padding: var(--jade-esp-g); }
|
|
667
|
+
.jade-card-titulo { font-size: 14px; font-weight: 500; color: var(--jade-secundaria); margin-bottom: 8px; }
|
|
668
|
+
.jade-card-valor { font-size: 28px; font-weight: 500; color: var(--jade-texto); }
|
|
669
|
+
|
|
670
|
+
/* Grid responsivo */
|
|
671
|
+
.jade-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
672
|
+
gap: var(--jade-esp-m); margin-bottom: var(--jade-esp-g); }
|
|
673
|
+
|
|
674
|
+
/* Badge de status */
|
|
675
|
+
.jade-badge { display: inline-block; padding: 2px 8px; border-radius: 9999px;
|
|
676
|
+
font-size: 12px; font-weight: 500; }
|
|
677
|
+
.jade-badge-sucesso { background: #dcfce7; color: #166534; }
|
|
678
|
+
.jade-badge-aviso { background: #fef9c3; color: #854d0e; }
|
|
679
|
+
.jade-badge-erro { background: #fee2e2; color: #991b1b; }
|
|
680
|
+
.jade-badge-info { background: #dbeafe; color: #1e40af; }
|
|
681
|
+
|
|
682
|
+
/* Acesso negado */
|
|
683
|
+
.jade-acesso-negado { padding: 40px; text-align: center; color: #dc2626; font-size: 16px; }
|
|
684
|
+
|
|
685
|
+
/* \u2500\u2500 Skeleton / Loading \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
686
|
+
/* Anima\xE7\xE3o de "brilho" para indicar conte\xFAdo carregando */
|
|
687
|
+
@keyframes jade-shimmer {
|
|
688
|
+
0% { background-position: -400px 0; }
|
|
689
|
+
100% { background-position: 400px 0; }
|
|
690
|
+
}
|
|
691
|
+
.jade-skeleton {
|
|
692
|
+
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
|
|
693
|
+
background-size: 400px 100%;
|
|
694
|
+
animation: jade-shimmer 1.4s ease-in-out infinite;
|
|
695
|
+
border-radius: var(--jade-raio);
|
|
696
|
+
}
|
|
697
|
+
.jade-skeleton-linha { height: 16px; margin-bottom: 8px; }
|
|
698
|
+
.jade-skeleton-titulo { height: 24px; width: 40%; margin-bottom: var(--jade-esp-m); }
|
|
699
|
+
.jade-skeleton-tabela-linha { height: 41px; margin-bottom: 1px; }
|
|
700
|
+
.jade-carregando { display: flex; flex-direction: column; gap: 8px; padding: var(--jade-esp-m); }
|
|
701
|
+
|
|
702
|
+
/* \u2500\u2500 Toast / Notifica\xE7\xF5es \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
703
|
+
#jade-toasts { position: fixed; top: 20px; right: 20px; z-index: 9999;
|
|
704
|
+
display: flex; flex-direction: column; gap: 8px; pointer-events: none; }
|
|
705
|
+
.jade-toast { padding: 12px 16px; border-radius: var(--jade-raio); font-size: 14px;
|
|
706
|
+
font-family: var(--jade-fonte); color: #fff; max-width: 340px; box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
|
707
|
+
pointer-events: auto; display: flex; align-items: center; gap: 8px;
|
|
708
|
+
animation: jade-toast-in 0.25s ease; }
|
|
709
|
+
@keyframes jade-toast-in { from { transform: translateX(20px); opacity: 0; }
|
|
710
|
+
to { transform: translateX(0); opacity: 1; } }
|
|
711
|
+
.jade-toast-saindo { animation: jade-toast-out 0.25s ease forwards; }
|
|
712
|
+
@keyframes jade-toast-out { to { transform: translateX(20px); opacity: 0; } }
|
|
713
|
+
.jade-toast-sucesso { background: #16a34a; }
|
|
714
|
+
.jade-toast-erro { background: #dc2626; }
|
|
715
|
+
.jade-toast-aviso { background: #d97706; }
|
|
716
|
+
.jade-toast-info { background: var(--jade-primaria); }
|
|
717
|
+
|
|
718
|
+
/* Responsivo */
|
|
719
|
+
@media (max-width: 768px) {
|
|
720
|
+
.jade-menu { display: none; }
|
|
721
|
+
.jade-conteudo { padding: var(--jade-esp-m); }
|
|
722
|
+
.jade-grid { grid-template-columns: 1fr; }
|
|
723
|
+
.jade-tela-titulo { font-size: 18px; }
|
|
724
|
+
}
|
|
725
|
+
`;
|
|
726
|
+
document.head.appendChild(style);
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
// ui/router.ts
|
|
730
|
+
var Router = class {
|
|
731
|
+
constructor(store, memory) {
|
|
732
|
+
this.store = store;
|
|
733
|
+
this.memory = memory;
|
|
734
|
+
}
|
|
735
|
+
rotas = /* @__PURE__ */ new Map();
|
|
736
|
+
handlers = /* @__PURE__ */ new Map();
|
|
737
|
+
telaAtiva = null;
|
|
738
|
+
container = null;
|
|
739
|
+
usuarioAtual = null;
|
|
740
|
+
/**
|
|
741
|
+
* Define o usuário logado. Necessário para verificar permissões de tela.
|
|
742
|
+
* Passar null para fazer logout (sem usuário = sem acesso a rotas protegidas).
|
|
743
|
+
*/
|
|
744
|
+
setUsuario(usuario) {
|
|
745
|
+
this.usuarioAtual = usuario;
|
|
746
|
+
}
|
|
747
|
+
/** Registra uma rota com seu handler de renderização. */
|
|
748
|
+
registrar(caminho, tela, handler, requerPapel) {
|
|
749
|
+
this.rotas.set(caminho, { caminho, tela, requerPapel });
|
|
750
|
+
this.handlers.set(tela, handler);
|
|
751
|
+
}
|
|
752
|
+
/**
|
|
753
|
+
* Monta o router em um container e renderiza a rota atual.
|
|
754
|
+
* CORREÇÃO: o briefing original só escutava `popstate` mas não renderizava
|
|
755
|
+
* a rota inicial — a tela aparecia em branco ao abrir o app.
|
|
756
|
+
*/
|
|
757
|
+
montar(container) {
|
|
758
|
+
this.container = container;
|
|
759
|
+
window.addEventListener("popstate", () => this.renderRota(location.pathname));
|
|
760
|
+
this.renderRota(location.pathname);
|
|
761
|
+
}
|
|
762
|
+
/** Navega para uma rota via History API. */
|
|
763
|
+
navegar(caminho) {
|
|
764
|
+
history.pushState({}, "", caminho);
|
|
765
|
+
this.renderRota(caminho);
|
|
766
|
+
}
|
|
767
|
+
renderRota(caminho) {
|
|
768
|
+
const rota = this.rotas.get(caminho);
|
|
769
|
+
if (!rota || !this.container) return;
|
|
770
|
+
if (rota.requerPapel) {
|
|
771
|
+
const papeis = this.usuarioAtual?.roles ?? [];
|
|
772
|
+
if (!papeis.includes(rota.requerPapel)) {
|
|
773
|
+
const p = document.createElement("p");
|
|
774
|
+
p.className = "jade-acesso-negado";
|
|
775
|
+
p.textContent = "Acesso negado: voc\xEA n\xE3o tem permiss\xE3o para acessar esta tela.";
|
|
776
|
+
this.container.innerHTML = "";
|
|
777
|
+
this.container.appendChild(p);
|
|
778
|
+
return;
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
if (this.telaAtiva) {
|
|
782
|
+
disposeOwner(this.telaAtiva);
|
|
783
|
+
this.memory.freeOwner(this.telaAtiva);
|
|
784
|
+
}
|
|
785
|
+
this.telaAtiva = rota.tela;
|
|
786
|
+
this.store.set("rota.ativa", caminho);
|
|
787
|
+
const handler = this.handlers.get(rota.tela);
|
|
788
|
+
if (handler) {
|
|
789
|
+
setEffectOwner(rota.tela);
|
|
790
|
+
this.container.innerHTML = "";
|
|
791
|
+
this.container.appendChild(handler());
|
|
792
|
+
setEffectOwner(null);
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
rotaAtiva() {
|
|
796
|
+
return this.telaAtiva;
|
|
797
|
+
}
|
|
798
|
+
};
|
|
799
|
+
|
|
800
|
+
// ui/responsive.ts
|
|
801
|
+
var BP_MOBILE = 640;
|
|
802
|
+
var CSS_ID = "jade-mobile-first";
|
|
803
|
+
var CSS_BASE = `
|
|
804
|
+
/* \u2500\u2500 Reset e base mobile-first \u2500\u2500 */
|
|
805
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
806
|
+
|
|
807
|
+
body {
|
|
808
|
+
font-family: system-ui, -apple-system, 'Segoe UI', sans-serif;
|
|
809
|
+
font-size: 16px;
|
|
810
|
+
line-height: 1.5;
|
|
811
|
+
background: var(--jade-fundo, #f9fafb);
|
|
812
|
+
color: var(--jade-texto, #111827);
|
|
813
|
+
-webkit-text-size-adjust: 100%;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
/* \u2500\u2500 Tela \u2500\u2500 */
|
|
817
|
+
.jade-tela {
|
|
818
|
+
padding: 16px;
|
|
819
|
+
max-width: 100%;
|
|
820
|
+
}
|
|
821
|
+
.jade-tela-titulo {
|
|
822
|
+
font-size: 1.25rem;
|
|
823
|
+
font-weight: 700;
|
|
824
|
+
margin-bottom: 16px;
|
|
825
|
+
color: var(--jade-texto, #111827);
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
/* \u2500\u2500 Toque m\xEDnimo 44px (briefing \xA72.1) \u2500\u2500 */
|
|
829
|
+
.jade-botao,
|
|
830
|
+
button,
|
|
831
|
+
[role="button"],
|
|
832
|
+
input[type="submit"],
|
|
833
|
+
input[type="button"] {
|
|
834
|
+
min-height: 44px;
|
|
835
|
+
min-width: 44px;
|
|
836
|
+
padding: 10px 20px;
|
|
837
|
+
font-size: 1rem;
|
|
838
|
+
border-radius: 8px;
|
|
839
|
+
border: none;
|
|
840
|
+
cursor: pointer;
|
|
841
|
+
display: inline-flex;
|
|
842
|
+
align-items: center;
|
|
843
|
+
justify-content: center;
|
|
844
|
+
gap: 8px;
|
|
845
|
+
touch-action: manipulation;
|
|
846
|
+
user-select: none;
|
|
847
|
+
-webkit-tap-highlight-color: transparent;
|
|
848
|
+
}
|
|
849
|
+
.jade-botao-primario { background: var(--jade-primaria, #2563eb); color: #fff; }
|
|
850
|
+
.jade-botao-secundario { background: transparent; border: 2px solid var(--jade-primaria, #2563eb); color: var(--jade-primaria, #2563eb); }
|
|
851
|
+
.jade-botao-perigo { background: #dc2626; color: #fff; }
|
|
852
|
+
.jade-botao:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
853
|
+
|
|
854
|
+
/* \u2500\u2500 Lista de cards (mobile \u2014 padr\xE3o base) \u2500\u2500 */
|
|
855
|
+
.jade-lista-cards {
|
|
856
|
+
display: flex;
|
|
857
|
+
flex-direction: column;
|
|
858
|
+
gap: 12px;
|
|
859
|
+
}
|
|
860
|
+
.jade-card-item {
|
|
861
|
+
background: #fff;
|
|
862
|
+
border-radius: 12px;
|
|
863
|
+
padding: 16px;
|
|
864
|
+
box-shadow: 0 1px 3px rgba(0,0,0,.08);
|
|
865
|
+
display: grid;
|
|
866
|
+
grid-template-columns: 1fr;
|
|
867
|
+
gap: 8px;
|
|
868
|
+
}
|
|
869
|
+
.jade-card-campo {
|
|
870
|
+
display: flex;
|
|
871
|
+
justify-content: space-between;
|
|
872
|
+
align-items: center;
|
|
873
|
+
gap: 8px;
|
|
874
|
+
font-size: 0.9375rem;
|
|
875
|
+
}
|
|
876
|
+
.jade-campo-label {
|
|
877
|
+
font-weight: 600;
|
|
878
|
+
color: var(--jade-texto-suave, #6b7280);
|
|
879
|
+
white-space: nowrap;
|
|
880
|
+
}
|
|
881
|
+
.jade-campo-valor {
|
|
882
|
+
color: var(--jade-texto, #111827);
|
|
883
|
+
text-align: right;
|
|
884
|
+
word-break: break-word;
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
/* \u2500\u2500 Grid de tabela (oculto no mobile \u2014 mostrado no desktop) \u2500\u2500 */
|
|
888
|
+
.jade-tabela-grid { display: none; }
|
|
889
|
+
|
|
890
|
+
/* \u2500\u2500 Controles de tabela \u2500\u2500 */
|
|
891
|
+
.jade-tabela-controles { margin-bottom: 12px; }
|
|
892
|
+
.jade-tabela-busca {
|
|
893
|
+
width: 100%;
|
|
894
|
+
min-height: 44px;
|
|
895
|
+
padding: 10px 14px;
|
|
896
|
+
border: 1.5px solid var(--jade-borda, #d1d5db);
|
|
897
|
+
border-radius: 8px;
|
|
898
|
+
font-size: 1rem;
|
|
899
|
+
background: #fff;
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
/* \u2500\u2500 Pagina\xE7\xE3o \u2500\u2500 */
|
|
903
|
+
.jade-tabela-paginacao {
|
|
904
|
+
display: flex;
|
|
905
|
+
align-items: center;
|
|
906
|
+
justify-content: center;
|
|
907
|
+
gap: 8px;
|
|
908
|
+
padding: 12px 0;
|
|
909
|
+
flex-wrap: wrap;
|
|
910
|
+
}
|
|
911
|
+
.jade-pag-btn {
|
|
912
|
+
min-height: 44px;
|
|
913
|
+
min-width: 44px;
|
|
914
|
+
padding: 8px 14px;
|
|
915
|
+
border: 1.5px solid var(--jade-borda, #d1d5db);
|
|
916
|
+
border-radius: 8px;
|
|
917
|
+
background: #fff;
|
|
918
|
+
cursor: pointer;
|
|
919
|
+
font-size: 0.9375rem;
|
|
920
|
+
}
|
|
921
|
+
.jade-pag-btn.ativo { background: var(--jade-primaria, #2563eb); color: #fff; border-color: transparent; }
|
|
922
|
+
.jade-pag-btn:disabled { opacity: 0.4; cursor: not-allowed; }
|
|
923
|
+
|
|
924
|
+
/* \u2500\u2500 Formul\xE1rio \u2500\u2500 */
|
|
925
|
+
.jade-formulario { display: flex; flex-direction: column; gap: 16px; }
|
|
926
|
+
.jade-campo { display: flex; flex-direction: column; gap: 6px; }
|
|
927
|
+
.jade-campo label { font-weight: 600; font-size: 0.9375rem; }
|
|
928
|
+
.jade-campo input,
|
|
929
|
+
.jade-campo select,
|
|
930
|
+
.jade-campo textarea {
|
|
931
|
+
min-height: 44px;
|
|
932
|
+
padding: 10px 14px;
|
|
933
|
+
border: 1.5px solid var(--jade-borda, #d1d5db);
|
|
934
|
+
border-radius: 8px;
|
|
935
|
+
font-size: 1rem;
|
|
936
|
+
background: #fff;
|
|
937
|
+
width: 100%;
|
|
938
|
+
}
|
|
939
|
+
.jade-campo-msg-erro { font-size: 0.875rem; color: #dc2626; min-height: 1.25em; }
|
|
940
|
+
|
|
941
|
+
/* \u2500\u2500 Card de m\xE9trica \u2500\u2500 */
|
|
942
|
+
.jade-card {
|
|
943
|
+
background: #fff;
|
|
944
|
+
border-radius: 12px;
|
|
945
|
+
padding: 20px;
|
|
946
|
+
box-shadow: 0 1px 3px rgba(0,0,0,.08);
|
|
947
|
+
}
|
|
948
|
+
.jade-card-titulo { font-size: 0.875rem; color: var(--jade-texto-suave, #6b7280); margin-bottom: 8px; }
|
|
949
|
+
.jade-card-valor { font-size: 1.75rem; font-weight: 700; }
|
|
950
|
+
|
|
951
|
+
/* \u2500\u2500 Toast \u2500\u2500 */
|
|
952
|
+
#jade-toasts {
|
|
953
|
+
position: fixed;
|
|
954
|
+
bottom: 16px;
|
|
955
|
+
left: 50%;
|
|
956
|
+
transform: translateX(-50%);
|
|
957
|
+
z-index: 9999;
|
|
958
|
+
display: flex;
|
|
959
|
+
flex-direction: column;
|
|
960
|
+
gap: 8px;
|
|
961
|
+
width: min(calc(100vw - 32px), 400px);
|
|
962
|
+
}
|
|
963
|
+
.jade-toast {
|
|
964
|
+
padding: 14px 18px;
|
|
965
|
+
border-radius: 10px;
|
|
966
|
+
font-size: 0.9375rem;
|
|
967
|
+
display: flex;
|
|
968
|
+
align-items: center;
|
|
969
|
+
gap: 10px;
|
|
970
|
+
box-shadow: 0 4px 16px rgba(0,0,0,.15);
|
|
971
|
+
animation: jade-toast-entrar 0.2s ease;
|
|
972
|
+
}
|
|
973
|
+
.jade-toast-saindo { animation: jade-toast-sair 0.2s ease forwards; }
|
|
974
|
+
.jade-toast-sucesso { background: #dcfce7; color: #166534; }
|
|
975
|
+
.jade-toast-erro { background: #fee2e2; color: #991b1b; }
|
|
976
|
+
.jade-toast-aviso { background: #fef9c3; color: #854d0e; }
|
|
977
|
+
.jade-toast-info { background: #dbeafe; color: #1e40af; }
|
|
978
|
+
@keyframes jade-toast-entrar { from { opacity:0; transform:translateY(8px); } to { opacity:1; transform:translateY(0); } }
|
|
979
|
+
@keyframes jade-toast-sair { to { opacity:0; transform:translateY(8px); } }
|
|
980
|
+
|
|
981
|
+
/* \u2500\u2500 Skeleton \u2500\u2500 */
|
|
982
|
+
.jade-skeleton {
|
|
983
|
+
background: linear-gradient(90deg, #f3f4f6 25%, #e5e7eb 50%, #f3f4f6 75%);
|
|
984
|
+
background-size: 200% 100%;
|
|
985
|
+
animation: jade-shimmer 1.4s infinite;
|
|
986
|
+
border-radius: 6px;
|
|
987
|
+
}
|
|
988
|
+
.jade-skeleton-titulo { height: 28px; width: 40%; margin-bottom: 16px; }
|
|
989
|
+
.jade-skeleton-linha { height: 56px; margin-bottom: 8px; }
|
|
990
|
+
@keyframes jade-shimmer { to { background-position: -200% 0; } }
|
|
991
|
+
|
|
992
|
+
/* \u2500\u2500 Vazio \u2500\u2500 */
|
|
993
|
+
.jade-tabela-vazio { text-align: center; padding: 32px; color: var(--jade-texto-suave, #6b7280); }
|
|
994
|
+
|
|
995
|
+
/* \u2500\u2500 Navega\xE7\xE3o mobile: bottom bar \u2500\u2500 */
|
|
996
|
+
.jade-nav-bottom {
|
|
997
|
+
position: fixed;
|
|
998
|
+
bottom: 0; left: 0; right: 0;
|
|
999
|
+
background: #fff;
|
|
1000
|
+
border-top: 1px solid var(--jade-borda, #e5e7eb);
|
|
1001
|
+
display: flex;
|
|
1002
|
+
z-index: 100;
|
|
1003
|
+
padding-bottom: env(safe-area-inset-bottom, 0px);
|
|
1004
|
+
}
|
|
1005
|
+
.jade-nav-item {
|
|
1006
|
+
flex: 1;
|
|
1007
|
+
display: flex;
|
|
1008
|
+
flex-direction: column;
|
|
1009
|
+
align-items: center;
|
|
1010
|
+
justify-content: center;
|
|
1011
|
+
min-height: 56px;
|
|
1012
|
+
font-size: 0.75rem;
|
|
1013
|
+
color: var(--jade-texto-suave, #6b7280);
|
|
1014
|
+
gap: 4px;
|
|
1015
|
+
text-decoration: none;
|
|
1016
|
+
cursor: pointer;
|
|
1017
|
+
-webkit-tap-highlight-color: transparent;
|
|
1018
|
+
}
|
|
1019
|
+
.jade-nav-item.ativo { color: var(--jade-primaria, #2563eb); }
|
|
1020
|
+
.jade-nav-icone { font-size: 1.4rem; line-height: 1; }
|
|
1021
|
+
|
|
1022
|
+
/* \u2500\u2500 Hamb\xFArguer (oculto no mobile por padr\xE3o \u2014 nav usa bottom bar) \u2500\u2500 */
|
|
1023
|
+
.jade-nav-lateral { display: none; }
|
|
1024
|
+
.jade-nav-topo { display: none; }
|
|
1025
|
+
|
|
1026
|
+
/* \u2500\u2500 Desktop: a partir de 640px \u2500\u2500 */
|
|
1027
|
+
@media (min-width: 640px) {
|
|
1028
|
+
.jade-tela { padding: 24px; max-width: 1200px; margin: 0 auto; }
|
|
1029
|
+
.jade-tela-titulo { font-size: 1.5rem; }
|
|
1030
|
+
|
|
1031
|
+
/* Tabela: oculta lista de cards, mostra grid */
|
|
1032
|
+
.jade-lista-cards { display: none; }
|
|
1033
|
+
.jade-tabela-grid {
|
|
1034
|
+
display: table;
|
|
1035
|
+
width: 100%;
|
|
1036
|
+
border-collapse: collapse;
|
|
1037
|
+
background: #fff;
|
|
1038
|
+
border-radius: 12px;
|
|
1039
|
+
overflow: hidden;
|
|
1040
|
+
box-shadow: 0 1px 3px rgba(0,0,0,.08);
|
|
1041
|
+
}
|
|
1042
|
+
.jade-tabela-grid th,
|
|
1043
|
+
.jade-tabela-grid td {
|
|
1044
|
+
padding: 12px 16px;
|
|
1045
|
+
text-align: left;
|
|
1046
|
+
border-bottom: 1px solid var(--jade-borda, #f3f4f6);
|
|
1047
|
+
font-size: 0.9375rem;
|
|
1048
|
+
}
|
|
1049
|
+
.jade-tabela-grid th {
|
|
1050
|
+
background: #f9fafb;
|
|
1051
|
+
font-weight: 600;
|
|
1052
|
+
color: var(--jade-texto-suave, #6b7280);
|
|
1053
|
+
white-space: nowrap;
|
|
1054
|
+
}
|
|
1055
|
+
.jade-tabela-grid th.ordenavel { cursor: pointer; user-select: none; }
|
|
1056
|
+
.jade-tabela-grid th.ordenavel:hover { background: #f3f4f6; }
|
|
1057
|
+
.jade-tabela-grid tbody tr:hover { background: #fafafa; }
|
|
1058
|
+
.jade-tabela-grid .jade-sort-icon { margin-left: 4px; opacity: 0.5; }
|
|
1059
|
+
|
|
1060
|
+
/* Bottom nav oculta no desktop */
|
|
1061
|
+
.jade-nav-bottom { display: none; }
|
|
1062
|
+
|
|
1063
|
+
/* Formul\xE1rio em grid */
|
|
1064
|
+
.jade-formulario { display: grid; grid-template-columns: repeat(2, 1fr); gap: 20px; }
|
|
1065
|
+
}
|
|
1066
|
+
`;
|
|
1067
|
+
var Responsivo = class {
|
|
1068
|
+
mql;
|
|
1069
|
+
callbacks = [];
|
|
1070
|
+
constructor() {
|
|
1071
|
+
this.mql = window.matchMedia(`(max-width: ${BP_MOBILE - 1}px)`);
|
|
1072
|
+
this.mql.addEventListener("change", (e) => {
|
|
1073
|
+
this.callbacks.forEach((cb) => cb(e.matches));
|
|
1074
|
+
});
|
|
1075
|
+
}
|
|
1076
|
+
isMobile() {
|
|
1077
|
+
return this.mql.matches;
|
|
1078
|
+
}
|
|
1079
|
+
/** Registra callback para mudança de breakpoint. Retorna função de cleanup. */
|
|
1080
|
+
observar(cb) {
|
|
1081
|
+
this.callbacks.push(cb);
|
|
1082
|
+
return () => {
|
|
1083
|
+
this.callbacks = this.callbacks.filter((c) => c !== cb);
|
|
1084
|
+
};
|
|
1085
|
+
}
|
|
1086
|
+
/**
|
|
1087
|
+
* Adapta uma tabela automaticamente:
|
|
1088
|
+
* mobile → lista de cards empilhados
|
|
1089
|
+
* desktop → grid com colunas
|
|
1090
|
+
* Troca automaticamente quando o viewport muda.
|
|
1091
|
+
*/
|
|
1092
|
+
adaptarTabela(config, wrapper, dados, termoBusca, paginaAtual) {
|
|
1093
|
+
const renderMobile = () => this._renderLista(config, wrapper, dados, termoBusca, paginaAtual);
|
|
1094
|
+
const renderDesktop = () => this._renderGrid(config, wrapper, dados, termoBusca, paginaAtual);
|
|
1095
|
+
const render = () => this.isMobile() ? renderMobile() : renderDesktop();
|
|
1096
|
+
render();
|
|
1097
|
+
this.observar(() => render());
|
|
1098
|
+
}
|
|
1099
|
+
/** Cria navegação adaptativa:
|
|
1100
|
+
* mobile → bottom navigation bar
|
|
1101
|
+
* desktop → sidebar ou topbar (hidden, gerenciado pelo Router)
|
|
1102
|
+
*/
|
|
1103
|
+
criarNavegacao(container, itens) {
|
|
1104
|
+
const nav = document.createElement("nav");
|
|
1105
|
+
nav.className = "jade-nav-bottom";
|
|
1106
|
+
nav.setAttribute("role", "navigation");
|
|
1107
|
+
nav.setAttribute("aria-label", "Navega\xE7\xE3o principal");
|
|
1108
|
+
for (const item of itens) {
|
|
1109
|
+
const a = document.createElement("a");
|
|
1110
|
+
a.className = "jade-nav-item" + (item.ativo ? " ativo" : "");
|
|
1111
|
+
a.href = item.caminho;
|
|
1112
|
+
a.addEventListener("click", (e) => {
|
|
1113
|
+
e.preventDefault();
|
|
1114
|
+
nav.querySelectorAll(".jade-nav-item").forEach((el) => el.classList.remove("ativo"));
|
|
1115
|
+
a.classList.add("ativo");
|
|
1116
|
+
window.history.pushState({}, "", item.caminho);
|
|
1117
|
+
window.dispatchEvent(new PopStateEvent("popstate"));
|
|
1118
|
+
});
|
|
1119
|
+
if (item.icone) {
|
|
1120
|
+
const icone = document.createElement("span");
|
|
1121
|
+
icone.className = "jade-nav-icone";
|
|
1122
|
+
icone.textContent = item.icone;
|
|
1123
|
+
a.appendChild(icone);
|
|
1124
|
+
}
|
|
1125
|
+
const label = document.createElement("span");
|
|
1126
|
+
label.textContent = item.label;
|
|
1127
|
+
a.appendChild(label);
|
|
1128
|
+
nav.appendChild(a);
|
|
1129
|
+
}
|
|
1130
|
+
container.appendChild(nav);
|
|
1131
|
+
return nav;
|
|
1132
|
+
}
|
|
1133
|
+
/** Injeta o CSS mobile-first base no <head> (idempotente). */
|
|
1134
|
+
injetarEstilos() {
|
|
1135
|
+
if (typeof document === "undefined") return;
|
|
1136
|
+
if (document.getElementById(CSS_ID)) return;
|
|
1137
|
+
const style = document.createElement("style");
|
|
1138
|
+
style.id = CSS_ID;
|
|
1139
|
+
style.textContent = CSS_BASE;
|
|
1140
|
+
document.head.appendChild(style);
|
|
1141
|
+
}
|
|
1142
|
+
// ── Renderização interna ────────────────────────────────────────────────────
|
|
1143
|
+
_renderLista(config, wrapper, dados, termoBusca, paginaAtual) {
|
|
1144
|
+
wrapper.querySelector(".jade-tabela-grid-wrapper")?.remove();
|
|
1145
|
+
let listaEl = wrapper.querySelector(".jade-lista-cards");
|
|
1146
|
+
if (!listaEl) {
|
|
1147
|
+
listaEl = document.createElement("div");
|
|
1148
|
+
listaEl.className = "jade-lista-cards";
|
|
1149
|
+
wrapper.appendChild(listaEl);
|
|
1150
|
+
}
|
|
1151
|
+
createEffect(() => {
|
|
1152
|
+
const termo = termoBusca.get();
|
|
1153
|
+
paginaAtual.get();
|
|
1154
|
+
const linhas = this._filtrarOrdenar(dados, config, termo, null, "asc");
|
|
1155
|
+
listaEl.innerHTML = "";
|
|
1156
|
+
if (linhas.length === 0) {
|
|
1157
|
+
const vazio = document.createElement("p");
|
|
1158
|
+
vazio.className = "jade-tabela-vazio";
|
|
1159
|
+
vazio.textContent = "Nenhum registro encontrado.";
|
|
1160
|
+
listaEl.appendChild(vazio);
|
|
1161
|
+
return;
|
|
1162
|
+
}
|
|
1163
|
+
linhas.forEach((item) => {
|
|
1164
|
+
const card = document.createElement("div");
|
|
1165
|
+
card.className = "jade-card-item";
|
|
1166
|
+
config.colunas.forEach((col) => {
|
|
1167
|
+
const campo = document.createElement("div");
|
|
1168
|
+
campo.className = "jade-card-campo";
|
|
1169
|
+
const labelEl = document.createElement("span");
|
|
1170
|
+
labelEl.className = "jade-campo-label";
|
|
1171
|
+
labelEl.textContent = col.titulo;
|
|
1172
|
+
const valorEl = document.createElement("span");
|
|
1173
|
+
valorEl.className = "jade-campo-valor";
|
|
1174
|
+
valorEl.textContent = String(item[col.campo] ?? "");
|
|
1175
|
+
campo.appendChild(labelEl);
|
|
1176
|
+
campo.appendChild(valorEl);
|
|
1177
|
+
card.appendChild(campo);
|
|
1178
|
+
});
|
|
1179
|
+
listaEl.appendChild(card);
|
|
1180
|
+
});
|
|
1181
|
+
});
|
|
1182
|
+
}
|
|
1183
|
+
_renderGrid(config, wrapper, dados, termoBusca, paginaAtual) {
|
|
1184
|
+
wrapper.querySelector(".jade-lista-cards")?.remove();
|
|
1185
|
+
let gridWrapper = wrapper.querySelector(".jade-tabela-grid-wrapper");
|
|
1186
|
+
if (gridWrapper) return;
|
|
1187
|
+
gridWrapper = document.createElement("div");
|
|
1188
|
+
gridWrapper.className = "jade-tabela-grid-wrapper";
|
|
1189
|
+
const campOrdem = new Signal(null);
|
|
1190
|
+
const direcaoOrdem = new Signal("asc");
|
|
1191
|
+
const table = document.createElement("table");
|
|
1192
|
+
table.className = "jade-tabela-grid";
|
|
1193
|
+
const thead = document.createElement("thead");
|
|
1194
|
+
const headerRow = document.createElement("tr");
|
|
1195
|
+
config.colunas.forEach((col) => {
|
|
1196
|
+
const th = document.createElement("th");
|
|
1197
|
+
th.textContent = col.titulo;
|
|
1198
|
+
th.className = "ordenavel";
|
|
1199
|
+
const sortIcon = document.createElement("span");
|
|
1200
|
+
sortIcon.className = "jade-sort-icon";
|
|
1201
|
+
sortIcon.textContent = "\u2195";
|
|
1202
|
+
th.appendChild(sortIcon);
|
|
1203
|
+
th.addEventListener("click", () => {
|
|
1204
|
+
if (campOrdem.peek() === col.campo) {
|
|
1205
|
+
direcaoOrdem.set(direcaoOrdem.peek() === "asc" ? "desc" : "asc");
|
|
1206
|
+
} else {
|
|
1207
|
+
campOrdem.set(col.campo);
|
|
1208
|
+
direcaoOrdem.set("asc");
|
|
1209
|
+
}
|
|
1210
|
+
headerRow.querySelectorAll("th").forEach((t) => t.classList.remove("sort-asc", "sort-desc"));
|
|
1211
|
+
th.classList.add(direcaoOrdem.peek() === "asc" ? "sort-asc" : "sort-desc");
|
|
1212
|
+
sortIcon.textContent = direcaoOrdem.peek() === "asc" ? "\u2191" : "\u2193";
|
|
1213
|
+
paginaAtual.set(0);
|
|
1214
|
+
});
|
|
1215
|
+
headerRow.appendChild(th);
|
|
1216
|
+
});
|
|
1217
|
+
thead.appendChild(headerRow);
|
|
1218
|
+
table.appendChild(thead);
|
|
1219
|
+
const tbody = document.createElement("tbody");
|
|
1220
|
+
table.appendChild(tbody);
|
|
1221
|
+
gridWrapper.appendChild(table);
|
|
1222
|
+
const linhasPorPagina = config.paginacao === true ? 20 : typeof config.paginacao === "number" ? config.paginacao : 0;
|
|
1223
|
+
let paginacaoDiv = null;
|
|
1224
|
+
if (linhasPorPagina > 0) {
|
|
1225
|
+
paginacaoDiv = document.createElement("div");
|
|
1226
|
+
paginacaoDiv.className = "jade-tabela-paginacao";
|
|
1227
|
+
gridWrapper.appendChild(paginacaoDiv);
|
|
1228
|
+
}
|
|
1229
|
+
wrapper.appendChild(gridWrapper);
|
|
1230
|
+
createEffect(() => {
|
|
1231
|
+
const termo = termoBusca.get();
|
|
1232
|
+
const pagAtual = paginaAtual.get();
|
|
1233
|
+
campOrdem.get();
|
|
1234
|
+
direcaoOrdem.get();
|
|
1235
|
+
let linhas = this._filtrarOrdenar(dados, config, termo, campOrdem.peek(), direcaoOrdem.peek());
|
|
1236
|
+
if (linhasPorPagina > 0 && paginacaoDiv) {
|
|
1237
|
+
const total = Math.max(1, Math.ceil(linhas.length / linhasPorPagina));
|
|
1238
|
+
const pag = Math.min(pagAtual, total - 1);
|
|
1239
|
+
if (pag !== pagAtual) paginaAtual.set(pag);
|
|
1240
|
+
linhas = linhas.slice(pag * linhasPorPagina, (pag + 1) * linhasPorPagina);
|
|
1241
|
+
this._renderPaginacao(paginacaoDiv, pag, total, paginaAtual, () => {
|
|
1242
|
+
});
|
|
1243
|
+
}
|
|
1244
|
+
tbody.innerHTML = "";
|
|
1245
|
+
linhas.forEach((item) => {
|
|
1246
|
+
const tr = document.createElement("tr");
|
|
1247
|
+
config.colunas.forEach((col) => {
|
|
1248
|
+
const td = document.createElement("td");
|
|
1249
|
+
td.textContent = String(item[col.campo] ?? "");
|
|
1250
|
+
tr.appendChild(td);
|
|
1251
|
+
});
|
|
1252
|
+
tbody.appendChild(tr);
|
|
1253
|
+
});
|
|
1254
|
+
});
|
|
1255
|
+
}
|
|
1256
|
+
_filtrarOrdenar(dados, config, termo, campo, direcao) {
|
|
1257
|
+
let linhas = [...dados];
|
|
1258
|
+
if (termo) {
|
|
1259
|
+
linhas = linhas.filter(
|
|
1260
|
+
(item) => config.colunas.some(
|
|
1261
|
+
(col) => String(item[col.campo] ?? "").toLowerCase().includes(termo)
|
|
1262
|
+
)
|
|
1263
|
+
);
|
|
1264
|
+
}
|
|
1265
|
+
if (campo) {
|
|
1266
|
+
const dir = direcao === "asc" ? 1 : -1;
|
|
1267
|
+
linhas.sort((a, b) => {
|
|
1268
|
+
const va = a[campo] ?? "", vb = b[campo] ?? "";
|
|
1269
|
+
if (va < vb) return -1 * dir;
|
|
1270
|
+
if (va > vb) return 1 * dir;
|
|
1271
|
+
return 0;
|
|
1272
|
+
});
|
|
1273
|
+
}
|
|
1274
|
+
return linhas;
|
|
1275
|
+
}
|
|
1276
|
+
_renderPaginacao(container, paginaAtual, total, paginaSignal, atualizar) {
|
|
1277
|
+
container.innerHTML = "";
|
|
1278
|
+
const btn = (texto, pagina, desabilitado = false) => {
|
|
1279
|
+
const b = document.createElement("button");
|
|
1280
|
+
b.textContent = texto;
|
|
1281
|
+
b.className = `jade-pag-btn${pagina === paginaAtual ? " ativo" : ""}`;
|
|
1282
|
+
b.disabled = desabilitado;
|
|
1283
|
+
b.addEventListener("click", () => {
|
|
1284
|
+
paginaSignal.set(pagina);
|
|
1285
|
+
atualizar();
|
|
1286
|
+
});
|
|
1287
|
+
container.appendChild(b);
|
|
1288
|
+
};
|
|
1289
|
+
const info = document.createElement("span");
|
|
1290
|
+
info.textContent = `${paginaAtual + 1} / ${total}`;
|
|
1291
|
+
container.appendChild(info);
|
|
1292
|
+
btn("\u2190", paginaAtual - 1, paginaAtual === 0);
|
|
1293
|
+
const inicio = Math.max(0, paginaAtual - 2);
|
|
1294
|
+
const fim = Math.min(total, inicio + 5);
|
|
1295
|
+
for (let p = inicio; p < fim; p++) btn(String(p + 1), p);
|
|
1296
|
+
btn("\u2192", paginaAtual + 1, paginaAtual >= total - 1);
|
|
1297
|
+
}
|
|
1298
|
+
};
|
|
1299
|
+
|
|
1300
|
+
// ui/ui_engine.ts
|
|
1301
|
+
var UIEngine = class {
|
|
1302
|
+
store;
|
|
1303
|
+
refs;
|
|
1304
|
+
memory;
|
|
1305
|
+
router;
|
|
1306
|
+
responsivo;
|
|
1307
|
+
telaAtiva = null;
|
|
1308
|
+
toastContainer = null;
|
|
1309
|
+
constructor(memory, tema) {
|
|
1310
|
+
this.memory = memory;
|
|
1311
|
+
this.store = new Store();
|
|
1312
|
+
this.refs = new RefManager();
|
|
1313
|
+
this.router = new Router(this.store, memory);
|
|
1314
|
+
this.responsivo = new Responsivo();
|
|
1315
|
+
if (typeof document !== "undefined") {
|
|
1316
|
+
aplicarTema(tema);
|
|
1317
|
+
this.responsivo.injetarEstilos();
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
// ── Gestão de telas ───────────────────────────────────────────────────────
|
|
1321
|
+
/**
|
|
1322
|
+
* Monta uma nova tela no container.
|
|
1323
|
+
* CORREÇÃO: ao trocar de tela, os efeitos reativos e dados da tela anterior
|
|
1324
|
+
* são descartados para evitar vazamento de memória e atualizações fantasma.
|
|
1325
|
+
*/
|
|
1326
|
+
montarTela(config, container) {
|
|
1327
|
+
if (this.telaAtiva) {
|
|
1328
|
+
disposeOwner(this.telaAtiva);
|
|
1329
|
+
this.memory.freeOwner(this.telaAtiva);
|
|
1330
|
+
this.store.clearNamespace(this.telaAtiva + ".");
|
|
1331
|
+
this.refs.limpar();
|
|
1332
|
+
}
|
|
1333
|
+
this.telaAtiva = config.nome;
|
|
1334
|
+
container.innerHTML = "";
|
|
1335
|
+
container.dataset.tela = config.nome;
|
|
1336
|
+
const div = document.createElement("div");
|
|
1337
|
+
div.className = "jade-tela";
|
|
1338
|
+
if (config.titulo) {
|
|
1339
|
+
const h1 = document.createElement("h1");
|
|
1340
|
+
h1.className = "jade-tela-titulo";
|
|
1341
|
+
h1.textContent = config.titulo;
|
|
1342
|
+
div.appendChild(h1);
|
|
1343
|
+
}
|
|
1344
|
+
container.appendChild(div);
|
|
1345
|
+
return div;
|
|
1346
|
+
}
|
|
1347
|
+
// ── Tabela ────────────────────────────────────────────────────────────────
|
|
1348
|
+
/**
|
|
1349
|
+
* Cria uma tabela com layout adaptativo mobile-first.
|
|
1350
|
+
* mobile → lista de cards empilhados (responsivo.ts)
|
|
1351
|
+
* desktop → grid com colunas, ordenação, paginação (responsivo.ts)
|
|
1352
|
+
* O runtime decide automaticamente — o usuário não controla o layout.
|
|
1353
|
+
*/
|
|
1354
|
+
criarTabela(config, container, dados) {
|
|
1355
|
+
setEffectOwner(this.telaAtiva);
|
|
1356
|
+
const wrapper = document.createElement("div");
|
|
1357
|
+
wrapper.className = "jade-tabela-wrapper";
|
|
1358
|
+
const termoBusca = new Signal("");
|
|
1359
|
+
const paginaAtual = new Signal(0);
|
|
1360
|
+
if (config.filtravel) {
|
|
1361
|
+
const controles = document.createElement("div");
|
|
1362
|
+
controles.className = "jade-tabela-controles";
|
|
1363
|
+
const busca = document.createElement("input");
|
|
1364
|
+
busca.type = "search";
|
|
1365
|
+
busca.placeholder = "Buscar...";
|
|
1366
|
+
busca.className = "jade-tabela-busca";
|
|
1367
|
+
busca.setAttribute("aria-label", "Buscar na tabela");
|
|
1368
|
+
busca.addEventListener("input", () => {
|
|
1369
|
+
termoBusca.set(busca.value.toLowerCase());
|
|
1370
|
+
paginaAtual.set(0);
|
|
1371
|
+
});
|
|
1372
|
+
controles.appendChild(busca);
|
|
1373
|
+
wrapper.appendChild(controles);
|
|
1374
|
+
}
|
|
1375
|
+
container.appendChild(wrapper);
|
|
1376
|
+
this.responsivo.adaptarTabela(config, wrapper, dados, termoBusca, paginaAtual);
|
|
1377
|
+
setEffectOwner(null);
|
|
1378
|
+
}
|
|
1379
|
+
// ── Formulário ────────────────────────────────────────────────────────────
|
|
1380
|
+
criarFormulario(config, container) {
|
|
1381
|
+
setEffectOwner(this.telaAtiva);
|
|
1382
|
+
const form = document.createElement("form");
|
|
1383
|
+
form.className = "jade-formulario";
|
|
1384
|
+
form.onsubmit = (e) => e.preventDefault();
|
|
1385
|
+
const signals = {};
|
|
1386
|
+
config.campos.forEach((campo) => {
|
|
1387
|
+
const wrapper = document.createElement("div");
|
|
1388
|
+
wrapper.className = "jade-campo";
|
|
1389
|
+
const label = document.createElement("label");
|
|
1390
|
+
label.textContent = campo.titulo + (campo.obrigatorio ? " *" : "");
|
|
1391
|
+
wrapper.appendChild(label);
|
|
1392
|
+
let input;
|
|
1393
|
+
if (campo.tipo === "select" && campo.opcoes) {
|
|
1394
|
+
input = document.createElement("select");
|
|
1395
|
+
campo.opcoes.forEach((op) => {
|
|
1396
|
+
const option = document.createElement("option");
|
|
1397
|
+
option.value = op.valor;
|
|
1398
|
+
option.textContent = op.label;
|
|
1399
|
+
input.appendChild(option);
|
|
1400
|
+
});
|
|
1401
|
+
} else {
|
|
1402
|
+
const inp = document.createElement("input");
|
|
1403
|
+
inp.type = campo.tipo === "numero" || campo.tipo === "decimal" ? "number" : campo.tipo === "booleano" ? "checkbox" : campo.tipo === "data" ? "date" : campo.tipo === "hora" ? "time" : "text";
|
|
1404
|
+
if (campo.placeholder) inp.placeholder = campo.placeholder;
|
|
1405
|
+
inp.required = campo.obrigatorio ?? false;
|
|
1406
|
+
input = inp;
|
|
1407
|
+
}
|
|
1408
|
+
const signal = new Signal("");
|
|
1409
|
+
signals[campo.nome] = signal;
|
|
1410
|
+
bindInput(input, signal);
|
|
1411
|
+
if (campo.ref) this.refs.registrar(campo.ref, input);
|
|
1412
|
+
wrapper.appendChild(input);
|
|
1413
|
+
const msgErro = document.createElement("span");
|
|
1414
|
+
msgErro.className = "jade-campo-msg-erro";
|
|
1415
|
+
wrapper.appendChild(msgErro);
|
|
1416
|
+
form.appendChild(wrapper);
|
|
1417
|
+
});
|
|
1418
|
+
container.appendChild(form);
|
|
1419
|
+
setEffectOwner(null);
|
|
1420
|
+
return signals;
|
|
1421
|
+
}
|
|
1422
|
+
// ── Botão ─────────────────────────────────────────────────────────────────
|
|
1423
|
+
criarBotao(texto, handler, container, opcoes) {
|
|
1424
|
+
setEffectOwner(this.telaAtiva);
|
|
1425
|
+
const btn = document.createElement("button");
|
|
1426
|
+
btn.className = `jade-botao jade-botao-${opcoes?.tipo ?? "primario"}`;
|
|
1427
|
+
if (opcoes?.icone) {
|
|
1428
|
+
const icon = document.createElement("span");
|
|
1429
|
+
icon.textContent = opcoes.icone;
|
|
1430
|
+
btn.appendChild(icon);
|
|
1431
|
+
}
|
|
1432
|
+
const label = document.createTextNode(texto);
|
|
1433
|
+
btn.appendChild(label);
|
|
1434
|
+
btn.addEventListener("click", handler);
|
|
1435
|
+
if (opcoes?.desabilitado) {
|
|
1436
|
+
bind(opcoes.desabilitado, btn, "disabled");
|
|
1437
|
+
}
|
|
1438
|
+
container.appendChild(btn);
|
|
1439
|
+
setEffectOwner(null);
|
|
1440
|
+
return btn;
|
|
1441
|
+
}
|
|
1442
|
+
// ── Card de métrica ────────────────────────────────────────────────────────
|
|
1443
|
+
criarCard(titulo, valorSignal, container) {
|
|
1444
|
+
setEffectOwner(this.telaAtiva);
|
|
1445
|
+
const card = document.createElement("div");
|
|
1446
|
+
card.className = "jade-card";
|
|
1447
|
+
const t = document.createElement("div");
|
|
1448
|
+
t.className = "jade-card-titulo";
|
|
1449
|
+
t.textContent = titulo;
|
|
1450
|
+
const v = document.createElement("div");
|
|
1451
|
+
v.className = "jade-card-valor";
|
|
1452
|
+
bind(valorSignal, v, "textContent");
|
|
1453
|
+
card.appendChild(t);
|
|
1454
|
+
card.appendChild(v);
|
|
1455
|
+
container.appendChild(card);
|
|
1456
|
+
setEffectOwner(null);
|
|
1457
|
+
}
|
|
1458
|
+
// ── Atualização cirúrgica ─────────────────────────────────────────────────
|
|
1459
|
+
/** Atualiza um único campo de uma entidade: só o nó DOM daquele campo é re-renderizado. */
|
|
1460
|
+
atualizarCampo(entidade, index, campo, valor) {
|
|
1461
|
+
this.store.set(`${entidade}.${index}.${campo}`, valor);
|
|
1462
|
+
}
|
|
1463
|
+
// ── Skeleton / Loading ────────────────────────────────────────────────────
|
|
1464
|
+
/**
|
|
1465
|
+
* Exibe um skeleton animado enquanto os dados carregam.
|
|
1466
|
+
* Retorna o elemento para que `ocultarCarregando` possa removê-lo.
|
|
1467
|
+
*/
|
|
1468
|
+
mostrarCarregando(container, linhas = 5) {
|
|
1469
|
+
const skeleton = document.createElement("div");
|
|
1470
|
+
skeleton.className = "jade-carregando";
|
|
1471
|
+
skeleton.setAttribute("aria-label", "Carregando...");
|
|
1472
|
+
const titulo = document.createElement("div");
|
|
1473
|
+
titulo.className = "jade-skeleton jade-skeleton-titulo";
|
|
1474
|
+
skeleton.appendChild(titulo);
|
|
1475
|
+
for (let i = 0; i < linhas; i++) {
|
|
1476
|
+
const linha = document.createElement("div");
|
|
1477
|
+
linha.className = `jade-skeleton jade-skeleton-${i === 0 ? "tabela" : ""}linha`;
|
|
1478
|
+
skeleton.appendChild(linha);
|
|
1479
|
+
}
|
|
1480
|
+
container.appendChild(skeleton);
|
|
1481
|
+
return skeleton;
|
|
1482
|
+
}
|
|
1483
|
+
ocultarCarregando(skeleton) {
|
|
1484
|
+
skeleton.remove();
|
|
1485
|
+
}
|
|
1486
|
+
// ── Toast / Notificações ──────────────────────────────────────────────────
|
|
1487
|
+
/**
|
|
1488
|
+
* Exibe uma notificação temporária no canto da tela.
|
|
1489
|
+
* Desaparece automaticamente após `duracao` ms (padrão 3s).
|
|
1490
|
+
*/
|
|
1491
|
+
mostrarNotificacao(mensagem, tipo = "info", duracao = 3e3) {
|
|
1492
|
+
if (!this.toastContainer) {
|
|
1493
|
+
this.toastContainer = document.createElement("div");
|
|
1494
|
+
this.toastContainer.id = "jade-toasts";
|
|
1495
|
+
document.body.appendChild(this.toastContainer);
|
|
1496
|
+
}
|
|
1497
|
+
const icones = {
|
|
1498
|
+
sucesso: "\u2713",
|
|
1499
|
+
erro: "\u2715",
|
|
1500
|
+
aviso: "\u26A0",
|
|
1501
|
+
info: "\u2139"
|
|
1502
|
+
};
|
|
1503
|
+
const toast = document.createElement("div");
|
|
1504
|
+
toast.className = `jade-toast jade-toast-${tipo}`;
|
|
1505
|
+
toast.setAttribute("role", "alert");
|
|
1506
|
+
const icon = document.createElement("span");
|
|
1507
|
+
icon.textContent = icones[tipo];
|
|
1508
|
+
toast.appendChild(icon);
|
|
1509
|
+
const msg = document.createTextNode(mensagem);
|
|
1510
|
+
toast.appendChild(msg);
|
|
1511
|
+
this.toastContainer.appendChild(toast);
|
|
1512
|
+
const remover = () => {
|
|
1513
|
+
toast.classList.add("jade-toast-saindo");
|
|
1514
|
+
toast.addEventListener("animationend", () => toast.remove(), { once: true });
|
|
1515
|
+
};
|
|
1516
|
+
setTimeout(remover, duracao);
|
|
1517
|
+
}
|
|
1518
|
+
// ── Bridge: descriptor do compilador → componentes ───────────────────────
|
|
1519
|
+
/**
|
|
1520
|
+
* Recebe o descriptor gerado pelo compilador (.jade-ui.json) e renderiza
|
|
1521
|
+
* automaticamente cada elemento declarado na tela.
|
|
1522
|
+
* É aqui que "usuário descreve O QUE, sistema decide COMO" se concretiza.
|
|
1523
|
+
*/
|
|
1524
|
+
renderizarTela(descriptor, container) {
|
|
1525
|
+
const div = this.montarTela({ nome: descriptor.nome, titulo: descriptor.titulo }, container);
|
|
1526
|
+
for (const el of descriptor.elementos) {
|
|
1527
|
+
const props = Object.fromEntries(el.propriedades.map((p) => [p.chave, p.valor]));
|
|
1528
|
+
switch (el.tipo) {
|
|
1529
|
+
case "tabela": {
|
|
1530
|
+
const entidade = String(props["entidade"] ?? el.nome);
|
|
1531
|
+
const colunas = Array.isArray(props["colunas"]) ? props["colunas"].map((c) => ({ campo: c, titulo: c })) : [];
|
|
1532
|
+
this.criarTabela(
|
|
1533
|
+
{
|
|
1534
|
+
entidade,
|
|
1535
|
+
colunas,
|
|
1536
|
+
filtravel: props["filtravel"] === "verdadeiro",
|
|
1537
|
+
paginacao: props["paginacao"] === "verdadeiro" ? true : Number(props["paginacao"]) || false
|
|
1538
|
+
},
|
|
1539
|
+
div,
|
|
1540
|
+
[]
|
|
1541
|
+
// dados vindos do runtime/WASM — [] por padrão até carregar
|
|
1542
|
+
);
|
|
1543
|
+
break;
|
|
1544
|
+
}
|
|
1545
|
+
case "formulario": {
|
|
1546
|
+
const campos = Array.isArray(props["campos"]) ? props["campos"].map((c) => ({ nome: c, titulo: c, tipo: "texto" })) : [];
|
|
1547
|
+
this.criarFormulario({ entidade: String(props["entidade"] ?? el.nome), campos }, div);
|
|
1548
|
+
break;
|
|
1549
|
+
}
|
|
1550
|
+
case "botao": {
|
|
1551
|
+
const acao = String(props["acao"] ?? props["clique"] ?? "");
|
|
1552
|
+
this.criarBotao(el.nome, () => {
|
|
1553
|
+
window.dispatchEvent(new CustomEvent("jade:acao", { detail: { acao, tela: descriptor.nome } }));
|
|
1554
|
+
}, div);
|
|
1555
|
+
break;
|
|
1556
|
+
}
|
|
1557
|
+
case "cartao": {
|
|
1558
|
+
const valor = new Signal(props["valor"] ?? "");
|
|
1559
|
+
this.criarCard(el.nome, valor, div);
|
|
1560
|
+
break;
|
|
1561
|
+
}
|
|
1562
|
+
// grafico e modal: placeholder até implementação completa
|
|
1563
|
+
default: {
|
|
1564
|
+
const placeholder = document.createElement("div");
|
|
1565
|
+
placeholder.className = "jade-placeholder";
|
|
1566
|
+
placeholder.textContent = `[${el.tipo}: ${el.nome}]`;
|
|
1567
|
+
placeholder.style.cssText = "padding:12px;border:1px dashed #d1d5db;border-radius:8px;color:#9ca3af;font-size:0.875rem;";
|
|
1568
|
+
div.appendChild(placeholder);
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
return div;
|
|
1573
|
+
}
|
|
1574
|
+
// ── Acessores ─────────────────────────────────────────────────────────────
|
|
1575
|
+
focar(nomeRef) {
|
|
1576
|
+
this.refs.focar(nomeRef);
|
|
1577
|
+
}
|
|
1578
|
+
getStore() {
|
|
1579
|
+
return this.store;
|
|
1580
|
+
}
|
|
1581
|
+
getRefs() {
|
|
1582
|
+
return this.refs;
|
|
1583
|
+
}
|
|
1584
|
+
getRouter() {
|
|
1585
|
+
return this.router;
|
|
1586
|
+
}
|
|
1587
|
+
getResponsivo() {
|
|
1588
|
+
return this.responsivo;
|
|
1589
|
+
}
|
|
1590
|
+
};
|
|
1591
|
+
|
|
1592
|
+
// pwa/pwa_generator.ts
|
|
1593
|
+
var PWAGenerator = class {
|
|
1594
|
+
gerarManifest(config) {
|
|
1595
|
+
return JSON.stringify({
|
|
1596
|
+
name: config.nome,
|
|
1597
|
+
short_name: config.nomeAbreviado ?? config.nome.slice(0, 12),
|
|
1598
|
+
description: config.descricao ?? "",
|
|
1599
|
+
display: "standalone",
|
|
1600
|
+
start_url: "/",
|
|
1601
|
+
scope: "/",
|
|
1602
|
+
theme_color: config.cor_tema ?? "#2563eb",
|
|
1603
|
+
background_color: config.cor_fundo ?? "#ffffff",
|
|
1604
|
+
icons: [
|
|
1605
|
+
{
|
|
1606
|
+
src: config.icone ?? "/icon-192.png",
|
|
1607
|
+
sizes: "192x192",
|
|
1608
|
+
type: "image/png"
|
|
1609
|
+
},
|
|
1610
|
+
{
|
|
1611
|
+
src: config.icone ?? "/icon-512.png",
|
|
1612
|
+
sizes: "512x512",
|
|
1613
|
+
type: "image/png"
|
|
1614
|
+
}
|
|
1615
|
+
]
|
|
1616
|
+
}, null, 2);
|
|
1617
|
+
}
|
|
1618
|
+
gerarServiceWorker(config) {
|
|
1619
|
+
const cacheName = `jade-${config.nome.toLowerCase().replace(/\s+/g, "-")}-v1`;
|
|
1620
|
+
const arquivos = config.arquivosCache ?? ["/", "/index.html", "/app.wasm", "/manifest.json"];
|
|
1621
|
+
return `const CACHE_NAME = '${cacheName}';
|
|
1622
|
+
const ARQUIVOS_CACHE = ${JSON.stringify(arquivos)};
|
|
1623
|
+
|
|
1624
|
+
self.addEventListener('install', e => {
|
|
1625
|
+
e.waitUntil(caches.open(CACHE_NAME).then(c => c.addAll(ARQUIVOS_CACHE)));
|
|
1626
|
+
self.skipWaiting();
|
|
1627
|
+
});
|
|
1628
|
+
|
|
1629
|
+
self.addEventListener('activate', e => {
|
|
1630
|
+
e.waitUntil(
|
|
1631
|
+
caches.keys().then(keys =>
|
|
1632
|
+
Promise.all(
|
|
1633
|
+
keys.filter(k => k.startsWith('jade-') && k !== CACHE_NAME)
|
|
1634
|
+
.map(k => caches.delete(k))
|
|
1635
|
+
)
|
|
1636
|
+
)
|
|
1637
|
+
);
|
|
1638
|
+
self.clients.claim();
|
|
1639
|
+
});
|
|
1640
|
+
|
|
1641
|
+
self.addEventListener('fetch', e => {
|
|
1642
|
+
if (e.request.method !== 'GET') return;
|
|
1643
|
+
e.respondWith(
|
|
1644
|
+
caches.match(e.request).then(cached => {
|
|
1645
|
+
if (cached) return cached;
|
|
1646
|
+
return fetch(e.request).then(res => {
|
|
1647
|
+
if (!res || res.status !== 200 || res.type !== 'basic') return res;
|
|
1648
|
+
const clone = res.clone();
|
|
1649
|
+
caches.open(CACHE_NAME).then(c => c.put(e.request, clone));
|
|
1650
|
+
return res;
|
|
1651
|
+
}).catch(() =>
|
|
1652
|
+
caches.match('/offline.html') ??
|
|
1653
|
+
new Response('<h1>Sem conex\xE3o</h1>', { headers: { 'Content-Type': 'text/html' } })
|
|
1654
|
+
);
|
|
1655
|
+
})
|
|
1656
|
+
);
|
|
1657
|
+
});
|
|
1658
|
+
|
|
1659
|
+
// Background sync: notifica o app quando conex\xE3o retorna
|
|
1660
|
+
self.addEventListener('sync', e => {
|
|
1661
|
+
if (e.tag === 'jade-sync') {
|
|
1662
|
+
e.waitUntil(
|
|
1663
|
+
self.clients.matchAll().then(clients =>
|
|
1664
|
+
clients.forEach(c => c.postMessage({ tipo: 'sync-requisitado' }))
|
|
1665
|
+
)
|
|
1666
|
+
);
|
|
1667
|
+
}
|
|
1668
|
+
});`;
|
|
1669
|
+
}
|
|
1670
|
+
gerarIndexHTML(config) {
|
|
1671
|
+
return `<!DOCTYPE html>
|
|
1672
|
+
<html lang="pt-BR">
|
|
1673
|
+
<head>
|
|
1674
|
+
<meta charset="UTF-8">
|
|
1675
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
1676
|
+
<title>${config.nome}</title>
|
|
1677
|
+
<link rel="manifest" href="/manifest.json">
|
|
1678
|
+
<meta name="theme-color" content="${config.cor_tema ?? "#2563eb"}">
|
|
1679
|
+
<meta name="description" content="${config.descricao ?? ""}">
|
|
1680
|
+
</head>
|
|
1681
|
+
<body>
|
|
1682
|
+
<div id="app"></div>
|
|
1683
|
+
<script type="module">
|
|
1684
|
+
if ('serviceWorker' in navigator) {
|
|
1685
|
+
navigator.serviceWorker.register('/service_worker.js')
|
|
1686
|
+
.then(() => console.log('[JADE] Service Worker registrado'))
|
|
1687
|
+
.catch(e => console.warn('[JADE] SW falhou:', e));
|
|
1688
|
+
}
|
|
1689
|
+
navigator.serviceWorker?.addEventListener('message', e => {
|
|
1690
|
+
if (e.data?.tipo === 'sync-requisitado') {
|
|
1691
|
+
window.dispatchEvent(new CustomEvent('jade:sync'));
|
|
1692
|
+
}
|
|
1693
|
+
});
|
|
1694
|
+
<\/script>
|
|
1695
|
+
</body>
|
|
1696
|
+
</html>`;
|
|
1697
|
+
}
|
|
1698
|
+
};
|
|
1699
|
+
|
|
1700
|
+
// stdlib/moeda.ts
|
|
1701
|
+
var MoedaStdlib = class _MoedaStdlib {
|
|
1702
|
+
// ── Conversão interna ─────────────────────────────────────
|
|
1703
|
+
/**
|
|
1704
|
+
* Converte reais para centavos inteiros
|
|
1705
|
+
* 1234.50 → 123450
|
|
1706
|
+
*/
|
|
1707
|
+
static toCentavos(valor) {
|
|
1708
|
+
return Math.round(valor * 100);
|
|
1709
|
+
}
|
|
1710
|
+
/**
|
|
1711
|
+
* Converte centavos inteiros para reais
|
|
1712
|
+
* 123450 → 1234.50
|
|
1713
|
+
*/
|
|
1714
|
+
static fromCentavos(centavos) {
|
|
1715
|
+
return centavos / 100;
|
|
1716
|
+
}
|
|
1717
|
+
// ── Formatação ────────────────────────────────────────────
|
|
1718
|
+
/**
|
|
1719
|
+
* Formata valor como moeda brasileira
|
|
1720
|
+
* 1234.5 → "R$ 1.234,50"
|
|
1721
|
+
* -500 → "-R$ 500,00"
|
|
1722
|
+
* 0 → "R$ 0,00"
|
|
1723
|
+
*/
|
|
1724
|
+
static formatarBRL(valor) {
|
|
1725
|
+
const negativo = valor < 0;
|
|
1726
|
+
const centavos = Math.round(Math.abs(valor) * 100);
|
|
1727
|
+
const reais = Math.floor(centavos / 100);
|
|
1728
|
+
const cents = centavos % 100;
|
|
1729
|
+
const reaisStr = reais.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ".");
|
|
1730
|
+
const resultado = `R$ ${reaisStr},${cents.toString().padStart(2, "0")}`;
|
|
1731
|
+
return negativo ? "-" + resultado : resultado;
|
|
1732
|
+
}
|
|
1733
|
+
/**
|
|
1734
|
+
* Formata valor em formato compacto para dashboards
|
|
1735
|
+
* 1_500_000 → "R$ 1,5mi"
|
|
1736
|
+
* 45_000 → "R$ 45mil"
|
|
1737
|
+
* 1_500 → "R$ 1,5mil"
|
|
1738
|
+
* 500 → "R$ 500,00"
|
|
1739
|
+
*/
|
|
1740
|
+
static formatarCompacto(valor) {
|
|
1741
|
+
const negativo = valor < 0;
|
|
1742
|
+
const abs = Math.abs(valor);
|
|
1743
|
+
let resultado;
|
|
1744
|
+
if (abs >= 1e6) {
|
|
1745
|
+
const mi = abs / 1e6;
|
|
1746
|
+
resultado = `R$ ${_MoedaStdlib._compactarNumero(mi)}mi`;
|
|
1747
|
+
} else if (abs >= 1e3) {
|
|
1748
|
+
const mil = abs / 1e3;
|
|
1749
|
+
resultado = `R$ ${_MoedaStdlib._compactarNumero(mil)}mil`;
|
|
1750
|
+
} else {
|
|
1751
|
+
resultado = _MoedaStdlib.formatarBRL(abs);
|
|
1752
|
+
}
|
|
1753
|
+
return negativo ? "-" + resultado : resultado;
|
|
1754
|
+
}
|
|
1755
|
+
static _compactarNumero(n) {
|
|
1756
|
+
const arredondado = Math.round(n * 10) / 10;
|
|
1757
|
+
return arredondado % 1 === 0 ? arredondado.toFixed(0) : arredondado.toFixed(1).replace(".", ",");
|
|
1758
|
+
}
|
|
1759
|
+
// ── Parsing ───────────────────────────────────────────────
|
|
1760
|
+
/**
|
|
1761
|
+
* Converte texto de moeda brasileira para número
|
|
1762
|
+
* "R$ 1.234,50" → 1234.50
|
|
1763
|
+
* "1.234,50" → 1234.50
|
|
1764
|
+
* "1234,50" → 1234.50
|
|
1765
|
+
* "-R$ 500,00" → -500.00
|
|
1766
|
+
* Retorna NaN se o formato não for reconhecido
|
|
1767
|
+
*/
|
|
1768
|
+
static parseBRL(texto) {
|
|
1769
|
+
const limpo = texto.trim().replace(/R\$\s?/g, "").trim();
|
|
1770
|
+
const negativo = limpo.startsWith("-");
|
|
1771
|
+
const sem_sinal = limpo.replace(/^-/, "").trim();
|
|
1772
|
+
const br = sem_sinal.replace(/\./g, "").replace(",", ".");
|
|
1773
|
+
const valor = parseFloat(br);
|
|
1774
|
+
if (isNaN(valor)) return NaN;
|
|
1775
|
+
return negativo ? -valor : valor;
|
|
1776
|
+
}
|
|
1777
|
+
// ── Aritmética segura (via centavos) ──────────────────────
|
|
1778
|
+
/**
|
|
1779
|
+
* Soma monetária sem erro de ponto flutuante
|
|
1780
|
+
* somar(0.1, 0.2) === 0.30 (não 0.30000000000000004)
|
|
1781
|
+
*/
|
|
1782
|
+
static somar(a, b) {
|
|
1783
|
+
return _MoedaStdlib.fromCentavos(
|
|
1784
|
+
_MoedaStdlib.toCentavos(a) + _MoedaStdlib.toCentavos(b)
|
|
1785
|
+
);
|
|
1786
|
+
}
|
|
1787
|
+
/**
|
|
1788
|
+
* Subtração monetária sem erro de ponto flutuante
|
|
1789
|
+
*/
|
|
1790
|
+
static subtrair(a, b) {
|
|
1791
|
+
return _MoedaStdlib.fromCentavos(
|
|
1792
|
+
_MoedaStdlib.toCentavos(a) - _MoedaStdlib.toCentavos(b)
|
|
1793
|
+
);
|
|
1794
|
+
}
|
|
1795
|
+
/**
|
|
1796
|
+
* Multiplica um valor monetário por um fator (ex: preço × quantidade)
|
|
1797
|
+
* O fator pode ser decimal (ex: 1.5 unidades)
|
|
1798
|
+
*/
|
|
1799
|
+
static multiplicar(valor, fator) {
|
|
1800
|
+
return _MoedaStdlib.fromCentavos(
|
|
1801
|
+
Math.round(_MoedaStdlib.toCentavos(valor) * fator)
|
|
1802
|
+
);
|
|
1803
|
+
}
|
|
1804
|
+
/**
|
|
1805
|
+
* Divide um valor monetário por um divisor
|
|
1806
|
+
* Arredonda para centavos (sem distribuição do resto — veja distribuir())
|
|
1807
|
+
*/
|
|
1808
|
+
static dividir(valor, divisor) {
|
|
1809
|
+
if (divisor === 0) return NaN;
|
|
1810
|
+
return _MoedaStdlib.fromCentavos(
|
|
1811
|
+
Math.round(_MoedaStdlib.toCentavos(valor) / divisor)
|
|
1812
|
+
);
|
|
1813
|
+
}
|
|
1814
|
+
// ── Comparações seguras ───────────────────────────────────
|
|
1815
|
+
/**
|
|
1816
|
+
* Compara dois valores monetários com precisão de centavos
|
|
1817
|
+
* Evita problemas de 0.1 + 0.2 !== 0.3
|
|
1818
|
+
*/
|
|
1819
|
+
static igual(a, b) {
|
|
1820
|
+
return _MoedaStdlib.toCentavos(a) === _MoedaStdlib.toCentavos(b);
|
|
1821
|
+
}
|
|
1822
|
+
static maior(a, b) {
|
|
1823
|
+
return _MoedaStdlib.toCentavos(a) > _MoedaStdlib.toCentavos(b);
|
|
1824
|
+
}
|
|
1825
|
+
static menor(a, b) {
|
|
1826
|
+
return _MoedaStdlib.toCentavos(a) < _MoedaStdlib.toCentavos(b);
|
|
1827
|
+
}
|
|
1828
|
+
static maiorOuIgual(a, b) {
|
|
1829
|
+
return _MoedaStdlib.toCentavos(a) >= _MoedaStdlib.toCentavos(b);
|
|
1830
|
+
}
|
|
1831
|
+
static menorOuIgual(a, b) {
|
|
1832
|
+
return _MoedaStdlib.toCentavos(a) <= _MoedaStdlib.toCentavos(b);
|
|
1833
|
+
}
|
|
1834
|
+
// ── Operações de negócio ──────────────────────────────────
|
|
1835
|
+
/**
|
|
1836
|
+
* Aplica desconto percentual sobre um valor
|
|
1837
|
+
* descontar(100, 10) → 90.00 (10% de desconto)
|
|
1838
|
+
*/
|
|
1839
|
+
static descontar(valor, percentual) {
|
|
1840
|
+
return _MoedaStdlib.fromCentavos(
|
|
1841
|
+
Math.round(_MoedaStdlib.toCentavos(valor) * (1 - percentual / 100))
|
|
1842
|
+
);
|
|
1843
|
+
}
|
|
1844
|
+
/**
|
|
1845
|
+
* Acrescenta percentual sobre um valor
|
|
1846
|
+
* acrescentar(100, 10) → 110.00 (10% de acréscimo)
|
|
1847
|
+
*/
|
|
1848
|
+
static acrescentar(valor, percentual) {
|
|
1849
|
+
return _MoedaStdlib.fromCentavos(
|
|
1850
|
+
Math.round(_MoedaStdlib.toCentavos(valor) * (1 + percentual / 100))
|
|
1851
|
+
);
|
|
1852
|
+
}
|
|
1853
|
+
/**
|
|
1854
|
+
* Calcula o valor de um percentual sobre um montante
|
|
1855
|
+
* porcentagem(200, 15) → 30.00 (15% de R$200)
|
|
1856
|
+
*/
|
|
1857
|
+
static porcentagem(valor, percentual) {
|
|
1858
|
+
return _MoedaStdlib.fromCentavos(
|
|
1859
|
+
Math.round(_MoedaStdlib.toCentavos(valor) * percentual / 100)
|
|
1860
|
+
);
|
|
1861
|
+
}
|
|
1862
|
+
/**
|
|
1863
|
+
* Distribui um valor em N partes iguais, resolvendo o problema do centavo
|
|
1864
|
+
* Os centavos restantes são distribuídos nas primeiras parcelas
|
|
1865
|
+
*
|
|
1866
|
+
* distribuir(10, 3) → [3.34, 3.33, 3.33] (não [3.33, 3.33, 3.33] = 9.99)
|
|
1867
|
+
* distribuir(100, 4) → [25, 25, 25, 25]
|
|
1868
|
+
*/
|
|
1869
|
+
static distribuir(total, partes) {
|
|
1870
|
+
if (partes <= 0) return [];
|
|
1871
|
+
const totalCentavos = _MoedaStdlib.toCentavos(total);
|
|
1872
|
+
const baseCentavos = Math.floor(totalCentavos / partes);
|
|
1873
|
+
const resto = totalCentavos % partes;
|
|
1874
|
+
return Array.from(
|
|
1875
|
+
{ length: partes },
|
|
1876
|
+
(_, i) => _MoedaStdlib.fromCentavos(i < resto ? baseCentavos + 1 : baseCentavos)
|
|
1877
|
+
);
|
|
1878
|
+
}
|
|
1879
|
+
/**
|
|
1880
|
+
* Calcula valor total de uma lista de itens (quantidade × preço unitário)
|
|
1881
|
+
* Seguro contra ponto flutuante
|
|
1882
|
+
*/
|
|
1883
|
+
static totalItens(itens) {
|
|
1884
|
+
const centavos = itens.reduce(
|
|
1885
|
+
(acc, item) => acc + Math.round(_MoedaStdlib.toCentavos(item.precoUnitario) * item.quantidade),
|
|
1886
|
+
0
|
|
1887
|
+
);
|
|
1888
|
+
return _MoedaStdlib.fromCentavos(centavos);
|
|
1889
|
+
}
|
|
1890
|
+
};
|
|
1891
|
+
var MoedaMetodos = {
|
|
1892
|
+
toCentavos: MoedaStdlib.toCentavos,
|
|
1893
|
+
fromCentavos: MoedaStdlib.fromCentavos,
|
|
1894
|
+
formatarBRL: MoedaStdlib.formatarBRL,
|
|
1895
|
+
formatarCompacto: MoedaStdlib.formatarCompacto,
|
|
1896
|
+
parseBRL: MoedaStdlib.parseBRL,
|
|
1897
|
+
somar: MoedaStdlib.somar,
|
|
1898
|
+
subtrair: MoedaStdlib.subtrair,
|
|
1899
|
+
multiplicar: MoedaStdlib.multiplicar,
|
|
1900
|
+
dividir: MoedaStdlib.dividir,
|
|
1901
|
+
igual: MoedaStdlib.igual,
|
|
1902
|
+
maior: MoedaStdlib.maior,
|
|
1903
|
+
menor: MoedaStdlib.menor,
|
|
1904
|
+
maiorOuIgual: MoedaStdlib.maiorOuIgual,
|
|
1905
|
+
menorOuIgual: MoedaStdlib.menorOuIgual,
|
|
1906
|
+
descontar: MoedaStdlib.descontar,
|
|
1907
|
+
acrescentar: MoedaStdlib.acrescentar,
|
|
1908
|
+
porcentagem: MoedaStdlib.porcentagem,
|
|
1909
|
+
distribuir: MoedaStdlib.distribuir,
|
|
1910
|
+
totalItens: MoedaStdlib.totalItens
|
|
1911
|
+
};
|
|
1912
|
+
|
|
1913
|
+
// stdlib/texto.ts
|
|
1914
|
+
var TextoStdlib = class _TextoStdlib {
|
|
1915
|
+
/**
|
|
1916
|
+
* Converts string to uppercase
|
|
1917
|
+
*/
|
|
1918
|
+
static maiusculo(texto) {
|
|
1919
|
+
return texto.toUpperCase();
|
|
1920
|
+
}
|
|
1921
|
+
/**
|
|
1922
|
+
* Converts string to lowercase
|
|
1923
|
+
*/
|
|
1924
|
+
static minusculo(texto) {
|
|
1925
|
+
return texto.toLowerCase();
|
|
1926
|
+
}
|
|
1927
|
+
/**
|
|
1928
|
+
* Trims whitespace from both ends of string
|
|
1929
|
+
*/
|
|
1930
|
+
static aparar(texto) {
|
|
1931
|
+
return texto.trim();
|
|
1932
|
+
}
|
|
1933
|
+
/**
|
|
1934
|
+
* Returns the length of the string (counts Unicode characters correctly)
|
|
1935
|
+
*/
|
|
1936
|
+
static tamanho(texto) {
|
|
1937
|
+
return Array.from(texto).length;
|
|
1938
|
+
}
|
|
1939
|
+
/**
|
|
1940
|
+
* Remove acentos e diacríticos de uma string
|
|
1941
|
+
* Ex: "João" → "Joao", "São Paulo" → "Sao Paulo"
|
|
1942
|
+
*/
|
|
1943
|
+
static semAcentos(texto) {
|
|
1944
|
+
return texto.normalize("NFD").replace(/[\u0300-\u036f]/g, "");
|
|
1945
|
+
}
|
|
1946
|
+
/**
|
|
1947
|
+
* Checks if string contains the specified substring
|
|
1948
|
+
* @param ignorarAcentos Se verdadeiro, "Joao" encontra "João" (default: false)
|
|
1949
|
+
*/
|
|
1950
|
+
static contem(texto, busca, ignorarAcentos = false) {
|
|
1951
|
+
if (!ignorarAcentos) return texto.includes(busca);
|
|
1952
|
+
const norm = (s) => _TextoStdlib.semAcentos(s).toLowerCase();
|
|
1953
|
+
return norm(texto).includes(norm(busca));
|
|
1954
|
+
}
|
|
1955
|
+
/**
|
|
1956
|
+
* Checks if string starts with the specified substring
|
|
1957
|
+
*/
|
|
1958
|
+
static comecaCom(texto, prefixo) {
|
|
1959
|
+
return texto.startsWith(prefixo);
|
|
1960
|
+
}
|
|
1961
|
+
/**
|
|
1962
|
+
* Checks if string ends with the specified substring
|
|
1963
|
+
*/
|
|
1964
|
+
static terminaCom(texto, sufixo) {
|
|
1965
|
+
return texto.endsWith(sufixo);
|
|
1966
|
+
}
|
|
1967
|
+
/**
|
|
1968
|
+
* Replaces all occurrences of a substring with another substring
|
|
1969
|
+
*/
|
|
1970
|
+
static substituir(texto, busca, substituto) {
|
|
1971
|
+
return texto.split(busca).join(substituto);
|
|
1972
|
+
}
|
|
1973
|
+
/**
|
|
1974
|
+
* Splits string by a delimiter into an array of strings
|
|
1975
|
+
*/
|
|
1976
|
+
static dividir(texto, delimitador) {
|
|
1977
|
+
return texto.split(delimitador);
|
|
1978
|
+
}
|
|
1979
|
+
/**
|
|
1980
|
+
* Normalizes Unicode string to NFC form
|
|
1981
|
+
*/
|
|
1982
|
+
static normalizar(texto) {
|
|
1983
|
+
return texto.normalize("NFC");
|
|
1984
|
+
}
|
|
1985
|
+
/**
|
|
1986
|
+
* Aplica uma máscara de formatação a uma string de dígitos
|
|
1987
|
+
* Use '#' para cada dígito esperado; demais caracteres são inseridos como literais
|
|
1988
|
+
*
|
|
1989
|
+
* Exemplos:
|
|
1990
|
+
* aplicarMascara("12345678901", "###.###.###-##") → "123.456.789-01"
|
|
1991
|
+
* aplicarMascara("00360305000104","##.###.###/####-##") → "00.360.305/0001-04"
|
|
1992
|
+
* aplicarMascara("01310100", "#####-###") → "01310-100"
|
|
1993
|
+
* aplicarMascara("11987654321", "(##) #####-####") → "(11) 98765-4321"
|
|
1994
|
+
*/
|
|
1995
|
+
static aplicarMascara(valor, mascara) {
|
|
1996
|
+
const digits = valor.replace(/\D/g, "");
|
|
1997
|
+
let resultado = "";
|
|
1998
|
+
let di = 0;
|
|
1999
|
+
for (const ch of mascara) {
|
|
2000
|
+
if (di >= digits.length) break;
|
|
2001
|
+
if (ch === "#") {
|
|
2002
|
+
resultado += digits[di++];
|
|
2003
|
+
} else {
|
|
2004
|
+
resultado += ch;
|
|
2005
|
+
}
|
|
2006
|
+
}
|
|
2007
|
+
return resultado;
|
|
2008
|
+
}
|
|
2009
|
+
// ── Validações e formatações brasileiras ─────────────────
|
|
2010
|
+
/**
|
|
2011
|
+
* Validates Brazilian CPF (Cadastro de Pessoas Físicas)
|
|
2012
|
+
* Aceita CPF formatado (123.456.789-01) ou apenas dígitos
|
|
2013
|
+
*/
|
|
2014
|
+
static validarCPF(cpf) {
|
|
2015
|
+
const cpfLimpo = cpf.replace(/\D/g, "");
|
|
2016
|
+
if (cpfLimpo.length !== 11) return false;
|
|
2017
|
+
if (/^(\d)\1{10}$/.test(cpfLimpo)) return false;
|
|
2018
|
+
let soma = 0;
|
|
2019
|
+
let resto;
|
|
2020
|
+
for (let i = 1; i <= 9; i++) {
|
|
2021
|
+
soma += parseInt(cpfLimpo.substring(i - 1, i)) * (11 - i);
|
|
2022
|
+
}
|
|
2023
|
+
resto = soma * 10 % 11;
|
|
2024
|
+
if (resto === 10 || resto === 11) resto = 0;
|
|
2025
|
+
if (resto !== parseInt(cpfLimpo.substring(9, 10))) return false;
|
|
2026
|
+
soma = 0;
|
|
2027
|
+
for (let i = 1; i <= 10; i++) {
|
|
2028
|
+
soma += parseInt(cpfLimpo.substring(i - 1, i)) * (12 - i);
|
|
2029
|
+
}
|
|
2030
|
+
resto = soma * 10 % 11;
|
|
2031
|
+
if (resto === 10 || resto === 11) resto = 0;
|
|
2032
|
+
if (resto !== parseInt(cpfLimpo.substring(10, 11))) return false;
|
|
2033
|
+
return true;
|
|
2034
|
+
}
|
|
2035
|
+
/**
|
|
2036
|
+
* Validates Brazilian CNPJ (Cadastro Nacional da Pessoa Jurídica)
|
|
2037
|
+
* Aceita CNPJ formatado (00.360.305/0001-04) ou apenas dígitos
|
|
2038
|
+
*/
|
|
2039
|
+
static validarCNPJ(cnpj) {
|
|
2040
|
+
const cnpjLimpo = cnpj.replace(/\D/g, "");
|
|
2041
|
+
if (cnpjLimpo.length !== 14) return false;
|
|
2042
|
+
if (/^(\d)\1{13}$/.test(cnpjLimpo)) return false;
|
|
2043
|
+
let soma = 0;
|
|
2044
|
+
let peso = 5;
|
|
2045
|
+
for (let i = 0; i < 12; i++) {
|
|
2046
|
+
soma += parseInt(cnpjLimpo[i]) * peso;
|
|
2047
|
+
peso = peso === 2 ? 9 : peso - 1;
|
|
2048
|
+
}
|
|
2049
|
+
let resto = soma % 11;
|
|
2050
|
+
const digito1 = resto < 2 ? 0 : 11 - resto;
|
|
2051
|
+
soma = 0;
|
|
2052
|
+
peso = 6;
|
|
2053
|
+
for (let i = 0; i < 13; i++) {
|
|
2054
|
+
soma += parseInt(cnpjLimpo[i]) * peso;
|
|
2055
|
+
peso = peso === 2 ? 9 : peso - 1;
|
|
2056
|
+
}
|
|
2057
|
+
resto = soma % 11;
|
|
2058
|
+
const digito2 = resto < 2 ? 0 : 11 - resto;
|
|
2059
|
+
return parseInt(cnpjLimpo[12]) === digito1 && parseInt(cnpjLimpo[13]) === digito2;
|
|
2060
|
+
}
|
|
2061
|
+
/**
|
|
2062
|
+
* Formats Brazilian CEP (Código de Endereçamento Postal)
|
|
2063
|
+
*/
|
|
2064
|
+
static formatarCEP(cep) {
|
|
2065
|
+
const cepLimpo = cep.replace(/\D/g, "");
|
|
2066
|
+
if (cepLimpo.length !== 8) return cep;
|
|
2067
|
+
return `${cepLimpo.substring(0, 5)}-${cepLimpo.substring(5)}`;
|
|
2068
|
+
}
|
|
2069
|
+
/**
|
|
2070
|
+
* Formats Brazilian phone number
|
|
2071
|
+
* Returns (XX) XXXXX-XXXX for mobile or (XX) XXXX-XXXX for landline
|
|
2072
|
+
*/
|
|
2073
|
+
static formatarTelefone(telefone) {
|
|
2074
|
+
const telLimpo = telefone.replace(/\D/g, "");
|
|
2075
|
+
if (telLimpo.length === 11) {
|
|
2076
|
+
return `(${telLimpo.substring(0, 2)}) ${telLimpo.substring(2, 7)}-${telLimpo.substring(7)}`;
|
|
2077
|
+
} else if (telLimpo.length === 10) {
|
|
2078
|
+
return `(${telLimpo.substring(0, 2)}) ${telLimpo.substring(2, 6)}-${telLimpo.substring(6)}`;
|
|
2079
|
+
}
|
|
2080
|
+
return telefone;
|
|
2081
|
+
}
|
|
2082
|
+
};
|
|
2083
|
+
var TextoMetodos = {
|
|
2084
|
+
maiusculo: TextoStdlib.maiusculo,
|
|
2085
|
+
minusculo: TextoStdlib.minusculo,
|
|
2086
|
+
aparar: TextoStdlib.aparar,
|
|
2087
|
+
tamanho: TextoStdlib.tamanho,
|
|
2088
|
+
semAcentos: TextoStdlib.semAcentos,
|
|
2089
|
+
contem: TextoStdlib.contem,
|
|
2090
|
+
comecaCom: TextoStdlib.comecaCom,
|
|
2091
|
+
terminaCom: TextoStdlib.terminaCom,
|
|
2092
|
+
substituir: TextoStdlib.substituir,
|
|
2093
|
+
dividir: TextoStdlib.dividir,
|
|
2094
|
+
normalizar: TextoStdlib.normalizar,
|
|
2095
|
+
aplicarMascara: TextoStdlib.aplicarMascara,
|
|
2096
|
+
validarCPF: TextoStdlib.validarCPF,
|
|
2097
|
+
validarCNPJ: TextoStdlib.validarCNPJ,
|
|
2098
|
+
formatarCEP: TextoStdlib.formatarCEP,
|
|
2099
|
+
formatarTelefone: TextoStdlib.formatarTelefone
|
|
2100
|
+
};
|
|
2101
|
+
|
|
2102
|
+
// stdlib/matematica.ts
|
|
2103
|
+
var MatematicaStdlib = class _MatematicaStdlib {
|
|
2104
|
+
// ── Básico ────────────────────────────────────────────────
|
|
2105
|
+
static soma(lista) {
|
|
2106
|
+
return lista.reduce((acc, v) => acc + v, 0);
|
|
2107
|
+
}
|
|
2108
|
+
static media(lista) {
|
|
2109
|
+
if (lista.length === 0) return NaN;
|
|
2110
|
+
return _MatematicaStdlib.soma(lista) / lista.length;
|
|
2111
|
+
}
|
|
2112
|
+
static mediana(lista) {
|
|
2113
|
+
if (lista.length === 0) return NaN;
|
|
2114
|
+
const sorted = [...lista].sort((a, b) => a - b);
|
|
2115
|
+
const mid = Math.floor(sorted.length / 2);
|
|
2116
|
+
return sorted.length % 2 !== 0 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
|
|
2117
|
+
}
|
|
2118
|
+
static desvioPadrao(lista) {
|
|
2119
|
+
if (lista.length === 0) return NaN;
|
|
2120
|
+
const m = _MatematicaStdlib.media(lista);
|
|
2121
|
+
const variancia = lista.reduce((acc, v) => acc + Math.pow(v - m, 2), 0) / lista.length;
|
|
2122
|
+
return Math.sqrt(variancia);
|
|
2123
|
+
}
|
|
2124
|
+
static variancia(lista) {
|
|
2125
|
+
if (lista.length === 0) return NaN;
|
|
2126
|
+
const m = _MatematicaStdlib.media(lista);
|
|
2127
|
+
return lista.reduce((acc, v) => acc + Math.pow(v - m, 2), 0) / lista.length;
|
|
2128
|
+
}
|
|
2129
|
+
// reduce evita estouro de pilha com listas grandes (Math.min/max(...lista) quebra ~100k+ itens)
|
|
2130
|
+
static minimo(lista) {
|
|
2131
|
+
if (lista.length === 0) return NaN;
|
|
2132
|
+
return lista.reduce((min, v) => v < min ? v : min, lista[0]);
|
|
2133
|
+
}
|
|
2134
|
+
static maximo(lista) {
|
|
2135
|
+
if (lista.length === 0) return NaN;
|
|
2136
|
+
return lista.reduce((max, v) => v > max ? v : max, lista[0]);
|
|
2137
|
+
}
|
|
2138
|
+
static arredondar(valor, casas = 2) {
|
|
2139
|
+
return Math.round(valor * Math.pow(10, casas)) / Math.pow(10, casas);
|
|
2140
|
+
}
|
|
2141
|
+
static abs(valor) {
|
|
2142
|
+
return Math.abs(valor);
|
|
2143
|
+
}
|
|
2144
|
+
static potencia(base, expoente) {
|
|
2145
|
+
return Math.pow(base, expoente);
|
|
2146
|
+
}
|
|
2147
|
+
static raiz(valor) {
|
|
2148
|
+
return Math.sqrt(valor);
|
|
2149
|
+
}
|
|
2150
|
+
// ── Análise estatística ───────────────────────────────────
|
|
2151
|
+
/**
|
|
2152
|
+
* Curva ABC (classificação de Pareto)
|
|
2153
|
+
* Retorna cada item com sua classe (A, B ou C) baseado em percentual acumulado
|
|
2154
|
+
* Classe A: 0–80%, Classe B: 80–95%, Classe C: 95–100%
|
|
2155
|
+
*/
|
|
2156
|
+
static curvaABC(itens) {
|
|
2157
|
+
const total = itens.reduce((acc, i) => acc + i.valor, 0);
|
|
2158
|
+
if (total === 0) return [];
|
|
2159
|
+
const sorted = [...itens].sort((a, b) => b.valor - a.valor);
|
|
2160
|
+
let acumulado = 0;
|
|
2161
|
+
return sorted.map((item) => {
|
|
2162
|
+
const percentual = item.valor / total * 100;
|
|
2163
|
+
acumulado += percentual;
|
|
2164
|
+
const classe = acumulado <= 80 ? "A" : acumulado <= 95 ? "B" : "C";
|
|
2165
|
+
return {
|
|
2166
|
+
id: item.id,
|
|
2167
|
+
valor: item.valor,
|
|
2168
|
+
percentual: Math.round(percentual * 100) / 100,
|
|
2169
|
+
acumulado: Math.round(acumulado * 100) / 100,
|
|
2170
|
+
classe
|
|
2171
|
+
};
|
|
2172
|
+
});
|
|
2173
|
+
}
|
|
2174
|
+
/**
|
|
2175
|
+
* Percentil — retorna o valor no percentil p (0–100) da lista
|
|
2176
|
+
*/
|
|
2177
|
+
static percentil(lista, p) {
|
|
2178
|
+
if (lista.length === 0) return NaN;
|
|
2179
|
+
const sorted = [...lista].sort((a, b) => a - b);
|
|
2180
|
+
const index = p / 100 * (sorted.length - 1);
|
|
2181
|
+
const lower = Math.floor(index);
|
|
2182
|
+
const upper = Math.ceil(index);
|
|
2183
|
+
if (lower === upper) return sorted[lower];
|
|
2184
|
+
return sorted[lower] + (index - lower) * (sorted[upper] - sorted[lower]);
|
|
2185
|
+
}
|
|
2186
|
+
/**
|
|
2187
|
+
* Correlação de Pearson entre dois conjuntos de dados
|
|
2188
|
+
*/
|
|
2189
|
+
static correlacao(x, y) {
|
|
2190
|
+
if (x.length !== y.length || x.length === 0) return NaN;
|
|
2191
|
+
const mx = _MatematicaStdlib.media(x);
|
|
2192
|
+
const my = _MatematicaStdlib.media(y);
|
|
2193
|
+
const num = x.reduce((acc, xi, i) => acc + (xi - mx) * (y[i] - my), 0);
|
|
2194
|
+
const denX = Math.sqrt(x.reduce((acc, xi) => acc + Math.pow(xi - mx, 2), 0));
|
|
2195
|
+
const denY = Math.sqrt(y.reduce((acc, yi) => acc + Math.pow(yi - my, 2), 0));
|
|
2196
|
+
if (denX === 0 || denY === 0) return 0;
|
|
2197
|
+
return num / (denX * denY);
|
|
2198
|
+
}
|
|
2199
|
+
/**
|
|
2200
|
+
* Média móvel simples (SMA) — O(n) com janela deslizante
|
|
2201
|
+
*/
|
|
2202
|
+
static mediaM\u00F3vel(lista, janela) {
|
|
2203
|
+
if (janela <= 0 || janela > lista.length) return [];
|
|
2204
|
+
const resultado = [];
|
|
2205
|
+
let somaJanela = 0;
|
|
2206
|
+
for (let i = 0; i < janela; i++) somaJanela += lista[i];
|
|
2207
|
+
resultado.push(somaJanela / janela);
|
|
2208
|
+
for (let i = janela; i < lista.length; i++) {
|
|
2209
|
+
somaJanela += lista[i] - lista[i - janela];
|
|
2210
|
+
resultado.push(somaJanela / janela);
|
|
2211
|
+
}
|
|
2212
|
+
return resultado;
|
|
2213
|
+
}
|
|
2214
|
+
/**
|
|
2215
|
+
* Taxa de crescimento percentual entre dois valores
|
|
2216
|
+
*/
|
|
2217
|
+
static taxaCrescimento(valorInicial, valorFinal) {
|
|
2218
|
+
if (valorInicial === 0) return 0;
|
|
2219
|
+
return (valorFinal - valorInicial) / Math.abs(valorInicial) * 100;
|
|
2220
|
+
}
|
|
2221
|
+
// ── Análise preditiva ─────────────────────────────────────
|
|
2222
|
+
/**
|
|
2223
|
+
* Regressão linear simples — ajusta a reta y = a·x + b aos dados
|
|
2224
|
+
* @returns { a, b, r2 }
|
|
2225
|
+
* a = inclinação (tendência por período)
|
|
2226
|
+
* b = intercepto (valor inicial projetado)
|
|
2227
|
+
* r2 = coeficiente de determinação 0–1 (1 = ajuste perfeito)
|
|
2228
|
+
*
|
|
2229
|
+
* Uso típico: prever demanda futura a partir do histórico de vendas
|
|
2230
|
+
* const { a, b } = Matematica.regressaoLinear(vendas)
|
|
2231
|
+
* previsao = a * proximoPeriodo + b
|
|
2232
|
+
*/
|
|
2233
|
+
static regressaoLinear(y) {
|
|
2234
|
+
const n = y.length;
|
|
2235
|
+
if (n < 2) return { a: NaN, b: NaN, r2: NaN };
|
|
2236
|
+
const somaX = n * (n - 1) / 2;
|
|
2237
|
+
const somaX2 = n * (n - 1) * (2 * n - 1) / 6;
|
|
2238
|
+
const somaY = _MatematicaStdlib.soma(y);
|
|
2239
|
+
const somaXY = y.reduce((acc, yi, i) => acc + i * yi, 0);
|
|
2240
|
+
const den = n * somaX2 - somaX * somaX;
|
|
2241
|
+
if (den === 0) return { a: 0, b: somaY / n, r2: 0 };
|
|
2242
|
+
const a = (n * somaXY - somaX * somaY) / den;
|
|
2243
|
+
const b = (somaY - a * somaX) / n;
|
|
2244
|
+
const mediaY = somaY / n;
|
|
2245
|
+
const ss_tot = y.reduce((acc, yi) => acc + Math.pow(yi - mediaY, 2), 0);
|
|
2246
|
+
const ss_res = y.reduce((acc, yi, i) => acc + Math.pow(yi - (a * i + b), 2), 0);
|
|
2247
|
+
const r2 = ss_tot === 0 ? 1 : 1 - ss_res / ss_tot;
|
|
2248
|
+
return { a, b, r2 };
|
|
2249
|
+
}
|
|
2250
|
+
/**
|
|
2251
|
+
* Detecta outliers usando o método IQR (Intervalo Interquartil)
|
|
2252
|
+
* Outlier: valor abaixo de Q1 − 1.5×IQR ou acima de Q3 + 1.5×IQR
|
|
2253
|
+
* @returns { normais, outliers, q1, q3, iqr, limiteInferior, limiteSuperior }
|
|
2254
|
+
*
|
|
2255
|
+
* Uso típico: filtrar picos atípicos de venda antes de projetar estoque
|
|
2256
|
+
*/
|
|
2257
|
+
static detectarOutliers(lista) {
|
|
2258
|
+
if (lista.length === 0) {
|
|
2259
|
+
return { normais: [], outliers: [], q1: NaN, q3: NaN, iqr: NaN, limiteInferior: NaN, limiteSuperior: NaN };
|
|
2260
|
+
}
|
|
2261
|
+
const q1 = _MatematicaStdlib.percentil(lista, 25);
|
|
2262
|
+
const q3 = _MatematicaStdlib.percentil(lista, 75);
|
|
2263
|
+
const iqr = q3 - q1;
|
|
2264
|
+
const limiteInferior = q1 - 1.5 * iqr;
|
|
2265
|
+
const limiteSuperior = q3 + 1.5 * iqr;
|
|
2266
|
+
const normais = [];
|
|
2267
|
+
const outliers = [];
|
|
2268
|
+
for (const v of lista) {
|
|
2269
|
+
if (v < limiteInferior || v > limiteSuperior) {
|
|
2270
|
+
outliers.push(v);
|
|
2271
|
+
} else {
|
|
2272
|
+
normais.push(v);
|
|
2273
|
+
}
|
|
2274
|
+
}
|
|
2275
|
+
return { normais, outliers, q1, q3, iqr, limiteInferior, limiteSuperior };
|
|
2276
|
+
}
|
|
2277
|
+
// ── Matemática Financeira ─────────────────────────────────
|
|
2278
|
+
/**
|
|
2279
|
+
* Juros compostos — retorna o montante final
|
|
2280
|
+
* @param principal Valor inicial (ex: 10000)
|
|
2281
|
+
* @param taxa Taxa por período como decimal (ex: 0.12 = 12% a.a.)
|
|
2282
|
+
* @param tempo Número de períodos (ex: 5 anos)
|
|
2283
|
+
* @returns Montante: principal × (1 + taxa)^tempo
|
|
2284
|
+
*/
|
|
2285
|
+
static jurosCompostos(principal, taxa, tempo) {
|
|
2286
|
+
return principal * Math.pow(1 + taxa, tempo);
|
|
2287
|
+
}
|
|
2288
|
+
/**
|
|
2289
|
+
* Valor Presente Líquido (VPL / NPV)
|
|
2290
|
+
* @param fluxoCaixa Array de fluxos de caixa — índice 0 = t=0 (investimento inicial, geralmente negativo)
|
|
2291
|
+
* @param taxa Taxa de desconto por período como decimal (ex: 0.1 = 10%)
|
|
2292
|
+
* @returns VPL — positivo indica projeto viável
|
|
2293
|
+
*
|
|
2294
|
+
* Exemplo: VPL de -10000 hoje, +4000 nos próximos 4 anos com taxa 10%:
|
|
2295
|
+
* valorPresenteLiquido([-10000, 4000, 4000, 4000, 4000], 0.10)
|
|
2296
|
+
*/
|
|
2297
|
+
static valorPresenteLiquido(fluxoCaixa, taxa) {
|
|
2298
|
+
if (fluxoCaixa.length === 0) return NaN;
|
|
2299
|
+
return fluxoCaixa.reduce((vpl, fc, t) => vpl + fc / Math.pow(1 + taxa, t), 0);
|
|
2300
|
+
}
|
|
2301
|
+
};
|
|
2302
|
+
var MatematicaMetodos = {
|
|
2303
|
+
soma: MatematicaStdlib.soma,
|
|
2304
|
+
media: MatematicaStdlib.media,
|
|
2305
|
+
mediana: MatematicaStdlib.mediana,
|
|
2306
|
+
desvioPadrao: MatematicaStdlib.desvioPadrao,
|
|
2307
|
+
variancia: MatematicaStdlib.variancia,
|
|
2308
|
+
minimo: MatematicaStdlib.minimo,
|
|
2309
|
+
maximo: MatematicaStdlib.maximo,
|
|
2310
|
+
arredondar: MatematicaStdlib.arredondar,
|
|
2311
|
+
abs: MatematicaStdlib.abs,
|
|
2312
|
+
potencia: MatematicaStdlib.potencia,
|
|
2313
|
+
raiz: MatematicaStdlib.raiz,
|
|
2314
|
+
curvaABC: MatematicaStdlib.curvaABC,
|
|
2315
|
+
percentil: MatematicaStdlib.percentil,
|
|
2316
|
+
correlacao: MatematicaStdlib.correlacao,
|
|
2317
|
+
mediaM\u00F3vel: MatematicaStdlib.mediaM\u00F3vel,
|
|
2318
|
+
taxaCrescimento: MatematicaStdlib.taxaCrescimento,
|
|
2319
|
+
regressaoLinear: MatematicaStdlib.regressaoLinear,
|
|
2320
|
+
detectarOutliers: MatematicaStdlib.detectarOutliers,
|
|
2321
|
+
jurosCompostos: MatematicaStdlib.jurosCompostos,
|
|
2322
|
+
valorPresenteLiquido: MatematicaStdlib.valorPresenteLiquido
|
|
2323
|
+
};
|
|
2324
|
+
|
|
2325
|
+
// stdlib/fiscal.ts
|
|
2326
|
+
function calcularICMS(baseCalculo, aliquota) {
|
|
2327
|
+
validarPositivo(baseCalculo, "baseCalculo");
|
|
2328
|
+
validarAliquota(aliquota);
|
|
2329
|
+
const valor = arredondar(baseCalculo * aliquota);
|
|
2330
|
+
return {
|
|
2331
|
+
baseCalculo,
|
|
2332
|
+
aliquota,
|
|
2333
|
+
valor,
|
|
2334
|
+
valorLiquido: arredondar(baseCalculo - valor)
|
|
2335
|
+
};
|
|
2336
|
+
}
|
|
2337
|
+
function calcularICMSST(valorProduto, aliquotaInterna, aliquotaInterestadual, mva) {
|
|
2338
|
+
validarPositivo(valorProduto, "valorProduto");
|
|
2339
|
+
validarAliquota(aliquotaInterna);
|
|
2340
|
+
validarAliquota(aliquotaInterestadual);
|
|
2341
|
+
if (mva < 0) throw new Error("MVA n\xE3o pode ser negativo");
|
|
2342
|
+
const icmsPropio = arredondar(valorProduto * aliquotaInterestadual);
|
|
2343
|
+
const baseCalculoST = arredondar(valorProduto * (1 + mva));
|
|
2344
|
+
const icmsST = arredondar(baseCalculoST * aliquotaInterna - icmsPropio);
|
|
2345
|
+
return { icmsPropio, baseCalculoST, icmsST: Math.max(0, icmsST) };
|
|
2346
|
+
}
|
|
2347
|
+
function calcularPISCOFINS(baseCalculo, aliquotaPIS = 65e-4, aliquotaCOFINS = 0.03) {
|
|
2348
|
+
validarPositivo(baseCalculo, "baseCalculo");
|
|
2349
|
+
validarAliquota(aliquotaPIS);
|
|
2350
|
+
validarAliquota(aliquotaCOFINS);
|
|
2351
|
+
const valorPIS = arredondar(baseCalculo * aliquotaPIS);
|
|
2352
|
+
const valorCOFINS = arredondar(baseCalculo * aliquotaCOFINS);
|
|
2353
|
+
return {
|
|
2354
|
+
baseCalculo,
|
|
2355
|
+
aliquotaPIS,
|
|
2356
|
+
aliquotaCOFINS,
|
|
2357
|
+
valorPIS,
|
|
2358
|
+
valorCOFINS,
|
|
2359
|
+
totalPISCOFINS: arredondar(valorPIS + valorCOFINS)
|
|
2360
|
+
};
|
|
2361
|
+
}
|
|
2362
|
+
function calcularPISCOFINSNaoCumulativo(baseCalculo, creditos = 0, aliquotaPIS = 0.0165, aliquotaCOFINS = 0.076) {
|
|
2363
|
+
const base = calcularPISCOFINS(baseCalculo, aliquotaPIS, aliquotaCOFINS);
|
|
2364
|
+
const totalLiquido = arredondar(Math.max(0, base.totalPISCOFINS - creditos));
|
|
2365
|
+
return { ...base, creditos, totalLiquido };
|
|
2366
|
+
}
|
|
2367
|
+
function calcularISS(baseCalculo, aliquota) {
|
|
2368
|
+
validarPositivo(baseCalculo, "baseCalculo");
|
|
2369
|
+
if (aliquota < 0.02 || aliquota > 0.05) {
|
|
2370
|
+
throw new Error(`Al\xEDquota ISS inv\xE1lida: ${(aliquota * 100).toFixed(2)}%. Deve estar entre 2% e 5%`);
|
|
2371
|
+
}
|
|
2372
|
+
const valor = arredondar(baseCalculo * aliquota);
|
|
2373
|
+
return {
|
|
2374
|
+
baseCalculo,
|
|
2375
|
+
aliquota,
|
|
2376
|
+
valor,
|
|
2377
|
+
valorLiquido: arredondar(baseCalculo - valor)
|
|
2378
|
+
};
|
|
2379
|
+
}
|
|
2380
|
+
function calcularIPI(valorProduto, aliquota) {
|
|
2381
|
+
validarPositivo(valorProduto, "valorProduto");
|
|
2382
|
+
validarAliquota(aliquota);
|
|
2383
|
+
const valorIPI = arredondar(valorProduto * aliquota);
|
|
2384
|
+
return {
|
|
2385
|
+
valorProduto,
|
|
2386
|
+
aliquota,
|
|
2387
|
+
valorIPI,
|
|
2388
|
+
valorTotal: arredondar(valorProduto + valorIPI)
|
|
2389
|
+
};
|
|
2390
|
+
}
|
|
2391
|
+
function calcularTotaisNF(itens, aliquotaPIS = 65e-4, aliquotaCOFINS = 0.03) {
|
|
2392
|
+
if (itens.length === 0) throw new Error("Nota fiscal sem itens");
|
|
2393
|
+
let totalProdutosCentavos = 0;
|
|
2394
|
+
let totalICMSCentavos = 0;
|
|
2395
|
+
let totalIPICentavos = 0;
|
|
2396
|
+
for (const item of itens) {
|
|
2397
|
+
validarPositivo(item.quantidade, "quantidade");
|
|
2398
|
+
validarPositivo(item.valorUnitario, "valorUnitario");
|
|
2399
|
+
const subtotal = MoedaStdlib.multiplicar(item.valorUnitario, item.quantidade);
|
|
2400
|
+
totalProdutosCentavos += MoedaStdlib.toCentavos(subtotal);
|
|
2401
|
+
if (item.aliquotaICMS !== void 0) {
|
|
2402
|
+
totalICMSCentavos += MoedaStdlib.toCentavos(calcularICMS(subtotal, item.aliquotaICMS).valor);
|
|
2403
|
+
}
|
|
2404
|
+
if (item.aliquotaIPI !== void 0) {
|
|
2405
|
+
totalIPICentavos += MoedaStdlib.toCentavos(calcularIPI(subtotal, item.aliquotaIPI).valorIPI);
|
|
2406
|
+
}
|
|
2407
|
+
}
|
|
2408
|
+
const totalProdutos = MoedaStdlib.fromCentavos(totalProdutosCentavos);
|
|
2409
|
+
const totalICMS = MoedaStdlib.fromCentavos(totalICMSCentavos);
|
|
2410
|
+
const totalIPI = MoedaStdlib.fromCentavos(totalIPICentavos);
|
|
2411
|
+
const pisCofins = calcularPISCOFINS(totalProdutos, aliquotaPIS, aliquotaCOFINS);
|
|
2412
|
+
return {
|
|
2413
|
+
totalProdutos,
|
|
2414
|
+
totalICMS,
|
|
2415
|
+
totalIPI,
|
|
2416
|
+
totalPIS: pisCofins.valorPIS,
|
|
2417
|
+
totalCOFINS: pisCofins.valorCOFINS,
|
|
2418
|
+
totalNF: MoedaStdlib.fromCentavos(totalProdutosCentavos + totalIPICentavos)
|
|
2419
|
+
};
|
|
2420
|
+
}
|
|
2421
|
+
function arredondar(valor) {
|
|
2422
|
+
return Math.round(valor * 100) / 100;
|
|
2423
|
+
}
|
|
2424
|
+
function validarPositivo(valor, nome) {
|
|
2425
|
+
if (typeof valor !== "number" || isNaN(valor) || valor < 0) {
|
|
2426
|
+
throw new Error(`'${nome}' deve ser um n\xFAmero n\xE3o-negativo`);
|
|
2427
|
+
}
|
|
2428
|
+
}
|
|
2429
|
+
function validarAliquota(aliquota) {
|
|
2430
|
+
if (typeof aliquota !== "number" || isNaN(aliquota) || aliquota < 0 || aliquota > 1) {
|
|
2431
|
+
throw new Error(`Al\xEDquota inv\xE1lida: ${aliquota}. Deve ser um decimal entre 0 e 1`);
|
|
2432
|
+
}
|
|
2433
|
+
}
|
|
2434
|
+
|
|
2435
|
+
// stdlib/wms.ts
|
|
2436
|
+
function validarEAN13(codigo) {
|
|
2437
|
+
const limpo = codigo.replace(/\D/g, "");
|
|
2438
|
+
if (limpo.length !== 13) {
|
|
2439
|
+
return { valido: false, formato: "EAN-13", mensagem: `EAN-13 deve ter 13 d\xEDgitos, recebeu ${limpo.length}` };
|
|
2440
|
+
}
|
|
2441
|
+
const digito = calcularDigitoEAN(limpo.slice(0, 12));
|
|
2442
|
+
const valido = digito === parseInt(limpo[12], 10);
|
|
2443
|
+
return {
|
|
2444
|
+
valido,
|
|
2445
|
+
formato: "EAN-13",
|
|
2446
|
+
mensagem: valido ? void 0 : `D\xEDgito verificador inv\xE1lido: esperado ${digito}, recebeu ${limpo[12]}`
|
|
2447
|
+
};
|
|
2448
|
+
}
|
|
2449
|
+
function validarEAN8(codigo) {
|
|
2450
|
+
const limpo = codigo.replace(/\D/g, "");
|
|
2451
|
+
if (limpo.length !== 8) {
|
|
2452
|
+
return { valido: false, formato: "EAN-8", mensagem: `EAN-8 deve ter 8 d\xEDgitos, recebeu ${limpo.length}` };
|
|
2453
|
+
}
|
|
2454
|
+
const digito = calcularDigitoEAN(limpo.slice(0, 7));
|
|
2455
|
+
const valido = digito === parseInt(limpo[7], 10);
|
|
2456
|
+
return {
|
|
2457
|
+
valido,
|
|
2458
|
+
formato: "EAN-8",
|
|
2459
|
+
mensagem: valido ? void 0 : `D\xEDgito verificador inv\xE1lido: esperado ${digito}, recebeu ${limpo[7]}`
|
|
2460
|
+
};
|
|
2461
|
+
}
|
|
2462
|
+
function gerarEAN13(base12) {
|
|
2463
|
+
const limpo = base12.replace(/\D/g, "");
|
|
2464
|
+
if (limpo.length !== 12) {
|
|
2465
|
+
throw new Error(`Base EAN-13 deve ter 12 d\xEDgitos, recebeu ${limpo.length}`);
|
|
2466
|
+
}
|
|
2467
|
+
const digito = calcularDigitoEAN(limpo);
|
|
2468
|
+
return limpo + digito;
|
|
2469
|
+
}
|
|
2470
|
+
function gerarEAN8(base7) {
|
|
2471
|
+
const limpo = base7.replace(/\D/g, "");
|
|
2472
|
+
if (limpo.length !== 7) {
|
|
2473
|
+
throw new Error(`Base EAN-8 deve ter 7 d\xEDgitos, recebeu ${limpo.length}`);
|
|
2474
|
+
}
|
|
2475
|
+
const digito = calcularDigitoEAN(limpo);
|
|
2476
|
+
return limpo + digito;
|
|
2477
|
+
}
|
|
2478
|
+
function validarCode128(codigo) {
|
|
2479
|
+
if (!codigo || codigo.length === 0) {
|
|
2480
|
+
return { valido: false, formato: "Code128", mensagem: "C\xF3digo vazio" };
|
|
2481
|
+
}
|
|
2482
|
+
if (codigo.length > 80) {
|
|
2483
|
+
return { valido: false, formato: "Code128", mensagem: `Code128 m\xE1ximo 80 caracteres, recebeu ${codigo.length}` };
|
|
2484
|
+
}
|
|
2485
|
+
const invalido = [...codigo].find((c) => c.charCodeAt(0) < 32 || c.charCodeAt(0) > 126);
|
|
2486
|
+
if (invalido) {
|
|
2487
|
+
return { valido: false, formato: "Code128", mensagem: `Caractere inv\xE1lido: '${invalido}' (ASCII ${invalido.charCodeAt(0)})` };
|
|
2488
|
+
}
|
|
2489
|
+
return { valido: true, formato: "Code128" };
|
|
2490
|
+
}
|
|
2491
|
+
function validarCodigoBarras(codigo) {
|
|
2492
|
+
const limpo = codigo.replace(/\D/g, "");
|
|
2493
|
+
if (limpo.length === 13) return validarEAN13(codigo);
|
|
2494
|
+
if (limpo.length === 8) return validarEAN8(codigo);
|
|
2495
|
+
return validarCode128(codigo);
|
|
2496
|
+
}
|
|
2497
|
+
function criarGrade(corredores, prateleiras, niveis, capacidadeKg, capacidadeM3) {
|
|
2498
|
+
if (corredores.length === 0) throw new Error("Armaz\xE9m deve ter ao menos 1 corredor");
|
|
2499
|
+
if (prateleiras < 1) throw new Error("Prateleiras deve ser >= 1");
|
|
2500
|
+
if (niveis < 1) throw new Error("N\xEDveis deve ser >= 1");
|
|
2501
|
+
if (capacidadeKg <= 0) throw new Error("capacidadeKg deve ser positivo");
|
|
2502
|
+
if (capacidadeM3 <= 0) throw new Error("capacidadeM3 deve ser positivo");
|
|
2503
|
+
const posicoes = /* @__PURE__ */ new Map();
|
|
2504
|
+
for (const corredor of corredores) {
|
|
2505
|
+
for (let p = 1; p <= prateleiras; p++) {
|
|
2506
|
+
for (let n = 1; n <= niveis; n++) {
|
|
2507
|
+
const codigo = formatarEndereco({ corredor, prateleira: p, nivel: n });
|
|
2508
|
+
posicoes.set(codigo, {
|
|
2509
|
+
corredor,
|
|
2510
|
+
prateleira: p,
|
|
2511
|
+
nivel: n,
|
|
2512
|
+
codigo,
|
|
2513
|
+
capacidadeKg,
|
|
2514
|
+
capacidadeM3,
|
|
2515
|
+
ocupadoKg: 0,
|
|
2516
|
+
ocupadoM3: 0,
|
|
2517
|
+
disponivel: true
|
|
2518
|
+
});
|
|
2519
|
+
}
|
|
2520
|
+
}
|
|
2521
|
+
}
|
|
2522
|
+
return {
|
|
2523
|
+
corredores,
|
|
2524
|
+
prateleirasPorCorredor: prateleiras,
|
|
2525
|
+
niveisPorPrateleira: niveis,
|
|
2526
|
+
capacidadePadraoKg: capacidadeKg,
|
|
2527
|
+
capacidadePadraoM3: capacidadeM3,
|
|
2528
|
+
posicoes
|
|
2529
|
+
};
|
|
2530
|
+
}
|
|
2531
|
+
function formatarEndereco(end) {
|
|
2532
|
+
return `${end.corredor}-${String(end.prateleira).padStart(3, "0")}-${end.nivel}`;
|
|
2533
|
+
}
|
|
2534
|
+
function parsearEndereco(codigo) {
|
|
2535
|
+
const partes = codigo.split("-");
|
|
2536
|
+
if (partes.length !== 3) throw new Error(`Endere\xE7o inv\xE1lido: '${codigo}'. Formato: CORREDOR-PRATELEIRA-NIVEL`);
|
|
2537
|
+
const corredor = partes[0];
|
|
2538
|
+
const prateleira = parseInt(partes[1], 10);
|
|
2539
|
+
const nivel = parseInt(partes[2], 10);
|
|
2540
|
+
if (!corredor || isNaN(prateleira) || isNaN(nivel)) {
|
|
2541
|
+
throw new Error(`Endere\xE7o inv\xE1lido: '${codigo}'`);
|
|
2542
|
+
}
|
|
2543
|
+
return { corredor, prateleira, nivel };
|
|
2544
|
+
}
|
|
2545
|
+
function alocarPosicao(grade, codigo, pesoKg, volumeM3) {
|
|
2546
|
+
const pos = grade.posicoes.get(codigo);
|
|
2547
|
+
if (!pos) return { sucesso: false, mensagem: `Posi\xE7\xE3o '${codigo}' n\xE3o existe` };
|
|
2548
|
+
if (pos.ocupadoKg + pesoKg > pos.capacidadeKg) {
|
|
2549
|
+
return {
|
|
2550
|
+
sucesso: false,
|
|
2551
|
+
mensagem: `Peso excede capacidade: ${pos.ocupadoKg + pesoKg}kg > ${pos.capacidadeKg}kg`
|
|
2552
|
+
};
|
|
2553
|
+
}
|
|
2554
|
+
if (pos.ocupadoM3 + volumeM3 > pos.capacidadeM3) {
|
|
2555
|
+
return {
|
|
2556
|
+
sucesso: false,
|
|
2557
|
+
mensagem: `Volume excede capacidade: ${(pos.ocupadoM3 + volumeM3).toFixed(3)}m\xB3 > ${pos.capacidadeM3}m\xB3`
|
|
2558
|
+
};
|
|
2559
|
+
}
|
|
2560
|
+
pos.ocupadoKg += pesoKg;
|
|
2561
|
+
pos.ocupadoM3 += volumeM3;
|
|
2562
|
+
pos.disponivel = pos.ocupadoKg < pos.capacidadeKg && pos.ocupadoM3 < pos.capacidadeM3;
|
|
2563
|
+
return { sucesso: true };
|
|
2564
|
+
}
|
|
2565
|
+
function liberarPosicao(grade, codigo) {
|
|
2566
|
+
const pos = grade.posicoes.get(codigo);
|
|
2567
|
+
if (!pos) throw new Error(`Posi\xE7\xE3o '${codigo}' n\xE3o existe`);
|
|
2568
|
+
pos.ocupadoKg = 0;
|
|
2569
|
+
pos.ocupadoM3 = 0;
|
|
2570
|
+
pos.disponivel = true;
|
|
2571
|
+
}
|
|
2572
|
+
function sugerirPosicao(grade, pesoKg, volumeM3) {
|
|
2573
|
+
let melhor = null;
|
|
2574
|
+
for (const pos of grade.posicoes.values()) {
|
|
2575
|
+
if (!pos.disponivel) continue;
|
|
2576
|
+
if (pos.ocupadoKg + pesoKg > pos.capacidadeKg) continue;
|
|
2577
|
+
if (pos.ocupadoM3 + volumeM3 > pos.capacidadeM3) continue;
|
|
2578
|
+
if (!melhor || pos.nivel < melhor.nivel || pos.nivel === melhor.nivel && pos.prateleira < melhor.prateleira) {
|
|
2579
|
+
melhor = pos;
|
|
2580
|
+
}
|
|
2581
|
+
}
|
|
2582
|
+
return melhor;
|
|
2583
|
+
}
|
|
2584
|
+
function estatisticasArmazem(grade) {
|
|
2585
|
+
let totalKgOcupado = 0;
|
|
2586
|
+
let totalM3Ocupado = 0;
|
|
2587
|
+
let totalKgCapacidade = 0;
|
|
2588
|
+
let totalM3Capacidade = 0;
|
|
2589
|
+
let posicoesDisponiveis = 0;
|
|
2590
|
+
for (const pos of grade.posicoes.values()) {
|
|
2591
|
+
totalKgOcupado += pos.ocupadoKg;
|
|
2592
|
+
totalM3Ocupado += pos.ocupadoM3;
|
|
2593
|
+
totalKgCapacidade += pos.capacidadeKg;
|
|
2594
|
+
totalM3Capacidade += pos.capacidadeM3;
|
|
2595
|
+
if (pos.disponivel) posicoesDisponiveis++;
|
|
2596
|
+
}
|
|
2597
|
+
const totalPosicoes = grade.posicoes.size;
|
|
2598
|
+
const posicoesOcupadas = totalPosicoes - posicoesDisponiveis;
|
|
2599
|
+
return {
|
|
2600
|
+
totalPosicoes,
|
|
2601
|
+
posicoesDisponiveis,
|
|
2602
|
+
posicoesOcupadas,
|
|
2603
|
+
ocupacaoPercent: totalPosicoes > 0 ? Math.round(posicoesOcupadas / totalPosicoes * 100) : 0,
|
|
2604
|
+
totalKgOcupado: Math.round(totalKgOcupado * 100) / 100,
|
|
2605
|
+
totalM3Ocupado: Math.round(totalM3Ocupado * 1e3) / 1e3,
|
|
2606
|
+
totalKgCapacidade,
|
|
2607
|
+
totalM3Capacidade
|
|
2608
|
+
};
|
|
2609
|
+
}
|
|
2610
|
+
function calcularDigitoEAN(digits) {
|
|
2611
|
+
let soma = 0;
|
|
2612
|
+
for (let i = 0; i < digits.length; i++) {
|
|
2613
|
+
const d = parseInt(digits[i], 10);
|
|
2614
|
+
soma += i % 2 === 0 ? d : d * 3;
|
|
2615
|
+
}
|
|
2616
|
+
return (10 - soma % 10) % 10;
|
|
2617
|
+
}
|
|
2618
|
+
|
|
2619
|
+
// apis/http_client.ts
|
|
2620
|
+
var HttpClient = class {
|
|
2621
|
+
defaultHeaders = {
|
|
2622
|
+
"Content-Type": "application/json"
|
|
2623
|
+
};
|
|
2624
|
+
interceptors = [];
|
|
2625
|
+
// Define headers padrão (ex: Authorization)
|
|
2626
|
+
setDefaultHeaders(headers) {
|
|
2627
|
+
this.defaultHeaders = { ...this.defaultHeaders, ...headers };
|
|
2628
|
+
}
|
|
2629
|
+
// Adiciona interceptor de request
|
|
2630
|
+
addInterceptor(fn) {
|
|
2631
|
+
this.interceptors.push(fn);
|
|
2632
|
+
}
|
|
2633
|
+
async get(url, options) {
|
|
2634
|
+
return this.request({ method: "GET", url, ...options });
|
|
2635
|
+
}
|
|
2636
|
+
async post(url, data, options) {
|
|
2637
|
+
return this.request({ method: "POST", url, data, ...options });
|
|
2638
|
+
}
|
|
2639
|
+
async put(url, data, options) {
|
|
2640
|
+
return this.request({ method: "PUT", url, data, ...options });
|
|
2641
|
+
}
|
|
2642
|
+
async delete(url, options) {
|
|
2643
|
+
return this.request({ method: "DELETE", url, ...options });
|
|
2644
|
+
}
|
|
2645
|
+
async request(config) {
|
|
2646
|
+
this.validateUrl(config.url);
|
|
2647
|
+
let finalConfig = { ...config };
|
|
2648
|
+
for (const interceptor of this.interceptors) {
|
|
2649
|
+
finalConfig = interceptor(finalConfig);
|
|
2650
|
+
}
|
|
2651
|
+
let url = finalConfig.url;
|
|
2652
|
+
if (finalConfig.params) {
|
|
2653
|
+
const query = new URLSearchParams(
|
|
2654
|
+
Object.fromEntries(
|
|
2655
|
+
Object.entries(finalConfig.params).map(([k, v]) => [k, String(v)])
|
|
2656
|
+
)
|
|
2657
|
+
).toString();
|
|
2658
|
+
url += (url.includes("?") ? "&" : "?") + query;
|
|
2659
|
+
}
|
|
2660
|
+
const headers = { ...this.defaultHeaders, ...finalConfig.headers || {} };
|
|
2661
|
+
const retries = finalConfig.retries ?? 0;
|
|
2662
|
+
let lastError = null;
|
|
2663
|
+
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
2664
|
+
try {
|
|
2665
|
+
const controller = new AbortController();
|
|
2666
|
+
const timeoutMs = finalConfig.timeout ?? 3e4;
|
|
2667
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
2668
|
+
const response = await fetch(url, {
|
|
2669
|
+
method: finalConfig.method,
|
|
2670
|
+
headers,
|
|
2671
|
+
body: finalConfig.data ? JSON.stringify(finalConfig.data) : void 0,
|
|
2672
|
+
signal: controller.signal
|
|
2673
|
+
});
|
|
2674
|
+
clearTimeout(timer);
|
|
2675
|
+
const responseHeaders = {};
|
|
2676
|
+
response.headers.forEach((value, key) => {
|
|
2677
|
+
responseHeaders[key] = value;
|
|
2678
|
+
});
|
|
2679
|
+
let data;
|
|
2680
|
+
const contentType = response.headers.get("content-type") || "";
|
|
2681
|
+
if (contentType.includes("application/json")) {
|
|
2682
|
+
data = await response.json();
|
|
2683
|
+
} else {
|
|
2684
|
+
data = await response.text();
|
|
2685
|
+
}
|
|
2686
|
+
return {
|
|
2687
|
+
data,
|
|
2688
|
+
status: response.status,
|
|
2689
|
+
headers: responseHeaders,
|
|
2690
|
+
ok: response.ok
|
|
2691
|
+
};
|
|
2692
|
+
} catch (e) {
|
|
2693
|
+
lastError = e;
|
|
2694
|
+
if (attempt < retries) {
|
|
2695
|
+
await new Promise((r) => setTimeout(r, Math.pow(2, attempt) * 100));
|
|
2696
|
+
}
|
|
2697
|
+
}
|
|
2698
|
+
}
|
|
2699
|
+
throw lastError ?? new Error("Requisi\xE7\xE3o falhou");
|
|
2700
|
+
}
|
|
2701
|
+
validateUrl(url) {
|
|
2702
|
+
let parsed;
|
|
2703
|
+
try {
|
|
2704
|
+
parsed = new URL(url);
|
|
2705
|
+
} catch {
|
|
2706
|
+
throw new Error(`URL inv\xE1lida: ${url}`);
|
|
2707
|
+
}
|
|
2708
|
+
if (!["http:", "https:"].includes(parsed.protocol)) {
|
|
2709
|
+
throw new Error(`Protocolo n\xE3o permitido: ${parsed.protocol}`);
|
|
2710
|
+
}
|
|
2711
|
+
const host = parsed.hostname.toLowerCase().replace(/^\[|\]$/g, "");
|
|
2712
|
+
if (host === "localhost" || host === "::1" || host.endsWith(".localhost")) {
|
|
2713
|
+
throw new Error("Requisi\xE7\xE3o bloqueada: destino interno n\xE3o permitido");
|
|
2714
|
+
}
|
|
2715
|
+
const ipv4 = host.match(/^(\d+)\.(\d+)\.(\d+)\.(\d+)$/);
|
|
2716
|
+
if (ipv4) {
|
|
2717
|
+
const [a, b] = [Number(ipv4[1]), Number(ipv4[2])];
|
|
2718
|
+
const privado = a === 127 || // 127.0.0.0/8 loopback
|
|
2719
|
+
a === 10 || // 10.0.0.0/8
|
|
2720
|
+
a === 0 || // 0.0.0.0/8
|
|
2721
|
+
a === 172 && b >= 16 && b <= 31 || // 172.16.0.0/12
|
|
2722
|
+
a === 192 && b === 168 || // 192.168.0.0/16
|
|
2723
|
+
a === 169 && b === 254;
|
|
2724
|
+
if (privado) {
|
|
2725
|
+
throw new Error("Requisi\xE7\xE3o bloqueada: endere\xE7o IP privado n\xE3o permitido");
|
|
2726
|
+
}
|
|
2727
|
+
}
|
|
2728
|
+
if (/^(::1$|fc|fd|fe80)/i.test(host)) {
|
|
2729
|
+
throw new Error("Requisi\xE7\xE3o bloqueada: endere\xE7o IPv6 privado n\xE3o permitido");
|
|
2730
|
+
}
|
|
2731
|
+
}
|
|
2732
|
+
};
|
|
2733
|
+
|
|
2734
|
+
// apis/console_api.ts
|
|
2735
|
+
var ConsoleAPI = class {
|
|
2736
|
+
minLevel = "debug";
|
|
2737
|
+
currentGroup = null;
|
|
2738
|
+
timers = /* @__PURE__ */ new Map();
|
|
2739
|
+
counters = /* @__PURE__ */ new Map();
|
|
2740
|
+
history = [];
|
|
2741
|
+
maxHistory = 1e3;
|
|
2742
|
+
levels = {
|
|
2743
|
+
debug: 0,
|
|
2744
|
+
info: 1,
|
|
2745
|
+
warn: 2,
|
|
2746
|
+
error: 3
|
|
2747
|
+
};
|
|
2748
|
+
setLevel(level) {
|
|
2749
|
+
this.minLevel = level;
|
|
2750
|
+
}
|
|
2751
|
+
getHistory() {
|
|
2752
|
+
return [...this.history];
|
|
2753
|
+
}
|
|
2754
|
+
// ── Métodos em português (API pública JADE) ──────────────────
|
|
2755
|
+
escrever(...args) {
|
|
2756
|
+
this.log("info", ...args);
|
|
2757
|
+
}
|
|
2758
|
+
avisar(...args) {
|
|
2759
|
+
this.log("warn", ...args);
|
|
2760
|
+
}
|
|
2761
|
+
erro(...args) {
|
|
2762
|
+
this.log("error", ...args);
|
|
2763
|
+
}
|
|
2764
|
+
informar(...args) {
|
|
2765
|
+
this.log("info", ...args);
|
|
2766
|
+
}
|
|
2767
|
+
depurar(...args) {
|
|
2768
|
+
this.log("debug", ...args);
|
|
2769
|
+
}
|
|
2770
|
+
// ── Aliases em inglês (uso interno / interop) ─────────────────
|
|
2771
|
+
debug(...args) {
|
|
2772
|
+
this.log("debug", ...args);
|
|
2773
|
+
}
|
|
2774
|
+
info(...args) {
|
|
2775
|
+
this.log("info", ...args);
|
|
2776
|
+
}
|
|
2777
|
+
warn(...args) {
|
|
2778
|
+
this.log("warn", ...args);
|
|
2779
|
+
}
|
|
2780
|
+
error(...args) {
|
|
2781
|
+
this.log("error", ...args);
|
|
2782
|
+
}
|
|
2783
|
+
log(level, ...args) {
|
|
2784
|
+
if (this.levels[level] < this.levels[this.minLevel]) return;
|
|
2785
|
+
const message = args.map(
|
|
2786
|
+
(a) => typeof a === "object" ? JSON.stringify(a) : String(a)
|
|
2787
|
+
).join(" ");
|
|
2788
|
+
const indent = this.currentGroup ? " " : "";
|
|
2789
|
+
const prefix = this.currentGroup ? `[${this.currentGroup}] ` : "";
|
|
2790
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
2791
|
+
const entry = {
|
|
2792
|
+
level,
|
|
2793
|
+
message: `${prefix}${message}`,
|
|
2794
|
+
args,
|
|
2795
|
+
timestamp,
|
|
2796
|
+
group: this.currentGroup ?? void 0
|
|
2797
|
+
};
|
|
2798
|
+
this.history.push(entry);
|
|
2799
|
+
if (this.history.length > this.maxHistory) {
|
|
2800
|
+
this.history.shift();
|
|
2801
|
+
}
|
|
2802
|
+
const consoleFn = level === "debug" ? console.debug : level === "warn" ? console.warn : level === "error" ? console.error : console.log;
|
|
2803
|
+
consoleFn(`[JADE ${level.toUpperCase()}] ${indent}${prefix}${message}`);
|
|
2804
|
+
}
|
|
2805
|
+
// ── Métodos de visualização (português) ──────────────────────
|
|
2806
|
+
tabela(dados) {
|
|
2807
|
+
this.table(dados);
|
|
2808
|
+
}
|
|
2809
|
+
grupo(rotulo) {
|
|
2810
|
+
this.group(rotulo);
|
|
2811
|
+
}
|
|
2812
|
+
fimGrupo() {
|
|
2813
|
+
this.groupEnd();
|
|
2814
|
+
}
|
|
2815
|
+
tempo(rotulo = "padr\xE3o") {
|
|
2816
|
+
this.time(rotulo);
|
|
2817
|
+
}
|
|
2818
|
+
fimTempo(rotulo = "padr\xE3o") {
|
|
2819
|
+
return this.timeEnd(rotulo);
|
|
2820
|
+
}
|
|
2821
|
+
contar(rotulo = "padr\xE3o") {
|
|
2822
|
+
return this.count(rotulo);
|
|
2823
|
+
}
|
|
2824
|
+
resetarContador(rotulo = "padr\xE3o") {
|
|
2825
|
+
this.countReset(rotulo);
|
|
2826
|
+
}
|
|
2827
|
+
afirmar(condicao, mensagem) {
|
|
2828
|
+
this.assert(condicao, mensagem);
|
|
2829
|
+
}
|
|
2830
|
+
limpar() {
|
|
2831
|
+
this.clear();
|
|
2832
|
+
}
|
|
2833
|
+
// ── Aliases em inglês (interop) ──────────────────────────────
|
|
2834
|
+
table(data) {
|
|
2835
|
+
console.table(data);
|
|
2836
|
+
}
|
|
2837
|
+
group(label) {
|
|
2838
|
+
this.currentGroup = label ?? "grupo";
|
|
2839
|
+
console.group(label);
|
|
2840
|
+
}
|
|
2841
|
+
groupEnd() {
|
|
2842
|
+
this.currentGroup = null;
|
|
2843
|
+
console.groupEnd();
|
|
2844
|
+
}
|
|
2845
|
+
time(label = "default") {
|
|
2846
|
+
this.timers.set(label, Date.now());
|
|
2847
|
+
}
|
|
2848
|
+
timeEnd(label = "default") {
|
|
2849
|
+
const start = this.timers.get(label);
|
|
2850
|
+
if (start === void 0) {
|
|
2851
|
+
this.warn(`Timer '${label}' n\xE3o iniciado`);
|
|
2852
|
+
return 0;
|
|
2853
|
+
}
|
|
2854
|
+
const elapsed = Date.now() - start;
|
|
2855
|
+
this.timers.delete(label);
|
|
2856
|
+
this.info(`${label}: ${elapsed}ms`);
|
|
2857
|
+
return elapsed;
|
|
2858
|
+
}
|
|
2859
|
+
count(label = "default") {
|
|
2860
|
+
const current = (this.counters.get(label) ?? 0) + 1;
|
|
2861
|
+
this.counters.set(label, current);
|
|
2862
|
+
this.info(`${label}: ${current}`);
|
|
2863
|
+
return current;
|
|
2864
|
+
}
|
|
2865
|
+
countReset(label = "default") {
|
|
2866
|
+
this.counters.delete(label);
|
|
2867
|
+
}
|
|
2868
|
+
assert(condition, message) {
|
|
2869
|
+
if (!condition) {
|
|
2870
|
+
this.error("Assertion falhou:", message ?? "sem mensagem");
|
|
2871
|
+
throw new Error(`[JADE Assert] ${message ?? "Assertion falhou"}`);
|
|
2872
|
+
}
|
|
2873
|
+
}
|
|
2874
|
+
clear() {
|
|
2875
|
+
this.history = [];
|
|
2876
|
+
console.clear();
|
|
2877
|
+
}
|
|
2878
|
+
};
|
|
2879
|
+
|
|
2880
|
+
// apis/datetime_api.ts
|
|
2881
|
+
var DateTimeAPI = class {
|
|
2882
|
+
// Data e hora atual
|
|
2883
|
+
agora() {
|
|
2884
|
+
return /* @__PURE__ */ new Date();
|
|
2885
|
+
}
|
|
2886
|
+
hoje() {
|
|
2887
|
+
const d = /* @__PURE__ */ new Date();
|
|
2888
|
+
d.setHours(0, 0, 0, 0);
|
|
2889
|
+
return d;
|
|
2890
|
+
}
|
|
2891
|
+
// Formata data — suporta dd/MM/yyyy HH:mm:ss e variações
|
|
2892
|
+
formatar(date, formato) {
|
|
2893
|
+
const pad = (n, len = 2) => String(n).padStart(len, "0");
|
|
2894
|
+
return formato.replace("yyyy", String(date.getFullYear())).replace("MM", pad(date.getMonth() + 1)).replace("dd", pad(date.getDate())).replace("HH", pad(date.getHours())).replace("mm", pad(date.getMinutes())).replace("ss", pad(date.getSeconds())).replace("SSS", pad(date.getMilliseconds(), 3));
|
|
2895
|
+
}
|
|
2896
|
+
// Parseia string de data
|
|
2897
|
+
parsear(str, formato) {
|
|
2898
|
+
const tokens = {};
|
|
2899
|
+
const fmtParts = formato.match(/yyyy|MM|dd|HH|mm|ss/g) ?? [];
|
|
2900
|
+
let regex = formato.replace(/yyyy|MM|dd|HH|mm|ss/g, "(\\d+)");
|
|
2901
|
+
const match = str.match(new RegExp(regex));
|
|
2902
|
+
if (!match) throw new Error(`N\xE3o foi poss\xEDvel parsear '${str}' com formato '${formato}'`);
|
|
2903
|
+
fmtParts.forEach((part, i) => {
|
|
2904
|
+
tokens[part] = parseInt(match[i + 1]);
|
|
2905
|
+
});
|
|
2906
|
+
return new Date(
|
|
2907
|
+
tokens["yyyy"] ?? 0,
|
|
2908
|
+
(tokens["MM"] ?? 1) - 1,
|
|
2909
|
+
tokens["dd"] ?? 1,
|
|
2910
|
+
tokens["HH"] ?? 0,
|
|
2911
|
+
tokens["mm"] ?? 0,
|
|
2912
|
+
tokens["ss"] ?? 0
|
|
2913
|
+
);
|
|
2914
|
+
}
|
|
2915
|
+
// Adiciona unidade de tempo
|
|
2916
|
+
adicionar(date, quantidade, unidade) {
|
|
2917
|
+
const d = new Date(date);
|
|
2918
|
+
switch (unidade) {
|
|
2919
|
+
case "anos":
|
|
2920
|
+
d.setFullYear(d.getFullYear() + quantidade);
|
|
2921
|
+
break;
|
|
2922
|
+
case "meses":
|
|
2923
|
+
d.setMonth(d.getMonth() + quantidade);
|
|
2924
|
+
break;
|
|
2925
|
+
case "dias":
|
|
2926
|
+
d.setDate(d.getDate() + quantidade);
|
|
2927
|
+
break;
|
|
2928
|
+
case "horas":
|
|
2929
|
+
d.setHours(d.getHours() + quantidade);
|
|
2930
|
+
break;
|
|
2931
|
+
case "minutos":
|
|
2932
|
+
d.setMinutes(d.getMinutes() + quantidade);
|
|
2933
|
+
break;
|
|
2934
|
+
case "segundos":
|
|
2935
|
+
d.setSeconds(d.getSeconds() + quantidade);
|
|
2936
|
+
break;
|
|
2937
|
+
case "milissegundos":
|
|
2938
|
+
d.setMilliseconds(d.getMilliseconds() + quantidade);
|
|
2939
|
+
break;
|
|
2940
|
+
}
|
|
2941
|
+
return d;
|
|
2942
|
+
}
|
|
2943
|
+
// Subtrai unidade de tempo
|
|
2944
|
+
subtrair(date, quantidade, unidade) {
|
|
2945
|
+
return this.adicionar(date, -quantidade, unidade);
|
|
2946
|
+
}
|
|
2947
|
+
// Diferença entre datas na unidade especificada
|
|
2948
|
+
diferenca(data1, data2, unidade = "dias") {
|
|
2949
|
+
const ms = Math.abs(data2.getTime() - data1.getTime());
|
|
2950
|
+
switch (unidade) {
|
|
2951
|
+
case "milissegundos":
|
|
2952
|
+
return ms;
|
|
2953
|
+
case "segundos":
|
|
2954
|
+
return Math.floor(ms / 1e3);
|
|
2955
|
+
case "minutos":
|
|
2956
|
+
return Math.floor(ms / 6e4);
|
|
2957
|
+
case "horas":
|
|
2958
|
+
return Math.floor(ms / 36e5);
|
|
2959
|
+
case "dias":
|
|
2960
|
+
return Math.floor(ms / 864e5);
|
|
2961
|
+
case "meses":
|
|
2962
|
+
return Math.floor(ms / (864e5 * 30));
|
|
2963
|
+
case "anos":
|
|
2964
|
+
return Math.floor(ms / (864e5 * 365));
|
|
2965
|
+
default:
|
|
2966
|
+
return ms;
|
|
2967
|
+
}
|
|
2968
|
+
}
|
|
2969
|
+
eValida(valor) {
|
|
2970
|
+
return valor instanceof Date && !isNaN(valor.getTime());
|
|
2971
|
+
}
|
|
2972
|
+
eAnoBissexto(ano) {
|
|
2973
|
+
return ano % 4 === 0 && ano % 100 !== 0 || ano % 400 === 0;
|
|
2974
|
+
}
|
|
2975
|
+
};
|
|
2976
|
+
|
|
2977
|
+
// persistence/local_datastore.ts
|
|
2978
|
+
var LocalDatastore = class {
|
|
2979
|
+
db = null;
|
|
2980
|
+
dbName;
|
|
2981
|
+
tables;
|
|
2982
|
+
constructor(dbName, tables) {
|
|
2983
|
+
this.dbName = dbName;
|
|
2984
|
+
this.tables = tables;
|
|
2985
|
+
}
|
|
2986
|
+
async init() {
|
|
2987
|
+
return new Promise((resolve, reject) => {
|
|
2988
|
+
const request = indexedDB.open(this.dbName, 1);
|
|
2989
|
+
request.onupgradeneeded = (event) => {
|
|
2990
|
+
const db = event.target.result;
|
|
2991
|
+
for (const table of this.tables) {
|
|
2992
|
+
if (!db.objectStoreNames.contains(table)) {
|
|
2993
|
+
db.createObjectStore(table, { keyPath: "id" });
|
|
2994
|
+
}
|
|
2995
|
+
}
|
|
2996
|
+
};
|
|
2997
|
+
request.onsuccess = () => {
|
|
2998
|
+
this.db = request.result;
|
|
2999
|
+
resolve();
|
|
3000
|
+
};
|
|
3001
|
+
request.onerror = () => reject(request.error);
|
|
3002
|
+
});
|
|
3003
|
+
}
|
|
3004
|
+
async insert(table, record) {
|
|
3005
|
+
if (!record.id) {
|
|
3006
|
+
record.id = this.generateUUID();
|
|
3007
|
+
}
|
|
3008
|
+
if (!record._rev) {
|
|
3009
|
+
record._rev = this.generateRev(0);
|
|
3010
|
+
}
|
|
3011
|
+
return this.runTransaction(table, "readwrite", (store) => store.add(record));
|
|
3012
|
+
}
|
|
3013
|
+
async find(table, query) {
|
|
3014
|
+
const all = await this.runTransaction(
|
|
3015
|
+
table,
|
|
3016
|
+
"readonly",
|
|
3017
|
+
(store) => store.getAll()
|
|
3018
|
+
);
|
|
3019
|
+
let results = all;
|
|
3020
|
+
if (query?.where) {
|
|
3021
|
+
results = results.filter(
|
|
3022
|
+
(record) => Object.entries(query.where).every(([key, val]) => record[key] === val)
|
|
3023
|
+
);
|
|
3024
|
+
}
|
|
3025
|
+
if (query?.orderBy) {
|
|
3026
|
+
const { field, direction } = query.orderBy;
|
|
3027
|
+
results.sort((a, b) => {
|
|
3028
|
+
const cmp = a[field] < b[field] ? -1 : a[field] > b[field] ? 1 : 0;
|
|
3029
|
+
return direction === "asc" ? cmp : -cmp;
|
|
3030
|
+
});
|
|
3031
|
+
}
|
|
3032
|
+
if (query?.limit) {
|
|
3033
|
+
results = results.slice(0, query.limit);
|
|
3034
|
+
}
|
|
3035
|
+
return results;
|
|
3036
|
+
}
|
|
3037
|
+
async findById(table, id) {
|
|
3038
|
+
const result = await this.runTransaction(
|
|
3039
|
+
table,
|
|
3040
|
+
"readonly",
|
|
3041
|
+
(store) => store.get(id)
|
|
3042
|
+
);
|
|
3043
|
+
return result ?? null;
|
|
3044
|
+
}
|
|
3045
|
+
// Retorna o registro atualizado, o _rev anterior (baseRev) e os
|
|
3046
|
+
// deltas por campo — necessários para o SyncManager detectar conflitos.
|
|
3047
|
+
async update(table, id, changes) {
|
|
3048
|
+
const record = await this.findById(table, id);
|
|
3049
|
+
if (!record) throw new Error(`Registro '${id}' n\xE3o encontrado em '${table}'`);
|
|
3050
|
+
const baseRev = record._rev ?? this.generateRev(0);
|
|
3051
|
+
const deltas = {};
|
|
3052
|
+
for (const [campo, novoValor] of Object.entries(changes)) {
|
|
3053
|
+
if (campo !== "_rev" && record[campo] !== novoValor) {
|
|
3054
|
+
deltas[campo] = { de: record[campo], para: novoValor };
|
|
3055
|
+
}
|
|
3056
|
+
}
|
|
3057
|
+
const newRev = this.bumpRev(baseRev);
|
|
3058
|
+
const updated = { ...record, ...changes, _rev: newRev };
|
|
3059
|
+
await this.runTransaction(table, "readwrite", (store) => store.put(updated));
|
|
3060
|
+
return { record: updated, baseRev, deltas };
|
|
3061
|
+
}
|
|
3062
|
+
async delete(table, id) {
|
|
3063
|
+
return this.runTransaction(table, "readwrite", (store) => store.delete(id));
|
|
3064
|
+
}
|
|
3065
|
+
runTransaction(table, mode, operation) {
|
|
3066
|
+
return new Promise((resolve, reject) => {
|
|
3067
|
+
if (!this.db) throw new Error("Datastore n\xE3o inicializado");
|
|
3068
|
+
const tx = this.db.transaction(table, mode);
|
|
3069
|
+
const store = tx.objectStore(table);
|
|
3070
|
+
const request = operation(store);
|
|
3071
|
+
request.onsuccess = () => resolve(request.result);
|
|
3072
|
+
request.onerror = () => reject(request.error);
|
|
3073
|
+
});
|
|
3074
|
+
}
|
|
3075
|
+
generateUUID() {
|
|
3076
|
+
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
|
|
3077
|
+
const r = Math.random() * 16 | 0;
|
|
3078
|
+
return (c === "x" ? r : r & 3 | 8).toString(16);
|
|
3079
|
+
});
|
|
3080
|
+
}
|
|
3081
|
+
// Gera hash de 7 bytes criptograficamente seguro (browser + Node 14.17+)
|
|
3082
|
+
randomHash() {
|
|
3083
|
+
const buf = new Uint8Array(4);
|
|
3084
|
+
crypto.getRandomValues(buf);
|
|
3085
|
+
return Array.from(buf).map((b) => b.toString(16).padStart(2, "0")).join("").slice(0, 7);
|
|
3086
|
+
}
|
|
3087
|
+
// Gera _rev inicial: '1-xxxxxxx'
|
|
3088
|
+
generateRev(seq) {
|
|
3089
|
+
return `${seq + 1}-${this.randomHash()}`;
|
|
3090
|
+
}
|
|
3091
|
+
// Incrementa a sequência do _rev: '2-xxxxxxx' → '3-xxxxxxx'
|
|
3092
|
+
bumpRev(rev) {
|
|
3093
|
+
const seq = parseInt(rev.split("-")[0] ?? "0", 10);
|
|
3094
|
+
return `${seq + 1}-${this.randomHash()}`;
|
|
3095
|
+
}
|
|
3096
|
+
};
|
|
3097
|
+
|
|
3098
|
+
// persistence/preferencias.ts
|
|
3099
|
+
var Preferencias = class {
|
|
3100
|
+
prefixo;
|
|
3101
|
+
constructor(opcoes = {}) {
|
|
3102
|
+
this.prefixo = opcoes.prefixo ?? "jade";
|
|
3103
|
+
}
|
|
3104
|
+
chave(nome) {
|
|
3105
|
+
return `${this.prefixo}:${nome}`;
|
|
3106
|
+
}
|
|
3107
|
+
/** Salva um valor. Aceita strings, números, booleanos e objetos serializáveis. */
|
|
3108
|
+
definir(nome, valor, ttl) {
|
|
3109
|
+
if (typeof localStorage === "undefined") return;
|
|
3110
|
+
const entrada = { valor };
|
|
3111
|
+
if (ttl != null) entrada.expira = Date.now() + ttl;
|
|
3112
|
+
try {
|
|
3113
|
+
localStorage.setItem(this.chave(nome), JSON.stringify(entrada));
|
|
3114
|
+
} catch {
|
|
3115
|
+
}
|
|
3116
|
+
}
|
|
3117
|
+
/** Lê um valor. Retorna undefined se não existir ou tiver expirado. */
|
|
3118
|
+
obter(nome) {
|
|
3119
|
+
if (typeof localStorage === "undefined") return void 0;
|
|
3120
|
+
const raw = localStorage.getItem(this.chave(nome));
|
|
3121
|
+
if (raw == null) return void 0;
|
|
3122
|
+
try {
|
|
3123
|
+
const entrada = JSON.parse(raw);
|
|
3124
|
+
if (entrada.expira != null && Date.now() > entrada.expira) {
|
|
3125
|
+
localStorage.removeItem(this.chave(nome));
|
|
3126
|
+
return void 0;
|
|
3127
|
+
}
|
|
3128
|
+
return entrada.valor;
|
|
3129
|
+
} catch {
|
|
3130
|
+
return void 0;
|
|
3131
|
+
}
|
|
3132
|
+
}
|
|
3133
|
+
/** Remove uma preferência. */
|
|
3134
|
+
remover(nome) {
|
|
3135
|
+
if (typeof localStorage === "undefined") return;
|
|
3136
|
+
localStorage.removeItem(this.chave(nome));
|
|
3137
|
+
}
|
|
3138
|
+
/** Retorna true se a preferência existe e não está expirada. */
|
|
3139
|
+
existe(nome) {
|
|
3140
|
+
return this.obter(nome) !== void 0;
|
|
3141
|
+
}
|
|
3142
|
+
/** Remove todas as preferências deste app (com este prefixo). */
|
|
3143
|
+
limpar() {
|
|
3144
|
+
if (typeof localStorage === "undefined") return;
|
|
3145
|
+
const chaves = [];
|
|
3146
|
+
for (let i = 0; i < localStorage.length; i++) {
|
|
3147
|
+
const k = localStorage.key(i);
|
|
3148
|
+
if (k?.startsWith(this.prefixo + ":")) chaves.push(k);
|
|
3149
|
+
}
|
|
3150
|
+
chaves.forEach((k) => localStorage.removeItem(k));
|
|
3151
|
+
}
|
|
3152
|
+
/** Lista todas as chaves armazenadas (sem o prefixo). */
|
|
3153
|
+
listar() {
|
|
3154
|
+
if (typeof localStorage === "undefined") return [];
|
|
3155
|
+
const resultado = [];
|
|
3156
|
+
for (let i = 0; i < localStorage.length; i++) {
|
|
3157
|
+
const k = localStorage.key(i);
|
|
3158
|
+
if (k?.startsWith(this.prefixo + ":")) {
|
|
3159
|
+
resultado.push(k.slice(this.prefixo.length + 1));
|
|
3160
|
+
}
|
|
3161
|
+
}
|
|
3162
|
+
return resultado;
|
|
3163
|
+
}
|
|
3164
|
+
};
|
|
3165
|
+
var preferencias = new Preferencias();
|
|
3166
|
+
|
|
3167
|
+
// core/entity_manager.ts
|
|
3168
|
+
var EntityManager = class {
|
|
3169
|
+
entityName;
|
|
3170
|
+
store;
|
|
3171
|
+
events;
|
|
3172
|
+
constructor(entityName, store, events) {
|
|
3173
|
+
this.entityName = entityName;
|
|
3174
|
+
this.store = store;
|
|
3175
|
+
this.events = events;
|
|
3176
|
+
}
|
|
3177
|
+
/** Cria uma nova entidade e emite evento '<Entidade>Criado' */
|
|
3178
|
+
async criar(dados) {
|
|
3179
|
+
const record = await this.store.insert(this.entityName, { ...dados });
|
|
3180
|
+
this.events.emit(`${this.entityName}Criado`, record);
|
|
3181
|
+
return record;
|
|
3182
|
+
}
|
|
3183
|
+
/** Busca entidades com filtro opcional */
|
|
3184
|
+
async buscar(query) {
|
|
3185
|
+
return await this.store.find(this.entityName, query);
|
|
3186
|
+
}
|
|
3187
|
+
/** Busca uma entidade pelo ID */
|
|
3188
|
+
async buscarPorId(id) {
|
|
3189
|
+
return await this.store.findById(this.entityName, id);
|
|
3190
|
+
}
|
|
3191
|
+
/** Atualiza campos de uma entidade e emite evento '<Entidade>Atualizado' */
|
|
3192
|
+
async atualizar(id, mudancas) {
|
|
3193
|
+
const { record } = await this.store.update(this.entityName, id, mudancas);
|
|
3194
|
+
this.events.emit(`${this.entityName}Atualizado`, record);
|
|
3195
|
+
return record;
|
|
3196
|
+
}
|
|
3197
|
+
/** Remove uma entidade e emite evento '<Entidade>Removido' */
|
|
3198
|
+
async remover(id) {
|
|
3199
|
+
const record = await this.store.findById(this.entityName, id);
|
|
3200
|
+
await this.store.delete(this.entityName, id);
|
|
3201
|
+
this.events.emit(`${this.entityName}Removido`, record ?? { id });
|
|
3202
|
+
}
|
|
3203
|
+
/** Conta entidades (com filtro opcional) */
|
|
3204
|
+
async contar(query) {
|
|
3205
|
+
const results = await this.buscar(query);
|
|
3206
|
+
return results.length;
|
|
3207
|
+
}
|
|
3208
|
+
};
|
|
3209
|
+
|
|
3210
|
+
// core/rule_engine.ts
|
|
3211
|
+
var RuleEngine = class {
|
|
3212
|
+
regras = /* @__PURE__ */ new Map();
|
|
3213
|
+
events;
|
|
3214
|
+
constructor(events) {
|
|
3215
|
+
this.events = events;
|
|
3216
|
+
}
|
|
3217
|
+
/** Registra uma regra no engine. */
|
|
3218
|
+
registrar(regra) {
|
|
3219
|
+
if (this.regras.has(regra.nome)) {
|
|
3220
|
+
throw new Error(`Regra '${regra.nome}' j\xE1 registrada`);
|
|
3221
|
+
}
|
|
3222
|
+
this.regras.set(regra.nome, regra);
|
|
3223
|
+
}
|
|
3224
|
+
/** Remove uma regra registrada. */
|
|
3225
|
+
remover(nome) {
|
|
3226
|
+
this.regras.delete(nome);
|
|
3227
|
+
}
|
|
3228
|
+
/**
|
|
3229
|
+
* Dispara uma regra específica com o contexto fornecido.
|
|
3230
|
+
* Retorna o resultado indicando se a regra disparou.
|
|
3231
|
+
*/
|
|
3232
|
+
async disparar(nome, contexto) {
|
|
3233
|
+
const regra = this.regras.get(nome);
|
|
3234
|
+
if (!regra) {
|
|
3235
|
+
throw new Error(`Regra '${nome}' n\xE3o encontrada`);
|
|
3236
|
+
}
|
|
3237
|
+
return this.avaliar(regra, contexto);
|
|
3238
|
+
}
|
|
3239
|
+
/**
|
|
3240
|
+
* Avalia todas as regras registradas contra o contexto.
|
|
3241
|
+
* Útil para disparar regras em lote após uma mudança de estado.
|
|
3242
|
+
*/
|
|
3243
|
+
async dispararTodas(contexto) {
|
|
3244
|
+
const resultados = [];
|
|
3245
|
+
for (const regra of this.regras.values()) {
|
|
3246
|
+
resultados.push(await this.avaliar(regra, contexto));
|
|
3247
|
+
}
|
|
3248
|
+
return resultados;
|
|
3249
|
+
}
|
|
3250
|
+
/**
|
|
3251
|
+
* Registra uma regra que é avaliada automaticamente quando um evento ocorre.
|
|
3252
|
+
* O payload do evento é passado como contexto para a regra.
|
|
3253
|
+
*/
|
|
3254
|
+
atrelarEvento(nomeEvento, nomeRegra) {
|
|
3255
|
+
this.events.on(nomeEvento, async (payload) => {
|
|
3256
|
+
const regra = this.regras.get(nomeRegra);
|
|
3257
|
+
if (regra) {
|
|
3258
|
+
await this.avaliar(regra, payload);
|
|
3259
|
+
}
|
|
3260
|
+
});
|
|
3261
|
+
}
|
|
3262
|
+
async avaliar(regra, contexto) {
|
|
3263
|
+
const erros = [];
|
|
3264
|
+
let disparou = false;
|
|
3265
|
+
try {
|
|
3266
|
+
const condicao = regra.quando(contexto);
|
|
3267
|
+
if (condicao) {
|
|
3268
|
+
disparou = true;
|
|
3269
|
+
await regra.entao(contexto);
|
|
3270
|
+
this.events.emit(`regra:${regra.nome}:disparou`, contexto);
|
|
3271
|
+
} else if (regra.senao) {
|
|
3272
|
+
await regra.senao(contexto);
|
|
3273
|
+
this.events.emit(`regra:${regra.nome}:ignorou`, contexto);
|
|
3274
|
+
}
|
|
3275
|
+
} catch (err) {
|
|
3276
|
+
erros.push(err?.message ?? String(err));
|
|
3277
|
+
this.events.emit(`regra:${regra.nome}:erro`, { contexto, erro: err?.message });
|
|
3278
|
+
}
|
|
3279
|
+
return { nome: regra.nome, disparou, erros };
|
|
3280
|
+
}
|
|
3281
|
+
};
|
|
3282
|
+
export {
|
|
3283
|
+
ConsoleAPI,
|
|
3284
|
+
DateTimeAPI,
|
|
3285
|
+
EntityManager,
|
|
3286
|
+
EventLoop,
|
|
3287
|
+
HttpClient,
|
|
3288
|
+
JadeRuntime,
|
|
3289
|
+
LocalDatastore,
|
|
3290
|
+
MatematicaMetodos,
|
|
3291
|
+
MatematicaStdlib,
|
|
3292
|
+
MemoryManager,
|
|
3293
|
+
MoedaMetodos,
|
|
3294
|
+
MoedaStdlib,
|
|
3295
|
+
PWAGenerator,
|
|
3296
|
+
Preferencias,
|
|
3297
|
+
Router,
|
|
3298
|
+
RuleEngine,
|
|
3299
|
+
Signal,
|
|
3300
|
+
Store,
|
|
3301
|
+
TextoMetodos,
|
|
3302
|
+
TextoStdlib,
|
|
3303
|
+
UIEngine,
|
|
3304
|
+
alocarPosicao,
|
|
3305
|
+
aplicarTema,
|
|
3306
|
+
calcularICMS,
|
|
3307
|
+
calcularICMSST,
|
|
3308
|
+
calcularIPI,
|
|
3309
|
+
calcularISS,
|
|
3310
|
+
calcularPISCOFINS,
|
|
3311
|
+
calcularPISCOFINSNaoCumulativo,
|
|
3312
|
+
calcularTotaisNF,
|
|
3313
|
+
createEffect,
|
|
3314
|
+
criarGrade,
|
|
3315
|
+
estatisticasArmazem,
|
|
3316
|
+
formatarEndereco,
|
|
3317
|
+
gerarEAN13,
|
|
3318
|
+
gerarEAN8,
|
|
3319
|
+
liberarPosicao,
|
|
3320
|
+
parsearEndereco,
|
|
3321
|
+
preferencias,
|
|
3322
|
+
sugerirPosicao,
|
|
3323
|
+
validarCode128,
|
|
3324
|
+
validarCodigoBarras,
|
|
3325
|
+
validarEAN13,
|
|
3326
|
+
validarEAN8
|
|
3327
|
+
};
|