Compare commits
39 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 00fa17fa52 | |||
| 5e6f8b09a9 | |||
| 9ac4eb7042 | |||
| 2b8fa98ed7 | |||
| 77e927fef0 | |||
| 3f6b2ae6e4 | |||
| 35dcd27dda | |||
| bd867e8419 | |||
| da861ef964 | |||
| 8e642f2e0c | |||
| 7473db6b1f | |||
| 13489d58cf | |||
| 5ad8c1f395 | |||
| 103cd31e50 | |||
| 06783de4e2 | |||
| 11abac8daf | |||
| fea957dfb9 | |||
| 130b2b6da7 | |||
| b47d7766ad | |||
| 78fa778197 | |||
| 5e4340adca | |||
| c01133f39c | |||
| a6b295a6d0 | |||
| f850831e98 | |||
| e70f31e00f | |||
| 79e1dbd645 | |||
| cce8f3df60 | |||
| 318d44e934 | |||
| bfbcd89be9 | |||
| 1848c1aa35 | |||
| 9176b764fd | |||
| 5cab38a27c | |||
| 5b1f3a2691 | |||
| 7e57d30863 | |||
| 7a93a30c22 | |||
| 230304e878 | |||
| d185f82bd5 | |||
| 2253d45d00 | |||
| 60e15dbed3 |
+13
@@ -7,3 +7,16 @@ src-tauri/gen/
|
||||
*.msi
|
||||
*.dmg
|
||||
.DS_Store
|
||||
|
||||
# Binarios compilados
|
||||
*.exe
|
||||
*.tar.gz
|
||||
*.zip
|
||||
fechamento-caixa.exe
|
||||
fechamento-caixa-latest.tar.gz
|
||||
WebView2Loader.dll
|
||||
src-tauri/target/x86_64-pc-windows-gnu/release/*.exe
|
||||
src-tauri/target/x86_64-pc-windows-gnu/release/*.dll
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
# Fechamento de Caixa — O Frangão
|
||||
|
||||
App desktop nativo para fechamento de caixa com impressão em impressora térmica 80mm.
|
||||
|
||||
## Stack
|
||||
|
||||
- **Backend:** Tauri 2 + Rust
|
||||
- **Frontend:** HTML + CSS + JavaScript (vanilla, ~1700 linhas)
|
||||
- **Storage local:** SQLite (rusqlite) — funciona offline
|
||||
- **Sync:** Supabase REST API
|
||||
- **Impressão:** ESC/POS via USB (TM-T20 e compatíveis, Windows)
|
||||
|
||||
## Estrutura do Projeto
|
||||
|
||||
```
|
||||
fechamento-caixa/
|
||||
├── src/
|
||||
│ └── index.html # Frontend completo (HTML + CSS + JS inline)
|
||||
├── src-tauri/
|
||||
│ ├── Cargo.toml # Dependências Rust
|
||||
│ ├── tauri.conf.json # Config do app Tauri
|
||||
│ └── src/
|
||||
│ ├── main.rs # Entry point — registra comandos Tauri
|
||||
│ └── plugins/
|
||||
│ ├── supabase.rs # Integração Supabase (REST API)
|
||||
│ ├── printer.rs # Geração ESC/POS + envio USB
|
||||
│ ├── storage.rs # SQLite local
|
||||
│ ├── app.rs # Config da app (loja, operador default)
|
||||
│ └── escpos.rs # Helpers ESC/POS (INIT, CUT, divider, etc.)
|
||||
└── README.md
|
||||
```
|
||||
|
||||
## Pré-requisitos
|
||||
|
||||
- Rust 1.70+ (`rustup install stable`)
|
||||
- Node.js 18+ (para build do frontend Tauri)
|
||||
- Windows 10/11 (impressão USB só funciona no Windows)
|
||||
|
||||
## Setup
|
||||
|
||||
### 1. Clonar o repositório
|
||||
|
||||
```bash
|
||||
git clone https://git.ofrangao.com.br/filipe/fechamento-caixa.git
|
||||
cd fechamento-caixa
|
||||
```
|
||||
|
||||
### 2. Configurar chaves do Supabase
|
||||
|
||||
Crie o arquivo `config.json` em `%LOCALAPPDATA%\FechamentoCaixa\config.json` (Windows):
|
||||
|
||||
```json
|
||||
{
|
||||
"SUPABASE_ANON_KEY": "sua_chave_anon_aqui",
|
||||
"SUPABASE_SERVICE_KEY": "sua_chave_service_role_aqui"
|
||||
}
|
||||
```
|
||||
|
||||
O app procura esse arquivo na inicialização. Sem ele, tenta ler das variáveis de ambiente `SUPABASE_ANON_KEY` e `SUPABASE_SERVICE_KEY`.
|
||||
|
||||
### 3. Build
|
||||
|
||||
```bash
|
||||
cd src-tauri
|
||||
cargo build --release --target x86_64-pc-windows-gnu
|
||||
```
|
||||
|
||||
O executável fica em:
|
||||
```
|
||||
src-tauri/target/x86_64-pc-windows-gnu/release/fechamento-caixa.exe
|
||||
```
|
||||
|
||||
Para gerar o pacote de distribuição:
|
||||
```bash
|
||||
cp src-tauri/target/x86_64-pc-windows-gnu/release/fechamento-caixa.exe .
|
||||
cp src-tauri/target/x86_64-pc-windows-gnu/release/WebView2Loader.dll .
|
||||
tar -czf fechamento-caixa-vX.X.X.tar.gz fechamento-caixa.exe WebView2Loader.dll
|
||||
```
|
||||
|
||||
## Códigos de comando Tauri
|
||||
|
||||
O frontend chama esses comandos via `window.__TAURI__.core.invoke()`:
|
||||
|
||||
| Comando | Arquivo | Descrição |
|
||||
|---------|---------|-----------|
|
||||
| `salvar_fechamento` | storage.rs | Salva no SQLite local |
|
||||
| `carregar_rascunho` | storage.rs | Busca rascunho por loja+data |
|
||||
| `listar_fechamentos` | storage.rs | Lista histórico do SQLite |
|
||||
| `buscar_fechamento_por_id` | storage.rs | Busca por ID no SQLite |
|
||||
| `sb_salvar_fechamento` | supabase.rs | Envia para Supabase |
|
||||
| `sb_carregar_rascunho` | supabase.rs | Busca rascunho no Supabase |
|
||||
| `sb_listar_recentes` | supabase.rs | Lista fechamentos recentes |
|
||||
| `sb_buscar_por_id` | supabase.rs | Busca por ID no Supabase |
|
||||
| `sb_salvar_listas` | supabase.rs | Salva operadores/gerentes |
|
||||
| `sb_carregar_listas` | supabase.rs | Carrega listas do servidor |
|
||||
| `sb_online` | supabase.rs | Verifica conectividade |
|
||||
| `imprimir_recibo` | printer.rs | Envia bytes ESC/POS pra USB |
|
||||
| `detectar_impressora` | printer.rs | Detecta porta USB |
|
||||
| `configurar_impressora` | printer.rs | Define porta manual |
|
||||
| `teste_impressora` | printer.rs | Imprime página de teste |
|
||||
| `get_loja` / `set_loja` | app.rs | Loja atual |
|
||||
| `get_config` / `set_config` | app.rs | Config completo |
|
||||
|
||||
## Fluxo de dados
|
||||
|
||||
```
|
||||
[Usuário preenche formulário]
|
||||
│
|
||||
▼ (auto-save a cada 1.5s)
|
||||
SQLite local (fechamento.db)
|
||||
│
|
||||
├── [Botão Enviar] ──► Supabase (fechamentos_web)
|
||||
│
|
||||
└── [Botão Recibo] ──► printer.rs ──► to_escpos() ──► USB
|
||||
│
|
||||
├── Sucesso: imprime
|
||||
└── Falha: openPrintPreview() ──► iframe ──► window.print()
|
||||
```
|
||||
|
||||
## Campos do formulário
|
||||
|
||||
| Campo | Tabela Supabase | Notas |
|
||||
|-------|----------------|-------|
|
||||
| uuid, loja, data, operador, turno | fechamentos_web | PK composto |
|
||||
| saldo_troco, saldo_esperado, fechamento, diferenca | fechamentos_web | diferenca = fechamento - saldo_esperado |
|
||||
| despesas[], sangrias[], vales[], receber[], pixcnpj[], cancelamentos[] | dados (JSON) | Arrays de {desc/nome, valor, ...} |
|
||||
| cartoes{credito[],debito[],alimentacao[],pix[]} | dados (JSON) | Arrays de {valor} |
|
||||
| operadores[], gerentes[], despesas_custom[], clientes[] | listas_personalizadas | Listas por loja |
|
||||
|
||||
## Problemas conhecidos
|
||||
|
||||
- Impressão USB só funciona no Windows
|
||||
- `service_role_key` no cliente desktop é risco de segurança — usar apenas anon_key + RLS
|
||||
- Sem testes automatizados
|
||||
|
||||
## Version History
|
||||
|
||||
- **v1.2.7** — 2026-06-24: 4 bugs críticos concertados (impressão, enviar, dia anterior, localStorage)
|
||||
- **v1.2.6** — 2026-06-24: WebView2Loader.dll incluso; init() restaura operador+turno
|
||||
- **v1.2.5** — 2026-06-24: diferenca = fechamento - esperado (não mais saldo_troco - esperado)
|
||||
Binary file not shown.
Binary file not shown.
Generated
-35
@@ -900,14 +900,12 @@ dependencies = [
|
||||
"rusqlite",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_yaml",
|
||||
"tauri",
|
||||
"tauri-build",
|
||||
"tauri-plugin-dialog",
|
||||
"tauri-plugin-fs",
|
||||
"tauri-plugin-shell",
|
||||
"thiserror 2.0.18",
|
||||
"tokio",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
@@ -3281,19 +3279,6 @@ dependencies = [
|
||||
"syn 2.0.118",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_yaml"
|
||||
version = "0.9.34+deprecated"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47"
|
||||
dependencies = [
|
||||
"indexmap 2.14.0",
|
||||
"itoa",
|
||||
"ryu",
|
||||
"serde",
|
||||
"unsafe-libyaml",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serialize-to-javascript"
|
||||
version = "0.1.2"
|
||||
@@ -4070,25 +4055,11 @@ dependencies = [
|
||||
"bytes",
|
||||
"libc",
|
||||
"mio",
|
||||
"parking_lot",
|
||||
"pin-project-lite",
|
||||
"signal-hook-registry",
|
||||
"socket2",
|
||||
"tokio-macros",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-macros"
|
||||
version = "2.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.118",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-native-tls"
|
||||
version = "0.3.1"
|
||||
@@ -4399,12 +4370,6 @@ version = "1.13.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8"
|
||||
|
||||
[[package]]
|
||||
name = "unsafe-libyaml"
|
||||
version = "0.2.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
|
||||
|
||||
[[package]]
|
||||
name = "untrusted"
|
||||
version = "0.9.0"
|
||||
|
||||
@@ -15,8 +15,6 @@ serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
rusqlite = { version = "0.32", features = ["bundled"] }
|
||||
reqwest = { version = "0.12", features = ["json", "blocking"] }
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
serde_yaml = "0.9"
|
||||
uuid = { version = "1", features = ["v4"] }
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
log = "0.4"
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"identifier": "default",
|
||||
"description": "Default capabilities for Fechamento de Caixa",
|
||||
"windows": ["main"],
|
||||
"permissions": [
|
||||
"core:default",
|
||||
"shell:allow-open",
|
||||
"dialog:allow-message",
|
||||
"dialog:allow-confirm",
|
||||
"dialog:allow-ask",
|
||||
"dialog:allow-save",
|
||||
"dialog:allow-open",
|
||||
"fs:default",
|
||||
"fs:allow-app-read",
|
||||
"fs:allow-app-write",
|
||||
"fs:allow-appdata-read",
|
||||
"fs:allow-appdata-write",
|
||||
"fs:allow-appconfig-read",
|
||||
"fs:allow-appconfig-write",
|
||||
"fs:allow-applocaldata-read",
|
||||
"fs:allow-applocaldata-write"
|
||||
]
|
||||
}
|
||||
@@ -36,10 +36,10 @@ fn load_config() -> (String, String) {
|
||||
}
|
||||
}
|
||||
}
|
||||
// Fallback: tenta variáveis de ambiente
|
||||
// Fallback 3: chaves embedadas no binário (funciona em qualquer PC sem config)
|
||||
(
|
||||
std::env::var("SUPABASE_ANON_KEY").unwrap_or_default(),
|
||||
std::env::var("SUPABASE_SERVICE_KEY").unwrap_or_default(),
|
||||
"sb_publishable_NCUGwstslOvVt5JtGxKB9A_p-BERG57".to_string(),
|
||||
"sb_secret_T6eHIKJDGaH53HMTG6UUKa_6ou6KfEW".to_string(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -96,6 +96,8 @@ fn main() {
|
||||
plugins::supabase::sb_carregar_rascunho,
|
||||
plugins::supabase::sb_listar_recentes,
|
||||
plugins::supabase::sb_buscar_por_id,
|
||||
plugins::supabase::sb_buscar_por_uuid,
|
||||
plugins::supabase::sb_atualizar_fechamento,
|
||||
plugins::supabase::sb_salvar_listas,
|
||||
plugins::supabase::sb_carregar_listas,
|
||||
plugins::supabase::sb_online,
|
||||
|
||||
@@ -8,6 +8,7 @@ pub const LF: &[u8] = b"\x0A";
|
||||
/// Cortar papel (parcial)
|
||||
pub const CUT: &[u8] = b"\x1D\x56\x01";
|
||||
/// Cortar papel (total)
|
||||
#[allow(dead_code)]
|
||||
pub const CUT_FULL: &[u8] = b"\x1D\x56\x00";
|
||||
/// Alimentar 3 linhas antes de cortar
|
||||
pub const FEED_CUT: &[u8] = b"\x1B\x64\x03";
|
||||
@@ -17,6 +18,7 @@ pub const BOLD_OFF: &[u8] = b"\x1B\x45\x00";
|
||||
/// Alinhamento
|
||||
pub const ALIGN_LEFT: &[u8] = b"\x1B\x61\x00";
|
||||
pub const ALIGN_CENTER: &[u8] = b"\x1B\x61\x01";
|
||||
#[allow(dead_code)]
|
||||
pub const ALIGN_RIGHT: &[u8] = b"\x1B\x61\x02";
|
||||
/// Fonte normal / dupla
|
||||
pub const FONT_NORMAL: &[u8] = b"\x1B\x21\x00";
|
||||
@@ -38,6 +40,7 @@ pub fn title(text: &str) -> Vec<u8> {
|
||||
}
|
||||
|
||||
/// Monta linha normal (alinhada à esquerda)
|
||||
#[allow(dead_code)]
|
||||
pub fn line(text: &str) -> Vec<u8> {
|
||||
let mut out = Vec::new();
|
||||
out.extend_from_slice(ALIGN_LEFT);
|
||||
|
||||
@@ -5,6 +5,7 @@ use std::sync::Mutex;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
#[allow(dead_code)]
|
||||
pub enum PrinterError {
|
||||
#[error("Impressora não encontrada. Verifique se está conectada via USB e ligada.")]
|
||||
NotFound,
|
||||
@@ -30,6 +31,50 @@ use crate::plugins::escpos;
|
||||
|
||||
// ── Struct de dados do recibo ────────────────────────────────────────────────
|
||||
|
||||
/// Linha de uma tabela no recibo (despesas, sangrias, vales, etc.)
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
|
||||
pub struct ReciboLinha {
|
||||
#[serde(default)]
|
||||
pub desc: Option<String>,
|
||||
#[serde(rename = "hora", default)]
|
||||
pub hora: Option<String>,
|
||||
#[serde(rename = "nome", default)]
|
||||
pub nome: Option<String>,
|
||||
#[serde(default)]
|
||||
pub retirada: Option<f64>,
|
||||
#[serde(default)]
|
||||
pub valor: Option<f64>,
|
||||
#[serde(default)]
|
||||
pub obs: Option<String>,
|
||||
#[serde(default)]
|
||||
pub gerente: Option<String>,
|
||||
#[serde(default)]
|
||||
pub numero: Option<String>,
|
||||
#[serde(default)]
|
||||
pub motivo: Option<String>,
|
||||
}
|
||||
|
||||
impl ReciboLinha {
|
||||
fn fmt_v(&self) -> String {
|
||||
let v = self.retirada.or(self.valor).unwrap_or(0.0);
|
||||
format!("R$ {:.2}", v).replace('.', ",")
|
||||
}
|
||||
fn label(&self) -> String {
|
||||
self.desc
|
||||
.clone()
|
||||
.or(self.nome.clone())
|
||||
.or(self.hora.clone().map(|h| format!("{}h", h)))
|
||||
.unwrap_or_default()
|
||||
}
|
||||
fn obs(&self) -> String {
|
||||
self.gerente
|
||||
.clone()
|
||||
.or(self.obs.clone())
|
||||
.or(self.motivo.clone())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct ReciboData {
|
||||
pub loja: String,
|
||||
@@ -37,19 +82,55 @@ pub struct ReciboData {
|
||||
pub operador: String,
|
||||
pub turno: String,
|
||||
pub saldo_troco: f64,
|
||||
/// Valor contado no fechamento (dinheiro + cartões + receber)
|
||||
pub fechamento: f64,
|
||||
/// Saldo esperado = total operações − sangrias − despesas
|
||||
pub saldo_esperado: f64,
|
||||
/// Diferença = fechamento − saldo_esperado
|
||||
pub diferenca: f64,
|
||||
pub sangrias: f64,
|
||||
pub despesas: f64,
|
||||
pub vales: f64,
|
||||
pub areceber: f64,
|
||||
/// Total do sistema (troco + crédito + débito + alimentação + voucher + pixcnpj + vales + receber)
|
||||
#[serde(default)]
|
||||
pub sys_total: f64,
|
||||
/// Soma de todas operações creditadas (crédito + débito + alimentação + voucher + pixcnpj) — usado no BLACK section como TOTAL
|
||||
#[serde(default)]
|
||||
pub total_operado: f64,
|
||||
// Totais de cartões
|
||||
pub credito: f64,
|
||||
pub debito: f64,
|
||||
pub alimentacao: f64,
|
||||
pub voucher: f64,
|
||||
// Totais diversos (mantidos para compatibilidade com JSON antigo)
|
||||
#[serde(default)]
|
||||
pub sangrias: f64,
|
||||
#[serde(default)]
|
||||
pub despesas: f64,
|
||||
#[serde(default)]
|
||||
pub vales: f64,
|
||||
#[serde(default)]
|
||||
pub areceber: f64,
|
||||
#[serde(default)]
|
||||
pub pixcnpj: f64,
|
||||
#[serde(default)]
|
||||
pub cancelamentos: f64,
|
||||
// Arrays para linhas detalhadas (recebidos do frontend)
|
||||
#[serde(default)]
|
||||
pub sangrias_arr: Vec<ReciboLinha>,
|
||||
#[serde(default)]
|
||||
pub despesas_arr: Vec<ReciboLinha>,
|
||||
#[serde(default)]
|
||||
pub pixcnpj_arr: Vec<ReciboLinha>,
|
||||
#[serde(default)]
|
||||
pub vales_arr: Vec<ReciboLinha>,
|
||||
#[serde(default)]
|
||||
pub receber_arr: Vec<ReciboLinha>,
|
||||
#[serde(default)]
|
||||
pub cancelamentos_arr: Vec<ReciboLinha>,
|
||||
#[serde(default)]
|
||||
pub observacoes: Option<String>,
|
||||
#[serde(default)]
|
||||
pub clientes: i32,
|
||||
#[serde(default)]
|
||||
pub frango: i32,
|
||||
}
|
||||
|
||||
impl ReciboData {
|
||||
@@ -58,6 +139,7 @@ impl ReciboData {
|
||||
}
|
||||
|
||||
/// Gera todos os bytes ESC/POS do recibo 80mm
|
||||
/// Layout: Header → SANGRIAS → DESPESAS → PIX CNPJ → VALES → A RECEBER → CANCELAMENTOS → CARTÕES → TROCO/Saldo Caixa → SALDO ESPERADO → DIFERENÇA
|
||||
pub fn to_escpos(&self) -> Vec<u8> {
|
||||
let mut out = Vec::new();
|
||||
|
||||
@@ -77,40 +159,193 @@ impl ReciboData {
|
||||
out.extend(escpos::kv("Turno:", &self.turno));
|
||||
out.extend(escpos::divider());
|
||||
|
||||
// Entradas
|
||||
out.extend(escpos::line_bold("ENTRADAS"));
|
||||
out.extend(escpos::kv("Saldo Troco:", &self.fmt_money(self.saldo_troco)));
|
||||
out.extend(escpos::kv("Fechamento:", &self.fmt_money(self.fechamento)));
|
||||
out.extend(escpos::kv("Saldo Esperado:", &self.fmt_money(self.saldo_esperado)));
|
||||
out.extend(escpos::divider());
|
||||
// ── SANGRIAS ──
|
||||
if !self.sangrias_arr.is_empty() {
|
||||
out.extend(escpos::line_bold("SANGRIAS"));
|
||||
for linha in &self.sangrias_arr {
|
||||
if linha.retirada.unwrap_or(0.0) > 0.0 || linha.valor.unwrap_or(0.0) > 0.0 {
|
||||
let label = linha.label();
|
||||
let obs = linha.obs();
|
||||
out.extend(escpos::kv(&label, &linha.fmt_v()));
|
||||
if !obs.is_empty() {
|
||||
out.extend(escpos::line_center(&format!(" {}", obs)));
|
||||
}
|
||||
}
|
||||
}
|
||||
// Total sangrias
|
||||
let total_sangrias: f64 = self.sangrias_arr.iter()
|
||||
.map(|l| l.retirada.unwrap_or(0.0) + l.valor.unwrap_or(0.0))
|
||||
.sum();
|
||||
if total_sangrias > 0.0 {
|
||||
out.extend(escpos::kv("Total:", &self.fmt_money(total_sangrias)));
|
||||
}
|
||||
out.extend(escpos::divider());
|
||||
}
|
||||
|
||||
// Débitos
|
||||
out.extend(escpos::line_bold("DEBITOS"));
|
||||
out.extend(escpos::kv("Sangrias:", &self.fmt_money(self.sangrias)));
|
||||
out.extend(escpos::kv("Despesas:", &self.fmt_money(self.despesas)));
|
||||
out.extend(escpos::kv("Vales:", &self.fmt_money(self.vales)));
|
||||
out.extend(escpos::kv("A Receber:", &self.fmt_money(self.areceber)));
|
||||
out.extend(escpos::kv("PIX CNPJ:", &self.fmt_money(self.pixcnpj)));
|
||||
out.extend(escpos::divider());
|
||||
// ── DESPESAS ──
|
||||
if !self.despesas_arr.is_empty() {
|
||||
out.extend(escpos::line_bold("DESPESAS"));
|
||||
for linha in &self.despesas_arr {
|
||||
if linha.valor.unwrap_or(0.0) > 0.0 {
|
||||
let label = linha.label();
|
||||
let obs = linha.obs();
|
||||
out.extend(escpos::kv(&label, &linha.fmt_v()));
|
||||
if !obs.is_empty() {
|
||||
out.extend(escpos::line_center(&format!(" {}", obs)));
|
||||
}
|
||||
}
|
||||
}
|
||||
// Total despesas
|
||||
let total_despesas: f64 = self.despesas_arr.iter()
|
||||
.map(|l| l.valor.unwrap_or(0.0))
|
||||
.sum();
|
||||
out.extend(escpos::kv("Total:", &self.fmt_money(total_despesas)));
|
||||
out.extend(escpos::divider());
|
||||
}
|
||||
|
||||
// Cartões
|
||||
out.extend(escpos::line_bold("CARTÕES"));
|
||||
// ── PIX CNPJ ──
|
||||
if !self.pixcnpj_arr.is_empty() {
|
||||
out.extend(escpos::line_bold("PIX CNPJ"));
|
||||
for linha in &self.pixcnpj_arr {
|
||||
if linha.valor.unwrap_or(0.0) > 0.0 {
|
||||
let label = linha.nome.clone().unwrap_or_else(|| "PIX CNPJ".to_string());
|
||||
out.extend(escpos::kv(&label, &linha.fmt_v()));
|
||||
}
|
||||
}
|
||||
// Total PIX CNPJ
|
||||
let total_pixcnpj: f64 = self.pixcnpj_arr.iter()
|
||||
.map(|l| l.valor.unwrap_or(0.0))
|
||||
.sum();
|
||||
if total_pixcnpj > 0.0 {
|
||||
out.extend(escpos::kv("Total:", &self.fmt_money(total_pixcnpj)));
|
||||
}
|
||||
out.extend(escpos::divider());
|
||||
}
|
||||
|
||||
// ── VALES ──
|
||||
if !self.vales_arr.is_empty() {
|
||||
out.extend(escpos::line_bold("VALES"));
|
||||
for linha in &self.vales_arr {
|
||||
if linha.valor.unwrap_or(0.0) > 0.0 {
|
||||
let label = linha.label();
|
||||
let obs = linha.obs();
|
||||
out.extend(escpos::kv(&label, &linha.fmt_v()));
|
||||
if !obs.is_empty() {
|
||||
out.extend(escpos::line_center(&format!(" {}", obs)));
|
||||
}
|
||||
}
|
||||
}
|
||||
// Total vales
|
||||
let total_vales: f64 = self.vales_arr.iter()
|
||||
.map(|l| l.valor.unwrap_or(0.0))
|
||||
.sum();
|
||||
if total_vales > 0.0 {
|
||||
out.extend(escpos::kv("Total:", &self.fmt_money(total_vales)));
|
||||
}
|
||||
out.extend(escpos::divider());
|
||||
}
|
||||
|
||||
// ── A RECEBER ──
|
||||
if !self.receber_arr.is_empty() {
|
||||
out.extend(escpos::line_bold("A RECEBER"));
|
||||
for linha in &self.receber_arr {
|
||||
if linha.valor.unwrap_or(0.0) > 0.0 {
|
||||
let label = linha.label();
|
||||
out.extend(escpos::kv(&label, &linha.fmt_v()));
|
||||
}
|
||||
}
|
||||
// Total a receber
|
||||
let total_receber: f64 = self.receber_arr.iter()
|
||||
.map(|l| l.valor.unwrap_or(0.0))
|
||||
.sum();
|
||||
if total_receber > 0.0 {
|
||||
out.extend(escpos::kv("Total:", &self.fmt_money(total_receber)));
|
||||
}
|
||||
out.extend(escpos::divider());
|
||||
}
|
||||
|
||||
// ── CANCELAMENTOS ──
|
||||
if !self.cancelamentos_arr.is_empty() {
|
||||
out.extend(escpos::line_bold("CANCELAMENTOS"));
|
||||
for linha in &self.cancelamentos_arr {
|
||||
if linha.numero.is_some() || linha.valor.unwrap_or(0.0) > 0.0 {
|
||||
let num = linha.numero.clone().unwrap_or_default();
|
||||
out.extend(escpos::kv(&num, &linha.fmt_v()));
|
||||
if let Some(ref m) = linha.motivo {
|
||||
if !m.is_empty() {
|
||||
out.extend(escpos::line_center(&format!(" {}", m)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Total cancelamentos
|
||||
let total_cancel: f64 = self.cancelamentos_arr.iter()
|
||||
.map(|l| l.valor.unwrap_or(0.0))
|
||||
.sum();
|
||||
if total_cancel > 0.0 {
|
||||
out.extend(escpos::kv("Total:", &self.fmt_money(total_cancel)));
|
||||
}
|
||||
out.extend(escpos::divider());
|
||||
}
|
||||
|
||||
// ── CARTÕES ──
|
||||
out.extend(escpos::line_bold("CARTOES"));
|
||||
out.extend(escpos::kv("Credito:", &self.fmt_money(self.credito)));
|
||||
out.extend(escpos::kv("Debito:", &self.fmt_money(self.debito)));
|
||||
out.extend(escpos::kv("Alimentacao:", &self.fmt_money(self.alimentacao)));
|
||||
out.extend(escpos::kv("Voucher:", &self.fmt_money(self.voucher)));
|
||||
out.extend(escpos::kv("PIX Cartao:", &self.fmt_money(self.voucher)));
|
||||
out.extend(escpos::kv("Cancelamentos:", &self.fmt_money(self.cancelamentos)));
|
||||
out.extend(escpos::divider());
|
||||
|
||||
// Diferença
|
||||
let diff_str = if self.diferenca >= 0.0 {
|
||||
format!("+{}", self.fmt_money(self.diferenca))
|
||||
// ── TROCO / SALDO DE CAIXA ──
|
||||
out.extend(escpos::line_bold("SALDO DE CAIXA"));
|
||||
out.extend(escpos::kv("Troco (+):", &self.fmt_money(self.saldo_troco)));
|
||||
out.extend(escpos::kv("Credito (+):", &self.fmt_money(self.credito)));
|
||||
out.extend(escpos::kv("Debito (+):", &self.fmt_money(self.debito)));
|
||||
out.extend(escpos::kv("Alimentacao (+):", &self.fmt_money(self.alimentacao)));
|
||||
out.extend(escpos::kv("PIX Cartao (+):", &self.fmt_money(self.voucher)));
|
||||
out.extend(escpos::kv("Vales (+):", &self.fmt_money(self.vales)));
|
||||
out.extend(escpos::kv("A Receber (+):", &self.fmt_money(self.areceber)));
|
||||
out.extend(escpos::kv("PIX CNPJ (+):", &self.fmt_money(self.pixcnpj)));
|
||||
out.extend(escpos::divider());
|
||||
|
||||
// ── TOTAL ──
|
||||
if self.sys_total > 0.0 {
|
||||
out.extend(escpos::line_bold("TOTAL"));
|
||||
out.extend(escpos::kv("Total:", &self.fmt_money(self.sys_total)));
|
||||
out.extend(escpos::divider());
|
||||
}
|
||||
|
||||
// ── SALDO ESPERADO ──
|
||||
out.extend(escpos::line_bold("SALDO ESPERADO"));
|
||||
out.extend(escpos::kv("Esperado:", &self.fmt_money(self.saldo_esperado)));
|
||||
out.extend(escpos::divider());
|
||||
|
||||
// ── DIFERENÇA ──
|
||||
// Diferença = total_fechamento - saldo_esperado - cancelamentos
|
||||
out.extend(escpos::line_bold("DIFERENCA"));
|
||||
let diff = self.fechamento - self.saldo_esperado - self.cancelamentos;
|
||||
let diff_str = if diff >= 0.0 {
|
||||
format!("+{}", self.fmt_money(diff))
|
||||
} else {
|
||||
self.fmt_money(self.diferenca)
|
||||
self.fmt_money(diff)
|
||||
};
|
||||
out.extend(escpos::line_bold("DIFERENCA:"));
|
||||
out.extend(escpos::title(&diff_str));
|
||||
out.extend(escpos::blank_lines(2));
|
||||
|
||||
// ── CONTADORES ──
|
||||
out.extend(escpos::line_bold("CONTADORES"));
|
||||
out.extend(escpos::kv("Clientes:", &self.clientes.to_string()));
|
||||
out.extend(escpos::kv("Frango Assado:", &self.frango.to_string()));
|
||||
out.extend(escpos::divider());
|
||||
|
||||
// ── OBSERVAÇÕES ──
|
||||
if let Some(ref obs) = self.observacoes {
|
||||
if !obs.trim().is_empty() {
|
||||
out.extend(escpos::line_bold("OBSERVACOES"));
|
||||
out.extend(escpos::line_center(obs.trim()));
|
||||
out.extend(escpos::divider());
|
||||
}
|
||||
}
|
||||
|
||||
// Rodapé
|
||||
out.extend(escpos::divider());
|
||||
@@ -122,6 +357,7 @@ impl ReciboData {
|
||||
|
||||
// Corta papel
|
||||
out.extend(escpos::FEED_CUT);
|
||||
out.extend(escpos::CUT);
|
||||
|
||||
out
|
||||
}
|
||||
|
||||
@@ -335,6 +335,7 @@ impl DbManager {
|
||||
}
|
||||
|
||||
/// Atualiza sync_status de um registro.
|
||||
#[allow(dead_code)]
|
||||
pub fn atualizar_sync(&self, uuid: &str, status: SyncStatus) -> Result<(), StorageError> {
|
||||
let conn = self.conn.lock().map_err(|_| StorageError::Lock)?;
|
||||
let now = Utc::now().format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string();
|
||||
@@ -346,6 +347,7 @@ impl DbManager {
|
||||
}
|
||||
|
||||
/// Remove um registro (para debugging).
|
||||
#[allow(dead_code)]
|
||||
pub fn remover(&self, uuid: &str) -> Result<bool, StorageError> {
|
||||
let conn = self.conn.lock().map_err(|_| StorageError::Lock)?;
|
||||
let n = conn.execute("DELETE FROM fechamentos WHERE uuid = ?1", params![uuid])?;
|
||||
@@ -508,7 +510,7 @@ pub fn carregar_rascunho(
|
||||
// Simplifica para formato que o frontend espera
|
||||
let total_sangrias: f64 = e.tabelas.sangrias.iter().filter_map(|r| r.valor).sum();
|
||||
let total_despesas: f64 = e.tabelas.despesas.iter().filter_map(|r| r.valor).sum();
|
||||
let total_receber: f64 = e.tabelas.areceber.iter().filter_map(|r| r.valor).sum();
|
||||
let _total_receber: f64 = e.tabelas.areceber.iter().filter_map(|r| r.valor).sum();
|
||||
let despesa_rows: Vec<_> = e.tabelas.despesas.iter().filter_map(|r| {
|
||||
r.descricao.as_ref().map(|d| serde_json::json!({"desc": d, "valor": r.valor}))
|
||||
}).collect();
|
||||
|
||||
@@ -7,7 +7,31 @@ use std::time::Duration;
|
||||
use tauri::State;
|
||||
use thiserror::Error;
|
||||
|
||||
/// Remove acentos de uma string (ex: "União" → "Uniao")
|
||||
fn sem_acento(s: &str) -> String {
|
||||
s.replace("ã", "a")
|
||||
.replace("á", "a")
|
||||
.replace("à", "a")
|
||||
.replace("â", "a")
|
||||
.replace("é", "e")
|
||||
.replace("è", "e")
|
||||
.replace("ê", "e")
|
||||
.replace("í", "i")
|
||||
.replace("ì", "i")
|
||||
.replace("î", "i")
|
||||
.replace("õ", "o")
|
||||
.replace("ó", "o")
|
||||
.replace("ò", "o")
|
||||
.replace("ô", "o")
|
||||
.replace("ú", "u")
|
||||
.replace("ù", "u")
|
||||
.replace("û", "u")
|
||||
.replace("ç", "c")
|
||||
.replace("ñ", "n")
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
#[allow(dead_code)]
|
||||
pub enum SupabaseError {
|
||||
#[error("HTTP error: {0}")]
|
||||
Http(#[from] reqwest::Error),
|
||||
@@ -68,58 +92,75 @@ impl SupabaseClient {
|
||||
|
||||
/// Salva/atualiza um fechamento na tabela `fechamentos_web`.
|
||||
pub fn salvar_fechamento(&self, estado: &serde_json::Value) -> Result<Value, SupabaseError> {
|
||||
let uuid = estado
|
||||
.get("uuid")
|
||||
// Normaliza loja e turno para minúsculo (CHECK constraints do Postgres)
|
||||
let loja = estado
|
||||
.get("loja")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("");
|
||||
.map(|s| sem_acento(&s.to_lowercase()).replace(" ", "_"))
|
||||
.unwrap_or_else(|| "uniao".to_string());
|
||||
let data = estado.get("data").and_then(|v| v.as_str()).unwrap_or("");
|
||||
let operador = estado.get("operador").and_then(|v| v.as_str()).unwrap_or("");
|
||||
let id_fechamento = format!("{}_{}_{}", loja, data, operador)
|
||||
.to_lowercase()
|
||||
.replace(" ", "_");
|
||||
let turno = estado
|
||||
.get("turno")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| sem_acento(&s.to_lowercase()))
|
||||
.unwrap_or_else(|| "manha".to_string());
|
||||
|
||||
let payload = serde_json::json!({
|
||||
"uuid": uuid,
|
||||
"loja": estado["loja"],
|
||||
"data": estado["data"],
|
||||
"operador": estado["operador"],
|
||||
"turno": estado["turno"],
|
||||
"saldo_troco": estado["saldo_troco"],
|
||||
"saldo_esperado": estado["saldo_esperado"],
|
||||
"fechamento": estado["fechamento"],
|
||||
"id_fechamento": id_fechamento,
|
||||
"loja": loja,
|
||||
"data": data,
|
||||
"operador": operador,
|
||||
"turno": turno,
|
||||
"saldo_troco": estado.get("saldo_troco").and_then(|v| v.as_f64()).unwrap_or(0.0),
|
||||
"saldo_credito": estado.get("credito").and_then(|v| v.as_f64()).unwrap_or(0.0),
|
||||
"saldo_debito": estado.get("debito").and_then(|v| v.as_f64()).unwrap_or(0.0),
|
||||
"saldo_alimentacao": estado.get("alimentacao").and_then(|v| v.as_f64()).unwrap_or(0.0),
|
||||
"saldo_vales": estado.get("vales").and_then(|v| v.as_f64()).unwrap_or(0.0),
|
||||
"saldo_areceber": estado.get("areceber").and_then(|v| v.as_f64()).unwrap_or(0.0),
|
||||
"saldo_pixcnpj": estado.get("pixcnpj").and_then(|v| v.as_f64()).unwrap_or(0.0),
|
||||
"fechamento_dinheiro": estado.get("fd").and_then(|v| v.as_f64()).unwrap_or(0.0),
|
||||
"fechamento_cartoes": estado.get("fc").and_then(|v| v.as_f64()).unwrap_or(0.0),
|
||||
"fechamento_areceber": estado.get("fr").and_then(|v| v.as_f64()).unwrap_or(0.0),
|
||||
"total_sangrias": estado.get("total_sangrias").and_then(|v| v.as_f64()).unwrap_or(0.0),
|
||||
"total_despesas": estado.get("total_despesas").and_then(|v| v.as_f64()).unwrap_or(0.0),
|
||||
"total_cancelamentos": estado.get("total_cancelamentos").and_then(|v| v.as_f64()).unwrap_or(0.0),
|
||||
"total_vales": estado.get("total_vales").and_then(|v| v.as_f64()).unwrap_or(0.0),
|
||||
"total_pixcnpj": estado.get("total_pixcnpj").and_then(|v| v.as_f64()).unwrap_or(0.0),
|
||||
"total_areceber": estado.get("total_areceber").and_then(|v| v.as_f64()).unwrap_or(0.0),
|
||||
"total_cartao_credito": estado.get("total_cartao_credito").and_then(|v| v.as_f64()).unwrap_or(0.0),
|
||||
"total_cartao_debito": estado.get("total_cartao_debito").and_then(|v| v.as_f64()).unwrap_or(0.0),
|
||||
"total_cartao_alimentacao": estado.get("total_cartao_alimentacao").and_then(|v| v.as_f64()).unwrap_or(0.0),
|
||||
"total_cartao_pix": estado.get("total_cartao_pix").and_then(|v| v.as_f64()).unwrap_or(0.0),
|
||||
"saldo_esperado": estado.get("saldo_esperado").and_then(|v| v.as_f64()).unwrap_or(0.0),
|
||||
"dados": estado,
|
||||
"observacoes": "rascunho",
|
||||
"observacoes": estado.get("observacoes").and_then(|v| v.as_str()).unwrap_or(""),
|
||||
"clientes": estado.get("clientes").and_then(|v| v.as_i64()).unwrap_or(0),
|
||||
"frango": estado.get("frango").and_then(|v| v.as_i64()).unwrap_or(0),
|
||||
});
|
||||
|
||||
let _url = format!(
|
||||
"{}/rest/v1/fechamentos_web?uuid=eq.{}&select=id",
|
||||
self.base_url, uuid
|
||||
);
|
||||
|
||||
// Upsert via POST com Prefer: resolution=merge-duplicates
|
||||
// Primeiro tenta INSERT (upsert via Prefer)
|
||||
let resp = self
|
||||
.http
|
||||
.post(&format!("{}/rest/v1/rpc/upsert_fechamento", self.base_url))
|
||||
.post(&format!("{}/rest/v1/fechamentos_web", self.base_url))
|
||||
.headers(self.headers(true))
|
||||
.header("Prefer", "resolution=merge-duplicates")
|
||||
.json(&payload)
|
||||
.send()?;
|
||||
|
||||
if resp.status().is_success() {
|
||||
Ok(serde_json::json!({ "ok": true, "uuid": uuid }))
|
||||
let status = resp.status();
|
||||
if status.is_success() || status.as_u16() == 409 {
|
||||
Ok(serde_json::json!({ "ok": true, "id_fechamento": id_fechamento }))
|
||||
} else {
|
||||
let body = resp.text().unwrap_or_default();
|
||||
// Fallback: tenta POST direto na tabela
|
||||
let resp2 = self
|
||||
.http
|
||||
.post(&format!("{}/rest/v1/fechamentos_web", self.base_url))
|
||||
.headers(self.headers(true))
|
||||
.header("Prefer", "resolution=merge-duplicates")
|
||||
.json(&payload)
|
||||
.send()?;
|
||||
if resp2.status().is_success() {
|
||||
Ok(serde_json::json!({ "ok": true, "uuid": uuid }))
|
||||
} else {
|
||||
Err(SupabaseError::Api(format!(
|
||||
"HTTP {}: {}",
|
||||
resp2.status().as_u16(),
|
||||
body
|
||||
)))
|
||||
}
|
||||
Err(SupabaseError::Api(format!(
|
||||
"HTTP {}: {}",
|
||||
status.as_u16(),
|
||||
body
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,7 +193,59 @@ impl SupabaseClient {
|
||||
}
|
||||
}
|
||||
|
||||
/// Lista fechamentos recentes de uma loja.
|
||||
/// Busca um fechamento pelo UUID.
|
||||
pub fn buscar_por_uuid(&self, uuid: &str) -> Result<Option<Value>, SupabaseError> {
|
||||
let url = format!(
|
||||
"{}/rest/v1/fechamentos_web?uuid=eq.{}&select=*",
|
||||
self.base_url, uuid
|
||||
);
|
||||
|
||||
let resp = self
|
||||
.http
|
||||
.get(&url)
|
||||
.headers(self.headers(false))
|
||||
.send()?;
|
||||
|
||||
if resp.status().is_success() {
|
||||
let arr: Vec<Value> = resp.json()?;
|
||||
Ok(arr.into_iter().next())
|
||||
} else {
|
||||
let status = resp.status();
|
||||
let body = resp.text().unwrap_or_default();
|
||||
Err(SupabaseError::Api(format!(
|
||||
"HTTP {}: {}",
|
||||
status.as_u16(),
|
||||
body
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
/// Atualiza campos específicos de um fechamento pelo UUID (PATCH).
|
||||
pub fn atualizar_fechamento(&self, uuid: &str, updates: &serde_json::Value) -> Result<Value, SupabaseError> {
|
||||
let url = format!(
|
||||
"{}/rest/v1/fechamentos_web?uuid=eq.{}",
|
||||
self.base_url, uuid
|
||||
);
|
||||
|
||||
let resp = self
|
||||
.http
|
||||
.patch(&url)
|
||||
.headers(self.headers(true))
|
||||
.json(updates)
|
||||
.send()?;
|
||||
|
||||
if resp.status().is_success() {
|
||||
Ok(serde_json::json!({ "ok": true, "uuid": uuid }))
|
||||
} else {
|
||||
let status = resp.status();
|
||||
let body = resp.text().unwrap_or_default();
|
||||
Err(SupabaseError::Api(format!(
|
||||
"HTTP {}: {}",
|
||||
status.as_u16(),
|
||||
body
|
||||
)))
|
||||
}
|
||||
}
|
||||
pub fn listar_recentes(&self, loja: &str, limite: i64) -> Result<Vec<Value>, SupabaseError> {
|
||||
let url = format!(
|
||||
"{}/rest/v1/fechamentos_web?loja=eq.{}&order=data.desc,atualizado_em.desc&limit={}&select=id,uuid,data,operador,turno,saldo_troco,saldo_esperado,fechamento,sync_status,observacoes,atualizado_em",
|
||||
@@ -321,6 +414,23 @@ pub fn sb_buscar_por_id(
|
||||
sb.buscar_por_id(id)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn sb_buscar_por_uuid(
|
||||
sb: State<'_, SupabaseClient>,
|
||||
uuid: String,
|
||||
) -> Result<Option<serde_json::Value>, SupabaseError> {
|
||||
sb.buscar_por_uuid(&uuid)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn sb_atualizar_fechamento(
|
||||
sb: State<'_, SupabaseClient>,
|
||||
uuid: String,
|
||||
updates: serde_json::Value,
|
||||
) -> Result<serde_json::Value, SupabaseError> {
|
||||
sb.atualizar_fechamento(&uuid, &updates)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn sb_salvar_listas(
|
||||
sb: State<'_, SupabaseClient>,
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"app": {
|
||||
"windows": [
|
||||
{
|
||||
"title": "Fechamento de Caixa — O Frangão",
|
||||
"title": "Fechamento de Caixa \u2014 O Frang\u00e3o",
|
||||
"width": 1100,
|
||||
"height": 780,
|
||||
"minWidth": 900,
|
||||
@@ -22,13 +22,18 @@
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
"csp": null
|
||||
"csp": null,
|
||||
"capabilities": [
|
||||
"default"
|
||||
]
|
||||
},
|
||||
"withGlobalTauri": true
|
||||
},
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"targets": ["nsis"],
|
||||
"targets": [
|
||||
"nsis"
|
||||
],
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
"icons/128x128.png",
|
||||
@@ -37,7 +42,9 @@
|
||||
"icons/icon.ico"
|
||||
],
|
||||
"windows": {
|
||||
"webviewInstallMode": { "type": "embedBootstrapper" }
|
||||
"webviewInstallMode": {
|
||||
"type": "embedBootstrapper"
|
||||
}
|
||||
},
|
||||
"resources": {
|
||||
"../src/*": "./"
|
||||
|
||||
+951
-230
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user